@wirecat/cli-core 0.563.0 → 0.565.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 (143) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/calendar/busy.d.ts +10 -10
  3. package/dist/calendar/busy.js +9 -9
  4. package/dist/calendar/cli/answers.d.ts +7 -8
  5. package/dist/calendar/cli/answers.js +4 -5
  6. package/dist/calendar/cli/bodies.d.ts +33 -0
  7. package/dist/calendar/cli/bodies.js +52 -0
  8. package/dist/calendar/cli/files.d.ts +8 -0
  9. package/dist/calendar/cli/files.js +41 -0
  10. package/dist/calendar/cli/index.d.ts +58 -51
  11. package/dist/calendar/cli/index.js +311 -284
  12. package/dist/calendar/cli/remind.d.ts +2 -11
  13. package/dist/calendar/cli/remind.js +81 -81
  14. package/dist/calendar/cli/search-all.d.ts +3 -8
  15. package/dist/calendar/cli/search-all.js +47 -42
  16. package/dist/calendar/mcp/index.d.ts +1 -1
  17. package/dist/calendar/prepare.d.ts +1 -1
  18. package/dist/calendar/testing/answers.d.ts +27 -5
  19. package/dist/calendar/testing/answers.js +88 -17
  20. package/dist/calendar/testing/writer.d.ts +1 -0
  21. package/dist/calendar/testing/writer.js +6 -0
  22. package/dist/calendar/writer.d.ts +6 -1
  23. package/dist/calendar/writes.d.ts +3 -1
  24. package/dist/calendar/writes.js +42 -20
  25. package/dist/cli/config-command.js +1 -1
  26. package/dist/cli/config-issues.d.ts +3 -1
  27. package/dist/cli/config-issues.js +2 -2
  28. package/dist/cli/contract-evidence.d.ts +1 -1
  29. package/dist/cli/early-refusal.d.ts +2 -1
  30. package/dist/cli/early-refusal.js +4 -2
  31. package/dist/cli/entry.d.ts +6 -2
  32. package/dist/cli/entry.js +4 -1
  33. package/dist/cli/envelope.d.ts +2 -2
  34. package/dist/cli/index.d.ts +1 -0
  35. package/dist/cli/index.js +1 -0
  36. package/dist/cli/messenger/archive-commands.js +17 -0
  37. package/dist/cli/messenger/catch-up-answers.d.ts +5 -0
  38. package/dist/cli/messenger/catch-up-answers.js +3 -0
  39. package/dist/cli/messenger/search-answers.d.ts +1 -0
  40. package/dist/cli/messenger/search-answers.js +1 -1
  41. package/dist/cli/messenger/stats-answers.d.ts +3 -3
  42. package/dist/cli/messenger/stats-answers.js +21 -9
  43. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  44. package/dist/cli/messenger/tasks-command.js +35 -14
  45. package/dist/cli/permission-keys.d.ts +3 -1
  46. package/dist/cli/permission-keys.js +2 -1
  47. package/dist/cli/preview.d.ts +12 -0
  48. package/dist/cli/preview.js +8 -1
  49. package/dist/cli/settings.d.ts +2 -0
  50. package/dist/cli/settings.js +8 -1
  51. package/dist/conversations/mail-text.d.ts +6 -0
  52. package/dist/conversations/mail-text.js +73 -0
  53. package/dist/domain/calendar.d.ts +2 -0
  54. package/dist/domain/calendar.js +6 -0
  55. package/dist/entities/chat-stats.d.ts +16 -2
  56. package/dist/entities/chat-stats.js +18 -3
  57. package/dist/entities/index.d.ts +50 -802
  58. package/dist/entities/index.js +23 -16
  59. package/dist/entities/mail-counts.d.ts +15 -0
  60. package/dist/entities/mail-counts.js +16 -0
  61. package/dist/entities/meeting.d.ts +1 -1
  62. package/dist/entities/meeting.js +1 -1
  63. package/dist/entities/store-records.d.ts +8 -0
  64. package/dist/entities/store-records.js +8 -0
  65. package/dist/errors/reasons.js +19 -1
  66. package/dist/mail/cli/index.d.ts +5 -0
  67. package/dist/mail/cli/index.js +27 -3
  68. package/dist/mail/cli/review.js +11 -4
  69. package/dist/mail/index.d.ts +2 -0
  70. package/dist/mail/index.js +2 -0
  71. package/dist/mail/stats.d.ts +50 -0
  72. package/dist/mail/stats.js +60 -0
  73. package/dist/mail/whole.d.ts +4 -4
  74. package/dist/mail/whole.js +4 -3
  75. package/dist/mcp/meeting-preparation.d.ts +1 -1
  76. package/dist/mcp/meeting-project-candidates.d.ts +1 -1
  77. package/dist/mcp/meeting-series-inventory.d.ts +1 -1
  78. package/dist/meetings/index.d.ts +2 -2
  79. package/dist/meetings/index.js +2 -2
  80. package/dist/meetings/mcp/index.d.ts +15 -15
  81. package/dist/meetings/mcp/passage.d.ts +1 -1
  82. package/dist/meetings/mcp/project-evidence.d.ts +1 -1
  83. package/dist/meetings/mcp/search.d.ts +1 -1
  84. package/dist/meetings/read-answers.d.ts +22 -1
  85. package/dist/meetings/read-answers.js +38 -0
  86. package/dist/meetings/stats.d.ts +44 -0
  87. package/dist/meetings/stats.js +77 -7
  88. package/dist/search/lucene/registry.js +2 -1
  89. package/dist/search/question-plan.d.ts +16 -0
  90. package/dist/search/question-plan.js +34 -0
  91. package/dist/services/events-search.d.ts +5 -1
  92. package/dist/services/events-search.js +10 -4
  93. package/dist/services/index.d.ts +1 -1
  94. package/dist/services/index.js +1 -1
  95. package/dist/services/meeting-stats.d.ts +12 -1
  96. package/dist/services/meeting-stats.js +12 -0
  97. package/dist/services/messages-discovery.d.ts +1 -5
  98. package/dist/services/messages-discovery.js +5 -18
  99. package/dist/services/messages-meaning.js +25 -3
  100. package/dist/services/messages.js +4 -3
  101. package/dist/services/metadata.d.ts +1 -1
  102. package/dist/services/notes-search.d.ts +5 -1
  103. package/dist/services/notes-search.js +16 -5
  104. package/dist/services/proposals/ids.d.ts +2 -2
  105. package/dist/services/proposals/ids.js +5 -3
  106. package/dist/services/proposals/kinds/findings.d.ts +1 -1
  107. package/dist/services/proposals/kinds/findings.js +1 -1
  108. package/dist/services/proposals/kinds/meeting-findings.d.ts +7 -0
  109. package/dist/services/proposals/kinds/meeting-findings.js +73 -0
  110. package/dist/services/proposals/kinds/task-close.d.ts +7 -0
  111. package/dist/services/proposals/kinds/task-close.js +76 -0
  112. package/dist/services/proposals/registry.js +4 -0
  113. package/dist/services/search-all.d.ts +5 -0
  114. package/dist/services/search-all.js +47 -14
  115. package/dist/services/task-stats.d.ts +41 -0
  116. package/dist/services/task-stats.js +126 -0
  117. package/dist/services/tasks.d.ts +17 -4
  118. package/dist/services/tasks.js +31 -14
  119. package/dist/store/chats.d.ts +3 -1
  120. package/dist/store/chats.js +6 -0
  121. package/dist/store/decisions.d.ts +4 -2
  122. package/dist/store/decisions.js +5 -3
  123. package/dist/store/exports.d.ts +1 -1
  124. package/dist/store/exports.js +1 -1
  125. package/dist/store/index.d.ts +2 -1
  126. package/dist/store/index.js +1 -1
  127. package/dist/store/sqlite/completeness.d.ts +17 -0
  128. package/dist/store/sqlite/completeness.js +49 -13
  129. package/dist/store/sqlite/lucene-fields.js +2 -0
  130. package/dist/store/sqlite/lucene-groups.d.ts +15 -0
  131. package/dist/store/sqlite/lucene-groups.js +21 -0
  132. package/dist/store/sqlite/lucene.d.ts +2 -9
  133. package/dist/store/sqlite/lucene.js +1 -12
  134. package/dist/store/sqlite/meeting-stats.d.ts +2 -0
  135. package/dist/store/sqlite/meeting-stats.js +27 -1
  136. package/dist/store/sqlite/retention.js +2 -7
  137. package/dist/store/sqlite/task-board.js +2 -0
  138. package/dist/store/sqlite/task-stats.d.ts +32 -0
  139. package/dist/store/sqlite/task-stats.js +45 -0
  140. package/dist/store/sqlite/tasks.d.ts +2 -1
  141. package/dist/store/sqlite/tasks.js +2 -0
  142. package/package.json +3 -3
  143. package/parity.json +13 -1
