@wirecat/cli-core 0.552.0 → 0.554.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 +36 -0
- package/dist/cli/compact.d.ts +4 -0
- package/dist/cli/compact.js +65 -7
- package/dist/cli/messenger/context.d.ts +3 -0
- package/dist/cli/messenger/projects-command.js +38 -11
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- 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.d.ts +0 -1
- package/dist/cli/schedule/schedule-command.js +6 -1
- package/dist/mail/email-input.d.ts +2 -0
- package/dist/mail/email-input.js +6 -1
- package/dist/mail/retry.js +2 -1
- package/dist/mail/sync.js +5 -1
- package/dist/mail/testing/fake.d.ts +2 -0
- package/dist/mail/testing/fake.js +1 -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/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/stored-project-gathering.d.ts +79 -4
- package/dist/services/stored-project-gathering.js +171 -22
- package/dist/store/messages.d.ts +2 -1
- package/dist/store/messages.js +2 -2
- package/dist/store/sqlite/email-locations.d.ts +1 -0
- package/dist/store/sqlite/email-locations.js +4 -0
- package/dist/store/sqlite/emails.d.ts +2 -0
- package/dist/store/sqlite/emails.js +3 -6
- 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
|
@@ -49,7 +49,16 @@ export const projectStatusSchema = v.looseObject({
|
|
|
49
49
|
attention: v.number(),
|
|
50
50
|
why: v.nullable(v.string()),
|
|
51
51
|
})),
|
|
52
|
-
|
|
52
|
+
purposes: v.union([v.literal("all"), v.array(v.string())]),
|
|
53
|
+
out_of_scope: v.optional(v.array(v.strictObject({
|
|
54
|
+
chat: v.string(),
|
|
55
|
+
reason: v.string(),
|
|
56
|
+
purpose: v.optional(v.string()),
|
|
57
|
+
messages: v.optional(v.number()),
|
|
58
|
+
first_at: v.optional(v.nullable(v.string())),
|
|
59
|
+
last_at: v.optional(v.nullable(v.string())),
|
|
60
|
+
read_command: v.optional(v.nullable(v.string())),
|
|
61
|
+
}))),
|
|
53
62
|
failed: v.optional(v.array(v.strictObject({ signal: v.string(), why: v.string() }))),
|
|
54
63
|
...Object.fromEntries(Object.keys(STATUS_SIGNAL_ROWS).map((name) => [listKey(name.slice(7)), v.optional(v.array(statusItem))])),
|
|
55
64
|
});
|
|
@@ -78,9 +87,21 @@ const nextOf = (found, hints) => {
|
|
|
78
87
|
const reads = top.flatMap(({ chat, why }) => partial(found, chat) ? [] : because(hints.read(chat, readFrom(found, chat.ref)), why));
|
|
79
88
|
return [...syncs, ...reads].slice(0, 2);
|
|
80
89
|
};
|
|
81
|
-
const
|
|
90
|
+
const leftOutOf = (found) => found.outOfScope.filter((one) => one.reason === "purpose");
|
|
91
|
+
const readLine = (found, one, hints) => {
|
|
92
|
+
const step = hints.readChat?.({ ref: one.chat, account: one.account, chatId: one.chatId }, day(found.window.since));
|
|
93
|
+
return step ? hints.line(step) : null;
|
|
94
|
+
};
|
|
95
|
+
/** `12 messages 2026-10-03…2026-11-01`, or that the window holds none. */
|
|
96
|
+
export const heldInWindow = (one) => one.messages && one.firstAt && one.lastAt
|
|
97
|
+
? `${one.messages} message${one.messages === 1 ? "" : "s"} ${day(one.firstAt)}…${day(one.lastAt)}`
|
|
98
|
+
: "no messages in the window";
|
|
99
|
+
const leftOutWhy = (one, read) => `${one.chat}, purpose ${one.purpose}, not read: ${heldInWindow(one)}` +
|
|
100
|
+
(read ? `; read it: ${read}` : "; add its purpose to --purpose to read it");
|
|
101
|
+
const missingOf = (found, hints) => {
|
|
82
102
|
const held = found.chats.filter((chat) => partial(found, chat));
|
|
83
103
|
const [first] = held;
|
|
104
|
+
const others = found.outOfScope.filter((one) => one.reason !== "purpose");
|
|
84
105
|
return [
|
|
85
106
|
...(first
|
|
86
107
|
? [
|
|
@@ -92,16 +113,20 @@ const missingOf = (found) => {
|
|
|
92
113
|
},
|
|
93
114
|
]
|
|
94
115
|
: []),
|
|
95
|
-
...(
|
|
116
|
+
...(others.length
|
|
96
117
|
? [
|
|
97
118
|
{
|
|
98
119
|
field: "chats",
|
|
99
|
-
why: `${
|
|
100
|
-
...new Set(
|
|
120
|
+
why: `${others.length} linked chats were not read here (${[
|
|
121
|
+
...new Set(others.map((one) => one.reason)),
|
|
101
122
|
].join(", ")}); memo projects status reads every account`,
|
|
102
123
|
},
|
|
103
124
|
]
|
|
104
125
|
: []),
|
|
126
|
+
...leftOutOf(found).map((one) => {
|
|
127
|
+
const read = readLine(found, one, hints);
|
|
128
|
+
return { field: "chats", why: leftOutWhy(one, read), ...(read ? { step: "messages.list" } : {}) };
|
|
129
|
+
}),
|
|
105
130
|
...found.failed.map((one) => ({ field: `signal:${one.signal}`, why: one.why })),
|
|
106
131
|
];
|
|
107
132
|
};
|
|
@@ -145,11 +170,26 @@ export const projectStatusAnswer = (found, hints) => {
|
|
|
145
170
|
attention: weights.get(chat.ref)?.weight ?? 0,
|
|
146
171
|
why: weights.get(chat.ref)?.why ?? null,
|
|
147
172
|
})),
|
|
148
|
-
|
|
173
|
+
purposes: found.purposes,
|
|
174
|
+
...(found.outOfScope.length
|
|
175
|
+
? {
|
|
176
|
+
outOfScope: found.outOfScope.map((one) => one.reason === "purpose"
|
|
177
|
+
? {
|
|
178
|
+
chat: one.chat,
|
|
179
|
+
reason: one.reason,
|
|
180
|
+
purpose: one.purpose,
|
|
181
|
+
messages: one.messages,
|
|
182
|
+
firstAt: one.firstAt,
|
|
183
|
+
lastAt: one.lastAt,
|
|
184
|
+
readCommand: readLine(found, one, hints),
|
|
185
|
+
}
|
|
186
|
+
: one),
|
|
187
|
+
}
|
|
188
|
+
: {}),
|
|
149
189
|
...(found.failed.length ? { failed: found.failed } : {}),
|
|
150
190
|
...lists,
|
|
151
191
|
});
|
|
152
|
-
const missing = missingOf(found);
|
|
192
|
+
const missing = missingOf(found, hints);
|
|
153
193
|
const complete = found.failed.length === 0 && !found.chats.some((chat) => partial(found, chat));
|
|
154
194
|
return {
|
|
155
195
|
item,
|
|
@@ -235,8 +275,11 @@ export const projectStatusText = (found, hints) => {
|
|
|
235
275
|
const cut = one.data.cut;
|
|
236
276
|
lines.push(` ${cut ? `${data.child} not read (${cut})` : `${data.key ?? data.child} · ${data.name} · ${data.chats} chats`}`);
|
|
237
277
|
}
|
|
238
|
-
|
|
239
|
-
|
|
278
|
+
const others = found.outOfScope.filter((one) => one.reason !== "purpose");
|
|
279
|
+
if (others.length)
|
|
280
|
+
lines.push(`Not read here: ${others.map((one) => `${one.chat} (${one.reason})`).join(", ")}`);
|
|
281
|
+
for (const one of leftOutOf(found))
|
|
282
|
+
lines.push(`Left out: ${leftOutWhy(one, readLine(found, one, hints))}`);
|
|
240
283
|
for (const one of found.failed)
|
|
241
284
|
lines.push(`Signal ${one.signal} failed: ${one.why}`);
|
|
242
285
|
if (next.length)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { type AppIdentity } from "../../ports/app.js";
|
|
3
3
|
import { type AnswerOutput } from "../envelope.js";
|
|
4
|
-
/** What the schedule command needs of a CLI: who it is and, per command, where it prints and which profile runs. */
|
|
5
4
|
export interface ScheduleHost {
|
|
6
5
|
app: AppIdentity;
|
|
7
6
|
context(command: Command, resolving?: {
|
|
@@ -68,6 +68,11 @@ const withWhen = (command) => command
|
|
|
68
68
|
.option("--every <duration>", "repeat: 30m, 2h, 1d")
|
|
69
69
|
.option("--at <HH:MM>", "every day at this local time")
|
|
70
70
|
.option("--at-time <time>", "one run: 30m, 2d from now, or an ISO time; the schedule removes itself after it");
|
|
71
|
+
/** What the schedule command needs of a CLI: who it is and, per command, where it prints and which profile runs. */
|
|
72
|
+
/** `skill show follow-ups` exists only for messenger CLIs; any other CLI gets a read-only summary it can run. */
|
|
73
|
+
const promptExample = (app) => app.messenger === false
|
|
74
|
+
? `summarize what came in during the last day: ${app.command} personal search messages 'date>1d' --newest --limit 20`
|
|
75
|
+
: `run the daily follow-up review: ${app.command} skill show follow-ups`;
|
|
71
76
|
/**
|
|
72
77
|
* Runs the CLI's own commands, or an agent, later or on a timer — through systemd (Linux), launchd
|
|
73
78
|
* (macOS) or Task Scheduler (Windows). Every timer runs `schedule exec <file>`; the schedule file holds
|
|
@@ -142,7 +147,7 @@ export const scheduleCommandFor = ({ app, context: contextOf }) => {
|
|
|
142
147
|
.description(`start an agent (claude or codex) with a prompt on a timer; it runs with ${app.envPrefix}_READ_ONLY=1, so it reads and drafts and cannot send — its answer is kept and you are notified`)
|
|
143
148
|
.argument("<name>", "lowercase letters, digits and dashes")
|
|
144
149
|
.addArgument(new Argument("<harness>", "the agent to start").choices(HARNESSES))
|
|
145
|
-
.argument("<prompt>", `what to do, e.g. "
|
|
150
|
+
.argument("<prompt>", `what to do, e.g. "${promptExample(app)}"`)
|
|
146
151
|
.addOption(new Option("--max-cost <usd>", "claude only: stop a run that costs more than this, in US dollars").argParser(Number))).action(async function (name, harness, prompt) {
|
|
147
152
|
const { context, system, environment, save } = prepare(this);
|
|
148
153
|
const options = this.opts();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { EmailInput } from "../store/index.js";
|
|
2
2
|
import type { ParsedMail } from "./parse.js";
|
|
3
3
|
import type { Listed } from "./source.js";
|
|
4
|
+
/** The Date header, or the server's internal date when the header is missing or unreadable. */
|
|
5
|
+
export declare const sentAtOf: (date: string | null, receivedAt: string) => number;
|
|
4
6
|
/** What one read email stores of itself, whichever run read it; where it is filed is the caller's. */
|
|
5
7
|
export declare const parsedEmail: (mail: ParsedMail, item: Listed, header: Record<string, string | boolean>, address: string) => Omit<EmailInput, "mailboxes">;
|
package/dist/mail/email-input.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { addressOf } from "./attachments.js";
|
|
2
|
+
/** The Date header, or the server's internal date when the header is missing or unreadable. */
|
|
3
|
+
export const sentAtOf = (date, receivedAt) => {
|
|
4
|
+
const sent = date === null ? Number.NaN : Date.parse(date);
|
|
5
|
+
return Number.isNaN(sent) ? Date.parse(receivedAt) : sent;
|
|
6
|
+
};
|
|
2
7
|
/** What one read email stores of itself, whichever run read it; where it is filed is the caller's. */
|
|
3
8
|
export const parsedEmail = (mail, item, header, address) => ({
|
|
4
9
|
externalId: item.messageId,
|
|
@@ -7,7 +12,7 @@ export const parsedEmail = (mail, item, header, address) => ({
|
|
|
7
12
|
to: mail.to.map(addressOf),
|
|
8
13
|
cc: mail.cc.map(addressOf),
|
|
9
14
|
bcc: mail.bcc.map(addressOf),
|
|
10
|
-
sentAt:
|
|
15
|
+
sentAt: sentAtOf(mail.date, item.receivedAt),
|
|
11
16
|
receivedAt: Date.parse(item.receivedAt),
|
|
12
17
|
bodyText: mail.text || null,
|
|
13
18
|
size: item.size ?? null,
|
package/dist/mail/retry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { mailAccountKey } from "./account-key.js";
|
|
3
3
|
import { addressOf, FETCH_CHUNK, readAttachments } from "./attachments.js";
|
|
4
|
+
import { sentAtOf } from "./email-input.js";
|
|
4
5
|
import { mailFolders } from "./folders.js";
|
|
5
6
|
import { mailHeadersOf } from "./headers.js";
|
|
6
7
|
import { parseMail } from "./parse.js";
|
|
@@ -74,7 +75,7 @@ export const retryUnread = async ({ store, source, account, since, max, maxBodyC
|
|
|
74
75
|
to: mail.to.map(addressOf),
|
|
75
76
|
cc: mail.cc.map(addressOf),
|
|
76
77
|
bcc: mail.bcc.map(addressOf),
|
|
77
|
-
sentAt:
|
|
78
|
+
sentAt: sentAtOf(mail.date, item.receivedAt),
|
|
78
79
|
receivedAt: Date.parse(item.receivedAt),
|
|
79
80
|
bodyText: mail.text || null,
|
|
80
81
|
size: item.size ?? stored.size,
|
package/dist/mail/sync.js
CHANGED
|
@@ -24,6 +24,8 @@ const DAY = 86_400_000;
|
|
|
24
24
|
const EDGE = 2 * DAY;
|
|
25
25
|
/** How often a long run saves where it is, so a kill costs at most this much listing again. */
|
|
26
26
|
const SAVE_EVERY_MS = 30_000;
|
|
27
|
+
/** About 55 runs clear a 276,000-email account; each is a short write that does not hold the sync up. */
|
|
28
|
+
const SENT_AT_BACKFILL_PER_RUN = 5000;
|
|
27
29
|
const SLICE_SIZE = 50;
|
|
28
30
|
const SLICE_PAUSE_MS = 100;
|
|
29
31
|
/** About a minute of listing; the 30-day window of the timer stays below it. */
|
|
@@ -682,7 +684,7 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
|
|
|
682
684
|
// Subject and text stay out of the columns search reads, so recorded mail is never a search hit.
|
|
683
685
|
subject: null,
|
|
684
686
|
from: headers?.from === undefined ? null : { address: headers.from, name: headers.fromName ?? null },
|
|
685
|
-
sentAt:
|
|
687
|
+
sentAt: Date.parse(item.receivedAt),
|
|
686
688
|
receivedAt: Date.parse(item.receivedAt),
|
|
687
689
|
bodyText: null,
|
|
688
690
|
size: item.size ?? null,
|
|
@@ -816,6 +818,8 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
|
|
|
816
818
|
await store.mail.setMailboxes(accountId, [bulkBox], bulk, startedAt);
|
|
817
819
|
if (!snapshot)
|
|
818
820
|
await store.sync.setSyncState(key, "mail_folder_scope", scope);
|
|
821
|
+
if (!snapshot)
|
|
822
|
+
await store.mail.backfillSentAt(accountId, SENT_AT_BACKFILL_PER_RUN);
|
|
819
823
|
await stopIfAborted();
|
|
820
824
|
const indexed = await index?.(store, key);
|
|
821
825
|
const coverage = {
|
|
@@ -31,7 +31,7 @@ export const rawOf = (m) => {
|
|
|
31
31
|
...(m.listId ? [`List-Id: News <${m.listId}>`, `List-Unsubscribe: <mailto:leave@${m.listId}>`] : []),
|
|
32
32
|
`Subject: ${m.subject}`,
|
|
33
33
|
`Message-ID: <${m.messageId}@example.test>`,
|
|
34
|
-
`Date: ${new Date(m.receivedAt).toUTCString()}`,
|
|
34
|
+
`Date: ${m.date ?? new Date(m.receivedAt).toUTCString()}`,
|
|
35
35
|
"MIME-Version: 1.0",
|
|
36
36
|
...(m.extraHeaders ?? []),
|
|
37
37
|
];
|
package/dist/services/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { type ChatCoverage, chatCoverage } from "./chat-coverage.js";
|
|
|
6
6
|
export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
|
|
7
7
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
8
8
|
export { excerpt } from "./excerpt.js";
|
|
9
|
-
export { aliasHits,
|
|
9
|
+
export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, type LinkCandidate, type LinkScorer, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput as LinkScanInput, type ScannedProject, type ScannedProposal, type ScanResult as LinkScanResult, type ScorerInput, type Signal as LinkSignal, scanProjectLinks, } from "./link-scorers/index.js";
|
|
10
10
|
export type { MediaArchiveOptions, MediaArchivePorts } from "./media-archive.js";
|
|
11
11
|
export { archiveMedia, checkMediaArchive } from "./media-archive.js";
|
|
12
12
|
export type { MediaArchiveEntry, MediaIdentity } from "./media-archive-manifest.js";
|
|
@@ -21,9 +21,10 @@ export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./pr
|
|
|
21
21
|
export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
|
|
22
22
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
23
23
|
export type { ProjectChatFetched, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectPerson, ProjectSender, ProjectUnitState, } from "./project-gathering-request.js";
|
|
24
|
-
export {
|
|
24
|
+
export { BUILT_IN_PURPOSE_CONFIG, BUILT_IN_PURPOSES, DEFAULT_PURPOSES, LINK_PURPOSE, type PurposeChoice, type PurposeConfig, purposeList, purposeRead, purposesOf, } from "./project-purposes.js";
|
|
25
|
+
export { type MoneyTotal, type OutOfScope, type ProjectStatus, type ProjectStatusInput, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, type StatusAttention, type StatusChat, type StatusChatSummary, type StatusContext, type StatusItem, type StatusSignal, } from "./project-status/index.js";
|
|
25
26
|
export { questions } from "./questions.js";
|
|
26
|
-
export { canonicalProjectRef, gatherStoredProject, type LinkedChat, linkedChatsOf, type StoredProjectGathering, type StoredProjectInput, } from "./stored-project-gathering.js";
|
|
27
|
+
export { type ChatCount, type ChatCounter, canonicalProjectRef, gatherStoredProject, type LeftOutChat, type LinkedChat, linkedChatsOf, type ProjectMember, type StoredProjectGathering, type StoredProjectInput, storedChatCount, } from "./stored-project-gathering.js";
|
|
27
28
|
import { type AccountService } from "./account.js";
|
|
28
29
|
import { type AdminService } from "./admin.js";
|
|
29
30
|
import { type AdminStatisticsService } from "./admin-statistics.js";
|
package/dist/services/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { AGENDA_DEFAULTS } from "./agenda.js";
|
|
|
5
5
|
export { chatCoverage } from "./chat-coverage.js";
|
|
6
6
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
7
7
|
export { excerpt } from "./excerpt.js";
|
|
8
|
-
export { aliasHits,
|
|
8
|
+
export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./link-scorers/index.js";
|
|
9
9
|
export { archiveMedia, checkMediaArchive } from "./media-archive.js";
|
|
10
10
|
export { moveMediaArchive } from "./media-archive-move.js";
|
|
11
11
|
export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
|
|
@@ -13,9 +13,10 @@ export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, moneyClassesOf, PROMISE } from
|
|
|
13
13
|
export { gatherProjectEvidence } from "./project-gathering.js";
|
|
14
14
|
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
15
15
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
16
|
-
export { DEFAULT_PURPOSES,
|
|
16
|
+
export { BUILT_IN_PURPOSE_CONFIG, BUILT_IN_PURPOSES, DEFAULT_PURPOSES, LINK_PURPOSE, purposeList, purposeRead, purposesOf, } from "./project-purposes.js";
|
|
17
|
+
export { projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, } from "./project-status/index.js";
|
|
17
18
|
export { questions } from "./questions.js";
|
|
18
|
-
export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, } from "./stored-project-gathering.js";
|
|
19
|
+
export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, storedChatCount, } from "./stored-project-gathering.js";
|
|
19
20
|
import { accountService } from "./account.js";
|
|
20
21
|
import { adminService } from "./admin.js";
|
|
21
22
|
import { adminStatisticsService } from "./admin-statistics.js";
|
|
@@ -5,5 +5,5 @@ export { emailDomain } from "./email-domain.js";
|
|
|
5
5
|
export { mailThread } from "./mail-thread.js";
|
|
6
6
|
export { memberChats } from "./member-chats.js";
|
|
7
7
|
export { memberDialog } from "./member-dialog.js";
|
|
8
|
-
export {
|
|
8
|
+
export { combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput, scanProjectLinks, } from "./scan.js";
|
|
9
9
|
export type { LinkCandidate, LinkScorer, ScannedProject, ScannedProposal, ScanResult, ScorerInput, Signal, } from "./types.js";
|
|
@@ -5,4 +5,4 @@ export { emailDomain } from "./email-domain.js";
|
|
|
5
5
|
export { mailThread } from "./mail-thread.js";
|
|
6
6
|
export { memberChats } from "./member-chats.js";
|
|
7
7
|
export { memberDialog } from "./member-dialog.js";
|
|
8
|
-
export {
|
|
8
|
+
export { combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./scan.js";
|
|
@@ -10,7 +10,6 @@ export declare const SCAN_BOT: {
|
|
|
10
10
|
readonly bot: "memo-scan";
|
|
11
11
|
readonly kind: "rule";
|
|
12
12
|
};
|
|
13
|
-
export declare const BUILT_IN_PURPOSES: readonly ["work", "sales", "support", "other"];
|
|
14
13
|
export declare const DEFAULT_THRESHOLD = 0.5;
|
|
15
14
|
/** A person ↔ person scan keeps the old rule's reach: one weak signal is enough to ask the owner. */
|
|
16
15
|
export declare const PERSON_THRESHOLD = 0.3;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { parseReference } from "../../domain/references.js";
|
|
3
|
+
import { BUILT_IN_PURPOSES } from "../project-purposes.js";
|
|
3
4
|
import { canonicalProjectRef } from "../stored-project-gathering.js";
|
|
4
5
|
import { aliasHits } from "./alias-hits.js";
|
|
5
6
|
import { chatSenders } from "./chat-senders.js";
|
|
@@ -15,7 +16,6 @@ export const SCORERS = [chatTitle, aliasHits, memberChats, memberDialog, chatSen
|
|
|
15
16
|
export const PERSON_SCORERS = [emailDomain];
|
|
16
17
|
/** The rule bot every scan proposal is written by: only the owner decides its proposals. */
|
|
17
18
|
export const SCAN_BOT = { bot: "memo-scan", kind: "rule" };
|
|
18
|
-
export const BUILT_IN_PURPOSES = ["work", "sales", "support", "other"];
|
|
19
19
|
export const DEFAULT_THRESHOLD = 0.5;
|
|
20
20
|
/** A person ↔ person scan keeps the old rule's reach: one weak signal is enough to ask the owner. */
|
|
21
21
|
export const PERSON_THRESHOLD = 0.3;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** What a chat can be for in a project, when the host's config names none. */
|
|
2
|
+
export declare const BUILT_IN_PURPOSES: readonly string[];
|
|
3
|
+
/** What `projects evidence` and `projects status` read when no purpose is asked for (built in; memo's config may change it). */
|
|
4
|
+
export declare const DEFAULT_PURPOSES: readonly string[];
|
|
5
|
+
/** A link with no purpose is a work chat. */
|
|
6
|
+
export declare const LINK_PURPOSE = "work";
|
|
7
|
+
/** The purposes a host's config lists; core knows only the built-in ones and takes the rest as given. */
|
|
8
|
+
export interface PurposeConfig {
|
|
9
|
+
purposes: readonly string[];
|
|
10
|
+
/** Read when no `--purpose` is given. */
|
|
11
|
+
defaults: readonly string[];
|
|
12
|
+
/** A project's own defaults, by its key or `project:<id>`. */
|
|
13
|
+
projects?: Readonly<Record<string, readonly string[]>>;
|
|
14
|
+
}
|
|
15
|
+
export declare const BUILT_IN_PURPOSE_CONFIG: PurposeConfig;
|
|
16
|
+
/** The purposes one run reads: `all`, or a set. */
|
|
17
|
+
export type PurposeChoice = "all" | ReadonlySet<string>;
|
|
18
|
+
/**
|
|
19
|
+
* The purposes a run reads: the asked ones, each checked against the config's list, else the project's own defaults,
|
|
20
|
+
* else the config's. `all` reads every purpose, a link's unlisted one too.
|
|
21
|
+
*/
|
|
22
|
+
export declare const purposesOf: (asked: readonly string[] | undefined, config?: PurposeConfig, project?: {
|
|
23
|
+
ref: string | null;
|
|
24
|
+
key: string | null;
|
|
25
|
+
}) => PurposeChoice;
|
|
26
|
+
export declare const purposeRead: (choice: PurposeChoice, purpose: string | null) => boolean;
|
|
27
|
+
/** `a,b` and repeated flags as one list. */
|
|
28
|
+
export declare const purposeList: (value: string, previous?: string[]) => string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
/** What a chat can be for in a project, when the host's config names none. */
|
|
3
|
+
export const BUILT_IN_PURPOSES = ["work", "sales", "support", "other"];
|
|
4
|
+
/** What `projects evidence` and `projects status` read when no purpose is asked for (built in; memo's config may change it). */
|
|
5
|
+
export const DEFAULT_PURPOSES = ["work"];
|
|
6
|
+
/** A link with no purpose is a work chat. */
|
|
7
|
+
export const LINK_PURPOSE = "work";
|
|
8
|
+
export const BUILT_IN_PURPOSE_CONFIG = { purposes: BUILT_IN_PURPOSES, defaults: DEFAULT_PURPOSES };
|
|
9
|
+
const unknown = (asked, config) => {
|
|
10
|
+
const wrong = asked.filter((one) => !config.purposes.includes(one));
|
|
11
|
+
if (wrong.length)
|
|
12
|
+
throw new CliError("validation_error", `unknown purpose ${wrong.join(", ")}; the configured ones are ${config.purposes.join(", ")}, or all`, { param: "purpose", purposes: [...config.purposes] });
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The purposes a run reads: the asked ones, each checked against the config's list, else the project's own defaults,
|
|
16
|
+
* else the config's. `all` reads every purpose, a link's unlisted one too.
|
|
17
|
+
*/
|
|
18
|
+
export const purposesOf = (asked, config = BUILT_IN_PURPOSE_CONFIG, project) => {
|
|
19
|
+
const words = (asked ?? []).map((one) => one.trim()).filter(Boolean);
|
|
20
|
+
if (words.includes("all")) {
|
|
21
|
+
if (words.length > 1)
|
|
22
|
+
throw new CliError("validation_error", "--purpose all reads every purpose; give it alone");
|
|
23
|
+
return "all";
|
|
24
|
+
}
|
|
25
|
+
if (words.length) {
|
|
26
|
+
unknown(words, config);
|
|
27
|
+
return new Set(words);
|
|
28
|
+
}
|
|
29
|
+
const own = (project?.key ? config.projects?.[project.key] : undefined) ??
|
|
30
|
+
(project?.ref ? config.projects?.[project.ref] : undefined);
|
|
31
|
+
return new Set(own ?? config.defaults);
|
|
32
|
+
};
|
|
33
|
+
export const purposeRead = (choice, purpose) => choice === "all" || choice.has(purpose ?? LINK_PURPOSE);
|
|
34
|
+
/** `a,b` and repeated flags as one list. */
|
|
35
|
+
export const purposeList = (value, previous = []) => [
|
|
36
|
+
...previous,
|
|
37
|
+
...value
|
|
38
|
+
.split(",")
|
|
39
|
+
.map((one) => one.trim())
|
|
40
|
+
.filter(Boolean),
|
|
41
|
+
];
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { Id } from "../../domain/models.js";
|
|
1
2
|
import type { AccountKey, MessageStore } from "../../store/store.js";
|
|
3
|
+
import { type PurposeConfig } from "../project-purposes.js";
|
|
4
|
+
import { type ChatCount, type ChatCounter } from "../stored-project-gathering.js";
|
|
2
5
|
import { STATUS_SIGNALS } from "./signals/index.js";
|
|
3
6
|
import type { StatusAttention, StatusChat, StatusItem, StatusProject, StatusSignal } from "./types.js";
|
|
4
7
|
export type * from "./types.js";
|
|
@@ -7,8 +10,6 @@ export { STATUS_SIGNALS };
|
|
|
7
10
|
export declare const STATUS_WINDOW = 300;
|
|
8
11
|
/** Linked chats read at most, as `projects evidence` reads. */
|
|
9
12
|
export declare const STATUS_CHATS = 16;
|
|
10
|
-
/** What `projects status` reads when no purpose is asked for (built in; memo's config may change it). */
|
|
11
|
-
export declare const DEFAULT_PURPOSES: readonly string[];
|
|
12
13
|
export interface ProjectStatusInput {
|
|
13
14
|
/** An id, key, name or alias, as `canonicalProjectRef` takes it. */
|
|
14
15
|
project: string;
|
|
@@ -18,12 +19,27 @@ export interface ProjectStatusInput {
|
|
|
18
19
|
after?: number;
|
|
19
20
|
asOf?: number;
|
|
20
21
|
contextQueries?: string[];
|
|
21
|
-
/** The chat purposes read; `all` reads every one.
|
|
22
|
+
/** The chat purposes read; `["all"]` reads every one. Left out, the config's defaults. */
|
|
22
23
|
purposes?: readonly string[];
|
|
24
|
+
/** The host's purposes; core's built-in ones when left out. */
|
|
25
|
+
purposeConfig?: PurposeConfig;
|
|
26
|
+
/** How many messages a chat left out by purpose holds in the window; the store's own count when left out. */
|
|
27
|
+
count?: ChatCounter;
|
|
23
28
|
/** Default `STATUS_SIGNALS`; tests pass their own. */
|
|
24
29
|
signals?: readonly StatusSignal[];
|
|
25
30
|
}
|
|
26
31
|
export type StatusChatSummary = Omit<StatusChat, "messages" | "account">;
|
|
32
|
+
/** A linked chat not read; one left out by its purpose says how much it holds in the window. */
|
|
33
|
+
export type OutOfScope = {
|
|
34
|
+
chat: string;
|
|
35
|
+
reason: "other-account" | "linked-chat-limit";
|
|
36
|
+
} | ({
|
|
37
|
+
chat: string;
|
|
38
|
+
reason: "purpose";
|
|
39
|
+
purpose: string;
|
|
40
|
+
account: AccountKey;
|
|
41
|
+
chatId: Id;
|
|
42
|
+
} & ChatCount);
|
|
27
43
|
export interface MoneyTotal {
|
|
28
44
|
direction: string;
|
|
29
45
|
currency: string | null;
|
|
@@ -48,11 +64,9 @@ export interface ProjectStatus {
|
|
|
48
64
|
money: {
|
|
49
65
|
open: MoneyTotal[];
|
|
50
66
|
};
|
|
51
|
-
outOfScope:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
purpose?: string;
|
|
55
|
-
}[];
|
|
67
|
+
outOfScope: OutOfScope[];
|
|
68
|
+
/** The purposes read: `all`, or the list. */
|
|
69
|
+
purposes: "all" | string[];
|
|
56
70
|
failed: {
|
|
57
71
|
signal: string;
|
|
58
72
|
why: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { formatReference } from "../../domain/references.js";
|
|
3
3
|
import { chatCoverage } from "../chat-coverage.js";
|
|
4
|
-
import {
|
|
4
|
+
import { LINK_PURPOSE, purposeRead, purposesOf } from "../project-purposes.js";
|
|
5
|
+
import { canonicalProjectRef, linkedChatsOf, storedChatCount, } from "../stored-project-gathering.js";
|
|
5
6
|
import { centsText } from "./signals/amounts.js";
|
|
6
7
|
import { STATUS_SIGNALS } from "./signals/index.js";
|
|
7
8
|
export { STATUS_SIGNALS };
|
|
@@ -12,8 +13,6 @@ export const STATUS_WINDOW = 300;
|
|
|
12
13
|
export const STATUS_CHATS = 16;
|
|
13
14
|
const CHILD_DEPTH = 2;
|
|
14
15
|
const CHILDREN = 8;
|
|
15
|
-
/** What `projects status` reads when no purpose is asked for (built in; memo's config may change it). */
|
|
16
|
-
export const DEFAULT_PURPOSES = ["work"];
|
|
17
16
|
const projectsCatalog = async (store) => new Map((await store.knowledge.vocabulary({ kind: "project" })).entries.map((entry) => [`project:${entry.id}`, entry]));
|
|
18
17
|
/** Confirmed `member-of` links from another project; depth two, eight at most, a project seen twice skipped. */
|
|
19
18
|
const childrenOf = async (store, root, catalog) => {
|
|
@@ -92,13 +91,14 @@ export const projectStatus = async (store, input) => {
|
|
|
92
91
|
if (!(since < now))
|
|
93
92
|
throw new CliError("validation_error", "the window must start before it ends");
|
|
94
93
|
const signals = input.signals ?? STATUS_SIGNALS;
|
|
95
|
-
const purposes = new Set(input.purposes ?? DEFAULT_PURPOSES);
|
|
96
94
|
const catalog = await projectsCatalog(store);
|
|
97
95
|
const ref = await canonicalProjectRef(store, input.project);
|
|
98
96
|
const entry = catalog.get(ref);
|
|
99
97
|
if (!entry)
|
|
100
98
|
throw new CliError("not_found", "the owner project is unavailable");
|
|
101
99
|
const root = { ref, key: entry.key, name: entry.name };
|
|
100
|
+
const purposes = purposesOf(input.purposes, input.purposeConfig, root);
|
|
101
|
+
const count = input.count ?? storedChatCount(store);
|
|
102
102
|
const { children, cut } = await childrenOf(store, root, catalog);
|
|
103
103
|
const outOfScope = [];
|
|
104
104
|
const linked = new Map();
|
|
@@ -110,10 +110,17 @@ export const projectStatus = async (store, input) => {
|
|
|
110
110
|
}
|
|
111
111
|
const chosen = [];
|
|
112
112
|
for (const [chat, link] of linked) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
if (!purposeRead(purposes, link.purpose)) {
|
|
114
|
+
const account = { provider: link.provider, account: link.account };
|
|
115
|
+
outOfScope.push({
|
|
116
|
+
chat,
|
|
117
|
+
reason: "purpose",
|
|
118
|
+
purpose: link.purpose ?? LINK_PURPOSE,
|
|
119
|
+
account,
|
|
120
|
+
chatId: link.chat,
|
|
121
|
+
...(await count(account, link.chat, { since, until: now })),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
117
124
|
else if (!inAccounts(input.accounts, link.provider, link.account))
|
|
118
125
|
outOfScope.push({ chat, reason: "other-account" });
|
|
119
126
|
else if (chosen.length >= STATUS_CHATS)
|
|
@@ -134,7 +141,7 @@ export const projectStatus = async (store, input) => {
|
|
|
134
141
|
title: coverage?.title ?? null,
|
|
135
142
|
kind: (await store.chats.chatKind(account, link.chat)) ?? "unknown",
|
|
136
143
|
role: link.role,
|
|
137
|
-
purpose:
|
|
144
|
+
purpose: link.purpose ?? LINK_PURPOSE,
|
|
138
145
|
project: link.project,
|
|
139
146
|
coverage: coverage,
|
|
140
147
|
messages: page.items.filter((message) => Date.parse(message.timestamp) <= now),
|
|
@@ -189,6 +196,7 @@ export const projectStatus = async (store, input) => {
|
|
|
189
196
|
items,
|
|
190
197
|
money: { open: totalsOf(items) },
|
|
191
198
|
outOfScope,
|
|
199
|
+
purposes: purposes === "all" ? "all" : [...purposes],
|
|
192
200
|
failed,
|
|
193
201
|
attention,
|
|
194
202
|
signals: signals.map(({ id, version }) => ({ id, version })),
|