@wirecat/cli-core 0.545.0 → 0.547.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 (175) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/dist/calendar/busy.js +4 -1
  3. package/dist/calendar/cli/answers.d.ts +31 -0
  4. package/dist/calendar/cli/answers.js +8 -0
  5. package/dist/calendar/cli/index.d.ts +6 -0
  6. package/dist/calendar/cli/index.js +43 -4
  7. package/dist/calendar/cli/remind.js +2 -1
  8. package/dist/calendar/expand.d.ts +17 -0
  9. package/dist/calendar/expand.js +340 -0
  10. package/dist/calendar/feeds.d.ts +96 -0
  11. package/dist/calendar/feeds.js +324 -0
  12. package/dist/calendar/ics.d.ts +15 -4
  13. package/dist/calendar/ics.js +18 -7
  14. package/dist/calendar/index.d.ts +3 -1
  15. package/dist/calendar/index.js +2 -0
  16. package/dist/calendar/server-search.js +3 -1
  17. package/dist/calendar/writes.js +15 -4
  18. package/dist/cli/after-dashes.d.ts +16 -0
  19. package/dist/cli/after-dashes.js +41 -0
  20. package/dist/cli/bot/context.d.ts +1 -1
  21. package/dist/cli/bot/context.js +9 -1
  22. package/dist/cli/bot/messages.js +2 -0
  23. package/dist/cli/bot/setup.js +1 -0
  24. package/dist/cli/contract-templates.js +2 -2
  25. package/dist/cli/entry.d.ts +2 -0
  26. package/dist/cli/entry.js +3 -2
  27. package/dist/cli/index.d.ts +2 -1
  28. package/dist/cli/index.js +2 -1
  29. package/dist/cli/messenger/admin-commands.js +5 -1
  30. package/dist/cli/messenger/admin-contacts-command.js +5 -1
  31. package/dist/cli/messenger/admin-folders-command.js +5 -1
  32. package/dist/cli/messenger/complete-command.js +1 -0
  33. package/dist/cli/messenger/history-query.d.ts +10 -4
  34. package/dist/cli/messenger/history-query.js +87 -31
  35. package/dist/cli/messenger/messages-command.js +12 -23
  36. package/dist/cli/messenger/messages-comments-command.js +4 -2
  37. package/dist/cli/messenger/messages-edit-command.js +1 -0
  38. package/dist/cli/messenger/messages-press-command.js +1 -0
  39. package/dist/cli/messenger/messages-send-command.js +1 -0
  40. package/dist/cli/messenger/observed.js +2 -2
  41. package/dist/cli/messenger/polls-command.js +1 -0
  42. package/dist/cli/messenger/private-people-command.js +5 -1
  43. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  44. package/dist/cli/messenger/stored.js +56 -3
  45. package/dist/cli/messenger/topics-command.js +1 -1
  46. package/dist/cli/option-hints.d.ts +6 -0
  47. package/dist/cli/option-hints.js +15 -0
  48. package/dist/cli/program.js +17 -6
  49. package/dist/cli/schedule/schedule-command.js +1 -0
  50. package/dist/domain/message-link.d.ts +3 -0
  51. package/dist/domain/message-link.js +9 -14
  52. package/dist/domain/money-words.d.ts +11 -0
  53. package/dist/domain/money-words.js +38 -0
  54. package/dist/domain/providers.d.ts +2 -0
  55. package/dist/domain/providers.js +15 -3
  56. package/dist/domain/ref-arguments.d.ts +31 -0
  57. package/dist/domain/ref-arguments.js +79 -0
  58. package/dist/domain/references.d.ts +20 -2
  59. package/dist/domain/references.js +132 -47
  60. package/dist/errors/reasons.js +31 -1
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.js +1 -1
  63. package/dist/mail/cli/index.js +4 -4
  64. package/dist/mail/index.d.ts +2 -0
  65. package/dist/mail/index.js +2 -0
  66. package/dist/mail/partial.d.ts +34 -0
  67. package/dist/mail/partial.js +94 -0
  68. package/dist/mail/server-search.js +17 -9
  69. package/dist/mail/source.d.ts +13 -2
  70. package/dist/mail/source.js +7 -1
  71. package/dist/mail/sync-types.d.ts +112 -0
  72. package/dist/mail/sync-types.js +1 -0
  73. package/dist/mail/sync.d.ts +4 -106
  74. package/dist/mail/sync.js +49 -4
  75. package/dist/mail/testing/fake.d.ts +3 -1
  76. package/dist/mail/testing/fake.js +10 -2
  77. package/dist/meetings/mcp/index.d.ts +15 -15
  78. package/dist/oauth/client-file.d.ts +14 -0
  79. package/dist/oauth/client-file.js +28 -0
  80. package/dist/oauth/index.d.ts +1 -0
  81. package/dist/oauth/index.js +1 -0
  82. package/dist/ports/messenger.d.ts +3 -2
  83. package/dist/resolve.d.ts +3 -1
  84. package/dist/resolve.js +8 -0
  85. package/dist/search/task-signature.d.ts +5 -1
  86. package/dist/search/task-signature.js +27 -9
  87. package/dist/services/agenda.d.ts +5 -0
  88. package/dist/services/agenda.js +3 -1
  89. package/dist/services/deps.d.ts +0 -2
  90. package/dist/services/deps.js +0 -2
  91. package/dist/services/documents/ingestion.js +1 -1
  92. package/dist/services/documents/knowledge.d.ts +10 -6
  93. package/dist/services/documents/knowledge.js +25 -11
  94. package/dist/services/index.d.ts +6 -1
  95. package/dist/services/index.js +6 -1
  96. package/dist/services/message-resolution.d.ts +4 -1
  97. package/dist/services/message-resolution.js +19 -3
  98. package/dist/services/messages.d.ts +2 -0
  99. package/dist/services/messages.js +40 -36
  100. package/dist/services/money-words.d.ts +3 -0
  101. package/dist/services/money-words.js +5 -0
  102. package/dist/services/proposal-tasks.d.ts +3 -2
  103. package/dist/services/proposal-tasks.js +5 -2
  104. package/dist/services/proposals/ids.d.ts +5 -0
  105. package/dist/services/proposals/ids.js +43 -0
  106. package/dist/services/proposals/index.d.ts +3 -0
  107. package/dist/services/proposals/index.js +2 -0
  108. package/dist/services/proposals/kinds/action.d.ts +7 -0
  109. package/dist/services/proposals/kinds/action.js +45 -0
  110. package/dist/services/proposals/kinds/fact.d.ts +4 -0
  111. package/dist/services/proposals/kinds/fact.js +43 -0
  112. package/dist/services/proposals/kinds/findings.d.ts +7 -0
  113. package/dist/services/proposals/kinds/findings.js +75 -0
  114. package/dist/services/proposals/kinds/key-document.d.ts +9 -0
  115. package/dist/services/proposals/kinds/key-document.js +75 -0
  116. package/dist/services/proposals/kinds/memory.d.ts +4 -0
  117. package/dist/services/proposals/kinds/memory.js +42 -0
  118. package/dist/services/proposals/kinds/relationship.d.ts +10 -0
  119. package/dist/services/proposals/kinds/relationship.js +82 -0
  120. package/dist/services/proposals/kinds/shared.d.ts +25 -0
  121. package/dist/services/proposals/kinds/shared.js +49 -0
  122. package/dist/services/proposals/registry.d.ts +9 -0
  123. package/dist/services/proposals/registry.js +129 -0
  124. package/dist/services/proposals/types.d.ts +85 -0
  125. package/dist/services/proposals/types.js +1 -0
  126. package/dist/services/questions.d.ts +7 -2
  127. package/dist/services/questions.js +13 -4
  128. package/dist/services/server-search.d.ts +7 -4
  129. package/dist/services/server-search.js +5 -2
  130. package/dist/services/stored-project-gathering.d.ts +9 -0
  131. package/dist/services/stored-project-gathering.js +9 -4
  132. package/dist/services/task-categories.d.ts +2 -0
  133. package/dist/services/task-categories.js +19 -8
  134. package/dist/services/tasks.js +8 -4
  135. package/dist/store/decisions.d.ts +65 -0
  136. package/dist/store/decisions.js +55 -0
  137. package/dist/store/evidence-findings.d.ts +6 -6
  138. package/dist/store/index.d.ts +4 -3
  139. package/dist/store/index.js +2 -1
  140. package/dist/store/meeting-findings.d.ts +2 -0
  141. package/dist/store/messages.d.ts +4 -9
  142. package/dist/store/sqlite/actors.d.ts +13 -2
  143. package/dist/store/sqlite/actors.js +24 -1
  144. package/dist/store/sqlite/email-locations.d.ts +16 -0
  145. package/dist/store/sqlite/email-locations.js +26 -0
  146. package/dist/store/sqlite/emails.d.ts +6 -2
  147. package/dist/store/sqlite/emails.js +2 -1
  148. package/dist/store/sqlite/evidence-findings.js +13 -3
  149. package/dist/store/sqlite/evidence-sources.js +2 -2
  150. package/dist/store/sqlite/facts.d.ts +16 -8
  151. package/dist/store/sqlite/facts.js +48 -15
  152. package/dist/store/sqlite/findings-core.d.ts +2 -1
  153. package/dist/store/sqlite/findings-core.js +4 -4
  154. package/dist/store/sqlite/identities.js +1 -1
  155. package/dist/store/sqlite/knowledge.d.ts +43 -7
  156. package/dist/store/sqlite/knowledge.js +62 -17
  157. package/dist/store/sqlite/meeting-findings.js +6 -6
  158. package/dist/store/sqlite/memories.d.ts +12 -8
  159. package/dist/store/sqlite/memories.js +33 -10
  160. package/dist/store/sqlite/notes.d.ts +5 -1
  161. package/dist/store/sqlite/notes.js +22 -6
  162. package/dist/store/sqlite/open-store.d.ts +4 -1
  163. package/dist/store/sqlite/open-store.js +8 -2
  164. package/dist/store/sqlite/open.d.ts +3 -0
  165. package/dist/store/sqlite/proposed-actions.d.ts +21 -8
  166. package/dist/store/sqlite/proposed-actions.js +29 -9
  167. package/dist/store/sqlite/reads.d.ts +13 -2
  168. package/dist/store/sqlite/reads.js +8 -4
  169. package/dist/store/sqlite/relation-proposals.d.ts +34 -0
  170. package/dist/store/sqlite/relation-proposals.js +114 -0
  171. package/dist/store/sqlite/things.d.ts +0 -1
  172. package/dist/store/sqlite/things.js +25 -24
  173. package/dist/store/store.d.ts +3 -0
  174. package/dist/store/store.js +2 -0
  175. package/package.json +1 -1