@@ -165,9 +165,38 @@ export const createEvent = async (context, calendarReference, fields, options =
165
165
  };
166
166
  /** The longest line `createEventFromText` sends; a quick add is one sentence, not a description. */
167
167
  const TEXT_MAX = 1000;
168
+ /** How far the provider's clock may run behind this computer's when a retry looks for what a lost quick add made. */
169
+ const CLOCK_SKEW_MS = 120_000;
170
+ const TIME_DIGITS = 9;
171
+ /**
172
+ * A quick add's id: `q`, which no hex create id has, then when it was sent, so a retry knows from when to look for the
173
+ * event it may have made.
174
+ */
175
+ const quickId = (at) => `q${at.toString(32).padStart(TIME_DIGITS, "0")}${newEventId()}`;
176
+ const QUICK_ID = new RegExp(`^q[a-v0-9]{${TIME_DIGITS}}[a-f0-9]{32}$`, "u");
177
+ const sentAt = (id) => Number.parseInt(id.slice(1, 1 + TIME_DIGITS), 32);
178
+ const wordsOf = (text) => text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
179
+ /**
180
+ * What a lost quick add made: an event this account made on the calendar since the attempt, whose title's every word
181
+ * is in the line. The provider builds the title from the line's own words.
182
+ */
183
+ const lostQuickAdd = async (context, calendar, line, id) => {
184
+ const quickAdded = context.writes.writer.quickAdded;
185
+ if (!quickAdded)
186
+ throw new CliError("validation_error", `this provider cannot find a lost quick add; leave out ${context.names.id}`, {
187
+ param: context.names.id,
188
+ });
189
+ const words = new Set(wordsOf(line));
190
+ const found = (await quickAdded.call(context.writes.writer, calendar, sentAt(id) - CLOCK_SKEW_MS)).filter((entry) => entry.status !== "cancelled" && wordsOf(entry.title ?? "").every((word) => words.has(word)));
191
+ if (found.length > 1)
192
+ throw new CliError("validation_error", `${found.length} events made since that attempt could be the one it added; look with events list ` +
193
+ "changed_at>=today and add it again only if none is", { reason: "event.quick-add-ambiguous", operation_id: id, retryable: false });
194
+ return found[0];
195
+ };
168
196
  /**
169
197
  * An event the provider makes from one line of text, as Google's quickAdd. Nobody is mailed unless `notify` says so:
170
- * the attendees are known only once the provider has read the text.
198
+ * the attendees are known only once the provider has read the text. The provider picks the event's id, so a retry
199
+ * with the `id` a lost attempt named first looks for the event that attempt made.
171
200
  */
