@wirecat/cli-core 0.344.0 → 0.345.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/cli/bot/command.js +3 -1
  3. package/dist/cli/command-contract.d.ts +13 -0
  4. package/dist/cli/command-contract.js +1 -1
  5. package/dist/cli/context.d.ts +2 -0
  6. package/dist/cli/guide.d.ts +52 -0
  7. package/dist/cli/guide.js +307 -0
  8. package/dist/cli/help/command-help.d.ts +30 -0
  9. package/dist/cli/help/command-help.js +218 -0
  10. package/dist/cli/help/commands-text.d.ts +2 -0
  11. package/dist/cli/help/commands-text.js +1 -1
  12. package/dist/cli/help/first-screen.d.ts +4 -3
  13. package/dist/cli/help/first-screen.js +6 -3
  14. package/dist/cli/help/sections.js +1 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +2 -0
  17. package/dist/cli/messenger/conversations-command.js +4 -1
  18. package/dist/cli/messenger/inbox.js +1 -1
  19. package/dist/cli/messenger/observed.js +15 -9
  20. package/dist/cli/messenger/private-people-command.js +10 -1
  21. package/dist/cli/messenger/review.js +1 -1
  22. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  23. package/dist/cli/messenger/store-maintenance-command.js +8 -5
  24. package/dist/cli/profile.js +1 -1
  25. package/dist/cli/program.js +2 -2
  26. package/dist/meetings/mcp/index.d.ts +15 -15
  27. package/dist/services/index.d.ts +3 -0
  28. package/dist/services/index.js +1 -0
  29. package/dist/services/project-gathering-request.d.ts +120 -0
  30. package/dist/services/project-gathering-request.js +217 -0
  31. package/dist/services/project-gathering.d.ts +89 -0
  32. package/dist/services/project-gathering.js +202 -0
  33. package/package.json +5 -4
  34. package/parity.json +2 -2