@@ -58,3 +58,5 @@ export interface CliEntry {
58
58
  * output still flushes.
59
59
  */
60
60
  export declare const startCli: (entry: CliEntry) => Promise<void>;
61
+ /** Opens `url` in the system browser, or in `browser` when given; resolves once the browser has started. */
62
+ export declare const openBrowser: (url: string, browser?: string, io?: Pick<EntryIo, "platform" | "spawn">) => Promise<void>;
package/dist/cli/entry.js CHANGED
@@ -88,7 +88,7 @@ export const startCli = async (entry) => {
88
88
  });
89
89
  return /^(y|yes)$/i.test(answer.trim());
90
90
  },
91
- openBrowser: (url, browser) => openBrowser(io, url, browser),
91
+ openBrowser: (url, browser) => openBrowser(url, browser, io),
92
92
  outputClosed: () => closed,
93
93
  };
94
94
  let code;
@@ -138,7 +138,8 @@ const commanderExit = (error) => {
138
138
  return 0;
139
139
  return code === "commander.help" ? 2 : undefined;
140
140
  };
141
- const openBrowser = (io, url, browser) => {
141
+ /** Opens `url` in the system browser, or in `browser` when given; resolves once the browser has started. */
142
+ export const openBrowser = (url, browser, io = { platform: process.platform, spawn }) => {
142
143
  const [command, prefix] = browser !== undefined
143
144
  ? [browser, []]
144
145
  : io.platform === "darwin"
@@ -39,6 +39,7 @@ export { unanswered } from "../services/questions.js";
39
39
  export { openRequestTask } from "../services/task-rules.js";
40
40
  export { TASK_PAIR_QUERY, TASK_QUERY } from "../services/tasks.js";
41
41
  export { answerStdinOption, type Need, type SignInSteps, type SignInStepsOptions, signInSteps, } from "../terminal/sign-in-steps.js";
42
+ export { liftOptionsAfterDashes } from "./after-dashes.js";
42
43
  export { type AgentPolicy, type AgentTask, type EvaluationResult, evaluateAgent, type Observation, } from "./agent-evaluation.js";
43
44
  export { answerEntity } from "./answer-entity.js";
44
45
  export { type AppDoctorOptions, appDoctorReport } from "./app-doctor-command.js";
@@ -67,7 +68,7 @@ export { type Closeable, withDeadline } from "./deadline.js";
67
68
  export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
68
69
  export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
69
70
  export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, readsOverMcp, readsOverMcpAt, refuseUnderReadOnly, storeChangeRefusal, writesFileOnlyWith, } from "./effects.js";
70
- export { type CliEntry, type EntryContext, type EntryIo, startCli } from "./entry.js";
71
+ export { type CliEntry, type EntryContext, type EntryIo, openBrowser, startCli } from "./entry.js";
71
72
  export { type AnswerOutput, answerSchema, type Body, type ItemBody, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
72
73
  export { withAbort } from "./execution.js";
73
74
  export { askSetupQuestions, asksAtStart, type FirstRun, type FirstRunIo, setupAtStart, systemLanguage, } from "./first-run.js";
package/dist/cli/index.js CHANGED
@@ -34,6 +34,7 @@ export { unanswered } from "../services/questions.js";
34
34
  export { openRequestTask } from "../services/task-rules.js";
35
35
  export { TASK_PAIR_QUERY, TASK_QUERY } from "../services/tasks.js";
36
36
  export { answerStdinOption, signInSteps, } from "../terminal/sign-in-steps.js";
37
+ export { liftOptionsAfterDashes } from "./after-dashes.js";
37
38
  export { evaluateAgent, } from "./agent-evaluation.js";
38
39
  export { answerEntity } from "./answer-entity.js";
39
40
  export { appDoctorReport } from "./app-doctor-command.js";
@@ -59,7 +60,7 @@ export { withDeadline } from "./deadline.js";
59
60
  export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
60
61
  export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
61
62
  export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, readsOverMcp, readsOverMcpAt, refuseUnderReadOnly, storeChangeRefusal, writesFileOnlyWith, } from "./effects.js";
62
- export { startCli } from "./entry.js";
63
+ export { openBrowser, startCli } from "./entry.js";
63
64
  export { answerSchema, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
64
65
  export { withAbort } from "./execution.js";
65
66
  export { askSetupQuestions, asksAtStart, setupAtStart, systemLanguage, } from "./first-run.js";
@@ -247,7 +247,11 @@ const requestsCommand = (messenger) => {
247
247
  };
248
248
  /** `chats create`, `join`, `leave`, `update` and `link`: the help text is max-cli's, the one both tools say (standard, help rule 2). */
249
249
  export const groupCommands = (messenger) => [
250
- annotate(new Command("create"), { output: { entity: "chat_group_change" }, effects: REMOTE_WRITE })
250
+ annotate(new Command("create"), {
251
+ output: { entity: "chat_group_change" },
252
+ effects: REMOTE_WRITE,
253
+ literal_after_dashes: true,
254
+ })
251
255
  .description("create a group or a channel; the people added are told")
252
256
  .argument("<title>", "the group's name")
253
257
  .argument("[person...]", "people to add: an id, or part of a name")
@@ -30,7 +30,11 @@ export const contactWriteCommands = (messenger) => {
30
30
  const done = await context.withServices((services) => services.people[name](person));
31
31
  renderAnswer(this, context, itemOf(snakeKeys(done)));
32
32
  }));
33
- const rename = annotate(new Command("rename"), { effects: REMOTE_WRITE, output: { entity: "contact_update" } })
33
+ const rename = annotate(new Command("rename"), {
34
+ effects: REMOTE_WRITE,
35
+ output: { entity: "contact_update" },
36
+ literal_after_dashes: true,
37
+ })
34
38
  .summary("change a contact's name in the messenger address book")
35
39
  .description("rename the contact in the messenger address book; use contacts alias for a private local name")
36
40
  .argument("<person>", PERSON)
@@ -57,7 +57,11 @@ export const foldersCommand = (messenger) => {
57
57
  const found = await context.withServices((services) => services.folders.show(folder));
58
58
  renderAnswer(this, context, itemOf(folderAnswer(found)));
59
59
  }));
