@wirecat/cli-core 0.383.0 → 0.385.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 +24 -0
- package/dist/cli/bot/moderation.js +4 -3
- package/dist/cli/bot/store.js +5 -4
- package/dist/cli/list-query.d.ts +11 -1
- package/dist/cli/list-query.js +11 -1
- package/dist/cli/meeting-batch.js +2 -1
- package/dist/cli/messenger/admin-moderation-command.js +4 -3
- package/dist/cli/messenger/archive-commands.js +5 -3
- package/dist/cli/messenger/backfill-command.js +9 -4
- package/dist/cli/messenger/catch-up-answers.d.ts +5 -2
- package/dist/cli/messenger/catch-up-answers.js +11 -6
- package/dist/cli/messenger/chat-lists.js +11 -6
- package/dist/cli/messenger/chats-stats-command.js +7 -7
- package/dist/cli/messenger/contacts-command.js +4 -3
- package/dist/cli/messenger/conversation-answers.d.ts +2 -2
- package/dist/cli/messenger/conversation-answers.js +5 -2
- package/dist/cli/messenger/conversations-command.js +7 -3
- package/dist/cli/messenger/inbox.js +2 -2
- package/dist/cli/messenger/replies-command.js +4 -3
- package/dist/cli/messenger/review.js +2 -1
- package/dist/cli/messenger/stats-answers.d.ts +1 -1
- package/dist/cli/messenger/stats-answers.js +4 -3
- package/dist/cli/runs/search.d.ts +7 -0
- package/dist/cli/runs/search.js +11 -3
- package/dist/meetings/answers.d.ts +4 -2
- package/dist/meetings/answers.js +2 -1
- package/dist/meetings/cli/index.d.ts +1 -1
- package/dist/meetings/cli/index.js +1 -1
- package/dist/meetings/cli/search.d.ts +5 -0
- package/dist/meetings/cli/search.js +23 -5
- package/dist/meetings/mcp/search.d.ts +3 -1
- package/dist/meetings/mcp/search.js +5 -4
- package/dist/services/admin-statistics.d.ts +3 -0
- package/dist/services/admin-statistics.js +10 -7
- package/dist/services/retention.d.ts +7 -0
- package/dist/services/retention.js +10 -5
- package/dist/store/migrations.d.ts +2 -1
- package/dist/store/migrations.js +12 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.385.0 — 22.10.2026
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **A store the pre-rename `cli-messaging` wrote is refused again, and left untouched.** Its `schema_migrations`
|
|
15
|
+
uses its own, higher numbers ("29, needs at least 28"); since store version 28 the version check took it for a
|
|
16
|
+
compatible file, opened it and failed on a missing column with `internal_error`, and a later version would have
|
|
17
|
+
migrated it. A file that has a version but no `meetings` table, which version 1 here always creates, now fails with
|
|
18
|
+
`configuration_error` (exit 3) before anything runs. `migrate` takes `ownTable: null` for synthetic test schemas.
|
|
19
|
+
|
|
20
|
+
## 0.384.0 — 22.10.2026
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **More commands say how they read a typed date** (WC-TIME-3, WC-TIME-5): `meta.window` gives the UTC bounds they
|
|
25
|
+
used, and `meta.timezone` names the zone a value without its own zone was read in. Now: `moderate` (the bot's and
|
|
26
|
+
the admin's), the bot's `store fetch`, `store export`, `messages sync`, `stats chats show`, `contacts context`,
|
|
27
|
+
`search conversations`, `replies test` (its 7-day default window too), `runs search`, `stats chats retention`, the
|
|
28
|
+
administrator reports (`stats messages unanswered`, `stats contacts responses`, `stats chats newcomers`,
|
|
29
|
+
`stats messages discussion`), `search meetings` (also in `meetings batch`; over MCP only the window, since it takes
|
|
30
|
+
epoch times), and a `time:` or `date:` term in `chats events`, `chats members history`, `inbox` and `review`.
|
|
31
|
+
`search conversations` now reads a date alone in its `--timezone` when one is given. A host of `search meetings`
|
|
32
|
+
passes `meetingSearchMeta(options)` to `meetingHitsList` to report the same.
|
|
33
|
+
|
|
10
34
|
## 0.383.0 — 22.10.2026
|
|
11
35
|
|
|
12
36
|
### Added
|
|
@@ -7,7 +7,7 @@ import { defaultRules, ModerationRules, moderationPathFor, RULE_KEYS } from "../
|
|
|
7
7
|
import { moderateWith } from "../../moderation/run.js";
|
|
8
8
|
import { guardedWrite } from "../../sends/guarded.js";
|
|
9
9
|
import { newOperationId } from "../../sends/send-id.js";
|
|
10
|
-
import {
|
|
10
|
+
import { dateWindow } from "../../services/moment.js";
|
|
11
11
|
import { environmentOf } from "../context.js";
|
|
12
12
|
import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, ownerOnlyOption, REMOTE_WRITE } from "../effects.js";
|
|
13
13
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
@@ -155,6 +155,7 @@ export const botModerateCommand = (bot) => annotate(new Command("moderate"), { e
|
|
|
155
155
|
// An in-process caller's answer counts under --json too: it is not a terminal that could wait for ever.
|
|
156
156
|
const given = environmentOf(this).answer;
|
|
157
157
|
const ask = async (question) => (given ? await given(question) : await answerOf(this, question));
|
|
158
|
+
const window = dateWindow(options.sinceTime === undefined ? {} : { sinceTime: options.sinceTime });
|
|
158
159
|
await context.run(async (events) => {
|
|
159
160
|
const adapter = await context.authenticated({ events });
|
|
160
161
|
const botId = await botIdOf(context, adapter);
|
|
@@ -169,7 +170,7 @@ export const botModerateCommand = (bot) => annotate(new Command("moderate"), { e
|
|
|
169
170
|
commandFor: (group, finding) => finding.action === "delete"
|
|
170
171
|
? `${context.words} messages delete ${group} ${finding.messageId} --allow-dangerous`
|
|
171
172
|
: `${context.words} chats members remove ${group} ${finding.personId}`,
|
|
172
|
-
...(
|
|
173
|
+
...(window.since === undefined ? {} : { since: window.since }),
|
|
173
174
|
dryRun: options.dryRun === true,
|
|
174
175
|
allowDangerous: options.allowDangerous === true,
|
|
175
176
|
maxActions,
|
|
@@ -188,7 +189,7 @@ export const botModerateCommand = (bot) => annotate(new Command("moderate"), { e
|
|
|
188
189
|
...(row.reason ? { why: row.reason } : {}),
|
|
189
190
|
})));
|
|
190
191
|
else
|
|
191
|
-
renderAnswer(this, context, itemOf({ chat_id: chatId, rows: snakeKeys(rows) }));
|
|
192
|
+
renderAnswer(this, context, itemOf({ chat_id: chatId, rows: snakeKeys(rows) }, { meta: window.meta }));
|
|
192
193
|
for (const note of notes)
|
|
193
194
|
context.renderer.note(note);
|
|
194
195
|
});
|
package/dist/cli/bot/store.js
CHANGED
|
@@ -3,9 +3,9 @@ import { annotate } from "@wirecat/cli-base/commands";
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { parseDuration } from "../../domain/duration.js";
|
|
5
5
|
import { fetchInto } from "../../services/archive.js";
|
|
6
|
-
import {
|
|
6
|
+
import { dateWindow } from "../../services/moment.js";
|
|
7
7
|
import { FETCH } from "../effects.js";
|
|
8
|
-
import {
|
|
8
|
+
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
9
9
|
import { stopOnSignal } from "../messenger/patience.js";
|
|
10
10
|
import { botContext, online } from "./context.js";
|
|
11
11
|
import { botCan, botIdOf } from "./messages.js";
|
|
@@ -37,7 +37,8 @@ export const botStoreCommand = (bot, fetching) => {
|
|
|
37
37
|
throw new CliError("validation_error", "give --since-time or --last, not both: how far back the fetch goes");
|
|
38
38
|
}
|
|
39
39
|
const pauseMs = parseDuration(options.pause, "--pause");
|
|
40
|
-
const
|
|
40
|
+
const bounds = dateWindow(options.sinceTime === undefined ? {} : { sinceTime: options.sinceTime });
|
|
41
|
+
const sinceMs = bounds.since;
|
|
41
42
|
const context = online(botContext(this, bot), this);
|
|
42
43
|
const ref = context.chatRef(chat);
|
|
43
44
|
const stop = stopOnSignal(this);
|
|
@@ -73,7 +74,7 @@ export const botStoreCommand = (bot, fetching) => {
|
|
|
73
74
|
stop: stop.signal,
|
|
74
75
|
onPage: () => { },
|
|
75
76
|
}));
|
|
76
|
-
|
|
77
|
+
renderAnswer(this, context, itemOf(snakeKeys(result), { meta: bounds.meta }));
|
|
77
78
|
}
|
|
78
79
|
finally {
|
|
79
80
|
await adapter.close();
|
package/dist/cli/list-query.d.ts
CHANGED
|
@@ -44,10 +44,20 @@ export declare const nativeTerms: (ast: QueryAst | undefined, command: string) =
|
|
|
44
44
|
* as validation already accepts them. A range or wildcard is refused rather than read as a value.
|
|
45
45
|
*/
|
|
46
46
|
export declare const queryTerms: (words: QueryWords, registry: QueryRegistry, command: string) => Map<string, string>;
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* A `time` term as epoch ms bounds: `time>=7d`, `time:[2026-10-01 TO 2026-10-07]`, `time:today`, and the `meta`
|
|
49
|
+
* an answer reports them by: the zone calendar words and bare dates were read in, and the bounds in UTC.
|
|
50
|
+
*/
|
|
48
51
|
export declare const timeBounds: (term: Predicate | undefined, zone?: string) => {
|
|
49
52
|
since?: number;
|
|
50
53
|
until?: number;
|
|
54
|
+
meta?: {
|
|
55
|
+
timezone: string;
|
|
56
|
+
window: {
|
|
57
|
+
since?: string;
|
|
58
|
+
until?: string;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
51
61
|
};
|
|
52
62
|
/** A text term as the plain words a provider searches by: `name:ana`, `name:"Ana K"`, `name:*ana*`. */
|
|
53
63
|
export declare const searchText: (term: Predicate | undefined) => string | undefined;
|
package/dist/cli/list-query.js
CHANGED
|
@@ -130,7 +130,10 @@ export const queryTerms = (words, registry, command) => {
|
|
|
130
130
|
}
|
|
131
131
|
return terms;
|
|
132
132
|
};
|
|
133
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* A `time` term as epoch ms bounds: `time>=7d`, `time:[2026-10-01 TO 2026-10-07]`, `time:today`, and the `meta`
|
|
135
|
+
* an answer reports them by: the zone calendar words and bare dates were read in, and the bounds in UTC.
|
|
136
|
+
*/
|
|
134
137
|
export const timeBounds = (term, zone = timezoneOf()) => {
|
|
135
138
|
if (!term)
|
|
136
139
|
return {};
|
|
@@ -139,6 +142,13 @@ export const timeBounds = (term, zone = timezoneOf()) => {
|
|
|
139
142
|
return {
|
|
140
143
|
...(range.lower === undefined ? {} : { since: range.lower }),
|
|
141
144
|
...(range.upper === undefined ? {} : { until: range.upper }),
|
|
145
|
+
meta: {
|
|
146
|
+
timezone: zone,
|
|
147
|
+
window: {
|
|
148
|
+
...(range.lower === undefined ? {} : { since: new Date(range.lower).toISOString() }),
|
|
149
|
+
...(range.upper === undefined ? {} : { until: new Date(range.upper).toISOString() }),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
142
152
|
};
|
|
143
153
|
};
|
|
144
154
|
/** A text term as the plain words a provider searches by: `name:ana`, `name:"Ana K"`, `name:*ana*`. */
|
|
@@ -2,7 +2,7 @@ import { BATCH_LIMITS, CliError, prepareBatch, runBatch, } from "@wirecat/cli-ba
|
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { failureContext } from "../errors/reasons.js";
|
|
5
|
-
import { createMeetingPassageCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingPassageRequest, meetingSearchArguments, } from "../meetings/cli/index.js";
|
|
5
|
+
import { createMeetingPassageCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingPassageRequest, meetingSearchArguments, meetingSearchMeta, } from "../meetings/cli/index.js";
|
|
6
6
|
import { meetingHitsList, parseMeetingReference, } from "../meetings/index.js";
|
|
7
7
|
import { formatMeetingEvidenceText, formatMeetingPassage } from "../services/meeting-passage-format.js";
|
|
8
8
|
import { itemOf, listOf } from "./envelope.js";
|
|
@@ -48,6 +48,7 @@ const handlers = (reader, meta) => {
|
|
|
48
48
|
...meetingHitsList(found, request.limit ?? 100, "search meetings", meetingSearchArguments(query, flags)),
|
|
49
49
|
meta: {
|
|
50
50
|
...meta(),
|
|
51
|
+
...meetingSearchMeta(flags),
|
|
51
52
|
complete,
|
|
52
53
|
...(!complete ? { issues: [{ code: "invalid_response", source: "meeting-index" }] } : {}),
|
|
53
54
|
},
|
|
@@ -3,7 +3,7 @@ import { annotate } from "@wirecat/cli-base/commands";
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { describe, MAX_ACTIONS } from "../../moderation/check.js";
|
|
5
5
|
import { RULE_KEYS } from "../../moderation/rules.js";
|
|
6
|
-
import {
|
|
6
|
+
import { dateWindow } from "../../services/moment.js";
|
|
7
7
|
import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, REMOTE_WRITE } from "../effects.js";
|
|
8
8
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
9
9
|
import { answerOf } from "./ask.js";
|
|
@@ -57,8 +57,9 @@ export const moderateCommand = (messenger) => annotate(new Command("moderate"),
|
|
|
57
57
|
if (!Number.isInteger(maxActions) || maxActions < 0) {
|
|
58
58
|
throw new CliError("validation_error", `--max-actions takes a whole number — got ${String(options.maxActions)}`);
|
|
59
59
|
}
|
|
60
|
+
const window = dateWindow(options.sinceTime === undefined ? {} : { sinceTime: options.sinceTime });
|
|
60
61
|
const { chatId, rows, notes } = await context.withServices((services) => services.moderation.moderate(chat, {
|
|
61
|
-
...(
|
|
62
|
+
...(window.since === undefined ? {} : { since: window.since }),
|
|
62
63
|
dryRun: options.dryRun === true,
|
|
63
64
|
allowDangerous: options.allowDangerous === true,
|
|
64
65
|
maxActions,
|
|
@@ -69,7 +70,7 @@ export const moderateCommand = (messenger) => annotate(new Command("moderate"),
|
|
|
69
70
|
return /^\s*y(es)?\s*$/i.test(answer);
|
|
70
71
|
},
|
|
71
72
|
}));
|
|
72
|
-
renderAnswer(this, context, itemOf(snakeKeys({ chatId, rows })), () => rows
|
|
73
|
+
renderAnswer(this, context, itemOf(snakeKeys({ chatId, rows }), { meta: window.meta }), () => rows
|
|
73
74
|
.map((row) => [row.kind, row.personName ?? row.personId, row.rule, row.action, row.outcome, row.reason ?? ""]
|
|
74
75
|
.join(" ")
|
|
75
76
|
.trimEnd())
|
|
@@ -9,7 +9,7 @@ import { renderMessages } from "../../render/messages.js";
|
|
|
9
9
|
import { seal } from "../../sealed.js";
|
|
10
10
|
import { CHAT_KINDS } from "../../services/chats.js";
|
|
11
11
|
import { kindsOf } from "../../services/inbox.js";
|
|
12
|
-
import {
|
|
12
|
+
import { dateWindow } from "../../services/moment.js";
|
|
13
13
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
14
14
|
import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
15
15
|
import { jobsCommand } from "./backfill-command.js";
|
|
@@ -185,7 +185,8 @@ const exportCommand = (messenger) => destroysNothing(annotate(new Command("expor
|
|
|
185
185
|
}
|
|
186
186
|
const password = encrypt ? await passwordOf(this, { twice: true }) : undefined;
|
|
187
187
|
const chat = chats[0];
|
|
188
|
-
const
|
|
188
|
+
const window = dateWindow(since === undefined ? {} : { sinceTime: since });
|
|
189
|
+
const from = window.since === undefined ? undefined : new Date(window.since).toISOString();
|
|
189
190
|
const { account, title, messages } = await context.withServices((services) => services.archive.export(chat, from === undefined ? {} : { since: from }));
|
|
190
191
|
if (output !== undefined) {
|
|
191
192
|
const path = resolve(output);
|
|
@@ -210,7 +211,7 @@ const exportCommand = (messenger) => destroysNothing(annotate(new Command("expor
|
|
|
210
211
|
format: format ?? "jsonl",
|
|
211
212
|
count: messages.length,
|
|
212
213
|
...(password === undefined ? {} : { encrypted: true }),
|
|
213
|
-
}));
|
|
214
|
+
}, { meta: window.meta }));
|
|
214
215
|
return;
|
|
215
216
|
}
|
|
216
217
|
if (format === "markdown")
|
|
@@ -223,6 +224,7 @@ const exportCommand = (messenger) => destroysNothing(annotate(new Command("expor
|
|
|
223
224
|
items: messageItems(messages, account),
|
|
224
225
|
limit: Math.max(1, messages.length),
|
|
225
226
|
has_more: false,
|
|
227
|
+
meta: window.meta,
|
|
226
228
|
}));
|
|
227
229
|
else
|
|
228
230
|
context.streams.data(renderMessages(messages, {
|
|
@@ -9,7 +9,7 @@ import { isCliFailure } from "../../errors/failures.js";
|
|
|
9
9
|
import { envName } from "../../ports/app.js";
|
|
10
10
|
import { FETCHING } from "../../services/archive.js";
|
|
11
11
|
import { JOBS_QUERY, jobItems } from "../../services/job-query.js";
|
|
12
|
-
import {
|
|
12
|
+
import { dateWindow } from "../../services/moment.js";
|
|
13
13
|
import { validateCatchUpBounds } from "../../services/search-catchup.js";
|
|
14
14
|
import { applyTaskRulesSinceLastPass } from "../../services/task-rules.js";
|
|
15
15
|
import { environmentOf } from "../context.js";
|
|
@@ -72,8 +72,9 @@ export const syncCommand = (messenger) => {
|
|
|
72
72
|
}
|
|
73
73
|
const limit = sizes.limit ?? fetching.maxPages * fetching.page;
|
|
74
74
|
const pauseMs = parseDuration(pause, "--pause");
|
|
75
|
-
const
|
|
76
|
-
const
|
|
75
|
+
const from = since ?? (all && last === undefined ? ALL_SINCE : undefined);
|
|
76
|
+
const window = dateWindow(from === undefined ? {} : { sinceTime: from });
|
|
77
|
+
const sinceMs = window.since;
|
|
77
78
|
const context = messengerContext(this, messenger);
|
|
78
79
|
const prepare = catchUp ?? context.settings.searchCatchUp ?? false;
|
|
79
80
|
const preparation = {
|
|
@@ -123,7 +124,10 @@ export const syncCommand = (messenger) => {
|
|
|
123
124
|
pageSize,
|
|
124
125
|
...(last === undefined ? {} : { last }),
|
|
125
126
|
});
|
|
126
|
-
renderAnswer(this, context, itemOf(snakeKeys(started), {
|
|
127
|
+
renderAnswer(this, context, itemOf(snakeKeys(started), {
|
|
128
|
+
next: [nextStep("store.job", { args: { job: started.job } })],
|
|
129
|
+
meta: window.meta,
|
|
130
|
+
}));
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
129
133
|
const jobs = jobsDir(messenger.app, context.env);
|
|
@@ -164,6 +168,7 @@ export const syncCommand = (messenger) => {
|
|
|
164
168
|
: []),
|
|
165
169
|
...(toReview > 0 ? [nextStep("mentions.review", { args: { new: true }, values: { shown } })] : []),
|
|
166
170
|
],
|
|
171
|
+
meta: window.meta,
|
|
167
172
|
}));
|
|
168
173
|
if (tasks?.held)
|
|
169
174
|
context.renderer.note(`the first sync left ${tasks.held} possible task(s) in ${tasks.heldChats ?? 0} chat(s) unopened ` +
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Meta } from "@wirecat/cli-base/standards";
|
|
1
2
|
import type { Inbox, Review } from "../../domain/models.js";
|
|
2
3
|
import { type Hearing } from "../../speech/hearing.js";
|
|
3
4
|
import type { AccountKey } from "../../store/store.js";
|
|
@@ -10,8 +11,9 @@ export declare const REVIEW_QUERY: import("@wirecat/cli-base/query").QueryRegist
|
|
|
10
11
|
export declare const catchUpQuery: (words: QueryWords, registry: typeof INBOX_QUERY, command: string) => {
|
|
11
12
|
since?: number;
|
|
12
13
|
chat?: string;
|
|
14
|
+
meta?: Partial<Meta>;
|
|
13
15
|
};
|
|
14
|
-
export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, reader: AccountKey, transcribe: boolean) => import("../envelope.js").ListBody<{
|
|
16
|
+
export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, reader: AccountKey, transcribe: boolean, meta?: Partial<Meta>) => import("../envelope.js").ListBody<{
|
|
15
17
|
ref: string;
|
|
16
18
|
chat_id: string;
|
|
17
19
|
chat_title: string | null | undefined;
|
|
@@ -51,10 +53,11 @@ export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, r
|
|
|
51
53
|
* A review the next one goes on from: from `until` when nothing was left out, else from the same start again.
|
|
52
54
|
* Saved points (`--new`) carry this themselves, and an `--unanswered` look is not a review to continue.
|
|
53
55
|
*/
|
|
54
|
-
export declare const reviewAnswer: (found: Review, hearing: Hearing | undefined, reader: AccountKey, { transcribe, fresh, chat }: {
|
|
56
|
+
export declare const reviewAnswer: (found: Review, hearing: Hearing | undefined, reader: AccountKey, { transcribe, fresh, chat, meta }: {
|
|
55
57
|
transcribe: boolean;
|
|
56
58
|
fresh: boolean;
|
|
57
59
|
chat?: string;
|
|
60
|
+
meta?: Partial<Meta>;
|
|
58
61
|
}) => import("../envelope.js").ListBody<{
|
|
59
62
|
ref: string;
|
|
60
63
|
chat_id: string;
|
|
@@ -17,13 +17,17 @@ export const REVIEW_QUERY = defineQuery({
|
|
|
17
17
|
export const catchUpQuery = (words, registry, command) => {
|
|
18
18
|
const terms = nativeTerms(listQuery(typedQuery(words), registry, command), command);
|
|
19
19
|
const term = terms.get("date");
|
|
20
|
-
const { since, until } = timeBounds(term);
|
|
20
|
+
const { since, until, meta } = timeBounds(term);
|
|
21
21
|
if (term && (until !== undefined || since === undefined))
|
|
22
22
|
queryError("query.unsupported-operator", term.span, `${command} reads from a point on: date>2h, not an end`, {
|
|
23
23
|
command,
|
|
24
24
|
});
|
|
25
25
|
const chat = terms.get("chat")?.value;
|
|
26
|
-
return {
|
|
26
|
+
return {
|
|
27
|
+
...(since === undefined ? {} : { since }),
|
|
28
|
+
...(chat === undefined ? {} : { chat }),
|
|
29
|
+
...(meta === undefined ? {} : { meta }),
|
|
30
|
+
};
|
|
27
31
|
};
|
|
28
32
|
const summaryOf = (message, reader) => {
|
|
29
33
|
const { ref, chat_id, chat_title, timestamp, sender_name, outgoing, text, transcript, attachments } = messageItem(message, reader);
|
|
@@ -44,7 +48,7 @@ const nameOf = (chat) => chat.title ?? chat.id;
|
|
|
44
48
|
* What every catch-up answer shares: the messages, oldest first, each naming its chat; what was left out as
|
|
45
49
|
* `missing`; and a chat cut short as a `messages.list` step, after the steps the command puts first.
|
|
46
50
|
*/
|
|
47
|
-
const catchUpList = (command, found, hearing, reader, { transcribe, first = [] }) => {
|
|
51
|
+
const catchUpList = (command, found, hearing, reader, { transcribe, first = [], meta = {} }) => {
|
|
48
52
|
const read = found.chats
|
|
49
53
|
.flatMap((chat) => heard(chat.messages, hearing).map((message) => ({ ...message, chatTitle: chat.title })))
|
|
50
54
|
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
@@ -73,16 +77,16 @@ const catchUpList = (command, found, hearing, reader, { transcribe, first = [] }
|
|
|
73
77
|
has_more: false,
|
|
74
78
|
next: [...first, ...cut.map((chat) => nextStep("messages.list", { args: { chat: chat.id } }))],
|
|
75
79
|
missing,
|
|
76
|
-
...(found.partial ? {
|
|
80
|
+
meta: { ...meta, ...(found.partial ? { truncated: true } : {}) },
|
|
77
81
|
});
|
|
78
82
|
};
|
|
79
|
-
export const inboxAnswer = (inbox, hearing, reader, transcribe) => catchUpList("inbox", inbox, hearing, reader, { transcribe });
|
|
83
|
+
export const inboxAnswer = (inbox, hearing, reader, transcribe, meta = {}) => catchUpList("inbox", inbox, hearing, reader, { transcribe, meta });
|
|
80
84
|
const quoted = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
81
85
|
/**
|
|
82
86
|
* A review the next one goes on from: from `until` when nothing was left out, else from the same start again.
|
|
83
87
|
* Saved points (`--new`) carry this themselves, and an `--unanswered` look is not a review to continue.
|
|
84
88
|
*/
|
|
85
|
-
export const reviewAnswer = (found, hearing, reader, { transcribe, fresh, chat }) => {
|
|
89
|
+
export const reviewAnswer = (found, hearing, reader, { transcribe, fresh, chat, meta }) => {
|
|
86
90
|
const continues = !fresh && found.unanswered === undefined;
|
|
87
91
|
const query = [
|
|
88
92
|
`date>${quoted(found.complete ? found.until : found.since)}`,
|
|
@@ -91,5 +95,6 @@ export const reviewAnswer = (found, hearing, reader, { transcribe, fresh, chat }
|
|
|
91
95
|
return catchUpList("review", found, hearing, reader, {
|
|
92
96
|
transcribe,
|
|
93
97
|
first: continues ? [nextStep("review.next", { args: { query: query.join(" ") } })] : [],
|
|
98
|
+
...(meta === undefined ? {} : { meta }),
|
|
94
99
|
});
|
|
95
100
|
};
|
|
@@ -18,9 +18,11 @@ export const chatsList = async (services, query, paging) => {
|
|
|
18
18
|
};
|
|
19
19
|
export const chatEvents = async (services, chat, query) => {
|
|
20
20
|
const terms = nativeTerms(listQuery(typedQuery(query), EVENTS_QUERY, "chats events"), "chats events");
|
|
21
|
-
const { since } = timeBounds(terms.get("time"));
|
|
21
|
+
const { since, meta } = timeBounds(terms.get("time"));
|
|
22
22
|
const found = await services.chats.events(chat, since === undefined ? {} : { since });
|
|
23
|
-
return wholeList("chats events", found.events.map((one) => chatEventAnswer(found.chatId, one)),
|
|
23
|
+
return wholeList("chats events", found.events.map((one) => chatEventAnswer(found.chatId, one)), {
|
|
24
|
+
meta: { ...meta, ...(found.more ? { truncated: true } : {}) },
|
|
25
|
+
});
|
|
24
26
|
};
|
|
25
27
|
export const membersList = async (services, chat, paging) => {
|
|
26
28
|
const { limit, page, all } = paging;
|
|
@@ -33,11 +35,14 @@ export const membersList = async (services, chat, paging) => {
|
|
|
33
35
|
};
|
|
34
36
|
export const membersHistory = async (services, chat, query) => {
|
|
35
37
|
const terms = nativeTerms(listQuery(typedQuery(query), HISTORY_QUERY, "chats members history"), "chats members history");
|
|
36
|
-
const { since } = timeBounds(terms.get("time"));
|
|
38
|
+
const { since, meta } = timeBounds(terms.get("time"));
|
|
37
39
|
const found = await services.chats.memberHistory(chat, since === undefined ? {} : { since });
|
|
38
|
-
return wholeList("chats members history", found.events.map((one) => memberEventAnswer(found.chatId, one)),
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
return wholeList("chats members history", found.events.map((one) => memberEventAnswer(found.chatId, one)), {
|
|
41
|
+
...(found.events.length === 0
|
|
42
|
+
? { next: [nextStep("chats.members-fetch", { args: { chat: found.chatId, track: true } })] }
|
|
43
|
+
: {}),
|
|
44
|
+
...(meta ? { meta } : {}),
|
|
45
|
+
});
|
|
41
46
|
};
|
|
42
47
|
export const membersAudit = async (services, chat, query, options) => {
|
|
43
48
|
const minScore = scoreFloor(listQuery(typedQuery(query), AUDIT_QUERY, "chats members audit"));
|
|
@@ -2,7 +2,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { EVENTS_DAYS } from "../../services/chats.js";
|
|
5
|
-
import {
|
|
5
|
+
import { dateWindow } from "../../services/moment.js";
|
|
6
6
|
import { REMOTE_READ } from "../effects.js";
|
|
7
7
|
import { renderAnswer } from "../envelope.js";
|
|
8
8
|
import { messengerContext } from "./context.js";
|
|
@@ -19,13 +19,13 @@ export const statsCommand = (messenger) => annotate(new Command("show"), { effec
|
|
|
19
19
|
const context = messengerContext(this, messenger);
|
|
20
20
|
const { sinceTime, by, timezone } = this.opts();
|
|
21
21
|
refuseJsonl(context.format);
|
|
22
|
+
const window = dateWindow({
|
|
23
|
+
...(sinceTime === undefined ? {} : { sinceTime }),
|
|
24
|
+
...(timezone === undefined ? {} : { zone: timezone }),
|
|
25
|
+
});
|
|
22
26
|
const { stats, account } = await context.withServices(async (services) => ({
|
|
23
27
|
stats: await services.chats.stats(chat, {
|
|
24
|
-
...(
|
|
25
|
-
? {}
|
|
26
|
-
: {
|
|
27
|
-
since: momentOf(sinceTime, "--since-time", Date.now(), timezone === undefined ? {} : { zone: timezone }),
|
|
28
|
-
}),
|
|
28
|
+
...(window.since === undefined ? {} : { since: window.since }),
|
|
29
29
|
...(by ? { by } : {}),
|
|
30
30
|
...(timezone === undefined ? {} : { timezone }),
|
|
31
31
|
}),
|
|
@@ -37,7 +37,7 @@ export const statsCommand = (messenger) => annotate(new Command("show"), { effec
|
|
|
37
37
|
context.renderer.note("the messenger stopped reading joins and leaves before the period's start — those are lower bounds");
|
|
38
38
|
if (!stats.members && !stats.membersUnrecorded)
|
|
39
39
|
context.renderer.note("joins and leaves were not asked of the messenger");
|
|
40
|
-
renderAnswer(this, context, chatStatsAnswer(stats, account), () => view(stats));
|
|
40
|
+
renderAnswer(this, context, chatStatsAnswer(stats, account, window.meta), () => view(stats));
|
|
41
41
|
});
|
|
42
42
|
const view = (stats) => {
|
|
43
43
|
const { questions, members } = stats;
|
|
@@ -4,7 +4,7 @@ import { Command } from "commander";
|
|
|
4
4
|
import { REGISTRIES, registriesCover } from "../../botcheck/registries.js";
|
|
5
5
|
import { levelFor } from "../../sends/permissions.js";
|
|
6
6
|
import { phoneOf } from "../../services/index.js";
|
|
7
|
-
import { dateWindow
|
|
7
|
+
import { dateWindow } from "../../services/moment.js";
|
|
8
8
|
import { maskedAccount } from "../../services/people.js";
|
|
9
9
|
import { CHAT_MESSAGES, CONTEXT_BYTES, CONTEXT_MESSAGES } from "../../services/person-context.js";
|
|
10
10
|
import { SCOPES, TIMELINE_ITEMS } from "../../services/person-timeline.js";
|
|
@@ -119,10 +119,11 @@ export const contactsCommand = (messenger) => {
|
|
|
119
119
|
}));
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
|
+
const window = dateWindow(sinceTime === undefined ? {} : { sinceTime });
|
|
122
123
|
const found = await context.withServices((services) => services.people.context(person, {
|
|
123
124
|
messages: limit ?? CONTEXT_MESSAGES,
|
|
124
125
|
bytes: CONTEXT_BYTES,
|
|
125
|
-
...(
|
|
126
|
+
...(window.since === undefined ? {} : { since: window.since }),
|
|
126
127
|
}));
|
|
127
128
|
const { hasMore, ...rest } = found;
|
|
128
129
|
renderAnswer(this, context, itemOf(snakeKeys(rest), {
|
|
@@ -131,7 +132,7 @@ export const contactsCommand = (messenger) => {
|
|
|
131
132
|
.filter((chat) => chat.provider === messenger.provider)
|
|
132
133
|
.map((chat) => nextStep("messages.sync", { args: { chat: chat.chatId } }))
|
|
133
134
|
: [],
|
|
134
|
-
...(hasMore ? {
|
|
135
|
+
meta: { ...window.meta, ...(hasMore ? { truncated: true } : {}) },
|
|
135
136
|
}));
|
|
136
137
|
if (hasMore)
|
|
137
138
|
context.renderer.note("cut at --limit; a larger one shows more");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Missing, NextStep } from "@wirecat/cli-base/standards";
|
|
1
|
+
import type { Meta, Missing, NextStep } from "@wirecat/cli-base/standards";
|
|
2
2
|
import type { Message } from "../../domain/models.js";
|
|
3
3
|
import { type MessengerTraits } from "../../ports/traits.js";
|
|
4
4
|
import type { ChatReadiness, FoundConversations, Refreshed } from "../../services/embeddings.js";
|
|
@@ -11,7 +11,7 @@ export declare const readinessGaps: ({ meaning, model, readiness, refreshed, cov
|
|
|
11
11
|
missing: Missing[];
|
|
12
12
|
};
|
|
13
13
|
/** `search conversations` and `conversations related`, CLI and MCP alike. */
|
|
14
|
-
export declare const foundConversations: (command: string, found: Readiness & Pick<FoundConversations, "hits" | "accounts">, limit: number, messenger: Syncing, chat?: string) => import("../envelope.js").ListBody<unknown>;
|
|
14
|
+
export declare const foundConversations: (command: string, found: Readiness & Pick<FoundConversations, "hits" | "accounts">, limit: number, messenger: Syncing, chat?: string, meta?: Partial<Meta>) => import("../envelope.js").ListBody<unknown>;
|
|
15
15
|
/** `conversations status`: each built chat with the model it was measured by; group chats never built are missing. */
|
|
16
16
|
export declare const readinessList: ({ model, chats, unbuiltGroups, }: {
|
|
17
17
|
model: string;
|
|
@@ -53,12 +53,15 @@ export const readinessGaps = ({ meaning, model, readiness, refreshed, coverage }
|
|
|
53
53
|
return { next, missing };
|
|
54
54
|
};
|
|
55
55
|
/** `search conversations` and `conversations related`, CLI and MCP alike. */
|
|
56
|
-
export const foundConversations = (command, found, limit, messenger, chat) => listOf(command, {
|
|
56
|
+
export const foundConversations = (command, found, limit, messenger, chat, meta = {}) => listOf(command, {
|
|
57
57
|
items: found.hits.map((hit) => snakeKeys(hit)),
|
|
58
58
|
limit,
|
|
59
59
|
has_more: false,
|
|
60
60
|
...readinessGaps(found, messenger, chat),
|
|
61
|
-
|
|
61
|
+
meta: {
|
|
62
|
+
...meta,
|
|
63
|
+
...(found.accounts ? { accounts: found.accounts.map((one) => `${one.provider}:${one.account}`) } : {}),
|
|
64
|
+
},
|
|
62
65
|
});
|
|
63
66
|
/** `conversations status`: each built chat with the model it was measured by; group chats never built are missing. */
|
|
64
67
|
export const readinessList = ({ model, chats, unbuiltGroups, }) => listOf("conversations status", {
|
|
@@ -9,7 +9,7 @@ import { renderMessages } from "../../render/messages.js";
|
|
|
9
9
|
import { BATCH_SIZE, CONVERSATION_QUERY } from "../../services/conversations.js";
|
|
10
10
|
import { REFRESH_BOUNDS, } from "../../services/embeddings.js";
|
|
11
11
|
import { servicesFor, storedDeps } from "../../services/index.js";
|
|
12
|
-
import {
|
|
12
|
+
import { dateWindow } from "../../services/moment.js";
|
|
13
13
|
import { analysisChoice } from "../analysis-choice.js";
|
|
14
14
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "../effects.js";
|
|
15
15
|
import { embeddingChoice, refuseRemoteQuery } from "../embedding-choice.js";
|
|
@@ -531,6 +531,10 @@ export const conversationsSearchCommand = (messenger) => syncOptions(boundOption
|
|
|
531
531
|
refuseLocalWrite(context, messenger.app.command, EMBED_KEY);
|
|
532
532
|
}
|
|
533
533
|
const { limit } = context.settings;
|
|
534
|
+
const window = dateWindow({
|
|
535
|
+
...(since === undefined ? {} : { sinceTime: since }),
|
|
536
|
+
...(options.timezone === undefined ? {} : { zone: options.timezone }),
|
|
537
|
+
});
|
|
534
538
|
const { found, refreshed } = await context.withServices(async (services) => {
|
|
535
539
|
const syncing = syncRequest(this, context);
|
|
536
540
|
const found = await services.embeddings.search(query, {
|
|
@@ -551,14 +555,14 @@ export const conversationsSearchCommand = (messenger) => syncOptions(boundOption
|
|
|
551
555
|
...(options.timezone === undefined ? {} : { timezone: options.timezone }),
|
|
552
556
|
...(chat === undefined ? {} : { chat }),
|
|
553
557
|
model,
|
|
554
|
-
...(since === undefined ? {} : { since: new Date(
|
|
558
|
+
...(window.since === undefined ? {} : { since: new Date(window.since).toISOString() }),
|
|
555
559
|
});
|
|
556
560
|
return { found, refreshed: found.prepared };
|
|
557
561
|
});
|
|
558
562
|
if (refreshed)
|
|
559
563
|
for (const note of leftNotes(refreshed, rootOf(this)))
|
|
560
564
|
context.renderer.note(note);
|
|
561
|
-
renderAnswer(this, context, foundConversations("search conversations", found, limit, messenger, chat), () => found.hits.map((hit) => `${hitLine(hit)}\n`).join(""));
|
|
565
|
+
renderAnswer(this, context, foundConversations("search conversations", found, limit, messenger, chat, window.meta), () => found.hits.map((hit) => `${hitLine(hit)}\n`).join(""));
|
|
562
566
|
if (found.hits.length === 0 && context.format === "pretty")
|
|
563
567
|
context.renderer.note(found.meaning === "unavailable"
|
|
564
568
|
? "nothing matches in words"
|
|
@@ -51,7 +51,7 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
|
|
|
51
51
|
const { app } = messenger;
|
|
52
52
|
const { settings, renderer, format, env } = context;
|
|
53
53
|
const kinds = kind === undefined ? undefined : kindsOf(kind);
|
|
54
|
-
const { since } = catchUpQuery(query, INBOX_QUERY, "inbox");
|
|
54
|
+
const { since, meta } = catchUpQuery(query, INBOX_QUERY, "inbox");
|
|
55
55
|
if (fresh && since !== undefined)
|
|
56
56
|
throw new CliError("validation_error", "--new keeps its own point — not with a date in the query");
|
|
57
57
|
const saved = fresh && since === undefined
|
|
@@ -83,7 +83,7 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
|
|
|
83
83
|
renderer.note(`marked ${marked.length} chat(s) read up to the newest message shown`);
|
|
84
84
|
if (inbox.partial && format === "pretty")
|
|
85
85
|
renderer.warn(PARTIAL_NOTE);
|
|
86
|
-
const answer = inboxAnswer(inbox, hearing, reader, transcribe === true);
|
|
86
|
+
const answer = inboxAnswer(inbox, hearing, reader, transcribe === true, meta);
|
|
87
87
|
renderAnswer(this, context, answer, () => {
|
|
88
88
|
if (answer.items.length === 0) {
|
|
89
89
|
renderer.note(inbox.mode === "unread" ? "nothing unread" : `nothing new since ${inbox.since}`);
|
|
@@ -6,7 +6,7 @@ import { replyRenderer } from "../../replies/rendering.js";
|
|
|
6
6
|
import { audienceWarnings, readReplies, repliesPathFor } from "../../replies/rules.js";
|
|
7
7
|
import { paused, readRepliesState, repliesStatePathFor, writeRepliesState } from "../../replies/state.js";
|
|
8
8
|
import { levelFor } from "../../sends/permissions.js";
|
|
9
|
-
import {
|
|
9
|
+
import { dateWindow } from "../../services/moment.js";
|
|
10
10
|
import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
11
11
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
12
12
|
import { messengerContext } from "./context.js";
|
|
@@ -37,7 +37,8 @@ export const repliesCommand = (messenger) => {
|
|
|
37
37
|
throw new CliError("validation_error", "--ai calls a model and cannot be combined with --offline");
|
|
38
38
|
if (ai && levelFor(settings.permissions, "messages.list").level === "deny")
|
|
39
39
|
throw new CliError("permission_error", "this profile denies reading messages — no stored data was sent to a reply model");
|
|
40
|
-
const
|
|
40
|
+
const window = dateWindow({ sinceTime: sinceTime ?? `${DRY_RUN_DAYS}d` });
|
|
41
|
+
const since = window.since;
|
|
41
42
|
const path = repliesPathFor(messenger.app, settings.profile, env);
|
|
42
43
|
const { rules, audience } = readReplies(path, messenger.provider);
|
|
43
44
|
if (rules.length === 0) {
|
|
@@ -81,7 +82,7 @@ export const repliesCommand = (messenger) => {
|
|
|
81
82
|
.sort(([, a], [, b]) => b - a)
|
|
82
83
|
.map(([why, count]) => ` passed over ${count}: ${why}`),
|
|
83
84
|
]);
|
|
84
|
-
renderAnswer(this, context, itemOf(answer), () => `${lines.join("\n")}\n`);
|
|
85
|
+
renderAnswer(this, context, itemOf(answer, { meta: window.meta }), () => `${lines.join("\n")}\n`);
|
|
85
86
|
});
|
|
86
87
|
const switched = (on) => async function () {
|
|
87
88
|
const context = messengerContext(this, messenger);
|
|
@@ -45,7 +45,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
|
45
45
|
const hearWith = modelWith(options.transcribe, options.model);
|
|
46
46
|
const context = messengerContext(this, messenger);
|
|
47
47
|
const { settings, renderer, format, env } = context;
|
|
48
|
-
const { since: from, chat } = catchUpQuery(query, REVIEW_QUERY, "review");
|
|
48
|
+
const { since: from, chat, meta } = catchUpQuery(query, REVIEW_QUERY, "review");
|
|
49
49
|
if (options.new && (from !== undefined || options.unanswered !== undefined)) {
|
|
50
50
|
// An open question stays open: moving past it would drop it from the next review.
|
|
51
51
|
throw new CliError("validation_error", "--new keeps its own point — not with a date or --unanswered");
|
|
@@ -96,6 +96,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
|
96
96
|
transcribe,
|
|
97
97
|
fresh: saved !== undefined,
|
|
98
98
|
...(chat === undefined ? {} : { chat }),
|
|
99
|
+
...(meta === undefined ? {} : { meta }),
|
|
99
100
|
}), () => {
|
|
100
101
|
const read = found.chats
|
|
101
102
|
.flatMap((one) => one.messages.map((message) => ({ ...message, chatTitle: one.title })))
|
|
@@ -67,7 +67,7 @@ export declare const chartAnswer: (chart: ChartData, file?: {
|
|
|
67
67
|
/** A report or a chart is one object: `--jsonl` would print one line pretending to be a stream. */
|
|
68
68
|
export declare const refuseJsonl: (format: string) => void;
|
|
69
69
|
/** `stats chats show`, CLI and MCP alike; a chat the store does not hold whole names the sync that fills it. */
|
|
70
|
-
export declare const chatStatsAnswer: (stats: ChatStats, account: AccountKey) => ItemBody<Of<"chat_stats">>;
|
|
70
|
+
export declare const chatStatsAnswer: (stats: ChatStats, account: AccountKey, meta?: Partial<Meta>) => ItemBody<Of<"chat_stats">>;
|
|
71
71
|
/** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
|
|
72
72
|
export declare const MEMBERS_UNRECORDED = "joins and leaves are not recorded for this chat yet, so they are not counted";
|
|
73
73
|
/** `stats tasks show`, CLI and MCP alike. */
|
|
@@ -167,7 +167,7 @@ export const adminAnswer = (found) => {
|
|
|
167
167
|
rows,
|
|
168
168
|
}, {
|
|
169
169
|
next: drilldownSteps(top?.drilldown, top?.drilldown.arguments.component, top?.message ?? (top?.id.startsWith("msg:") ? top.id : undefined)),
|
|
170
|
-
...
|
|
170
|
+
meta: { ...found.timeMeta, ...(found.hasMore ? { truncated: true } : {}) },
|
|
171
171
|
});
|
|
172
172
|
};
|
|
173
173
|
const cohortRow = (row) => ({
|
|
@@ -221,7 +221,7 @@ export const retentionAnswer = (found) => {
|
|
|
221
221
|
rows: found.items.map(cohortRow),
|
|
222
222
|
}, {
|
|
223
223
|
next: drilldownSteps(top?.drilldown, top?.drilldown.arguments.component),
|
|
224
|
-
...
|
|
224
|
+
meta: { ...found.timeMeta, ...(found.hasMore ? { truncated: true } : {}) },
|
|
225
225
|
});
|
|
226
226
|
};
|
|
227
227
|
const evidenceMessage = (hit) => ({
|
|
@@ -335,7 +335,7 @@ export const refuseJsonl = (format) => {
|
|
|
335
335
|
});
|
|
336
336
|
};
|
|
337
337
|
/** `stats chats show`, CLI and MCP alike; a chat the store does not hold whole names the sync that fills it. */
|
|
338
|
-
export const chatStatsAnswer = (stats, account) => {
|
|
338
|
+
export const chatStatsAnswer = (stats, account, meta) => {
|
|
339
339
|
const { chatId, completeness, topPosts, questions, memberCounts, members, series, fetch } = stats;
|
|
340
340
|
const item = defined({
|
|
341
341
|
chat: chatRef(account, chatId),
|
|
@@ -405,6 +405,7 @@ export const chatStatsAnswer = (stats, account) => {
|
|
|
405
405
|
...(completeness.state === "complete" || fetch === undefined
|
|
406
406
|
? {}
|
|
407
407
|
: { next: [nextStep("messages.sync", { args: { chat: chatId } })] }),
|
|
408
|
+
...(meta ? { meta } : {}),
|
|
408
409
|
});
|
|
409
410
|
};
|
|
410
411
|
/** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
|
|
@@ -42,6 +42,13 @@ export declare const searchRuns: (dir: string, { query, status, errorCode, opera
|
|
|
42
42
|
page: number;
|
|
43
43
|
limit: number;
|
|
44
44
|
hasMore: boolean;
|
|
45
|
+
meta: {
|
|
46
|
+
timezone?: string;
|
|
47
|
+
window?: {
|
|
48
|
+
since?: string;
|
|
49
|
+
until?: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
45
52
|
};
|
|
46
53
|
/** A page of `searchRuns` in the list envelope; `args` are the search's own, so `page.next` reruns it. */
|
|
47
54
|
export declare const runsFound: (found: ReturnType<typeof searchRuns>, args: Record<string, unknown>) => import("../envelope.js").ListBody<unknown>;
|
package/dist/cli/runs/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { findRun, listRuns, readEvents } from "../../runs/run.js";
|
|
3
|
-
import {
|
|
3
|
+
import { dateWindow } from "../../services/moment.js";
|
|
4
4
|
import { listOf, snakeKeys } from "../envelope.js";
|
|
5
5
|
const FIELDS = [
|
|
6
6
|
"event",
|
|
@@ -35,7 +35,8 @@ const safeEvent = (event) => Object.fromEntries(FIELDS.flatMap((name) => {
|
|
|
35
35
|
export const searchRuns = (dir, { query = "", status, errorCode, operation, profile, since, limit = 20, page = 1 } = {}) => {
|
|
36
36
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(page) || page < 1)
|
|
37
37
|
throw new CliError("validation_error", "run search uses a limit from 1 to 100 and a positive page");
|
|
38
|
-
const
|
|
38
|
+
const window = dateWindow(since === undefined ? {} : { sinceTime: since });
|
|
39
|
+
const from = window.since;
|
|
39
40
|
if (from !== undefined && !Number.isFinite(from))
|
|
40
41
|
throw new CliError("validation_error", "--since-time must be a valid date or ISO timestamp");
|
|
41
42
|
const needle = query.toLowerCase();
|
|
@@ -100,7 +101,13 @@ export const searchRuns = (dir, { query = "", status, errorCode, operation, prof
|
|
|
100
101
|
if (matches.length > offset + limit)
|
|
101
102
|
break;
|
|
102
103
|
}
|
|
103
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
items: matches.slice(offset, offset + limit),
|
|
106
|
+
page,
|
|
107
|
+
limit,
|
|
108
|
+
hasMore: matches.length > offset + limit,
|
|
109
|
+
meta: window.meta,
|
|
110
|
+
};
|
|
104
111
|
};
|
|
105
112
|
/** A page of `searchRuns` in the list envelope; `args` are the search's own, so `page.next` reruns it. */
|
|
106
113
|
export const runsFound = (found, args) => listOf("runs search", {
|
|
@@ -109,4 +116,5 @@ export const runsFound = (found, args) => listOf("runs search", {
|
|
|
109
116
|
has_more: found.hasMore,
|
|
110
117
|
page: found.page,
|
|
111
118
|
args: Object.fromEntries(Object.entries(args).filter(([name, value]) => value !== undefined && name !== "page")),
|
|
119
|
+
meta: found.meta,
|
|
112
120
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ItemEnvelope, type ListEnvelope } from "@wirecat/cli-base/standards";
|
|
1
|
+
import { type ItemEnvelope, type ListEnvelope, type Meta } from "@wirecat/cli-base/standards";
|
|
2
2
|
import * as v from "valibot";
|
|
3
3
|
import { type MeetingOwner } from "./item.js";
|
|
4
4
|
import type { Summary, Transcript, TranscriptRow } from "./model.js";
|
|
@@ -160,4 +160,6 @@ export declare const MEETING_STEPS: ({
|
|
|
160
160
|
export declare const meetingHitItem: (hit: SearchHit & {
|
|
161
161
|
ref: string;
|
|
162
162
|
}) => MeetingHitItem;
|
|
163
|
-
export declare const meetingHitsList: (found: MeetingSearchPage, limit: number, command?: string, args?: Record<string, unknown>) => MeetingListBody<MeetingHitItem
|
|
163
|
+
export declare const meetingHitsList: (found: MeetingSearchPage, limit: number, command?: string, args?: Record<string, unknown>, meta?: Partial<Meta>) => MeetingListBody<MeetingHitItem> & {
|
|
164
|
+
meta?: Partial<Meta>;
|
|
165
|
+
};
|
package/dist/meetings/answers.js
CHANGED
|
@@ -201,7 +201,7 @@ export const meetingHitItem = (hit) => ({
|
|
|
201
201
|
},
|
|
202
202
|
}),
|
|
203
203
|
});
|
|
204
|
-
export const meetingHitsList = (found, limit, command = "search meetings", args = {}) => {
|
|
204
|
+
export const meetingHitsList = (found, limit, command = "search meetings", args = {}, meta = {}) => {
|
|
205
205
|
const items = found.items.map(meetingHitItem);
|
|
206
206
|
const cursor = found.hasMore && found.nextCursor !== null ? found.nextCursor : undefined;
|
|
207
207
|
const pending = found.coverage.indexing === "pending";
|
|
@@ -238,5 +238,6 @@ export const meetingHitsList = (found, limit, command = "search meetings", args
|
|
|
238
238
|
],
|
|
239
239
|
}
|
|
240
240
|
: {}),
|
|
241
|
+
...(Object.keys(meta).length === 0 ? {} : { meta }),
|
|
241
242
|
};
|
|
242
243
|
};
|
|
@@ -17,4 +17,4 @@ export declare const addMeetingCommands: (program: Command, deps: MeetingCommand
|
|
|
17
17
|
export { addMeetingPassageCommand, createMeetingPassageCommand, type MeetingPassageFlags, meetingPassageRequest, } from "./passage.js";
|
|
18
18
|
export { addMeetingProjectEvidenceCommand } from "./project-evidence.js";
|
|
19
19
|
export { queryWordsAfterOptions } from "./query-options.js";
|
|
20
|
-
export { addMeetingSearchCommand, createMeetingSearchCommand, type MeetingHitSearch, type MeetingHitSearchOptions, type MeetingSearchFlags, meetingHitSearchRequest, meetingSearchArguments, validateMeetingHitSearch, } from "./search.js";
|
|
20
|
+
export { addMeetingSearchCommand, createMeetingSearchCommand, type MeetingHitSearch, type MeetingHitSearchOptions, type MeetingSearchFlags, meetingHitSearchRequest, meetingSearchArguments, meetingSearchMeta, validateMeetingHitSearch, } from "./search.js";
|
|
@@ -144,4 +144,4 @@ export const addMeetingCommands = (program, deps) => {
|
|
|
144
144
|
export { addMeetingPassageCommand, createMeetingPassageCommand, meetingPassageRequest, } from "./passage.js";
|
|
145
145
|
export { addMeetingProjectEvidenceCommand } from "./project-evidence.js";
|
|
146
146
|
export { queryWordsAfterOptions } from "./query-options.js";
|
|
147
|
-
export { addMeetingSearchCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingSearchArguments, validateMeetingHitSearch, } from "./search.js";
|
|
147
|
+
export { addMeetingSearchCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingSearchArguments, meetingSearchMeta, validateMeetingHitSearch, } from "./search.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Meta } from "@wirecat/cli-base/standards";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import type { MeetingSearchPage } from "../search-store.js";
|
|
3
4
|
import { type MeetingHitSearchOptions } from "../search-validation.js";
|
|
@@ -13,6 +14,8 @@ export interface MeetingSearchFlags {
|
|
|
13
14
|
topic?: string;
|
|
14
15
|
sinceTime?: number;
|
|
15
16
|
untilTime?: number;
|
|
17
|
+
/** The zone a date or time without one was read in; the parser sets it, since the typed text is gone after. */
|
|
18
|
+
timesReadIn?: string;
|
|
16
19
|
limit?: number;
|
|
17
20
|
cursor?: string;
|
|
18
21
|
maxReadBytes?: number;
|
|
@@ -23,6 +26,8 @@ export declare const createMeetingSearchCommand: (program: Command) => Command;
|
|
|
23
26
|
export declare const meetingHitSearchRequest: (query: string, options: MeetingSearchFlags, signal?: AbortSignal) => MeetingHitSearchOptions;
|
|
24
27
|
/** Parsed dates must become ISO values again so the CLI can replay its own continuation. */
|
|
25
28
|
export declare const meetingSearchArguments: (query: string, options: MeetingSearchFlags) => Record<string, unknown>;
|
|
29
|
+
/** What `meta` says about the dates: the zone one without its own was read in, and the window in UTC. */
|
|
30
|
+
export declare const meetingSearchMeta: (options: MeetingSearchFlags) => Pick<Meta, "timezone" | "window">;
|
|
26
31
|
export declare const addMeetingSearchCommand: (program: Command, deps: {
|
|
27
32
|
search: (query: string, request: MeetingHitSearchOptions, options: MeetingSearchFlags & Record<string, unknown>) => Promise<MeetingSearchPage>;
|
|
28
33
|
signal: () => AbortSignal | undefined;
|
|
@@ -5,13 +5,19 @@ import { Command } from "commander";
|
|
|
5
5
|
import { meetingHitsList } from "../answers.js";
|
|
6
6
|
import { validateMeetingHitSearch } from "../search-validation.js";
|
|
7
7
|
export { validateMeetingHitSearch } from "../search-validation.js";
|
|
8
|
-
const
|
|
8
|
+
const ZONED = /(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
9
|
+
const moment = (flag, end, readIn) => (value) => {
|
|
10
|
+
const zone = timezoneOf();
|
|
11
|
+
let time;
|
|
9
12
|
try {
|
|
10
|
-
|
|
13
|
+
time = zonedMoment(value, zone, { start: 0, end: value.length }, { end });
|
|
11
14
|
}
|
|
12
15
|
catch {
|
|
13
16
|
throw new CliError("validation_error", `${flag} takes an ISO date, or a time with or without an offset`);
|
|
14
17
|
}
|
|
18
|
+
if (!ZONED.test(value.trim()))
|
|
19
|
+
readIn(zone);
|
|
20
|
+
return time;
|
|
15
21
|
};
|
|
16
22
|
const integer = (value) => {
|
|
17
23
|
if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
@@ -25,6 +31,7 @@ export const createMeetingSearchCommand = (program) => {
|
|
|
25
31
|
search = new Command("search").description("search the stored resources");
|
|
26
32
|
program.addCommand(search);
|
|
27
33
|
}
|
|
34
|
+
const readIn = (zone) => command.setOptionValue("timesReadIn", zone);
|
|
28
35
|
const command = search
|
|
29
36
|
.command("meetings")
|
|
30
37
|
.description("find words in stored meetings, indexing new ones first")
|
|
@@ -37,8 +44,8 @@ export const createMeetingSearchCommand = (program) => {
|
|
|
37
44
|
.option("--project <name>", "meetings confirmed in this project, by name, key or alias")
|
|
38
45
|
.option("--tag <name>", "meetings with this tag, by name or alias")
|
|
39
46
|
.option("--topic <name>", "meetings with this topic, by name or alias")
|
|
40
|
-
.option("--since-time <time>", "meetings starting at or after this ISO 8601 date or time", moment("--since-time", false))
|
|
41
|
-
.option("--until-time <time>", "meetings starting at or before this ISO 8601 date or time; a date alone covers that whole local day", moment("--until-time", true))
|
|
47
|
+
.option("--since-time <time>", "meetings starting at or after this ISO 8601 date or time", moment("--since-time", false, readIn))
|
|
48
|
+
.option("--until-time <time>", "meetings starting at or before this ISO 8601 date or time; a date alone covers that whole local day", moment("--until-time", true, readIn))
|
|
42
49
|
.option("--limit <n>", "how many hits, 1 through 1000 (default 100)", integer)
|
|
43
50
|
.option("--cursor <cursor>", "continue from the previous page's page.next_cursor, with the same query and filters")
|
|
44
51
|
.option("--max-read-bytes <bytes>", "most stored bytes to read, 1 through 67108864 (default 4194304)", integer);
|
|
@@ -86,7 +93,18 @@ export const meetingSearchArguments = (query, options) => Object.fromEntries(Obj
|
|
|
86
93
|
until_time: options.untilTime === undefined ? undefined : new Date(options.untilTime).toISOString(),
|
|
87
94
|
max_read_bytes: options.maxReadBytes ?? 4194304,
|
|
88
95
|
}).filter(([, value]) => value !== undefined));
|
|
96
|
+
/** What `meta` says about the dates: the zone one without its own was read in, and the window in UTC. */
|
|
97
|
+
export const meetingSearchMeta = (options) => {
|
|
98
|
+
const window = {
|
|
99
|
+
...(options.sinceTime === undefined ? {} : { since: new Date(options.sinceTime).toISOString() }),
|
|
100
|
+
...(options.untilTime === undefined ? {} : { until: new Date(options.untilTime).toISOString() }),
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
...(options.timesReadIn === undefined ? {} : { timezone: options.timesReadIn }),
|
|
104
|
+
...(Object.keys(window).length === 0 ? {} : { window }),
|
|
105
|
+
};
|
|
106
|
+
};
|
|
89
107
|
export const addMeetingSearchCommand = (program, deps) => createMeetingSearchCommand(program).action(async (query, options) => {
|
|
90
108
|
const request = meetingHitSearchRequest(query, options, deps.signal());
|
|
91
|
-
deps.output(meetingHitsList(await deps.search(query, request, options), request.limit ?? 100, "search meetings", meetingSearchArguments(query, options)));
|
|
109
|
+
deps.output(meetingHitsList(await deps.search(query, request, options), request.limit ?? 100, "search meetings", meetingSearchArguments(query, options), meetingSearchMeta(options)));
|
|
92
110
|
});
|
|
@@ -150,7 +150,9 @@ export declare const callMeetingSearchTool: (search: MeetingHitSearch, input: un
|
|
|
150
150
|
started_at: string | null;
|
|
151
151
|
url: string | null;
|
|
152
152
|
} | undefined;
|
|
153
|
-
}
|
|
153
|
+
}> & {
|
|
154
|
+
meta?: Partial<import("@wirecat/cli-base/standards").Meta>;
|
|
155
|
+
};
|
|
154
156
|
} & {
|
|
155
157
|
isError?: boolean;
|
|
156
158
|
})>;
|
|
@@ -60,10 +60,11 @@ export const callMeetingSearchTool = async (search, input, signal, failure = {})
|
|
|
60
60
|
const query = args.query;
|
|
61
61
|
validateMeetingHitSearch(query, options);
|
|
62
62
|
signal?.throwIfAborted();
|
|
63
|
-
const
|
|
64
|
-
...
|
|
65
|
-
|
|
66
|
-
}
|
|
63
|
+
const window = {
|
|
64
|
+
...(options.since === undefined ? {} : { since: new Date(options.since).toISOString() }),
|
|
65
|
+
...(options.until === undefined ? {} : { until: new Date(options.until).toISOString() }),
|
|
66
|
+
};
|
|
67
|
+
const result = meetingHitsList(await search(query, options), options.limit ?? 100, "search meetings", { ...args, max_read_bytes: options.maxReadBytes ?? 4194304 }, Object.keys(window).length === 0 ? {} : { window });
|
|
67
68
|
signal?.throwIfAborted();
|
|
68
69
|
return agentResult({
|
|
69
70
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
@@ -2,6 +2,7 @@ import type { AdminOptions, AdminReport } from "../domain/admin-statistics.js";
|
|
|
2
2
|
import type { AdminStoreResult, MessageStore } from "../store/store.js";
|
|
3
3
|
import type { ServiceDeps } from "./deps.js";
|
|
4
4
|
import type { SearchQuery } from "./messages.js";
|
|
5
|
+
import { type DateWindow } from "./moment.js";
|
|
5
6
|
import { type RankingSelection } from "./rankings-selection.js";
|
|
6
7
|
export type AdminQuery = SearchQuery & {
|
|
7
8
|
answerers?: string[];
|
|
@@ -28,6 +29,8 @@ export interface AdminSelection {
|
|
|
28
29
|
}
|
|
29
30
|
export interface AdminFound extends AdminStoreResult {
|
|
30
31
|
report: AdminReport;
|
|
32
|
+
/** How the typed join dates were read, for the answer's `meta`. */
|
|
33
|
+
timeMeta?: DateWindow["meta"];
|
|
31
34
|
detectorVersion: 1;
|
|
32
35
|
items: (AdminStoreResult["items"][number] & {
|
|
33
36
|
drilldown: {
|
|
@@ -2,7 +2,7 @@ import { CliError, singleLine } from "@wirecat/cli-base";
|
|
|
2
2
|
import { parseLocator } from "../domain/locator.js";
|
|
3
3
|
import { isId, pickPerson } from "../resolve.js";
|
|
4
4
|
import { prepareLucene, stemExecution } from "./messages-search.js";
|
|
5
|
-
import {
|
|
5
|
+
import { dateWindow } from "./moment.js";
|
|
6
6
|
import { rankingSelection, readRankingSelection } from "./rankings-selection.js";
|
|
7
7
|
import { searchRecordOf } from "./search-record.js";
|
|
8
8
|
function invalid(message) {
|
|
@@ -205,6 +205,12 @@ export const adminStatisticsService = (deps) => ({
|
|
|
205
205
|
if (pinned &&
|
|
206
206
|
(query.source || query.ast || query.exact || (query.timezone && query.timezone !== prepared.timezone)))
|
|
207
207
|
invalid("a saved report keeps its resolved query, accounts and timezone");
|
|
208
|
+
const window = dateWindow({
|
|
209
|
+
...(query.sinceTime ? { sinceTime: query.sinceTime } : {}),
|
|
210
|
+
...(query.untilTime ? { untilTime: query.untilTime } : {}),
|
|
211
|
+
zone: prepared.timezone,
|
|
212
|
+
now: cutoff,
|
|
213
|
+
});
|
|
208
214
|
const options = {
|
|
209
215
|
report,
|
|
210
216
|
cutoff,
|
|
@@ -212,12 +218,8 @@ export const adminStatisticsService = (deps) => ({
|
|
|
212
218
|
? pinned.options.olderThan
|
|
213
219
|
: duration(query.olderThan ?? "24h", "--older-than"),
|
|
214
220
|
within: query.within === undefined && pinned ? pinned.options.within : duration(query.within ?? "7d", "--within"),
|
|
215
|
-
joinSince:
|
|
216
|
-
|
|
217
|
-
: (pinned?.options.joinSince ?? cutoff - 30 * 86_400_000),
|
|
218
|
-
joinUntil: query.untilTime
|
|
219
|
-
? momentOf(query.untilTime, "--until-time", cutoff, { zone: prepared.timezone, end: true })
|
|
220
|
-
: (pinned?.options.joinUntil ?? cutoff),
|
|
221
|
+
joinSince: window.since ?? pinned?.options.joinSince ?? cutoff - 30 * 86_400_000,
|
|
222
|
+
joinUntil: window.until ?? pinned?.options.joinUntil ?? cutoff,
|
|
221
223
|
minViews: count(query.minViews ?? pinned?.options.minViews ?? 1, "--min-views"),
|
|
222
224
|
maxReplies: count(query.maxReplies ?? pinned?.options.maxReplies ?? 0, "--max-replies"),
|
|
223
225
|
answerers: query.answerers === undefined && pinned
|
|
@@ -252,6 +254,7 @@ export const adminStatisticsService = (deps) => ({
|
|
|
252
254
|
return {
|
|
253
255
|
...result,
|
|
254
256
|
report,
|
|
257
|
+
timeMeta: window.meta,
|
|
255
258
|
detectorVersion: 1,
|
|
256
259
|
items: result.items.map((original) => {
|
|
257
260
|
const identityKnown = known.has(JSON.stringify([original.account.provider, original.account.account, original.id]));
|
|
@@ -75,6 +75,13 @@ export declare const retentionService: (deps: ServiceDeps) => {
|
|
|
75
75
|
}[];
|
|
76
76
|
account: import("../store/types.js").AccountKey;
|
|
77
77
|
report: "retention";
|
|
78
|
+
timeMeta: {
|
|
79
|
+
timezone?: string;
|
|
80
|
+
window?: {
|
|
81
|
+
since?: string;
|
|
82
|
+
until?: string;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
78
85
|
}>;
|
|
79
86
|
evidence: (target: "messages" | "contacts", reference: string, selection: unknown, query: {
|
|
80
87
|
component: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { timezoneOf } from "@wirecat/cli-base/query";
|
|
3
3
|
import { storedChatId } from "./message-resolution.js";
|
|
4
|
-
import {
|
|
4
|
+
import { dateWindow } from "./moment.js";
|
|
5
5
|
export const observationDuration = (text, flag) => {
|
|
6
6
|
const match = /^(\d+(?:\.\d+)?)([smhdw])$/.exec(text);
|
|
7
7
|
const units = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
|
|
@@ -14,11 +14,15 @@ export const retentionService = (deps) => ({
|
|
|
14
14
|
report: async (chat, query = {}) => {
|
|
15
15
|
const cutoff = Date.now();
|
|
16
16
|
const zone = timezoneOf(query.timezone);
|
|
17
|
+
const window = dateWindow({
|
|
18
|
+
...(query.sinceTime === undefined ? {} : { sinceTime: query.sinceTime }),
|
|
19
|
+
...(query.untilTime === undefined ? {} : { untilTime: query.untilTime }),
|
|
20
|
+
zone,
|
|
21
|
+
now: cutoff,
|
|
22
|
+
});
|
|
17
23
|
const options = {
|
|
18
|
-
since:
|
|
19
|
-
|
|
20
|
-
: momentOf(query.sinceTime, "--since-time", cutoff, { zone }),
|
|
21
|
-
until: query.untilTime === undefined ? cutoff : momentOf(query.untilTime, "--until-time", cutoff, { zone, end: true }),
|
|
24
|
+
since: window.since ?? cutoff - 90 * 86_400_000,
|
|
25
|
+
until: window.until ?? cutoff,
|
|
22
26
|
cutoff,
|
|
23
27
|
checkpoints: (query.checkpoints ?? "1d,7d,30d")
|
|
24
28
|
.split(",")
|
|
@@ -46,6 +50,7 @@ export const retentionService = (deps) => ({
|
|
|
46
50
|
})),
|
|
47
51
|
account,
|
|
48
52
|
report: "retention",
|
|
53
|
+
timeMeta: window.meta,
|
|
49
54
|
};
|
|
50
55
|
},
|
|
51
56
|
evidence: async (target, reference, selection, query) => {
|
|
@@ -24,8 +24,9 @@ export declare const MIGRATIONS: Migration[];
|
|
|
24
24
|
* that fails names `store reset` when the app has one: without it the owner is stuck until a build that can migrate
|
|
25
25
|
* the file. An app without that command (drive) passes no `command`, and the advice names none.
|
|
26
26
|
*/
|
|
27
|
-
export declare const migrate: (database: CacheDatabase, { migrations, now, command, }?: {
|
|
27
|
+
export declare const migrate: (database: CacheDatabase, { migrations, now, command, ownTable, }?: {
|
|
28
28
|
migrations?: Migration[];
|
|
29
29
|
now?: () => number;
|
|
30
30
|
command?: string;
|
|
31
|
+
ownTable?: string | null;
|
|
31
32
|
}) => void;
|
package/dist/store/migrations.js
CHANGED
|
@@ -17,6 +17,13 @@ const HISTORY = `CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
17
17
|
applied_at INTEGER NOT NULL
|
|
18
18
|
)`;
|
|
19
19
|
const latest = (migrations) => migrations.at(-1)?.version ?? 0;
|
|
20
|
+
/**
|
|
21
|
+
* The pre-rename `cli-messaging` store also has `schema_migrations`, with its own, higher numbers ("29, needs at
|
|
22
|
+
* least 28"). Once ours caught up, the version check alone took it for a compatible file, opened it and would have
|
|
23
|
+
* migrated it. Version 1 here always creates `meetings`; that store never had one.
|
|
24
|
+
*/
|
|
25
|
+
const OWN_TABLE = "meetings";
|
|
26
|
+
const hasTable = (database, name) => database.prepare("SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = ?").get(name) !== undefined;
|
|
20
27
|
const fileState = (database) => {
|
|
21
28
|
const row = database
|
|
22
29
|
.prepare("SELECT version, min_compatible FROM schema_migrations ORDER BY version DESC LIMIT 1")
|
|
@@ -29,12 +36,16 @@ const fileState = (database) => {
|
|
|
29
36
|
* that fails names `store reset` when the app has one: without it the owner is stuck until a build that can migrate
|
|
30
37
|
* the file. An app without that command (drive) passes no `command`, and the advice names none.
|
|
31
38
|
*/
|
|
32
|
-
export const migrate = (database, { migrations = MIGRATIONS, now = Date.now, command, } = {}) => {
|
|
39
|
+
export const migrate = (database, { migrations = MIGRATIONS, now = Date.now, command, ownTable = OWN_TABLE, } = {}) => {
|
|
33
40
|
const ours = latest(migrations);
|
|
34
41
|
database.exec("BEGIN IMMEDIATE");
|
|
35
42
|
try {
|
|
36
43
|
database.exec(HISTORY);
|
|
37
44
|
const file = fileState(database);
|
|
45
|
+
if (file.version > 0 && ownTable !== null && !hasTable(database, ownTable)) {
|
|
46
|
+
throw new CliError("configuration_error", `the message store was written by another, incompatible tool (schema ${file.version} by its own numbering, ` +
|
|
47
|
+
"with no meetings table) — this tool will not open or change it; move it aside to start a new store");
|
|
48
|
+
}
|
|
38
49
|
if (file.version > ours && file.minCompatible > ours) {
|
|
39
50
|
throw new CliError("configuration_error", `the message store was written by a newer version (schema ${file.version}, needs at least ` +
|
|
40
51
|
`${file.minCompatible}; this one speaks ${ours}) — upgrade this tool`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.385.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",
|