@@ -0,0 +1,218 @@
1
+ import { metaOf } from "@wirecat/cli-base/commands";
2
+ import { writes } from "@wirecat/cli-base/standards";
3
+ import { commandPathOf, outputOf } from "../command-contract.js";
4
+ import { commandAt } from "../steps.js";
5
+ import { short } from "./commands-text.js";
6
+ /** Argument names onto the kinds `domain/references.ts` names; a plural takes the same kind. */
7
+ const ARGUMENT_KINDS = {
8
+ chat: "chat",
9
+ chats: "chat",
10
+ message: "message",
11
+ messages: "message",
12
+ post: "message",
13
+ person: "person",
14
+ people: "person",
15
+ task: "task",
16
+ meeting: "meeting",
17
+ folder: "folder",
18
+ folders: "folder",
19
+ topic: "topic",
20
+ note: "note",
21
+ document: "document",
22
+ model: "model",
23
+ };
24
+ /** What a command answers with, by its declared entity, onto the same kinds. */
25
+ const ENTITY_KINDS = {
26
+ message: "message",
27
+ message_link: "message",
28
+ chat: "chat",
29
+ contact: "person",
30
+ member: "person",
31
+ person: "person",
32
+ task: "task",
33
+ meeting: "meeting",
34
+ meeting_details: "meeting",
35
+ meeting_hit: "meeting",
36
+ meeting_summary: "meeting",
37
+ folder: "folder",
38
+ topic: "topic",
39
+ note: "note",
40
+ document: "document",
41
+ model: "model",
42
+ };
43
+ /** The group a kind lives in, whose commands come first among the ones that take it. */
44
+ const HOME = {
45
+ chat: "chats",
46
+ message: "messages",
47
+ person: "contacts",
48
+ task: "tasks",
49
+ meeting: "meetings",
50
+ folder: "folders",
51
+ topic: "topics",
52
+ note: "notes",
53
+ document: "documents",
54
+ model: "models",
55
+ };
56
+ const SAMPLES = {
57
+ chat: "Team",
58
+ message: "1234",
59
+ post: "1234",
60
+ person: "Anna",
61
+ people: "Anna",
62
+ task: "12",
63
+ meeting: "123",
64
+ folder: "Work",
65
+ topic: "General",
66
+ model: "default",
67
+ query: '"invoice"',
68
+ text: '"on my way"',
69
+ title: '"Weekly sync"',
70
+ name: "daily",
71
+ id: "1",
72
+ file: "./report.pdf",
73
+ link: "https://example.com/join/abc",
74
+ url: "https://example.com",
75
+ since: "1d",
76
+ duration: "1d",
77
+ limit: "20",
78
+ };
79
+ const MAX_NEXT = 3;
80
+ const choices = new WeakMap();
81
+ /** Overrides the "Next usually" a command's help derives from types. Kept off `annotate`, like its section. */
82
+ export const nextUsually = (command, choice) => {
83
+ choices.set(command, choice);
84
+ return command;
85
+ };
86
+ const kindOfArgument = (argument) => ARGUMENT_KINDS[argument.name()];
87
+ const leaves = (command) => command.commands
88
+ .filter((child) => !child._hidden)
89
+ .flatMap((child) => (child.commands.length === 0 ? [child] : leaves(child)));
90
+ const rootOf = (command) => {
91
+ let at = command;
92
+ while (at.parent)
93
+ at = at.parent;
94
+ return at;
95
+ };
96
+ const call = (command) => [
97
+ rootOf(command).name(),
98
+ ...commandPathOf(command),
99
+ ...command.registeredArguments.map((one) => (one.required ? `<${one.name()}>` : `[${one.name()}]`)),
100
+ ].join(" ");
101
+ const returnsOf = (command) => {
102
+ const output = metaOf(command).output;
103
+ const { item, coverage } = outputOf(command);
104
+ if (!output || coverage !== "declared-entity")
105
+ return short(command.summary() || command.description()) || "not declared";
106
+ const required = item.required ?? [];
107
+ const fields = [
108
+ ...required,
109
+ ...Object.keys(item.properties ?? {}).filter((name) => !required.includes(name)),
110
+ ];
111
+ const noun = (output.view ? `${output.entity} ${output.view}` : output.entity).replaceAll("_", " ");
112
+ const more = fields.length > 8 ? `, and ${fields.length - 8} more` : "";
113
+ const root = rootOf(command);
114
+ const schema = commandAt(root, "commands schema")
115
+ ? `; every field: ${root.name()} commands schema ${commandPathOf(command).join(" ")}`
116
+ : "";
117
+ return `${noun} — ${fields.slice(0, 8).join(", ")}${more}${schema}`;
118
+ };
119
+ const nextOf = (command) => {
120
+ const root = rootOf(command);
121
+ const choice = choices.get(command) ?? {};
122
+ const drop = new Set(choice.drop ?? []);
123
+ const pinned = (choice.pin ?? []).flatMap((path) => commandAt(root, path) ?? []);
124
+ const output = metaOf(command).output;
125
+ const kind = output ? ENTITY_KINDS[output.entity] : undefined;
126
+ const all = leaves(root);
127
+ const derived = kind
128
+ ? all
129
+ .filter((other) => other !== command && !pinned.includes(other))
130
+ .filter((other) => other.registeredArguments.some((argument) => kindOfArgument(argument) === kind))
131
+ .map((other, order) => ({ other, order }))
132
+ .sort((a, b) => {
133
+ const rank = (other) => [
134
+ Number(writes(metaOf(other).effects)),
135
+ Number(!other.registeredArguments.some((one) => one.required && kindOfArgument(one) === kind)),
136
+ Number(commandPathOf(other)[0] !== HOME[kind]),
137
+ Number(other.parent !== command.parent),
138
+ ];
139
+ const [left, right] = [rank(a.other), rank(b.other)];
140
+ return left.reduce((found, value, at) => found || value - (right[at] ?? 0), 0) || a.order - b.order;
141
+ })
142
+ .map(({ other }) => other)
143
+ : [];
144
+ return [...pinned, ...derived].filter((other) => !drop.has(commandPathOf(other).join(" "))).slice(0, MAX_NEXT);
145
+ };
146
+ const sample = (name) => SAMPLES[name] ?? `<${name}>`;
147
+ const generatedExample = (command) => {
148
+ const { query } = metaOf(command);
149
+ const words = command.registeredArguments.flatMap((argument) => {
150
+ const name = argument.name();
151
+ // A list's `[query...]` is its field filters, not text; a search's is the text.
152
+ if (name === "query" && query)
153
+ return [];
154
+ if (!argument.required && SAMPLES[name] === undefined)
155
+ return [];
156
+ return [argument.argChoices?.[0] ?? sample(name)];
157
+ });
158
+ const options = command.options
159
+ .filter((option) => option.mandatory)
160
+ .flatMap((option) => {
161
+ const flag = option.long ?? option.short ?? option.flags;
162
+ const placeholder = /[<[]([^>\]]+)[>\]]/.exec(option.flags)?.[1];
163
+ if (!(option.required || option.optional))
164
+ return [flag];
165
+ return [flag, option.argChoices?.[0] ?? sample(placeholder ?? option.attributeName())];
166
+ });
167
+ // No profile word, as on the first screen and in written examples: the Usage line already shows `<profile>`.
168
+ return [rootOf(command).name(), ...commandPathOf(command), ...words, ...options].join(" ");
169
+ };
170
+ /** Returns, Next usually and Example for one command, from its definition alone. */
171
+ export const commandHelp = (command) => {
172
+ const written = metaOf(command).examples ?? [];
173
+ return {
174
+ returns: returnsOf(command),
175
+ next: nextOf(command).map(call),
176
+ examples: written.length > 0 ? written : [generatedExample(command)],
177
+ origin: written.length > 0 ? "written" : "generated",
178
+ };
179
+ };
180
+ const LABEL = 15;
181
+ /** The lines `--help` prints after a command's options. */
182
+ export const commandHelpText = (command) => {
183
+ const { returns, next, examples } = commandHelp(command);
184
+ const rows = (label, values) => values.map((value, at) => `${(at === 0 ? label : "").padEnd(LABEL)}${value}`);
185
+ return ["", ...rows("Returns:", [returns]), ...rows("Next usually:", next), ...rows("Example:", examples), ""].join("\n");
186
+ };
187
+ /** The same, as Markdown lines for `docs/commands.md`: hand it to cli-base's `commandsPage` per command. */
188
+ export const commandHelpMarkdown = (root, path) => {
189
+ const command = commandAt(root, path.join(" "));
190
+ if (!command || command.commands.length > 0)
191
+ return [];
192
+ const { returns, next, examples } = commandHelp(command);
193
+ const code = (values) => values.map((value) => `\`${value}\``).join(", ");
194
+ return [
195
+ `**Returns:** ${returns}`,
196
+ ...(next.length > 0 ? [`**Next usually:** ${code(next)}`] : []),
197
+ `**Example:** ${code(examples)}`,
198
+ ];
199
+ };
200
+ /** Puts Returns, Next usually and Example on every leaf command's `--help`, worked out when the help is shown. */
201
+ export const installCommandHelp = (program) => {
202
+ for (const command of leaves(program))
203
+ command.addHelpText("after", () => commandHelpText(command));
204
+ };
205
+ /** Which leaf commands still show a generated example, so the important ones get a written one. Report only. */
206
+ export const exampleReport = (program) => {
207
+ const written = [];
208
+ const generated = [];
209
+ for (const command of leaves(program))
210
+ (commandHelp(command).origin === "written" ? written : generated).push(commandPathOf(command).join(" "));
211
+ const total = written.length + generated.length;
212
+ const text = [
213
+ `${program.name()}: ${written.length} of ${total} commands have a written example; ${generated.length} show a generated one:`,
214
+ ...generated.map((path) => ` ${program.name()} ${path}`),
215
+ "",
216
+ ].join("\n");
217
+ return { written, generated, text };
218
+ };
@@ -1,5 +1,7 @@
1
1
  import type { CommandInfo } from "@wirecat/cli-base/commands";
