mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-07 12:28:57 -05:00
* [AI] Cache the CLI's local budget between invocations
Every `actual <cmd>` call currently delta-syncs the budget with the
sync server via `api.downloadBudget`, which hits the server's
500-req/min rate limit on scripted workflows. Actual is local-first:
once the budget is on disk, most read commands do not need fresh
server data.
Introduce a CLI-only cache layer inside `withConnection` that
decides per invocation whether to skip, sync, or re-download:
- Cache state lives at `{dataDir}/.actual-cli/{syncId}/state.json`,
keyed by `syncId` to avoid the chicken-and-egg of not knowing the
on-disk `budgetId` before the first download. The on-disk id is
resolved via `api.getBudgets()` and persisted after first download.
- Read commands (list, balance, query run, …) skip the `/sync`
call while `now - lastSyncedAt < cacheTtl`. Write commands
(create, update, delete, set-*, etc.) sync before and after the
operation to keep server state consistent.
- Encrypted budgets force a sync per call since `api/load-budget`
does not re-verify the password.
- New `proper-lockfile`-backed shared/exclusive lock serializes
writes while allowing parallel reads. Reader markers live in
`{meta}/readers/`; writers sweep stale markers by PID.
New `actual sync` command with three modes: default (sync now),
`--status` (print cache age, TTL, stale flag), `--clear` (delete
cache, holding the exclusive lock to avoid racing writers).
New config surface, following the existing flag → env → config file
→ default precedence chain:
- `--cache-ttl <s>` / `ACTUAL_CACHE_TTL` / `cacheTtl` (default 60)
- `--refresh` / `--no-cache`
- `--lock-timeout <s>` / `ACTUAL_LOCK_TIMEOUT` / `lockTimeout` (10)
- `--no-lock` / `ACTUAL_NO_LOCK` / `noLock`
Every `withConnection` call site now passes an explicit
`{ mutates: boolean, skipBudget?: boolean }` so read/write intent is
visible at the edge.
The old `budgets sync` subcommand is removed — it silently diverged
from the new top-level `actual sync`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Simplify CLI cache/lock internals
Use a discriminated SyncDecision union so connection.ts no longer needs
non-null assertions on the cached state. Thread the resolved CliConfig
through withConnection's callback to drop duplicate resolveConfig calls
in the sync and budgets commands. Extract an errorCode helper and
replace the existsSync+readdirSync TOCTOU pattern in the reader-wait
polling loop with a single readdir that tolerates ENOENT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Address PR #7539 review comments
- cache.ts: use a unique per-writer tmp filename so concurrent shared-
lock writers (encrypted budgets, --refresh, stale TTL) don't clobber
each other's publish and silently drop state updates.
- index.ts/config.ts: fix --no-cache and --no-lock flags. Commander
stores --no-foo under the positive key (cache/lock) and an explicit
false default makes the flag a no-op; the previous code also read
the wrong keys (noCache/noLock) so the flags had no effect at all.
Derive refresh/noLock from the correct keys.
- budgets.ts: invert encryption password precedence so the subcommand
flag (--encryption-password) wins over env/config-file values.
- sync.ts: report stale=true in --status when lastSyncedAt is in the
future, matching decideSyncAction's clock-skew handling.
- connection.ts: drop unnecessary `as` cast on api.getBudgets() now
that the return type is Promise<APIFileEntity[]>.
- utils.ts: parseBoolEnv throws on unrecognized values instead of
silently returning undefined so typos like ACTUAL_NO_LOCK=yes fail
loudly.
- Shorten 7539 release note to a single user-facing sentence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
200 lines
8.5 KiB
Markdown
200 lines
8.5 KiB
Markdown
# @actual-app/cli
|
|
|
|
> **WARNING:** This CLI is experimental.
|
|
|
|
Command-line interface for [Actual Budget](https://actualbudget.org). Query and modify your budget data from the terminal — accounts, transactions, categories, payees, rules, schedules, and more.
|
|
|
|
> **Note:** This CLI connects to a running [Actual sync server](https://actualbudget.org/docs/install/). It does not operate on local budget files directly.
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install -g @actual-app/cli
|
|
```
|
|
|
|
Requires Node.js >= 22.
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# Set connection details
|
|
export ACTUAL_SERVER_URL=http://localhost:5006
|
|
export ACTUAL_PASSWORD=your-password
|
|
export ACTUAL_SYNC_ID=your-sync-id # Found in Settings → Advanced → Sync ID
|
|
|
|
# List your accounts
|
|
actual accounts list
|
|
|
|
# Check a balance
|
|
actual accounts balance <account-id>
|
|
|
|
# View this month's budget
|
|
actual budgets month 2026-03
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Configuration is resolved in this order (highest priority first):
|
|
|
|
1. **CLI flags** (`--server-url`, `--password`, etc.)
|
|
2. **Environment variables**
|
|
3. **Config file** (via [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig))
|
|
4. **Defaults** (`dataDir` defaults to `~/.actual-cli/data`)
|
|
|
|
### Environment Variables
|
|
|
|
| Variable | Description |
|
|
| ---------------------- | ----------------------------------------------------- |
|
|
| `ACTUAL_SERVER_URL` | URL of the Actual sync server (required) |
|
|
| `ACTUAL_PASSWORD` | Server password (required unless using token) |
|
|
| `ACTUAL_SESSION_TOKEN` | Session token (alternative to password) |
|
|
| `ACTUAL_SYNC_ID` | Budget Sync ID (required for most commands) |
|
|
| `ACTUAL_DATA_DIR` | Local directory for cached budget data |
|
|
| `ACTUAL_CACHE_TTL` | Cache TTL in seconds (default: 60) |
|
|
| `ACTUAL_LOCK_TIMEOUT` | Budget-dir lock wait timeout in seconds (default: 10) |
|
|
| `ACTUAL_NO_LOCK` | Set to `1` to disable budget-dir locking |
|
|
|
|
### Config File
|
|
|
|
Create an `.actualrc.json` (or `.actualrc`, `.actualrc.yaml`, `actual.config.js`):
|
|
|
|
```json
|
|
{
|
|
"serverUrl": "http://localhost:5006",
|
|
"password": "your-password",
|
|
"syncId": "1cfdbb80-6274-49bf-b0c2-737235a4c81f",
|
|
"cacheTtl": 60,
|
|
"lockTimeout": 10,
|
|
"noLock": false
|
|
}
|
|
```
|
|
|
|
**Security:** Do not store plaintext passwords in config files (e.g. `.actualrc.json`, `.actualrc`, `.actualrc.yaml`, `actual.config.js`). Add these files to `.gitignore` if they contain secrets. Prefer the `ACTUAL_SESSION_TOKEN` environment variable instead of the `password` field. See [Environment Variables](#environment-variables) for using a session token.
|
|
|
|
### Global Flags
|
|
|
|
| Flag | Description |
|
|
| ------------------------- | ----------------------------------------------- |
|
|
| `--server-url <url>` | Server URL |
|
|
| `--password <pw>` | Server password |
|
|
| `--session-token <token>` | Session token |
|
|
| `--sync-id <id>` | Budget Sync ID |
|
|
| `--data-dir <path>` | Data directory |
|
|
| `--cache-ttl <seconds>` | Cache TTL; `0` disables caching (default: 60) |
|
|
| `--refresh` | Force a sync on this call, ignoring the cache |
|
|
| `--no-cache` | Alias for `--refresh` |
|
|
| `--lock-timeout <secs>` | Lock wait timeout (default: 10) |
|
|
| `--no-lock` | Disable budget-dir locking (use with care) |
|
|
| `--format <format>` | Output format: `json` (default), `table`, `csv` |
|
|
| `--verbose` | Show informational messages |
|
|
|
|
## Commands
|
|
|
|
| Command | Description |
|
|
| ----------------- | ------------------------------ |
|
|
| `accounts` | Manage accounts |
|
|
| `budgets` | Manage budgets and allocations |
|
|
| `categories` | Manage categories |
|
|
| `category-groups` | Manage category groups |
|
|
| `transactions` | Manage transactions |
|
|
| `payees` | Manage payees |
|
|
| `tags` | Manage tags |
|
|
| `rules` | Manage transaction rules |
|
|
| `schedules` | Manage scheduled transactions |
|
|
| `query` | Run an ActualQL query |
|
|
| `server` | Server utilities and lookups |
|
|
| `sync` | Refresh or inspect local cache |
|
|
|
|
Run `actual <command> --help` for subcommands and options.
|
|
|
|
### Examples
|
|
|
|
```bash
|
|
# List all accounts (as a table; excludes closed by default)
|
|
actual accounts list [--include-closed] --format table
|
|
|
|
# Find an entity ID by name
|
|
actual server get-id --type accounts --name "Checking"
|
|
|
|
# Add a transaction (amount in integer cents: -2500 = -$25.00)
|
|
actual transactions add --account <id> \
|
|
--data '[{"date":"2026-03-14","amount":-2500,"payee_name":"Coffee Shop"}]'
|
|
|
|
# Export transactions to CSV
|
|
actual transactions list --account <id> \
|
|
--start 2026-01-01 --end 2026-12-31 --format csv > transactions.csv
|
|
|
|
# Set budget amount ($500 = 50000 cents)
|
|
actual budgets set-amount --month 2026-03 --category <id> --amount 50000
|
|
|
|
# Run an ActualQL query
|
|
actual query run --table transactions \
|
|
--select "date,amount,payee" --filter '{"amount":{"$lt":0}}' --limit 10
|
|
```
|
|
|
|
### Amount Convention
|
|
|
|
All monetary amounts are **integer cents** when passed as input (flags, JSON):
|
|
|
|
| CLI Value | Dollar Amount |
|
|
| --------- | ------------- |
|
|
| `5000` | $50.00 |
|
|
| `-12350` | -$123.50 |
|
|
|
|
**Output formatting:** Table (`--format table`) and CSV (`--format csv`) output automatically converts cent values to decimal (e.g. `1665.00` instead of `166500`). JSON output always returns raw cents for programmatic use.
|
|
|
|
### Tips & Common Pitfalls
|
|
|
|
- **Split transactions:** When summing or counting transactions, filter `"is_parent": false` to avoid double-counting. A split parent holds the total amount, and its children hold the individual parts — including both would count the total twice.
|
|
|
|
- **Rapid sequential requests:** The CLI caches the budget locally (see [Caching](#caching)), so read-heavy scripts no longer need a single-query workaround by default. For very chatty scripts, run `actual sync` once and then use a long `--cache-ttl` for reads:
|
|
|
|
```bash
|
|
actual sync
|
|
actual --cache-ttl 3600 query run ...
|
|
actual --cache-ttl 3600 accounts list
|
|
```
|
|
|
|
- **Uncategorized transactions:** `category.name` is `null` for transactions without a category. Account for this when filtering or grouping by category.
|
|
|
|
- **No date sub-fields in AQL:** `date.month`, `date.year`, etc. are not supported as query fields. To group by month, fetch raw transactions with a date range filter and aggregate locally in a script.
|
|
|
|
## Caching
|
|
|
|
The CLI keeps a local copy of your budget so repeated commands don't hit the sync server on every call. Within the TTL (default `60` seconds), read commands (`list`, `balance`, `query run`, …) reuse the cached budget without a network round-trip. Write commands (`add`, `update`, `set-amount`, …) always sync with the server before and after the write.
|
|
|
|
- `actual sync` — refresh the cache now.
|
|
- `actual sync --status` — show how stale the local cache is.
|
|
- `actual sync --clear` — delete the local cache; the next command re-downloads.
|
|
- `--refresh` (or `--no-cache`) — force a sync on a single call.
|
|
- `--cache-ttl <seconds>` — override the TTL for a single call (use `0` to disable caching).
|
|
|
|
### Concurrency
|
|
|
|
The CLI takes a shared lock for reads and an exclusive lock for writes on the per-budget cache directory. Many parallel reads are safe; writes serialize. If another CLI process is holding the lock, subsequent invocations wait up to `--lock-timeout` seconds (default `10`) before failing with an error. Pass `--no-lock` to opt out in trusted single-process setups.
|
|
|
|
## Running Locally (Development)
|
|
|
|
If you're working on the CLI within the monorepo:
|
|
|
|
```bash
|
|
# 1. Build the CLI
|
|
yarn build:cli
|
|
|
|
# 2. Start a local sync server (in a separate terminal)
|
|
yarn start:server-dev
|
|
|
|
# 3. Open http://localhost:5006 in your browser, create a budget,
|
|
# then find the Sync ID in Settings → Advanced → Sync ID
|
|
|
|
# 4. Run the CLI directly from the build output
|
|
ACTUAL_SERVER_URL=http://localhost:5006 \
|
|
ACTUAL_PASSWORD=your-password \
|
|
ACTUAL_SYNC_ID=your-sync-id \
|
|
node packages/cli/dist/cli.js accounts list
|
|
|
|
# Or use a shorthand alias for convenience
|
|
alias actual-dev="node $(pwd)/packages/cli/dist/cli.js"
|
|
actual-dev budgets list
|
|
```
|