@wirecat/cli-core 0.502.0 → 0.504.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 +32 -0
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/messenger/projects-command.d.ts +3 -0
- package/dist/cli/messenger/projects-command.js +134 -0
- package/dist/cli/project-evidence.d.ts +50 -0
- package/dist/cli/project-evidence.js +142 -0
- package/dist/cli/shared-command-exports.d.ts +1 -0
- package/dist/cli/shared-command-exports.js +2 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/sends/permissions.js +1 -1
- package/dist/services/index.d.ts +3 -2
- package/dist/services/index.js +1 -0
- package/dist/services/project-gathering-request.d.ts +11 -6
- package/dist/services/project-gathering-request.js +1 -0
- package/dist/services/project-gathering.d.ts +29 -18
- package/dist/services/project-gathering.js +88 -26
- package/dist/services/stored-project-gathering.d.ts +50 -0
- package/dist/services/stored-project-gathering.js +207 -0
- package/package.json +1 -1
- package/parity.json +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.504.0 — 31.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`projects evidence --sync-first` in messenger CLIs: `projectsCommand(messenger)`.** A CLI
|
|
15
|
+
that mounts it reads a project's evidence from its profile's stored messages; `--sync-first` (off by default, not on
|
|
16
|
+
MCP) first syncs the project's linked chats the store does not hold back to `--since-time`, within `--sync-time`
|
|
17
|
+
(3 minutes), one chat at a time. A chat the time cut stays partial with its `missing` and `next`. The command is
|
|
18
|
+
declared a fetch (`remote: read`, `local: write`) because of `--sync-first`; cli-base fixes effects per command.
|
|
19
|
+
It prints what people wrote, so the `messages` permission guards it.
|
|
20
|
+
- **`gatherStoredProject`** (services) and **`projectAnswerParts`, `projectEvidenceText`, `projectWindowOf`,
|
|
21
|
+
`projectCutoffTime`** (cli): memo's `projects evidence` over a store, moved here so memo and the messenger CLIs
|
|
22
|
+
share it. The host resolves the sources and names its own sync command in the text.
|
|
23
|
+
|
|
24
|
+
### Changed — may break callers
|
|
25
|
+
|
|
26
|
+
- **`gatherProjectEvidence`'s fetch port is per linked chat.** `ProjectSourceFetcher` (one fetch per source, no
|
|
27
|
+
caller) is replaced by `ProjectChatFetcher`: `fetch({ account, chatId, after, before, until, signal, onProgress })` →
|
|
28
|
+
`{ state: "complete" | "partial" | "failed", reason? }`. `fetch: "before-read"` now needs the `coverage` port too,
|
|
29
|
+
takes `budgets.maxFetchMs`, and fetches only linked chats the store does not hold for the window. The result's
|
|
30
|
+
`fetch` lists those chats (`ProjectChatFetched`) instead of one row per source; `ProjectFetchState` is gone. The
|
|
31
|
+
read budgets, `maxElapsedMs` included, start after the fetch.
|
|
32
|
+
|
|
33
|
+
## 0.503.0 — 31.10.2026
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`session start --credential-storage` in the parity manifest, for max only** (WC-SECR-4). max's token goes through
|
|
38
|
+
cli-base's `Credentials`; tg's and WhatsApp's sign-ins are the library's own session file, the rule's exception.
|
|
39
|
+
|
|
8
40
|
## 0.502.0 — 31.10.2026
|
|
9
41
|
|
|
10
42
|
### Added
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -109,11 +109,12 @@ export { window, withPaging } from "./paging.js";
|
|
|
109
109
|
export { type CommandPermissions, commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, type PermissionKeyOf, type PermissionKeySource, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
110
110
|
export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
|
|
111
111
|
export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOptions, run } from "./program.js";
|
|
112
|
+
export { type ProjectTextHints, projectAnswerParts, projectCutoffTime, projectEvidenceText, projectWindowOf, } from "./project-evidence.js";
|
|
112
113
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
113
114
|
export { type Config, type Configuration, count, first, flag, fromFile, type GlobalFlags, NONE_NAMED, namedProfile, type ProfileKind, plain, type ResolveOptions, type Settings, type SettingsExtension, type SettingsExtensionContext, type Source, settingsFor, } from "./settings.js";
|
|
114
115
|
export { type PermissionAnswer, readSetupAnswers, type SetupAnswers, setupPermissionCeiling } from "./setup-answers.js";
|
|
115
116
|
export { AGENTS, type Agent, agentFor, screenFor } from "./setup-screen.js";
|
|
116
|
-
export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, appDoctorCommand, appMcpCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.js";
|
|
117
|
+
export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, appDoctorCommand, appMcpCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, projectsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.js";
|
|
117
118
|
export { assertSharedContracts, customizeSharedCommand, hasSharedContract, overrideSharedContract, type SharedContractOverride, sharedContractOverrides, sharedContracts, } from "./shared-contracts.js";
|
|
118
119
|
export { runnable, withSafety } from "./steps.js";
|
|
119
120
|
export { type StoreInfo, sharedStoreCommand, storeInfo, storeInfoCommand } from "./store-info.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -99,11 +99,12 @@ export { window, withPaging } from "./paging.js";
|
|
|
99
99
|
export { commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
100
100
|
export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
|
|
101
101
|
export { createProgram, run } from "./program.js";
|
|
102
|
+
export { projectAnswerParts, projectCutoffTime, projectEvidenceText, projectWindowOf, } from "./project-evidence.js";
|
|
102
103
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
103
104
|
export { count, first, flag, fromFile, NONE_NAMED, namedProfile, plain, settingsFor, } from "./settings.js";
|
|
104
105
|
export { readSetupAnswers, setupPermissionCeiling } from "./setup-answers.js";
|
|
105
106
|
export { AGENTS, agentFor, screenFor } from "./setup-screen.js";
|
|
106
|
-
export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, appDoctorCommand, appMcpCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.js";
|
|
107
|
+
export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, appDoctorCommand, appMcpCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, projectsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.js";
|
|
107
108
|
export { assertSharedContracts, customizeSharedCommand, hasSharedContract, overrideSharedContract, sharedContractOverrides, sharedContracts, } from "./shared-contracts.js";
|
|
108
109
|
export { runnable, withSafety } from "./steps.js";
|
|
109
110
|
export { sharedStoreCommand, storeInfo, storeInfoCommand } from "./store-info.js";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { annotate, annotateOption } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
|
+
import { parseDuration } from "../../domain/duration.js";
|
|
5
|
+
import { FETCHING } from "../../services/archive.js";
|
|
6
|
+
import { gatherStoredProject } from "../../services/stored-project-gathering.js";
|
|
7
|
+
import { FETCH } from "../effects.js";
|
|
8
|
+
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
9
|
+
import { positiveCount } from "../paging.js";
|
|
10
|
+
import { projectAnswerParts, projectCutoffTime, projectEvidenceText, projectWindowOf } from "../project-evidence.js";
|
|
11
|
+
import { lineOf } from "../steps.js";
|
|
12
|
+
import { messengerContext } from "./context.js";
|
|
13
|
+
const collect = (value, previous) => [...previous, value];
|
|
14
|
+
const upTo = (most, flag) => (value) => {
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
if (!/^\d+$/.test(value) || n > most)
|
|
17
|
+
throw new CliError("validation_error", `${flag} takes a whole number from 0 to ${most}`);
|
|
18
|
+
return n;
|
|
19
|
+
};
|
|
20
|
+
/** More than any chat one fetch budget can bring; the budget, not the count, ends a long chat. */
|
|
21
|
+
const FETCH_LIMIT = 1_000_000;
|
|
22
|
+
/** This profile's account, the only one its connection can sync. */
|
|
23
|
+
const sourcesOf = async (store, account) => {
|
|
24
|
+
const stored = (await store.accounts.storedAccounts()).find((one) => one.provider === account.provider && one.account === account.account);
|
|
25
|
+
if (!stored)
|
|
26
|
+
throw new CliError("not_found", "the store holds nothing of this profile's account yet; sync a chat first", {
|
|
27
|
+
param: "profile",
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
sources: [{ kind: "message", accountId: stored.id, provider: stored.provider, account: stored.account }],
|
|
31
|
+
unread: [],
|
|
32
|
+
scope: "explicit",
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* `archive.fetch` back to the window start, one linked chat at a time. Only this profile's account is a source, so
|
|
37
|
+
* the gathering never asks for another account's chat.
|
|
38
|
+
*/
|
|
39
|
+
const archiveFetcher = (withServices, messenger) => {
|
|
40
|
+
const fetching = messenger.fetching ?? FETCHING;
|
|
41
|
+
return {
|
|
42
|
+
fetch: async ({ chatId, after, signal, onProgress }) => {
|
|
43
|
+
const result = await withServices((services) => services.archive.fetch(chatId, {
|
|
44
|
+
limit: FETCH_LIMIT,
|
|
45
|
+
pageSize: fetching.page,
|
|
46
|
+
pauseMs: parseDuration(fetching.pause, "pause"),
|
|
47
|
+
sinceMs: after,
|
|
48
|
+
catchUp: false,
|
|
49
|
+
note: () => { },
|
|
50
|
+
stop: signal,
|
|
51
|
+
onPage: ({ fetched, oldest }) => onProgress?.({ fetched, oldest }),
|
|
52
|
+
}));
|
|
53
|
+
if (result.issue)
|
|
54
|
+
return { state: "partial", reason: result.issue.code };
|
|
55
|
+
if (result.stopped)
|
|
56
|
+
return { state: "partial", reason: "fetch-budget" };
|
|
57
|
+
return result.complete || result.reachedSince ? { state: "complete" } : { state: "partial", reason: "limit" };
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/** Off MCP: a tool call ends at about a minute, too soon for a sync, which would fail the whole answer. */
|
|
62
|
+
const cliOnly = (flags, description, value) => {
|
|
63
|
+
const option = new Option(flags, description);
|
|
64
|
+
if (value !== undefined)
|
|
65
|
+
option.default(value);
|
|
66
|
+
return annotateOption(option, { mcp: false });
|
|
67
|
+
};
|
|
68
|
+
export const projectsCommand = (messenger) => {
|
|
69
|
+
const projects = new Command("projects").description("what the store holds about the owner's projects");
|
|
70
|
+
const app = messenger.app.command;
|
|
71
|
+
projects.addCommand(annotate(new Command("evidence"), {
|
|
72
|
+
output: { entity: "project_evidence" },
|
|
73
|
+
examples: [`${app} personal projects evidence "Harbour" --since-time 30d --sync-first`],
|
|
74
|
+
// Declared as a fetch for --sync-first; without it nothing leaves this machine, and a fetch is never a write elsewhere.
|
|
75
|
+
effects: FETCH,
|
|
76
|
+
cooperative: true,
|
|
77
|
+
partial: true,
|
|
78
|
+
})
|
|
79
|
+
.summary("what this account's stored messages hold about a project, linked chats included")
|
|
80
|
+
.description("cited references about a project in this profile's stored messages. The project's name, key, each alias and " +
|
|
81
|
+
"each --alias, --context-query and --contact name is its own search; their hits are joined. Every message of " +
|
|
82
|
+
"a chat confirmed as the project's is read too, and so is what a confirmed person member wrote. Reads what the " +
|
|
83
|
+
"store holds; with --sync-first it first syncs the project's linked chats the store does not hold for the window")
|
|
84
|
+
.argument("<project>", "an existing project's id, key, exact name or alias, or a new project's name")
|
|
85
|
+
.option("--alias <name>", "another name of the project, searched on its own; repeat for more", collect, [])
|
|
86
|
+
.option("--context-query <words>", "more words that all appear in one text; repeat for another", collect, [])
|
|
87
|
+
.option("--contact <person>", "a name to search, or person:<id> to read what that person wrote", collect, [])
|
|
88
|
+
.option("--since-time <time>", "how far back: days (90d, at most 730d) or a start date (2026-09-01)", "60d")
|
|
89
|
+
.option("--before-time <time>", "fixed exclusive end: ISO 8601 timestamp with UTC offset")
|
|
90
|
+
.option("--limit <n>", "new references at most, up to 500; 100 if not given", positiveCount("--limit"))
|
|
91
|
+
.option("--max-pages <pages>", "store pages read at most this run, up to 512", positiveCount("--max-pages"))
|
|
92
|
+
.option("--cursor <cursor>", "continue a stopped search from its checkpoint, with the same options")
|
|
93
|
+
.option("--before-n <n>", "messages before each message hit, at most 5", upTo(5, "--before-n"), 2)
|
|
94
|
+
.option("--after-n <n>", "messages after each message hit, at most 5", upTo(5, "--after-n"), 2)
|
|
95
|
+
.option("--no-catch-up", "read the search index as it is, without indexing new text first")
|
|
96
|
+
.addOption(cliOnly("--sync-first", "first sync the linked chats this account does not hold back to --since-time; off by default"))
|
|
97
|
+
.addOption(cliOnly("--sync-time <duration>", "time for all of --sync-first's syncs together; 3m if not given", "3m"))
|
|
98
|
+
.action(async function (project) {
|
|
99
|
+
const options = this.opts();
|
|
100
|
+
const context = messengerContext(this, messenger);
|
|
101
|
+
const maxFetchMs = parseDuration(options.syncTime, "--sync-time");
|
|
102
|
+
const gather = (store, account, fetcher) => sourcesOf(store, account).then((sources) => gatherStoredProject(store, {
|
|
103
|
+
project,
|
|
104
|
+
sources,
|
|
105
|
+
...projectWindowOf(options.sinceTime),
|
|
106
|
+
...(options.beforeTime === undefined ? {} : { asOf: projectCutoffTime(options.beforeTime) }),
|
|
107
|
+
limit: options.limit ?? 100,
|
|
108
|
+
catchUp: options.catchUp,
|
|
109
|
+
beforeN: options.beforeN,
|
|
110
|
+
afterN: options.afterN,
|
|
111
|
+
...(options.alias.length ? { aliases: options.alias } : {}),
|
|
112
|
+
...(options.contextQuery.length ? { keywords: options.contextQuery } : {}),
|
|
113
|
+
...(options.contact.length ? { people: options.contact } : {}),
|
|
114
|
+
...(options.maxPages === undefined ? {} : { maxReads: options.maxPages }),
|
|
115
|
+
...(options.cursor === undefined ? {} : { checkpoint: options.cursor }),
|
|
116
|
+
...(fetcher ? { fetcher, maxFetchMs, onFetchProgress: progressNote(context.renderer.note) } : {}),
|
|
117
|
+
}));
|
|
118
|
+
const fetcher = options.syncFirst
|
|
119
|
+
? archiveFetcher((work) => context.withServices(work, { name: "projects evidence fetch" }), messenger)
|
|
120
|
+
: undefined;
|
|
121
|
+
const found = await context.withStore((store, account) => gather(store, account, fetcher));
|
|
122
|
+
const parts = projectAnswerParts(options.sinceTime, found);
|
|
123
|
+
renderAnswer(this, context, itemOf(snakeKeys(found), parts), () => projectEvidenceText(found, {
|
|
124
|
+
sync: (one, since) => one.provider === messenger.provider
|
|
125
|
+
? lineOf(this, `projects evidence ${quote(project)} --since-time ${since} --sync-first`)
|
|
126
|
+
: null,
|
|
127
|
+
showDocument: "the notes app that imported it",
|
|
128
|
+
}));
|
|
129
|
+
}));
|
|
130
|
+
return projects;
|
|
131
|
+
};
|
|
132
|
+
const quote = (text) => (/^[\w:.-]+$/.test(text) ? text : `"${text.replace(/(["\\$`])/g, "\\$1")}"`);
|
|
133
|
+
/** A line per saved page, counts only; a timed heartbeat with titles is the sync-progress work's (cli-core#2121). */
|
|
134
|
+
const progressNote = (note) => ({ chat, index, of, fetched }) => note(`chat ${index + 1} of ${of} · ${chat} · ${fetched} messages so far`);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StoredProjectGathering } from "../services/stored-project-gathering.js";
|
|
2
|
+
/** Days back (`90d`) or a start date (`2026-09-01`); either way the run freezes its end, so a resume keeps it. */
|
|
3
|
+
export declare const projectWindowOf: (value: string) => {
|
|
4
|
+
days: number;
|
|
5
|
+
} | {
|
|
6
|
+
after: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const projectCutoffTime: (value: string) => number;
|
|
9
|
+
/**
|
|
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
|
+
* reads finished and every linked chat held in full; the answer's own `complete` stays "reads finished".
|
|
12
|
+
*/
|
|
13
|
+
export declare const projectAnswerParts: (sinceTime: string, found: StoredProjectGathering) => {
|
|
14
|
+
next?: ({
|
|
15
|
+
kind: "command";
|
|
16
|
+
step: string;
|
|
17
|
+
command: string;
|
|
18
|
+
args: {
|
|
19
|
+
[x: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
line?: string | undefined;
|
|
22
|
+
why: string;
|
|
23
|
+
safe_to_auto_execute: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "user_action";
|
|
26
|
+
step: string;
|
|
27
|
+
why: string;
|
|
28
|
+
safe_to_auto_execute: false;
|
|
29
|
+
})[] | undefined;
|
|
30
|
+
missing?: {
|
|
31
|
+
field: string;
|
|
32
|
+
why: string;
|
|
33
|
+
step?: string | undefined;
|
|
34
|
+
}[] | undefined;
|
|
35
|
+
meta: {
|
|
36
|
+
timezone?: string | undefined;
|
|
37
|
+
window: {
|
|
38
|
+
since: string;
|
|
39
|
+
until: string;
|
|
40
|
+
};
|
|
41
|
+
complete: boolean;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export interface ProjectTextHints {
|
|
45
|
+
/** The command that syncs one linked chat back to `since` (`YYYY-MM-DD`), or null when no command here can. */
|
|
46
|
+
sync: (chat: StoredProjectGathering["linkedChats"][number], since: string) => string | null;
|
|
47
|
+
/** How to open a document too large to read here. */
|
|
48
|
+
showDocument: string;
|
|
49
|
+
}
|
|
50
|
+
export declare const projectEvidenceText: (found: StoredProjectGathering, hints: ProjectTextHints) => string;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
import { dateWindow, momentOf } from "../services/moment.js";
|
|
4
|
+
import { coverageGaps } from "./messenger/search-answers.js";
|
|
5
|
+
/** Days back (`90d`) or a start date (`2026-09-01`); either way the run freezes its end, so a resume keeps it. */
|
|
6
|
+
export const projectWindowOf = (value) => {
|
|
7
|
+
const days = /^([1-9]\d{0,2})d$/.exec(value)?.[1];
|
|
8
|
+
if (days !== undefined && Number(days) <= 730)
|
|
9
|
+
return { days: Number(days) };
|
|
10
|
+
if (/^\d{4}-\d{2}-\d{2}(T[\d:.]+(Z|[+-]\d{2}:\d{2})?)?$/.test(value))
|
|
11
|
+
return { after: momentOf(value, "--since-time") };
|
|
12
|
+
throw new CliError("validation_error", `--since-time takes 1d to 730d or a date, such as 90d or 2026-09-01 — not ${value}`, { param: "since_time" });
|
|
13
|
+
};
|
|
14
|
+
export const projectCutoffTime = (value) => {
|
|
15
|
+
const parsed = v.safeParse(v.pipe(v.string(), v.isoTimestamp()), value);
|
|
16
|
+
const at = parsed.success ? Date.parse(parsed.output) : Number.NaN;
|
|
17
|
+
const date = parsed.success ? Date.parse(`${parsed.output.slice(0, 10)}T00:00:00.000Z`) : Number.NaN;
|
|
18
|
+
if (!parsed.success ||
|
|
19
|
+
!/(Z|[+-]\d{2}:\d{2})$/.test(parsed.output) ||
|
|
20
|
+
!Number.isFinite(at) ||
|
|
21
|
+
at < 1 ||
|
|
22
|
+
!Number.isFinite(date) ||
|
|
23
|
+
new Date(date).toISOString().slice(0, 10) !== parsed.output.slice(0, 10))
|
|
24
|
+
throw new CliError("validation_error", "--before-time needs a real ISO 8601 timestamp with a UTC offset", {
|
|
25
|
+
param: "before_time",
|
|
26
|
+
});
|
|
27
|
+
return at;
|
|
28
|
+
};
|
|
29
|
+
const coverageRows = ({ linkedChats }) => linkedChats.flatMap((one) => one.completeness === null || one.held === null
|
|
30
|
+
? []
|
|
31
|
+
: [{ chatId: one.chat, title: one.title, completeness: one.completeness, held: one.held }]);
|
|
32
|
+
/**
|
|
33
|
+
* The zone a typed start date was read in, and the window the run used, in UTC. `complete` is "the answer is whole":
|
|
34
|
+
* reads finished and every linked chat held in full; the answer's own `complete` stays "reads finished".
|
|
35
|
+
*/
|
|
36
|
+
export const projectAnswerParts = (sinceTime, found) => {
|
|
37
|
+
const { window } = found;
|
|
38
|
+
const { timezone } = dateWindow({ sinceTime }).meta;
|
|
39
|
+
const { next, missing } = coverageGaps(coverageRows(found), 0, new Date(window.after).toISOString());
|
|
40
|
+
return {
|
|
41
|
+
...(next.length ? { next } : {}),
|
|
42
|
+
...(missing.length ? { missing } : {}),
|
|
43
|
+
meta: {
|
|
44
|
+
...(timezone === undefined ? {} : { timezone }),
|
|
45
|
+
window: { since: new Date(window.after).toISOString(), until: new Date(window.before).toISOString() },
|
|
46
|
+
complete: found.complete && found.completeness === "complete",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const LABELS = {
|
|
51
|
+
"person-only": " [person only]",
|
|
52
|
+
sender: " [sent by contact]",
|
|
53
|
+
"linked-chat": " [linked chat]",
|
|
54
|
+
};
|
|
55
|
+
const day = (at) => new Date(at).toISOString().slice(0, 10);
|
|
56
|
+
const FETCH_STATES = {
|
|
57
|
+
complete: "synced",
|
|
58
|
+
partial: "synced in part",
|
|
59
|
+
failed: "not synced",
|
|
60
|
+
skipped: "not synced, the fetch time ran out first",
|
|
61
|
+
};
|
|
62
|
+
export const projectEvidenceText = (found, hints) => {
|
|
63
|
+
const lanes = new Map(found.lanes.map((lane) => [
|
|
64
|
+
lane.id,
|
|
65
|
+
`${lane.query ?? lane.chat ?? `what ${lane.people.join(", ")} wrote`} (${lane.kinds.join(", ")})`,
|
|
66
|
+
]));
|
|
67
|
+
const linked = found.items.filter((item) => item.context === "linked-chat").length;
|
|
68
|
+
const sent = found.items.filter((item) => item.context === "sender").length;
|
|
69
|
+
const unreadSenders = found.lanes.filter((lane) => lane.kinds.includes("sender") && !found.units.some((unit) => unit.lane === lane.id));
|
|
70
|
+
const failed = found.units.filter((unit) => unit.state === "failed" || unit.state === "unavailable");
|
|
71
|
+
const lines = [
|
|
72
|
+
`${found.project.name}${found.projectRef ? ` — ${found.projectRef}` : ""}: ${found.items.length} new references, ` +
|
|
73
|
+
`${day(found.window.after)} to ${day(found.window.before)}, ${found.scope.sources.length} sources`,
|
|
74
|
+
`as of: ${new Date(found.window.asOf).toISOString()} (exclusive end; reuse --before-time to repeat this window)`,
|
|
75
|
+
`searched: ${[...lanes.values()].join("; ")}`,
|
|
76
|
+
...found.items.map((item) => `${day(item.at)} ${item.ref}${LABELS[item.context] ?? ""}\n ${item.text}` +
|
|
77
|
+
(item.around ?? [])
|
|
78
|
+
.map((one) => `\n ${one.found ? "(hit) " : ""}${one.sender ? `${one.sender}: ` : ""}${one.text}`)
|
|
79
|
+
.join("")),
|
|
80
|
+
];
|
|
81
|
+
const people = found.people.slice(0, 8);
|
|
82
|
+
const morePeople = found.people.length - people.length + found.peopleOmitted;
|
|
83
|
+
if (people.length)
|
|
84
|
+
lines.push(`people who wrote these: ${people.map((one) => `${one.name} (${one.items})`).join(", ")}` +
|
|
85
|
+
`${morePeople ? ` and ${morePeople} more` : ""} — by stored contact, not by names in the text; yours left out`);
|
|
86
|
+
if (linked)
|
|
87
|
+
lines.push(`${linked} of these come from the project's linked chats and name nothing: membership, not a word match`);
|
|
88
|
+
if (sent)
|
|
89
|
+
lines.push(`${sent} of these were written by a contact and may name nothing: matched by sender, not words`);
|
|
90
|
+
for (const lane of unreadSenders)
|
|
91
|
+
lines.push(`${lane.people.join(", ")}: no stored identity in the selected sources, so nothing they wrote was read`);
|
|
92
|
+
if (found.mentions.items.length)
|
|
93
|
+
lines.push(`also names: ${found.mentions.items
|
|
94
|
+
.slice(0, 8)
|
|
95
|
+
.map((one) => `${one.target.kind} ${one.target.name} (${one.matches.length})`)
|
|
96
|
+
.join(", ")} — words found, not membership`);
|
|
97
|
+
if (found.matchedAgain.length)
|
|
98
|
+
lines.push(`${found.matchedAgain.length} earlier references found again`);
|
|
99
|
+
if (found.oversized.length)
|
|
100
|
+
lines.push(`too large to read here: ${found.oversized.map((one) => `${one.ref} (${Math.ceil(one.bytes / 1024)} KiB)`).join(", ")}` +
|
|
101
|
+
` — open each with ${hints.showDocument}`);
|
|
102
|
+
if (failed.length)
|
|
103
|
+
lines.push(`${failed.length} reads failed or had no source: see --json units`);
|
|
104
|
+
if (found.index.state === "failed" || found.index.state === "unavailable")
|
|
105
|
+
lines.push(`the search index could not be brought up to date first (${found.index.code ?? found.index.state})`);
|
|
106
|
+
const pending = [
|
|
107
|
+
...new Set(found.units.filter((unit) => unit.issue?.startsWith("index-")).map((unit) => found.scope.sources[unit.source])),
|
|
108
|
+
];
|
|
109
|
+
if (pending.length)
|
|
110
|
+
lines.push(`not yet searchable: new text in ${pending.map((one) => `${one?.provider}:${one?.account}`).join(", ")} ` +
|
|
111
|
+
"is still waiting for the search index, so it was not found");
|
|
112
|
+
if (pending.some((one) => one?.kind === "mail" || one?.kind === "document") && found.index.state === "as-is")
|
|
113
|
+
lines.push("leave out --no-catch-up to index new mail and documents first");
|
|
114
|
+
if (pending.some((one) => one?.kind === "meeting"))
|
|
115
|
+
lines.push("index stored transcript cues through the meeting provider's indexing workflow; mail/document catch-up does not index them");
|
|
116
|
+
if (found.unreadSources.length)
|
|
117
|
+
lines.push(`not read: ${found.unreadSources
|
|
118
|
+
.map((one) => `${one.source} (${one.kind}${one.reason ? `, ${one.reason}` : ""})`)
|
|
119
|
+
.join(", ")}`);
|
|
120
|
+
for (const one of found.fetch)
|
|
121
|
+
lines.push(`${one.chat}: ${FETCH_STATES[one.state] ?? one.state} before reading` +
|
|
122
|
+
`${one.reason && one.state !== "skipped" ? ` (${one.reason})` : ""}`);
|
|
123
|
+
const held = (one) => one.completeness?.state === "unknown" || !one.held?.oldestAt
|
|
124
|
+
? "never fetched"
|
|
125
|
+
: `held from ${one.held.oldestAt.slice(0, 10)} only`;
|
|
126
|
+
for (const one of found.linkedChats)
|
|
127
|
+
if (one.completeness !== null && one.completeness.state !== "complete") {
|
|
128
|
+
const sync = hints.sync(one, day(found.window.after));
|
|
129
|
+
lines.push(`${one.title ?? one.ref}: ${held(one)}${one.state === "pending" ? ", not read this run" : ""}` +
|
|
130
|
+
(sync ? ` — sync it: ${sync}` : ""));
|
|
131
|
+
}
|
|
132
|
+
const capped = found.linkedChats.filter((one) => one.capped);
|
|
133
|
+
if (capped.length)
|
|
134
|
+
lines.push(`linked chats with more to read, left for --cursor so one chat does not fill the run: ${capped
|
|
135
|
+
.map((one) => `${one.ref} (${one.items} this run)`)
|
|
136
|
+
.join(", ")}`);
|
|
137
|
+
if (found.checkpoint)
|
|
138
|
+
lines.push(`stopped${found.stopped ? ` at the ${found.stopped} budget` : ""}; continue with the same options and`, `--cursor ${found.checkpoint}`);
|
|
139
|
+
else
|
|
140
|
+
lines.push("every read finished; a finished search does not prove there is nothing more");
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
};
|
|
@@ -775,6 +775,7 @@ export declare const tagsCommand: (messenger: import("./index.js").Messenger) =>
|
|
|
775
775
|
export declare const tasksCommand: (messenger: import("./index.js").Messenger) => import("commander").Command;
|
|
776
776
|
export declare const tasksStatsCommand: (messenger: import("./index.js").Messenger) => import("commander").Command;
|
|
777
777
|
export declare const topicsCommand: (messenger: import("./index.js").Messenger) => import("commander").Command;
|
|
778
|
+
export declare const projectsCommand: (messenger: import("./index.js").Messenger) => import("commander").Command;
|
|
778
779
|
export declare const watchCommand: (messenger: import("./index.js").Messenger) => import("commander").Command;
|
|
779
780
|
export declare const runsCommand: (app: import("./entry.js").AppIdentity) => import("commander").Command;
|
|
780
781
|
export declare const skillCommand: (app: import("./entry.js").AppIdentity, skill: URL) => import("commander").Command;
|
|
@@ -36,6 +36,7 @@ import { pinCommand as pinCommandDefinition, starCommands as starCommandsDefinit
|
|
|
36
36
|
import { metadataCommand as metadataCommandDefinition } from "./messenger/metadata-command.js";
|
|
37
37
|
import { modelsCommand as modelsCommandDefinition } from "./messenger/models-command.js";
|
|
38
38
|
import { pollsCommand as pollsCommandDefinition } from "./messenger/polls-command.js";
|
|
39
|
+
import { projectsCommand as projectsCommandDefinition } from "./messenger/projects-command.js";
|
|
39
40
|
import { reactionsCommand as reactionsCommandDefinition } from "./messenger/reactions-command.js";
|
|
40
41
|
import { callsCommand as callsCommandDefinition, stickersCommand as stickersCommandDefinition, } from "./messenger/records-command.js";
|
|
41
42
|
import { repliesCommand as repliesCommandDefinition } from "./messenger/replies-command.js";
|
|
@@ -112,6 +113,7 @@ export const tagsCommand = sharedCommand(tagsCommandDefinition);
|
|
|
112
113
|
export const tasksCommand = sharedCommand(tasksCommandDefinition);
|
|
113
114
|
export const tasksStatsCommand = sharedCommand(tasksStatsCommandDefinition);
|
|
114
115
|
export const topicsCommand = sharedCommand(topicsCommandDefinition);
|
|
116
|
+
export const projectsCommand = sharedCommand(projectsCommandDefinition);
|
|
115
117
|
export const watchCommand = sharedCommand(watchCommandDefinition);
|
|
116
118
|
export const runsCommand = sharedCommand(runsCommandDefinition);
|
|
117
119
|
export const skillCommand = sharedCommand(skillCommandDefinition);
|
package/dist/errors/reasons.js
CHANGED
|
@@ -88,6 +88,7 @@ const OTHERS = [
|
|
|
88
88
|
validation("memory.evidence-required", "a memory needs its evidence"),
|
|
89
89
|
validation("memory.scope-required", "a memory needs its scope"),
|
|
90
90
|
["model.not-downloaded", "not_found", "the model is not downloaded on this machine"],
|
|
91
|
+
["person.no-owner", "not_found", "no person is marked as the owner in the store"],
|
|
91
92
|
["transcription.no-key", "authentication_error", "no OpenAI key is set for transcription"],
|
|
92
93
|
validation("transcription.too-large", "the audio is over the transcription provider's size limit"),
|
|
93
94
|
[
|
|
@@ -322,7 +322,7 @@ const HOUSEKEEPING = new Set([
|
|
|
322
322
|
const BOT_HOUSEKEEPING = new Set(["auth", "list", "sends", "recipients", "mcp"]);
|
|
323
323
|
const STORE_MAINTENANCE = new Set(["info", "check", "copies"]);
|
|
324
324
|
/** Commands outside `messages` that print what people wrote, so `deny messages` reaches them too. */
|
|
325
|
-
const SHOW_MESSAGES = new Set(["inbox", "review", "watch", "serve", "store", "conversations"]);
|
|
325
|
+
const SHOW_MESSAGES = new Set(["inbox", "review", "watch", "serve", "store", "conversations", "projects"]);
|
|
326
326
|
export const readKeysForCommand = (path) => {
|
|
327
327
|
if (path[0] === "bot")
|
|
328
328
|
return readKeysForCommand(path.slice(1)).map((key) => `bot.${key}`);
|
package/dist/services/index.d.ts
CHANGED
|
@@ -9,13 +9,14 @@ export type { MediaArchiveEntry, MediaIdentity } from "./media-archive-manifest.
|
|
|
9
9
|
export type { MoveMediaArchiveOptions } from "./media-archive-move.js";
|
|
10
10
|
export { moveMediaArchive } from "./media-archive-move.js";
|
|
11
11
|
export { type DownloadedModel, downloadImageModel, downloadTextModel, type ModelDownloadOptions, } from "./model-downloads.js";
|
|
12
|
-
export type { ProjectEvidenceContext, ProjectEvidenceItem, ProjectEvidencePerson, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp,
|
|
12
|
+
export type { ProjectChatFetcher, ProjectChatFetchProgress, ProjectChatFetchRequest, ProjectEvidenceContext, ProjectEvidenceItem, ProjectEvidencePerson, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp, } from "./project-gathering.js";
|
|
13
13
|
export { gatherProjectEvidence } from "./project-gathering.js";
|
|
14
14
|
export type { ProjectNeighbour, ProjectNeighbourReader } from "./project-gathering-around.js";
|
|
15
15
|
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
16
16
|
export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
|
|
17
17
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
18
|
-
export type {
|
|
18
|
+
export type { ProjectChatFetched, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectPerson, ProjectSender, ProjectUnitState, } from "./project-gathering-request.js";
|
|
19
|
+
export { canonicalProjectRef, gatherStoredProject, type StoredProjectGathering, type StoredProjectInput, } from "./stored-project-gathering.js";
|
|
19
20
|
import { type AccountService } from "./account.js";
|
|
20
21
|
import { type AdminService } from "./admin.js";
|
|
21
22
|
import { type AdminStatisticsService } from "./admin-statistics.js";
|
package/dist/services/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
|
|
|
8
8
|
export { gatherProjectEvidence } from "./project-gathering.js";
|
|
9
9
|
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
10
10
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
11
|
+
export { canonicalProjectRef, gatherStoredProject, } from "./stored-project-gathering.js";
|
|
11
12
|
import { accountService } from "./account.js";
|
|
12
13
|
import { adminService } from "./admin.js";
|
|
13
14
|
import { adminStatisticsService } from "./admin-statistics.js";
|
|
@@ -52,6 +52,8 @@ export interface ProjectGatheringBudgets {
|
|
|
52
52
|
maxPageBytes?: number;
|
|
53
53
|
maxElapsedMs?: number;
|
|
54
54
|
maxItems?: number;
|
|
55
|
+
/** All `fetch: before-read` chat fetches together. */
|
|
56
|
+
maxFetchMs?: number;
|
|
55
57
|
}
|
|
56
58
|
export type ProjectLaneKind = "project" | "key" | "alias" | "keyword" | "person" | "sender" | "linked-chat";
|
|
57
59
|
export interface ProjectLane {
|
|
@@ -76,7 +78,13 @@ export interface ProjectUnitProgress {
|
|
|
76
78
|
} | null;
|
|
77
79
|
code: string | null;
|
|
78
80
|
}
|
|
79
|
-
|
|
81
|
+
/** A linked chat's fetch before the reads: `skipped` when the budget ran out before it started. */
|
|
82
|
+
export interface ProjectChatFetched {
|
|
83
|
+
chat: string;
|
|
84
|
+
state: "complete" | "partial" | "failed" | "skipped";
|
|
85
|
+
reason: string | null;
|
|
86
|
+
elapsedMs: number;
|
|
87
|
+
}
|
|
80
88
|
export interface ProjectGatheringCheckpoint {
|
|
81
89
|
version: 1;
|
|
82
90
|
runKey: string;
|
|
@@ -84,11 +92,8 @@ export interface ProjectGatheringCheckpoint {
|
|
|
84
92
|
after: number;
|
|
85
93
|
before: number;
|
|
86
94
|
asOf: number;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
history: "complete" | "partial" | "unknown" | null;
|
|
90
|
-
code: string | null;
|
|
91
|
-
}[];
|
|
95
|
+
/** The first invocation's chat fetches, carried so a resumed run reports them without fetching again. */
|
|
96
|
+
fetched: ProjectChatFetched[];
|
|
92
97
|
units: ProjectUnitProgress[];
|
|
93
98
|
/** Present only when the run reads linked chats: a hash of their references. */
|
|
94
99
|
linkedChats?: string;
|
|
@@ -21,6 +21,7 @@ const BUDGETS = {
|
|
|
21
21
|
maxPageBytes: [65536, 262144],
|
|
22
22
|
maxElapsedMs: [60000, 600000],
|
|
23
23
|
maxItems: [100, 500],
|
|
24
|
+
maxFetchMs: [180000, 1800000],
|
|
24
25
|
};
|
|
25
26
|
const fail = (message, param) => {
|
|
26
27
|
throw new CliError("validation_error", message, param ? { param } : undefined);
|
|
@@ -2,22 +2,38 @@ import type { ContextEvidenceItem, ContextEvidenceReader, ContextIndex, ContextS
|
|
|
2
2
|
import type { AccountKey } from "../store/types.js";
|
|
3
3
|
import type { ChatCoverage } from "./chat-coverage.js";
|
|
4
4
|
import { type ProjectNeighbour, type ProjectNeighbourReader } from "./project-gathering-around.js";
|
|
5
|
-
import { type
|
|
6
|
-
/**
|
|
7
|
-
export interface
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
import { type ProjectChatFetched, type ProjectGatheringRequest, type ProjectLane } from "./project-gathering-request.js";
|
|
6
|
+
/** One linked chat's history into the store, back to `after`; the reads that follow find what it saved. */
|
|
7
|
+
export interface ProjectChatFetchRequest {
|
|
8
|
+
account: AccountKey;
|
|
9
|
+
chatId: string;
|
|
10
|
+
after: number;
|
|
11
|
+
before: number;
|
|
12
|
+
/** Aborts when the run's fetch budget is spent; a fetcher stops there and keeps what it saved. */
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
/** When that budget ends, by the run's clock: a fetcher that runs a process gives it this much time to stop itself. */
|
|
15
|
+
until: number;
|
|
16
|
+
onProgress?: (progress: ProjectChatFetchProgress) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface ProjectChatFetchProgress {
|
|
19
|
+
fetched: number;
|
|
20
|
+
oldest: number | null;
|
|
21
|
+
}
|
|
22
|
+
export interface ProjectChatFetcher {
|
|
23
|
+
fetch(request: ProjectChatFetchRequest): Promise<{
|
|
24
|
+
state: "complete" | "partial" | "failed";
|
|
25
|
+
reason?: string;
|
|
16
26
|
}>;
|
|
17
27
|
}
|
|
18
28
|
export interface ProjectGatheringPorts {
|
|
19
29
|
evidence: ContextEvidenceReader;
|
|
20
|
-
fetcher?:
|
|
30
|
+
fetcher?: ProjectChatFetcher;
|
|
31
|
+
/** Each linked chat's fetch as it goes, for a heartbeat; never message text. */
|
|
32
|
+
onFetchProgress?: (progress: ProjectChatFetchProgress & {
|
|
33
|
+
chat: string;
|
|
34
|
+
index: number;
|
|
35
|
+
of: number;
|
|
36
|
+
}) => void;
|
|
21
37
|
index?: ContextIndex;
|
|
22
38
|
neighbours?: ProjectNeighbourReader;
|
|
23
39
|
/** What the store holds of the named chats of one account, e.g. `chatCoverage` over the store. */
|
|
@@ -103,12 +119,7 @@ export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, reque
|
|
|
103
119
|
items?: number | undefined;
|
|
104
120
|
capped?: boolean | undefined;
|
|
105
121
|
}[];
|
|
106
|
-
fetch:
|
|
107
|
-
state: ProjectFetchState;
|
|
108
|
-
history: "complete" | "partial" | "unknown" | null;
|
|
109
|
-
code: string | null;
|
|
110
|
-
source: number;
|
|
111
|
-
}[];
|
|
122
|
+
fetch: ProjectChatFetched[];
|
|
112
123
|
index: ProjectIndexCatchUp;
|
|
113
124
|
units: {
|
|
114
125
|
lane: string;
|
|
@@ -15,6 +15,84 @@ const contextOf = (lane) => lane.kinds.includes("linked-chat")
|
|
|
15
15
|
: lane.kinds.every((k) => k === "person")
|
|
16
16
|
? "person-only"
|
|
17
17
|
: "project";
|
|
18
|
+
/** Complete, or up to date with no gaps and held back past the window start: nothing a fetch would add. */
|
|
19
|
+
const heldForWindow = (row, after) => {
|
|
20
|
+
if (!row)
|
|
21
|
+
return false;
|
|
22
|
+
const { state, upToDate, gaps } = row.completeness;
|
|
23
|
+
if (state === "complete")
|
|
24
|
+
return upToDate !== false;
|
|
25
|
+
const oldest = row.held.oldestAt === null ? Number.NaN : Date.parse(row.held.oldestAt);
|
|
26
|
+
return upToDate === true && !gaps && oldest <= after;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Brings the linked chats the store does not hold for the window up to date, one at a time, inside one time budget
|
|
30
|
+
* for all of them. A chat cut by the budget stays partial and the next one is not started, so a run never waits
|
|
31
|
+
* longer than the budget whatever the fetcher does.
|
|
32
|
+
*/
|
|
33
|
+
const fetchLinkedChats = async (ports, plan, window, now, signal) => {
|
|
34
|
+
const wanted = [];
|
|
35
|
+
for (const linked of plan.linkedChats) {
|
|
36
|
+
if (linked.source === null)
|
|
37
|
+
continue;
|
|
38
|
+
const account = { provider: linked.provider, account: linked.account };
|
|
39
|
+
const [row] = await ports.coverage(account, [linked.chat]);
|
|
40
|
+
if (!heldForWindow(row, window.after))
|
|
41
|
+
wanted.push({ ...linked, account });
|
|
42
|
+
}
|
|
43
|
+
const deadline = now() + plan.budgets.maxFetchMs;
|
|
44
|
+
const results = [];
|
|
45
|
+
for (const [index, linked] of wanted.entries()) {
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
const started = now();
|
|
48
|
+
const left = deadline - started;
|
|
49
|
+
if (left <= 0) {
|
|
50
|
+
results.push({ chat: linked.ref, state: "skipped", reason: "fetch-budget", elapsedMs: 0 });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const budget = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => budget.abort(new CliError("timeout", "the fetch budget ran out")), left);
|
|
55
|
+
const onProgress = ports.onFetchProgress;
|
|
56
|
+
try {
|
|
57
|
+
const answer = await ports.fetcher.fetch({
|
|
58
|
+
account: linked.account,
|
|
59
|
+
chatId: linked.chat,
|
|
60
|
+
after: window.after,
|
|
61
|
+
before: window.before,
|
|
62
|
+
until: deadline,
|
|
63
|
+
signal: signal ? AbortSignal.any([signal, budget.signal]) : budget.signal,
|
|
64
|
+
...(onProgress
|
|
65
|
+
? { onProgress: (progress) => onProgress({ ...progress, chat: linked.ref, index, of: wanted.length }) }
|
|
66
|
+
: {}),
|
|
67
|
+
});
|
|
68
|
+
if (!["complete", "partial", "failed"].includes(answer?.state))
|
|
69
|
+
throw new CliError("invalid_response", "chat fetch returned an unknown state");
|
|
70
|
+
const cut = budget.signal.aborted || now() > deadline;
|
|
71
|
+
results.push({
|
|
72
|
+
chat: linked.ref,
|
|
73
|
+
state: cut && answer.state === "complete" ? "partial" : answer.state,
|
|
74
|
+
reason: answer.reason ?? (cut && answer.state !== "failed" ? "fetch-budget" : null),
|
|
75
|
+
elapsedMs: now() - started,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
signal?.throwIfAborted();
|
|
80
|
+
if (!(error instanceof CliError))
|
|
81
|
+
throw error;
|
|
82
|
+
const cut = budget.signal.aborted;
|
|
83
|
+
results.push({
|
|
84
|
+
chat: linked.ref,
|
|
85
|
+
state: cut ? "partial" : "failed",
|
|
86
|
+
reason: cut ? "fetch-budget" : error.code,
|
|
87
|
+
elapsedMs: now() - started,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
};
|
|
18
96
|
export const gatherProjectEvidence = async (ports, request) => {
|
|
19
97
|
if (!ports?.evidence || typeof ports.evidence.read !== "function")
|
|
20
98
|
throw new CliError("configuration_error", "stored context reads are unavailable in this store");
|
|
@@ -27,38 +105,22 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
27
105
|
// A default that cannot finish one round would leave whole searches or sources unread; an explicit one is kept.
|
|
28
106
|
if (request.budgets?.maxReads === undefined)
|
|
29
107
|
plan.budgets.maxReads = Math.min(512, Math.max(48, units.length));
|
|
30
|
-
if (previous &&
|
|
108
|
+
if (previous && previous.units.length !== units.length)
|
|
31
109
|
throw new CliError("validation_error", "invalid project gathering checkpoint", { param: "checkpoint" });
|
|
32
110
|
const progress = previous?.units.map((u) => ({ ...u })) ??
|
|
33
111
|
units.map(() => ({ state: "pending", pages: 0, cursor: null, code: null }));
|
|
34
|
-
|
|
35
|
-
plan.sources.map(() => ({ state: "not-requested", history: null, code: null }));
|
|
112
|
+
let fetched = previous?.fetched.map((f) => ({ ...f })) ?? [];
|
|
36
113
|
const seen = new Set(previous?.seen ?? []);
|
|
37
114
|
const signal = plan.signal;
|
|
38
115
|
const queries = plan.lanes.flatMap((lane) => (lane.query === null ? [] : [lane.query]));
|
|
39
|
-
if (plan.fetch === "before-read" && !ports.fetcher)
|
|
40
|
-
throw new CliError("configuration_error", "this host cannot fetch
|
|
116
|
+
if (plan.fetch === "before-read" && (!ports.fetcher || !ports.coverage))
|
|
117
|
+
throw new CliError("configuration_error", "this host cannot fetch chat history; use fetch none", {
|
|
41
118
|
param: "fetch",
|
|
42
119
|
});
|
|
43
|
-
if (plan.fetch === "before-read" && ports.fetcher)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
signal?.throwIfAborted();
|
|
48
|
-
try {
|
|
49
|
-
const answer = await ports.fetcher.fetch({ source, after: window.after, before: window.before, signal });
|
|
50
|
-
if (!["fetched", "unsupported"].includes(answer?.status) ||
|
|
51
|
-
!["complete", "partial", "unknown"].includes(answer?.history))
|
|
52
|
-
throw new CliError("invalid_response", "source fetch returned an unknown status");
|
|
53
|
-
fetched[index] = { state: answer.status, history: answer.history, code: null };
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
signal?.throwIfAborted();
|
|
57
|
-
if (!(error instanceof CliError))
|
|
58
|
-
throw error;
|
|
59
|
-
fetched[index] = { state: "failed", history: null, code: error.code };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
120
|
+
if (plan.fetch === "before-read" && ports.fetcher && ports.coverage && previous === null)
|
|
121
|
+
fetched = await fetchLinkedChats(ports, plan, window, now, signal);
|
|
122
|
+
// The fetch has its own budget; the reads keep theirs whole.
|
|
123
|
+
const readFrom = now();
|
|
62
124
|
const indexed = { state: "as-is", mail: 0, documents: 0, code: null };
|
|
63
125
|
const kinds = [...new Set(plan.sources.map((source) => source.kind))].filter((kind) => kind !== "message");
|
|
64
126
|
if (plan.index === "update" && kinds.length) {
|
|
@@ -173,7 +235,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
173
235
|
? "reads"
|
|
174
236
|
: usage.readBytes >= plan.budgets.maxReadBytes
|
|
175
237
|
? "read-bytes"
|
|
176
|
-
: now() -
|
|
238
|
+
: now() - readFrom >= plan.budgets.maxElapsedMs
|
|
177
239
|
? "elapsed"
|
|
178
240
|
: usage.items >= plan.budgets.maxItems
|
|
179
241
|
? "items"
|
|
@@ -348,7 +410,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
348
410
|
scope: { origin: plan.scope, sources: plan.sources },
|
|
349
411
|
lanes: plan.lanes,
|
|
350
412
|
linkedChats: linkedRows,
|
|
351
|
-
fetch: fetched
|
|
413
|
+
fetch: fetched,
|
|
352
414
|
index: indexed,
|
|
353
415
|
units: units.map((unit, index) => ({
|
|
354
416
|
lane: unit.lane.id,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ContextSource } from "../context/evidence.js";
|
|
2
|
+
import type { MessageStore } from "../store/store.js";
|
|
3
|
+
import { gatherProjectEvidence, type ProjectChatFetcher, type ProjectGatheringPorts } from "./project-gathering.js";
|
|
4
|
+
import { type EvidenceMentions } from "./project-gathering-mentions.js";
|
|
5
|
+
export interface StoredProjectInput {
|
|
6
|
+
project: string;
|
|
7
|
+
/** The sources the host resolved from its `--source`, and those it could not read. */
|
|
8
|
+
sources: {
|
|
9
|
+
sources: ContextSource[];
|
|
10
|
+
unread: {
|
|
11
|
+
source: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
reason?: "unknown-provider" | "out-of-scope";
|
|
14
|
+
}[];
|
|
15
|
+
scope: "explicit" | "all-connected";
|
|
16
|
+
};
|
|
17
|
+
aliases?: string[];
|
|
18
|
+
keywords?: string[];
|
|
19
|
+
people?: string[];
|
|
20
|
+
days?: number;
|
|
21
|
+
after?: number;
|
|
22
|
+
asOf?: number;
|
|
23
|
+
checkpoint?: string;
|
|
24
|
+
maxReads?: number;
|
|
25
|
+
limit?: number;
|
|
26
|
+
catchUp?: boolean;
|
|
27
|
+
beforeN?: number;
|
|
28
|
+
afterN?: number;
|
|
29
|
+
/** Syncs the linked chats the store does not hold for the window before reading; off when left out. */
|
|
30
|
+
fetcher?: ProjectChatFetcher;
|
|
31
|
+
maxFetchMs?: number;
|
|
32
|
+
onFetchProgress?: ProjectGatheringPorts["onFetchProgress"];
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
/** `project:<id>`, or the one project a key, name or alias names; several or none is refused, never guessed. */
|
|
36
|
+
export declare const canonicalProjectRef: (store: MessageStore, reference: string) => Promise<string>;
|
|
37
|
+
export type StoredProjectGathering = Awaited<ReturnType<typeof gatherProjectEvidence>> & {
|
|
38
|
+
projectRef: string | null;
|
|
39
|
+
unreadSources: {
|
|
40
|
+
source: string;
|
|
41
|
+
kind: string;
|
|
42
|
+
reason?: "out-of-scope" | "linked-chat-limit" | "member-limit" | "no-identity" | "unknown-provider";
|
|
43
|
+
}[];
|
|
44
|
+
mentions: EvidenceMentions;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* `projects evidence` over a whole store: the project's names, key and aliases, the given words and contacts, its
|
|
48
|
+
* confirmed person members and its confirmed linked chats, in the sources the host resolved.
|
|
49
|
+
*/
|
|
50
|
+
export declare const gatherStoredProject: (store: MessageStore, input: StoredProjectInput) => Promise<StoredProjectGathering>;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { formatReference, parseReference } from "../domain/references.js";
|
|
3
|
+
import { chatCoverage } from "./chat-coverage.js";
|
|
4
|
+
import { gatherProjectEvidence } from "./project-gathering.js";
|
|
5
|
+
import { storedNeighbours } from "./project-gathering-around.js";
|
|
6
|
+
import { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
7
|
+
const PROJECT_ID = /^project:[1-9]\d*$/;
|
|
8
|
+
/** `project:<id>`, or the one project a key, name or alias names; several or none is refused, never guessed. */
|
|
9
|
+
export const canonicalProjectRef = async (store, reference) => {
|
|
10
|
+
if (PROJECT_ID.test(reference)) {
|
|
11
|
+
const catalog = await store.knowledge.vocabulary({ kind: "project" });
|
|
12
|
+
if (!catalog.entries.some((entry) => entry.id === reference.slice(8)))
|
|
13
|
+
throw new CliError("not_found", "the owner project is unavailable");
|
|
14
|
+
return reference;
|
|
15
|
+
}
|
|
16
|
+
const found = await store.knowledge.resolveVocabulary(reference, { kind: "project" });
|
|
17
|
+
const [entry] = found.matches;
|
|
18
|
+
if (found.matches.length !== 1 || !entry)
|
|
19
|
+
throw new CliError(found.matches.length ? "validation_error" : "not_found", found.matches.length
|
|
20
|
+
? `${reference} names ${found.matches.length} projects; give one by its key or project:<id>`
|
|
21
|
+
: `no project ${reference}`, { candidates: found.matches.map((one) => ({ id: one.id, key: one.key, name: one.name })) });
|
|
22
|
+
return `project:${entry.id}`;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* An existing project brings its name and aliases; any other name is gathered as given. An id that names no
|
|
26
|
+
* project, or a name that matches several, is refused rather than guessed.
|
|
27
|
+
*/
|
|
28
|
+
const projectOf = async (store, reference) => {
|
|
29
|
+
const id = /^(project:)?[1-9]\d*$/.test(reference);
|
|
30
|
+
let ref;
|
|
31
|
+
try {
|
|
32
|
+
ref = await canonicalProjectRef(store, id && !reference.startsWith("project:") ? `project:${reference}` : reference);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (!id && error instanceof CliError && error.code === "not_found")
|
|
36
|
+
return { ref: null, name: reference, key: null, aliases: [] };
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
const entry = (await store.knowledge.vocabulary({ kind: "project" })).entries.find((one) => one.id === ref.slice(8));
|
|
40
|
+
if (!entry)
|
|
41
|
+
throw new CliError("not_found", "the owner project is unavailable");
|
|
42
|
+
return { ref, name: entry.name, key: entry.key, aliases: entry.aliases.map((alias) => alias.name) };
|
|
43
|
+
};
|
|
44
|
+
/** `person:me` as the owner's `person:<uid>`; any other reference as written. */
|
|
45
|
+
const withOwner = async (store, reference) => {
|
|
46
|
+
if (reference.trim() !== "person:me")
|
|
47
|
+
return reference;
|
|
48
|
+
const owner = await store.people.owner();
|
|
49
|
+
if (owner === undefined || owner.identities.length === 0)
|
|
50
|
+
throw new CliError("not_found", "person:me: no person is marked as you in the store; mark yours with `<messenger> <profile> contacts link <your id> me`", { reason: "person.no-owner" });
|
|
51
|
+
return `person:${owner.uid}`;
|
|
52
|
+
};
|
|
53
|
+
const sendersOf = (person) => person.identities.map((identity) => ({ provider: identity.provider, id: String(identity.id) }));
|
|
54
|
+
/**
|
|
55
|
+
* A `person:<id>` is read by what their stored identities wrote, and searched only by the short names the owner gave
|
|
56
|
+
* them: their stored name is a messenger display name («Степа Stefan Grachev AVITO M A») that no message spells out.
|
|
57
|
+
* Any other text is a name.
|
|
58
|
+
*/
|
|
59
|
+
const contactOf = async (store, contact) => {
|
|
60
|
+
const reference = await withOwner(store, contact);
|
|
61
|
+
if (!reference.startsWith("person:"))
|
|
62
|
+
return [{ name: contact }];
|
|
63
|
+
const person = await store.people.personByUid(reference.slice("person:".length));
|
|
64
|
+
if (!person)
|
|
65
|
+
throw new CliError("not_found", `no person ${contact}`, { param: "contact" });
|
|
66
|
+
const senders = sendersOf(person);
|
|
67
|
+
const aliases = new Set();
|
|
68
|
+
for (const identity of person.identities)
|
|
69
|
+
for (const account of identity.accounts) {
|
|
70
|
+
const alias = await store.people
|
|
71
|
+
.privateContact({ provider: identity.provider, account }, identity.id)
|
|
72
|
+
.then((one) => one.alias)
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
if (error instanceof CliError && error.code === "not_found")
|
|
75
|
+
return null;
|
|
76
|
+
throw error;
|
|
77
|
+
});
|
|
78
|
+
if (alias)
|
|
79
|
+
aliases.add(alias);
|
|
80
|
+
}
|
|
81
|
+
if (!senders.length && !aliases.size)
|
|
82
|
+
throw new CliError("validation_error", `${contact} has no stored identity or alias to search by; pass a name the texts use to --contact`, { param: "contact" });
|
|
83
|
+
const names = [...aliases].map((name) => ({ name, reference: contact }));
|
|
84
|
+
return senders.length ? [{ reference: contact, senders }, ...names] : names;
|
|
85
|
+
};
|
|
86
|
+
const LINKED_CHATS = 16;
|
|
87
|
+
const CORE_LIMIT = 16;
|
|
88
|
+
/** Confirmed `person` members, in a stable order, so a cut at the lane limit always drops the same ones. */
|
|
89
|
+
const memberRefsOf = async (store, project) => {
|
|
90
|
+
if (!project)
|
|
91
|
+
return [];
|
|
92
|
+
const refs = [];
|
|
93
|
+
for (const relation of await store.knowledge.relations(null, project))
|
|
94
|
+
if (relation.confirmed &&
|
|
95
|
+
relation.kind === "member-of" &&
|
|
96
|
+
relation.to === project &&
|
|
97
|
+
/^person:.+/.test(relation.from))
|
|
98
|
+
refs.push(relation.from);
|
|
99
|
+
return [...new Set(refs)].sort();
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Members are read by what their identities wrote, like `--contact person:<id>` but without the owner's aliases:
|
|
103
|
+
* a short alias would add a text lane that matches mentions, and membership is about who wrote, not who was named.
|
|
104
|
+
*/
|
|
105
|
+
const memberLanesOf = async (store, project, given, room) => {
|
|
106
|
+
const lanes = [];
|
|
107
|
+
const left = [];
|
|
108
|
+
for (const reference of (await memberRefsOf(store, project)).filter((one) => !given.has(one))) {
|
|
109
|
+
const person = await store.people.personByUid(reference.slice("person:".length));
|
|
110
|
+
const senders = person ? sendersOf(person) : [];
|
|
111
|
+
if (!senders.length)
|
|
112
|
+
left.push({ source: reference, kind: "member", reason: "no-identity" });
|
|
113
|
+
else if (lanes.length >= room)
|
|
114
|
+
left.push({ source: reference, kind: "member", reason: "member-limit" });
|
|
115
|
+
else
|
|
116
|
+
lanes.push({ reference, senders });
|
|
117
|
+
}
|
|
118
|
+
return { lanes, left };
|
|
119
|
+
};
|
|
120
|
+
/** Only links the owner confirmed: a suggested one would put a whole chat into the evidence on a guess. */
|
|
121
|
+
const linkedChatsOf = async (store, project) => {
|
|
122
|
+
if (!project)
|
|
123
|
+
return [];
|
|
124
|
+
const chats = [];
|
|
125
|
+
for (const relation of await store.knowledge.relations(null, project)) {
|
|
126
|
+
if (!relation.confirmed || relation.kind !== "member-of" || relation.to !== project)
|
|
127
|
+
continue;
|
|
128
|
+
if (!relation.from.startsWith("chat:"))
|
|
129
|
+
continue;
|
|
130
|
+
const reference = parseReference(relation.from);
|
|
131
|
+
if (reference.type === "chat")
|
|
132
|
+
chats.push(reference);
|
|
133
|
+
}
|
|
134
|
+
return chats.map(({ provider, account, chat }) => ({ provider, account, chat }));
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* `projects evidence` over a whole store: the project's names, key and aliases, the given words and contacts, its
|
|
138
|
+
* confirmed person members and its confirmed linked chats, in the sources the host resolved.
|
|
139
|
+
*/
|
|
140
|
+
export const gatherStoredProject = async (store, input) => {
|
|
141
|
+
if (input.asOf !== undefined && input.after !== undefined && input.asOf <= input.after)
|
|
142
|
+
throw new CliError("validation_error", "--before-time must be later than --since-time", { param: "before_time" });
|
|
143
|
+
if (!store.contextEvidence)
|
|
144
|
+
throw new CliError("configuration_error", "Stored context reads are unavailable in this store");
|
|
145
|
+
const project = await projectOf(store, input.project);
|
|
146
|
+
const { sources, unread, scope } = input.sources;
|
|
147
|
+
if (sources.length === 0)
|
|
148
|
+
throw new CliError("not_found", "no selected source holds anything to read", { param: "source" });
|
|
149
|
+
const contacts = (await Promise.all([...new Set(input.people ?? [])].map((contact) => contactOf(store, contact)))).flat();
|
|
150
|
+
const aliases = [...new Set([...project.aliases, ...(input.aliases ?? [])])].filter((alias) => alias !== project.name);
|
|
151
|
+
// Counted without merging look-alikes, so the room left for members is never more than Core will take.
|
|
152
|
+
const searchLanes = 1 +
|
|
153
|
+
(project.key ? 1 : 0) +
|
|
154
|
+
aliases.length +
|
|
155
|
+
(input.keywords?.length ?? 0) +
|
|
156
|
+
contacts.filter((one) => one.name).length;
|
|
157
|
+
const room = Math.min(CORE_LIMIT - contacts.length, CORE_LIMIT - searchLanes - contacts.filter((one) => one.senders).length);
|
|
158
|
+
const members = await memberLanesOf(store, project.ref, new Set(contacts.flatMap((one) => one.reference ?? [])), room);
|
|
159
|
+
const people = [...contacts, ...members.lanes];
|
|
160
|
+
const linked = await linkedChatsOf(store, project.ref);
|
|
161
|
+
const linkedChats = linked.slice(0, LINKED_CHATS);
|
|
162
|
+
const budgets = {
|
|
163
|
+
...(input.maxReads === undefined ? {} : { maxReads: input.maxReads }),
|
|
164
|
+
...(input.limit === undefined ? {} : { maxItems: input.limit }),
|
|
165
|
+
...(input.maxFetchMs === undefined ? {} : { maxFetchMs: input.maxFetchMs }),
|
|
166
|
+
};
|
|
167
|
+
const gathered = await gatherProjectEvidence({
|
|
168
|
+
evidence: store.contextEvidence,
|
|
169
|
+
neighbours: storedNeighbours(store),
|
|
170
|
+
coverage: (account, chatIds) => chatCoverage(store, account, chatIds),
|
|
171
|
+
...(store.contextIndex ? { index: store.contextIndex } : {}),
|
|
172
|
+
...(input.fetcher ? { fetcher: input.fetcher } : {}),
|
|
173
|
+
...(input.onFetchProgress ? { onFetchProgress: input.onFetchProgress } : {}),
|
|
174
|
+
}, {
|
|
175
|
+
project: {
|
|
176
|
+
name: project.name,
|
|
177
|
+
...(project.key ? { key: project.key } : {}),
|
|
178
|
+
...(aliases.length ? { aliases } : {}),
|
|
179
|
+
},
|
|
180
|
+
index: input.catchUp === false ? "as-is" : "update",
|
|
181
|
+
around: { before: input.beforeN ?? 2, after: input.afterN ?? 2 },
|
|
182
|
+
...(input.keywords?.length ? { keywords: input.keywords } : {}),
|
|
183
|
+
...(people.length ? { people } : {}),
|
|
184
|
+
...(linkedChats.length ? { linkedChats } : {}),
|
|
185
|
+
...(input.fetcher ? { fetch: "before-read" } : {}),
|
|
186
|
+
sources,
|
|
187
|
+
scope,
|
|
188
|
+
...(input.days === undefined ? {} : { lookbackDays: input.days }),
|
|
189
|
+
...(input.after === undefined ? {} : { after: input.after }),
|
|
190
|
+
...(input.asOf === undefined ? {} : { asOf: input.asOf }),
|
|
191
|
+
...(input.checkpoint === undefined ? {} : { checkpoint: input.checkpoint }),
|
|
192
|
+
...(Object.keys(budgets).length ? { budgets } : {}),
|
|
193
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
194
|
+
});
|
|
195
|
+
const mentions = await findEvidenceMentions(store.knowledge, gathered, { exclude: project.ref });
|
|
196
|
+
const unreadChats = [
|
|
197
|
+
...gathered.linkedChats
|
|
198
|
+
.filter((one) => one.state === "out-of-scope")
|
|
199
|
+
.map((one) => ({ source: one.ref, kind: "linked-chat", reason: "out-of-scope" })),
|
|
200
|
+
...linked.slice(LINKED_CHATS).map((one) => ({
|
|
201
|
+
source: formatReference({ type: "chat", ...one }),
|
|
202
|
+
kind: "linked-chat",
|
|
203
|
+
reason: "linked-chat-limit",
|
|
204
|
+
})),
|
|
205
|
+
];
|
|
206
|
+
return { projectRef: project.ref, unreadSources: [...unread, ...unreadChats, ...members.left], ...gathered, mentions };
|
|
207
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.504.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
package/parity.json
CHANGED
|
@@ -269,6 +269,11 @@
|
|
|
269
269
|
"value": "<names>",
|
|
270
270
|
"meaning": "the counter fields: views,reactions,comments"
|
|
271
271
|
},
|
|
272
|
+
"--credential-storage": {
|
|
273
|
+
"value": "<storage>",
|
|
274
|
+
"meaning": "where the sign-in secret is kept: keyring, which falls back to a private file by itself when no keyring works, or file, not recommended",
|
|
275
|
+
"default": "keyring"
|
|
276
|
+
},
|
|
272
277
|
"--cursor": {
|
|
273
278
|
"value": "<cursor>",
|
|
274
279
|
"meaning": "continue from the extraction or ranking-evidence cursor"
|
|
@@ -4238,6 +4243,10 @@
|
|
|
4238
4243
|
"in": ["tg"],
|
|
4239
4244
|
"reason": "Telegram asks each client for its own app from my.telegram.org; MAX has no such step; WhatsApp has no application registration"
|
|
4240
4245
|
},
|
|
4246
|
+
"--credential-storage": {
|
|
4247
|
+
"in": ["max"],
|
|
4248
|
+
"reason": "WC-SECR-4's discouraged file choice for the sign-in secret; max's token goes through cli-base's Credentials, while tg's and WhatsApp's sign-ins are the library's own session file (the rule's exception)"
|
|
4249
|
+
},
|
|
4241
4250
|
"--sms": {
|
|
4242
4251
|
"in": ["tg"],
|
|
4243
4252
|
"reason": "asks Telegram to resend its login code by SMS; MAX has no such choice; WhatsApp links a device by QR or pairing code, never by an SMS code"
|