driggsby 0.7.6 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -117,9 +117,19 @@ counterparty's name, or tags on a set of transactions. `rules describe`
117
117
  prints the rule format and the process. `rules preview` shows exactly what a
118
118
  new or changed rule would do and returns the confirm token `rules save`
119
119
  needs to apply it; renaming, pausing, or resuming a rule needs its
120
- `rule_ref` and no preview. `rules delete` needs `--yes`. Every action prints JSON. If you
121
- signed in with an earlier version of the CLI, run
122
- `npx driggsby@latest login` once more to use rules.
120
+ `rule_ref` and no preview. `rules delete` needs `--yes`. Every action prints
121
+ JSON.
122
+
123
+ ## Items
124
+
125
+ `npx driggsby@latest items` saves and removes the items behind your charges:
126
+ what an order or a receipt actually bought, each in a category, so Driggsby
127
+ counts a charge by what was in it. `items describe` prints the item format
128
+ and the process. `items save --params-file order.json` saves the items of up
129
+ to 25 charges (saving a charge again replaces its items); each charge is
130
+ saved or refused on its own, and the JSON result says which and why.
131
+ `items delete` needs `--yes`. Read items back with
132
+ `npx driggsby@latest query query_item_sql --sql "..."`.
123
133
 
124
134
  ## License
125
135
 
@@ -0,0 +1,24 @@
1
+ // Argv parsing for `driggsby items <ACTION> [--params <JSON> | --params-file
2
+ // <PATH>] [--yes]`: the rules command's grammar (args-rules.ts) over the
3
+ // item tools.
4
+ import { parseActionCommand } from "./args-rules.js";
5
+ import {} from "./args-shared.js";
6
+ import { ITEMS_HELP } from "./help.js";
7
+ import { isItemAction, ITEM_ACTIONS } from "./items/item-actions.js";
8
+ const ITEMS_SPEC = {
9
+ name: "items",
10
+ article: "an",
11
+ usage: "Usage: npx driggsby@latest items <ACTION> [--params <JSON>] [--yes]",
12
+ help: ITEMS_HELP,
13
+ actions: ITEM_ACTIONS,
14
+ isAction: isItemAction,
15
+ describeAction: "describe",
16
+ deleteAction: "delete",
17
+ deleteNeedsYes: "error: removing a charge's items can't be undone — Spending counts it whole\n" +
18
+ "again. To remove them, run the same command again with --yes.",
19
+ exampleParams: `'{"transaction_refs":["txn_..."]}'`,
20
+ };
21
+ export function parseItems(argv) {
22
+ const parsed = parseActionCommand(argv, ITEMS_SPEC);
23
+ return parsed.kind === "action" ? { kind: "items", action: parsed.action, params: parsed.params, yes: parsed.yes } : parsed;
24
+ }
@@ -1,17 +1,19 @@
1
1
  // Argv parsing for `driggsby query <TOOL> [--sql <SQL>] [--params <JSON>]`.
2
2
  // Same behavior contract as the rest of the tree: help on -h/--help, usage
3
3
  // errors exit 2, echoed argv is sanitized. The tool is checked against the
4
- // allowlist here so a typo is a usage error naming the twelve, not a
4
+ // allowlist here so a typo is a usage error naming the thirteen, not a
5
5
  // server round trip.
6
6
  import { helpCommand, unexpectedArgument } from "./args-shared.js";
7
7
  import { didYouMean } from "./clap-suggestions.js";
8
8
  import { CliError } from "./cli-error.js";
9
9
  import { APP_TOOL_ALLOWLIST } from "./dev/tool-allowlist.js";
10
10
  import { QUERY_HELP } from "./help.js";
11
+ import { ITEM_ACTION_TOOLS } from "./items/item-actions.js";
12
+ import { RULE_ACTION_TOOLS } from "./rules/rule-actions.js";
11
13
  import { quotedForTerminal, wrapNames } from "./terminal-text.js";
12
- // The two tools that take SQL: --sql applies only to these, and a retry
14
+ // The tools that take SQL: --sql applies only to these, and a retry
13
15
  // line names --sql only for them.
