Compare commits

...

5 Commits

Author SHA1 Message Date
Claude
62a68bd323 [AI] Add CLI tool documentation to docs site
https://claude.ai/code/session_01N9h7FzFPft3p4n7Rhy9gux
2026-03-14 10:55:41 +00:00
Claude
7f6348cd83 [AI] Add .gitignore for CLI package build artifacts
https://claude.ai/code/session_01N9h7FzFPft3p4n7Rhy9gux
2026-03-14 10:37:19 +00:00
Claude
8899b13548 [AI] Change CLI outDir from build to dist
https://claude.ai/code/session_01N9h7FzFPft3p4n7Rhy9gux
2026-03-14 10:36:53 +00:00
Claude
8de530daec [AI] Enable strict TypeScript mode and remove typescript-strict-plugin from CLI
Use native `"strict": true` in tsconfig instead of the typescript-strict-plugin.
Removed the plugin from devDependencies and simplified the typecheck script.

https://claude.ai/code/session_01N9h7FzFPft3p4n7Rhy9gux
2026-03-14 10:31:34 +00:00
Claude
d8f2d822b0 [AI] Add @actual-app/cli package and /actual Claude Code skill
New CLI tool wrapping the full @actual-app/api surface for interacting with
Actual Budget from the command line. Connects to a sync server and supports
all CRUD operations across accounts, budgets, categories, transactions,
payees, tags, rules, schedules, and AQL queries.

Also adds a Claude Code skill at .claude/skills/actual/ that enables
natural-language budget interactions via the /actual slash command.

https://claude.ai/code/session_01N9h7FzFPft3p4n7Rhy9gux
2026-03-13 23:54:31 +00:00
25 changed files with 1904 additions and 4 deletions

View File

@@ -0,0 +1,202 @@
---
name: actual
description: Interact with Actual Budget - query and modify your budget data via the CLI tool
allowed-tools: Bash(node packages/cli/dist/src/index.js *)
disable-model-invocation: true
argument-hint: <natural language request about your budget>
---
# Actual Budget CLI Skill
You are an assistant that helps users interact with their Actual Budget data using the `@actual-app/cli` tool.
## Prerequisites
The following environment variables must be set:
- `ACTUAL_SERVER_URL` - URL of the Actual sync server (required)
- `ACTUAL_BUDGET_ID` - Budget ID to load (required for most operations)
- `ACTUAL_PASSWORD` or `ACTUAL_SESSION_TOKEN` - Authentication (one required)
If these are not set, ask the user to configure them before proceeding.
## CLI Command Reference
Run commands using: `node packages/cli/dist/src/index.js <command> [options]`
Add `--quiet` to suppress informational messages when piping output.
### Global Options
| Option | Description |
| ------------------------- | ----------------------------------------------- |
| `--server-url <url>` | Server URL (overrides env) |
| `--password <pw>` | Password (overrides env) |
| `--session-token <token>` | Session token (overrides env) |
| `--budget-id <id>` | Budget ID (overrides env) |
| `--data-dir <path>` | Data directory |
| `--format <format>` | Output format: `json` (default), `table`, `csv` |
| `--quiet` | Suppress stderr info messages |
### Accounts
```bash
actual accounts list
actual accounts create --name "Checking" [--offbudget] [--balance 50000]
actual accounts update <id> [--name "New Name"] [--offbudget true]
actual accounts close <id> [--transfer-account <id>] [--transfer-category <id>]
actual accounts reopen <id>
actual accounts delete <id>
actual accounts balance <id> [--cutoff 2026-01-31]
```
### Budgets
```bash
actual budgets list
actual budgets download <syncId> [--encryption-password <pw>]
actual budgets sync
actual budgets months
actual budgets month 2026-03
actual budgets set-amount --month 2026-03 --category <id> --amount 50000
actual budgets set-carryover --month 2026-03 --category <id> --flag true
actual budgets hold-next-month --month 2026-03 --amount 10000
actual budgets reset-hold --month 2026-03
```
### Categories
```bash
actual categories list
actual categories create --name "Groceries" --group-id <id> [--is-income]
actual categories update <id> [--name "Food"] [--hidden true]
actual categories delete <id> [--transfer-to <id>]
```
### Category Groups
```bash
actual category-groups list
actual category-groups create --name "Essentials" [--is-income]
actual category-groups update <id> [--name "New Name"] [--hidden true]
actual category-groups delete <id> [--transfer-to <id>]
```
### Transactions
```bash
actual transactions list --account <id> --start 2026-01-01 --end 2026-03-31
actual transactions add --account <id> --data '[{"date":"2026-03-13","amount":-5000,"payee_name":"Store"}]'
actual transactions add --account <id> --file transactions.json
actual transactions import --account <id> --data '[...]' [--dry-run]
actual transactions update <id> --data '{"notes":"Updated note"}'
actual transactions delete <id>
```
### Payees
```bash
actual payees list
actual payees common
actual payees create --name "Grocery Store"
actual payees update <id> --name "New Name"
actual payees delete <id>
actual payees merge --target <id> --ids id1,id2,id3
```
### Tags
```bash
actual tags list
actual tags create --tag "vacation" [--color "#ff0000"] [--description "Vacation expenses"]
actual tags update <id> [--tag "trip"] [--color "#00ff00"]
actual tags delete <id>
```
### Rules
```bash
actual rules list
actual rules payee-rules <payeeId>
actual rules create --data '{"stage":"pre","conditionsOp":"and","conditions":[...],"actions":[...]}'
actual rules create --file rule.json
actual rules update --data '{"id":"...","stage":"pre",...}'
actual rules delete <id>
```
### Schedules
```bash
actual schedules list
actual schedules create --data '{"name":"Rent","date":"1st","amount":-150000,"amountOp":"is","account":"...","payee":"..."}'
actual schedules update <id> --data '{"name":"Updated Rent"}' [--reset-next-date]
actual schedules delete <id>
```
### Query (AQL)
```bash
actual query run --table transactions --select "date,amount,payee" --filter '{"amount":{"$lt":0}}' --limit 10
actual query run --file query.json
```
### Server
```bash
actual server version
actual server get-id --type accounts --name "Checking"
actual server get-id --type categories --name "Groceries"
actual server bank-sync [--account <id>]
```
## Important Notes
### Amount Convention
All monetary amounts are in **integer cents**. When displaying to users:
- `5000` = $50.00
- `-12350` = -$123.50
- Always convert cents to dollars for user-facing display (divide by 100)
- When the user says "$50", convert to `5000` for the CLI
### Workflow Tips
1. **Find entity IDs by name**: Use `server get-id --type <type> --name "<name>"` before operations that need IDs
2. **Explore first**: Start with `list` commands to understand what data exists
3. **Chain operations**: Use `--quiet --format json` when piping between commands
4. **Complex objects**: For rules/schedules, create a JSON file and use `--file`
### Common Workflows
**"Show my budget for this month":**
```bash
actual budgets month 2026-03 --quiet
```
**"How much have I spent on groceries?":**
```bash
# First get the category ID
actual server get-id --type categories --name "Groceries" --quiet
# Then query transactions
actual transactions list --account <id> --start 2026-03-01 --end 2026-03-31 --quiet
```
**"What's my checking account balance?":**
```bash
actual server get-id --type accounts --name "Checking" --quiet
actual accounts balance <id> --quiet
```
### Error Handling
- Non-zero exit codes indicate errors
- Error JSON is written to stdout: `{"error": "message"}`
- Always check the output before presenting results to the user
## User Request
$ARGUMENTS

