driggsby 0.9.0 → 0.9.2

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.
@@ -3,7 +3,7 @@
3
3
  // answer its rare question, and undo it; and driggsby accounts list. Built for agents with
4
4
  // no Driggsby context: the file (or the ID Driggsby gives it) is the only
5
5
  // handle, repeating any command is safe, `--wait` never outlasts a coding
6
- // agent's command limit, and every output ends with the exact next command
6
+ // agent's command limit, and every output ends with the exact next step
7
7
  // (import-render.ts holds that contract).
8
8
  import { apiBaseUrl } from "../api/base-url.js";
9
9
  import { CliError } from "../cli-error.js";
@@ -117,7 +117,8 @@ async function runStart(session, options, io) {
117
117
  }
118
118
  return renderStatus(view, {
119
119
  compact: options.wait && startedIn === "categorizing",
120
- repeated: target.kind === "file" && !created && given === null,
120
+ // Found already this far: a wait that watched it land shows it in full.
121
+ repeated: target.kind === "file" && !created && given === null && view.state === startedIn,
121
122
  unchanged: outcome.note !== null,
122
123
  rerun: { file: typedFile, account: options.account, create: options.create },
123
124
  }, context(io));
@@ -1,8 +1,9 @@
1
1
  // What the import command prints. Every output follows one contract, so an
2
2
  // agent with no Driggsby context never has to guess: the first line says
3
3
  // where things stand ("Started:", "Waiting for an answer:", "Done:", ...)
4
- // and is never wrapped, and the last lines give the exact command to run
5
- // next, complete, with one command per answer when the person must choose.
4
+ // and is never wrapped, and the last lines give the exact next step: a
5
+ // complete command wherever one exists, one per answer when the person must
6
+ // choose, and otherwise what to do (email support at the limit).
6
7
  // Names Driggsby sends (a file, an account) print quoted and capped, so they
7
8
  // read as names and never as the CLI's own sentence.
8
9
  import { formatBytes } from "@driggsby/deploy";
@@ -60,7 +61,9 @@ function statusBody(view, options, context) {
60
61
  case "waiting":
61
62
  return ok(renderQuestion(view));
62
63
  case "categorizing":
63
- return ok(options.compact ? categorizingLine(view) : landed(view, "Imported"));
64
+ if (options.compact)
65
+ return ok(categorizingLine(view));
66
+ return ok(options.repeated ? alreadyImported(view) : landed(view, "Imported"));
64
67
  case "done":
65
68
  return ok(done(view, options, context));
66
69
  case "stopped":
@@ -78,11 +81,15 @@ function undoingLine(view) {
78
81
  : `Undoing: removing the transactions ${name(view.file)} added.`;
79
82
  }
80
83
  // Importing the file again, aimed where the first try was; a stop about the
81
- // account starts from the account list instead.
84
+ // account starts from the account list instead, and one at the limit from
85
+ // support, since running anything again would stop the same way.
82
86
  function rerun(view, options) {
83
87
  if (ACCOUNT_STOPS.has(view.stopped?.code ?? "")) {
84
88
  return `Next, to see your accounts and their IDs:\n ${CLI} accounts list\n`;
85
89
  }
90
+ if (view.stopped?.code === "limit_reached") {
91
+ return next("Email support@driggsby.com to keep importing.");
92
+ }
86
93
  const flags = accountFlags(options.rerun.account, options.rerun.create);
87
94
  if (options.rerun.file !== null && flags !== "") {
88
95
  return next(`${CLI} import ${options.rerun.file}${flags}`);
@@ -136,10 +143,15 @@ export function renderQuestion(view) {
136
143
  export function choiceLines(choices, command) {
137
144
  return `${choices.map((choice) => ` ${name(choice.label)}\n ${command(choice)}`).join("\n")}\n`;
138
145
  }
146
+ // One transaction "is", any other count "are".
147
+ function be(count) {
148
+ return count === 1 ? "is" : "are";
149
+ }
139
150
  // The transactions are in: what landed, then what's left to do.
140
151
  function landed(view, headline) {
141
- const where = view.account === null ? " landed" : ` are in ${name(view.account)}`;
142
- const top = `${headline}: ${plural(view.counts?.new ?? 0, "transaction")} from ${name(view.file)}${where}.`;
152
+ const count = view.counts?.new ?? 0;
153
+ const where = view.account === null ? " landed" : ` ${be(count)} in ${name(view.account)}`;
154
+ const top = `${headline}: ${plural(count, "transaction")} from ${name(view.file)}${where}.`;
143
155
  const undoCommand = ` ${CLI} import undo ${view.ref}\n`;
144
156
  // A new account is the person's to name by its ID from here on.
145
157
  const again = view.newAccount && view.accountRef !== null ? `Next time, for this account:\n ${CLI} import <FILE> --account ${view.accountRef}\n\n` : "";
@@ -182,6 +194,14 @@ function progressText(view) {
182
194
  const left = progress.secondsLeft === null ? "" : `, ${timeLeft(progress.secondsLeft)}`;
183
195
  return `${progress.categorized.toLocaleString("en-US")} of ${progress.total.toLocaleString("en-US")} so far${left}`;
184
196
  }
197
+ // The same file sent again while categories fill in: it's already in, and
198
+ // this run imported nothing, so it never reads as a second import.
199
+ function alreadyImported(view) {
200
+ const count = view.counts?.new ?? 0;
201
+ const into = view.account === null ? " landed" : ` ${be(count)} in ${name(view.account)}`;
202
+ return (`Already imported: this file's ${plural(count, "transaction")}${into} (${view.ref}). Categories: ${progressText(view)}.\n\n` +
203
+ `${wrapProse("Nothing else to do unless you need the categories. To wait for them:")}\n ${waitCommand(view.ref)}\n`);
204
+ }
185
205
  function categorizingLine(view) {
186
206
  const progress = view.progress;
187
207
  const where = progress === null || progress.categorized === 0
@@ -194,14 +214,15 @@ function done(view, options, context) {
194
214
  const why = NOTHING_NEW[view.nothingNew] ?? "none of its transactions were new";
195
215
  return `Done: nothing new in ${name(view.file)}: ${why}.\n\nNothing else to do.\n`;
196
216
  }
217
+ const count = view.counts?.new ?? 0;
197
218
  // A wait that followed categorizing ends on how it went, even for a file
198
219
  // sent again.
199
220
  if (options.compact && view.categorized === false) {
200
- return (`Done: ${plural(view.counts?.new ?? 0, "transaction")} from ${name(view.file)} are in, but some couldn't be categorized.\n\n` +
221
+ return (`Done: ${plural(count, "transaction")} from ${name(view.file)} ${be(count)} in, but some couldn't be categorized.\n\n` +
201
222
  `${wrapProse("Those show as uncategorized. Nothing else to do: the import is finished.")}\n`);
202
223
  }
203
224
  if (options.compact) {
204
- return `Done: all ${plural(view.counts?.new ?? 0, "transaction")} from ${name(view.file)} are categorized.\n\nNothing else to do: the import is finished.\n`;
225
+ return `Done: ${count === 1 ? "the" : "all"} ${plural(count, "transaction")} from ${name(view.file)} ${be(count)} categorized.\n\nNothing else to do: the import is finished.\n`;
205
226
  }
206
227
  if (options.repeated) {
207
228
  const on = view.finishedAt === null ? "" : ` on ${shortDate(view.finishedAt, context)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driggsby",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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.9.0",
34
- "@driggsby/sdk": "0.9.0"
33
+ "@driggsby/deploy": "0.9.2",
34
+ "@driggsby/sdk": "0.9.2"
35
35
  }
36
36
  }