driggsby 0.7.6 → 0.8.0
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 +13 -3
- package/dist/args-items.js +24 -0
- package/dist/args-query.js +4 -4
- package/dist/args-rules.js +45 -29
- package/dist/args-shared.js +1 -0
- package/dist/args.js +5 -1
- package/dist/dev/tool-allowlist.js +1 -0
- package/dist/help.js +43 -4
- package/dist/init/templates.js +2 -0
- package/dist/items/item-actions.js +11 -0
- package/dist/items/items-command.js +97 -0
- package/dist/main.js +3 -0
- package/dist/rules/rules-rpc.js +7 -3
- package/package.json +3 -3
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
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}
|
package/dist/args-query.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
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";
|
|
@@ -9,9 +9,9 @@ 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
11
|
import { quotedForTerminal, wrapNames } from "./terminal-text.js";
|
|
12
|
-
// The
|
|
12
|
+
// The tools that take SQL: --sql applies only to these, and a retry
|
|
13
13
|
// line names --sql only for them.
|
|
14
|
-
export const SQL_TOOLS = new Set(["query_cash_sql", "query_investment_sql"]);
|
|
14
|
+
export const SQL_TOOLS = new Set(["query_cash_sql", "query_investment_sql", "query_item_sql"]);
|
|
15
15
|
const QUERY_USAGE = "Usage: npx driggsby@latest query <TOOL> [--sql <SQL>] [--params <JSON>]";
|
|
16
16
|
export function parseQuery(argv) {
|
|
17
17
|
let tool = null;
|
|
@@ -114,7 +114,7 @@ function unknownTool(tool) {
|
|
|
114
114
|
`${wrapNames([...APP_TOOL_ALLOWLIST], " ")}\n\n${tip}${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
|
|
115
115
|
}
|
|
116
116
|
function sqlForNonSqlTool(tool) {
|
|
117
|
-
return new CliError(`error: '--sql' only applies to query_cash_sql or
|
|
117
|
+
return new CliError(`error: '--sql' only applies to query_cash_sql, query_investment_sql, or query_item_sql;\n` +
|
|
118
118
|
`'${tool}' takes no SQL.\n\n` +
|
|
119
119
|
`${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
|
|
120
120
|
}
|
package/dist/args-rules.js
CHANGED
|
@@ -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(
|
|
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),
|
|
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,
|
|
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 (!
|
|
64
|
-
throw unknownAction(actionToken);
|
|
80
|
+
if (!spec.isAction(actionToken)) {
|
|
81
|
+
throw unknownAction(actionToken, spec);
|
|
65
82
|
}
|
|
66
83
|
const action = actionToken;
|
|
67
|
-
if (action ===
|
|
68
|
-
throw usageError(
|
|
84
|
+
if (action === spec.describeAction && source !== null) {
|
|
85
|
+
throw usageError(`error: ${spec.name} ${spec.describeAction} takes no params`, spec);
|
|
69
86
|
}
|
|
70
|
-
if (action ===
|
|
71
|
-
throw usageError(
|
|
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 !==
|
|
75
|
-
throw usageError(
|
|
90
|
+
if (action !== spec.deleteAction && yes) {
|
|
91
|
+
throw usageError(`error: '--yes' only applies to ${spec.name} ${spec.deleteAction}`, spec);
|
|
76
92
|
}
|
|
77
|
-
return { kind: "
|
|
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
|
|
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,
|
|
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
|
|
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${
|
|
196
|
+
function usageError(firstLines, spec) {
|
|
197
|
+
return new CliError(`${firstLines}\n\n${spec.usage}\n\nFor more information, try '--help'.`, 2);
|
|
182
198
|
}
|
package/dist/args-shared.js
CHANGED
|
@@ -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:
|
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
|
|
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
|
|
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>
|
package/dist/init/templates.js
CHANGED
|
@@ -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 {
|
package/dist/rules/rules-rpc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The rules
|
|
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(
|
|
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.
|
|
3
|
+
"version": "0.8.0",
|
|
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.
|
|
34
|
-
"@driggsby/sdk": "0.
|
|
33
|
+
"@driggsby/deploy": "0.8.0",
|
|
34
|
+
"@driggsby/sdk": "0.8.0"
|
|
35
35
|
}
|
|
36
36
|
}
|