View File

@@ -40,6 +40,7 @@
"build:desktop": "./bin/package-electron",
"build:plugins-service": "yarn workspace plugins-service build",
"build:api": "yarn workspace @actual-app/api build",
"build:cli": "yarn workspace @actual-app/cli build",
"build:docs": "yarn workspace docs build",
"build:storybook": "yarn workspace @actual-app/components build:storybook",
"deploy:docs": "yarn workspace docs deploy",

2
packages/cli/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
dist
coverage

33
packages/cli/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "@actual-app/cli",
"version": "26.3.0",
"description": "CLI for Actual Budget",
"license": "MIT",
"bin": {
"actual": "./dist/src/index.js",
"actual-cli": "./dist/src/index.js"
},
"files": [
"dist"
],
"type": "module",
"scripts": {
"build": "tsc",
"test": "vitest --run",
"typecheck": "tsc -b"
},
"dependencies": {
"@actual-app/api": "workspace:^",
"cli-table3": "^0.6.5",
"commander": "^13.0.0",
"cosmiconfig": "^9.0.0"
},
"devDependencies": {
"@types/node": "^22.19.10",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"engines": {
"node": ">=22"
}
}

View File

@@ -0,0 +1,114 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
export function registerAccountsCommand(program: Command) {
const accounts = program.command('accounts').description('Manage accounts');
accounts
.command('list')
.description('List all accounts')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getAccounts();
printOutput(result, opts.format as OutputFormat);
});
});
accounts
.command('create')
.description('Create a new account')
.requiredOption('--name <name>', 'Account name')
.option('--offbudget', 'Create as off-budget account', false)
.option('--balance <amount>', 'Initial balance in cents', '0')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createAccount(
{ name: cmdOpts.name, offbudget: cmdOpts.offbudget },
parseInt(cmdOpts.balance, 10),
);
printOutput({ id }, opts.format as OutputFormat);
});
});
accounts
.command('update <id>')
.description('Update an account')
.option('--name <name>', 'New account name')
.option('--offbudget <bool>', 'Set off-budget status')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const fields: Record<string, unknown> = {};
if (cmdOpts.name) fields.name = cmdOpts.name;
if (cmdOpts.offbudget !== undefined) {
fields.offbudget = cmdOpts.offbudget === 'true';
}
await api.updateAccount(id, fields);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
accounts
.command('close <id>')
.description('Close an account')
.option(
'--transfer-account <id>',
'Transfer remaining balance to this account',
)
.option(
'--transfer-category <id>',
'Transfer remaining balance to this category',
)
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.closeAccount(
id,
cmdOpts.transferAccount,
cmdOpts.transferCategory,
);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
accounts
.command('reopen <id>')
.description('Reopen a closed account')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.reopenAccount(id);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
accounts
.command('delete <id>')
.description('Delete an account')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteAccount(id);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
accounts
.command('balance <id>')
.description('Get account balance')
.option('--cutoff <date>', 'Cutoff date (YYYY-MM-DD)')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const cutoff = cmdOpts.cutoff ? new Date(cmdOpts.cutoff) : undefined;
const balance = await api.getAccountBalance(id, cutoff);
printOutput({ id, balance }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,132 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
export function registerBudgetsCommand(program: Command) {
const budgets = program.command('budgets').description('Manage budgets');
budgets
.command('list')
.description('List all available budgets')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getBudgets();
printOutput(result, opts.format as OutputFormat);
});
});
budgets
.command('download <syncId>')
.description('Download a budget by sync ID')
.option('--encryption-password <password>', 'Encryption password')
.action(async (syncId: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.downloadBudget(syncId, {
password: cmdOpts.encryptionPassword,
});
printOutput({ success: true, syncId }, opts.format as OutputFormat);
});
});
budgets
.command('sync')
.description('Sync the current budget')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.sync();
printOutput({ success: true }, opts.format as OutputFormat);
});
});
budgets
.command('months')
.description('List available budget months')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getBudgetMonths();
printOutput(result, opts.format as OutputFormat);
});
});
budgets
.command('month <month>')
.description('Get budget data for a specific month (YYYY-MM)')
.action(async (month: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getBudgetMonth(month);
printOutput(result, opts.format as OutputFormat);
});
});
budgets
.command('set-amount')
.description('Set budget amount for a category in a month')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.requiredOption('--category <id>', 'Category ID')
.requiredOption('--amount <amount>', 'Amount in cents')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.setBudgetAmount(
cmdOpts.month,
cmdOpts.category,
parseInt(cmdOpts.amount, 10),
);
printOutput({ success: true }, opts.format as OutputFormat);
});
});
budgets
.command('set-carryover')
.description('Enable/disable carryover for a category')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.requiredOption('--category <id>', 'Category ID')
.requiredOption('--flag <bool>', 'Enable (true) or disable (false)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.setBudgetCarryover(
cmdOpts.month,
cmdOpts.category,
cmdOpts.flag === 'true',
);
printOutput({ success: true }, opts.format as OutputFormat);
});
});
budgets
.command('hold-next-month')
.description('Hold budget amount for next month')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.requiredOption('--amount <amount>', 'Amount in cents')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.holdBudgetForNextMonth(
cmdOpts.month,
parseInt(cmdOpts.amount, 10),
);
printOutput({ success: true }, opts.format as OutputFormat);
});
});
budgets
.command('reset-hold')
.description('Reset budget hold for a month')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.resetBudgetHold(cmdOpts.month);
printOutput({ success: true }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,72 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
export function registerCategoriesCommand(program: Command) {
const categories = program
.command('categories')
.description('Manage categories');
categories
.command('list')
.description('List all categories')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getCategories();
printOutput(result, opts.format as OutputFormat);
});
});
categories
.command('create')
.description('Create a new category')
.requiredOption('--name <name>', 'Category name')
.requiredOption('--group-id <id>', 'Category group ID')
.option('--is-income', 'Mark as income category', false)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createCategory({
name: cmdOpts.name,
group_id: cmdOpts.groupId,
is_income: cmdOpts.isIncome,
hidden: false,
});
printOutput({ id }, opts.format as OutputFormat);
});
});
categories
.command('update <id>')
.description('Update a category')
.option('--name <name>', 'New category name')
.option('--hidden <bool>', 'Set hidden status')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const fields: Record<string, unknown> = {};
if (cmdOpts.name) fields.name = cmdOpts.name;
if (cmdOpts.hidden !== undefined) {
fields.hidden = cmdOpts.hidden === 'true';
}
await api.updateCategory(id, fields);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
categories
.command('delete <id>')
.description('Delete a category')
.option('--transfer-to <id>', 'Transfer transactions to this category')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteCategory(id, cmdOpts.transferTo);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,70 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
export function registerCategoryGroupsCommand(program: Command) {
const groups = program
.command('category-groups')
.description('Manage category groups');
groups
.command('list')
.description('List all category groups')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getCategoryGroups();
printOutput(result, opts.format as OutputFormat);
});
});
groups
.command('create')
.description('Create a new category group')
.requiredOption('--name <name>', 'Group name')
.option('--is-income', 'Mark as income group', false)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createCategoryGroup({
name: cmdOpts.name,
is_income: cmdOpts.isIncome,
hidden: false,
});
printOutput({ id }, opts.format as OutputFormat);
});
});
groups
.command('update <id>')
.description('Update a category group')
.option('--name <name>', 'New group name')
.option('--hidden <bool>', 'Set hidden status')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const fields: Record<string, unknown> = {};
if (cmdOpts.name) fields.name = cmdOpts.name;
if (cmdOpts.hidden !== undefined) {
fields.hidden = cmdOpts.hidden === 'true';
}
await api.updateCategoryGroup(id, fields);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
groups
.command('delete <id>')
.description('Delete a category group')
.option('--transfer-to <id>', 'Transfer categories to this category')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteCategoryGroup(id, cmdOpts.transferTo);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,83 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
export function registerPayeesCommand(program: Command) {
const payees = program.command('payees').description('Manage payees');
payees
.command('list')
.description('List all payees')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getPayees();
printOutput(result, opts.format as OutputFormat);
});
});
payees
.command('common')
.description('List frequently used payees')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getCommonPayees();
printOutput(result, opts.format as OutputFormat);
});
});
payees
.command('create')
.description('Create a new payee')
.requiredOption('--name <name>', 'Payee name')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createPayee({ name: cmdOpts.name });
printOutput({ id }, opts.format as OutputFormat);
});
});
payees
.command('update <id>')
.description('Update a payee')
.option('--name <name>', 'New payee name')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const fields: Record<string, unknown> = {};
if (cmdOpts.name) fields.name = cmdOpts.name;
await api.updatePayee(id, fields);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
payees
.command('delete <id>')
.description('Delete a payee')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deletePayee(id);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
payees
.command('merge')
.description('Merge payees into a target payee')
.requiredOption('--target <id>', 'Target payee ID')
.requiredOption('--ids <ids>', 'Comma-separated payee IDs to merge')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const mergeIds = (cmdOpts.ids as string).split(',');
await api.mergePayees(cmdOpts.target, mergeIds);
printOutput({ success: true }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,76 @@
import { readFileSync } from 'fs';
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
export function registerQueryCommand(program: Command) {
const query = program
.command('query')
.description('Run AQL (Actual Query Language) queries');
query
.command('run')
.description('Execute an AQL query')
.option(
'--table <table>',
'Table to query (transactions, accounts, categories, payees)',
)
.option('--select <fields>', 'Comma-separated fields to select')
.option('--filter <json>', 'Filter expression as JSON')
.option('--order-by <fields>', 'Comma-separated fields to order by')
.option('--limit <n>', 'Limit number of results')
.option(
'--file <path>',
'Read full query object from JSON file (use - for stdin)',
)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
let queryObj;
if (cmdOpts.file) {
const content =
cmdOpts.file === '-'
? readFileSync(0, 'utf-8')
: readFileSync(cmdOpts.file, 'utf-8');
// If file contains a pre-built query, use it directly
const parsed = JSON.parse(content);
queryObj = api.q(parsed.table || cmdOpts.table || 'transactions');
if (parsed.select) queryObj = queryObj.select(parsed.select);
if (parsed.filter) queryObj = queryObj.filter(parsed.filter);
if (parsed.orderBy) queryObj = queryObj.orderBy(parsed.orderBy);
if (parsed.limit) queryObj = queryObj.limit(parsed.limit);
} else {
if (!cmdOpts.table) {
throw new Error('--table is required (or use --file)');
}
queryObj = api.q(cmdOpts.table);
if (cmdOpts.select) {
const fields = (cmdOpts.select as string).split(',');
queryObj = queryObj.select(fields);
}
if (cmdOpts.filter) {
queryObj = queryObj.filter(JSON.parse(cmdOpts.filter));
}
if (cmdOpts.orderBy) {
const fields = (cmdOpts.orderBy as string).split(',');
queryObj = queryObj.orderBy(fields);
}
if (cmdOpts.limit) {
queryObj = queryObj.limit(parseInt(cmdOpts.limit, 10));
}
}
const result = await api.aqlQuery(queryObj);
printOutput(result, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,93 @@
import { readFileSync } from 'fs';
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
function readJsonInput(cmdOpts: { data?: string; file?: string }): unknown {
if (cmdOpts.data) {
return JSON.parse(cmdOpts.data);
}
if (cmdOpts.file) {
const content =
cmdOpts.file === '-'
? readFileSync(0, 'utf-8')
: readFileSync(cmdOpts.file, 'utf-8');
return JSON.parse(content);
}
throw new Error('Either --data or --file is required');
}
export function registerRulesCommand(program: Command) {
const rules = program
.command('rules')
.description('Manage transaction rules');
rules
.command('list')
.description('List all rules')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getRules();
printOutput(result, opts.format as OutputFormat);
});
});
rules
.command('payee-rules <payeeId>')
.description('List rules for a specific payee')
.action(async (payeeId: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getPayeeRules(payeeId);
printOutput(result, opts.format as OutputFormat);
});
});
rules
.command('create')
.description('Create a new rule')
.option('--data <json>', 'Rule definition as JSON')
.option('--file <path>', 'Read rule from JSON file (use - for stdin)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const rule = readJsonInput(cmdOpts) as Parameters<
typeof api.createRule
>[0];
const id = await api.createRule(rule);
printOutput({ id }, opts.format as OutputFormat);
});
});
rules
.command('update')
.description('Update a rule')
.option('--data <json>', 'Rule data as JSON (must include id)')
.option('--file <path>', 'Read rule from JSON file (use - for stdin)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const rule = readJsonInput(cmdOpts) as Parameters<
typeof api.updateRule
>[0];
await api.updateRule(rule);
printOutput({ success: true }, opts.format as OutputFormat);
});
});
rules
.command('delete <id>')
.description('Delete a rule')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteRule(id);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,83 @@
import { readFileSync } from 'fs';
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
function readJsonInput(cmdOpts: { data?: string; file?: string }): unknown {
if (cmdOpts.data) {
return JSON.parse(cmdOpts.data);
}
if (cmdOpts.file) {
const content =
cmdOpts.file === '-'
? readFileSync(0, 'utf-8')
: readFileSync(cmdOpts.file, 'utf-8');
return JSON.parse(content);
}
throw new Error('Either --data or --file is required');
}
export function registerSchedulesCommand(program: Command) {
const schedules = program
.command('schedules')
.description('Manage scheduled transactions');
schedules
.command('list')
.description('List all schedules')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getSchedules();
printOutput(result, opts.format as OutputFormat);
});
});
schedules
.command('create')
.description('Create a new schedule')
.option('--data <json>', 'Schedule definition as JSON')
.option('--file <path>', 'Read schedule from JSON file (use - for stdin)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const schedule = readJsonInput(cmdOpts) as Parameters<
typeof api.createSchedule
>[0];
const id = await api.createSchedule(schedule);
printOutput({ id }, opts.format as OutputFormat);
});
});
schedules
.command('update <id>')
.description('Update a schedule')
.option('--data <json>', 'Fields to update as JSON')
.option('--file <path>', 'Read fields from JSON file (use - for stdin)')
.option('--reset-next-date', 'Reset next occurrence date', false)
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const fields = readJsonInput(cmdOpts) as Parameters<
typeof api.updateSchedule
>[1];
await api.updateSchedule(id, fields, cmdOpts.resetNextDate);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
schedules
.command('delete <id>')
.description('Delete a schedule')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteSchedule(id);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,55 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
export function registerServerCommand(program: Command) {
const server = program.command('server').description('Server utilities');
server
.command('version')
.description('Get server version')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const version = await api.getServerVersion();
printOutput({ version }, opts.format as OutputFormat);
});
});
server
.command('get-id')
.description('Get entity ID by name')
.requiredOption(
'--type <type>',
'Entity type (accounts, categories, payees, schedules)',
)
.requiredOption('--name <name>', 'Entity name')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.getIDByName(cmdOpts.type, cmdOpts.name);
printOutput(
{ id, type: cmdOpts.type, name: cmdOpts.name },
opts.format as OutputFormat,
);
});
});
server
.command('bank-sync')
.description('Run bank synchronization')
.option('--account <id>', 'Specific account ID to sync')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const args = cmdOpts.account
? { accountId: cmdOpts.account }
: undefined;
await api.runBankSync(args);
printOutput({ success: true }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,68 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
export function registerTagsCommand(program: Command) {
const tags = program.command('tags').description('Manage tags');
tags
.command('list')
.description('List all tags')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getTags();
printOutput(result, opts.format as OutputFormat);
});
});
tags
.command('create')
.description('Create a new tag')
.requiredOption('--tag <tag>', 'Tag name')
.option('--color <color>', 'Tag color')
.option('--description <description>', 'Tag description')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createTag({
tag: cmdOpts.tag,
color: cmdOpts.color,
description: cmdOpts.description,
});
printOutput({ id }, opts.format as OutputFormat);
});
});
tags
.command('update <id>')
.description('Update a tag')
.option('--tag <tag>', 'New tag name')
.option('--color <color>', 'New tag color')
.option('--description <description>', 'New tag description')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const fields: Record<string, unknown> = {};
if (cmdOpts.tag) fields.tag = cmdOpts.tag;
if (cmdOpts.color) fields.color = cmdOpts.color;
if (cmdOpts.description) fields.description = cmdOpts.description;
await api.updateTag(id, fields);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
tags
.command('delete <id>')
.description('Delete a tag')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteTag(id);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,130 @@
import { readFileSync } from 'fs';
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection.js';
import { printOutput } from '../output.js';
import type { OutputFormat } from '../output.js';
function readJsonInput(cmdOpts: { data?: string; file?: string }): unknown {
if (cmdOpts.data) {
return JSON.parse(cmdOpts.data);
}
if (cmdOpts.file) {
const content =
cmdOpts.file === '-'
? readFileSync(0, 'utf-8')
: readFileSync(cmdOpts.file, 'utf-8');
return JSON.parse(content);
}
throw new Error('Either --data or --file is required');
}
export function registerTransactionsCommand(program: Command) {
const transactions = program
.command('transactions')
.description('Manage transactions');
transactions
.command('list')
.description('List transactions for an account')
.requiredOption('--account <id>', 'Account ID')
.requiredOption('--start <date>', 'Start date (YYYY-MM-DD)')
.requiredOption('--end <date>', 'End date (YYYY-MM-DD)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getTransactions(
cmdOpts.account,
cmdOpts.start,
cmdOpts.end,
);
printOutput(result, opts.format as OutputFormat);
});
});
transactions
.command('add')
.description('Add transactions to an account')
.requiredOption('--account <id>', 'Account ID')
.option('--data <json>', 'Transaction data as JSON array')
.option(
'--file <path>',
'Read transaction data from JSON file (use - for stdin)',
)
.option('--learn-categories', 'Learn category assignments', false)
.option('--run-transfers', 'Process transfers', false)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const transactions = readJsonInput(cmdOpts) as Parameters<
typeof api.addTransactions
>[1];
const result = await api.addTransactions(
cmdOpts.account,
transactions,
{
learnCategories: cmdOpts.learnCategories,
runTransfers: cmdOpts.runTransfers,
},
);
printOutput(result, opts.format as OutputFormat);
});
});
transactions
.command('import')
.description('Import transactions to an account')
.requiredOption('--account <id>', 'Account ID')
.option('--data <json>', 'Transaction data as JSON array')
.option(
'--file <path>',
'Read transaction data from JSON file (use - for stdin)',
)
.option('--dry-run', 'Preview without importing', false)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const transactions = readJsonInput(cmdOpts) as Parameters<
typeof api.importTransactions
>[1];
const result = await api.importTransactions(
cmdOpts.account,
transactions,
{
defaultCleared: true,
dryRun: cmdOpts.dryRun,
},
);
printOutput(result, opts.format as OutputFormat);
});
});
transactions
.command('update <id>')
.description('Update a transaction')
.option('--data <json>', 'Fields to update as JSON')
.option('--file <path>', 'Read fields from JSON file (use - for stdin)')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const fields = readJsonInput(cmdOpts) as Parameters<
typeof api.updateTransaction
>[1];
await api.updateTransaction(id, fields);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
transactions
.command('delete <id>')
.description('Delete a transaction')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteTransaction(id);
printOutput({ success: true, id }, opts.format as OutputFormat);
});
});
}