2
2
  import type { Command } from "commander";
3
+ /** The first clause that fits: a line per command is the point, the whole text is in `--help`. */
4
+ export declare const short: (text: string) => string;
3
5
  /**
4
6
  * `<tool> commands` for a person or an agent reading text: every command that runs, one line each, under its
5
7
  * section. `scope` is the group asked for (`commands messages`), whose commands are all listed.
@@ -4,7 +4,7 @@ import { SECTIONS, sectionOf } from "./sections.js";
4
4
  const DESCRIPTION = 90;
5
5
  const COLUMN = 52;
6
6
  /** The first clause that fits: a line per command is the point, the whole text is in `--help`. */
7
- const short = (text) => {
7
+ export const short = (text) => {
8
8
  const line = text.replace(/\s+/g, " ").trim();
9
9
  if (line.length <= DESCRIPTION)
10
10
  return line;
@@ -28,9 +28,10 @@ export declare const WIRECAT_TOOLS: readonly Tool[];
28
28
  */
29
29
  export declare const firstScreen: (program: Command, screen?: FirstScreen) => string;
30
30
  /**
31
- * Puts the first screen on `--help`, a bare run and `help`; `help options` prints the options for every command and
32
- * `--help-all` the full commander help. `help` and `--help-all` are hidden, so `commands --json` and MCP do not
33
- * change. For a tool that builds its own program; `createProgram` calls it for tg, max and wa.
31
+ * Puts the first screen on `--help`, a bare run and `help`, and Returns, Next usually and Example on every leaf
32
+ * command's `--help`. `help options` prints the options for every command, `--help-all` the full commander help;
33
+ * both are hidden, so `commands --json` and MCP do not change. For a tool that builds its own program;
34
+ * `createProgram` calls it for tg, max and wa.
34
35
  */
35
36
  export declare const installHelp: (program: Command, screen?: FirstScreen) => Command;
36
37
  export {};
@@ -4,6 +4,7 @@ import { Command, Help, Option } from "commander";
4
4
  import wrapAnsi from "wrap-ansi";
5
5
  import { NO_EFFECTS } from "../effects.js";
6
6
  import { commandAt } from "../steps.js";
7
+ import { installCommandHelp } from "./command-help.js";
7
8
  /** Every WireCat tool, as "Works with" names it; a tool's own entry is left out of its screen. */
8
9
  export const WIRECAT_TOOLS = [
9
10
  { command: "tg", does: "Telegram in the same local store: search, review and tasks offline" },
@@ -67,9 +68,10 @@ const optionsScreen = (program) => {
67
68
  ].join("\n");
68
69
  };
69
70
  /**
70
- * Puts the first screen on `--help`, a bare run and `help`; `help options` prints the options for every command and
71
- * `--help-all` the full commander help. `help` and `--help-all` are hidden, so `commands --json` and MCP do not
72
- * change. For a tool that builds its own program; `createProgram` calls it for tg, max and wa.
71
+ * Puts the first screen on `--help`, a bare run and `help`, and Returns, Next usually and Example on every leaf
72
+ * command's `--help`. `help options` prints the options for every command, `--help-all` the full commander help;
73
+ * both are hidden, so `commands --json` and MCP do not change. For a tool that builds its own program;
74
+ * `createProgram` calls it for tg, max and wa.
73
75
  */
74
76
  export const installHelp = (program, screen = {}) => {
75
77
  let full = false;
@@ -81,6 +83,7 @@ export const installHelp = (program, screen = {}) => {
81
83
  ? firstScreen(program, screen)
82
84
  : (own.formatHelp ?? Help.prototype.formatHelp).call(helper, command, helper),
83
85
  });
86
+ installCommandHelp(program);
84
87
  program.addOption(new Option("--help-all", "the full help: every command and option").hideHelp());
85
88
  program.on("option:help-all", () => {
86
89
  full = true;
@@ -61,6 +61,7 @@ const CORE_SECTIONS = {
61
61
  complete: "setup",
62
62
  config: "setup",
63
63
  doctor: "setup",
64
+ guide: "setup",
64
65
  models: "setup",
65
66
  skill: "setup",
66
67
  store: "setup",
@@ -56,6 +56,8 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
56
56
  export { changesAuthority, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
57
57
  export { type AnswerOutput, answerSchema, type Body, type ItemBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
58
58
  export { withAbort } from "./execution.js";
59
+ export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, guideCommand, loadScenarios, type Scenario, } from "./guide.js";
60
+ export { type CommandHelp, commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, type NextChoice, nextUsually, } from "./help/command-help.js";
59
61
  export { type FirstScreen, type FirstScreenLine, firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
60
62
  export { type HelpSection, inSection, SECTIONS } from "./help/sections.js";
61
63
  export { bufferedInput, type InputPolicy, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
package/dist/cli/index.js CHANGED
@@ -51,6 +51,8 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
51
51
  export { changesAuthority, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
52
52
  export { answerSchema, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
53
53
  export { withAbort } from "./execution.js";
54
+ export { checkGuide, checkStep, guideCommand, loadScenarios, } from "./guide.js";
55
+ export { commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, nextUsually, } from "./help/command-help.js";
54
56
  export { firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
55
57
  export { inSection, SECTIONS } from "./help/sections.js";
56
58
  export { bufferedInput, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
@@ -335,7 +335,9 @@ export const conversationsCommand = (messenger) => {
335
335
  const consents = conversations
336
336
  .command("consents")
337
337
  .description("remembered analysis permissions for this account's chats and provider endpoints");
338
- annotate(consents.command("list"), { effects: LOCAL_READ }).action(async function () {
338
+ annotate(consents.command("list"), { effects: LOCAL_READ })
339
+ .description("which chats and provider endpoints you allowed to analyse, and when")
340
+ .action(async function () {
339
341
  const context = messengerContext(this, messenger);
340
342
  const items = await context.withStore((store, account) => analysisConsents(store, account).list());
341
343
  renderAnswer(this, context, listOf("conversations consents list", {
@@ -345,6 +347,7 @@ export const conversationsCommand = (messenger) => {
345
347
  }));
346
348
  });
347
349
  annotate(consents.command("revoke"), { effects: LOCAL_WRITE })
350
+ .description("forget analysis permissions so the next analysis asks again")
348
351
  .option("--chat <chat>", "revoke only this chat's consents; defaults to every chat")
349
352
  .option("--provider <identity>", "exact provider identity from consents list; defaults to every provider")
350
353
  .action(async function () {
@@ -30,7 +30,7 @@ export const PARTIAL_NOTE = "the messenger did not list every chat, and did not
30
30
  */
31
31
  export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
32
32
  effects: REMOTE_READ,
33
- output: { entity: "message", view: "message.summary" },
33
+ output: { entity: "message", view: "summary" },
34
34
  query: INBOX_QUERY,
35
35
  })
36
36
  .description("other people's unread messages in every chat; --new for what arrived since the last check")
@@ -32,8 +32,15 @@ export const observed = (messenger, events) => {
32
32
  throw error;
33
33
  }
34
34
  };
35
+ const counted = async (operation, onEvent, work) => {
36
+ const counts = {};
37
+ await timed(operation, {}, () => work((event) => {
38
+ counts[event.event] = (counts[event.event] ?? 0) + 1;
39
+ onEvent(event);
40
+ }), () => ({ counts }));
41
+ };
35
42
  const mint = messenger.newSendId;
36
- const { chats, history, contact, around } = messenger;
43
+ const { chats, history, contact, around, watch, listen } = messenger;
37
44
  // A method not listed here is timed under its own name, with no ids until a line here names them.
38
45
  return throughWrapper(messenger, {
39
46
  self: () => messenger.self(),
@@ -75,15 +82,14 @@ export const observed = (messenger, events) => {
75
82
  ...(options.threadId ? { thread: options.threadId } : {}),
76
83
  },
77
84
  }, () => messenger.send(chatId, text, options), (sent) => ({ ids: { message_id: sent.message.id } })),
78
- ...(messenger.watch
85
+ ...(watch
86
+ ? {
87
+ watch: (onEvent, signal, onReady) => counted("messages.watch", onEvent, (counting) => watch.call(messenger, counting, signal, onReady)),
88
+ }
89
+ : {}),
90
+ ...(listen
79
91
  ? {
80
- watch: async (onEvent, signal, onReady) => {
81
- const counts = {};
82
- await timed("messages.watch", {}, () => messenger.watch?.((event) => {
83
- counts[event.event] = (counts[event.event] ?? 0) + 1;
84
- onEvent(event);
85
- }, signal, onReady) ?? Promise.resolve(), () => ({ counts }));
86
- },
92
+ listen: (onEvent, signal) => counted("adapter.listen", onEvent, (counting) => listen.call(messenger, counting, signal)),
87
93
  }
88
94
  : {}),
89
95
  logout: () => timed("session.logout", {}, () => messenger.logout()),
@@ -15,6 +15,7 @@ const everyNote = (command, items) => listOf(command, { items, limit: Math.max(1
15
15
  export const privatePeopleCommands = (messenger) => {
16
16
  const alias = new Command("alias").description("a private local display name in the selected account");
17
17
  annotate(alias.command("set"), { effects: LOCAL_WRITE })
18
+ .description("your own name for a person in this account; kept on this machine, never sent")
18
19
  .argument("<person>")
19
20
  .argument("<alias>")
20
21
  .action(async function (person, value) {
@@ -23,6 +24,7 @@ export const privatePeopleCommands = (messenger) => {
23
24
  renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.privatePeople.alias(person, value)))));
24
25
  });
25
26
  annotate(alias.command("rm"), { effects: LOCAL_WRITE })
27
+ .description("drop your local name for a person; their own name shows again")
26
28
  .argument("<person>")
27
29
  .action(async function (person) {
28
30
  const context = messengerContext(this, messenger);
@@ -30,13 +32,16 @@ export const privatePeopleCommands = (messenger) => {
30
32
  renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.privatePeople.alias(person, null)))));
31
33
  });
32
34
  const notes = new Command("notes").description("your private notes on a stored contact, the same in every account that sees them");
33
- withQuery(annotate(notes.command("list"), { effects: LOCAL_READ }).argument("<person>"), NOTES_QUERY).action(async function (person, words) {
35
+ withQuery(annotate(notes.command("list"), { effects: LOCAL_READ })
36
+ .description("every private note you keep on a person, oldest first")
37
+ .argument("<person>"), NOTES_QUERY).action(async function (person, words) {
34
38
  const context = messengerContext(this, messenger);
35
39
  listQuery(typedQuery(words), NOTES_QUERY, "contacts notes list");
36
40
  const items = await context.withServices((services) => services.privatePeople.notes(person));
37
41
  renderAnswer(this, context, everyNote("contacts notes list", items));
38
42
  });
39
43
  annotate(notes.command("show"), { effects: LOCAL_READ, output: { entity: "contact_note" } })
44
+ .description("one private note on a person, with its revision for edit")
40
45
  .argument("<person>")
41
46
  .argument("<id>")
42
47
  .action(async function (person, id) {
@@ -48,6 +53,9 @@ export const privatePeopleCommands = (messenger) => {
48
53
  effects: LOCAL_WRITE,
49
54
  output: { entity: "contact_note" },
50
55
  }), { name: "text", description: "the note's text" })
56
+ .description(name === "add"
57
+ ? "add a private note on a person from stdin or --file; never sent"
58
+ : "replace a note's text; --revision must match the one you read")
51
59
  .argument("<person>")
52
60
  .addOption(ownerOnlyOption("--file <path>", "read note text from a file; omitted or - reads stdin"));
53
61
  if (name === "edit")
@@ -67,6 +75,7 @@ export const privatePeopleCommands = (messenger) => {
67
75
  });
68
76
  }
69
77
  annotate(notes.command("remove"), { effects: LOCAL_WRITE })
78
+ .description("delete one private note on a person")
70
79
  .argument("<person>")
71
80
  .argument("<id>")
72
81
  .action(async function (person, id) {
@@ -26,7 +26,7 @@ export const unansweredHours = (value, flag = "--unanswered") => value === true
26
26
  */
27
27
  export const reviewCommand = (messenger) => annotate(new Command("review"), {
28
28
  effects: REMOTE_READ,
29
- output: { entity: "message", view: "message.summary" },
29
+ output: { entity: "message", view: "summary" },
30
30
  query: REVIEW_QUERY,
31
31
  })
32
32
  .description("every message, yours too, in chats that changed since a point — for reviewing who owes what")
@@ -2,9 +2,9 @@ import { Command } from "commander";
2
2
  import type { Messenger } from "./context.js";
3
3
  /** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
4
4
  export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
5
- error?: undefined;
6
5
  path: string;
7
6
  exists: boolean;
7
+ error?: undefined;
8
8
  } | {
9
9
  path: string;
10
10
  exists: boolean;
@@ -569,8 +569,10 @@ const restoreFrom = async (command, messenger, backup, given) => {
569
569
  const schema = await backupSchema(backup);
570
570
  const stamp = stampNow();
571
571
  const kept = existsSync(path) ? `${path}.before-restore-${stamp}` : null;
572
- if (kept)
573
- await quiesce(path, messenger, "restore", (message) => renderer.warn(message));
572
+ if (kept) {
573
+ const waitMs = environmentOf(command).storeLockWaitMs;
574
+ await quiesce(path, messenger, "restore", (message) => renderer.warn(message), waitMs);
575
+ }
574
576
  else
575
577
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
576
578
  const incoming = `${path}.restoring-${stamp}`;
@@ -614,12 +616,13 @@ const backupSchema = async (backup) => {
614
616
  }
615
617
  return schema;
616
618
  };
619
+ const STORE_LOCK_WAIT_MS = 1_000;
617
620
  /**
618
621
  * No other process has the file open, no write is under way, and the write-ahead log is folded in,
619
622
  * so the file set aside is whole on its own. In WAL mode SQLite cannot say who has the file open —
620
623
  * `EXCLUSIVE` behaves as `IMMEDIATE` — so that is asked of the system.
621
624
  */
622
- const quiesce = async (path, messenger, then, warn) => {
625
+ const quiesce = async (path, messenger, then, warn, waitMs = STORE_LOCK_WAIT_MS) => {
623
626
  const { command } = messenger.app;
624
627
  const files = [path, `${path}-wal`, `${path}-shm`]
625
628
  .filter((file) => existsSync(file))
@@ -634,7 +637,7 @@ const quiesce = async (path, messenger, then, warn) => {
634
637
  }
635
638
  const database = await openCache(path);
636
639
  try {
637
- database.exec("PRAGMA busy_timeout = 1000");
640
+ database.exec(`PRAGMA busy_timeout = ${waitMs}`);
638
641
  database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
639
642
  try {
640
643
  database.exec("BEGIN IMMEDIATE");
@@ -673,7 +676,7 @@ const resetCommand = (messenger) => annotate(new Command("reset"), { effects: LO
673
676
  if (!/^\s*y(es)?\s*$/i.test(answer))
674
677
  throw new CliError("cancelled", "cancelled — the store is unchanged");
675
678
  }
676
- await quiesce(path, messenger, "reset", (message) => renderer.warn(message));
679
+ await quiesce(path, messenger, "reset", (message) => renderer.warn(message), environmentOf(this).storeLockWaitMs);
677
680
  const backup = keep ? `${path}.backup-${stampNow()}` : null;
678
681
  let backedUp = null;
679
682
  if (backup) {
@@ -15,7 +15,7 @@ export const usableProfileName = (profile) => {
15
15
  return profile;
16
16
  };
17
17
  /** The top-level commands that never read a profile; every other one is `<tool> <profile> …`. */
18
- export const PROFILE_FREE = new Set(["commands", "complete", "help", "skill", "upgrade"]);
18
+ export const PROFILE_FREE = new Set(["commands", "complete", "guide", "help", "skill", "upgrade"]);
19
19
  /**
20
20
  * Every word that means a command at the top level. Taken off the built program rather than
21
21
  * written out, because a list written out goes stale the day a command is added — and `help` is in
@@ -53,8 +53,8 @@ export const createProgram = ({ app, commands, configure, firstScreen }, { out,
53
53
  .option("--no-input", "never prompt or open interactive login; piped input remains available")
54
54
  .option("--max-input-bytes <bytes>", "maximum buffered input bytes (default: 16777216)")
55
55
  .option("--max-output-bytes <bytes>", "maximum machine output bytes (default: 4194304; 0 disables)")
56
- .option("--brief", "found messages and mail as reference, title, time, sender and excerpt only")
57
- .option("--full", "found messages and mail with their whole text and every provider field")
56
+ .option("--brief", "found messages as reference, title, time, sender and excerpt only")
57
+ .option("--full", "found messages with their whole text and every provider field")
58
58
  .option("--dry-run", "preview parsed arguments and permissions before running the action")
59
59
  .option("--yes", "go ahead without the question an ask level puts before a write")
60
60
  .option("--record", "keep this run — ids and timings, never message content")