172
201
  export const createEventFromText = async (context, calendarReference, text, options = {}) => {
173
202
  const quickAdd = context.writes.writer.quickAdd;
@@ -178,29 +207,22 @@ export const createEventFromText = async (context, calendarReference, text, opti
178
207
  throw new CliError("validation_error", "the text is empty: say what, when and where");
179
208
  if (line.length > TEXT_MAX || /[\r\n]/u.test(line))
180
209
  throw new CliError("validation_error", `the text is one line of at most ${TEXT_MAX} characters`);
210
+ if (options.id !== undefined && !QUICK_ID.test(options.id))
211
+ throw new CliError("validation_error", `${context.names.id} is the id a quick add's outcome_unknown named`, {
212
+ param: context.names.id,
213
+ });
181
214
  const calendar = writable(await calendarIn(context.store, context.accountId, calendarReference));
215
+ const lost = options.id === undefined ? undefined : await lostQuickAdd(context, calendar, line, options.id);
182
216
  const notify = options.notify ?? false;
183
- const guard = writeGuard(context, keysFor(context, "quick-add", notify));
184
- const id = newEventId();
217
+ const id = lost ? options.id : quickId(context.now);
185
218
  let entry;
186
- try {
187
- entry = await guardedWrite(guard, { chatId: null, kind: "edit", key: COMMAND_KEY["quick-add"], operationId: id, sendId: id, notify }, () => quickAdd.call(context.writes.writer, calendar, line, { notify }));
188
- }
189
- catch (error) {
190
- if (isCliError(error) && error.code === "outcome_unknown")
191
- throw new CliError("outcome_unknown", `${error.message} — the provider may have made the event, and a create from text cannot be retried safely: ` +
192
- "sync, then look with events list changed_at>=today before adding it again", { ...error.details, operation_id: id, retryable: false });
193
- throw error;
194
- }
195
- let saved;
196
- try {
197
- ;
198
- [saved] = await context.store.saveEntries(calendar.id, [entry], context.now);
199
- }
200
- catch (error) {
201
- throw new CliError(isCliError(error) ? error.code : "invalid_response", `the provider made event ${entry.externalId}, but the local store did not save it ` +
202
- `(${error instanceof Error ? error.message : String(error)}); sync to store it — do not create it again`, { ...(isCliError(error) ? error.details : {}), operation_id: id, id: entry.externalId, retryable: false });
219
+ if (lost)
220
+ entry = lost;
221
+ else {
222
+ const guard = writeGuard(context, keysFor(context, "quick-add", notify));
223
+ entry = await retryable(context, id, () => guardedWrite(guard, { chatId: null, kind: "edit", key: COMMAND_KEY["quick-add"], operationId: id, sendId: id, notify }, () => quickAdd.call(context.writes.writer, calendar, line, { notify })));
203
224
  }
225
+ const [saved] = await savedAfterCreate(context, id, () => context.store.saveEntries(calendar.id, [entry], context.now));
204
226
  return answerOf(id, saved, (entry.recurrence?.length ?? 0) > 0);
205
227
  };
206
228
  /** The most events one import sends: a file of more is refused before anything goes. */
@@ -56,7 +56,7 @@ export const configCommand = (app, config, { permissionKey = config.permissions,
56
56
  ...(declared ? { default: declared.default, description: declared.description } : {}),
57
57
  };
58
58
  }),
59
- store_settings: await storeSettings(env),
59
+ store_settings: (await storeSettings(env)).filter(({ setting }) => offersStoreSetting(app, setting)),
60
60
  }));