View File

@@ -0,0 +1,86 @@
import { homedir } from 'os';
import { join } from 'path';
import { cosmiconfig } from 'cosmiconfig';
export type CliConfig = {
serverURL: string;
password?: string;
sessionToken?: string;
budgetId?: string;
dataDir: string;
};
type CliGlobalOpts = {
serverUrl?: string;
password?: string;
sessionToken?: string;
budgetId?: string;
dataDir?: string;
};
type ConfigFileContent = {
serverURL?: string;
password?: string;
sessionToken?: string;
budgetId?: string;
dataDir?: string;
};
async function loadConfigFile(): Promise<ConfigFileContent> {
const explorer = cosmiconfig('actual');
const result = await explorer.search();
if (result && !result.isEmpty) {
return result.config as ConfigFileContent;
}
return {};
}
export async function resolveConfig(
cliOpts: CliGlobalOpts,
): Promise<CliConfig> {
const fileConfig = await loadConfigFile();
const serverURL =
cliOpts.serverUrl ??
process.env.ACTUAL_SERVER_URL ??
fileConfig.serverURL ??
'';
const password =
cliOpts.password ?? process.env.ACTUAL_PASSWORD ?? fileConfig.password;
const sessionToken =
cliOpts.sessionToken ??
process.env.ACTUAL_SESSION_TOKEN ??
fileConfig.sessionToken;
const budgetId =
cliOpts.budgetId ?? process.env.ACTUAL_BUDGET_ID ?? fileConfig.budgetId;
const dataDir =
cliOpts.dataDir ??
process.env.ACTUAL_DATA_DIR ??
fileConfig.dataDir ??
join(homedir(), '.actual-cli', 'data');
if (!serverURL) {
throw new Error(
'Server URL is required. Set --server-url, ACTUAL_SERVER_URL env var, or serverURL in config file.',
);
}
if (!password && !sessionToken) {
throw new Error(
'Authentication required. Set --password/--session-token, ACTUAL_PASSWORD/ACTUAL_SESSION_TOKEN env var, or password/sessionToken in config file.',
);
}
return {
serverURL,
password,
sessionToken,
budgetId,
dataDir,
};
}

