Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ src/
config.ts - Credential storage (~/.config/lucas/)
api-client.ts - HTTP client with Bearer auth
output.ts - JSON output helpers (success/error)
body-builder.ts - Request body builder with --no-flag unset support
commands/
auth/
login.ts - Device authorization flow
Expand All @@ -29,6 +30,7 @@ src/
transfers/
list.ts - List all transfers
create.ts - Create transfer (from, to, amount, exchange-rate)
update.ts - Update transfer by ID
delete.ts - Delete transfer by ID
subscriptions/
list.ts - List all subscriptions
Expand All @@ -39,6 +41,7 @@ src/
loans/
list.ts - List all loans
create.ts - Create loan (name, principal, account)
update.ts - Update loan by ID
pay.ts - Make a loan payment
delete.ts - Delete loan by ID
stats/
Expand Down Expand Up @@ -143,6 +146,7 @@ Login flow writes human-readable output to stderr (not JSON) since it is interac
| DELETE | /api/transactions/:id | transactions delete |
| GET | /api/transfers | transfers list |
| POST | /api/transfers | transfers create |
| PUT | /api/transfers/:id | transfers update |
| DELETE | /api/transfers/:id | transfers delete |
| GET | /api/subscriptions | subscriptions list |
| POST | /api/subscriptions | subscriptions create |
Expand All @@ -152,6 +156,7 @@ Login flow writes human-readable output to stderr (not JSON) since it is interac
| GET | /api/loans | loans list |
| POST | /api/loans | loans create |
| POST | /api/loans/:id/pay | loans pay |
| PUT | /api/loans/:id | loans update |
| DELETE | /api/loans/:id | loans delete |
| GET | /api/stats/summary | stats summary |
| GET | /api/stats/monthly | stats monthly |
Expand Down
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ lucas accounts create \
--bank "BCP" \
--currency PEN \
--balance 1000 # Create an account
lucas accounts update <id> --name "New Name" # Update account
lucas accounts update <id> \
--name "New Name" \
--balance 5000 \
--credit-limit 10000 \
--is-archived true # Update account
lucas accounts delete <id> # Delete account
```

Expand Down Expand Up @@ -117,6 +121,12 @@ lucas transfers create \
--description "Monthly savings" \
--exchange-rate 3.72 # Create a transfer

lucas transfers update <id> \
--amount 1500 \
--exchange-rate 3.75 # Update a transfer
lucas transfers update <id> \
--amount 1000 \
--no-notes # Update with unset
lucas transfers delete <id> # Delete transfer
```

Expand All @@ -128,12 +138,15 @@ lucas subscriptions create \
--name "Netflix" \
--amount 44.90 \
--account-id <id> \
--currency PEN \
--frequency MONTHLY \
--billing-day 1 \
--next-billing-date 2026-04-01 # Create a subscription

lucas subscriptions update <id> \
--amount 49.90 \
--frequency YEARLY # Update subscription
--frequency YEARLY \
--no-account-id # Update subscription (unset account)

lucas subscriptions mark-paid <id> # Mark as paid
lucas subscriptions delete <id> # Delete subscription
Expand All @@ -149,10 +162,21 @@ lucas loans create \
--name "Car Loan" \
--principal 25000 \
--account-id <id> \
--currency PEN \
--interest-rate 12.5 \
--installments 36 \
--first-due-date 2026-02-15 \
--interval-unit MONTH \
--interval-count 1 \
--start-date 2026-01-15 # Create a loan

lucas loans update <id> \
--name "Auto Loan" \
--interest-rate 5.5 # Update a loan
lucas loans update <id> \
--no-agreed-installments \
--no-target-payment # Update with unset

lucas loans pay <id> --amount 750 # Make a payment
lucas loans delete <id> # Delete loan
```
Expand Down Expand Up @@ -180,6 +204,18 @@ lucas exchange-rate convert \
--amount 100 # Convert currencies
```

### Unsetting Optional Fields

Use `--no-<field>` to clear an optional field:

```bash
lucas subscriptions update <id> --no-account-id # Remove linked account
lucas transactions update <id> --no-category-id # Clear category
lucas accounts update <id> --no-credit-limit # Remove credit limit
lucas transfers update <id> --no-notes # Clear notes
lucas loans update <id> --no-agreed-installments # Remove agreed installments
```

## AI Integration

LucasApp CLI outputs structured JSON exclusively, making it a natural fit for AI agents with terminal access.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lucasapp-cli",
"version": "0.1.0",
"version": "0.2.0",
"description": "LucasApp CLI - Financial data management for AI agents",
"author": "StevenACZ",
"license": "MIT",
Expand Down
33 changes: 27 additions & 6 deletions src/commands/accounts/update.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Command } from "commander";
import { apiRequest } from "../../lib/api-client.js";
import { output } from "../../lib/output.js";
import { buildBody } from "../../lib/body-builder.js";

export const updateAccountCommand = new Command("update")
.description("Update an account")
Expand All @@ -9,13 +10,33 @@ export const updateAccountCommand = new Command("update")
.option("--bank <bank>", "Bank name")
.option("--color <color>", "Account color")
.option("--icon <icon>", "Account icon")
.option("--balance <amount>", "Account balance")
.option("--credit-limit <amount>", "Credit limit")
.option("--current-debt <amount>", "Current debt")
.option("--statement-closing-day <day>", "Statement closing day")
.option("--display-order <n>", "Display order")
.option("--excluded", "Exclude from totals")
.option("--no-excluded", "Include in totals")
.option("--is-archived", "Archive account")
.option("--no-is-archived", "Unarchive account")
.action(async (id: string, opts) => {
const body: Record<string, unknown> = {};
if (opts.name) body.name = opts.name;
if (opts.bank) body.bank = opts.bank;
if (opts.color) body.color = opts.color;
if (opts.icon) body.icon = opts.icon;

const body = buildBody(opts, [
{ opt: "name", body: "name" },
{ opt: "bank", body: "bank" },
{ opt: "color", body: "color" },
{ opt: "icon", body: "icon" },
{ opt: "balance", body: "balance", type: "number" },
{ opt: "creditLimit", body: "creditLimit", type: "number" },
{ opt: "currentDebt", body: "currentDebt", type: "number" },
{
opt: "statementClosingDay",
body: "statementClosingDay",
type: "number",
},
{ opt: "displayOrder", body: "displayOrder", type: "number" },
{ opt: "excluded", body: "excluded", type: "boolean" },
{ opt: "isArchived", body: "isArchived", type: "boolean" },
]);
const data = await apiRequest("PUT", `/api/accounts/${id}`, body);
output.success(data);
});
45 changes: 33 additions & 12 deletions src/commands/loans/create.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,46 @@
import { Command } from "commander";
import { apiRequest } from "../../lib/api-client.js";
import { output } from "../../lib/output.js";
import { buildBody } from "../../lib/body-builder.js";