60
- folders.addCommand(withRules(annotate(new Command("create"), { output: { entity: "chat_folder_change" }, effects: REMOTE_WRITE })
60
+ folders.addCommand(withRules(annotate(new Command("create"), {
61
+ output: { entity: "chat_folder_change" },
62
+ effects: REMOTE_WRITE,
63
+ literal_after_dashes: true,
64
+ })
61
65
  .description("create a chat folder")
62
66
  .argument("<title>", "the folder's name; the app may refuse a long one")
63
67
  .option("--chat <chat>", "a chat to put in it, by id or name; repeat it for more", collect), messenger, false).action(async function (title) {
@@ -20,6 +20,7 @@ const SHELLS = ["zsh", "bash", "fish", "powershell"];
20
20
  * exists — no store, no names — and nothing goes to stderr: a shell shows whatever it is given.
21
21
  */
22
22
  export const completeCommand = (messenger, config, options = {}) => annotate(new Command("complete"), {
23
+ literal_after_dashes: true,
23
24
  effects: LOCAL_READ,
24
25
  mcp: false,
25
26
  examples: [`${messenger.app.command} complete zsh`],
@@ -9,14 +9,20 @@ export declare const historyQuery: (messenger: {
9
9
  }) => import("@wirecat/cli-base/query").QueryRegistry;
10
10
  declare const COMMENTS_QUERY: import("@wirecat/cli-base/query").QueryRegistry;
11
11
  export { COMMENTS_QUERY };
12
- type Start = Pick<ListWindow, "before" | "beforeTime" | "after">;
12
+ type Start = Pick<ListWindow, "before" | "beforeTime" | "after" | "until">;
13
13
  /**
14
- * Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward. Ids are
15
- * opaque, so only the first colon splits it.
14
+ * Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward. A window's end
15
+ * travels as `~<epoch ms>`, so a resumed read stops where the first one would have. Ids are opaque, so only the first
16
+ * colon splits it.
16
17
  */
17
18
  export declare const cursorOf: (start: Start, edge: {
18
19
  first?: string;
19
20
  last?: string;
20
21
  }) => string | undefined;
21
- /** A list reads one way from one place: the query or the cursor names at most one starting point. */
22
+ /** A list reads one way from one place: the query or the cursor names at most one window. */
22
23
  export declare const historyWindow: (words: QueryWords, cursor: string | undefined, command: string, registry?: import("@wirecat/cli-base/query").QueryRegistry) => Omit<ListWindow, "limit">;
24
+ /**
25
+ * The query `page.next` reruns with its cursor: the topic and the stars, never the bounds, which the cursor carries
26
+ * and which would name a second starting point beside it.
27
+ */
28
+ export declare const resumeQuery: (words: QueryWords, command: string, registry?: import("@wirecat/cli-base/query").QueryRegistry) => string | undefined;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- import { dateEndpoint, defineQuery, queryError, timezoneOf } from "@wirecat/cli-base/query";
3
- import { listQuery, nativeField, nativeTerms, typedQuery } from "../list-query.js";
2
+ import { dateRange, defineQuery, queryError, timezoneOf } from "@wirecat/cli-base/query";
3
+ import { listQuery, nativeField, nativeTermLists, typedQuery } from "../list-query.js";
4
4
  /** What a chat's history can be read by: where it starts, and one topic. */
5
5
  export const HISTORY_QUERY = defineQuery({
6
6
  entity: "message",
@@ -36,52 +36,108 @@ const COMMENTS_QUERY = defineQuery({
36
36
  });
37
37
  export { COMMENTS_QUERY };
38
38
  /**
39
- * Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward. Ids are
40
- * opaque, so only the first colon splits it.
39
+ * Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward. A window's end
40
+ * travels as `~<epoch ms>`, so a resumed read stops where the first one would have. Ids are opaque, so only the first
41
+ * colon splits it.
41
42
  */
42
43
  export const cursorOf = (start, edge) => {
43
44
  const forward = start.after !== undefined;
44
45
  const id = forward ? edge.last : edge.first;
45
- return id === undefined ? undefined : `${forward ? "after" : "before"}:${id}`;
46
+ if (id === undefined)
47
+ return undefined;
48
+ const end = forward && start.until !== undefined ? `~${start.until}` : "";
49
+ return `${forward ? "after" : "before"}:${id}${end}`;
46
50
  };
47
51
  const isId = (value) => value !== "" && !/[\s\p{Cc}]/u.test(value);
48
52
  const fromCursor = (cursor) => {
49
53
  const at = cursor.indexOf(":");
50
- const [way, id] = at < 0 ? [cursor, ""] : [cursor.slice(0, at), cursor.slice(at + 1)];
54
+ const [way, rest] = at < 0 ? [cursor, ""] : [cursor.slice(0, at), cursor.slice(at + 1)];
55
+ const ended = way === "after" ? /^(.*)~(\d+)$/u.exec(rest) : null;
56
+ const id = ended ? ended[1] : rest;
51
57
  if (!isId(id) || (way !== "before" && way !== "after"))
52
58
  throw new CliError("validation_error", `--cursor takes what page.next gave, before:<id> or after:<id> — not "${cursor}"`);
53
- return way === "before" ? { before: id } : { after: { id } };
59
+ if (way === "before")
60
+ return { before: id };
61
+ return { after: { id }, ...(ended ? { until: Number(ended[2]) } : {}) };
54
62
  };
55
- const startOf = (term, command) => {
63
+ /** A term's lower and upper bound; a date's as moments in the zone, an exclusive end 1 ms inside. */
64
+ const boundsOf = (term, command) => {
56
65
  const context = { command };
57
- const older = term.value === "*" && term.upper !== undefined && term.upper !== "*";
58
- const newer = term.value !== "*" && (term.upper === undefined || term.upper === "*");
59
- if (older === newer)
60
- queryError("query.unsupported-operator", term.span, `${term.field} takes one side: ${term.field}< or ${term.field}>`, context);
61
- if (term.field === "id")
62
- return older ? { before: term.upper } : { after: { id: term.value } };
63
- const bound = older ? term.upper : term.value;
64
- const inclusive = (older ? term.upper_inclusive : term.lower_inclusive) ?? true;
65
- const { time } = dateEndpoint(bound, timezoneOf(), older ? "upper" : "lower", inclusive, term.span);
66
- if (time === undefined)
67
- queryError("query.invalid-date", term.span, `${bound} is not a time`, context);
68
- return older ? { beforeTime: time } : { after: { time } };
66
+ const hasLower = term.value !== "*";
67
+ const hasUpper = term.upper !== undefined && term.upper !== "*";
68
+ const span = term.span;
69
+ if (term.field === "id") {
70
+ if (hasLower === hasUpper)
71
+ queryError("query.unsupported-operator", span, "id takes one side: id< or id>", context);
72
+ return hasLower
73
+ ? { lower: { start: { after: { id: term.value } }, span, date: false } }
74
+ : { upper: { start: { before: term.upper }, span, date: false } };
75
+ }
76
+ if (!hasLower && !hasUpper)
77
+ queryError("query.unsupported-operator", span, `${term.field} needs a start, an end or both`, context);
78
+ const range = dateRange(term.value, term.upper ?? "*", term.lower_inclusive ?? true, term.upper_inclusive ?? true, timezoneOf(), span);
79
+ if ((hasLower && range.lower === undefined) || (hasUpper && range.upper === undefined))
80
+ queryError("query.invalid-date", span, "that is not a time", context);
81
+ return {
82
+ // The messenger and the store read strictly after a start, so an inclusive one moves 1 ms back.
83
+ ...(range.lower === undefined
84
+ ? {}
85
+ : { lower: { start: { after: { time: range.lower - (range.lower_inclusive ? 1 : 0) } }, span, date: true } }),
86
+ ...(range.upper === undefined
87
+ ? {}
88
+ : { upper: { start: { until: range.upper - (range.upper_inclusive ? 0 : 1) }, span, date: true } }),
89
+ };
69
90
  };
70
- /** A list reads one way from one place: the query or the cursor names at most one starting point. */
91
+ /**
92
+ * The window the query names. A start reads forward from it, oldest first, and stops at an end; an end alone reads
93
+ * back from it, newest page first.
94
+ */
95
+ const windowOf = (terms, command) => {
96
+ const context = { command };
97
+ const lowers = [];
98
+ const uppers = [];
99
+ for (const term of terms) {
100
+ const { lower, upper } = boundsOf(term, command);
101
+ if (lower)
102
+ lowers.push(lower);
103
+ if (upper)
104
+ uppers.push(upper);
105
+ }
106
+ const [lower, extraLower] = lowers;
107
+ const [upper, extraUpper] = uppers;
108
+ if (extraLower)
109
+ queryError("query.unsupported-placement", extraLower.span, "the window has one start", context);
110
+ if (extraUpper)
111
+ queryError("query.unsupported-placement", extraUpper.span, "the window has one end", context);
112
+ if (lower && upper && !upper.date)
113
+ queryError("query.unsupported-placement", upper.span, "a window from a start ends at a date: date<…", context);
114
+ if (lower)
115
+ return { ...lower.start, ...upper?.start };
116
+ if (upper?.start.until !== undefined)
117
+ return { beforeTime: upper.start.until + 1 };
118
+ return upper?.start ?? {};
119
+ };
120
+ /** A list reads one way from one place: the query or the cursor names at most one window. */
71
121
  export const historyWindow = (words, cursor, command, registry = HISTORY_QUERY) => {
72
- const terms = [...nativeTerms(listQuery(typedQuery(words), registry, command), command).values()];
73
- const starts = terms
74
- .filter((term) => term.field !== "topic" && term.field !== "starred")
75
- .map((term) => startOf(term, command));
76
- if (cursor !== undefined)
77
- starts.push(fromCursor(cursor));
78
- if (starts.length > 1)
122
+ const terms = nativeTermLists(listQuery(typedQuery(words), registry, command), command, ["date", "id"]);
123
+ const bounds = [...(terms.get("date") ?? []), ...(terms.get("id") ?? [])];
124
+ const start = windowOf(bounds, command);
125
+ if (cursor !== undefined && bounds.length > 0)
79
126
  throw new CliError("validation_error", "the query and --cursor name two starting points; give one of them");
80
- const topic = terms.find((term) => term.field === "topic")?.value;
81
- const starred = terms.find((term) => term.field === "starred")?.value.toLowerCase();
127
+ const topic = terms.get("topic")?.[0]?.value;
128
+ const starred = terms.get("starred")?.[0]?.value.toLowerCase();
82
129
  return {
83
- ...starts[0],
130
+ ...(cursor === undefined ? start : fromCursor(cursor)),
84
131
  ...(topic === undefined ? {} : { threadId: topic }),
85
132
  ...(starred === undefined ? {} : { starred: starred === "true" }),
86
133
  };
87
134
  };
135
+ /**
136
+ * The query `page.next` reruns with its cursor: the topic and the stars, never the bounds, which the cursor carries
137
+ * and which would name a second starting point beside it.
138
+ */
139
+ export const resumeQuery = (words, command, registry = HISTORY_QUERY) => {
140
+ const terms = nativeTermLists(listQuery(typedQuery(words), registry, command), command, ["date", "id"]);
141
+ const kept = ["topic", "starred"].flatMap((field) => terms.get(field)?.map((term) => `${field}:${term.value}`) ?? []);
142
+ return kept.length > 0 ? kept.join(" ") : undefined;
143
+ };
@@ -12,6 +12,7 @@ import { modelWith, spoken } from "../../speech/hearing.js";
12
12
  import { REMOTE_READ } from "../effects.js";
13
13
  import { itemOf, listOf, renderAnswer } from "../envelope.js";
14
14
  import { queryHelp } from "../list-query.js";
15
+ import { withOptionHints } from "../option-hints.js";
15
16
  import { positiveCount } from "../paging.js";
16
17
  import { syncCommand } from "./backfill-command.js";
17
18
  import { messengerContext } from "./context.js";
@@ -19,7 +20,7 @@ import { linksCommand } from "./conversations-command.js";
19
20
  import { downloadSubcommand } from "./download-command.js";
20
21
  import { heardItems, hearForCommand, hearingMissing, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION, } from "./hearing-command.js";
21
22
  import { historyLimitGaps } from "./history-gaps.js";
22
- import { cursorOf, historyQuery, historyWindow } from "./history-query.js";
23
+ import { cursorOf, historyQuery, historyWindow, resumeQuery } from "./history-query.js";
23
24
  import { hideNever } from "./lacks.js";
24
25
  import { threadAnswer } from "./message-answers.js";
25
26
  import { messageItem, messageItems, viewOf } from "./message-items.js";
@@ -36,6 +37,8 @@ import { sendCommand } from "./messages-send-command.js";
36
37
  import { coverageGaps } from "./search-answers.js";
37
38
  import { threadOptions, threadRequest } from "./thread-options.js";
38
39
  import { transcribeSubcommand } from "./transcribe-command.js";
40
+ const WINDOW_EXAMPLE = "date>=2026-10-01 date<2026-10-20";
41
+ const TIME_FLAGS = ["--since", "--since-time", "--until", "--until-time", "--after-time", "--before-time"];
39
42
  /** `messages`: reading, and sending through the guard. A CLI may add its own subcommands. */
40
43
  export const messagesCommand = (messenger) => {
41
44
  const messages = new Command("messages").description("read and send messages");
@@ -51,8 +54,8 @@ export const messagesCommand = (messenger) => {
51
54
  .addHelpText("after", queryHelp(history))
52
55
  .argument("<chat>", messenger.chatArgument)
53
56
  .argument("[query...]", `${messenger.topics === false
54
- ? "where to start: id<42, date>2026-10-01"
55
- : "where to start and which topic: id<42, date>2026-10-01, topic:12"}${messenger.messageStars === true ? "; starred:true keeps the ones you starred" : ""}`)
57
+ ? `where to start or which window: id<42, ${WINDOW_EXAMPLE}`
58
+ : `where to start or which window, and which topic: id<42, ${WINDOW_EXAMPLE}, topic:12`}${messenger.messageStars === true ? "; starred:true keeps the ones you starred" : ""}`)
56
59
  .option("--limit <n>", "how many", positiveCount("--limit"))
57
60
  .option("--cursor <cursor>", "go on from where the last page stopped; page.next gives it")
58
61
  .option(...TRANSCRIBE_OPTION)
@@ -73,6 +76,8 @@ export const messagesCommand = (messenger) => {
73
76
  const next = page.hasMore
74
77
  ? cursorOf(window, { first: page.items[0]?.id, last: page.items.at(-1)?.id })
75
78
  : undefined;
79
+ const typed = next === undefined ? query.join(" ") : resumeQuery(query, "messages list", history);
80
+ const queryArg = typed ? { query: typed } : {};
76
81
  // Offline, the store is the whole answer: say how much of the chat it holds. Online the messenger is read.
77
82
  const gaps = context.settings.offline
78
83
  ? await context.withStore(async (store, account) => {
@@ -88,18 +93,18 @@ export const messagesCommand = (messenger) => {
88
93
  has_more: page.hasMore,
89
94
  paging: "cursor",
90
95
  ...(next === undefined ? {} : { cursor: next }),
91
- args: { chat, ...(query.length > 0 ? { query: query.join(" ") } : {}) },
96
+ args: { chat, ...queryArg },
92
97
  next: gaps.next,
93
98
  missing: [...hearingMissing(hearing, transcribe === true), ...gaps.missing],
94
99
  ...(gaps.missing.length > 0 ? { meta: { complete: false } } : {}),
95
100
  }), () => renderMessages(spokenItems(page.items, hearing), viewOf(context, messenger)));
96
101
  });
97
102
  annotate(list, { output: { entity: "message" }, query: history });
103
+ withOptionHints(list, Object.fromEntries(TIME_FLAGS.map((flag) => [flag, `messages list reads a window by its query: '${WINDOW_EXAMPLE}'`])));
98
104
  const readWindow = async (command, chat, message, window) => {
99
105
  const context = messengerContext(command, messenger);
100
- const target = targetOf(messenger, chat, message);
101
106
  const { found, reader } = await context.withServices(async (services) => ({
102
- found: await services.messages.around(isLocator(chat) ? chat : target.chat, isLocator(chat) ? undefined : target.message, window),
107
+ found: await services.messages.around(chat, message?.trim(), window),
103
108
  reader: await services.messages.reader(),
104
109
  }));
105
110
  const hearing = await hearForCommand(context, messenger, found, false);
@@ -156,9 +161,8 @@ export const messagesCommand = (messenger) => {
156
161
  const requested = threadRequest(this).thread;
157
162
  if (requested) {
158
163
  const context = messengerContext(this, messenger);
159
- const target = targetOf(messenger, chat, message);
160
164
  const { found, reader } = await context.withServices(async (services) => ({
161
- found: await services.messages.thread(isLocator(chat) ? chat : target.chat, isLocator(chat) ? undefined : target.message, { ...requested, before, after }),
165
+ found: await services.messages.thread(chat, message?.trim(), { ...requested, before, after }),
162
166
  reader: await services.messages.reader(),
163
167
  }));
164
168
  if (found.fallback)
@@ -203,19 +207,4 @@ const count = (value) => {
203
207
  throw new CliError("validation_error", `"${value}" is not a count`);
204
208
  return parsed;
205
209
  };
206
- /** A chat and a message id, or one locator naming both — which must be this messenger's. */
207
- const targetOf = (messenger, chat, message) => {
208
- if (isLocator(chat)) {
209
- if (message !== undefined)
210
- throw new CliError("validation_error", "a locator already names the message");
211
- const locator = parseLocator(chat);
212
- if (locator.provider !== messenger.provider) {
213
- throw new CliError("validation_error", `that locator is a ${locator.provider} message, not ${messenger.provider}`);
214
- }
215
- return { chat: locator.chat, message: locator.message };
216
- }
217
- if (message === undefined)
218
- throw new CliError("validation_error", "which message? give its id after the chat");
219
- return { chat, message: message.trim() };
220
- };
221
210
  export { sendCommand } from "./messages-send-command.js";
@@ -7,7 +7,7 @@ import { listOf, renderAnswer } from "../envelope.js";
7
7
  import { queryHelp } from "../list-query.js";
8
8
  import { positiveCount } from "../paging.js";
9
9
  import { messengerContext } from "./context.js";
10
- import { COMMENTS_QUERY, cursorOf, historyWindow } from "./history-query.js";
10
+ import { COMMENTS_QUERY, cursorOf, historyWindow, resumeQuery } from "./history-query.js";
11
11
  import { messageItems, viewOf } from "./message-items.js";
12
12
  export const commentsCommand = (messenger) => {
13
13
  const otherPosts = messenger.readsComments === true && messenger.channelComments === false;
@@ -42,13 +42,15 @@ export const commentsCommand = (messenger) => {
42
42
  reader: await services.messages.reader(),
43
43
  }));
44
44
  const next = found.hasMore ? cursorOf(window, { first: found.items[0]?.id }) : undefined;
45
+ const typed = next === undefined ? query.join(" ") : resumeQuery(query, "messages comments", COMMENTS_QUERY);
46
+ const queryArg = typed ? { query: typed } : {};
45
47
  renderAnswer(this, context, listOf("messages comments", {
46
48
  items: messageItems(found.items, reader),
47
49
  limit,
48
50
  has_more: found.hasMore,
49
51
  paging: "cursor",
50
52
  ...(next === undefined ? {} : { cursor: next }),
51
- args: { chat, post, ...(query.length > 0 ? { query: query.join(" ") } : {}) },
53
+ args: { chat, post, ...queryArg },
52
54
  }), () => renderMessages(found.items, viewOf(context, messenger)));
53
55
  });
54
56
  return annotate(command, { output: { entity: "message" }, query: COMMENTS_QUERY });
@@ -9,6 +9,7 @@ import { HTML_HELP } from "./messages-send-command.js";
9
9
  import { readAll } from "./stdin.js";
10
10
  export const editCommand = (messenger) => {
11
11
  const edit = annotate(new Command("edit"), {
12
+ literal_after_dashes: true,
12
13
  examples: [`${messenger.app.command} personal messages edit "Book club" 101 "The meeting starts at 11."`],
13
14
  effects: REMOTE_WRITE,
14
15
  output: { entity: "message_write" },
@@ -6,6 +6,7 @@ import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
6
6
  import { messengerContext } from "./context.js";
7
7
  /** `messages press` — one of a bot's callback buttons; the bot sees who pressed it. */
8
8
  export const pressCommand = (messenger) => annotate(new Command("press"), {
9
+ literal_after_dashes: true,
9
10
  effects: REMOTE_WRITE,
10
11
  examples: [`${messenger.app.command} personal messages press "Example Bot" 101 1`],
11
12
  })
@@ -20,6 +20,7 @@ const collect = (value, previous) => [...previous, value];
20
20
  const repeatable = (flags, description) => new Option(flags, description).argParser(collect).default([], "none");
21
21
  export const sendCommand = (messenger) => {
22
22
  const send = annotate(new Command("send"), {
23
+ literal_after_dashes: true,
23
24
  examples: [`${messenger.app.command} personal messages send "Book club" "The meeting starts at 10."`],
24
25
  effects: REMOTE_WRITE,
25
26
  output: { entity: messenger.drafts === true ? "message_send" : "message_write" },
@@ -42,9 +42,9 @@ export const observed = (messenger, events) => {
42
42
  };
43
43
  const counted = async (operation, onItem, work, count) => {
44
44
  const counts = {};
45
- await timed(operation, {}, () => work((item) => {
45
+ await timed(operation, {}, () => work((item, ...rest) => {
46
46
  count(item, counts);
47
- return onItem(item);
47
+ return onItem(item, ...rest);
48
48
  }), () => ({ counts }));
49
49
  };
50
50
  const mint = messenger.newSendId;
@@ -110,6 +110,7 @@ export const pollsCommand = (messenger) => {
110
110
  renderAnswer(this, context, itemOf(snakeKeys(closed)));
111
111
  });
112
112
  const create = annotate(polls.command("create"), {
113
+ literal_after_dashes: true,
113
114
  output: { entity: "message_write" },
114
115
  effects: REMOTE_WRITE,
115
116
  examples: [`${messenger.app.command} personal polls create "Book club" "Which day?" Monday Tuesday`],
@@ -14,7 +14,11 @@ const NOTES_QUERY = unfilteredQuery("contact_note", contactNote);
14
14
  const everyNote = (command, items) => listOf(command, { items, limit: Math.max(1, items.length), has_more: false });
15
15
  export const privatePeopleCommands = (messenger) => {
16
16
  const alias = new Command("aliases").description("a private local display name in the selected account");
17
- annotate(alias.command("set"), { effects: LOCAL_WRITE, output: { entity: "contact_alias" } })
17
+ annotate(alias.command("set"), {
18
+ effects: LOCAL_WRITE,
19
+ output: { entity: "contact_alias" },
20
+ literal_after_dashes: true,
21
+ })
18
22
  .description("your own name for a person in this account; kept on this machine, never sent")
19
23
  .argument("<person>", "the person's id, reference or known name")
20
24
  .argument("<alias>", "the private name to use for this person")
@@ -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;
@@ -1,9 +1,10 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
+ import { chatReferenceOf, ensureAccount } from "../../domain/ref-arguments.js";
2
3
  import { capability, throughWrapper, } from "../../ports/messenger.js";
3
4
  import { foldName, isId, pickPerson } from "../../resolve.js";
4
5
  import { codeOf } from "../../sends/guarded.js";
5
6
  import { limitBatch } from "../../services/history-limits.js";
6
- import { chatIdIn } from "../../services/message-resolution.js";
7
+ import { chatIdIn, profilesOnAccount } from "../../services/message-resolution.js";
7
8
  const REKEY_PACING = { lockMs: 50, pauseMs: 25 };
8
9
  /**
9
10
  * The adapter, with what each read answers written to the store. **A failed write never fails the
@@ -201,12 +202,12 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
201
202
  pending?.delete(saving);
202
203
  rekeysOnly.delete(saving);
203
204
  });
204
- void onBatch(batch);
205
+ void onBatch(batch, saving);
205
206
  return saving;
206
207
  };
207
208
  };
208
209
  // A method not listed here saves nothing and passes through; a lane that should save adds its line.
209
- return throughWrapper(messenger, {
210
+ const wrapped = throughWrapper(messenger, {
210
211
  ...(messenger.forumState
211
212
  ? {
212
213
  forumState: async (chatId) => {
@@ -465,6 +466,58 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
465
466
  return sent;
466
467
  },
467
468
  });
469
+ const chatOf = async (typed) => {
470
+ const named = chatReferenceOf(typed, account.provider);
471
+ if (!named)
472
+ return typed;
473
+ let known;
474
+ try {
475
+ known = account.account;
476
+ }
477
+ catch { }
478
+ await ensureAccount(named, { account: known, ...(profile ? { profile: profile.name } : {}) }, async (other) => {
479
+ const opened = await store();
480
+ return opened && profilesOnAccount(opened, profile?.app, account.provider)(other);
481
+ });
482
+ return named.chat;
483
+ };
484
+ return withChatReferences(wrapped, chatOf);
485
+ };
486
+ /** The adapter methods whose first argument is a chat as typed: an id, a title, or a `chat:` or `msg:` reference. */
487
+ const TYPED_CHAT = [
488
+ "resolve",
489
+ "chat",
490
+ "history",
491
+ "around",
492
+ "historyAfter",
493
+ "historyBefore",
494
+ "historyFrom",
495
+ "topics",
496
+ "topic",
497
+ "topicHistory",
498
+ "download",
499
+ "transcribe",
500
+ "scheduled",
501
+ "admins",
502
+ "chatEvents",
503
+ "leave",
504
+ "group",
505
+ "officialChatStats",
506
+ ];
507
+ /**
508
+ * A reference is read here, not in each adapter: an adapter resolves a typed chat as an id or a title, and would take
509
+ * `chat:max/…` for a title. One of another account is refused before the messenger is asked.
510
+ */
511
+ const withChatReferences = (adapter, chatOf) => {
512
+ const methods = adapter;
513
+ const own = Object.fromEntries(TYPED_CHAT.flatMap((name) => {
514
+ const method = methods[name];
515
+ if (typeof method !== "function")
516
+ return [];
517
+ const call = async (typed, ...rest) => method.call(adapter, await chatOf(typed), ...rest);
518
+ return [[name, call]];
519
+ }));
520
+ return throughWrapper(adapter, own);
468
521
  };
469
522
  /**
470
523
  * A write is saved under the chat the messenger answered, not the one typed: WhatsApp answers a re-keyed person's
@@ -68,7 +68,7 @@ export const topicsCommand = (messenger) => {
68
68
  const done = await context.withServices((services) => services.topics.enable(chat, { upgrade: this.opts().upgrade === true }));
69
69
  renderAnswer(this, context, itemOf(snakeKeys(done)));
70
70
  });
71
- annotate(topics.command("create"), { effects: REMOTE_WRITE })
71
+ annotate(topics.command("create"), { effects: REMOTE_WRITE, literal_after_dashes: true })
72
72
  .description("create a named topic in an existing forum; never enable or upgrade a group implicitly")
73
73
  .argument("<chat>", messenger.chatArgument)
74
74
  .argument("<title>", "the topic title, at most 128 UTF-8 bytes")
@@ -0,0 +1,6 @@
1
+ import type { Command } from "commander";
2
+ /**
3
+ * A flag a command does not take, but an agent is likely to type, fails with what to type instead rather than
4
+ * commander's bare "unknown option".
5
+ */
6
+ export declare const withOptionHints: (command: Command, hints: Readonly<Record<string, string>>) => Command;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A flag a command does not take, but an agent is likely to type, fails with what to type instead rather than
3
+ * commander's bare "unknown option".
4
+ */
5
+ export const withOptionHints = (command, hints) => {
6
+ const unknown = command;
7
+ const fallback = unknown.unknownOption.bind(command);
8
+ unknown.unknownOption = (flag) => {
9
+ const hint = hints[flag.split("=")[0]];
10
+ if (hint === undefined)
11
+ return fallback(flag);
12
+ return command.error(`unknown option '${flag}' — ${hint}`, { code: "commander.unknownOption" });
13
+ };
14
+ return command;
15
+ };