@wirecat/cli-core 0.347.0 → 0.349.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 (76) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli/bot/copy.d.ts +1 -1
  3. package/dist/cli/bot/copy.js +1 -1
  4. package/dist/cli/embedding-choice.js +2 -1
  5. package/dist/cli/envelope.d.ts +4 -0
  6. package/dist/cli/envelope.js +4 -3
  7. package/dist/cli/guide.js +4 -0
  8. package/dist/cli/help/first-screen.js +13 -1
  9. package/dist/cli/help/sections.d.ts +4 -3
  10. package/dist/cli/help/sections.js +5 -10
  11. package/dist/cli/index.d.ts +1 -0
  12. package/dist/cli/index.js +1 -0
  13. package/dist/cli/messenger/backfill-command.js +6 -6
  14. package/dist/cli/messenger/context.d.ts +3 -3
  15. package/dist/cli/messenger/context.js +44 -17
  16. package/dist/cli/messenger/doctor-command.js +3 -3
  17. package/dist/cli/messenger/flooded.js +1 -1
  18. package/dist/cli/messenger/mcp-command.js +1 -1
  19. package/dist/cli/messenger/messages-delete-command.js +2 -1
  20. package/dist/cli/messenger/observed.js +24 -7
  21. package/dist/cli/messenger/schedule-command.js +4 -3
  22. package/dist/cli/messenger/serve-relay.js +1 -0
  23. package/dist/cli/messenger/store-maintenance-command.js +3 -3
  24. package/dist/cli/messenger/stored.js +36 -30
  25. package/dist/cli/program.js +10 -9
  26. package/dist/cli/runs/command.js +3 -2
  27. package/dist/cli/steps.d.ts +4 -0
  28. package/dist/cli/steps.js +9 -0
  29. package/dist/cli/store-settings.js +4 -3
  30. package/dist/context/evidence.d.ts +10 -0
  31. package/dist/domain/models.d.ts +11 -1
  32. package/dist/kit/contract.d.ts +2 -2
  33. package/dist/kit/contract.js +25 -0
  34. package/dist/kit/fake.d.ts +1 -0
  35. package/dist/kit/fake.js +15 -16
  36. package/dist/mcp/meeting-preparation.d.ts +1 -0
  37. package/dist/mcp/meeting-project-candidates.d.ts +1 -0
  38. package/dist/mcp/meeting-series-inventory.d.ts +1 -0
  39. package/dist/mcp/tool.js +2 -0
  40. package/dist/mcp/tools/messages-photo.js +2 -1
  41. package/dist/meetings/mcp/passage.d.ts +1 -0
  42. package/dist/meetings/mcp/project-evidence.d.ts +1 -0
  43. package/dist/meetings/mcp/search.d.ts +1 -0
  44. package/dist/meetings/reads.d.ts +1 -0
  45. package/dist/ports/messenger.d.ts +10 -2
  46. package/dist/runs/recording.js +1 -0
  47. package/dist/runs/scope.d.ts +7 -0
  48. package/dist/runs/scope.js +5 -0
  49. package/dist/services/archive.js +2 -1
  50. package/dist/services/chats.js +3 -2
  51. package/dist/services/context-evidence.d.ts +1 -1
  52. package/dist/services/conversations.js +2 -1
  53. package/dist/services/deps.js +2 -1
  54. package/dist/services/embeddings-maintenance.js +3 -2
  55. package/dist/services/index.d.ts +6 -4
  56. package/dist/services/index.js +1 -0
  57. package/dist/services/project-gathering-around.d.ts +37 -0
  58. package/dist/services/project-gathering-around.js +57 -0
  59. package/dist/services/project-gathering-request.d.ts +13 -1
  60. package/dist/services/project-gathering-request.js +23 -5
  61. package/dist/services/project-gathering.d.ts +15 -1
  62. package/dist/services/project-gathering.js +32 -1
  63. package/dist/services/search-all-meetings.d.ts +3 -4
  64. package/dist/services/search-all.d.ts +8 -1
  65. package/dist/services/search-all.js +1 -1
  66. package/dist/speech/transcribe.js +2 -1
  67. package/dist/store/chats.d.ts +2 -2
  68. package/dist/store/sqlite/chats.d.ts +2 -2
  69. package/dist/store/sqlite/chats.js +7 -7
  70. package/dist/store/sqlite/context-evidence.d.ts +4 -1
  71. package/dist/store/sqlite/context-evidence.js +12 -0
  72. package/dist/store/store.d.ts +2 -1
  73. package/dist/store/store.js +5 -1
  74. package/dist/store/types.d.ts +2 -2
  75. package/package.json +3 -3
  76. package/parity.json +2 -12
