@wirecat/cli-core 0.544.0 → 0.546.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 (114) hide show
  1. package/CHANGELOG.md +88 -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/archive-commands.js +20 -2
  33. package/dist/cli/messenger/backfill-command.js +13 -2
  34. package/dist/cli/messenger/complete-command.js +1 -0
  35. package/dist/cli/messenger/context.js +3 -0
  36. package/dist/cli/messenger/history-gaps.d.ts +12 -0
  37. package/dist/cli/messenger/history-gaps.js +41 -0
  38. package/dist/cli/messenger/messages-command.js +9 -22
  39. package/dist/cli/messenger/messages-edit-command.js +1 -0
  40. package/dist/cli/messenger/messages-press-command.js +1 -0
  41. package/dist/cli/messenger/messages-search-command.js +5 -1
  42. package/dist/cli/messenger/messages-send-command.js +1 -0
  43. package/dist/cli/messenger/observed.js +2 -2
  44. package/dist/cli/messenger/polls-command.js +1 -0
  45. package/dist/cli/messenger/private-people-command.js +5 -1
  46. package/dist/cli/messenger/search-answers.d.ts +3 -2
  47. package/dist/cli/messenger/search-answers.js +12 -5
  48. package/dist/cli/messenger/search-command.js +3 -1
  49. package/dist/cli/messenger/stored.d.ts +6 -1
  50. package/dist/cli/messenger/stored.js +94 -5
  51. package/dist/cli/messenger/topics-command.js +1 -1
  52. package/dist/cli/program.js +17 -6
  53. package/dist/cli/schedule/schedule-command.js +1 -0
  54. package/dist/cli/settings.d.ts +9 -0
  55. package/dist/cli/settings.js +20 -0
  56. package/dist/cli/shared-command-exports.d.ts +24 -0
  57. package/dist/cli/steps.js +1 -0
  58. package/dist/domain/message-link.d.ts +3 -0
  59. package/dist/domain/message-link.js +9 -14
  60. package/dist/domain/money-words.d.ts +11 -0
  61. package/dist/domain/money-words.js +38 -0
  62. package/dist/domain/providers.d.ts +2 -0
  63. package/dist/domain/providers.js +15 -3
  64. package/dist/domain/ref-arguments.d.ts +31 -0
  65. package/dist/domain/ref-arguments.js +79 -0
  66. package/dist/domain/references.d.ts +20 -2
  67. package/dist/domain/references.js +132 -47
  68. package/dist/entities/index.d.ts +1 -0
  69. package/dist/entities/message-transfer.d.ts +4 -0
  70. package/dist/entities/message-transfer.js +2 -0
  71. package/dist/errors/reasons.js +18 -0
  72. package/dist/index.d.ts +1 -1
  73. package/dist/index.js +1 -1
  74. package/dist/meetings/mcp/index.d.ts +15 -15
  75. package/dist/oauth/client-file.d.ts +14 -0
  76. package/dist/oauth/client-file.js +28 -0
  77. package/dist/oauth/index.d.ts +1 -0
  78. package/dist/oauth/index.js +1 -0
  79. package/dist/ports/messenger.d.ts +3 -2
  80. package/dist/resolve.d.ts +3 -1
  81. package/dist/resolve.js +8 -0
  82. package/dist/search/task-signature.d.ts +5 -1
  83. package/dist/search/task-signature.js +27 -9
  84. package/dist/services/agenda.d.ts +5 -0
  85. package/dist/services/agenda.js +3 -1
  86. package/dist/services/history-limits.d.ts +45 -0
  87. package/dist/services/history-limits.js +50 -0
  88. package/dist/services/index.d.ts +5 -1
  89. package/dist/services/index.js +5 -1
  90. package/dist/services/message-resolution.d.ts +4 -1
  91. package/dist/services/message-resolution.js +19 -3
  92. package/dist/services/messages.js +15 -13
  93. package/dist/services/money-words.d.ts +3 -0
  94. package/dist/services/money-words.js +5 -0
  95. package/dist/services/questions.d.ts +7 -2
  96. package/dist/services/questions.js +13 -4
  97. package/dist/services/stored-project-gathering.d.ts +9 -0
  98. package/dist/services/stored-project-gathering.js +9 -4
  99. package/dist/services/task-categories.d.ts +2 -0
  100. package/dist/services/task-categories.js +19 -8
  101. package/dist/store/chats.d.ts +10 -0
  102. package/dist/store/chats.js +25 -0
  103. package/dist/store/sqlite/chats.js +2 -1
  104. package/dist/store/sqlite/evidence-sources.js +2 -2
  105. package/dist/store/sqlite/history-skips.d.ts +24 -0
  106. package/dist/store/sqlite/history-skips.js +95 -0
  107. package/dist/store/sqlite/identities.js +1 -1
  108. package/dist/store/sqlite/knowledge.js +7 -2
  109. package/dist/store/sqlite/notes.js +4 -4
  110. package/dist/store/sqlite/things.d.ts +0 -1
  111. package/dist/store/sqlite/things.js +25 -24
  112. package/dist/store/store.d.ts +1 -0
  113. package/package.json +1 -1
  114. package/parity.json +4 -0