export const createLoanCommand = new Command("create")
.description("Create a new loan")
.requiredOption("--name <name>", "Loan name")
.requiredOption("--principal <amount>", "Principal amount")
.requiredOption("--account-id <id>", "Account ID")
.requiredOption("--currency <code>", "Currency code")
.requiredOption("--first-due-date <date>", "First due date (YYYY-MM-DD)")
.requiredOption(
"--interval-unit <unit>",
"Interval unit (DAY|WEEK|MONTH|YEAR)",
)
.requiredOption("--interval-count <n>", "Interval count")
.option("--account-id <id>", "Payment account ID")
.option("--agreed-installments <n>", "Total installments")
.option("--target-payment <amount>", "Target payment amount")
.option("--interest-rate <rate>", "Interest rate")
.option("--installments <n>", "Number of installments")
.option("--start-date <date>", "Start date (YYYY-MM-DD)")
.option("--interest-rate-unit <unit>", "Rate unit (ANNUAL|MONTHLY)")
.option("--interest-enabled", "Enable interest")
.option("--late-fee-amount <amount>", "Late fee amount")
.option("--late-fee-grace-days <n>", "Late fee grace days")
.option("--late-fee-enabled", "Enable late fees")
.action(async (opts) => {
const body: Record<string, unknown> = {
name: opts.name,
principal: Number(opts.principal),
accountId: opts.accountId,
};
if (opts.interestRate) body.interestRate = Number(opts.interestRate);
if (opts.installments) body.installments = Number(opts.installments);
if (opts.startDate) body.startDate = opts.startDate;

const body = buildBody(opts, [
{ opt: "name", body: "name" },
{ opt: "principal", body: "principal", type: "number" },
{ opt: "currency", body: "currency" },
{ opt: "firstDueDate", body: "firstDueDate" },
{ opt: "intervalUnit", body: "intervalUnit" },
{ opt: "intervalCount", body: "intervalCount", type: "number" },
{ opt: "accountId", body: "paymentAccountId" },
{ opt: "agreedInstallments", body: "agreedInstallments", type: "number" },
{ opt: "targetPayment", body: "targetPayment", type: "number" },
{ opt: "interestRate", body: "interestRate", type: "number" },
{ opt: "interestRateUnit", body: "interestRateUnit" },
{ opt: "interestEnabled", body: "interestEnabled", type: "boolean" },
{ opt: "lateFeeAmount", body: "lateFeeAmount", type: "number" },
{ opt: "lateFeeGraceDays", body: "lateFeeGraceDays", type: "number" },
{ opt: "lateFeeEnabled", body: "lateFeeEnabled", type: "boolean" },
]);
const data = await apiRequest("POST", "/api/loans", body);
output.success(data);
});
48 changes: 48 additions & 0 deletions src/commands/loans/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Command } from "commander";
import { apiRequest } from "../../lib/api-client.js";
import { output } from "../../lib/output.js";
import { buildBody } from "../../lib/body-builder.js";

export const updateLoanCommand = new Command("update")
.description("Update a loan")
.argument("<id>", "Loan ID")
.option("--name <name>", "Loan name")
.option("--account-id <id>", "Payment account ID")
.option("--is-primary", "Set as primary")
.option("--no-is-primary", "Unset primary")
.option("--is-archived", "Archive loan")
.option("--no-is-archived", "Unarchive loan")
.option("--first-due-date <date>", "First due date (YYYY-MM-DD)")
.option("--interval-unit <unit>", "Interval unit (DAY|WEEK|MONTH|YEAR)")
.option("--interval-count <n>", "Interval count")
.option("--agreed-installments <n>", "Total installments")
.option("--target-payment <amount>", "Target payment amount")
.option("--interest-rate <rate>", "Interest rate")
.option("--interest-rate-unit <unit>", "Rate unit (ANNUAL|MONTHLY)")
.option("--interest-enabled", "Enable interest")
.option("--no-interest-enabled", "Disable interest")
.option("--late-fee-amount <amount>", "Late fee amount")
.option("--late-fee-grace-days <n>", "Late fee grace days")
.option("--late-fee-enabled", "Enable late fees")
.option("--no-late-fee-enabled", "Disable late fees")
.action(async (id, opts) => {
const body = buildBody(opts, [
{ opt: "name", body: "name" },
{ opt: "accountId", body: "paymentAccountId" },
{ opt: "isPrimary", body: "isPrimary", type: "boolean" },
{ opt: "isArchived", body: "isArchived", type: "boolean" },
{ opt: "firstDueDate", body: "firstDueDate" },
{ opt: "intervalUnit", body: "intervalUnit" },
{ opt: "intervalCount", body: "intervalCount", type: "number" },
{ opt: "agreedInstallments", body: "agreedInstallments", type: "number" },
{ opt: "targetPayment", body: "targetPayment", type: "number" },
{ opt: "interestRate", body: "interestRate", type: "number" },
{ opt: "interestRateUnit", body: "interestRateUnit" },
{ opt: "interestEnabled", body: "interestEnabled", type: "boolean" },
{ opt: "lateFeeAmount", body: "lateFeeAmount", type: "number" },
{ opt: "lateFeeGraceDays", body: "lateFeeGraceDays", type: "number" },
{ opt: "lateFeeEnabled", body: "lateFeeEnabled", type: "boolean" },
]);
const data = await apiRequest("PUT", `/api/loans/${id}`, body);
output.success(data);
});
12 changes: 10 additions & 2 deletions src/commands/stats/by-category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import { output } from "../../lib/output.js";

