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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ safe to publish.
setup.
- Default production API is `https://api.lucasapp.app`; browser approval lives
on `https://dashboard.lucasapp.app/cli`.
- Keep `investments` commands as thin API clients; backend owns quotes,
catalog search, Premium gating, cash validation, and archive semantics.
- Keep subscription calendar and group commands as thin API clients; backend
owns Premium gating, billing dates, group ownership, and reorder semantics.
- Keep subscription charge commands as thin API clients; backend owns charge
generation, transaction side effects, confirmation, and SSE semantics.
- Match public list commands to the backend response shape, including
pagination wrappers such as `{ items, summary, pagination }`.
- Build API paths with `resourcePath()` when an ID appears in the URL path.
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,27 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Added `lucas investments` commands for instrument discovery, portfolio
overview, positions, activity, trades, cash adjustments, and archived
investment recovery.
- Added investment history and permanent archive cleanup commands.
- Added `lucas investments refresh` for backend catalog/EOD/snapshot refresh
jobs.
- Added `lucas subscriptions calendar` for the backend monthly billing
calendar.
- Added `lucas subscription-groups` list/create/update/delete/reorder commands.
- Added `lucas subscription-charges` commands for pending charges, account
charges, pay, confirm, and manual paid actions.

### Changed

- Changed the default production API URL to `https://api.lucasapp.app` and the
CLI landing/approval URL to `https://dashboard.lucasapp.app/cli`.
- `accounts update` now supports `--currency` to match backend account currency
edits.
- `subscriptions create` and `subscriptions update` now support `--group-id`.

## [0.6.6] - 2026-05-20

Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,16 @@ lucas transfers list --limit 10 --offset 0
lucas transfers create --from-account-id <id> --to-account-id <id> --amount 500

lucas subscriptions list --type SERVICE --limit 20
lucas subscriptions calendar --month 2026-05 --type SUBSCRIPTION --frequency MONTHLY
lucas subscriptions mark-paid <id>
lucas subscription-groups list
lucas subscription-charges pending --limit 10
lucas subscription-charges pay <charge-id>

lucas investments instruments --rank popular --limit 20
lucas investments refresh --action eod
lucas investments trade <account-id> --instrument-id <id> --side BUY --quantity 1 --price 100
lucas investments history <account-id> --range 90d