@@ -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) {
@@ -12,11 +12,12 @@ import { kindsOf } from "../../services/inbox.js";
12
12
  import { dateWindow } from "../../services/moment.js";
13
13
  import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
14
14
  import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
15
- import { commandAt } from "../steps.js";
15
+ import { commandAt, lineOf } from "../steps.js";
16
16
  import { jobsCommand } from "./backfill-command.js";
17
17
  import { messengerContext } from "./context.js";
18
18
  import { admitPassword, linesOf, openFolder, runFileFor, saveManifest, sealRun, writeChanges } from "./export-folder.js";
19
19
  import { gapsCommand } from "./gaps-command.js";
20
+ import { historyLimitGaps } from "./history-gaps.js";
20
21
  import { messageItems } from "./message-items.js";
21
22
  import { ENCRYPT_OPTION, passwordOf } from "./password.js";
22
23
  import { storeMaintenanceCommands } from "./store-maintenance-command.js";
@@ -71,9 +72,26 @@ const statusCommand = (messenger) => annotate(new Command("status"), { output: {
71
72
  .action(async function (chat) {
72
73
  const context = messengerContext(this, messenger);
73
74
  const rows = await context.withServices((services) => services.archive.status(chat));
74
- renderAnswer(this, context, listOf("store status", { items: rows.map(snakeKeys), limit: Math.max(1, rows.length), has_more: false }));
75
+ const skips = await context
76
+ .withStore((store, account) => store.chats.historySkips(account))
77
+ .then((all) => chat === undefined ? all : all.filter((skip) => rows.some((row) => row.chatId === skip.chatId)))
78
+ .catch(() => []);
79
+ const limited = historyLimitGaps(skips);
80
+ renderAnswer(this, context, listOf("store status", {
81
+ items: rows.map(snakeKeys),
82
+ limit: Math.max(1, rows.length),
83
+ has_more: false,
84
+ next: limited.next,
85
+ missing: limited.missing,
86
+ }));
75
87
  if (rows.length === 0 && context.format === "pretty")
76
88
  context.renderer.note("the store holds no messages for this profile yet");
89
+ if (skips.length > 0 && context.format === "pretty")
90
+ context.renderer.note(`history limits left out pushed history of ${skips.length} chats — \`${lineOf(this, "messages sync <chat> --full-history")}\` fetches one and keeps it in full:\n` +
91
+ skips
92
+ .slice(0, 20)
93
+ .map((skip) => ` ${skip.title ?? skip.chatId} ${skip.messages} messages ${skip.reason}`)
94
+ .join("\n"));
77
95
  });
78
96
  const LISTED = 20;
79
97
  /** Titles are the owner's: these lines go to a person's terminal, never into an error message. */
@@ -58,14 +58,17 @@ export const syncCommand = (messenger) => {
58
58
  .option("--catch-up-chunks <n>", "at most this many local vector chunks", wholeNumber)
59
59
  .option("--catch-up-messages <n>", "skip a graph rebuild larger than this many messages", wholeNumber)
60
60
  .option("--catch-up-time <duration>", "local preparation time budget, 30s by default")
61
+ .option("--full-history", "keep this chat's pushed history in full from now on, past the history limits (history_* settings)")
61
62
  .option("--background", "run as a job that outlives this command; `store jobs show` follows it")
62
63
  .option("--dry-run", "only estimate how many messages, requests and minutes a full fetch would still take — from the store, no request")
63
64
  .action(async function (chat) {
64
- const { all, pause, sinceTime: since, last, background, dryRun: estimate, catchUp, catchUpChunks, catchUpMessages, catchUpTime, ...sizes } = this.opts();
65
+ const { all, pause, sinceTime: since, last, background, dryRun: estimate, catchUp, catchUpChunks, catchUpMessages, catchUpTime, fullHistory: include, ...sizes } = this.opts();
65
66
  if ((chat === undefined) === (all !== true))
66
67
  throw new CliError("validation_error", "name a chat, or --all for every chat");
67
68
  if (all && estimate)
68
69
  throw new CliError("validation_error", "--dry-run prices one chat; not with --all");
70
+ if (include && chat === undefined)
71
+ throw new CliError("validation_error", "--full-history keeps one chat: name it");
69
72
  if (all && messenger.history === "store") {
70
73
  throw new CliError("validation_error", `--all walks the messenger's chat list, and ${messenger.name ?? messenger.app.command} pushes its chats ` +
71
74
  `instead — keep \`${lineOf(this, "serve")}\` running${messenger.historyOnDemand ? ", or name one chat" : ""}`);
@@ -164,6 +167,10 @@ export const syncCommand = (messenger) => {
164
167
  const result = await context.withServices((services) => chat === undefined ? services.archive.fetchAll(options) : services.archive.fetch(chat, options));
165
168
  if (jobId)
166
169
  finishJob(messenger.app, jobs, jobId, { finishedAt: new Date().toISOString(), result });
170
+ const synced = "chat" in result ? result.chat : null;
171
+ const included = include && synced
172
+ ? await context.withStore((store, account) => store.chats.setHistoryIncluded(account, synced, true))
173
+ : undefined;
167
174
  const tasks = await taskRulesAfterSync(context);
168
175
  const prepared = "prepared" in result ? result.prepared : undefined;
169
176
  const unprepared = prepared && !prepared.complete && "chat" in result && result.chat !== null;
@@ -171,7 +178,11 @@ export const syncCommand = (messenger) => {
171
178
  const review = await context.withServices((services) => services.mentions.newCount(point.after));
172
179
  const toReview = review.count;
173
180
  const shown = `${toReview}${review.more ? "+" : ""}`;
174
- renderAnswer(this, context, itemOf(snakeKeys(tasks && tasks.added + tasks.closed + (tasks.held ?? 0) > 0 ? { ...result, tasks } : result), {
181
+ renderAnswer(this, context, itemOf(snakeKeys({
182
+ ...result,
183
+ ...(tasks && tasks.added + tasks.closed + (tasks.held ?? 0) > 0 ? { tasks } : {}),
184
+ ...(included === undefined ? {} : { historyIncluded: included }),
185
+ }), {
175
186
  next: [
176
187
  ...(unprepared
177
188
  ? [
@@ -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`],
@@ -231,6 +231,9 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
231
231
  recipientIds: () => (new RecipientList(recipientsPathFor(app, settings.profile, env), app.command).read() ?? []).flatMap((recipient) => [recipient.id, ...(recipient.partnerId ? [recipient.partnerId] : [])]),
232
232
  ...(history === "store" ? { namesFromStore: savedChatId ? { savedChatId } : {} } : {}),
233
233
  ...(unreadCounts === "store" ? { countsUnread: true } : {}),
234
+ ...(history === "store" && settings.historyLimits
235
+ ? { historyLimits: settings.historyLimits, command: `${app.command} ${settings.profile}` }
236
+ : {}),
234
237
  });
235
238
  const passives = history === "store" && self !== null && !watching
236
239
  ? passivesOf(saving).map((passive) => listened(passive, events, renderer.warn, closeWaitMs))
@@ -0,0 +1,12 @@
1
+ import type { Missing, NextStep } from "@wirecat/cli-base/standards";
2
+ import type { AccountKey, MessageStore, StoredHistorySkip } from "../../store/store.js";
3
+ /**
4
+ * What the history limits (`history_*` settings) left out of pushed history, as `missing` and `next`: an answer read
5
+ * from the store must not pass for every message the messenger has.
6
+ */
7
+ export declare const historyLimitGaps: (skips: StoredHistorySkip[]) => {
8
+ next: NextStep[];
9
+ missing: Missing[];
10
+ };
11
+ /** What the limits left out in every account an answer read, most messages first. */
12
+ export declare const historySkipsOf: (store: Pick<MessageStore, "chats">, accounts: readonly AccountKey[]) => Promise<StoredHistorySkip[]>;
@@ -0,0 +1,41 @@
1
+ import { nextStep } from "../steps.js";
2
+ const SHOWN = 5;
3
+ const reasonOf = (skip) => skip.reason === "group_too_large"
4
+ ? `a group of ${skip.members ?? "many"} people, not saved`
5
+ : `only from ${skip.keptFrom?.slice(0, 10) ?? "the window's start"}`;
6
+ const nameOf = (skip) => skip.title ?? skip.chatId;
7
+ /**
8
+ * What the history limits (`history_*` settings) left out of pushed history, as `missing` and `next`: an answer read
9
+ * from the store must not pass for every message the messenger has.
10
+ */
11
+ export const historyLimitGaps = (skips) => {
12
+ const [first] = skips;
13
+ if (!first)
14
+ return { next: [], missing: [] };
15
+ const messages = skips.reduce((sum, skip) => sum + skip.messages, 0);
16
+ const listed = skips
17
+ .slice(0, SHOWN)
18
+ .map((skip) => `${nameOf(skip)} (${skip.messages}, ${reasonOf(skip)})`)
19
+ .join("; ");
20
+ const more = skips.length > SHOWN ? `; and ${skips.length - SHOWN} more — \`store status\` lists them` : "";
21
+ return {
22
+ next: [
23
+ {
24
+ ...nextStep("messages.sync", { args: { chat: first.chatId, full_history: true } }),
25
+ why: "fetches this chat's history and keeps it in full from now on, past the history limits",
26
+ },
27
+ ],
28
+ missing: [
29
+ {
30
+ field: "history_limits",
31
+ why: `the history limits (history_* settings) left ${messages} pushed messages of ${skips.length} chats out of ` +
32
+ `the store: ${listed}${more}`,
33
+ step: "messages.sync",
34
+ },
35
+ ],
36
+ };
37
+ };
38
+ /** What the limits left out in every account an answer read, most messages first. */
39
+ export const historySkipsOf = async (store, accounts) => (await Promise.all(accounts.map((account) => store.chats.historySkips(account))))
40
+ .flat()
41
+ .sort((a, b) => b.messages - a.messages);
@@ -18,6 +18,7 @@ import { messengerContext } from "./context.js";
18
18
  import { linksCommand } from "./conversations-command.js";
19
19
  import { downloadSubcommand } from "./download-command.js";
20
20
  import { heardItems, hearForCommand, hearingMissing, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION, } from "./hearing-command.js";
21
+ import { historyLimitGaps } from "./history-gaps.js";
21
22
  import { cursorOf, historyQuery, historyWindow } from "./history-query.js";
22
23
  import { hideNever } from "./lacks.js";
23
24
  import { threadAnswer } from "./message-answers.js";
@@ -74,9 +75,12 @@ export const messagesCommand = (messenger) => {
74
75
  : undefined;
75
76
  // Offline, the store is the whole answer: say how much of the chat it holds. Online the messenger is read.
76
77
  const gaps = context.settings.offline
77
- ? coverageGaps(await context.withStore(async (store, account) => chatCoverage(store, account, [
78
- await storedChatId(messenger, chat, store, account, { includeLeft: true }),
79
- ])))
78
+ ? await context.withStore(async (store, account) => {
79
+ const chatId = await storedChatId(messenger, chat, store, account, { includeLeft: true });
80
+ const held = coverageGaps(await chatCoverage(store, account, [chatId]));
81
+ const limited = historyLimitGaps((await store.chats.historySkips(account)).filter((skip) => skip.chatId === chatId));
82
+ return { next: [...limited.next, ...held.next], missing: [...limited.missing, ...held.missing] };
83
+ })
80
84
  : { next: [], missing: [] };
81
85
  renderAnswer(this, context, listOf("messages list", {
82
86
  items: messageItems(heardItems(page.items, hearing), reader),
@@ -93,9 +97,8 @@ export const messagesCommand = (messenger) => {
93
97
  annotate(list, { output: { entity: "message" }, query: history });
94
98
  const readWindow = async (command, chat, message, window) => {
95
99
  const context = messengerContext(command, messenger);
96
- const target = targetOf(messenger, chat, message);
97
100
  const { found, reader } = await context.withServices(async (services) => ({
98
- found: await services.messages.around(isLocator(chat) ? chat : target.chat, isLocator(chat) ? undefined : target.message, window),
101
+ found: await services.messages.around(chat, message?.trim(), window),
99
102
  reader: await services.messages.reader(),
100
103
  }));
101
104
  const hearing = await hearForCommand(context, messenger, found, false);
@@ -152,9 +155,8 @@ export const messagesCommand = (messenger) => {
152
155
  const requested = threadRequest(this).thread;
153
156
  if (requested) {
154
157
  const context = messengerContext(this, messenger);
155
- const target = targetOf(messenger, chat, message);
156
158
  const { found, reader } = await context.withServices(async (services) => ({
157
- found: await services.messages.thread(isLocator(chat) ? chat : target.chat, isLocator(chat) ? undefined : target.message, { ...requested, before, after }),
159
+ found: await services.messages.thread(chat, message?.trim(), { ...requested, before, after }),
158
160
  reader: await services.messages.reader(),
159
161
  }));
160
162
  if (found.fallback)
@@ -199,19 +201,4 @@ const count = (value) => {
199
201
  throw new CliError("validation_error", `"${value}" is not a count`);
200
202
  return parsed;
201
203
  };
202
- /** A chat and a message id, or one locator naming both — which must be this messenger's. */
203
- const targetOf = (messenger, chat, message) => {
204
- if (isLocator(chat)) {
205
- if (message !== undefined)
206
- throw new CliError("validation_error", "a locator already names the message");
207
- const locator = parseLocator(chat);
208
- if (locator.provider !== messenger.provider) {
209
- throw new CliError("validation_error", `that locator is a ${locator.provider} message, not ${messenger.provider}`);
210
- }
211
- return { chat: locator.chat, message: locator.message };
212
- }
213
- if (message === undefined)
214
- throw new CliError("validation_error", "which message? give its id after the chat");
215
- return { chat, message: message.trim() };
216
- };
217
204
  export { sendCommand } from "./messages-send-command.js";
@@ -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
  })
@@ -12,6 +12,7 @@ import { renderAnswer } from "../envelope.js";
12
12
  import { positiveCount } from "../paging.js";
13
13
  import { archiveSummary } from "./archive-summary.js";
14
14
  import { messengerContext } from "./context.js";
15
+ import { historySkipsOf } from "./history-gaps.js";
15
16
  import { meaningNote, messagesAnswer, offsetOf, shownOf } from "./search-answers.js";
16
17
  import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
17
18
  import { syncOptions, syncRequest, syncsFirst } from "./search-sync-options.js";
@@ -207,7 +208,10 @@ const searchAction = (messenger) => async function (given) {
207
208
  ...(newest ? { newest: true } : {}),
208
209
  ...(timezone === undefined ? {} : { timezone }),
209
210
  };
210
- renderAnswer(this, context, messagesAnswer("search messages", found, { limit, page: page ?? 1, again }, given.join(" ")), view);
211
+ const skips = await context
212
+ .withStore((store) => historySkipsOf(store, found.coverage?.accounts ?? found.accounts ?? []))
213
+ .catch(() => []);
214
+ renderAnswer(this, context, messagesAnswer("search messages", found, { limit, page: page ?? 1, again }, given.join(" "), skips), view);
211
215
  };
212
216
  const withOffset = (page, limit) => {
213
217
  const offset = offsetOf(page, limit);
@@ -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")
@@ -8,6 +8,7 @@ import type { MeaningSkipped } from "../../services/messages-discovery.js";
8
8
  import type { FoundNote, NotesFoundOf } from "../../services/notes-search.js";
9
9
  import type { SearchAllItem } from "../../services/search-all.js";
10
10
  import type { MeetingSearchItem, SearchAllIncludingMeetingsFound } from "../../services/search-all-meetings.js";
11
+ import type { StoredHistorySkip } from "../../store/store.js";
11
12
  import { type ListBody } from "../envelope.js";
12
13
  /** What a source tool's `search all` reads: never mail, which is memo's (docs/dev/TOOL-BOUNDARIES.md). */
13
14
  export declare const SOURCE_SEARCHED: readonly ["messages", "notes", "events", "meetings"];
@@ -42,10 +43,10 @@ export interface Paging {
42
43
  /** "showing 20 of 144", for the human view under a page of hits; `+` when the total is a lower bound. */
43
44
  export declare const shownOf: (shown: number, offset: number, total: SearchTotal | undefined) => string | undefined;
44
45
  /** `search messages` and `search mail`, CLI and MCP alike. */
45
- export declare const messagesAnswer: (command: string, found: SearchFound, { limit, page, again }: Paging, query?: string) => ListBody<MessageHitItem>;
46
+ export declare const messagesAnswer: (command: string, found: SearchFound, { limit, page, again }: Paging, query?: string, skips?: StoredHistorySkip[]) => ListBody<MessageHitItem>;
46
47
  export declare const noteHitItem: (hit: FoundNote) => NoteHitItem;
47
48
  export declare const allHitItem: (item: SearchAllItem | MeetingSearchItem) => HitItem;
48
49
  /** `search notes`, CLI and MCP alike. */
49
50
  export declare const notesAnswer: (command: string, found: NotesFoundOf, { limit, page, again }: Paging, exact?: boolean) => ListBody<NoteHitItem>;
50
51
  /** `search all`, CLI and MCP alike. A bounded meeting scan that could not tell whether more match says so. */
51
- export declare const allAnswer: (command: string, found: SearchAllIncludingMeetingsFound, { limit, page, again }: Paging, exact?: boolean) => ListBody<HitItem>;
52
+ export declare const allAnswer: (command: string, found: SearchAllIncludingMeetingsFound, { limit, page, again }: Paging, exact?: boolean, skips?: StoredHistorySkip[]) => ListBody<HitItem>;
@@ -7,6 +7,7 @@ import { MAX_SEARCH_OFFSET } from "../../services/messages-archive-search.js";
7
7
  import { listOf } from "../envelope.js";
8
8
  import { nextStep } from "../steps.js";
9
9
  import { archiveSummary } from "./archive-summary.js";
10
+ import { historyLimitGaps } from "./history-gaps.js";
10
11
  /** What a source tool's `search all` reads: never mail, which is memo's (docs/dev/TOOL-BOUNDARIES.md). */
11
12
  export const SOURCE_SEARCHED = ["messages", "notes", "events", "meetings"];
12
13
  const nonEmpty = (list) => (list.length > 0 ? list : undefined);
@@ -249,8 +250,10 @@ export const shownOf = (shown, offset, total) => total === undefined || shown ==
249
250
  ? undefined
250
251
  : `showing ${offset + 1}–${offset + shown} of ${total.value}${total.relation === "gte" ? "+" : ""}`;
251
252
  /** `search messages` and `search mail`, CLI and MCP alike. */
252
- export const messagesAnswer = (command, found, { limit, page, again }, query = "") => {
253
+ export const messagesAnswer = (command, found, { limit, page, again }, query = "", skips = []) => {
253
254
  const accounts = found.coverage?.accounts ?? found.accounts ?? [];
255
+ const gaps = gapsOf(found);
256
+ const limited = historyLimitGaps(skips);
254
257
  return listOf(command, {
255
258
  items: [...found.items.map((hit) => messageHitItem(hit, query)), ...pictureItems(found)],
256
259
  limit,
@@ -258,8 +261,9 @@ export const messagesAnswer = (command, found, { limit, page, again }, query = "
258
261
  page: page ?? 1,
259
262
  ...(found.total ? { total: found.total } : {}),
260
263
  ...(again ? { args: again } : {}),
261
- ...gapsOf(found),
262
- meta: metaOf(accounts, found.skipped, coverageGaps(found).missing.length > 0),
264
+ next: [...gaps.next, ...limited.next],
265
+ missing: [...gaps.missing, ...limited.missing],
266
+ meta: metaOf(accounts, found.skipped, coverageGaps(found).missing.length > 0 || skips.length > 0),
263
267
  });
264
268
  };
265
269
  /**
@@ -327,7 +331,8 @@ export const notesAnswer = (command, found, { limit, page, again }, exact = fals
327
331
  meta: metaOf(found.accounts, found.skipped),
328
332
  });
329
333
  /** `search all`, CLI and MCP alike. A bounded meeting scan that could not tell whether more match says so. */
330
- export const allAnswer = (command, found, { limit, page, again }, exact = false) => {
334
+ export const allAnswer = (command, found, { limit, page, again }, exact = false, skips = []) => {
335
+ const limited = historyLimitGaps(skips);
331
336
  const scan = found.meetings?.complete === false ? found.meetings : undefined;
332
337
  const notes = meaningGap(found.notes?.meaningSkipped, exact);
333
338
  const messages = messageMeaningGap(found.messages?.meaningSkipped, "found_by");
@@ -351,6 +356,7 @@ export const allAnswer = (command, found, { limit, page, again }, exact = false)
351
356
  ...notes.next,
352
357
  ...messages.next.filter((step) => !notes.next.some((asked) => JSON.stringify(asked) === JSON.stringify(step))),
353
358
  ...archive.next,
359
+ ...limited.next,
354
360
  ],
355
361
  missing: [
356
362
  ...(scan
@@ -365,7 +371,8 @@ export const allAnswer = (command, found, { limit, page, again }, exact = false)
365
371
  ...notes.missing,
366
372
  ...messages.missing,
367
373
  ...archive.missing,
374
+ ...limited.missing,
368
375
  ],
369
- meta: metaOf(found.accounts, found.skipped, archive.missing.length > 0),
376
+ meta: metaOf(found.accounts, found.skipped, archive.missing.length > 0 || skips.length > 0),
370
377
  });
371
378
  };
@@ -10,6 +10,7 @@ import { runsSearchCommand } from "../runs/search-command.js";
10
10
  import { commandAt } from "../steps.js";
11
11
  import { messengerContext } from "./context.js";
12
12
  import { conversationsSearchCommand } from "./conversations-command.js";
13
+ import { historySkipsOf } from "./history-gaps.js";
13
14
  import { messagesSearchCommand } from "./messages-search-command.js";
14
15
  import { allAnswer, meaningNote, notesAnswer, offsetOf, SOURCE_SEARCHED, shownOf, } from "./search-answers.js";
15
16
  import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
@@ -92,7 +93,8 @@ const allCommand = (messenger) => {
92
93
  ...(exact ? { exact: true } : {}),
93
94
  ...(timezone === undefined ? {} : { timezone }),
94
95
  };
95
- renderAnswer(this, context, allAnswer("search all", found, { limit, page: page ?? 1, again }), () => {
96
+ const skips = await context.withStore((store) => historySkipsOf(store, found.accounts)).catch(() => []);
97
+ renderAnswer(this, context, allAnswer("search all", found, { limit, page: page ?? 1, again }, false, skips), () => {
96
98
  if (found.items.length === 0)
97
99
  context.renderer.note(found.server && !found.server.skipped
98
100
  ? "nothing found in the local store or on the messenger's server"
@@ -2,6 +2,7 @@ import type { Id } from "../../domain/models.js";
2
2
  import { type MessengerAdapter } from "../../ports/messenger.js";
3
3
  import type { MessengerTraits } from "../../ports/traits.js";
4
4
  import type { EventSink, Warn } from "../../runs/events.js";
5
+ import { type HistoryLimits } from "../../services/history-limits.js";
5
6
  import type { AccountKey, DeletionScope, MessageStore } from "../../store/store.js";
6
7
  export interface Saving {
7
8
  account: AccountKey;
@@ -41,6 +42,10 @@ export interface Saving {
41
42
  namesFromStore?: Pick<MessengerTraits, "savedChatId">;
42
43
  /** `MessengerTraits.unreadCounts: "store"`: a message `watch` or `listen` saves raises its chat's unread count. */
43
44
  countsUnread?: boolean;
45
+ /** Pushed history (`feed`, `listenHistory`) outside these is not saved, and each chat records what it left out. */
46
+ historyLimits?: HistoryLimits;
47
+ /** The app command and profile, for the warning that names the command listing what the limits left out. */
48
+ command?: string;
44
49
  }
45
50
  /**
46
51
  * The adapter, with what each read answers written to the store. **A failed write never fails the
@@ -50,4 +55,4 @@ export interface Saving {
50
55
  * would erase what `chats list` stored. Nor are `chat` and `contact`: a card's member list is not
51
56
  * known to be whole, and `saveMembers` replaces the list.
52
57
  */
53
- export declare const stored: (messenger: MessengerAdapter, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing, closing, namesFromStore, recipientIds, countsUnread, }: Saving) => MessengerAdapter;
58
+ export declare const stored: (messenger: MessengerAdapter, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing, closing, namesFromStore, recipientIds, countsUnread, historyLimits, command, }: Saving) => MessengerAdapter;