export const byCategoryCommand = new Command("by-category")
.description("Get statistics by category")
.action(async () => {
const data = await apiRequest("GET", "/api/stats/by-category");
.option("--currency <code>", "Currency code")
.action(async (opts) => {
const params: Record<string, string> = {};
if (opts.currency) params.currency = opts.currency;
const data = await apiRequest(
"GET",
"/api/stats/by-category",
undefined,
params,
);
output.success(data);
});
14 changes: 12 additions & 2 deletions src/commands/stats/monthly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import { output } from "../../lib/output.js";

export const monthlyCommand = new Command("monthly")
.description("Get monthly statistics")
.action(async () => {
const data = await apiRequest("GET", "/api/stats/monthly");
.option("--currency <code>", "Currency code")
.option("--months <n>", "Number of months")
.action(async (opts) => {
const params: Record<string, string> = {};
if (opts.currency) params.currency = opts.currency;
if (opts.months) params.months = opts.months;
const data = await apiRequest(
"GET",
"/api/stats/monthly",
undefined,
params,
);
output.success(data);
});
12 changes: 10 additions & 2 deletions src/commands/stats/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import { output } from "../../lib/output.js";

export const summaryCommand = new Command("summary")
.description("Get financial summary")
.action(async () => {
const data = await apiRequest("GET", "/api/stats/summary");
.option("--currency <code>", "Currency code")
.action(async (opts) => {
const params: Record<string, string> = {};
if (opts.currency) params.currency = opts.currency;
const data = await apiRequest(
"GET",
"/api/stats/summary",
undefined,
params,
);
output.success(data);
});
40 changes: 28 additions & 12 deletions src/commands/subscriptions/create.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
import { Command } from "commander";
import { apiRequest } from "../../lib/api-client.js";
import { output } from "../../lib/output.js";
import { buildBody } from "../../lib/body-builder.js";

export const createSubscriptionCommand = new Command("create")
.description("Create a new subscription")
.requiredOption("--name <name>", "Subscription name")
.requiredOption("--amount <amount>", "Subscription amount")
.requiredOption("--account-id <id>", "Account ID")
.requiredOption("--frequency <freq>", "Frequency (MONTHLY|YEARLY|WEEKLY)")
.option("--next-billing-date <date>", "Next billing date")
.requiredOption("--frequency <freq>", "Frequency (MONTHLY|YEARLY)")
.requiredOption("--billing-day <day>", "Billing day of the month")
.option("--account-id <id>", "Account ID")
.option("--currency <code>", "Currency code")
.option("--billing-month <month>", "Billing month (for YEARLY)")
.option("--icon <icon>", "Icon")
.option("--color <color>", "Color")
.option("--category-id <id>", "Category ID")
.option("--type <type>", "Type")
.option("--auto-record", "Enable auto-record")
.option("--start-date <date>", "Start date (YYYY-MM-DD)")
.option("--description <desc>", "Description")
.action(async (opts) => {
const body: Record<string, unknown> = {
name: opts.name,
amount: Number(opts.amount),
accountId: opts.accountId,
frequency: opts.frequency,
};
if (opts.nextBillingDate) body.nextBillingDate = opts.nextBillingDate;
if (opts.description) body.description = opts.description;

const body = buildBody(opts, [
{ opt: "name", body: "name" },
{ opt: "amount", body: "amount", type: "number" },
{ opt: "frequency", body: "frequency" },
{ opt: "billingDay", body: "billingDay", type: "number" },
{ opt: "accountId", body: "accountId" },
{ opt: "currency", body: "currency" },
{ opt: "billingMonth", body: "billingMonth", type: "number" },
{ opt: "icon", body: "icon" },
{ opt: "color", body: "color" },
{ opt: "categoryId", body: "categoryId" },
{ opt: "type", body: "type" },
{ opt: "autoRecord", body: "autoRecord", type: "boolean" },
{ opt: "startDate", body: "startDate" },
{ opt: "description", body: "description" },
]);
const data = await apiRequest("POST", "/api/subscriptions", body);
output.success(data);
});
Loading
Loading