lucas loans list
lucas loans pay <id> --amount 750 --verified
Expand All @@ -54,7 +63,9 @@ lucas ai insights "How am I doing this month?" --period month --currency PEN
```

Command groups: `auth`, `accounts`, `transactions`, `transfers`,
`subscriptions`, `loans`, `stats`, `categories`, `exchange-rate`, and `ai`.
`investments`, `subscriptions`, `subscription-groups`,
`subscription-charges`, `loans`, `stats`, `categories`, `exchange-rate`, and
`ai`.
The `ai` group is intentionally limited to usage, text/image expense parsing,
and read-only insights.

Expand All @@ -67,6 +78,13 @@ List commands are intentionally agent-friendly:
transfer pair still returns its two transaction rows.
- `subscriptions list` supports `--limit`, `--offset`, `--frequency`, `--type`,
and `--group-id`.
- `subscriptions calendar` mirrors the backend monthly billing calendar, and
`subscription-groups` exposes group list/create/update/delete/reorder.
- `subscription-charges` exposes generated charges, pending-charge pagination,
account-scoped charges, and pay/confirm/manual-paid actions.
- `investments` mirrors the Premium backend contract for instrument discovery,
portfolio overview, positions, activity, history, trades, cash adjustments,
backend refresh jobs, and archived investment recovery.
- `accounts list --include-archived` includes archived accounts in the account
array and adds `archivedAccounts` metadata. Balance/debt totals remain the
active-account totals returned by LucasApp.
Expand Down
2 changes: 2 additions & 0 deletions src/commands/accounts/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const updateAccountCommand = new Command("update")
.argument("<id>", "Account ID")
.option("--name <name>", "Account name")
.option("--bank <bank>", "Bank name")
.option("--currency <currency>", "Currency code")
.option("--color <color>", "Account color")
.option("--clear-color", "Clear account color")
.option("--icon <icon>", "Account icon")
Expand All @@ -29,6 +30,7 @@ export const updateAccountCommand = new Command("update")
const body = buildBody(opts, [
{ opt: "name", body: "name" },
{ opt: "bank", body: "bank" },
{ opt: "currency", body: "currency" },
{ opt: "color", body: "color", clearOpt: "clearColor" },
{ opt: "icon", body: "icon", clearOpt: "clearIcon" },
{ opt: "balance", body: "balance", type: "number" },
Expand Down
114 changes: 114 additions & 0 deletions src/commands/investments/archive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Command } from "commander";
import { apiRequest } from "../../lib/api-client.js";
import { output } from "../../lib/output.js";
import { resourcePath } from "../../lib/resource-path.js";

interface ArchivedItemsOptions {
limit?: string;
offset?: string;
kind?: string;
}

export function buildArchivedItemsParams(
opts: ArchivedItemsOptions,
): Record<string, string> | undefined {
const params: Record<string, string> = {};
if (opts.limit) params.limit = opts.limit;
if (opts.offset) params.offset = opts.offset;
if (opts.kind) params.kind = opts.kind;
return Object.keys(params).length > 0 ? params : undefined;
}

export const listArchivedInvestmentsCommand = new Command("archived")
.description("List archived investment accounts summary")
.action(async () => {
const data = await apiRequest("GET", "/api/investments/archived");
output.success(data);
});

export const listArchivedInvestmentItemsCommand = new Command("archived-items")
.description("List archived investment trades and cash adjustments")
.option("--limit <n>", "Items per page (1..100)")
.option("--offset <n>", "Pagination offset")
.option("--kind <kind>", "Item kind (all|trade|cash-adjustment)")
.action(async (opts: ArchivedItemsOptions) => {
const data = await apiRequest(
"GET",
"/api/investments/archived/items",
undefined,
buildArchivedItemsParams(opts),
);
output.success(data);
});

export const restoreArchivedInvestmentsCommand = new Command("restore-archived")
.description("Restore all archived investments for an account")
.argument("<account-id>", "Investment account ID")
.action(async (accountId: string) => {
const data = await apiRequest(
"POST",
resourcePath("/api/investments/archived", accountId, "restore"),
);
output.success(data);
});

export const restoreTradeCommand = new Command("trade-restore")
.description("Restore an archived investment trade")
.argument("<trade-id>", "Trade ID")
.action(async (tradeId: string) => {
const data = await apiRequest(
"POST",
resourcePath("/api/investments/trades", tradeId, "restore"),
);
output.success(data);
});

export const restoreCashAdjustmentCommand = new Command("cash-restore")
.description("Restore an archived investment cash adjustment")
.argument("<adjustment-id>", "Cash adjustment ID")
.action(async (adjustmentId: string) => {
const data = await apiRequest(
"POST",
resourcePath(
"/api/investments/cash-adjustments",
adjustmentId,
"restore",
),
);
output.success(data);
});

export const permanentDeleteTradeCommand = new Command("trade-permanent-delete")
.description("Permanently delete an archived investment trade")
.argument("<trade-id>", "Trade ID")
.action(async (tradeId: string) => {
const data = await apiRequest(
"DELETE",
resourcePath("/api/investments/trades", tradeId, "permanent"),
);
output.success(data);
});

export const permanentDeleteCashAdjustmentCommand = new Command(
"cash-permanent-delete",
)
.description("Permanently delete an archived investment cash adjustment")
.argument("<adjustment-id>", "Cash adjustment ID")
.action(async (adjustmentId: string) => {
const data = await apiRequest(
"DELETE",
resourcePath(
"/api/investments/cash-adjustments",
adjustmentId,
"permanent",
),
);
output.success(data);
});

export const emptyArchivedInvestmentsCommand = new Command("archived-empty")
.description("Permanently delete all archived investment items")
.action(async () => {
const data = await apiRequest("DELETE", "/api/investments/archived");
output.success(data);
});
81 changes: 81 additions & 0 deletions src/commands/investments/cash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Command } from "commander";
import { apiRequest } from "../../lib/api-client.js";
import { buildBody } from "../../lib/body-builder.js";
import { parseFiniteNumber } from "../../lib/number-parser.js";
import { output } from "../../lib/output.js";
import { resourcePath } from "../../lib/resource-path.js";

interface CreateCashAdjustmentOptions {
amount: string;
occurredAt?: string;
note?: string;
}

interface UpdateCashAdjustmentOptions {
amount?: string;
occurredAt?: string;
note?: string;
clearNote?: boolean;
}

export function buildCreateCashAdjustmentBody(
opts: CreateCashAdjustmentOptions,
): Record<string, unknown> {
return {
amount: parseFiniteNumber(opts.amount, "--amount"),
...(opts.occurredAt && { occurredAt: opts.occurredAt }),
...(opts.note && { note: opts.note }),
};
}

export function buildUpdateCashAdjustmentBody(
opts: UpdateCashAdjustmentOptions,
): Record<string, unknown> {
return buildBody(opts as Record<string, unknown>, [
{ opt: "amount", body: "amount", type: "number" },
{ opt: "occurredAt", body: "occurredAt" },
{ opt: "note", body: "note", clearOpt: "clearNote" },
]);
}

export const createCashAdjustmentCommand = new Command("cash")
.description("Create an investment cash adjustment")
.argument("<account-id>", "Investment account ID")
.requiredOption("--amount <amount>", "Signed cash amount")
.option("--occurred-at <iso>", "Adjustment datetime (ISO 8601)")
.option("--note <note>", "Adjustment note")
.action(async (accountId: string, opts: CreateCashAdjustmentOptions) => {
const data = await apiRequest(
"POST",
resourcePath("/api/investments/accounts", accountId, "cash-adjustments"),
buildCreateCashAdjustmentBody(opts),
);
output.success(data);
});

export const updateCashAdjustmentCommand = new Command("cash-update")
.description("Update an investment cash adjustment")
.argument("<adjustment-id>", "Cash adjustment ID")
.option("--amount <amount>", "Signed cash amount")
.option("--occurred-at <iso>", "Adjustment datetime (ISO 8601)")
.option("--note <note>", "Adjustment note")
.option("--clear-note", "Clear adjustment note")
.action(async (adjustmentId: string, opts: UpdateCashAdjustmentOptions) => {
const data = await apiRequest(
"PUT",
resourcePath("/api/investments/cash-adjustments", adjustmentId),
buildUpdateCashAdjustmentBody(opts),
);
output.success(data);
});

export const deleteCashAdjustmentCommand = new Command("cash-delete")
.description("Delete an investment cash adjustment")
.argument("<adjustment-id>", "Cash adjustment ID")
.action(async (adjustmentId: string) => {
const data = await apiRequest(
"DELETE",
resourcePath("/api/investments/cash-adjustments", adjustmentId),
);
output.success(data);
});
60 changes: 60 additions & 0 deletions src/commands/investments/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Command } from "commander";
import {
getInstrumentCommand,
listInstrumentsCommand,
searchInstrumentsCommand,
} from "./instruments.js";
import { refreshInvestmentsCommand } from "./refresh.js";
import {
investmentActivityCommand,
investmentHistoryCommand,
investmentOverviewCommand,
investmentPositionsCommand,
} from "./portfolio.js";
import {
createTradeCommand,
deleteTradeCommand,
updateTradeCommand,
} from "./trades.js";
import {
createCashAdjustmentCommand,
deleteCashAdjustmentCommand,
updateCashAdjustmentCommand,
} from "./cash.js";
import {
listArchivedInvestmentItemsCommand,
listArchivedInvestmentsCommand,
restoreArchivedInvestmentsCommand,
restoreCashAdjustmentCommand,
restoreTradeCommand,
permanentDeleteCashAdjustmentCommand,
permanentDeleteTradeCommand,
emptyArchivedInvestmentsCommand,
} from "./archive.js";

export const investmentsCommand = new Command("investments").description(
"Manage investment accounts, positions, trades, and catalog",
);

investmentsCommand.addCommand(listInstrumentsCommand);
investmentsCommand.addCommand(searchInstrumentsCommand);
investmentsCommand.addCommand(getInstrumentCommand);
investmentsCommand.addCommand(refreshInvestmentsCommand);
investmentsCommand.addCommand(investmentOverviewCommand);
investmentsCommand.addCommand(investmentPositionsCommand);
investmentsCommand.addCommand(investmentActivityCommand);
investmentsCommand.addCommand(investmentHistoryCommand);
investmentsCommand.addCommand(createTradeCommand);
investmentsCommand.addCommand(updateTradeCommand);
investmentsCommand.addCommand(deleteTradeCommand);
investmentsCommand.addCommand(createCashAdjustmentCommand);
investmentsCommand.addCommand(updateCashAdjustmentCommand);
investmentsCommand.addCommand(deleteCashAdjustmentCommand);
investmentsCommand.addCommand(listArchivedInvestmentsCommand);
investmentsCommand.addCommand(listArchivedInvestmentItemsCommand);
investmentsCommand.addCommand(restoreArchivedInvestmentsCommand);
investmentsCommand.addCommand(restoreTradeCommand);
investmentsCommand.addCommand(restoreCashAdjustmentCommand);
investmentsCommand.addCommand(permanentDeleteTradeCommand);
investmentsCommand.addCommand(permanentDeleteCashAdjustmentCommand);
investmentsCommand.addCommand(emptyArchivedInvestmentsCommand);
Loading