61
61
  for (const issue of [
62
62
  ...(settings.configIssues ?? []),
@@ -76,11 +76,13 @@ export declare const issueMessage: (issues: readonly ConfigIssue[], doctor?: str
76
76
  * `knownPermissions` (from `knownPermissionKeys`), each saved permission key outside it is an issue too, in every
77
77
  * section.
78
78
  */
79
- export declare const checkConfigText: <T extends v.GenericSchema>(text: string, { file, tool, schema, knownPermissions, }: {
79
+ export declare const checkConfigText: <T extends v.GenericSchema>(text: string, { file, tool, schema, knownPermissions, repair, }: {
80
80
  file: string;
81
81
  tool: string;
82
82
  schema: T;
83
83
  knownPermissions?: ReadonlySet<string>;
84
+ /** `false` for a CLI with its own `config` that has no `repair`: no fix then names one. */
85
+ repair?: boolean;
84
86
  }) => {
85
87
  issues: ConfigIssue[];
86
88
  config: v.InferOutput<T> | undefined;
@@ -317,12 +317,12 @@ export const issueMessage = (issues, doctor = "doctor") => {
317
317
  * `knownPermissions` (from `knownPermissionKeys`), each saved permission key outside it is an issue too, in every
318
318
  * section.
319
319
  */
320
- export const checkConfigText = (text, { file, tool, schema, knownPermissions, }) => {
320
+ export const checkConfigText = (text, { file, tool, schema, knownPermissions, repair = true, }) => {
321
321
  const parsed = parseConfigText(text, file);
322
322
  if ("issue" in parsed) {
323
323
  const issue = publicIssue(parsed.issue);
324
324
  // Offered only where it would work, so the desktop never shows a Repair button that fails.
325
- if (repairConfigText(text).state === "repaired")
325
+ if (repair && repairConfigText(text).state === "repaired")
326
326
  issue.fix = `${tool} config repair`;
327
327
  return { issues: [issue], config: undefined };
328
328
  }
@@ -15,7 +15,7 @@ export declare const contractEvidence: (root: Command, { fields, errors }?: Cont
15
15
  field_only: string[];
16
16
  reasons: readonly {
17
17
  reason: string;
18
- code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
18
+ code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "conflict" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
19
19
  message: string;
20
20
  details: string[];
21
21
  step?: string | undefined;
@@ -22,7 +22,8 @@ export declare const refuseWriteBeforeConnecting: (action: Command, { app, env,
22
22
  * A command of a CLI that declares its own keys (`commandPermissions`), refused by its key before anything connects:
23
23
  * `deny` stops it, `readonly` stops a write, and the read-only lock stops a write to the service. `ask` is left to
24
24
  * the write guard, which can put the question. A saved or `--permission` key that names no command stops every keyed
25
- * command; housekeeping still runs, so `config unset` can remove it.
25
+ * command; housekeeping still runs, so `config unset` can remove it. Under the lock a non-messenger CLI still previews
26
+ * a `--dry-run`, which sends nothing (WC-PERM-12); a messenger's preview stays refused (SEC-65).
26
27
  */
27
28
  export declare const refuseByCommandPermissions: (action: Command, permissions: CommandPermissions, { app, env, dryRun, resolve }: {
28
29
  app: AppIdentity;
@@ -79,7 +79,8 @@ export const refuseWriteBeforeConnecting = (action, { app, env, dryRun, resolve
79
79
  * A command of a CLI that declares its own keys (`commandPermissions`), refused by its key before anything connects:
80
80
  * `deny` stops it, `readonly` stops a write, and the read-only lock stops a write to the service. `ask` is left to
81
81
  * the write guard, which can put the question. A saved or `--permission` key that names no command stops every keyed
82
- * command; housekeeping still runs, so `config unset` can remove it.
82
+ * command; housekeeping still runs, so `config unset` can remove it. Under the lock a non-messenger CLI still previews
83
+ * a `--dry-run`, which sends nothing (WC-PERM-12); a messenger's preview stays refused (SEC-65).
83
84
  */
84
85
  export const refuseByCommandPermissions = (action, permissions, { app, env, dryRun, resolve }) => {
85
86
  const key = permissions.keyOf(commandPathOf(action));
@@ -87,7 +88,8 @@ export const refuseByCommandPermissions = (action, permissions, { app, env, dryR
87
88
  return;
88
89
  const { effects } = metaOf(action);
89
90
  const lock = readOnlyLockOf(app, env);
90
- if (lock && effects?.remote === "write")
91
+ const preview = dryRun && app.messenger === false;
92
+ if (lock && effects?.remote === "write" && !preview)
91
93
  throw readOnlyRefusal(lock, `run ${commandPathOf(action).join(" ")}`);
92
94
  const flags = action.optsWithGlobals();
93
95
  const overrides = environmentOf(action).agent?.overrides ?? [];
@@ -16,8 +16,12 @@ export interface EntryContext {
16
16
  atStart: (command: Command) => Promise<void>;
17
17
  /** A yes/no question on stderr; false without asking when there is no terminal, under CI, `--json`, `--jsonl` or `--no-input`. */
18
18
  ask: (question: string, command?: Command) => Promise<boolean>;
19
- /** Opens the URL in the system browser, or in `browser` when given; never waits for the browser to close. */
20
- openBrowser: (url: string, browser?: string) => Promise<void>;
19
+ /**
20
+ * Opens the URL in the system browser, or in `browser` when given; never waits for the browser to close. Does nothing
21
+ * where nobody can use a browser (WC-CMD-15): under CI, `--json`, `--jsonl`, `--no-input`, or with no terminal.
22
+ * The caller prints the URL either way.
23
+ */
24
+ openBrowser: (url: string, browser?: string, command?: Command) => Promise<void>;
21
25
  /** True once a reader closed the output (`| head`): nothing more reaches the terminal. */
22
26
  outputClosed: () => boolean;
23
27
  }
package/dist/cli/entry.js CHANGED
@@ -88,7 +88,10 @@ export const startCli = async (entry) => {
88
88
  });
89
89
  return /^(y|yes)$/i.test(answer.trim());
90
90
  },
91
- openBrowser: (url, browser) => openBrowser(url, browser, io),
91
+ openBrowser: async (url, browser, command) => {
92
+ if (!headless(command))
93
+ await openBrowser(url, browser, io);
94
+ },
92
95
  outputClosed: () => closed,
93
96
  };
94
97
  let code;
@@ -89,7 +89,7 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
89
89
  readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
90
90
  readonly complete: v.BooleanSchema<undefined>;
91
91
  readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
92
- readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
92
+ readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
93
93
  readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
94
94
  readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
95
95
  }, undefined>, undefined>, undefined>;
@@ -144,7 +144,7 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
144
144
  readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
145
145
  readonly complete: v.BooleanSchema<undefined>;
146
146
  readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
147
- readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
147
+ readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
148
148
  readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
149
149
  readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
150
150
  }, undefined>, undefined>, undefined>;
@@ -45,6 +45,7 @@ export { type AgentPolicy, type AgentTask, type EvaluationResult, evaluateAgent,
45
45
  export { answerEntity } from "./answer-entity.js";
46
46
  export { type AppDoctorOptions, appDoctorReport } from "./app-doctor-command.js";
47
47
  export type { AppMcpOptions } from "./app-mcp-command.js";
48
+ export { actorOf } from "./author.js";
48
49
  export type { ApiCommandInput, ApiCommands } from "./bot/api.js";
49
50
  export { apiFlagOf, apiJson, apiOptionKey, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson, readApiBody, } from "./bot/api-input.js";
50
51
  export { prepareRpcApiBody, type RpcApiBody } from "./bot/api-rpc.js";
package/dist/cli/index.js CHANGED
@@ -39,6 +39,7 @@ export { liftOptionsAfterDashes } from "./after-dashes.js";
39
39
  export { evaluateAgent, } from "./agent-evaluation.js";
40
40
  export { answerEntity } from "./answer-entity.js";
41
41
  export { appDoctorReport } from "./app-doctor-command.js";
42
+ export { actorOf } from "./author.js";
42
43
  export { apiFlagOf, apiJson, apiOptionKey, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson, readApiBody, } from "./bot/api-input.js";
43
44
  export { prepareRpcApiBody } from "./bot/api-rpc.js";
44
45
  export { botContext, botWords } from "./bot/context.js";
@@ -26,6 +26,7 @@ export const storeCommand = (messenger) => {
26
26
  const store = new Command("store")
27
27
  .description("the local store of messages")
28
28
  .addCommand(statusCommand(messenger))
29
+ .addCommand(storedChatsCommand(messenger))
29
30
  .addCommand(gapsCommand(messenger))
30
31
  .addCommand(jobsCommand(messenger))
31
32
  .addCommand(exportCommand(messenger))
@@ -35,6 +36,7 @@ export const storeCommand = (messenger) => {
35
36
  const examples = {
36
37
  backup: "./backup.db",
37
38
  check: "",
39
+ "chats status": "",
38
40
  clear: "--left --dry-run",
39
41
  "copies delete": "messages_copy_1",
40
42
  decrypt: "./backup.enc --output ./restored.db",
@@ -93,6 +95,21 @@ const statusCommand = (messenger) => annotate(new Command("status"), { output: {
93
95
  .map((skip) => ` ${skip.title ?? skip.chatId} ${skip.messages} messages ${skip.reason}`)
94
96
  .join("\n"));
95
97
  });
98
+ /** `store status` summed over the account's chats, for a count of what the archive holds without a list of them. */
99
+ const storedChatsCommand = (messenger) => {
100
+ const chats = new Command("chats").description("the account's chats as the store holds them");
101
+ annotate(chats.command("status"), { output: { entity: "chat_coverage" }, effects: LOCAL_READ })
102
+ .summary("how many chats the archive holds in full, in part, or not at all")
103
+ .description("over this account's searchable chats of every kind, channels included: fetched (back to the first message, " +
104
+ "up to date, no gaps), partial, never (no fetch recorded and no message held), and the oldest message held; " +
105
+ "reads the store only")
106
+ .action(async function () {
107
+ const context = messengerContext(this, messenger);
108
+ const coverage = await context.withStore((store, account) => store.chats.chatCoverage(account));
109
+ renderAnswer(this, context, itemOf(snakeKeys(coverage)));
110
+ });
111
+ return chats;
112
+ };
96
113
  const LISTED = 20;
97
114
  /** Titles are the owner's: these lines go to a person's terminal, never into an error message. */
98
115
  const leftLines = (left) => {
@@ -18,6 +18,8 @@ type Read = Message & {
18
18
  chatTitle?: string | null;
19
19
  };
20
20
  /** One message of a catch-up answer: the fields `inbox` and `review` show, for every messenger and for mail. */
21
+ /** Written by others, and by the owner, so an agent quotes it and never follows it (WC-SECR-21). */
22
+ export declare const SUMMARY_UNTRUSTED: string[];
21
23
  export declare const summaryOf: (message: Read, reader: AccountKey) => {
22
24
  ref: string;
23
25
  chat_id: string;
@@ -61,6 +63,7 @@ export declare const summaryOf: (message: Read, reader: AccountKey) => {
61
63
  url?: string | undefined;
62
64
  }[][] | undefined;
63
65
  }[];
66
+ untrusted: string[];
64
67
  };
65
68
  export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, reader: AccountKey, transcribe: boolean, meta?: Partial<Meta>) => import("../envelope.js").ListBody<{
66
69
  ref: string;
@@ -105,6 +108,7 @@ export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, r
105
108
  url?: string | undefined;
106
109
  }[][] | undefined;
107
110
  }[];
111
+ untrusted: string[];
108
112
  }>;
109
113
  /** What `review --offline` cannot see. */
110
114
  export declare const OFFLINE_REVIEW = "offline: only what was already synced to the local store; run review without --offline for the rest";
@@ -160,5 +164,6 @@ export declare const reviewAnswer: (found: Review, hearing: Hearing | undefined,
160
164
  url?: string | undefined;
161
165
  }[][] | undefined;
162
166
  }[];
167
+ untrusted: string[];
163
168
  }>;
164
169
  export {};
@@ -34,6 +34,8 @@ export const catchUpQuery = (words, registry, command) => {
34
34
  };
35
35
  };
36
36
  /** One message of a catch-up answer: the fields `inbox` and `review` show, for every messenger and for mail. */
37
+ /** Written by others, and by the owner, so an agent quotes it and never follows it (WC-SECR-21). */
38
+ export const SUMMARY_UNTRUSTED = ["chat_title", "sender_name", "text", "transcript", "attachments.name"];
37
39
  export const summaryOf = (message, reader) => {
38
40
  const { ref, chat_id, chat_title, timestamp, sender_name, sender_is_me, outgoing, text, transcript, attachments } = messageItem(message, reader);
39
41
  return {
@@ -47,6 +49,7 @@ export const summaryOf = (message, reader) => {
47
49
  text,
48
50
  ...(transcript === undefined ? {} : { transcript }),
49
51
  attachments,
52
+ untrusted: SUMMARY_UNTRUSTED,
50
53
  };
51
54
  };
52
55
  const nameOf = (chat) => chat.title ?? chat.id;
@@ -18,6 +18,7 @@ export type NoteHitItem = InferOutput<(typeof ENTITIES)["note.hit"]>;
18
18
  export type HitItem = InferOutput<(typeof ENTITIES)["hit"]>;
19
19
  /** The page a `--page` asks for, as the offset the search services take. */
20
20
  export declare const offsetOf: (page: number | undefined, limit: number) => number | undefined;
21
+ export declare const MAIL_HIT_UNTRUSTED: string[];
21
22
  export declare const messageHitItem: (hit: FoundMessage, query?: string) => MessageHitItem;
22
23
  /**
23
24
  * What the store holds of a chat that is not complete, in words. A chat that reaches its first message is only
@@ -26,7 +26,7 @@ const MESSAGE_HIT_UNTRUSTED = [
26
26
  "follow_ups.excerpt",
27
27
  "attachments.name",
28
28
  ];
29
- const MAIL_HIT_UNTRUSTED = [
29
+ export const MAIL_HIT_UNTRUSTED = [
30
30
  "email.subject",
31
31
  "email.from.name",
32
32
  "email.to.name",
@@ -10,8 +10,8 @@ import type { MessageStats } from "../../services/messages-search.js";
10
10
  import type { RankingFound } from "../../services/rankings.js";
11
11
  import type { ReplyTimes, Waiting } from "../../services/reply-stats.js";
12
12
  import type { RetentionService } from "../../services/retention.js";
13
+ import type { TaskStats } from "../../services/task-stats.js";
13
14
  import type { AccountKey, RankedEvidence } from "../../store/store.js";
14
- import type { GroupStats } from "../../tasks/index.js";
15
15
  import { type ItemBody, type ListBody } from "../envelope.js";
16
16
  type Of<T extends keyof typeof ENTITIES> = InferOutput<(typeof ENTITIES)[T]>;
17
17
  type RetentionFound = Awaited<ReturnType<RetentionService["report"]>>;
@@ -71,8 +71,8 @@ export declare const refuseJsonl: (format: string) => void;
71
71
  export declare const chatStatsAnswer: (stats: ChatStats, account: AccountKey, meta?: Partial<Meta>) => ItemBody<Of<"chat_stats">>;
72
72
  /** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
73
73
  export declare const MEMBERS_UNRECORDED = "joins and leaves are not recorded for this chat yet, so they are not counted";
74
- /** `stats tasks show`, CLI and MCP alike. */
75
- export declare const taskStatsAnswer: (rows: GroupStats[], account: AccountKey) => ItemBody<Of<"task_stats">>;
74
+ /** `stats tasks show`, CLI and MCP alike. A row of `--by chat` also names its chat as `chat`, as it always has. */
75
+ export declare const taskStatsAnswer: (stats: TaskStats, account: AccountKey) => ItemBody<Of<"task_stats">>;
76
76
  export declare const responsesAnswer: (found: ReplyTimes) => ItemBody<Of<"response_times">>;
77
77
  export declare const waitingAnswer: (found: Waiting) => ItemBody<Of<"waiting_chats">>;
78
78
  export {};
@@ -410,15 +410,27 @@ export const chatStatsAnswer = (stats, account, meta) => {
410
410
  };
411
411
  /** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
412
412
  export const MEMBERS_UNRECORDED = "joins and leaves are not recorded for this chat yet, so they are not counted";
413
- /** `stats tasks show`, CLI and MCP alike. */
414
- export const taskStatsAnswer = (rows, account) => itemOf({
415
- open: rows.reduce((sum, row) => sum + row.open, 0),
416
- rows: rows.map((row) => ({
417
- chat: chatRef(account, row.group),
418
- open: row.open,
419
- oldest_open_at: row.oldestOpenAt?.toISOString() ?? null,
420
- median_close_ms: row.medianCloseMs ?? null,
421
- })),
413
+ /** `stats tasks show`, CLI and MCP alike. A row of `--by chat` also names its chat as `chat`, as it always has. */
414
+ export const taskStatsAnswer = (stats, account) => itemOf({
415
+ open: stats.open,
416
+ created: stats.created,
417
+ closed: stats.closed,
418
+ median_close_ms: stats.medianCloseMs ?? null,
419
+ mean_open_age_ms: stats.meanOpenAgeMs ?? null,
420
+ rows: stats.rows.map((row) => {
421
+ const chat = stats.by === "chat" ? chatRef(account, row.group) : undefined;
422
+ return {
423
+ ...(chat === undefined ? {} : { chat }),
424
+ key: chat ?? (stats.by === "project" ? `project:${row.group}` : row.group),
425
+ name: row.name,
426
+ open: row.open,
427
+ created: row.created,
428
+ closed: row.closed,
429
+ oldest_open_at: row.oldestOpenAt?.toISOString() ?? null,
430
+ median_close_ms: row.medianCloseMs ?? null,
431
+ mean_open_age_ms: row.meanOpenAgeMs ?? null,
432
+ };
433
+ }),
422
434
  });
423
435
  const replyFigures = (one) => ({
424
436
  replies: one.replies,
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
3
3
  import type { Messenger } from "./context.js";
4
4
  /** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
5
5
  export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
6
+ error?: undefined;
6
7
  path: string;
7
8
  exists: boolean;
8
- error?: undefined;
9
9
  } | {
10
10
  path: string;
11
11
  exists: boolean;
@@ -8,7 +8,8 @@ import { aheadOf, momentOf } from "../../services/moment.js";
8
8
  import { refuseAlreadyRead } from "../../services/task-analysis.js";
9
9
  import { unlistedNote } from "../../services/task-categories.js";
10
10
  import { ARRIVAL_WINDOW, applyTaskRulesToChats } from "../../services/task-rules.js";
11
- import { CLAIM_LEASE_MS, closedStateOf, TASK_PAIR_QUERY, TASK_QUERY, taskLinkKindOf, taskTypeOf, } from "../../services/tasks.js";
11
+ import { TASK_STATS_BY } from "../../services/task-stats.js";
12
+ import { CLAIM_LEASE_MS, closedStateOf, TASK_PAIR_QUERY, TASK_QUERY, TASK_STATS_QUERY, taskLinkKindOf, taskTypeOf, } from "../../services/tasks.js";
12
13
  import { PRIORITY_LABELS, priorityOption } from "../../tasks/index.js";
13
14
  import { actorOf, holderOf as holderIn } from "../author.js";
14
15
  import { environmentOf } from "../context.js";
@@ -539,32 +540,52 @@ export const tasksCommand = (messenger) => {
539
540
  });
540
541
  return tasks;
541
542
  };
543
+ const statsByOf = (value) => {
544
+ if (!TASK_STATS_BY.includes(value))
545
+ throw new CliError("validation_error", `--by takes ${TASK_STATS_BY.join(", ")}`);
546
+ return value;
547
+ };
548
+ const minutes = (ms) => (ms === undefined ? "" : `${Math.round(ms / 60_000)} min`);
542
549
  export const tasksStatsCommand = (messenger) => {
543
- return annotate(new Command("show"), { effects: LOCAL_READ, output: { entity: "task_stats" } })
544
- .description("per chat: how many tasks are open, the oldest open one, the median time to close")
550
+ return withQuery(annotate(new Command("show"), {
551
+ effects: LOCAL_READ,
552
+ output: { entity: "task_stats" },
553
+ examples: [`${messenger.app.command} stats tasks show --by meeting -- created>=30d`],
554
+ }), TASK_STATS_QUERY)
555
+ .summary("count tasks by chat, meeting or project, open and closed")
556
+ .description("tasks per chat, meeting or project: open now, and opened and closed in the period (created: in the query), " +
557
+ "with the median time to close and the mean age of the open ones")
558
+ .option("--by <group>", "chat (default) or meeting: this account's tasks; project: the owner's project tasks, every project", statsByOf)
545
559
  .option("--chat <chat>", `only this chat; ${messenger.chatArgument}`)
546
560
  .option("--type <type>", "only this type: question, request, mention or promise", taskTypeOf)
547
- .action(async function () {
561
+ .option("--timezone <zone>", "the IANA timezone for calendar days in the period, such as created>=today")
562
+ .action(async function (words) {
548
563
  const context = messengerContext(this, messenger);
549
- const { chat, type } = this.opts();
564
+ const { by, chat, type, timezone } = this.opts();
550
565
  refuseJsonl(context.format);
551
- const { rows, account } = await context.withServices(async (services) => ({
552
- rows: await services.tasks.stats({
566
+ const query = listQuery(typedQuery(words), TASK_STATS_QUERY, "stats tasks show");
567
+ const { stats, account } = await context.withServices(async (services) => ({
568
+ stats: await services.tasks.stats({
569
+ ...(by === undefined ? {} : { by }),
553
570
  ...(chat === undefined ? {} : { chat }),
554
- ...(type === undefined ? {} : { type }),
571
+ ...(type === undefined ? {} : { kind: type }),
572
+ ...(query === undefined ? {} : { query }),
573
+ ...(timezone === undefined ? {} : { timezone }),
555
574
  }),
556
575
  account: await services.messages.reader(),
557
576
  }));
558
- if (rows.length === 0)
577
+ if (stats.rows.length === 0)
559
578
  context.renderer.note("no tasks yet");
560
- renderAnswer(this, context, taskStatsAnswer(rows, account), () => rows.length === 0
579
+ renderAnswer(this, context, taskStatsAnswer(stats, account), () => stats.rows.length === 0
561
580
  ? ""
562
- : `${rows
563
- .map(({ group, open, oldestOpenAt, medianCloseMs }) => [
564
- group,
581
+ : `${stats.rows
582
+ .map(({ group, name, open, created, closed, oldestOpenAt, medianCloseMs }) => [
583
+ name ? `${name} (${group})` : group,
565
584
  `${open} open`,
585
+ `${created} created`,
586
+ `${closed} closed`,
566
587
  oldestOpenAt ? `oldest ${oldestOpenAt.toISOString().slice(0, 16).replace("T", " ")}` : "",
567
- medianCloseMs === undefined ? "" : `median ${Math.round(medianCloseMs / 60_000)} min to close`,
588
+ medianCloseMs === undefined ? "" : `median ${minutes(medianCloseMs)} to close`,
568
589
  ]
569
590
  .filter(Boolean)
570
591
  .join(" "))
@@ -14,12 +14,14 @@ export interface CommandPermissions {
14
14
  extra: readonly PermissionKey[];
15
15
  /** cli-core's built-in levels and the CLI's own; they only ever tighten. */
16
16
  defaults: Readonly<Record<PermissionKey, Level>>;
17
+ /** The other keys a command's real run checks, so its `--dry-run` preview shows each: gcal's `api.calendar.*`. */
18
+ alsoChecks?: (path: readonly string[]) => readonly PermissionKey[];
17
19
  }
18
20
  /** Either way of naming a program's keys: a messenger's per-command mapping, or a declared CLI's own. */
19
21
  export type PermissionKeySource = PermissionKeyOf | CommandPermissions;
20
22
  /** A command's own path is its key; the commands that look after the tool have none. */
21
23
  export declare const commandPathKey: (path: readonly string[]) => PermissionKey | null;
22
- export declare const commandPermissions: ({ keyOf, extra, defaults, }?: Partial<CommandPermissions>) => CommandPermissions;
24
+ export declare const commandPermissions: ({ keyOf, extra, defaults, alsoChecks, }?: Partial<CommandPermissions>) => CommandPermissions;
23
25
  /**
24
26
  * The keys `permissions` may name: every command of the program, every write key, and each of their
25
27
  * parents. A key outside it is a typo that would otherwise do nothing (BUG-137).
@@ -16,10 +16,11 @@ const HOUSEKEEPING = new Set([
16
16
  ]);
17
17
  /** A command's own path is its key; the commands that look after the tool have none. */
18
18
  export const commandPathKey = (path) => path[0] === undefined || HOUSEKEEPING.has(path[0]) ? null : path.join(".");
19
- export const commandPermissions = ({ keyOf = commandPathKey, extra = [], defaults = {}, } = {}) => ({
19
+ export const commandPermissions = ({ keyOf = commandPathKey, extra = [], defaults = {}, alsoChecks, } = {}) => ({
20
20
  keyOf,
21
21
  extra: [AUTHORITY_KEY, ...extra],
22
22
  defaults: { ...DEFAULT_PERMISSIONS, ...defaults },
23
+ ...(alsoChecks ? { alsoChecks } : {}),
23
24
  });
24
25
  const pathOf = (command) => {
25
26
  const words = [];
@@ -10,6 +10,10 @@ export declare const preview: (command: Command, settings: Pick<Settings, "permi
10
10
  permission?: {
11
11
  key: PermissionKey | null;
12
12
  level: string;
13
+ also?: {
14
+ key: PermissionKey;
15
+ level: string;
16
+ }[];
13
17
  } | undefined;
14
18
  validation: {
15
19
  arguments: string;
@@ -42,12 +46,20 @@ export declare const preview: (command: Command, settings: Pick<Settings, "permi
42
46
  export declare const previewBody: (command: Command, permission: {
43
47
  key: PermissionKey | null;
44
48
  level: string;
49
+ also?: {
50
+ key: PermissionKey;
51
+ level: string;
52
+ }[];
45
53
  } | undefined, messenger: boolean) => {
46
54
  preview: boolean;
47
55
  path: string[];
48
56
  permission?: {
49
57
  key: PermissionKey | null;
50
58
  level: string;
59
+ also?: {
60
+ key: PermissionKey;
61
+ level: string;
62
+ }[];
51
63
  } | undefined;
52
64
  validation: {
53
65
  arguments: string;
@@ -23,7 +23,14 @@ export const preview = (command, settings, tool, keys, messenger = true) => {
23
23
  (!declared && key !== null && key !== undefined && WRITE_KEYS.includes(key));
24
24
  if (level === "deny" || (writes && level === "readonly"))
25
25
  throw new CliError("permission_error", `this profile does not permit ${path.join(" ")}`, { permission: key });
26
- return previewBody(command, { key: key ?? null, level }, messenger);
26
+ const also = (declared?.alsoChecks?.(path) ?? []).map((one) => ({
27
+ key: one,
28
+ level: levelFor(permissions, one, declared?.defaults).level,
29
+ }));
30
+ const stopped = also.find((one) => one.level === "deny" || (writes && one.level === "readonly"));
31
+ if (stopped)
32
+ throw new CliError("permission_error", `this profile does not permit ${stopped.key}`, { permission: stopped.key });
33
+ return previewBody(command, { key: key ?? null, level, ...(also.length ? { also } : {}) }, messenger);
27
34
  };
28
35
  const TARGETS = new Set(["chat", "message", "person", "profile"]);
29
36
  /**
@@ -42,6 +42,8 @@ export interface SettingsExtension<T extends object = Record<never, never>> {
42
42
  * `--permission` then take any command path, which the program checks against its commands.
43
43
  */
44
44
  permissions?: CommandPermissions;
45
+ /** `false` for a CLI with its own `config` that has no `repair`: no config issue then names one. */
46
+ configRepair?: boolean;
45
47
  }
46
48
  export interface SettingsExtensionContext {
47
49
  flags: GlobalFlags;
@@ -102,6 +102,8 @@ export const MESSENGER_ONLY_SETTINGS = [
102
102
  "history_group_days",
103
103
  "history_group_members_max",
104
104
  "read_other_bots",
105
+ "speech_model",
106
+ "picture_search",
105
107
  ];
106
108
  /** One program, one version: whether to look for a newer one is not a per-profile matter. */
107
109
  const SHARED_DEFAULTS_ONLY = {
@@ -259,7 +261,12 @@ export const settingsFor = (app, extension = {}) => {
259
261
  return { config: { profiles: {} }, issues: [read.issue] };
260
262
  if (read.text === undefined)
261
263
  return { config: { profiles: {} }, issues: [] };
262
- const { config, issues } = checkConfigText(read.text, { file: path, tool: app.command, schema: readSchema });
264
+ const { config, issues } = checkConfigText(read.text, {
265
+ file: path,
266
+ tool: app.command,
267
+ schema: readSchema,
268
+ repair: extension.configRepair !== false,
269
+ });
263
270
  return { config: (config ?? { profiles: {} }), issues };
264
271
  };
265
272
  /**
@@ -4,3 +4,9 @@
4
4
  * not hold stays, so a forward or a reply to an unsaved email is still found.
5
5
  */
6
6
  export declare const mailCore: (body: string, earlier: string[]) => string;
7
+ /**
8
+ * What an email says itself, for a model that should not read earlier mail or the sender's signature: no quoted
9
+ * lines, nothing after a reply or Outlook header, no signature or mobile footer. A forwarded message is kept whole,
10
+ * since it is what was sent. When nothing would be left — an email that is only a quote — the body stays as it is.
11
+ */
12
+ export declare const mailOwnText: (body: string) => string;