@wirecat/cli-core 0.347.0 → 0.348.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.
- package/CHANGELOG.md +54 -0
- package/dist/cli/bot/copy.d.ts +1 -1
- package/dist/cli/bot/copy.js +1 -1
- package/dist/cli/embedding-choice.js +2 -1
- package/dist/cli/envelope.d.ts +4 -0
- package/dist/cli/envelope.js +4 -3
- package/dist/cli/guide.js +4 -0
- package/dist/cli/help/first-screen.js +13 -1
- package/dist/cli/help/sections.d.ts +4 -3
- package/dist/cli/help/sections.js +5 -10
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/messenger/backfill-command.js +6 -6
- package/dist/cli/messenger/context.d.ts +3 -3
- package/dist/cli/messenger/context.js +44 -17
- package/dist/cli/messenger/doctor-command.js +3 -3
- package/dist/cli/messenger/flooded.js +1 -1
- package/dist/cli/messenger/mcp-command.js +1 -1
- package/dist/cli/messenger/messages-delete-command.js +2 -1
- package/dist/cli/messenger/observed.js +21 -7
- package/dist/cli/messenger/schedule-command.js +4 -3
- package/dist/cli/messenger/serve-relay.js +1 -0
- package/dist/cli/messenger/store-maintenance-command.js +3 -3
- package/dist/cli/messenger/stored.js +36 -30
- package/dist/cli/program.js +10 -9
- package/dist/cli/runs/command.js +3 -2
- package/dist/cli/steps.d.ts +4 -0
- package/dist/cli/steps.js +9 -0
- package/dist/cli/store-settings.js +4 -3
- package/dist/domain/models.d.ts +11 -1
- package/dist/kit/contract.d.ts +2 -2
- package/dist/kit/contract.js +25 -0
- package/dist/kit/fake.d.ts +1 -0
- package/dist/kit/fake.js +15 -16
- package/dist/mcp/meeting-preparation.d.ts +1 -0
- package/dist/mcp/meeting-project-candidates.d.ts +1 -0
- package/dist/mcp/meeting-series-inventory.d.ts +1 -0
- package/dist/mcp/tool.js +2 -0
- package/dist/mcp/tools/messages-photo.js +2 -1
- package/dist/meetings/mcp/passage.d.ts +1 -0
- package/dist/meetings/mcp/project-evidence.d.ts +1 -0
- package/dist/meetings/mcp/search.d.ts +1 -0
- package/dist/meetings/reads.d.ts +1 -0
- package/dist/ports/messenger.d.ts +10 -2
- package/dist/runs/recording.js +1 -0
- package/dist/runs/scope.d.ts +7 -0
- package/dist/runs/scope.js +5 -0
- package/dist/services/archive.js +2 -1
- package/dist/services/chats.js +3 -2
- package/dist/services/conversations.js +2 -1
- package/dist/services/deps.js +2 -1
- package/dist/services/embeddings-maintenance.js +3 -2
- package/dist/services/index.d.ts +1 -1
- package/dist/services/search-all-meetings.d.ts +3 -4
- package/dist/services/search-all.d.ts +8 -1
- package/dist/services/search-all.js +1 -1
- package/dist/speech/transcribe.js +2 -1
- package/dist/store/chats.d.ts +2 -2
- package/dist/store/sqlite/chats.d.ts +2 -2
- package/dist/store/sqlite/chats.js +7 -7
- package/dist/store/types.d.ts +2 -2
- package/package.json +3 -3
|
@@ -15,7 +15,7 @@ import { drainNoteIndex, fillSearchIndex, fillStems, involvementStoreOver, noteI
|
|
|
15
15
|
import { environmentOf, outputFor } from "../context.js";
|
|
16
16
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
17
17
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
18
|
-
import { nextStep } from "../steps.js";
|
|
18
|
+
import { lineOf, nextStep } from "../steps.js";
|
|
19
19
|
import { answerOf } from "./ask.js";
|
|
20
20
|
import { ENCRYPT_OPTION, passwordOf } from "./password.js";
|
|
21
21
|
import { servingProfiles } from "./serve-command.js";
|
|
@@ -443,7 +443,7 @@ const reindexCommand = (messenger) => destroysNothing(annotate(new Command("rein
|
|
|
443
443
|
`${schema.minCompatible}; this one speaks ${SPEAKS}) — upgrade this tool`);
|
|
444
444
|
}
|
|
445
445
|
if (schema.version < SPEAKS) {
|
|
446
|
-
throw new CliError("validation_error", `the store is behind this build — \`${
|
|
446
|
+
throw new CliError("validation_error", `the store is behind this build — \`${lineOf(this, "store migrate")}\` first`);
|
|
447
447
|
}
|
|
448
448
|
resetSearchIndex(database);
|
|
449
449
|
const fileTexts = resetAttachmentWords(database);
|
|
@@ -591,7 +591,7 @@ const restoreFrom = async (command, messenger, backup, given) => {
|
|
|
591
591
|
// One that has not touched the store yet holds nothing open, and was not seen.
|
|
592
592
|
renderer.note("restart every serve and mcp of either CLI that was running, so they read the restored store");
|
|
593
593
|
if (schema.version < SPEAKS) {
|
|
594
|
-
renderer.note(`the backup is behind this build; the next command migrates it — \`${
|
|
594
|
+
renderer.note(`the backup is behind this build; the next command migrates it — \`${lineOf(command, "store migrate")}\` now`);
|
|
595
595
|
}
|
|
596
596
|
};
|
|
597
597
|
const backupSchema = async (backup) => {
|
|
@@ -49,6 +49,36 @@ export const stored = (messenger, { account, store, warn, events, pending, delet
|
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
const saveBatches = (operation, onBatch) => {
|
|
53
|
+
// In order: a batch's messages may belong to chats only an earlier batch named.
|
|
54
|
+
let previous = Promise.resolve();
|
|
55
|
+
return (batch) => {
|
|
56
|
+
if (namesFromStore) {
|
|
57
|
+
for (const { from, to } of batch.rekeys ?? []) {
|
|
58
|
+
const title = titles.get(from);
|
|
59
|
+
titles.delete(from);
|
|
60
|
+
if (title && !titles.has(to))
|
|
61
|
+
titles.set(to, title);
|
|
62
|
+
}
|
|
63
|
+
for (const chat of batch.chats ?? [])
|
|
64
|
+
if (chat.title)
|
|
65
|
+
titles.set(chat.id, chat.title);
|
|
66
|
+
}
|
|
67
|
+
// Each re-key apart: one refused leaves the chat split, never the batch's history unsaved.
|
|
68
|
+
const saving = previous.then(async () => {
|
|
69
|
+
for (const { from, to } of batch.rekeys ?? [])
|
|
70
|
+
await attempt("history.rekey", async (opened) => {
|
|
71
|
+
await opened.chats.rekeyChat(account, from, to);
|
|
72
|
+
await rekeyed?.(from, to);
|
|
73
|
+
});
|
|
74
|
+
await attempt(operation, (opened) => fill(opened, account, batch));
|
|
75
|
+
});
|
|
76
|
+
previous = saving;
|
|
77
|
+
pending?.add(saving);
|
|
78
|
+
void saving.finally(() => pending?.delete(saving));
|
|
79
|
+
onBatch(batch);
|
|
80
|
+
};
|
|
81
|
+
};
|
|
52
82
|
// A method not listed here saves nothing and passes through; a lane that should save adds its line.
|
|
53
83
|
return throughWrapper(messenger, {
|
|
54
84
|
...(messenger.forumState
|
|
@@ -213,36 +243,12 @@ export const stored = (messenger, { account, store, warn, events, pending, delet
|
|
|
213
243
|
: {}),
|
|
214
244
|
...(messenger.feed
|
|
215
245
|
? {
|
|
216
|
-
feed: (onBatch, signal) =>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const title = titles.get(from);
|
|
223
|
-
titles.delete(from);
|
|
224
|
-
if (title && !titles.has(to))
|
|
225
|
-
titles.set(to, title);
|
|
226
|
-
}
|
|
227
|
-
for (const chat of batch.chats ?? [])
|
|
228
|
-
if (chat.title)
|
|
229
|
-
titles.set(chat.id, chat.title);
|
|
230
|
-
}
|
|
231
|
-
// Each re-key apart: one refused leaves the chat split, never the batch's history unsaved.
|
|
232
|
-
const saving = previous.then(async () => {
|
|
233
|
-
for (const { from, to } of batch.rekeys ?? [])
|
|
234
|
-
await attempt("history.rekey", async (opened) => {
|
|
235
|
-
await opened.chats.rekeyChat(account, from, to);
|
|
236
|
-
await rekeyed?.(from, to);
|
|
237
|
-
});
|
|
238
|
-
await attempt("history.feed", (opened) => fill(opened, account, batch));
|
|
239
|
-
});
|
|
240
|
-
previous = saving;
|
|
241
|
-
pending?.add(saving);
|
|
242
|
-
void saving.finally(() => pending?.delete(saving));
|
|
243
|
-
onBatch(batch);
|
|
244
|
-
}, signal) ?? Promise.resolve());
|
|
245
|
-
},
|
|
246
|
+
feed: (onBatch, signal) => messenger.feed?.(saveBatches("history.feed", onBatch), signal) ?? Promise.resolve(),
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
249
|
+
...(messenger.listenHistory
|
|
250
|
+
? {
|
|
251
|
+
listenHistory: (onBatch, signal) => messenger.listenHistory?.(saveBatches("history.listen", onBatch), signal) ?? Promise.resolve(),
|
|
246
252
|
}
|
|
247
253
|
: {}),
|
|
248
254
|
...(messenger.edit
|
package/dist/cli/program.js
CHANGED
|
@@ -19,7 +19,6 @@ import { changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTarge
|
|
|
19
19
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
20
20
|
import { byteCount, DEFAULT_COMMAND_MS, DEFAULT_OUTPUT_BYTES, execution } from "./execution.js";
|
|
21
21
|
import { installHelp } from "./help/first-screen.js";
|
|
22
|
-
import { sectionCoreGroups } from "./help/sections.js";
|
|
23
22
|
import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
|
|
24
23
|
import { checkJson, liftJson } from "./json-fields.js";
|
|
25
24
|
import { movedPath, movedPointer } from "./moved.js";
|
|
@@ -27,7 +26,7 @@ import { PreviewComplete, preview } from "./preview.js";
|
|
|
27
26
|
import { commandWords, liftProfile, PROFILE_FREE, valuedOptions } from "./profile.js";
|
|
28
27
|
import { absolutePaths, relayTarget } from "./relay-route.js";
|
|
29
28
|
import { settingsFor } from "./settings.js";
|
|
30
|
-
import { commandAt } from "./steps.js";
|
|
29
|
+
import { commandAt, lineOf, withLines } from "./steps.js";
|
|
31
30
|
/**
|
|
32
31
|
* The command tree, built fresh on each call. Commander is global state by default —
|
|
33
32
|
* `exitOverride` and the write hooks turn it into a value a test can drive.
|
|
@@ -63,13 +62,13 @@ export const createProgram = ({ app, commands, configure, firstScreen }, { out,
|
|
|
63
62
|
for (const command of commands())
|
|
64
63
|
program.addCommand(command);
|
|
65
64
|
configure?.(program);
|
|
66
|
-
sectionCoreGroups(program);
|
|
67
65
|
installHelp(program, { pitch: app.description, ...firstScreen });
|
|
68
66
|
// Commander builds a usage line from the command tree, where the profile is not a word.
|
|
69
67
|
for (const top of program.commands) {
|
|
70
68
|
if (PROFILE_FREE.has(top.name()))
|
|
71
69
|
continue;
|
|
72
70
|
forEachCommand(top, (command) => command.configureHelp({
|
|
71
|
+
...command.configureHelp(),
|
|
73
72
|
commandUsage: (shown) => `${app.command} <profile>${new Help().commandUsage(shown).slice(app.command.length)}`,
|
|
74
73
|
}));
|
|
75
74
|
}
|
|
@@ -327,7 +326,7 @@ export const run = async (argv, definition, options = {}) => {
|
|
|
327
326
|
}
|
|
328
327
|
// Not inside the scheduled run itself: its own failure is already on its way to last.json.
|
|
329
328
|
if (!flags.quiet && commandEnv[envName(definition.app, "SCHEDULED")] === undefined && path[0] !== "schedule") {
|
|
330
|
-
const notice = failingNoticeFor(definition.app, commandEnv);
|
|
329
|
+
const notice = failingNoticeFor(definition.app, commandEnv, program);
|
|
331
330
|
if (notice)
|
|
332
331
|
streams.diagnostic(notice);
|
|
333
332
|
}
|
|
@@ -450,10 +449,10 @@ const quietBrokenPipes = () => {
|
|
|
450
449
|
throw error;
|
|
451
450
|
});
|
|
452
451
|
};
|
|
453
|
-
const failingNoticeFor = (app, env) => {
|
|
452
|
+
const failingNoticeFor = (app, env, program) => {
|
|
454
453
|
try {
|
|
455
454
|
const state = resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state;
|
|
456
|
-
return failingNotice(app.command, scheduledRuns(state),
|
|
455
|
+
return failingNotice(app.command, scheduledRuns(state), lineOf(program, "schedule list"));
|
|
457
456
|
}
|
|
458
457
|
catch {
|
|
459
458
|
return undefined;
|
|
@@ -572,6 +571,7 @@ const failed = ({ app }, program, rest, error, settled, debug) => {
|
|
|
572
571
|
return {
|
|
573
572
|
object,
|
|
574
573
|
cli: app.command,
|
|
574
|
+
program,
|
|
575
575
|
...(debug
|
|
576
576
|
? { record: errorRecordOf(failure, object, { command, service: app.command, version: app.version }) }
|
|
577
577
|
: {}),
|
|
@@ -581,12 +581,13 @@ const failed = ({ app }, program, rest, error, settled, debug) => {
|
|
|
581
581
|
* **A failure never reaches stdout.** It goes to stderr — as JSON when nobody is watching, because
|
|
582
582
|
* an exit code says which kind of thing went wrong and nothing about which chat or how long to wait.
|
|
583
583
|
*/
|
|
584
|
-
const report = (streams, options, { object, record, cli }) => {
|
|
584
|
+
const report = (streams, options, { object, record, cli, program }) => {
|
|
585
585
|
if (options.agent) {
|
|
586
586
|
options.agent.failure(object);
|
|
587
587
|
return exitCodeOfError(object);
|
|
588
588
|
}
|
|
589
589
|
const interactive = options.tty ?? process.stdout.isTTY === true;
|
|
590
|
-
|
|
591
|
-
|
|
590
|
+
const lined = object.error.next ? { error: { ...object.error, next: withLines(program, object.error.next) } } : object;
|
|
591
|
+
streams.diagnostic(renderError(lined, { cli, interactive, ...(record ? { record } : {}) }));
|
|
592
|
+
return exitCodeOfError(lined);
|
|
592
593
|
};
|
package/dist/cli/runs/command.js
CHANGED
|
@@ -6,6 +6,7 @@ import { environmentOf, outputFor } from "../context.js";
|
|
|
6
6
|
import { LOCAL_READ, NO_EFFECTS } from "../effects.js";
|
|
7
7
|
import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
8
8
|
import { positiveCount } from "../paging.js";
|
|
9
|
+
import { lineOf } from "../steps.js";
|
|
9
10
|
import { runsFound, searchRuns } from "./search.js";
|
|
10
11
|
/** Reading the log is not itself worth recording, so **nothing here starts a run**. */
|
|
11
12
|
export const runsCommand = (app) => {
|
|
@@ -14,7 +15,7 @@ export const runsCommand = (app) => {
|
|
|
14
15
|
const runOrRefuse = (from, id) => {
|
|
15
16
|
const found = findRun(dir(from), id);
|
|
16
17
|
if (!found)
|
|
17
|
-
throw new CliError("not_found", `no recorded run "${id}" — \`${
|
|
18
|
+
throw new CliError("not_found", `no recorded run "${id}" — \`${lineOf(from, "runs list")}\` shows them`);
|
|
18
19
|
return found;
|
|
19
20
|
};
|
|
20
21
|
annotate(command.command("list"), { effects: LOCAL_READ, mcp: false })
|
|
@@ -50,7 +51,7 @@ export const runsCommand = (app) => {
|
|
|
50
51
|
events: shown,
|
|
51
52
|
}));
|
|
52
53
|
if (shown.length < events.length)
|
|
53
|
-
renderer.note(`${events.length - shown.length} other events — \`${
|
|
54
|
+
renderer.note(`${events.length - shown.length} other events — \`${lineOf(this, `runs show ${id}`)}\` shows them all`);
|
|
54
55
|
});
|
|
55
56
|
annotate(command.command("search"), { effects: LOCAL_READ })
|
|
56
57
|
.description("search recorded diagnostic metadata and events; no message contents")
|
package/dist/cli/steps.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare const STEPS: ({
|
|
|
20
20
|
})[];
|
|
21
21
|
/** A step from its registered definition; an argument left undefined is left out, as the command line would. */
|
|
22
22
|
export declare const nextStep: (id: string, input: StepInput) => NextStep;
|
|
23
|
+
/** `typedCommand` with the profile this command line named, else the run's. */
|
|
24
|
+
export declare const lineOf: (command: Command, words: string) => string;
|
|
23
25
|
export declare const rootOf: (command: Command) => Command;
|
|
24
26
|
/** The command a path names in this program, or nothing when this CLI does not have it. */
|
|
25
27
|
export declare const commandAt: (root: Command, path: string) => Command | undefined;
|
|
@@ -35,6 +37,8 @@ type Targets = NonNullable<StepInput["targets"]>;
|
|
|
35
37
|
export declare const runnable: (root: Command, steps: readonly NextStep[] | undefined) => NextStep[];
|
|
36
38
|
/** Each command step judged by its command's effects: safe to run unasked only when it writes nothing remotely. */
|
|
37
39
|
export declare const withSafety: (steps: readonly NextStep[], targets: Targets) => NextStep[];
|
|
40
|
+
/** Each command step with `line`, the whole command to type — only the CLI knows the profile, so MCP gets none. */
|
|
41
|
+
export declare const withLines: (root: Command, steps: readonly NextStep[]) => NextStep[];
|
|
38
42
|
/**
|
|
39
43
|
* The step as a person types it: the profile the command ran with, positional arguments in their place, then the
|
|
40
44
|
* options. A CLI that assumes no profile refuses the line without it.
|
package/dist/cli/steps.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { STEPS as BASE_STEPS, defineSteps, MAX_NEXT_STEPS, stepOf, writes, } from "@wirecat/cli-base/standards";
|
|
2
|
+
import { typedCommand } from "../runs/scope.js";
|
|
2
3
|
import { judgedTargets } from "./effects.js";
|
|
3
4
|
/**
|
|
4
5
|
* Every step an answer or an error may name. A step's command must exist in tg and max alike, and its
|
|
@@ -211,6 +212,12 @@ export const nextStep = (id, input) => {
|
|
|
211
212
|
const args = Object.fromEntries(Object.entries(input.args ?? {}).filter(([, value]) => value !== undefined));
|
|
212
213
|
return stepOf(definition, { ...input, args });
|
|
213
214
|
};
|
|
215
|
+
/** `typedCommand` with the profile this command line named, else the run's. */
|
|
216
|
+
export const lineOf = (command, words) => {
|
|
217
|
+
const root = rootOf(command);
|
|
218
|
+
const profile = root.getOptionValue("profile");
|
|
219
|
+
return typedCommand(root.name(), words, typeof profile === "string" && profile !== "" ? profile : undefined);
|
|
220
|
+
};
|
|
214
221
|
export const rootOf = (command) => {
|
|
215
222
|
let root = command;
|
|
216
223
|
while (root.parent)
|
|
@@ -239,6 +246,8 @@ export const withSafety = (steps, targets) => steps.map((step) => {
|
|
|
239
246
|
const effects = targets(step.command)?.effects;
|
|
240
247
|
return { ...step, safe_to_auto_execute: effects !== undefined && !writes(effects) };
|
|
241
248
|
});
|
|
249
|
+
/** Each command step with `line`, the whole command to type — only the CLI knows the profile, so MCP gets none. */
|
|
250
|
+
export const withLines = (root, steps) => steps.map((step) => (step.kind === "command" ? { ...step, line: stepLine(root, step) } : step));
|
|
242
251
|
const kebab = (name) => name.replaceAll("_", "-");
|
|
243
252
|
const quoted = (value) => {
|
|
244
253
|
const text = String(value);
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
3
|
import { DEFAULT_PHONE_COUNTRIES, parsePhoneCountries } from "../domain/phones.js";
|
|
4
4
|
import { DEFAULT_TASK_CATEGORIES, taskCategoryValue, unlistedNote, withTaskCategory, } from "../domain/task-categories.js";
|
|
5
|
+
import { typedCommand } from "../runs/scope.js";
|
|
5
6
|
import { ACCOUNT_GROUPS_SETTING, groupMembers, groupName } from "../search/account-list.js";
|
|
6
7
|
import { DEFAULT_STEMMERS, parseStemmers } from "../search/stem.js";
|
|
7
8
|
import { openCache } from "../store/open.js";
|
|
@@ -82,7 +83,7 @@ const changePhoneCountries = async (app, env, value) => {
|
|
|
82
83
|
value: (countries ?? DEFAULT_PHONE_COUNTRIES).join(","),
|
|
83
84
|
},
|
|
84
85
|
note: "store-wide — a number written without + is read in these countries, the first that fits; numbers already " +
|
|
85
|
-
`found keep their old reading until \`${app.command
|
|
86
|
+
`found keep their old reading until \`${typedCommand(app.command, "mentions rebuild phone")}\``,
|
|
86
87
|
rebuild: true,
|
|
87
88
|
};
|
|
88
89
|
};
|
|
@@ -150,7 +151,7 @@ const changeFirstPass = async (app, env, value) => {
|
|
|
150
151
|
return {
|
|
151
152
|
result: { store: storePath(env), scope: "store", setting: FIRST_PASS, value: now },
|
|
152
153
|
note: `store-wide — an account's first sync opens at most ${now} task(s) per chat from history; later syncs read ` +
|
|
153
|
-
`only new messages. \`${app.command
|
|
154
|
+
`only new messages. \`${typedCommand(app.command, "tasks scan <chat>")}\` builds tasks from a chat's history when asked`,
|
|
154
155
|
};
|
|
155
156
|
}
|
|
156
157
|
finally {
|
|
@@ -199,7 +200,7 @@ const changeGroup = async (app, env, setting, value) => {
|
|
|
199
200
|
return {
|
|
200
201
|
result: { store: storePath(env), scope: "store", setting, value: members ?? null },
|
|
201
202
|
note: `store-wide — \`--source group:${name}\` searches these accounts from every app; ` +
|
|
202
|
-
`a profile is found once its app has saved something (\`${app.command
|
|
203
|
+
`a profile is found once its app has saved something (\`${typedCommand(app.command, "config show")}\`)`,
|
|
203
204
|
};
|
|
204
205
|
}
|
|
205
206
|
finally {
|
package/dist/domain/models.d.ts
CHANGED
|
@@ -25,7 +25,10 @@ export interface Chat {
|
|
|
25
25
|
id: Id;
|
|
26
26
|
title: string | null;
|
|
27
27
|
kind: ChatKind;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* `null` means the provider did not say, which is not the same as zero. A save that does not know it leaves it
|
|
30
|
+
* out.
|
|
31
|
+
*/
|
|
29
32
|
unreadCount: number | null;
|
|
30
33
|
/** ISO 8601, or `null` when the chat has never had a message. */
|
|
31
34
|
lastMessageAt: string | null;
|
|
@@ -43,6 +46,13 @@ export interface Chat {
|
|
|
43
46
|
*/
|
|
44
47
|
providerMetadata?: ProviderMetadata;
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* A chat as an adapter saves it. A `title`, `unreadCount`, `lastMessageAt` or `participantsCount` left out keeps the
|
|
51
|
+
* stored value; `null` stores none. So a push that knows only some of them, such as a notice that a group left its
|
|
52
|
+
* community, leaves the rest out.
|
|
53
|
+
*/
|
|
54
|
+
export type ChatUpdate = Omit<Chat, KeptFields> & Partial<Pick<Chat, KeptFields>>;
|
|
55
|
+
type KeptFields = "title" | "unreadCount" | "lastMessageAt" | "participantsCount";
|
|
46
56
|
/** A group's switches; `null` where the messenger does not say, or has no such switch. */
|
|
47
57
|
export interface GroupSettings {
|
|
48
58
|
allCanPin: boolean | null;
|
package/dist/kit/contract.d.ts
CHANGED
|
@@ -16,13 +16,13 @@ export interface ContractOptions {
|
|
|
16
16
|
ids?: IdMaker;
|
|
17
17
|
/** As the messenger's `fetching.orderBy`: whether `history`'s `before` is a message id or an ISO time. */
|
|
18
18
|
orderBy?: "id" | "time";
|
|
19
|
-
/** How long `watch` may take to say it listens, and `watch`, `listen` and `feed` to end
|
|
19
|
+
/** How long `watch` may take to say it listens, and `watch`, `listen`, `listenHistory` and `feed` to end on abort. */
|
|
20
20
|
waitMs?: number;
|
|
21
21
|
/** As the messenger's `history`: a `"server"` messenger's adapter must have every `ServerReads` method. */
|
|
22
22
|
history?: "server" | "store";
|
|
23
23
|
}
|
|
24
24
|
/** Every method of the optional groups, which `capability` hands over or refuses. */
|
|
25
|
-
export declare const OPTIONAL_METHODS: readonly ["fetchCounters", "chats", "history", "around", "contact", "historyAfter", "historyBefore", "topics", "inspect", "edit", "forward", "delete", "pin", "unpin", "star", "react", "markRead", "markTopicRead", "poll", "vote", "closePoll", "createPoll", "watch", "listen", "feed", "download", "transcribe", "scheduled", "members", "admins", "chatEvents", "sessions", "lookup", "addressBook", "people", "createGroup", "join", "leave", "group", "updateGroup", "resetInviteLink", "addMembers", "removeMembers", "addAdmin", "removeAdmin", "folders", "createFolder", "updateFolder", "deleteFolder", "addContact", "removeContact", "block", "unblock", "renameContact", "importContacts", "updateProfile", "endOtherSessions"];
|
|
25
|
+
export declare const OPTIONAL_METHODS: readonly ["fetchCounters", "chats", "history", "around", "contact", "historyAfter", "historyBefore", "topics", "inspect", "edit", "forward", "delete", "pin", "unpin", "star", "react", "markRead", "markTopicRead", "poll", "vote", "closePoll", "createPoll", "watch", "listen", "listenHistory", "feed", "download", "transcribe", "scheduled", "members", "admins", "chatEvents", "sessions", "lookup", "addressBook", "people", "createGroup", "join", "leave", "group", "updateGroup", "resetInviteLink", "addMembers", "removeMembers", "addAdmin", "removeAdmin", "folders", "createFolder", "updateFolder", "deleteFolder", "addContact", "removeContact", "block", "unblock", "renameContact", "importContacts", "updateProfile", "endOtherSessions"];
|
|
26
26
|
/**
|
|
27
27
|
* The port's promises as cases any test runner can run: each connects through `connect`, checks
|
|
28
28
|
* with `node:assert/strict` and closes the adapter. None needs a live service.
|
package/dist/kit/contract.js
CHANGED
|
@@ -29,6 +29,7 @@ export const OPTIONAL_METHODS = [
|
|
|
29
29
|
"createPoll",
|
|
30
30
|
"watch",
|
|
31
31
|
"listen",
|
|
32
|
+
"listenHistory",
|
|
32
33
|
"feed",
|
|
33
34
|
"download",
|
|
34
35
|
"transcribe",
|
|
@@ -430,6 +431,30 @@ export const contractCases = ({ connect, ids = wordIds, orderBy = "id", waitMs =
|
|
|
430
431
|
await within(listening, "listen did not end when the signal aborted", waitMs);
|
|
431
432
|
},
|
|
432
433
|
],
|
|
434
|
+
[
|
|
435
|
+
"listenHistory on a signal already aborted resolves at once and does not reject",
|
|
436
|
+
async (adapter) => {
|
|
437
|
+
const skipped = lacking(adapter, "listenHistory");
|
|
438
|
+
if (skipped)
|
|
439
|
+
return skipped;
|
|
440
|
+
const controller = new AbortController();
|
|
441
|
+
controller.abort();
|
|
442
|
+
await within(Promise.resolve(adapter.listenHistory?.(() => { }, controller.signal)), "listenHistory did not resolve on a signal already aborted", waitMs);
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
[
|
|
446
|
+
"listenHistory keeps listening until the signal aborts, then resolves",
|
|
447
|
+
async (adapter) => {
|
|
448
|
+
const skipped = lacking(adapter, "listenHistory");
|
|
449
|
+
if (skipped)
|
|
450
|
+
return skipped;
|
|
451
|
+
const controller = new AbortController();
|
|
452
|
+
const listening = Promise.resolve(adapter.listenHistory?.(() => { }, controller.signal));
|
|
453
|
+
assert.equal(await settledSoon(listening), false, "listenHistory ended before the signal aborted");
|
|
454
|
+
controller.abort();
|
|
455
|
+
await within(listening, "listenHistory did not end when the signal aborted", waitMs);
|
|
456
|
+
},
|
|
457
|
+
],
|
|
433
458
|
[
|
|
434
459
|
"feed pushes only the seed's chats and messages, with ids as strings, and ends when the signal aborts",
|
|
435
460
|
async (adapter, seed) => {
|
package/dist/kit/fake.d.ts
CHANGED
package/dist/kit/fake.js
CHANGED
|
@@ -28,7 +28,8 @@ const missing = (what) => new CliError("not_found", `the fake adapter has no ${w
|
|
|
28
28
|
*
|
|
29
29
|
* With `{ feed: true }` it also pushes its history, as a messenger with `history: "store"` does:
|
|
30
30
|
* the chats and people in one batch, then each chat's messages in a batch of their own.
|
|
31
|
-
* With `{ listen: true }` it also has `listen`, which hands on what `watch` does
|
|
31
|
+
* With `{ listen: true }` it also has `listen`, which hands on what `watch` does, and with `{ listenHistory: true }`
|
|
32
|
+
* `listenHistory`, which hands on what `feed` does.
|
|
32
33
|
*/
|
|
33
34
|
const withRules = (folder, { emoji, include, skip, exclude, pin }) => {
|
|
34
35
|
if (emoji !== undefined)
|
|
@@ -57,6 +58,17 @@ export const fakeAdapter = (seed = contractSeed(), options = {}) => {
|
|
|
57
58
|
const folders = [];
|
|
58
59
|
const listeners = new Set();
|
|
59
60
|
let counter = 0;
|
|
61
|
+
const pushHistory = async (onBatch, signal) => {
|
|
62
|
+
if (signal.aborted)
|
|
63
|
+
return;
|
|
64
|
+
onBatch({ chats: structuredClone(state.chats), people: state.people.map(asMember) });
|
|
65
|
+
for (const chat of state.chats) {
|
|
66
|
+
const messages = messagesOf(chat.id);
|
|
67
|
+
if (messages.length > 0)
|
|
68
|
+
onBatch({ messages: structuredClone(messages) });
|
|
69
|
+
}
|
|
70
|
+
await new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
71
|
+
};
|
|
60
72
|
const ownId = () => {
|
|
61
73
|
if (!account)
|
|
62
74
|
throw new CliError("authentication_error", "the fake adapter is not logged in");
|
|
@@ -323,21 +335,8 @@ export const fakeAdapter = (seed = contractSeed(), options = {}) => {
|
|
|
323
335
|
},
|
|
324
336
|
}
|
|
325
337
|
: {}),
|
|
326
|
-
...(options.feed
|
|
327
|
-
|
|
328
|
-
feed: async (onBatch, signal) => {
|
|
329
|
-
if (signal.aborted)
|
|
330
|
-
return;
|
|
331
|
-
onBatch({ chats: structuredClone(state.chats), people: state.people.map(asMember) });
|
|
332
|
-
for (const chat of state.chats) {
|
|
333
|
-
const messages = messagesOf(chat.id);
|
|
334
|
-
if (messages.length > 0)
|
|
335
|
-
onBatch({ messages: structuredClone(messages) });
|
|
336
|
-
}
|
|
337
|
-
await new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
338
|
-
},
|
|
339
|
-
}
|
|
340
|
-
: {}),
|
|
338
|
+
...(options.feed ? { feed: pushHistory } : {}),
|
|
339
|
+
...(options.listenHistory ? { listenHistory: pushHistory } : {}),
|
|
341
340
|
download: async (reference, messageId) => {
|
|
342
341
|
const message = messageOf(chatOf(reference).id, messageId);
|
|
343
342
|
return {
|
package/dist/mcp/tool.js
CHANGED
|
@@ -116,10 +116,12 @@ export const entryRunner = (registration) => {
|
|
|
116
116
|
...(traceId === undefined ? {} : { traceId }),
|
|
117
117
|
})
|
|
118
118
|
: undefined;
|
|
119
|
+
const profile = registration.defaults.settings.profile;
|
|
119
120
|
const scope = {
|
|
120
121
|
runId: recording?.runId ?? newRunId(new Date(startedAt), command),
|
|
121
122
|
command,
|
|
122
123
|
startedAt,
|
|
124
|
+
...(profile === undefined ? {} : { profile }),
|
|
123
125
|
...(traceId === undefined ? {} : { traceId }),
|
|
124
126
|
};
|
|
125
127
|
const result = await inRun(scope, () => {
|
|
@@ -3,6 +3,7 @@ import * as v from "valibot";
|
|
|
3
3
|
import { itemOf } from "../../cli/envelope.js";
|
|
4
4
|
import { keepRefreshedRefs } from "../../cli/messenger/refreshed-refs.js";
|
|
5
5
|
import { capability } from "../../ports/messenger.js";
|
|
6
|
+
import { typedCommand } from "../../runs/scope.js";
|
|
6
7
|
import { chatOf, message, Picture, READ, tool } from "../tool.js";
|
|
7
8
|
/** Larger than this and a client may refuse the answer, or spend the model's context on one picture. */
|
|
8
9
|
export const PHOTO_LIMIT = 512 * 1024;
|
|
@@ -26,7 +27,7 @@ export const messagesPhotoTools = (messenger) => ({
|
|
|
26
27
|
annotations: READ,
|
|
27
28
|
online: async (adapter, args, defaults) => {
|
|
28
29
|
const { id: chatId } = await adapter.resolve(args.chat);
|
|
29
|
-
const saveIt = `the owner can save it with \`${messenger.app.command
|
|
30
|
+
const saveIt = `the owner can save it with \`${typedCommand(messenger.app.command, `messages download ${chatId} ${args.message}`)}\``;
|
|
30
31
|
const { files } = await capability(adapter, "download", "download attachments")(chatId, args.message);
|
|
31
32
|
const photo = args.index === undefined ? files.find((file) => file.kind === "photo") : files[args.index];
|
|
32
33
|
if (!photo) {
|
package/dist/meetings/reads.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { CounterField, CounterObservations } from "../domain/counters.js";
|
|
|
3
3
|
import type { HtmlFormatting, MarkdownFormatting, TextSpan } from "../domain/formatting.js";
|
|
4
4
|
import type { Markup } from "../domain/markdown.js";
|
|
5
5
|
import type { MessagePermalink } from "../domain/message-link.js";
|
|
6
|
-
import type { Account, AccountSession, AdminRight, Button, CallRecord, Chat, ChatCard, ChatEvent, ChatEvents, Discussion, Folder, FolderChange, FolderRules, GroupCard, GroupChange, GroupMember, Id, InviteLink, InviteLinkChange, JoinRequest, LinkTarget, MediaKind, Member, Message, MessageEvent, MessageHit, OfficialChatStats, Page, PersonCard, PhoneBookEntry, Poll, PollVote, PrivacySettings, ProfileFacts, ProviderMetadata, SenderIdentity, Sticker, StickerSet, Topic, TopicChange, WindowedMessage } from "../domain/models.js";
|
|
6
|
+
import type { Account, AccountSession, AdminRight, Button, CallRecord, Chat, ChatCard, ChatEvent, ChatEvents, ChatUpdate, Discussion, Folder, FolderChange, FolderRules, GroupCard, GroupChange, GroupMember, Id, InviteLink, InviteLinkChange, JoinRequest, LinkTarget, MediaKind, Member, Message, MessageEvent, MessageHit, OfficialChatStats, Page, PersonCard, PhoneBookEntry, Poll, PollVote, PrivacySettings, ProfileFacts, ProviderMetadata, SenderIdentity, Sticker, StickerSet, Topic, TopicChange, WindowedMessage } from "../domain/models.js";
|
|
7
7
|
/** One attachment's bytes, fetched over the adapter's connection when `bytes` is read — so read it before the command closes. */
|
|
8
8
|
export interface RemoteFile {
|
|
9
9
|
kind: string;
|
|
@@ -316,6 +316,14 @@ export interface PassiveUpdates {
|
|
|
316
316
|
* be handed on.
|
|
317
317
|
*/
|
|
318
318
|
listen(onEvent: (event: MessageEvent) => void, signal: AbortSignal): Promise<void>;
|
|
319
|
+
/**
|
|
320
|
+
* What the open connection is pushed of the messenger's history, as `feed` hands it, until `signal` aborts — on
|
|
321
|
+
* the same connections as `listen`, under the same rules: it never connects or waits for the connection, and
|
|
322
|
+
* resolves once nothing more will be handed on. A messenger that pushes history once (`history: "store"`) then
|
|
323
|
+
* has it saved by a command that never feeds. Optional within the group, so an adapter typed as having
|
|
324
|
+
* `PassiveUpdates` before it existed still is one.
|
|
325
|
+
*/
|
|
326
|
+
listenHistory?(onBatch: (batch: HistoryBatch) => void, signal: AbortSignal): Promise<void>;
|
|
319
327
|
}
|
|
320
328
|
/** One voter's vote in the poll message `messageId` of `chatId` carries, as `PollVoteEvent` has it. */
|
|
321
329
|
export interface HistoryVote {
|
|
@@ -346,7 +354,7 @@ export interface HistoryBatch {
|
|
|
346
354
|
from: Id;
|
|
347
355
|
to: Id;
|
|
348
356
|
}[];
|
|
349
|
-
chats?:
|
|
357
|
+
chats?: ChatUpdate[];
|
|
350
358
|
people?: Member[];
|
|
351
359
|
messages?: Message[];
|
|
352
360
|
votes?: HistoryVote[];
|
package/dist/runs/recording.js
CHANGED
|
@@ -19,6 +19,7 @@ export const recorded = async (options, body) => {
|
|
|
19
19
|
runId: recording.runId,
|
|
20
20
|
command: options.command,
|
|
21
21
|
startedAt: Date.now(),
|
|
22
|
+
profile: options.profile,
|
|
22
23
|
...(recording.traceId ? { traceId: recording.traceId } : {}),
|
|
23
24
|
}, () => body(recording.events));
|
|
24
25
|
await recording.succeed(answer);
|
package/dist/runs/scope.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface RunScope {
|
|
|
5
5
|
startedAt?: number;
|
|
6
6
|
/** The caller's trace — an MCP client's `_meta.traceparent`, or `TRACEPARENT` from whatever started this process. */
|
|
7
7
|
traceId?: string;
|
|
8
|
+
/** The profile the run is for, so a hint deep in a service can name the command with it. */
|
|
9
|
+
profile?: string;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* The run a log line written now belongs to, for a writer that is not handed one — the send journal,
|
|
@@ -12,6 +14,11 @@ export interface RunScope {
|
|
|
12
14
|
*/
|
|
13
15
|
export declare const currentRun: () => RunScope;
|
|
14
16
|
export declare const inRun: <T>(scope: RunScope, body: () => T) => T;
|
|
17
|
+
/**
|
|
18
|
+
* A command as a person types it: `max personal store migrate`. A hint without the profile fails when copied,
|
|
19
|
+
* since no profile is assumed; outside a run with a profile it stays `max store migrate`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const typedCommand: (tool: string, words: string, profile?: string | undefined) => string;
|
|
15
22
|
/**
|
|
16
23
|
* For a one-shot command whose action runs outside `inRun` — max's `max bot`, recorded from commander's hooks, which
|
|
17
24
|
* cannot wrap the action — so its answers' `meta.run_id` names the run `runs show` finds. Never in a server.
|
package/dist/runs/scope.js
CHANGED
|
@@ -21,6 +21,11 @@ export const currentRun = () => {
|
|
|
21
21
|
return processScope;
|
|
22
22
|
};
|
|
23
23
|
export const inRun = (scope, body) => scopes.run(scope, body);
|
|
24
|
+
/**
|
|
25
|
+
* A command as a person types it: `max personal store migrate`. A hint without the profile fails when copied,
|
|
26
|
+
* since no profile is assumed; outside a run with a profile it stays `max store migrate`.
|
|
27
|
+
*/
|
|
28
|
+
export const typedCommand = (tool, words, profile = currentRun().profile) => [tool, profile, words].filter((part) => part !== undefined && part !== "").join(" ");
|
|
24
29
|
/**
|
|
25
30
|
* For a one-shot command whose action runs outside `inRun` — max's `max bot`, recorded from commander's hooks, which
|
|
26
31
|
* cannot wrap the action — so its answers' `meta.run_id` names the run `runs show` finds. Never in a server.
|
package/dist/services/archive.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
3
|
import { capability } from "../ports/messenger.js";
|
|
4
|
+
import { typedCommand } from "../runs/scope.js";
|
|
4
5
|
import { fetchedKey, historyStartKey, } from "../store/store.js";
|
|
5
6
|
import { estimateBackfill } from "./backfill-estimate.js";
|
|
6
7
|
import { actionable, batchProgress } from "./batch.js";
|
|
@@ -11,7 +12,7 @@ import { catchUpSearch, validateCatchUp } from "./search-catchup.js";
|
|
|
11
12
|
const pushed = (deps, what) => {
|
|
12
13
|
if (deps.reads === "store") {
|
|
13
14
|
throw new CliError("validation_error", `${what} asks for older history, and this messenger pushes its history instead — keep ` +
|
|
14
|
-
`\`${deps.messenger.app.command
|
|
15
|
+
`\`${typedCommand(deps.messenger.app.command, "serve")}\` running`);
|
|
15
16
|
}
|
|
16
17
|
};
|
|
17
18
|
/** The most messages a provider hands out per history request — Telegram's cap. */
|