@wirecat/cli-core 0.545.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 (88) hide show
  1. package/CHANGELOG.md +76 -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/messages-command.js +2 -19
  34. package/dist/cli/messenger/messages-edit-command.js +1 -0
  35. package/dist/cli/messenger/messages-press-command.js +1 -0
  36. package/dist/cli/messenger/messages-send-command.js +1 -0
  37. package/dist/cli/messenger/observed.js +2 -2
  38. package/dist/cli/messenger/polls-command.js +1 -0
  39. package/dist/cli/messenger/private-people-command.js +5 -1
  40. package/dist/cli/messenger/stored.js +56 -3
  41. package/dist/cli/messenger/topics-command.js +1 -1
  42. package/dist/cli/program.js +17 -6
  43. package/dist/cli/schedule/schedule-command.js +1 -0
  44. package/dist/domain/message-link.d.ts +3 -0
  45. package/dist/domain/message-link.js +9 -14
  46. package/dist/domain/money-words.d.ts +11 -0
  47. package/dist/domain/money-words.js +38 -0
  48. package/dist/domain/providers.d.ts +2 -0
  49. package/dist/domain/providers.js +15 -3
  50. package/dist/domain/ref-arguments.d.ts +31 -0
  51. package/dist/domain/ref-arguments.js +79 -0
  52. package/dist/domain/references.d.ts +20 -2
  53. package/dist/domain/references.js +132 -47
  54. package/dist/errors/reasons.js +18 -0
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/meetings/mcp/index.d.ts +15 -15
  58. package/dist/oauth/client-file.d.ts +14 -0
  59. package/dist/oauth/client-file.js +28 -0
  60. package/dist/oauth/index.d.ts +1 -0
  61. package/dist/oauth/index.js +1 -0
  62. package/dist/ports/messenger.d.ts +3 -2
  63. package/dist/resolve.d.ts +3 -1
  64. package/dist/resolve.js +8 -0
  65. package/dist/search/task-signature.d.ts +5 -1
  66. package/dist/search/task-signature.js +27 -9
  67. package/dist/services/agenda.d.ts +5 -0
  68. package/dist/services/agenda.js +3 -1
  69. package/dist/services/index.d.ts +5 -1
  70. package/dist/services/index.js +5 -1
  71. package/dist/services/message-resolution.d.ts +4 -1
  72. package/dist/services/message-resolution.js +19 -3
  73. package/dist/services/messages.js +15 -13
  74. package/dist/services/money-words.d.ts +3 -0
  75. package/dist/services/money-words.js +5 -0
  76. package/dist/services/questions.d.ts +7 -2
  77. package/dist/services/questions.js +13 -4
  78. package/dist/services/stored-project-gathering.d.ts +9 -0
  79. package/dist/services/stored-project-gathering.js +9 -4
  80. package/dist/services/task-categories.d.ts +2 -0
  81. package/dist/services/task-categories.js +19 -8
  82. package/dist/store/sqlite/evidence-sources.js +2 -2
  83. package/dist/store/sqlite/identities.js +1 -1
  84. package/dist/store/sqlite/knowledge.js +7 -2
  85. package/dist/store/sqlite/notes.js +4 -4
  86. package/dist/store/sqlite/things.d.ts +0 -1
  87. package/dist/store/sqlite/things.js +25 -24
  88. 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`],
@@ -97,9 +97,8 @@ export const messagesCommand = (messenger) => {
97
97
  annotate(list, { output: { entity: "message" }, query: history });
98
98
  const readWindow = async (command, chat, message, window) => {
99
99
  const context = messengerContext(command, messenger);
100
- const target = targetOf(messenger, chat, message);
101
100
  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),
101
+ found: await services.messages.around(chat, message?.trim(), window),
103
102
  reader: await services.messages.reader(),
104
103
  }));
105
104
  const hearing = await hearForCommand(context, messenger, found, false);
@@ -156,9 +155,8 @@ export const messagesCommand = (messenger) => {
156
155
  const requested = threadRequest(this).thread;
157
156
  if (requested) {
158
157
  const context = messengerContext(this, messenger);
159
- const target = targetOf(messenger, chat, message);
160
158
  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 }),
159
+ found: await services.messages.thread(chat, message?.trim(), { ...requested, before, after }),
162
160
  reader: await services.messages.reader(),
163
161
  }));
164
162
  if (found.fallback)
@@ -203,19 +201,4 @@ const count = (value) => {
203
201
  throw new CliError("validation_error", `"${value}" is not a count`);
204
202
  return parsed;
205
203
  };
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
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
  })
@@ -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")
@@ -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")
@@ -13,6 +13,7 @@ import { recorded, wasSettled } from "../runs/recording.js";
13
13
  import { runContextOf } from "../runs/run-context.js";
14
14
  import { processRun, useAsProcessRun } from "../runs/scope.js";
15
15
  import { AUTHORITY_KEY, BOT_API_KEY, isBotApi, keyForCommand } from "../sends/permissions.js";
16
+ import { liftOptionsAfterDashes } from "./after-dashes.js";
16
17
  import { logParsedArgv, logVersion } from "./argv-log.js";
17
18
  import { commandAvailability } from "./command-availability.js";
18
19
  import { commandPathOf } from "./command-contract.js";
@@ -135,15 +136,16 @@ export const run = async (argv, definition, options = {}) => {
135
136
  useAsProcessRun(outer);
136
137
  }
137
138
  };
139
+ const isMachine = (words) => words.some((word) => word === "--json" || word.startsWith("--json=") || word === "--jsonl");
138
140
  const runOnce = async (argv, definition, options) => {
139
141
  const streams = options.streams ?? processStreams;
140
142
  if (!options.streams)
141
143
  quietBrokenPipes();
142
144
  const delimiter = argv.indexOf("--");
143
145
  const flags = argv.slice(0, delimiter < 0 ? undefined : delimiter);
144
- const machine = flags.some((word) => word === "--json" || word.startsWith("--json=") || word === "--jsonl");
145
- const debug = flags.includes("--debug");
146
- const reporting = machine ? { ...options, tty: false } : options;
146
+ let machine = isMachine(flags);
147
+ let debug = flags.includes("--debug");
148
+ let reporting = machine ? { ...options, tty: false } : options;
147
149
  const control = execution(streams, { maxOutputBytes: 0 });
148
150
  let restoreInput;
149
151
  let signalExit;
@@ -439,12 +441,21 @@ const runOnce = async (argv, definition, options) => {
439
441
  forEachCommand(program, (child) => child.exitOverride());
440
442
  // Before commander, not inside it: commander has no hook that runs before it decides which
441
443
  // subcommand it is looking at.
442
- const { profile, rest: lifted } = liftProfile(argv, commandWords(program), valuedOptions(program));
444
+ const profiled = liftProfile(argv, commandWords(program), valuedOptions(program));
445
+ const { profile } = profiled;
446
+ // An agent's call builds its own argv, with its values after `--` on purpose: none of them may become an option.
447
+ const lifted = options.agent
448
+ ? profiled.rest
449
+ : liftOptionsAfterDashes(profiled.rest, (words) => commandAt(program, commandPath(program, words)));
450
+ const liftedEnd = lifted.indexOf("--");
451
+ const liftedFlags = lifted.slice(0, liftedEnd < 0 ? undefined : liftedEnd);
452
+ machine ||= isMachine(liftedFlags);
453
+ debug ||= liftedFlags.includes("--debug");
454
+ reporting = machine ? { ...options, tty: false } : options;
443
455
  // `wa 'chats list' --help` reads "chats list" as a profile and prints the root help with exit 0.
444
456
  try {
445
457
  // Only before `--`: what follows is a positional, an agent's text among them, never a command word.
446
- const end = lifted.indexOf("--");
447
- for (const words of [flags, end < 0 ? lifted : lifted.slice(0, end)])
458
+ for (const words of [flags, liftedFlags])
448
459
  refuseJoinedPath(program, words.filter((word) => !word.startsWith("-")), profile !== undefined && !/\s/.test(profile) ? [profile] : []);
449
460
  }
450
461
  catch (failure) {
@@ -110,6 +110,7 @@ export const scheduleCommandFor = ({ app, context: contextOf }) => {
110
110
  return { context, system, state, say, timer, environment, save };
111
111
  };
112
112
  withWhen(grantsAuthority(annotate(command.command("command"), {
113
+ literal_after_dashes: true,
113
114
  effects: LOCAL_WRITE,
114
115
  examples: [
115
116
  `${app.command} personal schedule command health --every 1h -- doctor`,
@@ -24,13 +24,16 @@ export type MessageLink = MessagePermalink & {
24
24
  files?: FileLink[];
25
25
  };
26
26
  export declare const fileLinks: (attachments: readonly Attachment[]) => FileLink[];
27
+ /** A chat and a message id, or one reference naming both; a reference's account is for the caller to check. */
27
28
  export declare const messageLinkTarget: (reference: string, message: string | undefined, provider: string) => {
28
29
  chat: string;
29
30
  message: string;
30
31
  account: string;
32
+ text: string;
31
33
  } | {
32
34
  chat: string;
33
35
  message: string;
34
36
  account: undefined;
37
+ text: string;
35
38
  };
36
39
  export declare const validatePermalink: (link: MessagePermalink) => MessagePermalink;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { hitFiles } from "./attachments.js";
3
- import { isLocator, parseLocator } from "./locator.js";
3
+ import { chatReferenceOf } from "./ref-arguments.js";
4
4
  export const fileLinks = (attachments) => hitFiles(attachments).map(({ position, kind, name }) => ({
5
5
  position,
6
6
  kind,
@@ -8,28 +8,23 @@ export const fileLinks = (attachments) => hitFiles(attachments).map(({ position,
8
8
  url: null,
9
9
  reason: "unsupported_provider",
10
10
  }));
11
+ /** A chat and a message id, or one reference naming both; a reference's account is for the caller to check. */
11
12
  export const messageLinkTarget = (reference, message, provider) => {
12
- if (isLocator(reference)) {
13
+ const named = chatReferenceOf(reference, provider, "message");
14
+ if (named?.message !== undefined) {
13
15
  if (message !== undefined)
14
16
  throw new CliError("validation_error", "a locator already names the message");
15
- let target;
16
- try {
17
- target = parseLocator(reference);
18
- }
19
- catch {
20
- throw new CliError("validation_error", "expected msg:<provider>/<account>/<chat>/<message>");
21
- }
22
- if (target.provider !== provider)
23
- throw new CliError("validation_error", "that locator belongs to another messenger");
24
- validMessageId(target.message);
25
- return { chat: target.chat, message: target.message, account: target.account };
17
+ validMessageId(named.message);
18
+ return { chat: named.chat, message: named.message, account: named.account, text: named.text };
26
19
  }
27
20
  if (message === undefined)
28
21
  throw new CliError("validation_error", "which message? give its id after the chat");
29
22
  validMessageId(message);
23
+ if (named)
24
+ return { chat: named.chat, message, account: named.account, text: named.text };
30
25
  if (!reference.trim())
31
26
  throw new CliError("validation_error", "which chat? give its id or title");
32
- return { chat: reference, message, account: undefined };
27
+ return { chat: reference, message, account: undefined, text: reference };
33
28
  };
34
29
  const validMessageId = (id) => {
35
30
  if (!id ||
@@ -0,0 +1,11 @@
1
+ import type { TaskCategory } from "./task-categories.js";
2
+ /**
3
+ * Verb classes for reading money in a chat, matched by stem as task categories are. A stem cannot tell the imperative
4
+ * from the past tense (`оплати`, `оплатил` → `оплат`) or from a promise (`pagué`, `pagaré` → `pag`), so a stem that
5
+ * several classes share is listed once, under the request, and a word only counts where its stem is its own. The
6
+ * signal that reads these keeps the sender's side (owner or other) to decide the direction.
7
+ */
8
+ export declare const MONEY_REQUEST = "money-request";
9
+ export declare const MONEY_DONE = "money-done";
10
+ export declare const PROMISE = "promise";
11
+ export declare const MONEY_WORDS: readonly TaskCategory[];
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Verb classes for reading money in a chat, matched by stem as task categories are. A stem cannot tell the imperative
3
+ * from the past tense (`оплати`, `оплатил` → `оплат`) or from a promise (`pagué`, `pagaré` → `pag`), so a stem that
4
+ * several classes share is listed once, under the request, and a word only counts where its stem is its own. The
5
+ * signal that reads these keeps the sender's side (owner or other) to decide the direction.
6
+ */
7
+ export const MONEY_REQUEST = "money-request";
8
+ export const MONEY_DONE = "money-done";
9
+ export const PROMISE = "promise";
10
+ export const MONEY_WORDS = [
11
+ {
12
+ name: MONEY_REQUEST,
13
+ meaning: "asks for a payment or sends an invoice",
14
+ triggers: {
15
+ en: ["pay", "send", "transfer", "invoice", "owe", "refund", "remit"],
16
+ ru: ["переведи", "оплати", "заплати", "вышли", "отправь", "скинь", "верни", "счёт", "выстави", "должен"],
17
+ es: ["envía", "transfiere", "factura", "debes", "devuelve", "abona"],
18
+ },
19
+ },
20
+ {
21
+ name: MONEY_DONE,
22
+ meaning: "says a payment was made or received",
23
+ triggers: {
24
+ en: ["paid", "sent", "received", "settled"],
25
+ ru: ["перевёл", "перевела", "выслал", "вернул", "скинул", "получил", "получила"],
26
+ es: ["recibido"],
27
+ },
28
+ },
29
+ {
30
+ name: PROMISE,
31
+ meaning: "promises to pay or send later",
32
+ triggers: {
33
+ en: ["promise", "promised", "guarantee"],
34
+ ru: ["оплачу", "заплачу", "отправлю", "обещаю", "гарантирую"],
35
+ es: ["prometo", "transferiré", "devolveré"],
36
+ },
37
+ },
38
+ ];
@@ -11,6 +11,8 @@ export interface ProviderEntry {
11
11
  }
12
12
  /** The seed: what cli-core knows before any CLI has written its entry into the store. */
13
13
  export declare const PROVIDERS: readonly ProviderEntry[];
14
+ /** How a text names a provider to a person: `MAX`, not `max`. */
15
+ export declare const providerLabel: (provider: Provider) => string;
14
16
  /**
15
17
  * A notes folder is an `accounts` row of one of these providers; people call every one `notes`. `obsidian` is a vault,
16
18
  * `local` a plain folder (store version 32). `folder` is what they were before, and what Drive's stand-in folders and
@@ -3,11 +3,11 @@ import { CliError } from "@wirecat/cli-base";
3
3
  export const KINDS = ["messages", "mail", "notes", "documents", "meetings", "events"];
4
4
  /** The seed: what cli-core knows before any CLI has written its entry into the store. */
5
5
  export const PROVIDERS = [
6
- { name: "telegram", kind: "messages", aliases: ["tg"] },
6
+ { name: "telegram", kind: "messages", aliases: ["tg"], command: "tg" },
7
7
  { name: "telegram-bot", kind: "messages", aliases: ["tg-bot"] },
8
- { name: "max", kind: "messages", aliases: [] },
8
+ { name: "max", kind: "messages", aliases: [], command: "max" },
9
9
  { name: "max-bot", kind: "messages", aliases: [] },
10
- { name: "whatsapp", kind: "messages", aliases: ["wa"] },
10
+ { name: "whatsapp", kind: "messages", aliases: ["wa"], command: "wa" },
11
11
  { name: "whatsapp-bot", kind: "messages", aliases: ["wa-bot"] },
12
12
  { name: "email", kind: "mail", aliases: [] },
13
13
  { name: "notes", kind: "notes", aliases: [] },
@@ -15,6 +15,18 @@ export const PROVIDERS = [
15
15
  { name: "obsidian", kind: "notes", aliases: [] },
16
16
  { name: "zoom", kind: "meetings", aliases: [] },
17
17
  ];
18
+ const LABELS = {
19
+ telegram: "Telegram",
20
+ "telegram-bot": "Telegram bot",
21
+ max: "MAX",
22
+ "max-bot": "MAX bot",
23
+ whatsapp: "WhatsApp",
24
+ "whatsapp-bot": "WhatsApp bot",
25
+ "google-drive": "Google Drive",
26
+ email: "mail",
27
+ };
28
+ /** How a text names a provider to a person: `MAX`, not `max`. */
29
+ export const providerLabel = (provider) => LABELS[provider] ?? provider;
18
30
  /**
19
31
  * A notes folder is an `accounts` row of one of these providers; people call every one `notes`. `obsidian` is a vault,
20
32
  * `local` a plain folder (store version 32). `folder` is what they were before, and what Drive's stand-in folders and
@@ -0,0 +1,31 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import type { Id, Provider } from "./models.js";
3
+ /** A `chat:` or `msg:` reference given where a messenger command takes a chat or a message. */
4
+ export interface ChatReference {
5
+ text: string;
6
+ provider: Provider;
7
+ account: Id;
8
+ chat: Id;
9
+ message?: Id;
10
+ }
11
+ /** The account a reference must belong to; `account` is `undefined` while this profile's is not known yet. */
12
+ export interface RefScope {
13
+ account: Id | undefined;
14
+ profile?: string;
15
+ }
16
+ /** The profiles of this app the store saw on an account; `undefined` when that cannot be told. */
17
+ export type ProfilesOn = (account: Id) => Promise<readonly string[] | undefined>;
18
+ /**
19
+ * The chat or message a reference names, when `text` is one of this provider's; `undefined` for text that is no
20
+ * reference, which stays an id or a title. Another kind or another messenger is refused, never read as a title.
21
+ */
22
+ export declare const chatReferenceOf: (text: string, provider: Provider, takes?: "chat" | "message") => ChatReference | undefined;
23
+ /**
24
+ * The identity a `contact:` reference names, when it is one of this provider's; `undefined` for any other text. A
25
+ * `person:` reference is the store's person, which the callers that take one read before this.
26
+ */
27
+ export declare const contactReferenceOf: (text: string, provider: Provider | undefined) => Id | undefined;
28
+ /** The refusal of another account's reference: switching to it silently would read or send as somebody else. */
29
+ export declare const wrongAccount: (reference: Pick<ChatReference, "text" | "account">, scope: RefScope, profiles?: readonly string[]) => CliError;
30
+ /** Refuses a reference of another account than the scope's, naming the profiles that have it. */
31
+ export declare const ensureAccount: (reference: Pick<ChatReference, "text" | "account">, scope: RefScope, profilesOn?: ProfilesOn) => Promise<void>;