14
- export const SQL_TOOLS = new Set(["query_cash_sql", "query_investment_sql"]);
16
+ export const SQL_TOOLS = new Set(["query_cash_sql", "query_investment_sql", "query_item_sql"]);
15
17
  const QUERY_USAGE = "Usage: npx driggsby@latest query <TOOL> [--sql <SQL>] [--params <JSON>]";
16
18
  export function parseQuery(argv) {
17
19
  let tool = null;
@@ -106,15 +108,31 @@ function invalidParams() {
106
108
  function missingTool() {
107
109
  return new CliError(`error: the following required arguments were not provided:\n <TOOL>\n\n${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
108
110
  }
111
+ // A tool with a command of its own (the items and rules actions): its
112
+ // refusal names that command rather than a read tool that looks alike.
113
+ function commandFor(tool) {
114
+ const commands = [["items", ITEM_ACTION_TOOLS], ["rules", RULE_ACTION_TOOLS]];
115
+ for (const [command, tools] of commands) {
116
+ const action = Object.keys(tools).find((name) => tools[name] === tool);
117
+ if (action !== undefined)
118
+ return `npx driggsby@latest ${command} ${action}`;
119
+ }
120
+ return undefined;
121
+ }
109
122
  function unknownTool(tool) {
110
- const similar = didYouMean(tool, [...APP_TOOL_ALLOWLIST]);
111
- const tip = similar === undefined ? "" : ` tip: a similar tool exists: '${similar}'\n\n`;
123
+ const command = commandFor(tool);
124
+ const similar = command === undefined ? didYouMean(tool, [...APP_TOOL_ALLOWLIST]) : undefined;
125
+ const tip = command !== undefined
126
+ ? ` tip: run ${tool} with its own command:\n ${command}\n\n`
127
+ : similar === undefined
128
+ ? ""
129
+ : ` tip: a similar tool exists: '${similar}'\n\n`;
112
130
  return new CliError(`error: ${quotedForTerminal(tool, 60)} isn't a tool a Driggsby app can call.\n` +
113
131
  `Apps can watch these read-only tools:\n` +
114
132
  `${wrapNames([...APP_TOOL_ALLOWLIST], " ")}\n\n${tip}${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
115
133
  }
116
134
  function sqlForNonSqlTool(tool) {
117
- return new CliError(`error: '--sql' only applies to query_cash_sql or query_investment_sql;\n` +
135
+ return new CliError(`error: '--sql' only applies to query_cash_sql, query_investment_sql, or query_item_sql;\n` +
118
136
  `'${tool}' takes no SQL.\n\n` +
119
137
  `${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
120
138
  }
@@ -12,11 +12,28 @@ import { RULES_HELP } from "./help.js";
12
12
  import { isRuleAction, RULE_ACTIONS } from "./rules/rule-actions.js";
13
13
  import { quotedForTerminal } from "./terminal-text.js";
14
14
  const RULES_USAGE = "Usage: npx driggsby@latest rules <ACTION> [--params <JSON>] [--yes]";
15
+ const RULES_SPEC = {
16
+ name: "rules",
17
+ article: "a",
18
+ usage: RULES_USAGE,
19
+ help: RULES_HELP,
20
+ actions: RULE_ACTIONS,
21
+ isAction: isRuleAction,
22
+ describeAction: "describe",
23
+ deleteAction: "delete",
24
+ deleteNeedsYes: "error: deleting a rule can't be undone — its categories and tags come off every\n" +
25
+ "transaction it covers. To delete it, run the same command again with --yes.",
26
+ exampleParams: `'{"rule_ref":"rule_..."}'`,
27
+ };
15
28
  // A saved rule with learned patterns and its answers is a few KB; this
16
29
  // bounds a mistaken path to something like a log file.
17
30
  export const MAX_PARAMS_FILE_BYTES = 1_048_576;
18
31
  const MAX_PARAMS_FILE_MB = String(MAX_PARAMS_FILE_BYTES / 1_048_576);
19
32
  export function parseRules(argv) {
33
+ const parsed = parseActionCommand(argv, RULES_SPEC);
34
+ return parsed.kind === "action" ? { kind: "rules", action: parsed.action, params: parsed.params, yes: parsed.yes } : parsed;
35
+ }
36
+ export function parseActionCommand(argv, spec) {
20
37
  let actionToken = null;
21
38
  let source = null;
22
39
  let yes = false;
@@ -28,24 +45,24 @@ export function parseRules(argv) {
28
45
  continue;
29
46
  }
30
47
  if (!optionsEnded && (token === "-h" || token === "--help")) {
31
- return helpCommand(RULES_HELP);
48
+ return helpCommand(spec.help);
32
49
  }
33
50
  if (!optionsEnded && token === "--yes") {
34
51
  yes = true;
35
52
  continue;
36
53
  }
37
54
  if (!optionsEnded && token.startsWith("--yes=")) {
38
- throw unexpectedFlagValue("--yes", token.slice("--yes=".length), RULES_USAGE);
55
+ throw unexpectedFlagValue("--yes", token.slice("--yes=".length), spec.usage);
39
56
  }
40
57
  const flag = !optionsEnded ? paramsFlag(token) : null;
41
58
  if (flag !== null) {
42
59
  if (source !== null) {
43
- throw usageError("error: give the params once, with either --params or --params-file");
60
+ throw usageError("error: give the params once, with either --params or --params-file", spec);
44
61
  }
45
62
  const attached = token.length > flag.length;
46
63
  const value = attached ? token.slice(flag.length + 1) : argv[index + 1];
47
64
  if (value === undefined || value === "" || (!attached && value.length > 1 && value.startsWith("-"))) {
48
- throw usageError(`error: a value is required for '${flag}' but none was supplied`);
65
+ throw usageError(`error: a value is required for '${flag}' but none was supplied`, spec);
49
66
  }
50
67
  source = flag === "--params" ? { flag, json: value } : { flag, path: value };
51
68
  index += attached ? 0 : 1;
@@ -55,26 +72,25 @@ export function parseRules(argv) {
55
72
  actionToken = token;
56
73
  continue;
57
74
  }
58
- throw unexpectedArgument(token, RULES_USAGE);
75
+ throw unexpectedArgument(token, spec.usage);
59
76
  }
60
77
  if (actionToken === null) {
61
- throw usageError("error: the following required arguments were not provided:\n <ACTION>");
78
+ throw usageError("error: the following required arguments were not provided:\n <ACTION>", spec);
62
79
  }
63
- if (!isRuleAction(actionToken)) {
64
- throw unknownAction(actionToken);
80
+ if (!spec.isAction(actionToken)) {
81
+ throw unknownAction(actionToken, spec);
65
82
  }
66
83
  const action = actionToken;
67
- if (action === "describe" && source !== null) {
68
- throw usageError("error: rules describe takes no params");
84
+ if (action === spec.describeAction && source !== null) {
85
+ throw usageError(`error: ${spec.name} ${spec.describeAction} takes no params`, spec);
69
86
  }
70
- if (action === "delete" && !yes) {
71
- throw usageError("error: deleting a rule can't be undone — its categories and tags come off every\n" +
72
- "transaction it covers. To delete it, run the same command again with --yes.");
87
+ if (action === spec.deleteAction && !yes) {
88
+ throw usageError(spec.deleteNeedsYes, spec);
73
89
  }
74
- if (action !== "delete" && yes) {
75
- throw usageError("error: '--yes' only applies to rules delete");
90
+ if (action !== spec.deleteAction && yes) {
91
+ throw usageError(`error: '--yes' only applies to ${spec.name} ${spec.deleteAction}`, spec);
76
92
  }
77
- return { kind: "rules", action, params: source === null ? {} : resolveParams(source), yes };
93
+ return { kind: "action", action, params: source === null ? {} : resolveParams(source, spec), yes };
78
94
  }
79
95
  // "--params", "--params=…", "--params-file", "--params-file=…" → the flag.
80
96
  function paramsFlag(token) {
@@ -85,13 +101,13 @@ function paramsFlag(token) {
85
101
  }
86
102
  return null;
87
103
  }
88
- function resolveParams(source) {
104
+ function resolveParams(source, spec) {
89
105
  if (source.flag === "--params") {
90
- return parseParamsObject(source.json, "--params", "\nFor JSON that's hard to quote in this shell, save it to a file and pass\n--params-file <PATH> instead.");
106
+ return parseParamsObject(source.json, "--params", "\nFor JSON that's hard to quote in this shell, save it to a file and pass\n--params-file <PATH> instead.", spec);
91
107
  }
92
- return parseParamsObject(readParamsFile(source.path), "--params-file", "");
108
+ return parseParamsObject(readParamsFile(source.path, spec), "--params-file", "", spec);
93
109
  }
94
- function parseParamsObject(rawValue, flag, hint) {
110
+ function parseParamsObject(rawValue, flag, hint, spec) {
95
111
  let parsed;
96
112
  try {
97
113
  parsed = JSON.parse(rawValue);
@@ -100,7 +116,7 @@ function parseParamsObject(rawValue, flag, hint) {
100
116
  parsed = null;
101
117
  }
102
118
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
103
- throw usageError(`error: invalid value for '${flag}':\n'${flag}' must be a JSON object, like '{"rule_ref":"rule_..."}'${hint}`);
119
+ throw usageError(`error: invalid value for '${flag}':\n'${flag}' must be a JSON object, like ${spec.exampleParams}${hint}`, spec);
104
120
  }
105
121
  return parsed;
106
122
  }
@@ -110,7 +126,7 @@ function parseParamsObject(rawValue, flag, hint) {
110
126
  // read). Special files are refused before they are opened, since opening a
111
127
  // FIFO blocks; on POSIX the open is also non-blocking, so a FIFO swapped in
112
128
  // after the check is refused by the fstat instead of hanging.
113
- function readParamsFile(path) {
129
+ function readParamsFile(path, spec) {
114
130
  let bytes;
115
131
  try {
116
132
  if (!statSync(path).isFile()) {
@@ -119,10 +135,10 @@ function readParamsFile(path) {
119
135
  bytes = readAtMost(path, MAX_PARAMS_FILE_BYTES + 1);
120
136
  }
121
137
  catch (error) {
122
- throw usageError(`error: couldn't read the --params-file: ${paramsFileProblem(error)}`);
138
+ throw usageError(`error: couldn't read the --params-file: ${paramsFileProblem(error)}`, spec);
123
139
  }
124
140
  if (bytes.length > MAX_PARAMS_FILE_BYTES) {
125
- throw usageError(`error: couldn't read the --params-file: it's over ${MAX_PARAMS_FILE_MB} MB`);
141
+ throw usageError(`error: couldn't read the --params-file: it's over ${MAX_PARAMS_FILE_MB} MB`, spec);
126
142
  }
127
143
  return decodeParamsFile(bytes);
128
144
  }
@@ -172,11 +188,11 @@ function decodeParamsFile(bytes) {
172
188
  const text = bytes.toString("utf8");
173
189
  return text.startsWith("\uFEFF") ? text.slice(1) : text;
174
190
  }
175
- function unknownAction(token) {
176
- const similar = didYouMean(token, RULE_ACTIONS);
191
+ function unknownAction(token, spec) {
192
+ const similar = didYouMean(token, spec.actions);
177
193
  const tip = similar === undefined ? "" : `\n\n tip: a similar action exists: '${similar}'`;
178
- return usageError(`error: ${quotedForTerminal(token, 60)} isn't a rules action. Actions: ${RULE_ACTIONS.join(", ")}.${tip}`);
194
+ return usageError(`error: ${quotedForTerminal(token, 60)} isn't ${spec.article} ${spec.name} action. Actions: ${spec.actions.join(", ")}.${tip}`, spec);
179
195
  }
180
- function usageError(firstLines) {
181
- return new CliError(`${firstLines}\n\n${RULES_USAGE}\n\nFor more information, try '--help'.`, 2);
196
+ function usageError(firstLines, spec) {
197
+ return new CliError(`${firstLines}\n\n${spec.usage}\n\nFor more information, try '--help'.`, 2);
182
198
  }
@@ -4,6 +4,7 @@
4
4
  // terminal through an echoed argument, tip, or usage line.
5
5
  import { CliError } from "./cli-error.js";
6
6
  import {} from "./mcp-setup/known-client.js";
7
+ import {} from "./items/item-actions.js";
7
8
  import {} from "./rules/rule-actions.js";
8
9
  import { sanitizeForTerminal } from "./terminal-text.js";
9
10
  export function helpCommand(text) {
package/dist/args.js CHANGED
@@ -13,6 +13,7 @@ import { parseInit } from "./args-init.js";
13
13
  import { parseLogin } from "./args-login.js";
14
14
  import { parseMcpSetup } from "./args-mcp-setup.js";
15
15
  import { parseQuery } from "./args-query.js";
16
+ import { parseItems } from "./args-items.js";
16
17
  import { parseRules } from "./args-rules.js";
17
18
  import { removeDashesTip, similarArgumentTip, unexpectedArgument, unexpectedFlagValue, } from "./args-shared.js";
18
19
  import { didYouMean } from "./clap-suggestions.js";
@@ -25,7 +26,7 @@ const MCP_USAGE = "Usage: npx driggsby@latest mcp <COMMAND>";
25
26
  const ROOT_LEVEL = {
26
27
  helpText: ROOT_HELP,
27
28
  usage: ROOT_USAGE,
28
- subcommands: ["mcp", "login", "logout", "init", "dev", "deploy", "rollback", "versions", "delete", "query", "rules"],
29
+ subcommands: ["mcp", "login", "logout", "init", "dev", "deploy", "rollback", "versions", "delete", "query", "rules", "items"],
29
30
  longFlags: ["help", "version"],
30
31
  subcommandFlags: [
31
32
  { name: "mcp", longFlags: ["help"] },
@@ -39,6 +40,7 @@ const ROOT_LEVEL = {
39
40
  { name: "delete", longFlags: ["yes", "help"] },
40
41
  { name: "query", longFlags: ["sql", "params", "help"] },
41
42
  { name: "rules", longFlags: ["params", "params-file", "yes", "help"] },
43
+ { name: "items", longFlags: ["params", "params-file", "yes", "help"] },
42
44
  ],
43
45
  usagePrefix: "npx driggsby@latest",
44
46
  hasVersion: true,
@@ -110,6 +112,8 @@ function dispatchSubcommand(name, rest) {
110
112
  return parseQuery(rest);
111
113
  case "rules":
112
114
  return parseRules(rest);
115
+ case "items":
116
+ return parseItems(rest);
113
117
  case "setup":
114
118
  return parseMcpSetup(rest);
115
119
  default:
@@ -13,6 +13,7 @@ export const APP_TOOL_ALLOWLIST = new Set([
13
13
  "list_recurring_transactions",
14
14
  "query_cash_sql",
15
15
  "query_investment_sql",
16
+ "query_item_sql",
16
17
  "search_cash_transactions",
17
18
  "search_investment_activity",
18
19
  ]);
package/dist/help.js CHANGED
@@ -22,6 +22,7 @@ Commands:
22
22
  delete Permanently remove a deployed app.
23
23
  query Run one Driggsby data tool and print its result as JSON.
24
24
  rules Create, change, and delete your transaction rules.
25
+ items Save and remove the items behind your charges.
25
26
 
26
27
  Options:
27
28
  -h, --help Print help
@@ -38,6 +39,7 @@ Examples:
38
39
  npx driggsby@latest deploy
39
40
  npx driggsby@latest query get_overview
40
41
  npx driggsby@latest rules describe
42
+ npx driggsby@latest items describe
41
43
  `;
42
44
  export const INIT_HELP = `Create a new Driggsby app in a new folder.
43
45
 
@@ -61,8 +63,8 @@ export const LOGIN_HELP = `Sign in to Driggsby and save an app token on this mac
61
63
 
62
64
  This opens a Driggsby approval page in your browser. Approving it issues an
63
65
  app token that lets this machine read your Driggsby data, deploy Driggsby
64
- apps, and manage your transaction rules. The token is saved locally for
65
- other driggsby commands and is never printed.
66
+ apps, and manage your transaction rules and items. The token is saved
67
+ locally for other driggsby commands and is never printed.
66
68
 
67
69
  If you approve in a browser on another computer, the page shows a code
68
70
  instead. Paste it where login is waiting, or finish with --code.
@@ -180,8 +182,8 @@ Arguments:
180
182
  ${QUERY_TOOL_LINES}
181
183
 
182
184
  Options:
183
- --sql <SQL> The SQL for query_cash_sql or query_investment_sql.
184
- Overrides a sql key given in --params.
185
+ --sql <SQL> The SQL for query_cash_sql, query_investment_sql, or
186
+ query_item_sql. Overrides a sql key given in --params.
185
187
  --params <JSON> Other params as a JSON object, for example
186
188
  '{"history_type":"liabilities"}'.
187
189
  -h, --help Print help
@@ -230,6 +232,43 @@ Examples:
230
232
  npx driggsby@latest rules tags
231
233
  npx driggsby@latest rules save --params-file answers.json
232
234
  `;
235
+ export const ITEMS_HELP = `Save and remove the items behind your charges.
236
+
237
+ An itemized charge carries what it bought: an order's or a receipt's items,
238
+ each in a category, so Driggsby counts the charge by what was in it. Each
239
+ action runs one Driggsby item tool and prints its result as JSON; the JSON
240
+ goes to stdout and nothing else does. Start with describe: it prints the
241
+ item format and the process. Read items back with
242
+ npx driggsby@latest query query_item_sql --sql <SQL>.
243
+
244
+ Sign in first with npx driggsby@latest login.
245
+
246
+ Usage: npx driggsby@latest items <ACTION> [--params <JSON>] [--yes]
247
+
248
+ Actions:
249
+ describe Print the item format and the process.
250
+ save Save the items of up to 25 charges. Saving a charge again
251
+ replaces its items.
252
+ delete Remove the items of charges by transaction_ref. There is no
253
+ undo; needs --yes.
254
+
255
+ Options:
256
+ --params <JSON> The action's params as a JSON object, like
257
+ '{"transaction_refs":["txn_..."]}'.
258
+ --params-file <PATH> Read the params object from a JSON file instead.
259
+ --yes Confirm a delete.
260
+ -h, --help Print help
261
+
262
+ Each charge in a save is saved or refused on its own (items that don't add
263
+ up to the charge, say): the JSON result reports each one, with the reason
264
+ and the fix, and the command exits 0. Only a failure of the whole call puts
265
+ its message on stderr and exits 1.
266
+
267
+ Examples:
268
+ npx driggsby@latest items describe
269
+ npx driggsby@latest items save --params-file order.json
270
+ npx driggsby@latest items delete --params-file refs.json --yes
271
+ `;
233
272
  export const MCP_HELP = `Set up Driggsby MCP clients.
234
273
 
235
274
  Usage: npx driggsby@latest mcp <COMMAND>
@@ -111,6 +111,8 @@ export const APP_JS = `// Your app's code. Edit anything — driggsby dev reload
111
111
  // list_recurring_transactions subscriptions, bills, and paychecks
112
112
  // query_cash_sql SQL over cash transactions ({ sql: "..." })
113
113
  // query_investment_sql SQL over investment activity ({ sql: "..." })
114
+ // query_item_sql SQL over the items in itemized charges
115
+ // ({ sql: "..." })
114
116
  // search_cash_transactions bank and card transaction search
115
117
  // search_investment_activity trades, dividends, and transfers
116
118
  //
@@ -0,0 +1,11 @@
1
+ // The items command's actions. Each maps to exactly one Driggsby item tool
2
+ // (the same tools the Driggsby MCP server offers); describe reads those
3
+ // tools' own descriptions instead of calling one.
4
+ export const ITEM_ACTION_TOOLS = {
5
+ save: "save_transaction_items",
6
+ delete: "delete_transaction_items",
7
+ };
8
+ export const ITEM_ACTIONS = ["describe", "save", "delete"];
9
+ export function isItemAction(value) {
10
+ return ITEM_ACTIONS.includes(value);
11
+ }
@@ -0,0 +1,97 @@
1
+ // driggsby items: save and remove the items behind the signed-in person's
2
+ // charges (what an order or a receipt actually bought). Each action is one
3
+ // call to a Driggsby item tool (the same tools the Driggsby MCP server
4
+ // offers); describe prints those tools' own descriptions, so the item format
5
+ // is never copied into this CLI. stdout carries only JSON (the result, or a
6
+ // refusal's own details) so an agent can pipe or save it; every message is a
7
+ // CliError on stderr.
8
+ import { apiBaseUrl } from "../api/base-url.js";
9
+ import { CliError } from "../cli-error.js";
10
+ import { defaultCredentialEnvironment } from "../credentials/store.js";
11
+ import { deployFailure, requireDeploySession } from "../deploy/api-session.js";
12
+ import { callRuleTool, listTools } from "../rules/rules-rpc.js";
13
+ import { terminalSafeJson } from "../terminal-text.js";
14
+ import { ITEM_ACTION_TOOLS, ITEM_ACTIONS } from "./item-actions.js";
15
+ // The item tools take an optional short reason; when the caller gives none,
16
+ // this names the CLI.
17
+ export const ITEMS_REASON = "Itemizing charges from the driggsby CLI.";
18
+ export const ITEMS_NOT_AVAILABLE_MESSAGE = "This sign-in can't save items. Sign in again and approve the request:\n" +
19
+ " npx driggsby@latest login\n" +
20
+ "If items still aren't available after that, they aren't open to your account yet.";
21
+ function defaultItemsIo() {
22
+ return {
23
+ out: (text) => {
24
+ process.stdout.write(text);
25
+ },
26
+ };
27
+ }
28
+ export async function runItems(options, environment = defaultCredentialEnvironment(), io = defaultItemsIo()) {
29
+ const { action, params } = options;
30
+ if (action === "delete" && options.yes !== true) {
31
+ throw new CliError("Removing a charge's items can't be undone, so it needs --yes.", 2);
32
+ }
33
+ const baseUrl = apiBaseUrl(environment.env);
34
+ const retryCommand = retryCommandFor(options);
35
+ try {
36
+ const session = await requireDeploySession(environment);
37
+ if (action === "describe") {
38
+ const tools = await listTools(session, new Set(Object.values(ITEM_ACTION_TOOLS)), ITEMS_NOT_AVAILABLE_MESSAGE);
39
+ io.out(`${terminalSafeJson(describePayload(tools))}\n`);
40
+ return 0;
41
+ }
42
+ const outcome = await callRuleTool(session, ITEM_ACTION_TOOLS[action], withReason(params));
43
+ if (outcome.kind === "ok") {
44
+ io.out(`${terminalSafeJson(outcome.result)}\n`);
45
+ return 0;
46
+ }
47
+ if (outcome.kind === "refused") {
48
+ io.out(`${terminalSafeJson(outcome.details)}\n`);
49
+ }
50
+ throw new CliError(outcome.message, 1);
51
+ }
52
+ catch (error) {
53
+ throw deployFailure(error, baseUrl, retryCommand);
54
+ }
55
+ }
56
+ // Each action's flags, keyed by action so a new action can't be left out.
57
+ const ACTION_FLAGS = {
58
+ save: " --params <JSON>",
59
+ delete: " --params <JSON> --yes",
60
+ };
61
+ // What describe prints: the tools' own descriptions, plus the CLI command
62
+ // that runs each tool the descriptions name.
63
+ export function describePayload(tools) {
64
+ const howToRun = {};
65
+ for (const action of ITEM_ACTIONS) {
66
+ if (action !== "describe") {
67
+ howToRun[ITEM_ACTION_TOOLS[action]] = `npx driggsby@latest items ${action}${ACTION_FLAGS[action]}`;
68
+ }
69
+ }
70
+ // The two read tools the item descriptions send an agent to: finding the
71
+ // charges, and reading their items back.
72
+ howToRun.search_cash_transactions = "npx driggsby@latest query search_cash_transactions --params <JSON>";
73
+ howToRun.query_item_sql = "npx driggsby@latest query query_item_sql --sql <SQL>";
74
+ return {
75
+ how_to_run: howToRun,
76
+ notes: [
77
+ "--params is the tool's arguments object as JSON; --params-file <PATH> reads the same object from a file.",
78
+ "The CLI fills in reason when you leave it out.",
79
+ ],
80
+ tools,
81
+ };
82
+ }
83
+ function withReason(params) {
84
+ const reason = params.reason;
85
+ if (typeof reason === "string" && reason.trim() !== "") {
86
+ return params;
87
+ }
88
+ return { ...params, reason: ITEMS_REASON };
89
+ }
90
+ // The command to run again after a network blip. Params are never rebuilt
91
+ // into a shell line (no quoting is right for every shell), so they are named
92
+ // in a plain sentence under the command instead. A retried save is safe:
93
+ // saving a charge again replaces its items rather than adding to them.
94
+ function retryCommandFor(options) {
95
+ const command = `npx driggsby@latest items ${options.action}${options.action === "delete" ? " --yes" : ""}`;
96
+ return Object.keys(options.params).length === 0 ? command : `${command}\n with the same params as before`;
97
+ }
package/dist/main.js CHANGED
@@ -11,6 +11,7 @@ import { runInit } from "./init/init-command.js";
11
11
  import { runRollback } from "./deploy/rollback-command.js";
12
12
  import { runVersions } from "./deploy/versions-command.js";
13
13
  import { runQuery } from "./query/query-command.js";
14
+ import { runItems } from "./items/items-command.js";
14
15
  import { runRules } from "./rules/rules-command.js";
15
16
  import { runLogin, runLoginWithCode } from "./login/login.js";
16
17
  import { runLogout } from "./login/logout.js";
@@ -51,6 +52,8 @@ async function run(argv) {
51
52
  return await runQuery({ tool: command.tool, params: command.params });
52
53
  case "rules":
53
54
  return await runRules({ action: command.action, params: command.params, yes: command.yes });
55
+ case "items":
56
+ return await runItems({ action: command.action, params: command.params, yes: command.yes });
54
57
  }
55
58
  }
56
59
  try {
@@ -1,4 +1,4 @@
1
- // The rules command's channel to Driggsby: one JSON-RPC POST to /mcp with
1
+ // The rules and items commands' channel to Driggsby: one JSON-RPC POST to /mcp with
2
2
  // the saved sign-in. Unlike the dev preview's broker, a tool refusal keeps
3
3
  // its structured details (a blocking suggestion and its options, the
4
4
  // overlapping rule), because an agent needs them to answer and save again.
@@ -29,13 +29,17 @@ export async function callRuleTool(session, tool, toolArguments) {
29
29
  // this sign-in sees. None listed means the sign-in predates rules or rules
30
30
  // aren't open to the account; either way the next step is the same.
31
31
  export async function listRuleTools(session) {
32
+ return listTools(session, new Set(Object.values(RULE_ACTION_TOOLS)), RULES_NOT_AVAILABLE_MESSAGE);
33
+ }
34
+ // The wanted tools' own descriptions from this sign-in's tools/list (the
35
+ // rules and items commands each describe their own tools this way).
36
+ export async function listTools(session, wanted, notAvailableMessage) {
32
37
  const payload = await postMcp(session, { jsonrpc: "2.0", id: 1, method: "tools/list" });
33
38
  const tools = asRecord(asRecord(payload)?.result)?.tools;
34
39
  if (!Array.isArray(tools)) {
35
40
  throw new CliError(GENERIC_TOOL_TROUBLE, 1);
36
41
  }
37
42
  const listed = tools;
38
- const wanted = new Set(Object.values(RULE_ACTION_TOOLS));
39
43
  const found = listed.flatMap((tool) => {
40
44
  const record = asRecord(tool);
41
45
  if (record === null ||
@@ -54,7 +58,7 @@ export async function listRuleTools(session) {
54
58
  ];
55
59
  });
56
60
  if (found.length === 0) {
57
- throw new CliError(RULES_NOT_AVAILABLE_MESSAGE, 1);
61
+ throw new CliError(notAvailableMessage, 1);
58
62
  }
59
63
  return found;
60
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driggsby",
3
- "version": "0.7.6",
3
+ "version": "0.8.1",
4
4
  "description": "Set up and build on Driggsby, the MCP server for personal finance.",
5
5
  "license": "Apache-2.0 AND OFL-1.1",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "node": ">=18"
31
31
  },
32
32
  "dependencies": {
33
- "@driggsby/deploy": "0.7.6",
34
- "@driggsby/sdk": "0.7.6"
33
+ "@driggsby/deploy": "0.8.1",
34
+ "@driggsby/sdk": "0.8.1"
35
35
  }
36
36
  }