@wirecat/cli-core 0.551.0 → 0.553.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 +83 -0
- package/README.md +1 -1
- package/dist/background/schedules.d.ts +12 -1
- package/dist/background/schedules.js +65 -24
- package/dist/background/units.d.ts +16 -9
- package/dist/background/units.js +26 -11
- package/dist/cli/compact.d.ts +4 -0
- package/dist/cli/compact.js +65 -7
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/meeting-preparation-answer.d.ts +3 -0
- package/dist/cli/meeting-preparation-projects.d.ts +3 -0
- package/dist/cli/meeting-preparation-read.d.ts +3 -0
- package/dist/cli/messenger/context.d.ts +3 -0
- package/dist/cli/messenger/projects-command.js +38 -11
- package/dist/cli/messenger/search-answers.js +8 -1
- package/dist/cli/messenger/serve-catch-up.d.ts +43 -0
- package/dist/cli/messenger/serve-catch-up.js +90 -0
- package/dist/cli/messenger/serve-command.js +54 -1
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- package/dist/cli/messenger/server-command.d.ts +4 -3
- package/dist/cli/messenger/server-command.js +30 -12
- package/dist/cli/program.js +9 -4
- package/dist/cli/project-evidence.d.ts +33 -4
- package/dist/cli/project-evidence.js +38 -4
- package/dist/cli/project-status-rows.js +11 -1
- package/dist/cli/project-status.d.ts +24 -0
- package/dist/cli/project-status.js +52 -9
- package/dist/cli/schedule/schedule-command.js +5 -1
- package/dist/entities/hit.d.ts +5 -0
- package/dist/entities/hit.js +5 -0
- package/dist/entities/index.d.ts +27 -0
- package/dist/entities/listener.d.ts +6 -0
- package/dist/entities/listener.js +1 -0
- package/dist/entities/meeting-preparation.d.ts +3 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/mcp/meeting-preparation.d.ts +3 -0
- package/dist/meetings/answers.d.ts +5 -0
- package/dist/meetings/answers.js +8 -0
- package/dist/meetings/cli/index.js +14 -0
- package/dist/meetings/index.d.ts +2 -1
- package/dist/meetings/index.js +1 -0
- package/dist/meetings/ingest.js +2 -1
- package/dist/meetings/language.d.ts +5 -0
- package/dist/meetings/language.js +57 -0
- package/dist/meetings/mcp/search.d.ts +3 -0
- package/dist/meetings/project-evidence-answer.d.ts +3 -0
- package/dist/meetings/result-items.d.ts +9 -0
- package/dist/meetings/result-items.js +7 -0
- package/dist/meetings/store.d.ts +22 -0
- package/dist/meetings/testing/memory.js +3 -0
- package/dist/meetings/transcription.d.ts +2 -0
- package/dist/meetings/transcription.js +5 -1
- package/dist/search/lucene/types.d.ts +7 -0
- package/dist/search/lucene/types.js +11 -1
- package/dist/services/index.d.ts +4 -3
- package/dist/services/index.js +4 -3
- package/dist/services/link-scorers/index.d.ts +1 -1
- package/dist/services/link-scorers/index.js +1 -1
- package/dist/services/link-scorers/scan.d.ts +0 -1
- package/dist/services/link-scorers/scan.js +1 -1
- package/dist/services/messages-archive-search.js +3 -2
- package/dist/services/messages-combined.js +2 -2
- package/dist/services/messages-discovery.js +2 -2
- package/dist/services/notes-search.js +4 -5
- package/dist/services/project-purposes.d.ts +28 -0
- package/dist/services/project-purposes.js +41 -0
- package/dist/services/project-status/index.d.ts +22 -8
- package/dist/services/project-status/index.js +17 -9
- package/dist/services/search-all-meetings.d.ts +3 -0
- package/dist/services/search-all-meetings.js +6 -1
- package/dist/services/stored-project-gathering.d.ts +79 -4
- package/dist/services/stored-project-gathering.js +171 -22
- package/dist/speech/local.d.ts +2 -0
- package/dist/speech/local.js +8 -1
- package/dist/speech/models.d.ts +2 -0
- package/dist/speech/models.js +2 -0
- package/dist/store/driver.d.ts +11 -0
- package/dist/store/driver.js +13 -0
- package/dist/store/drivers/node-sqlite.d.ts +1 -1
- package/dist/store/drivers/node-sqlite.js +3 -0
- package/dist/store/messages.d.ts +2 -1
- package/dist/store/messages.js +2 -2
- package/dist/store/sqlite/lucene.d.ts +1 -1
- package/dist/store/sqlite/lucene.js +33 -7
- package/dist/store/sqlite/meeting-hit-search.js +10 -3
- package/dist/store/sqlite/meeting-languages.d.ts +4 -0
- package/dist/store/sqlite/meeting-languages.js +36 -0
- package/dist/store/sqlite/meetings.d.ts +2 -1
- package/dist/store/sqlite/meetings.js +9 -3
- package/dist/store/sqlite/note-search.js +2 -2
- package/dist/store/sqlite/reads.d.ts +1 -1
- package/dist/store/sqlite/reads.js +2 -2
- package/package.json +1 -1
- package/parity.json +10 -0
|
@@ -3,6 +3,7 @@ import { annotate, annotateOption } from "@wirecat/cli-base/commands";
|
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { parseDuration } from "../../domain/duration.js";
|
|
5
5
|
import { FETCHING } from "../../services/archive.js";
|
|
6
|
+
import { BUILT_IN_PURPOSES, purposeList } from "../../services/project-purposes.js";
|
|
6
7
|
import { projectStatus, STATUS_SIGNALS } from "../../services/project-status/index.js";
|
|
7
8
|
import { gatherStoredProject } from "../../services/stored-project-gathering.js";
|
|
8
9
|
import { answerEntity } from "../answer-entity.js";
|
|
@@ -61,6 +62,8 @@ const archiveFetcher = (withServices, messenger) => {
|
|
|
61
62
|
},
|
|
62
63
|
};
|
|
63
64
|
};
|
|
65
|
+
const purposeHelp = (messenger) => `the linked chats' purposes to read, comma separated (${(messenger.purposes?.purposes ?? BUILT_IN_PURPOSES).join(", ")}), ` +
|
|
66
|
+
"or all; the configured defaults if not given, and a chat left out is counted, never dropped silently";
|
|
64
67
|
/** Off MCP: a tool call ends at about a minute, too soon for a sync, which would fail the whole answer. */
|
|
65
68
|
const cliOnly = (flags, description, value) => {
|
|
66
69
|
const option = new Option(flags, description);
|
|
@@ -88,6 +91,8 @@ export const projectsCommand = (messenger) => {
|
|
|
88
91
|
.option("--alias <name>", "another name of the project, searched on its own; repeat for more", collect, [])
|
|
89
92
|
.option("--context-query <words>", "more words that all appear in one text; repeat for another", collect, [])
|
|
90
93
|
.option("--contact <person>", "a name to search, or person:<id> to read what that person wrote", collect, [])
|
|
94
|
+
.option("--purpose <purposes>", purposeHelp(messenger), purposeList, [])
|
|
95
|
+
.option("--include-proposed", "also read chats and members proposed for the project and not yet confirmed; each item they bring is marked")
|
|
91
96
|
.option("--since-time <time>", "how far back: days (90d, at most 730d) or a start date (2026-09-01)", "60d")
|
|
92
97
|
.option("--before-time <time>", "fixed exclusive end: ISO 8601 timestamp with UTC offset")
|
|
93
98
|
.option("--limit <n>", "new references at most, up to 500; 100 if not given", positiveCount("--limit"))
|
|
@@ -116,20 +121,31 @@ export const projectsCommand = (messenger) => {
|
|
|
116
121
|
...(options.contact.length ? { people: options.contact } : {}),
|
|
117
122
|
...(options.maxPages === undefined ? {} : { maxReads: options.maxPages }),
|
|
118
123
|
...(options.cursor === undefined ? {} : { checkpoint: options.cursor }),
|
|
124
|
+
...(options.purpose.length ? { purposes: options.purpose } : {}),
|
|
125
|
+
...(messenger.purposes ? { purposeConfig: messenger.purposes } : {}),
|
|
126
|
+
...(options.includeProposed ? { includeProposed: true } : {}),
|
|
119
127
|
...(fetcher ? { fetcher, maxFetchMs, onFetchProgress: progressNote(context.renderer.note) } : {}),
|
|
120
128
|
}));
|
|
121
129
|
const fetcher = options.syncFirst
|
|
122
130
|
? archiveFetcher((work) => context.withServices(work, { name: "projects evidence fetch" }), messenger)
|
|
123
131
|
: undefined;
|
|
124
|
-
|
|
125
|
-
const
|
|
132
|
+
let profile;
|
|
133
|
+
const found = await context.withStore((store, account) => {
|
|
134
|
+
profile = account;
|
|
135
|
+
return gather(store, account, fetcher);
|
|
136
|
+
});
|
|
137
|
+
const root = rootOf(this);
|
|
126
138
|
const hints = {
|
|
127
139
|
sync: (one, since) => one.provider === messenger.provider
|
|
128
140
|
? lineOf(this, `projects evidence ${quote(project)} --since-time ${since} --sync-first`)
|
|
129
141
|
: null,
|
|
142
|
+
read: (one, since) => one.provider === profile?.provider && one.account === profile.account
|
|
143
|
+
? lineOf(this, wordsOf(root, readStep(one.chat, since)))
|
|
144
|
+
: null,
|
|
130
145
|
showDocument: "the notes app that imported it",
|
|
131
146
|
};
|
|
132
|
-
|
|
147
|
+
const parts = projectAnswerParts(options.sinceTime, found, hints);
|
|
148
|
+
renderAnswer(this, context, itemOf(snakeKeys(withSyncCommands(found, hints.sync, hints.read)), parts), () => projectEvidenceText(found, hints));
|
|
133
149
|
}));
|
|
134
150
|
projects.addCommand(statusCommand(messenger));
|
|
135
151
|
return projects;
|
|
@@ -149,26 +165,37 @@ const statusCommand = (messenger) => answerEntity(annotate(new Command("status")
|
|
|
149
165
|
.argument("<project>", "an existing project's id, key, exact name or alias")
|
|
150
166
|
.option("--since-time <time>", "how far back: days (60d, at most 730d) or a start date (2026-09-01)", "60d")
|
|
151
167
|
.option("--before-time <time>", "fixed exclusive end: ISO 8601 timestamp with UTC offset")
|
|
168
|
+
.option("--purpose <purposes>", purposeHelp(messenger), purposeList, [])
|
|
152
169
|
.option("--context-query <words>", "words that all appear in one text; reports their last mention per side. Repeat for another", collect, [])
|
|
153
170
|
.action(async function (project) {
|
|
154
171
|
const options = this.opts();
|
|
155
172
|
const context = messengerContext(this, messenger);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
let profile;
|
|
174
|
+
const found = await context.withStore((store, account) => {
|
|
175
|
+
profile = account;
|
|
176
|
+
return projectStatus(store, {
|
|
177
|
+
project,
|
|
178
|
+
accounts: [account],
|
|
179
|
+
...projectWindowOf(options.sinceTime),
|
|
180
|
+
...(options.beforeTime === undefined ? {} : { asOf: projectCutoffTime(options.beforeTime) }),
|
|
181
|
+
contextQueries: options.contextQuery,
|
|
182
|
+
...(options.purpose.length ? { purposes: options.purpose } : {}),
|
|
183
|
+
...(messenger.purposes ? { purposeConfig: messenger.purposes } : {}),
|
|
184
|
+
});
|
|
185
|
+
});
|
|
163
186
|
const root = rootOf(this);
|
|
164
187
|
const hints = {
|
|
165
188
|
sync: (chat, since) => nextStep("messages.sync", { args: { chat: chat.chatId, since_time: since } }),
|
|
166
|
-
read: (chat, since) =>
|
|
189
|
+
read: (chat, since) => readStep(chat.chatId, since),
|
|
190
|
+
readChat: (chat, since) => chat.account.provider === profile?.provider && chat.account.account === profile.account
|
|
191
|
+
? readStep(chat.chatId, since)
|
|
192
|
+
: null,
|
|
167
193
|
line: (step) => (step.kind === "command" ? lineOf(this, wordsOf(root, step)) : step.why),
|
|
168
194
|
};
|
|
169
195
|
const { item, ...parts } = projectStatusAnswer(found, hints);
|
|
170
196
|
renderAnswer(this, context, itemOf(item, parts), () => projectStatusText(found, hints));
|
|
171
197
|
}), "project_status", projectStatusSchema, { row: projectStatusRow(STATUS_SIGNALS) });
|
|
198
|
+
const readStep = (chat, since) => nextStep("messages.list", { args: { chat, query: `date>=${since}` } });
|
|
172
199
|
/** A step's words after the tool and the profile, so `lineOf` names the profile the run used. */
|
|
173
200
|
const wordsOf = (root, step) => {
|
|
174
201
|
const profile = root.getOptionValue("profile");
|
|
@@ -293,7 +293,14 @@ export const allHitItem = (item) => item.kind === "meeting"
|
|
|
293
293
|
offset_ms: item.startMs,
|
|
294
294
|
scope: item.scope,
|
|
295
295
|
id: String(item.id),
|
|
296
|
-
...(item.cueRef === undefined
|
|
296
|
+
...(item.cueRef === undefined
|
|
297
|
+
? {}
|
|
298
|
+
: {
|
|
299
|
+
cue_ref: item.cueRef,
|
|
300
|
+
sender_name: item.speakerName ?? null,
|
|
301
|
+
language: item.language ?? null,
|
|
302
|
+
...(item.line === undefined ? {} : { line: item.line, lines_total: item.linesTotal }),
|
|
303
|
+
}),
|
|
297
304
|
untrusted: ALL_HIT_UNTRUSTED.meeting,
|
|
298
305
|
}
|
|
299
306
|
: defined({
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Chat } from "../../domain/models.js";
|
|
2
|
+
import type { ServeNotice } from "./serve-notice.js";
|
|
3
|
+
/** The provider's own catch-up lands first; this pass then finds most chats already current. */
|
|
4
|
+
export declare const START_SYNC_DELAY_MS = 30000;
|
|
5
|
+
export declare const START_SYNC_BUDGET_MS: number;
|
|
6
|
+
/** A longer downtime is `messages sync --all`'s job, not a start's. */
|
|
7
|
+
export declare const START_SYNC_WINDOW_MS: number;
|
|
8
|
+
export declare const START_SYNC_PER_CHAT = 500;
|
|
9
|
+
export interface StartSyncSummary {
|
|
10
|
+
chats: number;
|
|
11
|
+
fetched: number;
|
|
12
|
+
failed: number;
|
|
13
|
+
stopped: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface StartSync {
|
|
16
|
+
/** Runs the pass once, after the delay; a second call is ignored. */
|
|
17
|
+
start(): void;
|
|
18
|
+
/** Aborts the pass and waits for it, which stops before its next request. */
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
summary(): StartSyncSummary | undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `serve`'s one pass on start over the chats that moved while nothing listened. A provider's own
|
|
24
|
+
* catch-up drops a gap it finds too long (Telegram's `differenceTooLong`, `channelDifferenceTooLong`
|
|
25
|
+
* past 100 updates), so each chat whose newest message is newer than the newest one held is read back
|
|
26
|
+
* to that one. A chat never held is read back to the newest message held anywhere, which is about when
|
|
27
|
+
* the last serve stopped. Bounded by a window, a per-chat limit and a time budget; an empty store is
|
|
28
|
+
* left to `messages sync --all`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const startSync: ({ chats, newestHeld, fetch, note, warn, delayMs, budgetMs, windowMs, now, }: {
|
|
31
|
+
chats: (stop: AbortSignal) => Promise<Chat[]>;
|
|
32
|
+
/** The newest message held from before this serve started, epoch milliseconds. */
|
|
33
|
+
newestHeld: (chatId: string) => Promise<number | undefined>;
|
|
34
|
+
fetch: (chatId: string, sinceMs: number, stop: AbortSignal) => Promise<{
|
|
35
|
+
fetched: number;
|
|
36
|
+
}>;
|
|
37
|
+
note: (notice: ServeNotice) => void;
|
|
38
|
+
warn: (notice: ServeNotice) => void;
|
|
39
|
+
delayMs?: number;
|
|
40
|
+
budgetMs?: number;
|
|
41
|
+
windowMs?: number;
|
|
42
|
+
now?: () => number;
|
|
43
|
+
}) => StartSync;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { failureKey } from "../../errors/failures.js";
|
|
2
|
+
/** The provider's own catch-up lands first; this pass then finds most chats already current. */
|
|
3
|
+
export const START_SYNC_DELAY_MS = 30_000;
|
|
4
|
+
export const START_SYNC_BUDGET_MS = 5 * 60_000;
|
|
5
|
+
/** A longer downtime is `messages sync --all`'s job, not a start's. */
|
|
6
|
+
export const START_SYNC_WINDOW_MS = 7 * 86_400_000;
|
|
7
|
+
export const START_SYNC_PER_CHAT = 500;
|
|
8
|
+
/**
|
|
9
|
+
* `serve`'s one pass on start over the chats that moved while nothing listened. A provider's own
|
|
10
|
+
* catch-up drops a gap it finds too long (Telegram's `differenceTooLong`, `channelDifferenceTooLong`
|
|
11
|
+
* past 100 updates), so each chat whose newest message is newer than the newest one held is read back
|
|
12
|
+
* to that one. A chat never held is read back to the newest message held anywhere, which is about when
|
|
13
|
+
* the last serve stopped. Bounded by a window, a per-chat limit and a time budget; an empty store is
|
|
14
|
+
* left to `messages sync --all`.
|
|
15
|
+
*/
|
|
16
|
+
export const startSync = ({ chats, newestHeld, fetch, note, warn, delayMs = START_SYNC_DELAY_MS, budgetMs = START_SYNC_BUDGET_MS, windowMs = START_SYNC_WINDOW_MS, now = Date.now, }) => {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
let timer;
|
|
19
|
+
let pass = Promise.resolve();
|
|
20
|
+
let started = false;
|
|
21
|
+
let done;
|
|
22
|
+
const run = async () => {
|
|
23
|
+
const stop = AbortSignal.any([controller.signal, AbortSignal.timeout(budgetMs)]);
|
|
24
|
+
const floor = now() - windowMs;
|
|
25
|
+
const moved = [];
|
|
26
|
+
for (const chat of await chats(stop)) {
|
|
27
|
+
const lastAt = Date.parse(chat.lastMessageAt ?? "");
|
|
28
|
+
if (Number.isNaN(lastAt) || lastAt < floor)
|
|
29
|
+
continue;
|
|
30
|
+
moved.push({ chat, lastAt, held: await newestHeld(chat.id) });
|
|
31
|
+
}
|
|
32
|
+
const anywhere = moved.reduce((top, { held }) => (held === undefined ? top : Math.max(top, held)), -Infinity);
|
|
33
|
+
if (!Number.isFinite(anywhere))
|
|
34
|
+
return;
|
|
35
|
+
const behind = moved
|
|
36
|
+
.filter(({ lastAt, held }) => lastAt > (held ?? anywhere))
|
|
37
|
+
.sort((one, other) => other.lastAt - one.lastAt);
|
|
38
|
+
if (behind.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
const summary = { chats: 0, fetched: 0, failed: 0, stopped: false };
|
|
41
|
+
done = summary;
|
|
42
|
+
for (const { chat, held } of behind) {
|
|
43
|
+
if (stop.aborted)
|
|
44
|
+
break;
|
|
45
|
+
try {
|
|
46
|
+
summary.fetched += (await fetch(chat.id, Math.max(held ?? anywhere, floor), stop)).fetched;
|
|
47
|
+
summary.chats += 1;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (stop.aborted)
|
|
51
|
+
break;
|
|
52
|
+
summary.failed += 1;
|
|
53
|
+
const errorCode = failureKey(error);
|
|
54
|
+
warn({
|
|
55
|
+
code: "start_sync_chat_failed",
|
|
56
|
+
chatId: chat.id,
|
|
57
|
+
errorCode,
|
|
58
|
+
text: `what ${chat.id} received while serve was down was not fetched: ${errorCode}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
summary.stopped = stop.aborted && summary.chats + summary.failed < behind.length;
|
|
63
|
+
note({
|
|
64
|
+
code: "start_sync",
|
|
65
|
+
text: `caught up ${summary.fetched} message(s) in ${summary.chats} chat(s) that moved while serve was down` +
|
|
66
|
+
(summary.stopped ? ` — stopped at the time limit, ${behind.length - summary.chats - summary.failed} left` : ""),
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
start: () => {
|
|
71
|
+
if (started || controller.signal.aborted)
|
|
72
|
+
return;
|
|
73
|
+
started = true;
|
|
74
|
+
timer = setTimeout(() => {
|
|
75
|
+
pass = run().catch((error) => {
|
|
76
|
+
if (controller.signal.aborted)
|
|
77
|
+
return;
|
|
78
|
+
const errorCode = failureKey(error);
|
|
79
|
+
warn({ code: "start_sync_failed", errorCode, text: `the catch-up on start failed: ${errorCode}` });
|
|
80
|
+
});
|
|
81
|
+
}, delayMs);
|
|
82
|
+
},
|
|
83
|
+
stop: async () => {
|
|
84
|
+
controller.abort();
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
await pass;
|
|
87
|
+
},
|
|
88
|
+
summary: () => done,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
@@ -2,7 +2,9 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { holdLock, lockPath, releaseLock, takeLock } from "../../background/lock.js";
|
|
5
|
+
import { parseDuration } from "../../domain/duration.js";
|
|
5
6
|
import { failureKey } from "../../errors/failures.js";
|
|
7
|
+
import { capability } from "../../ports/messenger.js";
|
|
6
8
|
import { senderFacts } from "../../replies/dry-run.js";
|
|
7
9
|
import { replyRenderer } from "../../replies/rendering.js";
|
|
8
10
|
import { audienceWarnings, readReplies, repliesPathFor } from "../../replies/rules.js";
|
|
@@ -10,14 +12,17 @@ import { NO_RULES, replyTo } from "../../replies/serve.js";
|
|
|
10
12
|
import { repliesStatePathFor } from "../../replies/state.js";
|
|
11
13
|
import { levelFor } from "../../sends/permissions.js";
|
|
12
14
|
import { newSendId } from "../../sends/send-id.js";
|
|
15
|
+
import { FETCHING } from "../../services/archive.js";
|
|
13
16
|
import { onlineDeps } from "../../services/deps.js";
|
|
14
17
|
import { servicesFor } from "../../services/index.js";
|
|
18
|
+
import { patiently } from "../../services/patience.js";
|
|
15
19
|
import { applyTaskRulesOnArrival, openRequestTask } from "../../services/task-rules.js";
|
|
16
20
|
import { environmentOf } from "../context.js";
|
|
17
21
|
import { REMOTE_WRITE } from "../effects.js";
|
|
18
22
|
import { itemOf, renderAnswer } from "../envelope.js";
|
|
19
23
|
import { recalledAccount } from "./accounts.js";
|
|
20
24
|
import { messengerContext } from "./context.js";
|
|
25
|
+
import { START_SYNC_PER_CHAT, startSync } from "./serve-catch-up.js";
|
|
21
26
|
import { memberFetches } from "./serve-members.js";
|
|
22
27
|
import { noticeFor, recordDiagnostics } from "./serve-notice.js";
|
|
23
28
|
import { serveRelay } from "./serve-relay.js";
|
|
@@ -75,7 +80,7 @@ export const serveCommand = (messenger) => {
|
|
|
75
80
|
const counts = {};
|
|
76
81
|
const rules = replying(context, messenger, Date.parse(startedAt));
|
|
77
82
|
const tasks = tasking(context, warn);
|
|
78
|
-
const { handled } = environmentOf(this);
|
|
83
|
+
const { handled, startSyncDelayMs } = environmentOf(this);
|
|
79
84
|
const count = (event) => {
|
|
80
85
|
counts[event.event] = (counts[event.event] ?? 0) + 1;
|
|
81
86
|
if (event.event === "message") {
|
|
@@ -99,6 +104,50 @@ export const serveCommand = (messenger) => {
|
|
|
99
104
|
}).chats.fetchMembers(chatId, {}),
|
|
100
105
|
warn,
|
|
101
106
|
});
|
|
107
|
+
const fetching = messenger.fetching ?? FETCHING;
|
|
108
|
+
const catchingUp = startSync({
|
|
109
|
+
chats: async (stop) => {
|
|
110
|
+
const list = capability(connection, "chats", "list its chats");
|
|
111
|
+
const all = [];
|
|
112
|
+
for (let offset = 0;;) {
|
|
113
|
+
const page = await patiently(() => list({ offset, limit: 100 }), context.renderer.note, stop);
|
|
114
|
+
for (const chat of page.items)
|
|
115
|
+
all.push(chat);
|
|
116
|
+
offset += page.items.length;
|
|
117
|
+
if (!page.hasMore || page.items.length === 0)
|
|
118
|
+
return all;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
newestHeld: (chatId) => context.withStore(async (store, account) => {
|
|
122
|
+
// What this serve stored itself would hide the gap before it.
|
|
123
|
+
const [newest] = await store.messages.messagesWindow(account, chatId, {
|
|
124
|
+
at: startedAt,
|
|
125
|
+
before: 1,
|
|
126
|
+
after: 0,
|
|
127
|
+
});
|
|
128
|
+
return newest ? Date.parse(newest.timestamp) : undefined;
|
|
129
|
+
}, { name: "serve catch-up" }),
|
|
130
|
+
fetch: (chatId, sinceMs, stop) => context.withStore((store, account) => servicesFor({
|
|
131
|
+
...onlineDeps(messenger, connection, context.guard, {
|
|
132
|
+
profile: context.profile,
|
|
133
|
+
env: context.env,
|
|
134
|
+
}),
|
|
135
|
+
store: async () => store,
|
|
136
|
+
account: async () => account,
|
|
137
|
+
}).archive.fetch(chatId, {
|
|
138
|
+
limit: START_SYNC_PER_CHAT,
|
|
139
|
+
pageSize: fetching.page,
|
|
140
|
+
pauseMs: parseDuration(fetching.pause, "pause"),
|
|
141
|
+
sinceMs,
|
|
142
|
+
catchUp: false,
|
|
143
|
+
note: context.renderer.note,
|
|
144
|
+
stop,
|
|
145
|
+
onPage: () => { },
|
|
146
|
+
}), { name: "serve catch-up" }),
|
|
147
|
+
note,
|
|
148
|
+
warn,
|
|
149
|
+
...(startSyncDelayMs === undefined ? {} : { delayMs: startSyncDelayMs }),
|
|
150
|
+
});
|
|
102
151
|
const stems = stemFills({
|
|
103
152
|
withStore: (work) => context.withStore((store) => work(store), { name: "serve stems" }),
|
|
104
153
|
warn,
|
|
@@ -113,6 +162,8 @@ export const serveCommand = (messenger) => {
|
|
|
113
162
|
rules.connected(open);
|
|
114
163
|
members.start();
|
|
115
164
|
stems.start();
|
|
165
|
+
if (messenger.history !== "store" && typeof open.chats === "function")
|
|
166
|
+
catchingUp.start();
|
|
116
167
|
},
|
|
117
168
|
});
|
|
118
169
|
}
|
|
@@ -120,6 +171,7 @@ export const serveCommand = (messenger) => {
|
|
|
120
171
|
await relaying;
|
|
121
172
|
await relay?.close();
|
|
122
173
|
rules.stop();
|
|
174
|
+
await catchingUp.stop();
|
|
123
175
|
await members.stop();
|
|
124
176
|
await stems.stop();
|
|
125
177
|
await rules.settled();
|
|
@@ -135,6 +187,7 @@ export const serveCommand = (messenger) => {
|
|
|
135
187
|
...rules.summary(),
|
|
136
188
|
...(members.summary().fetched + members.summary().failed > 0 ? { members: members.summary() } : {}),
|
|
137
189
|
...(stems.summary().stemmed > 0 ? { stems: stems.summary() } : {}),
|
|
190
|
+
...(catchingUp.summary() ? { caught_up: catchingUp.summary() } : {}),
|
|
138
191
|
...tasks.summary(),
|
|
139
192
|
}));
|
|
140
193
|
});
|
|
@@ -41,6 +41,7 @@ export declare const serveRelay: (command: Command, messenger: Messenger) => {
|
|
|
41
41
|
resolveSettings: (flags: import("../settings.js").GlobalFlags, options?: import("../settings.js").ResolveOptions) => import("../settings.js").Settings;
|
|
42
42
|
profileState?: (profile: string, env: NodeJS.ProcessEnv) => import("./context.js").ProfileState;
|
|
43
43
|
chatArgument: string;
|
|
44
|
+
purposes?: import("../../services/project-purposes.js").PurposeConfig;
|
|
44
45
|
groupSettings?: readonly import("../../domain/models.js").GroupSettingKey[];
|
|
45
46
|
pollQuiz?: boolean;
|
|
46
47
|
pollCloseSeconds?: readonly [min: number, max: number];
|
|
@@ -35,8 +35,9 @@ export interface ServerOptions {
|
|
|
35
35
|
/**
|
|
36
36
|
* `serve` in the background: `start`, `stop`, `restart`, `status`, `logs`. Without a unit, `start`
|
|
37
37
|
* runs `serve` as a process of its own; with one — `install`, a systemd user unit or a launchd
|
|
38
|
-
* agent — it goes through systemd or launchd. **Nothing starts until `start` is typed** (NEED-9):
|
|
39
|
-
* `install`
|
|
40
|
-
*
|
|
38
|
+
* agent — it goes through systemd or launchd. **Nothing starts now until `start` is typed** (NEED-9):
|
|
39
|
+
* `install` writes the file and enables it, so it starts with every session from the next login (or boot,
|
|
40
|
+
* under linger) until `uninstall`. What the server does stays each CLI's; `options.process` says how to find,
|
|
41
|
+
* start and stop it.
|
|
41
42
|
*/
|
|
42
43
|
export declare const serverCommand: (messenger: Messenger, options?: ServerOptions) => Command;
|
|
@@ -54,9 +54,10 @@ const lockProcess = (app, marker, serveArgv) => (context, system, logPath) => ({
|
|
|
54
54
|
/**
|
|
55
55
|
* `serve` in the background: `start`, `stop`, `restart`, `status`, `logs`. Without a unit, `start`
|
|
56
56
|
* runs `serve` as a process of its own; with one — `install`, a systemd user unit or a launchd
|
|
57
|
-
* agent — it goes through systemd or launchd. **Nothing starts until `start` is typed** (NEED-9):
|
|
58
|
-
* `install`
|
|
59
|
-
*
|
|
57
|
+
* agent — it goes through systemd or launchd. **Nothing starts now until `start` is typed** (NEED-9):
|
|
58
|
+
* `install` writes the file and enables it, so it starts with every session from the next login (or boot,
|
|
59
|
+
* under linger) until `uninstall`. What the server does stays each CLI's; `options.process` says how to find,
|
|
60
|
+
* start and stop it.
|
|
60
61
|
*/
|
|
61
62
|
export const serverCommand = (messenger, options = {}) => {
|
|
62
63
|
const { app } = messenger;
|
|
@@ -226,8 +227,11 @@ export const serverCommand = (messenger, options = {}) => {
|
|
|
226
227
|
: undefined;
|
|
227
228
|
const gaveUpReason = gaveUp === undefined ? undefined : Object.entries(EXIT_CODES).find(([, code]) => code === gaveUp)?.[0];
|
|
228
229
|
const flood = new FloodMemory(floodPathFor(app, unit.profile, context.env)).read();
|
|
230
|
+
const startsAtLogin = isInstalled && state.enabled === true;
|
|
229
231
|
const unitLine = isInstalled
|
|
230
|
-
? `Unit: ${tilde(unit.path, context.env)} — ${state.detail ?? (state.active ? "active" : "inactive")}
|
|
232
|
+
? `Unit: ${tilde(unit.path, context.env)} — ${state.detail ?? (state.active ? "active" : "inactive")}, ${startsAtLogin
|
|
233
|
+
? "starts at login."
|
|
234
|
+
: `does not start at login — \`${app.command} ${unit.profile} server install\` turns that on.`}`
|
|
231
235
|
: `No unit installed — \`${app.command} ${unit.profile} server install\` adds one, for starting under systemd or launchd.`;
|
|
232
236
|
say({
|
|
233
237
|
profile: unit.profile,
|
|
@@ -248,6 +252,7 @@ export const serverCommand = (messenger, options = {}) => {
|
|
|
248
252
|
...(gaveUp !== undefined
|
|
249
253
|
? { stopped: { exitCode: gaveUp, reason: gaveUpReason ?? null, restarts: false } }
|
|
250
254
|
: {}),
|
|
255
|
+
startsAtLogin,
|
|
251
256
|
unit: { name: unit.name, path: unit.path, installed: isInstalled, ...state },
|
|
252
257
|
flood: { deadlines: flood.deadlines, sendBlock: flood.sendBlock ?? null },
|
|
253
258
|
}, [
|
|
@@ -293,16 +298,28 @@ export const serverCommand = (messenger, options = {}) => {
|
|
|
293
298
|
effects: LOCAL_WRITE,
|
|
294
299
|
examples: [`${app.command} personal server install`],
|
|
295
300
|
}))
|
|
296
|
-
.
|
|
297
|
-
.
|
|
298
|
-
|
|
301
|
+
.summary("install this profile's background server to start at every login")
|
|
302
|
+
.description("write a systemd user unit or a launchd agent for this profile and have it start at every login; starts nothing now")
|
|
303
|
+
.action(async function () {
|
|
304
|
+
const { context, platform, unit, each, say } = prepare(this);
|
|
299
305
|
const replaced = existsSync(unit.path);
|
|
300
306
|
writeSecurely(unit.path, platform.text(unit, app), 0o644);
|
|
301
307
|
platform.prepare?.(unit);
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
// A unit of moved folders runs a development checkout, which must not start at every login on the owner's keyring.
|
|
309
|
+
const moved = Object.keys(locationVariables(app, context.env)).length > 0;
|
|
310
|
+
if (!moved)
|
|
311
|
+
await each(platform.install(unit));
|
|
312
|
+
say({
|
|
313
|
+
profile: unit.profile,
|
|
314
|
+
unit: unit.name,
|
|
315
|
+
path: unit.path,
|
|
316
|
+
command: unit.command,
|
|
317
|
+
replaced,
|
|
318
|
+
startsAtLogin: !moved,
|
|
319
|
+
}, [
|
|
320
|
+
`${replaced ? "Rewrote" : "Wrote"} ${tilde(unit.path, context.env)} — ${moved ? "with moved folders it does not start at login" : "it starts at every login"}, nothing started now.`,
|
|
304
321
|
`It runs: ${unit.command.map((word) => tilde(word, context.env)).join(" ")}`,
|
|
305
|
-
`Start it now: ${app.command} ${unit.profile} server start · ${
|
|
322
|
+
`Start it now: ${app.command} ${unit.profile} server start · never at login: ${app.command} ${unit.profile} server uninstall`,
|
|
306
323
|
// Suggested, never turned on: an agent reading chats every morning is the owner's choice (NEED-161).
|
|
307
324
|
`A daily follow-up review is off. To turn it on: ${app.command} ${unit.profile} schedule agent follow-ups claude "${followUpsPrompt(app.command)}" --at 08:30`,
|
|
308
325
|
]);
|
|
@@ -311,13 +328,14 @@ export const serverCommand = (messenger, options = {}) => {
|
|
|
311
328
|
effects: LOCAL_WRITE,
|
|
312
329
|
examples: [`${app.command} personal server uninstall`],
|
|
313
330
|
}))
|
|
314
|
-
.description("remove this profile's unit; stop it first")
|
|
331
|
+
.description("remove this profile's unit, so it no longer starts at login; stop it first")
|
|
315
332
|
.action(async function () {
|
|
316
|
-
const { context, platform, unit, say } = prepare(this);
|
|
333
|
+
const { context, platform, unit, each, say } = prepare(this);
|
|
317
334
|
installed(unit);
|
|
318
335
|
if ((await platform.state(unit)).active) {
|
|
319
336
|
throw new CliError("validation_error", `the unit is running — \`${app.command} ${unit.profile} server stop\` first`);
|
|
320
337
|
}
|
|
338
|
+
await each(platform.uninstall(unit));
|
|
321
339
|
rmSync(unit.path, { force: true });
|
|
322
340
|
say({ profile: unit.profile, unit: unit.name, path: unit.path, removed: true }, [
|
|
323
341
|
`Removed ${tilde(unit.path, context.env)}.`,
|
package/dist/cli/program.js
CHANGED
|
@@ -192,11 +192,15 @@ const runOnce = async (argv, definition, options) => {
|
|
|
192
192
|
};
|
|
193
193
|
const sigint = () => interrupted("SIGINT");
|
|
194
194
|
const sigterm = () => interrupted("SIGTERM");
|
|
195
|
+
// A sync, or a one-shot's wait on work it started (`schedule run`), outlives its reader: a desktop that closed the
|
|
196
|
+
// pipe 18 minutes in must not lose the sync, and the run record still says how it ended. Its later lines go
|
|
197
|
+
// nowhere. A listening command's stream is its work, so it still ends.
|
|
198
|
+
let outlivesReader = false;
|
|
195
199
|
const pipeError = (error) => {
|
|
196
|
-
if (error.code
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
if (error.code !== "EPIPE" || outlivesReader || (phased && !listens))
|
|
201
|
+
return;
|
|
202
|
+
signalExit = 0;
|
|
203
|
+
control.abort(error);
|
|
200
204
|
};
|
|
201
205
|
const externalAbort = () => {
|
|
202
206
|
const cancelled = new CliError("cancelled", "command cancelled", { retryable: false });
|
|
@@ -343,6 +347,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
343
347
|
const { lifetime, cooperative: stopsAtItsPace, partial: answersPartial } = metaOf(action);
|
|
344
348
|
const persistent = lifetime === "persistent";
|
|
345
349
|
listens = persistent;
|
|
350
|
+
outlivesReader = stopsAtItsPace === true;
|
|
346
351
|
const interactiveLogin = !headless && lifetime === "interactive";
|
|
347
352
|
const timeout = flags.timeout ?? (commandEnv[envName(definition.app, "TIMEOUT")]?.trim() || undefined);
|
|
348
353
|
// A background fetch was asked to outlive the shell; only an explicit --timeout bounds it.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StoredProjectGathering } from "../services/stored-project-gathering.js";
|
|
1
|
+
import type { LeftOutChat, StoredProjectGathering } from "../services/stored-project-gathering.js";
|
|
2
2
|
/** Days back (`90d`) or a start date (`2026-09-01`); either way the run freezes its end, so a resume keeps it. */
|
|
3
3
|
export declare const projectWindowOf: (value: string) => {
|
|
4
4
|
days: number;
|
|
@@ -10,7 +10,7 @@ export declare const projectCutoffTime: (value: string) => number;
|
|
|
10
10
|
* The zone a typed start date was read in, and the window the run used, in UTC. `complete` is "the answer is whole":
|
|
11
11
|
* reads finished and every linked chat held in full; the answer's own `complete` stays "reads finished".
|
|
12
12
|
*/
|
|
13
|
-
export declare const projectAnswerParts: (sinceTime: string, found: StoredProjectGathering) => {
|
|
13
|
+
export declare const projectAnswerParts: (sinceTime: string, found: StoredProjectGathering, hints?: Pick<ProjectTextHints, "read">) => {
|
|
14
14
|
next?: ({
|
|
15
15
|
kind: "command";
|
|
16
16
|
step: string;
|
|
@@ -45,7 +45,7 @@ export declare const projectAnswerParts: (sinceTime: string, found: StoredProjec
|
|
|
45
45
|
* The gathering with each linked chat's `syncCommand`: the line that syncs a chat held in part back to the window
|
|
46
46
|
* start, or null when it needs none or no command here can. JSON readers get what the text says per chat.
|
|
47
47
|
*/
|
|
48
|
-
export declare const withSyncCommands: (found: StoredProjectGathering, sync: ProjectTextHints["sync"]) => {
|
|
48
|
+
export declare const withSyncCommands: (found: StoredProjectGathering, sync: ProjectTextHints["sync"], read?: ProjectTextHints["read"]) => {
|
|
49
49
|
schemaVersion: 1;
|
|
50
50
|
runKey: string;
|
|
51
51
|
fingerprint: string;
|
|
@@ -80,7 +80,6 @@ export declare const withSyncCommands: (found: StoredProjectGathering, sync: Pro
|
|
|
80
80
|
pages: number;
|
|
81
81
|
issue: string | null;
|
|
82
82
|
}[];
|
|
83
|
-
items: import("../services/project-gathering.js").ProjectEvidenceItem[];
|
|
84
83
|
people: import("../services/project-gathering.js").ProjectEvidencePerson[];
|
|
85
84
|
peopleOmitted: number;
|
|
86
85
|
oversized: {
|
|
@@ -110,6 +109,9 @@ export declare const withSyncCommands: (found: StoredProjectGathering, sync: Pro
|
|
|
110
109
|
complete: boolean;
|
|
111
110
|
completeness: "complete" | "partial" | "unknown";
|
|
112
111
|
checkpoint: string | null;
|
|
112
|
+
items: (import("../services/project-gathering.js").ProjectEvidenceItem & {
|
|
113
|
+
proposed?: string;
|
|
114
|
+
})[];
|
|
113
115
|
projectRef: string | null;
|
|
114
116
|
unreadSources: {
|
|
115
117
|
source: string;
|
|
@@ -117,6 +119,14 @@ export declare const withSyncCommands: (found: StoredProjectGathering, sync: Pro
|
|
|
117
119
|
reason?: "out-of-scope" | "linked-chat-limit" | "member-limit" | "no-identity" | "unknown-provider";
|
|
118
120
|
}[];
|
|
119
121
|
mentions: import("../services/project-gathering-mentions.js").EvidenceMentions;
|
|
122
|
+
purposes: "all" | string[];
|
|
123
|
+
members: import("../services/stored-project-gathering.js").ProjectMember[];
|
|
124
|
+
purposeHint: {
|
|
125
|
+
chat: string;
|
|
126
|
+
relationship: string;
|
|
127
|
+
items: number;
|
|
128
|
+
of: number;
|
|
129
|
+
} | null;
|
|
120
130
|
linkedChats: {
|
|
121
131
|
provider: string;
|
|
122
132
|
account: string;
|
|
@@ -134,13 +144,32 @@ export declare const withSyncCommands: (found: StoredProjectGathering, sync: Pro
|
|
|
134
144
|
} | null;
|
|
135
145
|
items?: number | undefined;
|
|
136
146
|
capped?: boolean | undefined;
|
|
147
|
+
purpose: string;
|
|
148
|
+
proposed?: string;
|
|
149
|
+
score?: number;
|
|
137
150
|
syncCommand: string | null;
|
|
138
151
|
}[];
|
|
152
|
+
leftOut: {
|
|
153
|
+
ref: string;
|
|
154
|
+
provider: string;
|
|
155
|
+
account: string;
|
|
156
|
+
chat: string;
|
|
157
|
+
title: string | null;
|
|
158
|
+
purpose: string;
|
|
159
|
+
relationship: string | null;
|
|
160
|
+
proposed?: string;
|
|
161
|
+
messages: number;
|
|
162
|
+
firstAt: string | null;
|
|
163
|
+
lastAt: string | null;
|
|
164
|
+
readCommand: string | null;
|
|
165
|
+
}[];
|
|
139
166
|
};
|
|
140
167
|
export interface ProjectTextHints {
|
|
141
168
|
/** The command that syncs one linked chat back to `since` (`YYYY-MM-DD`), or null when no command here can. */
|
|
142
169
|
sync: (chat: StoredProjectGathering["linkedChats"][number], since: string) => string | null;
|
|
143
170
|
/** How to open a document too large to read here. */
|
|
144
171
|
showDocument: string;
|
|
172
|
+
/** The command that reads a chat left out by its purpose from `since`, or null when no command here can. */
|
|
173
|
+
read?: (chat: LeftOutChat, since: string) => string | null;
|
|
145
174
|
}
|
|
146
175
|
export declare const projectEvidenceText: (found: StoredProjectGathering, hints: ProjectTextHints) => string;
|