package/CHANGELOG.md CHANGED
@@ -7,6 +7,88 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.349.0 — 19.10.2026
11
+
12
+ ### Added
13
+
14
+ - **`gatherProjectEvidence` brings the mail and document index up to date before reading** through the new
15
+ optional `MessageStore.contextIndex` port (`ContextIndex.catchUp`), the same fills a mail search and the notes
16
+ search run. `index: "as-is"` skips it; the result's `index` says what happened. On a store just synced, new
17
+ mail and imported documents were missed before.
18
+ - **The project key is a search lane of its own** (`project.key`, lane kind `key`), and an explicit window may
19
+ give `after` alone: `before` is `asOf`, frozen with the run.
20
+ - **Each message hit of `gatherProjectEvidence` carries the messages next to it** (`around`, default 2 before and 2
21
+ after, at most 5) through the new `neighbours` port; `storedMessageNeighbours(store.messages)` builds it. They
22
+ are cited context, not matches, and their bytes count against `maxReadBytes`. A reply that names nothing was
23
+ out of reach of every search.
24
+ - **The default read budget covers one full round**: one read per search × source × 90-day chunk when that is more
25
+ than 48 (at most 512), so `--source all` with several searches no longer stops before every search ran once.
26
+ - **`ProjectFetchState`, `ProjectUnitState`, `ProjectIndexCatchUp` and `ContextIndex` are exported** from
27
+ `@wirecat/cli-core/services`, so a host can name the result type.
28
+
29
+ ### Changed — may break callers
30
+
31
+ - **`feed` and a one-shot command's passive listen keep one operation name in their run records and warnings**:
32
+ `history.feed` for `feed`, `messages.listen` for `listen` and `history.listen` for `listenHistory`, as
33
+ `messages.watch` does for `watch`. Before, the run record said `adapter.feed`, `adapter.listen` or
34
+ `adapter.listenHistory`, so `runs --operation adapter.feed` and `adapter.listen` stop matching new runs.
35
+ - **`parity.json` expects `--qr-file` on `session start` and `setup` in all three CLIs**: max has it since
36
+ max-cli#736, so the rows are `all` instead of planned for max.
37
+
38
+ ## 0.348.0 — 19.10.2026
39
+
40
+ ### Added
41
+
42
+ - **A next step in `--json` output and in an error carries `line`, the whole command with the profile**:
43
+ `"line": "max personal mcp config"` beside `"command": "mcp config"`. A copied `command` alone failed with "name a
44
+ profile". MCP answers leave it out; `command` is unchanged.
45
+ - **`searchAll(…, { app, hintCommand })`**: the tool a missing-model hint names, apart from `app.command`. memo passes
46
+ tg's identity for `group:` lists but has no `models` command; `hintCommand: null` names the tools that have one
47
+ (tg, max or wa) instead of `tg models text download`. `searchAllIncludingMeetings` and `searchAllWithMeetings`
48
+ take it too (`SearchAllMessenger`).
49
+ - **`commandsText(program, describeProgram(program), scope?)` from `./cli`**: the `<tool> commands` text list by
50
+ section, for a tool that builds its own commander program and keeps its own `commands` command (zm, drive, memo).
51
+ cli-core's groups land in their section on such a program too; before, only `createProgram` put them there.
52
+ - **A messenger whose history is in the store can save the history pushed during any command, not only `serve`
53
+ and `watch`.** `PassiveUpdates` may now also have `listenHistory(onBatch, signal)`, optional even in an adapter
54
+ typed as `PassiveUpdates`: it hands on the same `HistoryBatch`es as `feed`, re-keys included, from a connection that
55
+ does not watch, without connecting. A one-shot command and an MCP session then save them through `feed`'s own
56
+ code, and `close` stops it with `listen` (both within 5 s in all) before it waits for the saves. Without it, a
57
+ WhatsApp history push that came during such a command was acknowledged and then lost. An adapter without it
58
+ behaves as before. Its run record is an `adapter.listenHistory` request whose response counts `batches` and each
59
+ part's items (`rekeys`, `chats`, `messages`, …); a failure is a `listen_failed` warning for `history.listen`. The
60
+ contract kit checks it as it checks `listen` when the adapter has it, `OPTIONAL_METHODS` lists it, and
61
+ `fakeAdapter(seed, { listenHistory: true })` has it.
62
+
63
+ ### Changed — may break callers
64
+
65
+ - **Peer `@wirecat/cli-base` is `>=0.47.0 <0.48.0`** (was `>=0.45.0 <0.47.0`): 0.47.0 is the first whose strict
66
+ `nextStepSchema` accepts `line`, so a tool on an older cli-base would refuse cli-core's own answers in its schema
67
+ checks. Bump cli-base with cli-core.
68
+ - **Saving a chat keeps a `title`, `unreadCount`, `lastMessageAt` or `participantsCount` the update leaves out**, the
69
+ rule #1596 set for `providerMetadata` keys and `parentChatId`: left out keeps the stored value, `null` stores none,
70
+ a value replaces it. So a push that knows only part of a chat, such as WhatsApp's notice that a group left its
71
+ community, sends `{ id, kind, parentChatId: null }` and no longer erases the rest. `saveChats`, `Delta.chats` and
72
+ `HistoryBatch.chats` take the new `ChatUpdate` type, a `Chat` with those four fields optional; a full `Chat` still
73
+ fits, and every adapter today sends all four, so none has to change. To clear a field, send `null`, never leave it
74
+ out. Code that reads `HistoryBatch.chats` or `Delta.chats` now sees the four fields as possibly absent.
75
+
76
+ ### Fixed
77
+
78
+ - **The first screen names only the option topics the tool has.** `help options` was described as "output, offline,
79
+ timeouts, dry run" on every program; a tool with its own program and none of `--json`, `--offline`, `--timeout`,
80
+ `--dry-run` (memo, zm, drive) now reads "the options every command takes", and one with some names those.
81
+ - **A guide step may ask for help.** `checkGuide` refused `chat messages list --help` and `-h` ("no option --help")
82
+ and `chat messages --help` ("needs one of: …"); a step with the help flag now only needs its command path to exist.
83
+ - **A command's own help settings survive the `<profile>` usage line.** `createProgram` replaced each command's help
84
+ configuration when it set the usage line, so tg's `setup --help` lost its "Global Options" block
85
+ (`showGlobalOptions`). It now adds the usage line to what the command set.
86
+ - **Hints in errors and notes name the profile**: "the store is behind this build — `max personal store migrate`"
87
+ instead of `max store migrate`, which fails with "name a profile" when copied. Covers about 30 hints (store, runs,
88
+ schedules, sync, models, conversations, serve, MCP's photo and transcribe tools). `typedCommand(tool, words)`
89
+ (run scope) and `lineOf(command, words)` build them; a run's scope now carries its `profile`. Consumers' tests
90
+ that match these messages change on bump.
91
+
10
92
  ## 0.347.0 — 18.10.2026
11
93
 
12
94
  ### Added
@@ -9,7 +9,7 @@ import type { BotEvent } from "./port.js";
9
9
  export declare const botCopy: (provider: Provider) => {
10
10
  accountOf: (botId: string) => AccountKey;
11
11
  keep: (botId: string, messages: readonly Message[], via: IngestedVia, warn: (message: string) => void, senders?: readonly PersonFacts[]) => Promise<boolean>;
12
- /** Only a chat the messenger has just described in full: the store replaces a title it is not given. */
12
+ /** Only a chat the messenger has just described in full: the store takes its title and counts as given. */
13
13
  keepChat: (botId: string, chat: Chat, warn: (message: string) => void) => Promise<void>;
14
14
  /** Messages the bot deleted, kept as tombstones: reads and search stop returning them. */
15
15
  forget: (botId: string, chatId: string, messageIds: string[], warn: (message: string) => void) => Promise<boolean>;
@@ -57,7 +57,7 @@ export const botCopy = (provider) => {
57
57
  await store.people.savePeople(accountOf(botId), [...senders]);
58
58
  }, warn);
59
59
  },