View File

@@ -0,0 +1,52 @@
import * as api from '@actual-app/api';
import { resolveConfig } from './config.js';
import type { CliConfig } from './config.js';
type GlobalOpts = {
serverUrl?: string;
password?: string;
sessionToken?: string;
budgetId?: string;
dataDir?: string;
quiet?: boolean;
};
function info(message: string, quiet?: boolean) {
if (!quiet) {
process.stderr.write(message + '\n');
}
}
export async function withConnection<T>(
globalOpts: GlobalOpts,
fn: (config: CliConfig) => Promise<T>,
): Promise<T> {
const config = await resolveConfig(globalOpts);
info(`Connecting to ${config.serverURL}...`, globalOpts.quiet);
if (config.sessionToken) {
await api.init({
serverURL: config.serverURL,
sessionToken: config.sessionToken,
dataDir: config.dataDir,
});
} else if (config.password) {
await api.init({
serverURL: config.serverURL,
password: config.password,
dataDir: config.dataDir,
});
}
try {
if (config.budgetId) {
info(`Downloading budget ${config.budgetId}...`, globalOpts.quiet);
await api.downloadBudget(config.budgetId);
}
return await fn(config);
} finally {
await api.shutdown();
}
}

50
packages/cli/src/index.ts Normal file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { registerAccountsCommand } from './commands/accounts.js';
import { registerBudgetsCommand } from './commands/budgets.js';
import { registerCategoriesCommand } from './commands/categories.js';
import { registerCategoryGroupsCommand } from './commands/category-groups.js';
import { registerPayeesCommand } from './commands/payees.js';
import { registerQueryCommand } from './commands/query.js';
import { registerRulesCommand } from './commands/rules.js';
import { registerSchedulesCommand } from './commands/schedules.js';
import { registerServerCommand } from './commands/server.js';
import { registerTagsCommand } from './commands/tags.js';
import { registerTransactionsCommand } from './commands/transactions.js';
const program = new Command();
program
.name('actual')
.description('CLI for Actual Budget')
.version('26.3.0')
.option('--server-url <url>', 'Actual server URL (env: ACTUAL_SERVER_URL)')
.option('--password <password>', 'Server password (env: ACTUAL_PASSWORD)')
.option(
'--session-token <token>',
'Session token (env: ACTUAL_SESSION_TOKEN)',
)
.option('--budget-id <id>', 'Budget ID to load (env: ACTUAL_BUDGET_ID)')
.option('--data-dir <path>', 'Data directory (env: ACTUAL_DATA_DIR)')
.option('--format <format>', 'Output format: json, table, csv', 'json')
.option('--quiet', 'Suppress informational messages', false);
registerAccountsCommand(program);
registerBudgetsCommand(program);
registerCategoriesCommand(program);
registerCategoryGroupsCommand(program);
registerTransactionsCommand(program);
registerPayeesCommand(program);
registerTagsCommand(program);
registerRulesCommand(program);
registerSchedulesCommand(program);
registerQueryCommand(program);
registerServerCommand(program);
program.parseAsync(process.argv).catch((err: Error) => {
process.stderr.write(`Error: ${err.message}\n`);
process.stdout.write(JSON.stringify({ error: err.message }) + '\n');
process.exitCode = 1;
});

