@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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.554.0 — 02.11.2026
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`schedule agent` help no longer shows `skill show follow-ups` to a CLI without that skill.** The prompt example is
|
|
15
|
+
the follow-up review for a messenger and a read-only `search messages` summary for a CLI with `messenger: false`
|
|
16
|
+
(mail).
|
|
17
|
+
- **Mail sync stores `sent_at`.** It is the Date header, or the server's internal date when the header is missing or
|
|
18
|
+
unreadable; `received_at` is unchanged. Each sync also gives up to 5,000 emails stored without `sent_at` the internal
|
|
19
|
+
date (`MailStore.backfillSentAt`), so the `emails_by_time` index becomes useful and an account fills in about a day.
|
|
20
|
+
No schema change.
|
|
21
|
+
|
|
22
|
+
## 0.553.0 — 02.11.2026
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`projects evidence` and `projects status` read the linked chats of the purposes asked.** `--purpose sales,work`
|
|
27
|
+
or `--purpose all` picks them; without it, the configured defaults (built in: `work`). A chat left out is one
|
|
28
|
+
`missing` row with its purpose, message count, dates and the command that reads it; an unknown purpose is refused
|
|
29
|
+
with the configured list. `gatherStoredProject` and `projectStatus` take `purposes`, `purposeConfig` and a `count`
|
|
30
|
+
port (`storedChatCount`); `purposesOf`, `BUILT_IN_PURPOSE_CONFIG` and `Messenger.purposes` let a host pass its list.
|
|
31
|
+
When one linked chat with no purpose gives most of a run's items, the answer suggests a purpose once.
|
|
32
|
+
- **`projects evidence --include-proposed` reads chats and members proposed for the project**, after the confirmed
|
|
33
|
+
ones. Each linked chat and member row carries `proposed` and `score`, and each item only they brought `proposed`.
|
|
34
|
+
`gatherStoredProject` takes `includeProposed`; `linkedChatsOf` takes `{ includeProposed }`.
|
|
35
|
+
- **`--compact` rows for `projects evidence`**: items, linked chats, chats left out by purpose and members. A row
|
|
36
|
+
column may take a `prefix`, a shape may print fields `whole` (the cursor), and `time`/`day` columns read epoch ms.
|
|
37
|
+
- `countMessages` takes `until`.
|
|
38
|
+
|
|
39
|
+
### Changed — may break callers
|
|
40
|
+
|
|
41
|
+
- **`LinkedChat` gains `purpose` and `relationship`, `ProjectStatus` gains `purposes`, and a status chat left out by
|
|
42
|
+
purpose carries its account, chat id and counts.** `StoredProjectGathering` gains `leftOut`, `members`, `purposes`
|
|
43
|
+
and `purposeHint`. A caller that builds these by hand adds the fields. `DEFAULT_PURPOSES` moved to
|
|
44
|
+
`project-purposes`, still exported from `@wirecat/cli-core/services`.
|
|
45
|
+
|
|
10
46
|
## 0.552.0 — 02.11.2026
|
|
11
47
|
|
|
12
48
|
### Added
|
package/dist/cli/compact.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface RowColumn {
|
|
|
8
8
|
as?: RowValue;
|
|
9
9
|
/** Printed for a field that is `true`, and after a count: `{ field: ["sender_is_me", "sender_name"], tag: "me" }`. */
|
|
10
10
|
tag?: string;
|
|
11
|
+
/** Printed before a text value, so a bare id says what it is: `proposed 17079`. */
|
|
12
|
+
prefix?: string;
|
|
11
13
|
}
|
|
12
14
|
export interface RowShape {
|
|
13
15
|
/** Printed in order, joined by " · "; the first is the ref. A missing value is left out. */
|
|
@@ -16,6 +18,8 @@ export interface RowShape {
|
|
|
16
18
|
text?: readonly string[];
|
|
17
19
|
/** An item answer's lists, drawn as rows of another entity: `{ items: "evidence_item" }`. */
|
|
18
20
|
sections?: Readonly<Record<string, string>>;
|
|
21
|
+
/** An item answer's fields printed whole, never cut: a cursor that is pasted back. */
|
|
22
|
+
whole?: readonly string[];
|
|
19
23
|
}
|
|
20
24
|
export interface CompactOptions {
|
|
21
25
|
/** IANA zone for times; the answer's `meta.timezone` wins. */
|
package/dist/cli/compact.js
CHANGED
|
@@ -17,6 +17,61 @@ const messageRow = {
|
|
|
17
17
|
],
|
|
18
18
|
text: ["text", "transcript", "excerpt"],
|
|
19
19
|
};
|
|
20
|
+
const linkedChatColumns = [
|
|
21
|
+
{ field: "ref" },
|
|
22
|
+
{ field: "title" },
|
|
23
|
+
{ field: "purpose" },
|
|
24
|
+
{ field: "proposed", prefix: "proposed" },
|
|
25
|
+
];
|
|
26
|
+
/** `projects evidence`: its items, the chats it read and left out, and its members. */
|
|
27
|
+
const EVIDENCE_ROWS = {
|
|
28
|
+
project_evidence: {
|
|
29
|
+
columns: [],
|
|
30
|
+
sections: {
|
|
31
|
+
items: "evidence_item",
|
|
32
|
+
linked_chats: "linked_chat",
|
|
33
|
+
left_out: "linked_chat.left_out",
|
|
34
|
+
members: "evidence_member",
|
|
35
|
+
},
|
|
36
|
+
whole: ["checkpoint"],
|
|
37
|
+
},
|
|
38
|
+
evidence_item: {
|
|
39
|
+
columns: [
|
|
40
|
+
{ field: "ref" },
|
|
41
|
+
{ field: "at", as: "day" },
|
|
42
|
+
{ field: "context" },
|
|
43
|
+
{ field: "proposed", prefix: "proposed" },
|
|
44
|
+
],
|
|
45
|
+
text: ["text"],
|
|
46
|
+
},
|
|
47
|
+
linked_chat: {
|
|
48
|
+
columns: [
|
|
49
|
+
...linkedChatColumns,
|
|
50
|
+
{ field: "state" },
|
|
51
|
+
{ field: "items", as: "count", tag: "items" },
|
|
52
|
+
{ field: "capped", tag: "capped" },
|
|
53
|
+
{ field: "score", prefix: "score" },
|
|
54
|
+
{ field: "sync_command" },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
"linked_chat.left_out": {
|
|
58
|
+
columns: [
|
|
59
|
+
...linkedChatColumns,
|
|
60
|
+
{ field: "messages", as: "count", tag: "messages" },
|
|
61
|
+
{ field: "first_at", as: "day" },
|
|
62
|
+
{ field: "last_at", as: "day" },
|
|
63
|
+
{ field: "read_command" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
evidence_member: {
|
|
67
|
+
columns: [
|
|
68
|
+
{ field: "person" },
|
|
69
|
+
{ field: "state" },
|
|
70
|
+
{ field: "proposed", prefix: "proposed" },
|
|
71
|
+
{ field: "score", prefix: "score" },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
20
75
|
/** Rows for cli-core's shared entities. */
|
|
21
76
|
export const SHARED_ROWS = {
|
|
22
77
|
message: messageRow,
|
|
@@ -41,6 +96,7 @@ export const SHARED_ROWS = {
|
|
|
41
96
|
],
|
|
42
97
|
},
|
|
43
98
|
...STATUS_ROWS,
|
|
99
|
+
...EVIDENCE_ROWS,
|
|
44
100
|
};
|
|
45
101
|
/** Any record: what names it, what it is called, what state it is in, then what it says. */
|
|
46
102
|
export const GENERIC_ROW = {
|
|
@@ -136,11 +192,13 @@ const cellOf = (item, column, clock) => {
|
|
|
136
192
|
switch (column.as) {
|
|
137
193
|
case "time":
|
|
138
194
|
case "day": {
|
|
139
|
-
|
|
195
|
+
// Some answers keep their times as epoch milliseconds.
|
|
196
|
+
const iso = typeof value === "number" && Number.isFinite(value) ? new Date(value).toISOString() : value;
|
|
197
|
+
if (typeof iso !== "string")
|
|
140
198
|
return undefined;
|
|
141
|
-
const parts = clock.parts(
|
|
199
|
+
const parts = clock.parts(iso);
|
|
142
200
|
if (!parts)
|
|
143
|
-
return { text: clean(
|
|
201
|
+
return { text: clean(iso) };
|
|
144
202
|
const day = parts.date.slice(5);
|
|
145
203
|
return { text: column.as === "day" ? day : `${day} ${parts.time}`, date: parts.date };
|
|
146
204
|
}
|
|
@@ -164,7 +222,7 @@ const cellOf = (item, column, clock) => {
|
|
|
164
222
|
if (value === true)
|
|
165
223
|
return column.tag ? { text: column.tag } : undefined;
|
|
166
224
|
if (typeof value === "string" || typeof value === "number")
|
|
167
|
-
return { text: clean(String(value)) };
|
|
225
|
+
return { text: clean(column.prefix ? `${column.prefix} ${value}` : String(value)) };
|
|
168
226
|
return undefined;
|
|
169
227
|
}
|
|
170
228
|
};
|
|
@@ -233,7 +291,7 @@ const rowsOfItems = (items, shape, clock, chars) => {
|
|
|
233
291
|
};
|
|
234
292
|
const blockLine = (fields, chars) => blockOf(fields, chars).join(" ");
|
|
235
293
|
/** A record as `field: value` lines; a nested record is indented under its field, a list of records counted. */
|
|
236
|
-
const blockOf = (fields, chars, indent = "") => Object.entries(fields)
|
|
294
|
+
const blockOf = (fields, chars, indent = "", whole = []) => Object.entries(fields)
|
|
237
295
|
.filter(([name]) => !(name === "priority" && "priority_label" in fields))
|
|
238
296
|
.filter(([, value]) => present(value) || value === false)
|
|
239
297
|
.flatMap(([name, value]) => {
|
|
@@ -244,7 +302,7 @@ const blockOf = (fields, chars, indent = "") => Object.entries(fields)
|
|
|
244
302
|
return value.every((one) => !isFields(one) && !Array.isArray(one))
|
|
245
303
|
? [`${label} ${cut(clean(value.join(", ")), chars)}`]
|
|
246
304
|
: [`${label} ${value.length} — --json shows them`];
|
|
247
|
-
return [`${label} ${cut(clean(String(value)), chars)}`];
|
|
305
|
+
return [`${label} ${whole.includes(name) ? clean(String(value)) : cut(clean(String(value)), chars)}`];
|
|
248
306
|
});
|
|
249
307
|
const footerOf = (body) => {
|
|
250
308
|
const lines = [];
|
|
@@ -300,7 +358,7 @@ export const compactText = (body, shape, options, rowsOf = rowsOfEntity()) => {
|
|
|
300
358
|
else if (isFields(body.item)) {
|
|
301
359
|
const sections = shape.sections ?? {};
|
|
302
360
|
const rest = Object.fromEntries(Object.entries(body.item).filter(([name]) => !Object.hasOwn(sections, name)));
|
|
303
|
-
lines = blockOf(rest, chars).map(guarded);
|
|
361
|
+
lines = blockOf(rest, chars, "", shape.whole).map(guarded);
|
|
304
362
|
for (const [name, entity] of Object.entries(sections)) {
|
|
305
363
|
const list = body.item[name];
|
|
306
364
|
if (!Array.isArray(list))
|
|
@@ -6,6 +6,7 @@ import { type Asker, type SendGuard } from "../../sends/guard.js";
|
|
|
6
6
|
import { type PaceRate } from "../../sends/pace.js";
|
|
7
7
|
import { type PermissionKey } from "../../sends/permissions.js";
|
|
8
8
|
import { type ServiceMessenger, type Services } from "../../services/index.js";
|
|
9
|
+
import type { PurposeConfig } from "../../services/project-purposes.js";
|
|
9
10
|
import type { OpenRecognizer } from "../../speech/transcribe.js";
|
|
10
11
|
import { type AccountKey, type DeletionScope, type MessageStore } from "../../store/store.js";
|
|
11
12
|
import { type BaseContext, type BaseEnvironment } from "../context.js";
|
|
@@ -76,6 +77,8 @@ export interface Messenger extends ServiceMessenger {
|
|
|
76
77
|
profileState?: (profile: string, env: NodeJS.ProcessEnv) => ProfileState;
|
|
77
78
|
/** The help for a `<chat>` argument, in this messenger's words. */
|
|
78
79
|
chatArgument: string;
|
|
80
|
+
/** The chat purposes `projects evidence` and `projects status` know; core's built-in ones when unset. */
|
|
81
|
+
purposes?: PurposeConfig;
|
|
79
82
|
/** The group settings this messenger has, as `chats update` offers them; every one when unset. */
|
|
80
83
|
groupSettings?: readonly GroupSettingKey[];
|
|
81
84
|
/** Whether `polls create` makes quizzes (`--quiz --correct --solution`); no when unset. */
|
|
@@ -3,6 +3,7 @@ import { annotate, annotateOption } from "@wirecat/cli-base/commands";
|
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { parseDuration } from "../../domain/duration.js";
|
|
5
5
|
import { FETCHING } from "../../services/archive.js";
|
|
6
|
+
import { BUILT_IN_PURPOSES, purposeList } from "../../services/project-purposes.js";
|
|
6
7
|
import { projectStatus, STATUS_SIGNALS } from "../../services/project-status/index.js";
|
|
7
8
|
import { gatherStoredProject } from "../../services/stored-project-gathering.js";
|
|
8
9
|
import { answerEntity } from "../answer-entity.js";
|
|
@@ -61,6 +62,8 @@ const archiveFetcher = (withServices, messenger) => {
|
|
|
61
62
|
},
|
|
62
63
|
};
|
|
63
64
|
};
|
|
65
|
+
const purposeHelp = (messenger) => `the linked chats' purposes to read, comma separated (${(messenger.purposes?.purposes ?? BUILT_IN_PURPOSES).join(", ")}), ` +
|
|
66
|
+
"or all; the configured defaults if not given, and a chat left out is counted, never dropped silently";
|
|
64
67
|
/** Off MCP: a tool call ends at about a minute, too soon for a sync, which would fail the whole answer. */
|
|
65
68
|
const cliOnly = (flags, description, value) => {
|
|
66
69
|
const option = new Option(flags, description);
|
|
@@ -88,6 +91,8 @@ export const projectsCommand = (messenger) => {
|
|
|
88
91
|
.option("--alias <name>", "another name of the project, searched on its own; repeat for more", collect, [])
|
|
89
92
|
.option("--context-query <words>", "more words that all appear in one text; repeat for another", collect, [])
|
|
90
93
|
.option("--contact <person>", "a name to search, or person:<id> to read what that person wrote", collect, [])
|
|
94
|
+
.option("--purpose <purposes>", purposeHelp(messenger), purposeList, [])
|
|
95
|
+
.option("--include-proposed", "also read chats and members proposed for the project and not yet confirmed; each item they bring is marked")
|
|
91
96
|
.option("--since-time <time>", "how far back: days (90d, at most 730d) or a start date (2026-09-01)", "60d")
|
|
92
97
|
.option("--before-time <time>", "fixed exclusive end: ISO 8601 timestamp with UTC offset")
|
|
93
98
|
.option("--limit <n>", "new references at most, up to 500; 100 if not given", positiveCount("--limit"))
|
|
@@ -116,20 +121,31 @@ export const projectsCommand = (messenger) => {
|
|
|
116
121
|
...(options.contact.length ? { people: options.contact } : {}),
|
|
117
122
|
...(options.maxPages === undefined ? {} : { maxReads: options.maxPages }),
|
|
118
123
|
...(options.cursor === undefined ? {} : { checkpoint: options.cursor }),
|
|
124
|
+
...(options.purpose.length ? { purposes: options.purpose } : {}),
|
|
125
|
+
...(messenger.purposes ? { purposeConfig: messenger.purposes } : {}),
|
|
126
|
+
...(options.includeProposed ? { includeProposed: true } : {}),
|
|
119
127
|
...(fetcher ? { fetcher, maxFetchMs, onFetchProgress: progressNote(context.renderer.note) } : {}),
|
|
120
128
|
}));
|
|
121
129
|
const fetcher = options.syncFirst
|
|
122
130
|
? archiveFetcher((work) => context.withServices(work, { name: "projects evidence fetch" }), messenger)
|
|
123
131
|
: undefined;
|
|
124
|
-
|
|
125
|
-
const
|
|
132
|
+
let profile;
|
|
133
|
+
const found = await context.withStore((store, account) => {
|
|
134
|
+
profile = account;
|
|
135
|
+
return gather(store, account, fetcher);
|
|
136
|
+
});
|
|
137
|
+
const root = rootOf(this);
|
|
126
138
|
const hints = {
|
|
127
139
|
sync: (one, since) => one.provider === messenger.provider
|
|
128
140
|
? lineOf(this, `projects evidence ${quote(project)} --since-time ${since} --sync-first`)
|
|
129
141
|
: null,
|
|
142
|
+
read: (one, since) => one.provider === profile?.provider && one.account === profile.account
|
|
143
|
+
? lineOf(this, wordsOf(root, readStep(one.chat, since)))
|
|
144
|
+
: null,
|
|
130
145
|
showDocument: "the notes app that imported it",
|
|
131
146
|
};
|
|
132
|
-
|
|
147
|
+
const parts = projectAnswerParts(options.sinceTime, found, hints);
|
|
148
|
+
renderAnswer(this, context, itemOf(snakeKeys(withSyncCommands(found, hints.sync, hints.read)), parts), () => projectEvidenceText(found, hints));
|
|
133
149
|
}));
|
|
134
150
|
projects.addCommand(statusCommand(messenger));
|
|
135
151
|
return projects;
|
|
@@ -149,26 +165,37 @@ const statusCommand = (messenger) => answerEntity(annotate(new Command("status")
|
|
|
149
165
|
.argument("<project>", "an existing project's id, key, exact name or alias")
|
|
150
166
|
.option("--since-time <time>", "how far back: days (60d, at most 730d) or a start date (2026-09-01)", "60d")
|
|
151
167
|
.option("--before-time <time>", "fixed exclusive end: ISO 8601 timestamp with UTC offset")
|
|
168
|
+
.option("--purpose <purposes>", purposeHelp(messenger), purposeList, [])
|
|
152
169
|
.option("--context-query <words>", "words that all appear in one text; reports their last mention per side. Repeat for another", collect, [])
|
|
153
170
|
.action(async function (project) {
|
|
154
171
|
const options = this.opts();
|
|
155
172
|
const context = messengerContext(this, messenger);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
let profile;
|
|
174
|
+
const found = await context.withStore((store, account) => {
|
|
175
|
+
profile = account;
|
|
176
|
+
return projectStatus(store, {
|
|
177
|
+
project,
|
|
178
|
+
accounts: [account],
|
|
179
|
+
...projectWindowOf(options.sinceTime),
|
|
180
|
+
...(options.beforeTime === undefined ? {} : { asOf: projectCutoffTime(options.beforeTime) }),
|
|
181
|
+
contextQueries: options.contextQuery,
|
|
182
|
+
...(options.purpose.length ? { purposes: options.purpose } : {}),
|
|
183
|
+
...(messenger.purposes ? { purposeConfig: messenger.purposes } : {}),
|
|
184
|
+
});
|
|
185
|
+
});
|
|
163
186
|
const root = rootOf(this);
|
|
164
187
|
const hints = {
|
|
165
188
|
sync: (chat, since) => nextStep("messages.sync", { args: { chat: chat.chatId, since_time: since } }),
|
|
166
|
-
read: (chat, since) =>
|
|
189
|
+
read: (chat, since) => readStep(chat.chatId, since),
|
|
190
|
+
readChat: (chat, since) => chat.account.provider === profile?.provider && chat.account.account === profile.account
|
|
191
|
+
? readStep(chat.chatId, since)
|
|
192
|
+
: null,
|
|
167
193
|
line: (step) => (step.kind === "command" ? lineOf(this, wordsOf(root, step)) : step.why),
|
|
168
194
|
};
|
|
169
195
|
const { item, ...parts } = projectStatusAnswer(found, hints);
|
|
170
196
|
renderAnswer(this, context, itemOf(item, parts), () => projectStatusText(found, hints));
|
|
171
197
|
}), "project_status", projectStatusSchema, { row: projectStatusRow(STATUS_SIGNALS) });
|
|
198
|
+
const readStep = (chat, since) => nextStep("messages.list", { args: { chat, query: `date>=${since}` } });
|
|
172
199
|
/** A step's words after the tool and the profile, so `lineOf` names the profile the run used. */
|
|
173
200
|
const wordsOf = (root, step) => {
|
|
174
201
|
const profile = root.getOptionValue("profile");
|
|
@@ -41,6 +41,7 @@ export declare const serveRelay: (command: Command, messenger: Messenger) => {
|
|
|
41
41
|
resolveSettings: (flags: import("../settings.js").GlobalFlags, options?: import("../settings.js").ResolveOptions) => import("../settings.js").Settings;
|
|
42
42
|
profileState?: (profile: string, env: NodeJS.ProcessEnv) => import("./context.js").ProfileState;
|
|
43
43
|
chatArgument: string;
|
|
44
|
+
purposes?: import("../../services/project-purposes.js").PurposeConfig;
|
|
44
45
|
groupSettings?: readonly import("../../domain/models.js").GroupSettingKey[];
|
|
45
46
|
pollQuiz?: boolean;
|
|
46
47
|
pollCloseSeconds?: readonly [min: number, max: number];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StoredProjectGathering } from "../services/stored-project-gathering.js";
|
|
1
|
+
import type { LeftOutChat, StoredProjectGathering } from "../services/stored-project-gathering.js";
|
|
2
2
|
/** Days back (`90d`) or a start date (`2026-09-01`); either way the run freezes its end, so a resume keeps it. */
|
|
3
3
|
export declare const projectWindowOf: (value: string) => {
|
|
4
4
|
days: number;
|
|
@@ -10,7 +10,7 @@ export declare const projectCutoffTime: (value: string) => number;
|
|
|
10
10
|
* The zone a typed start date was read in, and the window the run used, in UTC. `complete` is "the answer is whole":
|
|
11
11
|
* reads finished and every linked chat held in full; the answer's own `complete` stays "reads finished".
|
|
12
12
|
*/
|
|
13
|
-
export declare const projectAnswerParts: (sinceTime: string, found: StoredProjectGathering) => {
|
|
13
|
+
export declare const projectAnswerParts: (sinceTime: string, found: StoredProjectGathering, hints?: Pick<ProjectTextHints, "read">) => {
|
|
14
14
|
next?: ({
|
|
15
15
|
kind: "command";
|
|
16
16
|
step: string;
|
|
@@ -45,7 +45,7 @@ export declare const projectAnswerParts: (sinceTime: string, found: StoredProjec
|
|
|
45
45
|
* The gathering with each linked chat's `syncCommand`: the line that syncs a chat held in part back to the window
|
|
46
46
|
* start, or null when it needs none or no command here can. JSON readers get what the text says per chat.
|
|
47
47
|
*/
|
|
48
|
-
export declare const withSyncCommands: (found: StoredProjectGathering, sync: ProjectTextHints["sync"]) => {
|
|
48
|
+
export declare const withSyncCommands: (found: StoredProjectGathering, sync: ProjectTextHints["sync"], read?: ProjectTextHints["read"]) => {
|
|
49
49
|
schemaVersion: 1;
|
|
50
50
|
runKey: string;
|
|
51
51
|
fingerprint: string;
|
|
@@ -80,7 +80,6 @@ export declare const withSyncCommands: (found: StoredProjectGathering, sync: Pro
|
|
|
80
80
|
pages: number;
|
|
81
81
|
issue: string | null;
|
|
82
82
|
}[];
|
|
83
|
-
items: import("../services/project-gathering.js").ProjectEvidenceItem[];
|
|
84
83
|
people: import("../services/project-gathering.js").ProjectEvidencePerson[];
|
|
85
84
|
peopleOmitted: number;
|
|
86
85
|
oversized: {
|
|
@@ -110,6 +109,9 @@ export declare const withSyncCommands: (found: StoredProjectGathering, sync: Pro
|
|
|
110
109
|
complete: boolean;
|
|
111
110
|
completeness: "complete" | "partial" | "unknown";
|
|
112
111
|
checkpoint: string | null;
|
|
112
|
+
items: (import("../services/project-gathering.js").ProjectEvidenceItem & {
|
|
113
|
+
proposed?: string;
|
|
114
|
+
})[];
|
|
113
115
|
projectRef: string | null;
|
|
114
116
|
unreadSources: {
|
|
115
117
|
source: string;
|
|
@@ -117,6 +119,14 @@ export declare const withSyncCommands: (found: StoredProjectGathering, sync: Pro
|
|
|
117
119
|
reason?: "out-of-scope" | "linked-chat-limit" | "member-limit" | "no-identity" | "unknown-provider";
|
|
118
120
|
}[];
|
|
119
121
|
mentions: import("../services/project-gathering-mentions.js").EvidenceMentions;
|
|
122
|
+
purposes: "all" | string[];
|
|
123
|
+
members: import("../services/stored-project-gathering.js").ProjectMember[];
|
|
124
|
+
purposeHint: {
|
|
125
|
+
chat: string;
|
|
126
|
+
relationship: string;
|
|
127
|
+
items: number;
|
|
128
|
+
of: number;
|
|
129
|
+
} | null;
|
|
120
130
|
linkedChats: {
|
|
121
131
|
provider: string;
|
|
122
132
|
account: string;
|
|
@@ -134,13 +144,32 @@ export declare const withSyncCommands: (found: StoredProjectGathering, sync: Pro
|
|
|
134
144
|
} | null;
|
|
135
145
|
items?: number | undefined;
|
|
136
146
|
capped?: boolean | undefined;
|
|
147
|
+
purpose: string;
|
|
148
|
+
proposed?: string;
|
|
149
|
+
score?: number;
|
|
137
150
|
syncCommand: string | null;
|
|
138
151
|
}[];
|
|
152
|
+
leftOut: {
|
|
153
|
+
ref: string;
|
|
154
|
+
provider: string;
|
|
155
|
+
account: string;
|
|
156
|
+
chat: string;
|
|
157
|
+
title: string | null;
|
|
158
|
+
purpose: string;
|
|
159
|
+
relationship: string | null;
|
|
160
|
+
proposed?: string;
|
|
161
|
+
messages: number;
|
|
162
|
+
firstAt: string | null;
|
|
163
|
+
lastAt: string | null;
|
|
164
|
+
readCommand: string | null;
|
|
165
|
+
}[];
|
|
139
166
|
};
|
|
140
167
|
export interface ProjectTextHints {
|
|
141
168
|
/** The command that syncs one linked chat back to `since` (`YYYY-MM-DD`), or null when no command here can. */
|
|
142
169
|
sync: (chat: StoredProjectGathering["linkedChats"][number], since: string) => string | null;
|
|
143
170
|
/** How to open a document too large to read here. */
|
|
144
171
|
showDocument: string;
|
|
172
|
+
/** The command that reads a chat left out by its purpose from `since`, or null when no command here can. */
|
|
173
|
+
read?: (chat: LeftOutChat, since: string) => string | null;
|
|
145
174
|
}
|
|
146
175
|
export declare const projectEvidenceText: (found: StoredProjectGathering, hints: ProjectTextHints) => string;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { MAX_NEXT_STEPS } from "@wirecat/cli-base/standards";
|
|
2
3
|
import * as v from "valibot";
|
|
3
4
|
import { dateWindow, momentOf } from "../services/moment.js";
|
|
4
5
|
import { coverageGaps, heldPhrase } from "./messenger/search-answers.js";
|
|
6
|
+
import { heldInWindow } from "./project-status.js";
|
|
5
7
|
/** Days back (`90d`) or a start date (`2026-09-01`); either way the run freezes its end, so a resume keeps it. */
|
|
6
8
|
export const projectWindowOf = (value) => {
|
|
7
9
|
const days = /^([1-9]\d{0,2})d$/.exec(value)?.[1];
|
|
@@ -33,10 +35,24 @@ const coverageRows = ({ linkedChats }) => linkedChats.flatMap((one) => one.compl
|
|
|
33
35
|
* The zone a typed start date was read in, and the window the run used, in UTC. `complete` is "the answer is whole":
|
|
34
36
|
* reads finished and every linked chat held in full; the answer's own `complete` stays "reads finished".
|
|
35
37
|
*/
|
|
36
|
-
export const projectAnswerParts = (sinceTime, found) => {
|
|
38
|
+
export const projectAnswerParts = (sinceTime, found, hints) => {
|
|
37
39
|
const { window } = found;
|
|
38
40
|
const { timezone } = dateWindow({ sinceTime }).meta;
|
|
39
|
-
const
|
|
41
|
+
const gaps = coverageGaps(coverageRows(found), 0, new Date(window.after).toISOString());
|
|
42
|
+
const next = [...gaps.next];
|
|
43
|
+
const missing = [
|
|
44
|
+
...gaps.missing,
|
|
45
|
+
...found.leftOut.map((one) => {
|
|
46
|
+
const read = hints?.read?.(one, day(window.after)) ?? null;
|
|
47
|
+
return { field: "linked_chats", why: leftOutLine(one, read), ...(read ? { step: "messages.list" } : {}) };
|
|
48
|
+
}),
|
|
49
|
+
];
|
|
50
|
+
if (found.purposeHint) {
|
|
51
|
+
const why = purposeHintLine(found.purposeHint);
|
|
52
|
+
missing.push({ field: "purpose", why });
|
|
53
|
+
if (next.length < MAX_NEXT_STEPS)
|
|
54
|
+
next.push({ kind: "user_action", step: "relationships.purpose", why, safe_to_auto_execute: false });
|
|
55
|
+
}
|
|
40
56
|
return {
|
|
41
57
|
...(next.length ? { next } : {}),
|
|
42
58
|
...(missing.length ? { missing } : {}),
|
|
@@ -51,19 +67,27 @@ export const projectAnswerParts = (sinceTime, found) => {
|
|
|
51
67
|
* The gathering with each linked chat's `syncCommand`: the line that syncs a chat held in part back to the window
|
|
52
68
|
* start, or null when it needs none or no command here can. JSON readers get what the text says per chat.
|
|
53
69
|
*/
|
|
54
|
-
export const withSyncCommands = (found, sync) => ({
|
|
70
|
+
export const withSyncCommands = (found, sync, read) => ({
|
|
55
71
|
...found,
|
|
56
72
|
linkedChats: found.linkedChats.map((one) => ({
|
|
57
73
|
...one,
|
|
58
74
|
syncCommand: one.completeness === null || one.completeness.state === "complete" ? null : sync(one, day(found.window.after)),
|
|
59
75
|
})),
|
|
76
|
+
leftOut: found.leftOut.map((one) => ({ ...one, readCommand: read?.(one, day(found.window.after)) ?? null })),
|
|
60
77
|
});
|
|
78
|
+
/** One line for a chat left out by its purpose: what it holds in the window and how to read it. */
|
|
79
|
+
const leftOutLine = (one, read) => `${one.title ? `«${one.title}» (${one.ref})` : one.ref}, purpose ${one.purpose}` +
|
|
80
|
+
`${one.proposed ? `, proposed ${one.proposed}` : ""}, not read: ${heldInWindow(one)}` +
|
|
81
|
+
(read ? `; read it: ${read}` : `; add --purpose ${one.purpose} to read it here`);
|
|
82
|
+
const purposeHintLine = (hint) => `${hint.chat} gave ${hint.items} of ${hint.of} items by membership alone; if it is not a work chat, ask the owner ` +
|
|
83
|
+
`to set its purpose: memo relationships update ${hint.relationship} --purpose <purpose>`;
|
|
61
84
|
const LABELS = {
|
|
62
85
|
"person-only": " [person only]",
|
|
63
86
|
sender: " [sent by contact]",
|
|
64
87
|
"linked-chat": " [linked chat]",
|
|
65
88
|
};
|
|
66
89
|
const day = (at) => new Date(at).toISOString().slice(0, 10);
|
|
90
|
+
const PROPOSED = " [proposed, not confirmed]";
|
|
67
91
|
const FETCH_STATES = {
|
|
68
92
|
complete: "synced",
|
|
69
93
|
partial: "synced in part",
|
|
@@ -84,7 +108,7 @@ export const projectEvidenceText = (found, hints) => {
|
|
|
84
108
|
`${day(found.window.after)} to ${day(found.window.before)}, ${found.scope.sources.length} sources`,
|
|
85
109
|
`as of: ${new Date(found.window.asOf).toISOString()} (exclusive end; reuse --before-time to repeat this window)`,
|
|
86
110
|
`searched: ${[...lanes.values()].join("; ")}`,
|
|
87
|
-
...found.items.map((item) => `${day(item.at)} ${item.ref}${LABELS[item.context] ?? ""}\n ${item.text}` +
|
|
111
|
+
...found.items.map((item) => `${day(item.at)} ${item.ref}${LABELS[item.context] ?? ""}${item.proposed ? PROPOSED : ""}\n ${item.text}` +
|
|
88
112
|
(item.around ?? [])
|
|
89
113
|
.map((one) => `\n ${one.found ? "(hit) " : ""}${one.sender ? `${one.sender}: ` : ""}${one.text}`)
|
|
90
114
|
.join("")),
|
|
@@ -138,6 +162,16 @@ export const projectEvidenceText = (found, hints) => {
|
|
|
138
162
|
lines.push(`${one.title ?? one.ref}: ${held}${one.state === "pending" ? ", not read this run" : ""}` +
|
|
139
163
|
(sync ? ` — sync it: ${sync}` : ""));
|
|
140
164
|
}
|
|
165
|
+
const proposed = [
|
|
166
|
+
...found.linkedChats.flatMap((one) => (one.proposed ? [`${one.title ?? one.ref} (${one.proposed})`] : [])),
|
|
167
|
+
...found.members.flatMap((one) => (one.proposed ? [`${one.person} (${one.proposed})`] : [])),
|
|
168
|
+
];
|
|
169
|
+
if (proposed.length)
|
|
170
|
+
lines.push(`read as proposed, not confirmed: ${proposed.join(", ")}`);
|
|
171
|
+
for (const one of found.leftOut)
|
|
172
|
+
lines.push(`left out: ${leftOutLine(one, hints.read?.(one, day(found.window.after)) ?? null)}`);
|
|
173
|
+
if (found.purposeHint)
|
|
174
|
+
lines.push(purposeHintLine(found.purposeHint));
|
|
141
175
|
const capped = found.linkedChats.filter((one) => one.capped);
|
|
142
176
|
if (capped.length)
|
|
143
177
|
lines.push(`linked chats with more to read, left for --cursor so one chat does not fill the run: ${capped
|
|
@@ -97,6 +97,16 @@ export const STATUS_ROWS = {
|
|
|
97
97
|
],
|
|
98
98
|
},
|
|
99
99
|
"status.signal": { columns: [{ field: "id" }, { field: "version" }] },
|
|
100
|
-
"status.out-of-scope": {
|
|
100
|
+
"status.out-of-scope": {
|
|
101
|
+
columns: [
|
|
102
|
+
{ field: "chat" },
|
|
103
|
+
{ field: "reason" },
|
|
104
|
+
{ field: "purpose" },
|
|
105
|
+
{ field: "messages", as: "count", tag: "messages" },
|
|
106
|
+
{ field: "first_at", as: "day" },
|
|
107
|
+
{ field: "last_at", as: "day" },
|
|
108
|
+
{ field: "read_command" },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
101
111
|
"status.failed": { columns: [{ field: "signal" }], text: ["why"] },
|
|
102
112
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { NextStep } from "@wirecat/cli-base/standards";
|
|
2
2
|
import * as v from "valibot";
|
|
3
|
+
import type { Id } from "../domain/models.js";
|
|
3
4
|
import type { ProjectStatus, StatusChatSummary } from "../services/project-status/index.js";
|
|
5
|
+
import type { AccountKey } from "../store/store.js";
|
|
4
6
|
import type { RowShape } from "./compact.js";
|
|
5
7
|
export interface ProjectStatusHints {
|
|
6
8
|
/** The step that syncs a chat held in part back to `since` (`YYYY-MM-DD`); null when no command here can. */
|
|
@@ -9,6 +11,12 @@ export interface ProjectStatusHints {
|
|
|
9
11
|
read: (chat: StatusChatSummary, since: string) => NextStep | null;
|
|
10
12
|
/** The command line a step runs, for the text form. */
|
|
11
13
|
line: (step: NextStep) => string;
|
|
14
|
+
/** The step that reads a chat left out by its purpose from `since`; null when no command here can. */
|
|
15
|
+
readChat?: (chat: {
|
|
16
|
+
ref: string;
|
|
17
|
+
account: AccountKey;
|
|
18
|
+
chatId: Id;
|
|
19
|
+
}, since: string) => NextStep | null;
|
|
12
20
|
}
|
|
13
21
|
/** The `project_status` answer drawn by `--compact`: each signal's list with that signal's row. */
|
|
14
22
|
export declare const projectStatusRow: (signals: readonly {
|
|
@@ -44,10 +52,15 @@ export declare const projectStatusSchema: v.LooseObjectSchema<{
|
|
|
44
52
|
readonly attention: v.NumberSchema<undefined>;
|
|
45
53
|
readonly why: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
46
54
|
}, undefined>, undefined>;
|
|
55
|
+
readonly purposes: v.UnionSchema<[v.LiteralSchema<"all", undefined>, v.ArraySchema<v.StringSchema<undefined>, undefined>], undefined>;
|
|
47
56
|
readonly out_of_scope: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
48
57
|
readonly chat: v.StringSchema<undefined>;
|
|
49
58
|
readonly reason: v.StringSchema<undefined>;
|
|
50
59
|
readonly purpose: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
60
|
+
readonly messages: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
61
|
+
readonly first_at: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
62
|
+
readonly last_at: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
63
|
+
readonly read_command: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
51
64
|
}, undefined>, undefined>, undefined>;
|
|
52
65
|
readonly failed: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
53
66
|
readonly signal: v.StringSchema<undefined>;
|
|
@@ -55,6 +68,12 @@ export declare const projectStatusSchema: v.LooseObjectSchema<{
|
|
|
55
68
|
}, undefined>, undefined>, undefined>;
|
|
56
69
|
}, undefined>;
|
|
57
70
|
export type ProjectStatusItem = v.InferOutput<typeof projectStatusSchema>;
|
|
71
|
+
/** `12 messages 2026-10-03…2026-11-01`, or that the window holds none. */
|
|
72
|
+
export declare const heldInWindow: (one: {
|
|
73
|
+
messages: number;
|
|
74
|
+
firstAt: string | null;
|
|
75
|
+
lastAt: string | null;
|
|
76
|
+
}) => string;
|
|
58
77
|
/** The answer of `projects status`: the item, two steps at most, what is missing, and the window. */
|
|
59
78
|
export declare const projectStatusAnswer: (found: ProjectStatus, hints: ProjectStatusHints) => {
|
|
60
79
|
item: {
|
|
@@ -86,10 +105,15 @@ export declare const projectStatusAnswer: (found: ProjectStatus, hints: ProjectS
|
|
|
86
105
|
attention: number;
|
|
87
106
|
why: string | null;
|
|
88
107
|
}[];
|
|
108
|
+
purposes: "all" | string[];
|
|
89
109
|
out_of_scope?: {
|
|
90
110
|
chat: string;
|
|
91
111
|
reason: string;
|
|
92
112
|
purpose?: string | undefined;
|
|
113
|
+
messages?: number | undefined;
|
|
114
|
+
first_at?: string | null | undefined;
|
|
115
|
+
last_at?: string | null | undefined;
|
|
116
|
+
read_command?: string | null | undefined;
|
|
93
117
|
}[] | undefined;
|
|
94
118
|
failed?: {
|
|
95
119
|
signal: string;
|