60
- /** Only a chat the messenger has just described in full: the store replaces a title it is not given. */
60
+ /** Only a chat the messenger has just described in full: the store takes its title and counts as given. */
61
61
  keepChat: async (botId, chat, warn) => {
62
62
  if (CHAT_ID.test(chat.id))
63
63
  await quietly((store) => store.chats.saveChats(accountOf(botId), [chat]), warn);
@@ -4,6 +4,7 @@ import { embeddingKeys, endpointKeyName } from "../embeddings/keys.js";
4
4
  import { DEFAULT_TEXT_MODEL } from "../embeddings/models.js";
5
5
  import { isLocal, remoteModel } from "../embeddings/remote.js";
6
6
  import { readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
7
+ import { typedCommand } from "../runs/scope.js";
7
8
  /** A local model id, or a remote model with its key from `models text key set`. */
8
9
  export const embeddingChoice = ({ model, provider, baseUrl, dims, concurrency }, app, settings, env, { needKey = true, keyring } = {}) => {
9
10
  const flagsChooseProvider = (provider !== undefined && provider !== (settings.embeddingProvider ?? "local")) ||
@@ -30,7 +31,7 @@ export const embeddingChoice = ({ model, provider, baseUrl, dims, concurrency },
30
31
  const keyName = baseUrl === undefined ? "openai" : endpointKeyName(baseUrl);
31
32
  const key = embeddingKeys(app, env, keyring).read(keyName)?.key;
32
33
  if (!key && needKey && baseUrl === undefined) {
33
- throw new CliError("authentication_error", `no OpenAI key — \`${app.command} models text key set openai\`, or OPENAI_API_KEY`);
34
+ throw new CliError("authentication_error", `no OpenAI key — \`${typedCommand(app.command, "models text key set openai")}\`, or OPENAI_API_KEY`);
34
35
  }
35
36
  return { remote, ...(key ? { apiKey: key } : {}), ...(concurrency ? { concurrency } : {}) };
36
37
  };
@@ -44,6 +44,7 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
44
44
  readonly step: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>;
45
45
  readonly command: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
46
46
  readonly args: v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>;
47
+ readonly line: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
47
48
  readonly why: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
48
49
  readonly safe_to_auto_execute: v.BooleanSchema<undefined>;
49
50
  }, undefined>, v.StrictObjectSchema<{
@@ -58,6 +59,7 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
58
59
  args: {
59
60
  [x: string]: unknown;
60
61
  };
62
+ line?: string | undefined;
61
63
  why: string;
62
64
  safe_to_auto_execute: boolean;
63
65
  } | {
@@ -90,6 +92,7 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
90
92
  readonly step: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>;
91
93
  readonly command: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
92
94
  readonly args: v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>;
95
+ readonly line: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
93
96
  readonly why: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
94
97
  readonly safe_to_auto_execute: v.BooleanSchema<undefined>;
95
98
  }, undefined>, v.StrictObjectSchema<{
@@ -104,6 +107,7 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
104
107
  args: {
105
108
  [x: string]: unknown;
106
109
  };
110
+ line?: string | undefined;
107
111
  why: string;
108
112
  safe_to_auto_execute: boolean;
109
113
  } | {
@@ -3,9 +3,9 @@ import { metaOf } from "@wirecat/cli-base/commands";
3
3
  import { itemEnvelopeSchema, jsonlOf, listEnvelopeSchema, MAX_NEXT_STEPS, } from "@wirecat/cli-base/standards";
4
4
  import * as v from "valibot";
5
5
  import { currentRun } from "../runs/scope.js";
6
- import { setExitCode } from "./context.js";
6
+ import { environmentOf, setExitCode } from "./context.js";
7
7
  import { withoutProviderRefs } from "./provider-refs.js";
8
- import { nextStep, rootOf, runnable, stepLine, withSafety } from "./steps.js";
8
+ import { nextStep, rootOf, runnable, stepLine, withLines, withSafety } from "./steps.js";
9
9
  /** Every answer: a list or one thing, with items of `item` — any value where the command declares no entity. */
10
10
  export const answerSchema = (item = v.unknown()) => v.union([listEnvelopeSchema(item), itemEnvelopeSchema(item)]);
11
11
  const capped = (steps) => (steps.length > 0 ? { next: steps.slice(0, MAX_NEXT_STEPS) } : {});
@@ -131,7 +131,8 @@ export const renderAnswer = (command, { renderer, format, streams }, body, view)
131
131
  if (!isBody(body))
132
132
  refuse(body);
133
133
  const root = rootOf(command);
134
- const next = runnable(root, body.next);
134
+ const runnableSteps = runnable(root, body.next);
135
+ const next = environmentOf(command).agent ? runnableSteps : withLines(root, runnableSteps);
135
136
  const kept = withoutProviderRefs({ ...body, ...(next.length > 0 ? { next } : {}) });
136
137
  if (next.length === 0)
137
138
  delete kept.next;
package/dist/cli/guide.js CHANGED
@@ -211,6 +211,10 @@ export const checkStep = (program, run) => {
211
211
  }
212
212
  if (at === program)
213
213
  return [`no command "${rest[0] ?? ""}" — steps leave out the profile`];
214
+ const help = at._getHelpOption();
215
+ const beforeDashes = rest.slice(index, rest.includes("--") ? rest.indexOf("--") : undefined);
216
+ if (help && beforeDashes.some((word) => word === help.long || word === help.short))
217
+ return [];
214
218
  if (at.commands.length > 0)
215
219
  return [`needs one of: ${at.commands.map((one) => one.name()).join(", ")}`];
216
220
  for (const command of chain)
@@ -17,13 +17,25 @@ export const WIRECAT_TOOLS = [
17
17
  const WIDTH = 100;
18
18
  const widest = (texts) => texts.reduce((most, text) => Math.max(most, text.length), 0);
19
19
  const block = (title, rows, column) => rows.length === 0 ? [] : ["", `${title}:`, ...rows.map(([left, right]) => ` ${left.padEnd(column)}${right}`)];
20
+ const TOPICS = [
21
+ ["--json", "output"],
22
+ ["--offline", "offline"],
23
+ ["--timeout", "timeouts"],
24
+ ["--dry-run", "dry run"],
25
+ ];
26
+ /** Only the topics this program has options for: memo has no --offline. */
27
+ const optionTopics = (program) => {
28
+ const longs = new Set(program.options.map((option) => option.long));
29
+ const topics = TOPICS.filter(([long]) => longs.has(long)).map(([, topic]) => topic);
30
+ return topics.length > 0 ? topics.join(", ") : "the options every command takes";
31
+ };
20
32
  /** Only commands the tool has: `guide` and `setup` are not in every tool. */
21
33
  const pointers = (program, cli, start, profile) => {
22
34
  const has = (path) => commandAt(program, path) !== undefined;
23
35
  const next = [
24
36
  has("commands") ? [`${cli} commands`, "every command, by section"] : undefined,
25
37
  has("guide") ? [`${cli} guide`, "scenarios: command sequences for common tasks"] : undefined,
26
- [`${cli} help options`, "output, offline, timeouts, dry run"],
38
+ [`${cli} help options`, optionTopics(program)],
27
39
  ].filter((row) => row !== undefined);
28
40
  const rows = [
29
41
  ...(start ? [["Start here:", start, profile ? "(any profile name works)" : ""]] : []),
@@ -13,7 +13,8 @@ export type HelpSection = Exclude<keyof typeof SECTIONS, "other">;
13
13
  * fields of `commands --json`, which must not change.
14
14
  */
15
15
  export declare const inSection: <T extends Command>(command: T, section: HelpSection) => T;
16
- /** The nearest section on the command or above it; `other` when none is set. */
16
+ /**
17
+ * The nearest section on the command or above it, then cli-core's own for a top-level group of that name; `other`
18
+ * when neither is set. Looked up when listed, so a tool with its own program needs no install step.
19
+ */
17
20
  export declare const sectionOf: (command: Command) => keyof typeof SECTIONS;
18
- /** Puts each of cli-core's groups mounted on `program` in its section, unless the tool already chose one. */
19
- export declare const sectionCoreGroups: (program: Command) => void;
@@ -15,10 +15,13 @@ export const inSection = (command, section) => {
15
15
  sections.set(command, section);
16
16
  return command;
17
17
  };
18
- /** The nearest section on the command or above it; `other` when none is set. */
18
+ /**
19
+ * The nearest section on the command or above it, then cli-core's own for a top-level group of that name; `other`
20
+ * when neither is set. Looked up when listed, so a tool with its own program needs no install step.
21
+ */
19
22
  export const sectionOf = (command) => {
20
23
  for (let at = command; at; at = at.parent) {
21
- const section = sections.get(at);
24
+ const section = sections.get(at) ?? (at.parent && !at.parent.parent ? CORE_SECTIONS[at.name()] : undefined);
22
25
  if (section)
23
26
  return section;
24
27
  }
@@ -67,11 +70,3 @@ const CORE_SECTIONS = {
67
70
  store: "setup",
68
71
  upgrade: "setup",
69
72
  };
70
- /** Puts each of cli-core's groups mounted on `program` in its section, unless the tool already chose one. */
71
- export const sectionCoreGroups = (program) => {
72
- for (const command of program.commands) {
73
- const section = CORE_SECTIONS[command.name()];
74
- if (section && !sections.has(command))
75
- inSection(command, section);
76
- }
77
- };
@@ -58,6 +58,7 @@ export { type AnswerOutput, answerSchema, type Body, type ItemBody, itemAnswer,
58
58
  export { withAbort } from "./execution.js";
59
59
  export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, guideCommand, loadScenarios, type Scenario, } from "./guide.js";
60
60
  export { type CommandHelp, commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, type NextChoice, nextUsually, } from "./help/command-help.js";
61
+ export { commandsText } from "./help/commands-text.js";
61
62
  export { type FirstScreen, type FirstScreenLine, firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
62
63
  export { type HelpSection, inSection, SECTIONS } from "./help/sections.js";
63
64
  export { bufferedInput, type InputPolicy, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
package/dist/cli/index.js CHANGED
@@ -53,6 +53,7 @@ export { answerSchema, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, m
53
53
  export { withAbort } from "./execution.js";
54
54
  export { checkGuide, checkStep, guideCommand, loadScenarios, } from "./guide.js";
55
55
  export { commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, nextUsually, } from "./help/command-help.js";
56
+ export { commandsText } from "./help/commands-text.js";
56
57
  export { firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
57
58
  export { inSection, SECTIONS } from "./help/sections.js";
58
59
  export { bufferedInput, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
@@ -17,7 +17,7 @@ import { destroysNothing, FETCH, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
17
17
  import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
18
18
  import { listQuery, pagedList, pageOf, typedQuery, withQuery } from "../list-query.js";
19
19
  import { withPaging } from "../paging.js";
20
- import { nextStep } from "../steps.js";
20
+ import { lineOf, nextStep } from "../steps.js";
21
21
  import { finishJob, jobDetails, jobStarted, jobsDir, listJobs, readJob, removeJob, saveJob, spawnDetached, stateOf, tracedJob, updateJob, } from "./backfill-jobs.js";
22
22
  import { messengerContext, refuseLocalWrite } from "./context.js";
23
23
  import { reviewPoint } from "./mention-points.js";
@@ -60,7 +60,7 @@ export const syncCommand = (messenger) => {
60
60
  throw new CliError("validation_error", "--dry-run prices one chat; not with --all");
61
61
  if (all && messenger.history === "store") {
62
62
  throw new CliError("validation_error", `--all walks the messenger's chat list, and ${messenger.name ?? messenger.app.command} pushes its chats ` +
63
- `instead — keep \`${messenger.app.command} serve\` running${messenger.historyOnDemand ? ", or name one chat" : ""}`);
63
+ `instead — keep \`${lineOf(this, "serve")}\` running${messenger.historyOnDemand ? ", or name one chat" : ""}`);
64
64
  }
65
65
  if (since !== undefined && last !== undefined) {
66
66
  throw new CliError("validation_error", "give --since-time or --last, not both: how far back the fetch goes");
@@ -91,7 +91,7 @@ export const syncCommand = (messenger) => {
91
91
  const answer = await context.withServices((services) => services.archive.estimate(named, { limit, pageSize, pauseMs }));
92
92
  renderAnswer(this, context, itemOf(snakeKeys(answer)));
93
93
  if (answer.missing === null) {
94
- context.renderer.note(`nothing held of this chat to measure by — \`${messenger.app.command} messages sync ${chat} --limit ${pageSize}\` gives the estimate something to go on`);
94
+ context.renderer.note(`nothing held of this chat to measure by — \`${lineOf(this, `messages sync ${chat} --limit ${pageSize}`)}\` gives the estimate something to go on`);
95
95
  }
96
96
  else if (answer.missing > 0)
97
97
  context.renderer.note("an estimate: provider waits (FloodWait) come on top");
@@ -167,10 +167,10 @@ export const syncCommand = (messenger) => {
167
167
  }));
168
168
  if (tasks?.held)
169
169
  context.renderer.note(`the first sync left ${tasks.held} possible task(s) in ${tasks.heldChats ?? 0} chat(s) unopened ` +
170
- `(config task_first_pass_per_chat) — ask the owner, then \`${messenger.app.command} tasks scan <chat>\``);
170
+ `(config task_first_pass_per_chat) — ask the owner, then \`${lineOf(this, "tasks scan <chat>")}\``);
171
171
  if (toReview > 0)
172
172
  context.renderer.note(`${shown} mention${toReview === 1 ? "" : "s"} found since the last review — ` +
173
- `\`${messenger.app.command} mentions list --new\``);
173
+ `\`${lineOf(this, "mentions list --new")}\``);
174
174
  }
175
175
  catch (error) {
176
176
  if (jobId) {
@@ -327,7 +327,7 @@ const launchJob = (command, context, messenger, { chat, argv, limit, pageSize, l
327
327
  const dir = jobsDir(app, context.env);
328
328
  const running = listJobs(dir).find((job) => job.profile === context.profile && job.chat === chat && stateOf(job) === "running");
329
329
  if (running) {
330
- throw new CliError("validation_error", `job ${running.id} is already fetching ${chat} (PID ${running.pid}) — \`${app.command} store jobs show ${running.id}\``);
330
+ throw new CliError("validation_error", `job ${running.id} is already fetching ${chat} (PID ${running.pid}) — \`${app.command} ${context.profile} store jobs show ${running.id}\``);
331
331
  }
332
332
  const now = new Date();
333
333
  const id = `${now.toISOString().replace(/[-:]/g, "").slice(0, 15)}-${randomBytes(3).toString("hex")}`;
@@ -230,9 +230,9 @@ export interface MessengerContext extends BaseContext {
230
230
  * the reads answer saved to the store. `close` closes the connection and the store it opened. One
231
231
  * function, so a command and an MCP tool call save and record exactly alike.
232
232
  *
233
- * A `history: "store"` messenger with `listen` saves what arrives for as long as a connection that does not
234
- * `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again. A
235
- * connection opened with `listen: true` is left to its `watch`, which saves the same events.
233
+ * A `history: "store"` messenger with `listen` or `listenHistory` saves what arrives for as long as a connection that
234
+ * does not `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again.
235
+ * A connection opened with `listen: true` is left to its `watch` and `feed`, which save the same.
236
236
  */
237
237
  export declare const connected: (connection: MessengerAdapter, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, }: Pick<Messenger, "app" | "provider" | "name" | "deletedWithoutChat" | "pace" | "history" | "savedChatId">, { settings, env, renderer }: Pick<BaseContext, "settings" | "env" | "renderer">, events: EventSink, store?: LazyStore, { listen: watching }?: Pick<ConnectOptions, "listen">) => {
238
238
  adapter: MessengerAdapter;
@@ -114,24 +114,24 @@ const settled = async (pending, ms) => {
114
114
  }
115
115
  };
116
116
  /**
117
- * Runs the adapter's `listen` until `stop`, which answers whether it ended in time. A listen that fails warns and
117
+ * Runs one passive listen until `stop`, which answers whether it ended in time. A listen that fails warns and
118
118
  * never fails the command; one that fails once the connection is `closing` is the close, not news. It still listens
119
119
  * while the connection closes, so what arrives then is saved too.
120
120
  */
121
- const listened = (adapter, events, warn) => {
122
- if (!adapter.listen)
123
- return undefined;
121
+ const listened = ({ run, operation, what }, events, warn) => {
124
122
  const abort = new AbortController();
125
123
  let closing = false;
126
124
  const ended = Promise.resolve()
127
- .then(() => adapter.listen?.(() => { }, abort.signal))
125
+ .then(() => run(abort.signal))
128
126
  .catch((error) => {
129
127
  if (closing)
130
128
  return;
131
- events({ event: "warning", code: "listen_failed", operation: "messages.listen" });
132
- warn(`messages arriving during this command are not saved: ${error instanceof Error ? error.message : String(error)}`);
129
+ events({ event: "warning", code: "listen_failed", operation });
130
+ warn(`${what} arriving during this command are not saved: ${error instanceof Error ? error.message : String(error)}`);
133
131
  });
134
132
  return {
133
+ operation,
134
+ what,
135
135
  closing: () => {
136
136
  closing = true;
137
137
  },
@@ -141,6 +141,27 @@ const listened = (adapter, events, warn) => {
141
141
  },
142
142
  };
143
143
  };
144
+ /** The passive listens a connection of a `history: "store"` messenger runs: the ones its adapter has. */
145
+ const passivesOf = (adapter) => [
146
+ ...(adapter.listen
147
+ ? [
148
+ {
149
+ run: (signal) => adapter.listen?.(() => { }, signal),
150
+ operation: "messages.listen",
151
+ what: "messages",
152
+ },
153
+ ]
154
+ : []),
155
+ ...(adapter.listenHistory
156
+ ? [
157
+ {
158
+ run: (signal) => adapter.listenHistory?.(() => { }, signal),
159
+ operation: "history.listen",
160
+ what: "history batches",
161
+ },
162
+ ]
163
+ : []),
164
+ ];
144
165
  /** A list that cannot be read stays as it is, which refuses every send: the warning says why. */
145
166
  const rekeyRecipients = async (app, profile, env, warn, from, to) => {
146
167
  try {
@@ -155,9 +176,9 @@ const rekeyRecipients = async (app, profile, env, warn, from, to) => {
155
176
  * the reads answer saved to the store. `close` closes the connection and the store it opened. One
156
177
  * function, so a command and an MCP tool call save and record exactly alike.
157
178
  *
158
- * A `history: "store"` messenger with `listen` saves what arrives for as long as a connection that does not
159
- * `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again. A
160
- * connection opened with `listen: true` is left to its `watch`, which saves the same events.
179
+ * A `history: "store"` messenger with `listen` or `listenHistory` saves what arrives for as long as a connection that
180
+ * does not `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again.
181
+ * A connection opened with `listen: true` is left to its `watch` and `feed`, which save the same.
161
182
  */
162
183
  export const connected = (connection, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, }, { settings, env, renderer }, events, store = lazyStore(env, app.command), { listen: watching = false } = {}) => {
163
184
  const self = connection.self();
@@ -185,20 +206,26 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
185
206
  rekeyed: (from, to) => rekeyRecipients(app, settings.profile, env, renderer.warn, from, to),
186
207
  ...(history === "store" ? { namesFromStore: savedChatId ? { savedChatId } : {} } : {}),
187
208
  });
188
- const passive = history === "store" && self !== null && !watching ? listened(saving, events, renderer.warn) : undefined;
209
+ const passives = history === "store" && self !== null && !watching
210
+ ? passivesOf(saving).map((passive) => listened(passive, events, renderer.warn))
211
+ : [];
189
212
  return {
190
213
  adapter: saving,
191
214
  close: async () => {
192
- passive?.closing();
215
+ for (const passive of passives)
216
+ passive.closing();
193
217
  try {
194
218
  await connection.close();
195
219
  }
196
220
  finally {
197
221
  try {
198
- if (passive && !(await passive.stop())) {
199
- events({ event: "warning", code: "store_not_written", operation: "messages.listen" });
200
- renderer.warn("not saved to the local store: the messenger was still handing on messages at exit");
201
- }
222
+ const ended = await Promise.all(passives.map((passive) => passive.stop()));
223
+ passives.forEach(({ operation, what }, at) => {
224
+ if (ended[at])
225
+ return;
226
+ events({ event: "warning", code: "store_not_written", operation });
227
+ renderer.warn(`not saved to the local store: the messenger was still handing on ${what} at exit`);
228
+ });
202
229
  if (!(await settled(pending, SAVES_WAIT_MS))) {
203
230
  events({ event: "warning", code: "store_not_written", operation: "close" });
204
231
  renderer.warn("not saved to the local store: the last messages were still being written at exit");
@@ -252,7 +279,7 @@ const warnUnserved = async (messenger, profile, { env, renderer }, store) => {
252
279
  const newest = (await store.chats.chatStats(account))
253
280
  .map((one) => one.newestAt)
254
281
  .reduce((latest, at) => (at !== null && (latest === null || at > latest) ? at : latest), null);
255
- renderer.warn(`no \`${messenger.app.command} serve\` keeps profile "${profile}" up to date — ` +
282
+ renderer.warn(`no \`${messenger.app.command} ${profile} serve\` keeps profile "${profile}" up to date — ` +
256
283
  (newest === null ? "the local store holds no messages yet" : `the newest stored message is from ${newest}`));
257
284
  };
258
285
  /** The words of a command below the program, `["messages", "list"]` — the profile word is not one of them. */
@@ -11,7 +11,7 @@ import { storePath } from "../../store/path.js";
11
11
  import { baseContext, environmentOf, outputFor } from "../context.js";
12
12
  import { destroysNothing, LOCAL_WRITE, REMOTE_READ } from "../effects.js";
13
13
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
14
- import { nextStep } from "../steps.js";
14
+ import { lineOf, nextStep } from "../steps.js";
15
15
  import { recalledAccount } from "./accounts.js";
16
16
  import { messengerContext } from "./context.js";
17
17
  import { privateFiles, withSqliteSidecars } from "./private-files.js";
@@ -61,7 +61,7 @@ const reportCommand = (messenger) => {
61
61
  .description("what a problem report holds; writes nothing")
62
62
  .action(function () {
63
63
  const output = outputFor(this);
64
- const create = `${app.command} doctor report create`;
64
+ const create = lineOf(this, "doctor report create");
65
65
  renderAnswer(this, output, itemOf({ includes: INCLUDES, excludes: EXCLUDES, ...(app.issues ? { send_to: app.issues } : {}) }, { next: [nextStep("doctor.report-create", { args: {} })] }), () => [
66
66
  `A problem report is one file for the author of ${app.command}.`,
67
67
  "",
@@ -71,7 +71,7 @@ const reportCommand = (messenger) => {
71
71
  `It never holds: ${EXCLUDES.join(", ")}.`,
72
72
  "",
73
73
  `Create one: ${create}`,
74
- `About a given run: ${create} --run <id> (ids: ${app.command} runs list)`,
74
+ `About a given run: ${create} --run <id> (ids: ${lineOf(this, "runs list")})`,
75
75
  ].join("\n"));
76
76
  });
77
77
  destroysNothing(annotate(report.command("create"), { effects: LOCAL_WRITE }))
@@ -5,7 +5,7 @@ import { providerErrorKey } from "../../runs/events.js";
5
5
  import { holdEnd } from "../../sends/flood.js";
6
6
  const LONG_LIVED = new Set(["self", "newSendId", "close", "formatMarkdown", "formatHtml"]);
7
7
  /** Neither held nor paced, as they listen for as long as the run does; what ends them is still remembered. */
8
- const LISTENING = new Set(["watch", "feed", "listen"]);
8
+ const LISTENING = new Set(["watch", "feed", "listen", "listenHistory"]);
9
9
  const BLOCKING = new Set(["frozen", "limited", "banned"]);
10
10
  /** Shorter waits for the pace pass unannounced; a person at a terminal would not notice them. */
11
11
  const ANNOUNCE_WAIT_MS = 5_000;
@@ -216,7 +216,7 @@ const VERSION_MANAGER = /[\\/](\.nvm|nvm|\.fnm|fnm|fnm_multishells|\.volta|volta
216
216
  export const serverEntry = (app, { profile, flags = {}, execPath, scriptPath, env, }) => {
217
217
  if (installerOf(scriptPath) === "npx") {
218
218
  throw new CliError("validation_error", `this ${app.command} runs from npx's cache, which gets cleared, and the path would stop working — ` +
219
- `install it globally, then run \`${app.command} mcp config\` again`);
219
+ `install it globally, then run \`${app.command} ${profile} mcp config\` again`);
220
220
  }
221
221
  const names = [
222
222
  ...["CONFIG_DIR", "STATE_DIR", "CACHE_DIR"].map((name) => envName(app, name)),
@@ -5,6 +5,7 @@ import { DELETE_AT_ONCE } from "../../services/index.js";
5
5
  import { environmentOf } from "../context.js";
6
6
  import { ownerOnlyOption, REMOTE_WRITE } from "../effects.js";
7
7
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
8
+ import { lineOf } from "../steps.js";
8
9
  import { messengerContext } from "./context.js";
9
10
  /**
10
11
  * The guard asks first at the default level, `ask`; `--allow-dangerous` is the yes. For everyone only
@@ -20,7 +21,7 @@ export const deleteCommand = (messenger) => annotate(new Command("delete"), { ef
20
21
  const everyone = this.opts().forEveryone === true;
21
22
  if (environmentOf(this).agent && (everyone || messenger.deleteForMe === false))
22
23
  throw new CliError("permission_error", `${everyone ? "" : `${messenger.name ?? messenger.provider} deletes only for everyone, and `}an agent never ` +
23
- `deletes for everyone — the owner can run \`${messenger.app.command} messages delete --for-everyone\``, { permission: "messages.delete", sent: false });
24
+ `deletes for everyone — the owner can run \`${lineOf(this, "messages delete --for-everyone")}\``, { permission: "messages.delete", sent: false });
24
25
  const context = messengerContext(this, messenger);
25
26
  const deleted = await context.withServices((services) => services.messages.delete({
26
27
  chat,
@@ -32,15 +32,15 @@ export const observed = (messenger, events) => {
32
32
  throw error;
33
33
  }
34
34
  };
35
- const counted = async (operation, onEvent, work) => {
35
+ const counted = async (operation, onItem, work, count) => {
36
36
  const counts = {};
37
- await timed(operation, {}, () => work((event) => {
38
- counts[event.event] = (counts[event.event] ?? 0) + 1;
39
- onEvent(event);
37
+ await timed(operation, {}, () => work((item) => {
38
+ count(item, counts);
39
+ onItem(item);
40
40
  }), () => ({ counts }));
41
41
  };
42
42
  const mint = messenger.newSendId;
43
- const { chats, history, contact, around, watch, listen } = messenger;
43
+ const { chats, history, contact, around, watch, feed, listen, listenHistory } = messenger;
44
44
  // A method not listed here is timed under its own name, with no ids until a line here names them.
45
45
  return throughWrapper(messenger, {
46
46
  self: () => messenger.self(),
@@ -84,15 +84,32 @@ export const observed = (messenger, events) => {
84
84
  }, () => messenger.send(chatId, text, options), (sent) => ({ ids: { message_id: sent.message.id } })),
85
85
  ...(watch
86
86
  ? {
87
- watch: (onEvent, signal, onReady) => counted("messages.watch", onEvent, (counting) => watch.call(messenger, counting, signal, onReady)),
87
+ watch: (onEvent, signal, onReady) => counted("messages.watch", onEvent, (counting) => watch.call(messenger, counting, signal, onReady), byEvent),
88
88
  }
89
89
  : {}),
90
+ ...(feed
91
+ ? { feed: (onBatch, signal) => timed("history.feed", {}, () => feed.call(messenger, onBatch, signal)) }
92
+ : {}),
90
93
  ...(listen
91
94
  ? {
92
- listen: (onEvent, signal) => counted("adapter.listen", onEvent, (counting) => listen.call(messenger, counting, signal)),
95
+ listen: (onEvent, signal) => counted("messages.listen", onEvent, (counting) => listen.call(messenger, counting, signal), byEvent),
96
+ }
97
+ : {}),
98
+ ...(listenHistory
99
+ ? {
100
+ listenHistory: (onBatch, signal) => counted("history.listen", onBatch, (counting) => listenHistory.call(messenger, counting, signal), byPart),
93
101
  }
94
102
  : {}),
95
103
  logout: () => timed("session.logout", {}, () => messenger.logout()),
96
104
  close: () => messenger.close(),
97
105
  }, (name, call) => (...args) => timed(`adapter.${name}`, {}, () => call(...args)));
98
106
  };
107
+ const byEvent = (event, counts) => {
108
+ counts[event.event] = (counts[event.event] ?? 0) + 1;
109
+ };
110
+ const byPart = (batch, counts) => {
111
+ counts.batches = (counts.batches ?? 0) + 1;
112
+ for (const [part, items] of Object.entries(batch))
113
+ if (Array.isArray(items) && items.length > 0)
114
+ counts[part] = (counts[part] ?? 0) + items.length;
115
+ };
@@ -13,6 +13,7 @@ import { storePath } from "../../store/path.js";
13
13
  import { environmentOf, setExitCode } from "../context.js";
14
14
  import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, REMOTE_WRITE } from "../effects.js";
15
15
  import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
16
+ import { lineOf } from "../steps.js";
16
17
  import { messengerContext } from "./context.js";
17
18
  /** Each run of an agent is a paid model call that reads chats; keeping the store current is `serve`'s job. */
18
19
  const AGENT_MINIMUM_MINUTES = 60;
@@ -84,7 +85,7 @@ export const scheduleCommand = (messenger) => {
84
85
  say(scheduleAnswer(schedule, { file, timer: timer().where(schedule.name), replaced }), [
85
86
  `${replaced ? "Replaced" : "Added"} ${schedule.name}: ${describeWhen(schedule.when)}.`,
86
87
  `It runs: ${schedule.kind === "agent" ? schedule.argv.join(" ") : `${app.command} ${schedule.argv.join(" ")}`}`,
87
- `Output: ${join(dirname(file), "runs")} · now: ${app.command} schedule run ${schedule.name} · stop: ${app.command} schedule remove ${schedule.name}`,
88
+ `Output: ${join(dirname(file), "runs")} · now: ${lineOf(self, `schedule run ${schedule.name}`)} · stop: ${lineOf(self, `schedule remove ${schedule.name}`)}`,
88
89
  ]);
89
90
  };
90
91
  return { context, system, state, say, timer, environment, save };
@@ -172,7 +173,7 @@ export const scheduleCommand = (messenger) => {
172
173
  });
173
174
  renderAnswer(this, context, answer, () => {
174
175
  if (rows.length === 0) {
175
- context.renderer.note(`no schedules — \`${app.command} schedule command|agent\` adds one`);
176
+ context.renderer.note(`no schedules — \`${lineOf(this, "schedule command|agent")}\` adds one`);
176
177
  return "";
177
178
  }
178
179
  return rows
@@ -210,7 +211,7 @@ export const scheduleCommand = (messenger) => {
210
211
  const { state, say, timer } = prepare(this);
211
212
  const file = scheduleFile(state, scheduleName(name));
212
213
  if (!existsSync(file))
213
- throw new CliError("not_found", `no schedule ${name} — \`${app.command} schedule list\``);
214
+ throw new CliError("not_found", `no schedule ${name} — \`${lineOf(this, "schedule list")}\``);
214
215
  await timer().remove(name);
215
216
  rmSync(file, { force: true });
216
217
  say({ name, removed: true }, [`Removed ${name}.`]);