View File

@@ -0,0 +1,82 @@
import Table from 'cli-table3';
export type OutputFormat = 'json' | 'table' | 'csv';
export function formatOutput(
data: unknown,
format: OutputFormat = 'json',
): string {
switch (format) {
case 'json':
return JSON.stringify(data, null, 2);
case 'table':
return formatTable(data);
case 'csv':
return formatCsv(data);
default:
return JSON.stringify(data, null, 2);
}
}
function formatTable(data: unknown): string {
if (!Array.isArray(data)) {
if (data && typeof data === 'object') {
const table = new Table();
for (const [key, value] of Object.entries(data)) {
table.push({ [key]: String(value) });
}
return table.toString();
}
return String(data);
}
if (data.length === 0) {
return '(no results)';
}
const keys = Object.keys(data[0] as Record<string, unknown>);
const table = new Table({ head: keys });
for (const row of data) {
const r = row as Record<string, unknown>;
table.push(keys.map(k => String(r[k] ?? '')));
}
return table.toString();
}
function formatCsv(data: unknown): string {
if (!Array.isArray(data)) {
if (data && typeof data === 'object') {
const entries = Object.entries(data);
const header = entries.map(([k]) => escapeCsv(k)).join(',');
const values = entries.map(([, v]) => escapeCsv(String(v))).join(',');
return header + '\n' + values;
}
return String(data);
}
if (data.length === 0) {
return '';
}
const keys = Object.keys(data[0] as Record<string, unknown>);
const header = keys.map(k => escapeCsv(k)).join(',');
const rows = data.map(row => {
const r = row as Record<string, unknown>;
return keys.map(k => escapeCsv(String(r[k] ?? ''))).join(',');
});
return [header, ...rows].join('\n');
}
function escapeCsv(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return '"' + value.replace(/"/g, '""') + '"';
}
return value;
}
export function printOutput(data: unknown, format: OutputFormat = 'json') {
process.stdout.write(formatOutput(data, format) + '\n');
}

View File

@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"lib": ["ES2021"],
"types": ["node"],
"noEmit": false,
"declaration": true,
"declarationMap": true,
"strict": true,
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"references": [{ "path": "../api" }],
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "coverage"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
},
});

View File

@@ -230,6 +230,7 @@ const sidebars = {
link: { type: 'doc', id: 'api/index' },
items: [
'api/reference',
'api/cli',
{
type: 'category',
label: 'ActualQL',

View File

@@ -0,0 +1,348 @@
---
title: 'CLI'
---
# CLI Tool
The `@actual-app/cli` package provides a command-line interface for interacting with your Actual Budget data. It connects to your sync server and lets you query and modify budgets, accounts, transactions, categories, payees, rules, schedules, and more — all from the terminal.
:::note
This is different from the [sync-server CLI tool](../install/cli-tool.md) (`@actual-app/sync-server`), which is used to host and manage the Actual server itself.
:::
## Installation
Node.js v22 or higher is required.
```bash
npm install --save @actual-app/cli
```
Or install globally:
```bash
npm install --location=global @actual-app/cli
```
## Configuration
The CLI requires a connection to a running Actual sync server. Configuration can be provided via environment variables, CLI flags, or a config file.
### Environment Variables
| Variable | Description |
| ---------------------- | ------------------------------------------ |
| `ACTUAL_SERVER_URL` | URL of the Actual sync server (required) |
| `ACTUAL_BUDGET_ID` | Budget ID to load (required for most commands) |
| `ACTUAL_PASSWORD` | Server password (one of password or token required) |
| `ACTUAL_SESSION_TOKEN` | Session token (alternative to password) |
### CLI Flags
Global flags override environment variables:
| Flag | Description |
| --------------------------- | ----------------------------------------------- |
| `--server-url <url>` | Server URL |
| `--password <pw>` | Server password |
| `--session-token <token>` | Session token |
| `--budget-id <id>` | Budget ID |
| `--data-dir <path>` | Local data directory for cached budget data |
| `--format <format>` | Output format: `json` (default), `table`, `csv` |
| `--quiet` | Suppress informational messages on stderr |
### Config File
The CLI uses [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) for configuration. You can create a config file in any of these formats:
- `.actualrc` (JSON or YAML)
- `.actualrc.json`, `.actualrc.yaml`, `.actualrc.yml`, `.actualrc.js`, `.actualrc.cjs`
- `actual.config.js`, `actual.config.cjs`
- An `"actual"` key in your `package.json`
Example `.actualrc.json`:
```json
{
"serverUrl": "http://localhost:5006",
"password": "your-password",
"budgetId": "1cfdbb80-6274-49bf-b0c2-737235a4c81f"
}
```
## Usage
```bash
actual <command> <subcommand> [options]
```
## Commands
### Accounts
```bash
# List all accounts
actual accounts list
# Create an account
actual accounts create --name "Checking" [--offbudget] [--balance 50000]
# Update an account
actual accounts update <id> [--name "New Name"] [--offbudget true]
# Close an account (with optional transfer)
actual accounts close <id> [--transfer-account <id>] [--transfer-category <id>]
# Reopen a closed account
actual accounts reopen <id>
# Delete an account
actual accounts delete <id>
# Get account balance
actual accounts balance <id> [--cutoff 2026-01-31]
```
### Budgets
```bash
# List available budgets on the server
actual budgets list
# Download a budget by sync ID
actual budgets download <syncId> [--encryption-password <pw>]
# Sync the current budget
actual budgets sync
# List budget months
actual budgets months
# View a specific month
actual budgets month 2026-03
# Set a budget amount (in integer cents)
actual budgets set-amount --month 2026-03 --category <id> --amount 50000
# Set carryover flag
actual budgets set-carryover --month 2026-03 --category <id> --flag true
# Hold funds for next month
actual budgets hold-next-month --month 2026-03 --amount 10000
# Reset held funds
actual budgets reset-hold --month 2026-03
```
### Categories
```bash
# List all categories
actual categories list
# Create a category
actual categories create --name "Groceries" --group-id <id> [--is-income]
# Update a category
actual categories update <id> [--name "Food"] [--hidden true]
# Delete a category (with optional transfer)
actual categories delete <id> [--transfer-to <id>]
```
### Category Groups
```bash
# List all category groups
actual category-groups list
# Create a category group
actual category-groups create --name "Essentials" [--is-income]
# Update a category group
actual category-groups update <id> [--name "New Name"] [--hidden true]
# Delete a category group (with optional transfer)
actual category-groups delete <id> [--transfer-to <id>]
```
### Transactions
```bash
# List transactions for an account within a date range
actual transactions list --account <id> --start 2026-01-01 --end 2026-03-31
# Add transactions (inline JSON)
actual transactions add --account <id> --data '[{"date":"2026-03-13","amount":-5000,"payee_name":"Store"}]'
# Add transactions (from file)
actual transactions add --account <id> --file transactions.json
# Import transactions with reconciliation (deduplication)
actual transactions import --account <id> --data '[...]' [--dry-run]
# Update a transaction
actual transactions update <id> --data '{"notes":"Updated note"}'
# Delete a transaction
actual transactions delete <id>
```
### Payees
```bash
# List all payees
actual payees list
# List common payees
actual payees common
# Create a payee
actual payees create --name "Grocery Store"
# Update a payee
actual payees update <id> --name "New Name"
# Delete a payee
actual payees delete <id>
# Merge multiple payees into one
actual payees merge --target <id> --ids id1,id2,id3
```
### Tags
```bash
# List all tags
actual tags list
# Create a tag
actual tags create --tag "vacation" [--color "#ff0000"] [--description "Vacation expenses"]
# Update a tag
actual tags update <id> [--tag "trip"] [--color "#00ff00"]
# Delete a tag
actual tags delete <id>
```
### Rules
```bash
# List all rules
actual rules list
# List rules for a specific payee
actual rules payee-rules <payeeId>
# Create a rule (inline JSON)
actual rules create --data '{"stage":"pre","conditionsOp":"and","conditions":[...],"actions":[...]}'
# Create a rule (from file)
actual rules create --file rule.json
# Update a rule
actual rules update --data '{"id":"...","stage":"pre",...}'
# Delete a rule
actual rules delete <id>
```
### Schedules
```bash
# List all schedules
actual schedules list
# Create a schedule
actual schedules create --data '{"name":"Rent","date":"1st","amount":-150000,"amountOp":"is","account":"...","payee":"..."}'
# Update a schedule
actual schedules update <id> --data '{"name":"Updated Rent"}' [--reset-next-date]
# Delete a schedule
actual schedules delete <id>
```
### Query (ActualQL)
Run queries using [ActualQL](./actual-ql/index.md):
```bash
# Run a query (inline)
actual query run --table transactions --select "date,amount,payee" --filter '{"amount":{"$lt":0}}' --limit 10
# Run a query (from file)
actual query run --file query.json
```
### Server
```bash
# Get the server version
actual server version
# Look up an entity ID by name
actual server get-id --type accounts --name "Checking"
actual server get-id --type categories --name "Groceries"
# Trigger bank sync
actual server bank-sync [--account <id>]
```
## Amount Convention
All monetary amounts are represented as **integer cents**:
| CLI Value | Dollar Amount |
| --------- | ------------- |
| `5000` | $50.00 |
| `-12350` | -$123.50 |
| `100` | $1.00 |
When providing amounts, always use integer cents. For example, to budget $50, pass `5000`.
## Output Formats
The `--format` flag controls how results are displayed:
- **`json`** (default) — Machine-readable JSON output, ideal for scripting
- **`table`** — Human-readable table format
- **`csv`** — Comma-separated values for spreadsheet import
Use `--quiet` to suppress informational messages on stderr when piping output to other tools.
## Common Workflows
**View your budget for the current month:**
```bash
actual budgets month 2026-03 --format table
```
**Check an account balance:**
```bash
# Find the account ID
actual server get-id --type accounts --name "Checking" --quiet
# Get the balance
actual accounts balance <id> --quiet
```
**Export transactions to CSV:**
```bash
actual transactions list --account <id> --start 2026-01-01 --end 2026-12-31 --format csv > transactions.csv
```
**Add a transaction:**
```bash
actual transactions add --account <id> --data '[{"date":"2026-03-14","amount":-2500,"payee_name":"Coffee Shop"}]'
```
## Error Handling
- Non-zero exit codes indicate an error
- Error output is JSON on stdout: `{"error": "message"}`
- Use `--quiet` to suppress informational stderr messages without affecting error output

View File

@@ -7,7 +7,8 @@
{ "path": "./packages/api" },
{ "path": "./packages/desktop-client" },
{ "path": "./packages/sync-server" },
{ "path": "./packages/desktop-electron" }
{ "path": "./packages/desktop-electron" },
{ "path": "./packages/cli" }
],
"compilerOptions": {
"target": "ES2022",

View File

@@ -19,7 +19,7 @@ __metadata:
languageName: node
linkType: hard
"@actual-app/api@workspace:packages/api":
"@actual-app/api@workspace:^, @actual-app/api@workspace:packages/api":
version: 0.0.0-use.local
resolution: "@actual-app/api@workspace:packages/api"
dependencies:
@@ -49,6 +49,24 @@ __metadata:
languageName: unknown
linkType: soft
"@actual-app/cli@workspace:packages/cli":
version: 0.0.0-use.local
resolution: "@actual-app/cli@workspace:packages/cli"
dependencies:
"@actual-app/api": "workspace:^"
"@types/node": "npm:^22.19.10"
cli-table3: "npm:^0.6.5"
commander: "npm:^13.0.0"
cosmiconfig: "npm:^9.0.0"
typescript: "npm:^5.9.3"
typescript-strict-plugin: "npm:^2.4.4"
vitest: "npm:^4.0.18"
bin:
actual: ./build/src/index.js
actual-cli: ./build/src/index.js
languageName: unknown
linkType: soft
"@actual-app/components@workspace:*, @actual-app/components@workspace:packages/component-library":
version: 0.0.0-use.local
resolution: "@actual-app/components@workspace:packages/component-library"
@@ -13182,7 +13200,7 @@ __metadata:
languageName: node
linkType: hard
"cli-table3@npm:^0.6.3":
"cli-table3@npm:^0.6.3, cli-table3@npm:^0.6.5":
version: 0.6.5
resolution: "cli-table3@npm:0.6.5"
dependencies:
@@ -13492,6 +13510,13 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^13.0.0":
version: 13.1.0
resolution: "commander@npm:13.1.0"
checksum: 10/d3b4b79e6be8471ddadacbb8cd441fe82154d7da7393b50e76165a9e29ccdb74fa911a186437b9a211d0fc071db6051915c94fb8ef16d77511d898e9dbabc6af
languageName: node
linkType: hard
"commander@npm:^14.0.0, commander@npm:^14.0.2":
version: 14.0.2
resolution: "commander@npm:14.0.2"
@@ -13892,6 +13917,23 @@ __metadata:
languageName: node
linkType: hard
"cosmiconfig@npm:^9.0.0":
version: 9.0.1
resolution: "cosmiconfig@npm:9.0.1"
dependencies:
env-paths: "npm:^2.2.1"
import-fresh: "npm:^3.3.0"
js-yaml: "npm:^4.1.0"
parse-json: "npm:^5.2.0"
peerDependencies:
typescript: ">=4.9.5"
peerDependenciesMeta:
typescript:
optional: true
checksum: 10/89fcac84d062f0710091bb2d6a6175bcde22f5448877db9c43429694408191d3d4e215193b3ac4d54f7f89ef188d55cd481c7a2295b0dc572e65b528bf6fec01
languageName: node
linkType: hard
"crc@npm:^3.8.0":
version: 3.8.0
resolution: "crc@npm:3.8.0"
@@ -15678,7 +15720,7 @@ __metadata:
languageName: node
linkType: hard
"env-paths@npm:^2.2.0":
"env-paths@npm:^2.2.0, env-paths@npm:^2.2.1":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
checksum: 10/65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e