@wirecat/cli-core 0.400.0 → 0.401.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 +29 -0
- package/README.md +1 -1
- package/dist/cli/bot/store.js +1 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/meeting-evidence-answer.d.ts +46 -0
- package/dist/cli/meeting-evidence-answer.js +22 -0
- package/dist/cli/meeting-preparation-answer.js +1 -13
- package/dist/cli/messenger/account-sessions-command.js +0 -2
- package/dist/cli/messenger/archive-commands.js +1 -1
- package/dist/cli/messenger/doctor-command.js +0 -1
- package/dist/cli/messenger/message-writes.d.ts +192 -2
- package/dist/cli/messenger/message-writes.js +6 -2
- package/dist/cli/messenger/messages-send-command.js +9 -6
- package/dist/cli/messenger/models-command.js +0 -3
- package/dist/cli/messenger/records-command.js +0 -2
- package/dist/cli/messenger/review.js +1 -1
- package/dist/cli/messenger/schedule-command.js +2 -2
- package/dist/cli/messenger/search-answers.js +2 -0
- package/dist/cli/messenger/server-command.js +0 -3
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/store-maintenance-command.js +7 -7
- package/dist/cli/messenger/stored.js +4 -1
- package/dist/cli/messenger/tasks-command.js +10 -6
- package/dist/documents/index.d.ts +1 -0
- package/dist/documents/index.js +1 -0
- package/dist/entities/hit.d.ts +4 -0
- package/dist/entities/hit.js +4 -0
- package/dist/entities/index.d.ts +285 -0
- package/dist/entities/index.js +5 -1
- package/dist/entities/meeting-evidence.d.ts +86 -0
- package/dist/entities/meeting-evidence.js +25 -0
- package/dist/entities/meeting-preparation-parts.d.ts +14 -0
- package/dist/entities/meeting-preparation-parts.js +15 -13
- package/dist/entities/meeting.d.ts +1 -1
- package/dist/entities/meeting.js +1 -1
- package/dist/entities/message-write.d.ts +187 -0
- package/dist/entities/message-write.js +2 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/mcp/agent.js +8 -4
- package/dist/meetings/cli/index.js +3 -1
- package/dist/meetings/index.d.ts +1 -1
- package/dist/meetings/index.js +1 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/meetings/read-answers.d.ts +18 -0
- package/dist/meetings/read-answers.js +30 -0
- package/dist/ports/messenger.d.ts +10 -1
- package/dist/sends/guard.d.ts +1 -1
- package/dist/sends/guard.js +9 -5
- package/dist/sends/journal.d.ts +1 -1
- package/dist/sends/permissions.d.ts +1 -1
- package/dist/sends/permissions.js +31 -1
- package/dist/sends/upload.d.ts +4 -4
- package/dist/sends/upload.js +24 -20
- package/dist/services/documents/mirrors.d.ts +98 -0
- package/dist/services/documents/mirrors.js +144 -0
- package/dist/services/messages.js +1 -0
- package/dist/services/search-all-meetings.js +21 -8
- package/dist/services/task-rules.d.ts +1 -1
- package/dist/services/tasks.d.ts +2 -2
- package/dist/services/tasks.js +1 -1
- package/dist/store/sqlite/drizzle/bun.js +3 -3
- package/dist/store/sqlite/drizzle/bun.js.map +1 -1
- package/dist/store/sqlite/drizzle/core.js +2 -2
- package/dist/store/sqlite/drizzle/core.js.map +1 -1
- package/dist/store/sqlite/drizzle/node.js +3 -3
- package/dist/store/sqlite/drizzle/node.js.map +1 -1
- package/dist/store/sqlite/drizzle/{shared-O7CTKP45.js → shared-HKXF6HKX.js} +57 -57
- package/dist/store/sqlite/drizzle/{shared-O7CTKP45.js.map → shared-HKXF6HKX.js.map} +1 -1
- package/package.json +2 -2
- package/parity.json +6 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import type { MeetingLink } from "./links.js";
|
|
3
|
+
import type { Event } from "./model.js";
|
|
3
4
|
import type { MeetingReviewItem } from "./review.js";
|
|
4
5
|
import type { MeetingSpeakerStats, MeetingStats } from "./stats.js";
|
|
5
6
|
/** One meeting in `review`: what the store holds about it, never transcript text. */
|
|
@@ -165,3 +166,20 @@ export declare const meetingPassage: v.StrictObjectSchema<{
|
|
|
165
166
|
readonly revision: v.PicklistSchema<["current", "superseded"], undefined>;
|
|
166
167
|
}, undefined>, undefined>;
|
|
167
168
|
}, undefined>;
|
|
169
|
+
/** A stored event a meeting belongs to; `id` is what `meetings update --event-id` takes. */
|
|
170
|
+
export declare const event: v.StrictObjectSchema<{
|
|
171
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
172
|
+
readonly event_series_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
173
|
+
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
174
|
+
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
175
|
+
readonly location: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
176
|
+
readonly starts_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
177
|
+
readonly ends_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
178
|
+
readonly timezone: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
179
|
+
/** `auto`: made from a meeting; `owner`: made by hand. */
|
|
180
|
+
readonly origin: v.PicklistSchema<["auto", "owner"], undefined>;
|
|
181
|
+
readonly deleted_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
182
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
183
|
+
readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
184
|
+
}, undefined>;
|
|
185
|
+
export declare const eventItem: (one: Event) => v.InferOutput<typeof event>;
|
|
@@ -181,3 +181,33 @@ export const meetingPassage = v.strictObject({
|
|
|
181
181
|
revision,
|
|
182
182
|
})),
|
|
183
183
|
});
|
|
184
|
+
/** A stored event a meeting belongs to; `id` is what `meetings update --event-id` takes. */
|
|
185
|
+
export const event = v.strictObject({
|
|
186
|
+
id: v.pipe(v.string(), v.minLength(1)),
|
|
187
|
+
event_series_id: v.nullable(text),
|
|
188
|
+
title: v.nullable(text),
|
|
189
|
+
description: v.nullable(text),
|
|
190
|
+
location: v.nullable(text),
|
|
191
|
+
starts_at: v.nullable(isoTime),
|
|
192
|
+
ends_at: v.nullable(isoTime),
|
|
193
|
+
timezone: v.nullable(text),
|
|
194
|
+
/** `auto`: made from a meeting; `owner`: made by hand. */
|
|
195
|
+
origin: v.picklist(["auto", "owner"]),
|
|
196
|
+
deleted_at: v.nullable(isoTime),
|
|
197
|
+
created_at: isoTime,
|
|
198
|
+
updated_at: isoTime,
|
|
199
|
+
});
|
|
200
|
+
export const eventItem = (one) => ({
|
|
201
|
+
id: String(one.id),
|
|
202
|
+
event_series_id: one.eventSeriesId === null ? null : String(one.eventSeriesId),
|
|
203
|
+
title: one.title,
|
|
204
|
+
description: one.description,
|
|
205
|
+
location: one.location,
|
|
206
|
+
starts_at: iso(one.startsAt),
|
|
207
|
+
ends_at: iso(one.endsAt),
|
|
208
|
+
timezone: one.timezone,
|
|
209
|
+
origin: one.origin,
|
|
210
|
+
deleted_at: iso(one.deletedAt),
|
|
211
|
+
created_at: new Date(one.createdAt).toISOString(),
|
|
212
|
+
updated_at: new Date(one.updatedAt).toISOString(),
|
|
213
|
+
});
|
|
@@ -41,7 +41,10 @@ export interface SendOptions {
|
|
|
41
41
|
formatting?: TextSpan[];
|
|
42
42
|
/** ISO time: the messenger holds it and sends it then, under a new id. */
|
|
43
43
|
at?: string;
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Sent in one message, `text` as the caption. More than one is an album: photos first, then files, each in the
|
|
46
|
+
* order given. An adapter whose messenger cannot send one refuses it with `validation_error` before anything goes.
|
|
47
|
+
*/
|
|
45
48
|
attachments?: Upload[];
|
|
46
49
|
/** One sticker by id, alone: no text, no attachment. Only where `Messenger.stickers` is on. */
|
|
47
50
|
sticker?: Id;
|
|
@@ -82,7 +85,13 @@ export interface NewPoll {
|
|
|
82
85
|
closeAfter?: number;
|
|
83
86
|
}
|
|
84
87
|
export interface Sent {
|
|
88
|
+
/** The message sent; for an album, its first. */
|
|
85
89
|
message: Message;
|
|
90
|
+
/**
|
|
91
|
+
* For an album, every message it made, in order, `message` among them. Unset for a single message, and where an
|
|
92
|
+
* adapter has not learned to answer with them yet.
|
|
93
|
+
*/
|
|
94
|
+
messages?: Message[];
|
|
86
95
|
/** The send's identity. Repeat it after an unknown outcome, never a new one. */
|
|
87
96
|
sendId: string;
|
|
88
97
|
}
|
package/dist/sends/guard.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface GuardRequest {
|
|
|
12
12
|
chatId: Id | null;
|
|
13
13
|
kind?: SendKind;
|
|
14
14
|
action?: ChatAction | AccountAction;
|
|
15
|
-
/** Messages a deletion names, or people a chat change adds. */
|
|
15
|
+
/** Messages a deletion names or an album sends, or people a chat change adds. */
|
|
16
16
|
count?: number;
|
|
17
17
|
sendId?: string;
|
|
18
18
|
sendAs?: Id;
|
package/dist/sends/guard.js
CHANGED
|
@@ -40,11 +40,11 @@ const countsTowardLimit = ({ kind = "message", action, notify }) => kind === "me
|
|
|
40
40
|
action === "join" ||
|
|
41
41
|
action === "contact-import";
|
|
42
42
|
/**
|
|
43
|
-
* Each deleted message, each person added to a group or let in by a
|
|
44
|
-
* counts as one.
|
|
43
|
+
* Each deleted message, each photo or file of an album (`NEED-1660`), each person added to a group or let in by a
|
|
44
|
+
* request, and each imported number counts as one.
|
|
45
45
|
*/
|
|
46
|
-
const weightOf = ({ kind, action, count, people }) => kind === "delete"
|
|
47
|
-
? (count ?? 1)
|
|
46
|
+
const weightOf = ({ kind = "message", action, count, people }) => kind === "delete" || (kind === "message" && action === undefined)
|
|
47
|
+
? Math.max(1, count ?? 1)
|
|
48
48
|
: action === "create" || action === "members.add" || action === "requests.accept" || action === "contact-import"
|
|
49
49
|
? Math.max(1, people ?? count ?? 1)
|
|
50
50
|
: 1;
|
|
@@ -180,7 +180,11 @@ export const sendGuard = ({ profile, recipients, command = recipients.command, r
|
|
|
180
180
|
reservation: id,
|
|
181
181
|
...(kind ? { kind } : {}),
|
|
182
182
|
...(action ? { action } : {}),
|
|
183
|
-
...(asked.people === undefined
|
|
183
|
+
...(asked.people === undefined
|
|
184
|
+
? {}
|
|
185
|
+
: kind === "delete" || kind === "message" || kind === undefined
|
|
186
|
+
? { count: asked.people }
|
|
187
|
+
: { people: asked.people }),
|
|
184
188
|
...(sendId === undefined ? {} : { sendId }),
|
|
185
189
|
...(operationId === undefined ? {} : { operationId }),
|
|
186
190
|
...(scheduledFor ? { scheduledFor } : {}),
|
package/dist/sends/journal.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface SendEntry {
|
|
|
23
23
|
messageId?: Id;
|
|
24
24
|
/** The command a write belongs to, where it named one: `polls.create` sets a poll apart from a message. */
|
|
25
25
|
key?: string;
|
|
26
|
-
/** How many messages a `delete` entry named — each one counts toward the hourly limit. */
|
|
26
|
+
/** How many messages a `delete` entry named, or an album sent — each one counts toward the hourly limit. */
|
|
27
27
|
count?: number;
|
|
28
28
|
forEveryone?: boolean;
|
|
29
29
|
/**
|
|
@@ -22,7 +22,7 @@ export declare const AUTHORITY_KEY: PermissionKey;
|
|
|
22
22
|
/** Whether these levels let an agent run a command that changes what a later run may do; `ask` does, as for any write. */
|
|
23
23
|
export declare const authorityAllowed: (levels: Readonly<Record<PermissionKey, Level>>) => boolean;
|
|
24
24
|
/** The resources at the top of the command tree, which `readOnly` and `allow` turn read-only as a whole. */
|
|
25
|
-
export declare const RESOURCES: readonly ["messages", "reactions", "polls", "topics", "chats", "contacts", "account", "bot", "conversations", "tags", "mentions", "search", "searches", "tasks", "replies", "attachments", "stats", "store", "metadata", "mcp"];
|
|
25
|
+
export declare const RESOURCES: readonly ["messages", "reactions", "polls", "topics", "chats", "contacts", "account", "bot", "conversations", "tags", "mentions", "search", "searches", "tasks", "replies", "attachments", "stats", "store", "metadata", "mcp", "server", "models", "schedule", "doctor", "images"];
|
|
26
26
|
/** The command path an old `allow` word stood for, as one key — what an MCP tool's `permission` is checked against. */
|
|
27
27
|
export declare const keyOfWord: (word: Permission) => PermissionKey;
|
|
28
28
|
/**
|
|
@@ -79,7 +79,27 @@ export const DEFAULT_PERMISSIONS = {
|
|
|
79
79
|
"replies.send": "deny",
|
|
80
80
|
// NEED-1662: an agent that changes settings, schedules or recipients could lift its own limits; the owner opts in.
|
|
81
81
|
"mcp.authority": "deny",
|
|
82
|
+
// NEED-1663: writes no level stopped before, so MCP could not offer them; over MCP `ask` goes ahead.
|
|
83
|
+
"store.backup": "ask",
|
|
84
|
+
"store.restore": "ask",
|
|
85
|
+
"store.migrate": "ask",
|
|
86
|
+
"store.repair": "ask",
|
|
87
|
+
"store.reset": "ask",
|
|
88
|
+
"store.clear": "ask",
|
|
89
|
+
"store.decrypt": "ask",
|
|
90
|
+
"store.copies.delete": "ask",
|
|
91
|
+
"server.start": "ask",
|
|
92
|
+
"server.stop": "ask",
|
|
93
|
+
"server.restart": "ask",
|
|
94
|
+
"models.audio.download": "ask",
|
|
95
|
+
"models.text.download": "ask",
|
|
96
|
+
"images.models.download": "ask",
|
|
97
|
+
"schedule.run": "ask",
|
|
98
|
+
"schedule.exec": "ask",
|
|
99
|
+
"doctor.report.create": "ask",
|
|
82
100
|
};
|
|
101
|
+
/** Commands under a housekeeping word, or store maintenance, that still have a key of their own: their path. */
|
|
102
|
+
const KEYED_UNDER_HOUSEKEEPING = new Set(Object.keys(DEFAULT_PERMISSIONS).filter((key) => /^(store|server|models|images|schedule|doctor)\./.test(key)));
|
|
83
103
|
/** Lets an MCP server offer the commands that change what a later run may do (`grantsAuthority`). */
|
|
84
104
|
export const AUTHORITY_KEY = "mcp.authority";
|
|
85
105
|
/** Whether these levels let an agent run a command that changes what a later run may do; `ask` does, as for any write. */
|
|
@@ -109,6 +129,11 @@ export const RESOURCES = [
|
|
|
109
129
|
"store",
|
|
110
130
|
"metadata",
|
|
111
131
|
"mcp",
|
|
132
|
+
"server",
|
|
133
|
+
"models",
|
|
134
|
+
"schedule",
|
|
135
|
+
"doctor",
|
|
136
|
+
"images",
|
|
112
137
|
];
|
|
113
138
|
const OLD_WORDS = {
|
|
114
139
|
send: ["messages.send", "polls.create", "chats.start"],
|
|
@@ -375,7 +400,7 @@ const HOUSEKEEPING = new Set([
|
|
|
375
400
|
]);
|
|
376
401
|
/** Under `bot`: its token, its names, its lists and its MCP server. */
|
|
377
402
|
const BOT_HOUSEKEEPING = new Set(["auth", "list", "sends", "recipients", "mcp"]);
|
|
378
|
-
const STORE_MAINTENANCE = new Set(["info", "check", "
|
|
403
|
+
const STORE_MAINTENANCE = new Set(["info", "check", "copies"]);
|
|
379
404
|
/** Commands outside `messages` that print what people wrote, so `deny messages` reaches them too. */
|
|
380
405
|
const SHOW_MESSAGES = new Set(["inbox", "review", "watch", "serve", "store", "conversations"]);
|
|
381
406
|
/** Before every search moved under `search` (2026-10-09). */
|
|
@@ -405,6 +430,9 @@ export const readKeysForCommand = (path) => {
|
|
|
405
430
|
return [key, "messages"];
|
|
406
431
|
if (path[0] === "store" && path[1] === "gaps" && path[2] === "repair")
|
|
407
432
|
return [key, "messages"];
|
|
433
|
+
// Checked as `messages` before they had keys of their own: `deny messages` keeps stopping them.
|
|
434
|
+
if (path[0] === "store" && ["reset", "clear", "decrypt"].includes(path[1] ?? ""))
|
|
435
|
+
return [key, "messages"];
|
|
408
436
|
// A search shows what people wrote, so `deny messages` reaches every leaf that reads messages.
|
|
409
437
|
if (path[0] === "search") {
|
|
410
438
|
if (path[1] === "notes")
|
|
@@ -438,6 +466,8 @@ export const keyForCommand = (path) => {
|
|
|
438
466
|
const inner = keyForCommand(path.slice(1));
|
|
439
467
|
return `bot.${inner ?? path.slice(1).join(".")}`;
|
|
440
468
|
}
|
|
469
|
+
if (KEYED_UNDER_HOUSEKEEPING.has(path.join(".")))
|
|
470
|
+
return path.join(".");
|
|
441
471
|
if (top === undefined || HOUSEKEEPING.has(top) || (top === "search" && next === "runs"))
|
|
442
472
|
return null;
|
|
443
473
|
// Picture models moved from `models image` (0.270.0); a model is housekeeping wherever it is listed.
|
package/dist/sends/upload.d.ts
CHANGED
|
@@ -18,12 +18,12 @@ export declare const readUpload: (kind: UploadKind, path: string, { app, env, an
|
|
|
18
18
|
photoTypes?: readonly string[];
|
|
19
19
|
}) => Promise<Upload>;
|
|
20
20
|
/**
|
|
21
|
-
* What a send attaches, as `messages send` and its tool take it.
|
|
22
|
-
* messengers' own apps send one.
|
|
21
|
+
* What a send attaches, as `messages send` and its tool take it. More than one photo or file is an album: photos
|
|
22
|
+
* first, then files, each in the order given. A voice message goes alone, as the messengers' own apps send one.
|
|
23
23
|
*/
|
|
24
24
|
export declare const readAttachments: ({ photo, file, voice, asFile, filename, text, }: {
|
|
25
|
-
photo?: string;
|
|
26
|
-
file?: string;
|
|
25
|
+
photo?: string | readonly string[];
|
|
26
|
+
file?: string | readonly string[];
|
|
27
27
|
voice?: string;
|
|
28
28
|
asFile?: boolean;
|
|
29
29
|
filename?: string;
|
package/dist/sends/upload.js
CHANGED
|
@@ -197,37 +197,41 @@ export const readUpload = async (kind, path, { app, env = process.env, anyFile =
|
|
|
197
197
|
}
|
|
198
198
|
return { kind, name: basename(path), source: { kind: "file", path: resolve(path), size } };
|
|
199
199
|
};
|
|
200
|
-
const checkFilename = (filename,
|
|
201
|
-
if (
|
|
200
|
+
const checkFilename = (filename, files) => {
|
|
201
|
+
if (files.length === 0)
|
|
202
202
|
throw new CliError("validation_error", "--filename names a --file; give one");
|
|
203
|
+
if (files.length > 1)
|
|
204
|
+
throw new CliError("validation_error", "--filename names one --file; an album keeps its names");
|
|
203
205
|
if (filename.trim() === "" || /[/\\\0]/.test(filename) || filename.length > 255)
|
|
204
206
|
throw new CliError("validation_error", "--filename is a name, not a path: no / or \\, at most 255 characters");
|
|
205
207
|
};
|
|
208
|
+
const listOf = (value) => value === undefined ? [] : typeof value === "string" ? [value] : value;
|
|
206
209
|
/**
|
|
207
|
-
* What a send attaches, as `messages send` and its tool take it.
|
|
208
|
-
* messengers' own apps send one.
|
|
210
|
+
* What a send attaches, as `messages send` and its tool take it. More than one photo or file is an album: photos
|
|
211
|
+
* first, then files, each in the order given. A voice message goes alone, as the messengers' own apps send one.
|
|
209
212
|
*/
|
|
210
213
|
export const readAttachments = async ({ photo, file, voice, asFile, filename, text, }, read) => {
|
|
211
|
-
|
|
214
|
+
const photos = listOf(photo);
|
|
215
|
+
const files = listOf(file);
|
|
216
|
+
if (voice !== undefined && (photos.length > 0 || files.length > 0 || (text ?? "").trim() !== "")) {
|
|
212
217
|
throw new CliError("validation_error", "a voice message goes alone — no text, no file, no photo beside it");
|
|
213
218
|
}
|
|
214
|
-
if (asFile &&
|
|
219
|
+
if (asFile && files.length === 0)
|
|
215
220
|
throw new CliError("validation_error", "--as-file is about a --file; give one");
|
|
216
221
|
if (filename !== undefined)
|
|
217
|
-
checkFilename(filename,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
];
|
|
222
|
+
checkFilename(filename, files);
|
|
223
|
+
const uploads = [];
|
|
224
|
+
for (const one of photos)
|
|
225
|
+
uploads.push(await readUpload("photo", one, read));
|
|
226
|
+
for (const one of files)
|
|
227
|
+
uploads.push({
|
|
228
|
+
...(await readUpload("file", one, read)),
|
|
229
|
+
...(filename === undefined ? {} : { name: filename }),
|
|
230
|
+
...(asFile ? { asFile: true } : {}),
|
|
231
|
+
});
|
|
232
|
+
if (voice !== undefined)
|
|
233
|
+
uploads.push(await readUpload("voice", voice, read));
|
|
234
|
+
return uploads;
|
|
231
235
|
};
|
|
232
236
|
/** A sticker to send from a file: a WebP image of at most `maxBytes`, behind the same file guard as `--file`. */
|
|
233
237
|
export const readStickerFile = async (path, maxBytes, read) => {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { DocumentTarget, Keep, PushStatePort } from "../../documents/push-model.js";
|
|
2
|
+
import { type PushConflict, type PushOptions } from "./push.js";
|
|
3
|
+
/** A local folder mirrored into a remote folder, as the app's profile config keeps it. */
|
|
4
|
+
export interface Mirror {
|
|
5
|
+
id: string;
|
|
6
|
+
path: string;
|
|
7
|
+
folder: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const mirrorId: (root: string, folder: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
* A mirror's sync state in `<dir>/<id>.json`. A missing file is a first sync; an unreadable or invalid one is
|
|
12
|
+
* refused rather than reset, since a reset would push every file again as new.
|
|
13
|
+
*/
|
|
14
|
+
export declare const fileMirrorState: <S>(dir: string, mirror: Mirror, principal: string, kinds: [string, ...string[]]) => PushStatePort<S>;
|
|
15
|
+
/** What the next sync of each mirror would create or send, from local files and the state alone. */
|
|
16
|
+
export declare const mirrorStatus: <S>(mirrors: Mirror[], kindOf: (path: string) => string | undefined, stateOf: (mirror: Mirror) => PushStatePort<S>) => Promise<{
|
|
17
|
+
id: string;
|
|
18
|
+
path: string;
|
|
19
|
+
folder: string;
|
|
20
|
+
create: string[];
|
|
21
|
+
update: string[];
|
|
22
|
+
unchanged: number;
|
|
23
|
+
orphans: string[];
|
|
24
|
+
unsupported: number;
|
|
25
|
+
remote_checked: boolean;
|
|
26
|
+
}[]>;
|
|
27
|
+
/** A conflict as the user reads it, with the remote side named the way the app names it (`google`). */
|
|
28
|
+
export declare const conflictReport: (conflict: PushConflict, remote: string) => {
|
|
29
|
+
path: string;
|
|
30
|
+
url: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
parts?: {
|
|
33
|
+
[x: string]: string;
|
|
34
|
+
last_sync: string;
|
|
35
|
+
local: string;
|
|
36
|
+
}[] | undefined;
|
|
37
|
+
local_changed_at?: string | undefined;
|
|
38
|
+
};
|
|
39
|
+
export interface MirrorSyncOptions<S> extends Pick<PushOptions<S>, "principal" | "recreate" | "guard" | "operationId" | "errorOf" | "signal"> {
|
|
40
|
+
mirrors: Mirror[];
|
|
41
|
+
target: DocumentTarget<S>;
|
|
42
|
+
stateOf(mirror: Mirror): PushStatePort<S>;
|
|
43
|
+
keep?: Keep;
|
|
44
|
+
/** Taken once for all the mirrors, so a second sync or a watcher's next cycle waits its turn. */
|
|
45
|
+
lock(): Promise<{
|
|
46
|
+
assertHeld(): void;
|
|
47
|
+
release(): Promise<void> | void;
|
|
48
|
+
}>;
|
|
49
|
+
/** How the app names the remote side in conflict reports. */
|
|
50
|
+
remote: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Syncs each mirror both ways. A mirror with conflicts, missing remote files or errors does not stop the others;
|
|
54
|
+
* it gets one `mirror.partial` issue, so the caller can answer `complete: false` and fail after printing.
|
|
55
|
+
*/
|
|
56
|
+
export declare const syncMirrors: <S>(options: MirrorSyncOptions<S>) => Promise<{
|
|
57
|
+
items: {
|
|
58
|
+
created: string[];
|
|
59
|
+
updated: string[];
|
|
60
|
+
pulled: string[];
|
|
61
|
+
unchanged: number;
|
|
62
|
+
missing: string[];
|
|
63
|
+
orphans: {
|
|
64
|
+
path: string;
|
|
65
|
+
url: string;
|
|
66
|
+
}[];
|
|
67
|
+
errors: {
|
|
68
|
+
path: string;
|
|
69
|
+
code: string;
|
|
70
|
+
message: string;
|
|
71
|
+
}[];
|
|
72
|
+
unsupported: number;
|
|
73
|
+
plan?: {
|
|
74
|
+
create: string[];
|
|
75
|
+
update: string[];
|
|
76
|
+
orphans: string[];
|
|
77
|
+
};
|
|
78
|
+
stopped?: true;
|
|
79
|
+
id: string;
|
|
80
|
+
path: string;
|
|
81
|
+
conflicts: {
|
|
82
|
+
path: string;
|
|
83
|
+
url: string;
|
|
84
|
+
reason: string;
|
|
85
|
+
parts?: {
|
|
86
|
+
[x: string]: string;
|
|
87
|
+
last_sync: string;
|
|
88
|
+
local: string;
|
|
89
|
+
}[] | undefined;
|
|
90
|
+
local_changed_at?: string | undefined;
|
|
91
|
+
}[];
|
|
92
|
+
}[];
|
|
93
|
+
issues: {
|
|
94
|
+
code: "provider_error";
|
|
95
|
+
reason: string;
|
|
96
|
+
source: string;
|
|
97
|
+
}[];
|
|
98
|
+
}>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { CliError, writeSecurely } from "@wirecat/cli-base";
|
|
5
|
+
import * as v from "valibot";
|
|
6
|
+
import { planPush, pushDocuments, scanLocal } from "./push.js";
|
|
7
|
+
export const mirrorId = (root, folder) => createHash("sha256")
|
|
8
|
+
.update(JSON.stringify([root, folder]))
|
|
9
|
+
.digest("hex")
|
|
10
|
+
.slice(0, 8);
|
|
11
|
+
const ID = v.pipe(v.string(), v.regex(/^[A-Za-z0-9_-]{1,200}$/));
|
|
12
|
+
const stateSchema = (kinds) => v.object({
|
|
13
|
+
version: v.literal(1),
|
|
14
|
+
principal: v.string(),
|
|
15
|
+
root: v.string(),
|
|
16
|
+
target: ID,
|
|
17
|
+
folders: v.record(v.string(), ID),
|
|
18
|
+
entries: v.record(v.string(), v.object({
|
|
19
|
+
id: ID,
|
|
20
|
+
url: v.string(),
|
|
21
|
+
kind: v.string(),
|
|
22
|
+
hash: v.string(),
|
|
23
|
+
// Checked by kind only: the rest is the provider's own read-back.
|
|
24
|
+
snapshot: v.looseObject({ kind: v.picklist(kinds) }),
|
|
25
|
+
pushedAt: v.string(),
|
|
26
|
+
})),
|
|
27
|
+
pending: v.record(v.string(), v.object({ origin: v.string(), operationId: v.string() })),
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* A mirror's sync state in `<dir>/<id>.json`. A missing file is a first sync; an unreadable or invalid one is
|
|
31
|
+
* refused rather than reset, since a reset would push every file again as new.
|
|
32
|
+
*/
|
|
33
|
+
export const fileMirrorState = (dir, mirror, principal, kinds) => {
|
|
34
|
+
const path = join(dir, `${mirror.id}.json`);
|
|
35
|
+
const schema = stateSchema(kinds);
|
|
36
|
+
return {
|
|
37
|
+
read: () => {
|
|
38
|
+
let value;
|
|
39
|
+
try {
|
|
40
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code === "ENOENT")
|
|
44
|
+
return {
|
|
45
|
+
version: 1,
|
|
46
|
+
principal,
|
|
47
|
+
root: mirror.path,
|
|
48
|
+
target: mirror.folder,
|
|
49
|
+
folders: {},
|
|
50
|
+
entries: {},
|
|
51
|
+
pending: {},
|
|
52
|
+
};
|
|
53
|
+
throw new CliError("configuration_error", "mirror state is unreadable; refusing to reset it", {
|
|
54
|
+
reason: "state.invalid",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const parsed = v.safeParse(schema, value);
|
|
58
|
+
if (!parsed.success)
|
|
59
|
+
throw new CliError("configuration_error", "mirror state is invalid; refusing to reset it", {
|
|
60
|
+
reason: "state.invalid",
|
|
61
|
+
});
|
|
62
|
+
return parsed.output;
|
|
63
|
+
},
|
|
64
|
+
save: (state) => {
|
|
65
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
66
|
+
writeSecurely(path, `${JSON.stringify(state)}\n`, 0o600);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/** What the next sync of each mirror would create or send, from local files and the state alone. */
|
|
71
|
+
export const mirrorStatus = async (mirrors, kindOf, stateOf) => {
|
|
72
|
+
const items = [];
|
|
73
|
+
for (const mirror of mirrors) {
|
|
74
|
+
const { files, unsupported } = await scanLocal(mirror.path, kindOf);
|
|
75
|
+
const plan = planPush(files, stateOf(mirror).read());
|
|
76
|
+
items.push({
|
|
77
|
+
id: mirror.id,
|
|
78
|
+
path: mirror.path,
|
|
79
|
+
folder: mirror.folder,
|
|
80
|
+
create: plan.create.map((file) => file.path),
|
|
81
|
+
update: plan.update.map((file) => file.path),
|
|
82
|
+
unchanged: plan.unchanged.length,
|
|
83
|
+
orphans: plan.orphans,
|
|
84
|
+
unsupported,
|
|
85
|
+
remote_checked: false,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return items;
|
|
89
|
+
};
|
|
90
|
+
/** A conflict as the user reads it, with the remote side named the way the app names it (`google`). */
|
|
91
|
+
export const conflictReport = (conflict, remote) => ({
|
|
92
|
+
path: conflict.path,
|
|
93
|
+
url: conflict.url,
|
|
94
|
+
reason: conflict.reason,
|
|
95
|
+
...(conflict.parts
|
|
96
|
+
? { parts: conflict.parts.map((part) => ({ last_sync: part.base, local: part.local, [remote]: part.remote })) }
|
|
97
|
+
: {}),
|
|
98
|
+
...(conflict.localChangedAt ? { local_changed_at: conflict.localChangedAt } : {}),
|
|
99
|
+
...(conflict.remoteChangedAt ? { [`${remote}_changed_at`]: conflict.remoteChangedAt } : {}),
|
|
100
|
+
...(conflict.remoteChangedBy ? { [`${remote}_changed_by`]: conflict.remoteChangedBy } : {}),
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* Syncs each mirror both ways. A mirror with conflicts, missing remote files or errors does not stop the others;
|
|
104
|
+
* it gets one `mirror.partial` issue, so the caller can answer `complete: false` and fail after printing.
|
|
105
|
+
*/
|
|
106
|
+
export const syncMirrors = async (options) => {
|
|
107
|
+
const { mirrors, keep, remote } = options;
|
|
108
|
+
const lock = await options.lock();
|
|
109
|
+
const results = [];
|
|
110
|
+
try {
|
|
111
|
+
for (const mirror of mirrors)
|
|
112
|
+
results.push({
|
|
113
|
+
id: mirror.id,
|
|
114
|
+
path: mirror.path,
|
|
115
|
+
...(await pushDocuments({
|
|
116
|
+
root: mirror.path,
|
|
117
|
+
folder: mirror.folder,
|
|
118
|
+
target: options.target,
|
|
119
|
+
state: options.stateOf(mirror),
|
|
120
|
+
principal: options.principal,
|
|
121
|
+
...(keep ? { keep } : {}),
|
|
122
|
+
recreate: options.recreate === true,
|
|
123
|
+
guard: options.guard,
|
|
124
|
+
operationId: options.operationId,
|
|
125
|
+
errorOf: options.errorOf,
|
|
126
|
+
assertHeld: () => lock.assertHeld(),
|
|
127
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
128
|
+
})),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await lock.release();
|
|
133
|
+
}
|
|
134
|
+
const issues = results
|
|
135
|
+
.filter((result) => result.errors.length || result.conflicts.length || result.missing.length)
|
|
136
|
+
.map((result) => ({ code: "provider_error", reason: "mirror.partial", source: `mirror ${result.id}` }));
|
|
137
|
+
return {
|
|
138
|
+
items: results.map(({ conflicts, ...result }) => ({
|
|
139
|
+
...result,
|
|
140
|
+
conflicts: conflicts.map((conflict) => conflictReport(conflict, remote)),
|
|
141
|
+
})),
|
|
142
|
+
issues,
|
|
143
|
+
};
|
|
144
|
+
};
|
|
@@ -283,6 +283,7 @@ export const messagesService = (deps) => {
|
|
|
283
283
|
sendId: id,
|
|
284
284
|
operationId: id,
|
|
285
285
|
length: text.length,
|
|
286
|
+
...(attachments.length > 1 ? { count: attachments.length } : {}),
|
|
286
287
|
...(replyTo === undefined ? {} : { replyTo }),
|
|
287
288
|
...(threadId === undefined ? {} : { threadId }),
|
|
288
289
|
...(sendAs === undefined ? {} : { sendAs }),
|
|
@@ -4,6 +4,7 @@ import { formatMeetingReference } from "../meetings/index.js";
|
|
|
4
4
|
import { entriesOfList, fitting } from "../search/account-list.js";
|
|
5
5
|
import { SEARCH_QUERY } from "../search/lucene/types.js";
|
|
6
6
|
import { codeOf } from "../sends/guarded.js";
|
|
7
|
+
import { MAX_SEARCH_OFFSET } from "./messages-archive-search.js";
|
|
7
8
|
import { RRF_K } from "./notes-search.js";
|
|
8
9
|
import { RESOURCES_SEARCHED, searchAll, } from "./search-all.js";
|
|
9
10
|
import { listContextOf } from "./search-kind.js";
|
|
@@ -139,26 +140,35 @@ export const searchAllIncludingMeetings = async (store, account, request, messen
|
|
|
139
140
|
whole(request.meetingCursor.hitOffset, 0, Number.MAX_SAFE_INTEGER, "cursor hit offset");
|
|
140
141
|
}
|
|
141
142
|
cancelled(request.signal);
|
|
142
|
-
const
|
|
143
|
+
const others = { ...request, only: only.filter((one) => one !== "meetings") };
|
|
144
|
+
if (!only.includes("meetings")) {
|
|
145
|
+
const found = await searchAll(store, account, others, messenger, searchMessages);
|
|
146
|
+
cancelled(request.signal);
|
|
147
|
+
return found;
|
|
148
|
+
}
|
|
149
|
+
// Meetings rank with the rest, so a later page is the same merge read deeper, then cut.
|
|
150
|
+
const offset = request.offset ?? 0;
|
|
151
|
+
const window = offset + request.limit;
|
|
152
|
+
const legacy = await searchAll(store, account, { ...others, offset: 0, limit: window }, messenger, searchMessages);
|
|
143
153
|
cancelled(request.signal);
|
|
144
|
-
if (!only.includes("meetings"))
|
|
145
|
-
return legacy;
|
|
146
154
|
const compiled = meetingQuery(request.text, request.exact);
|
|
147
155
|
if ("reason" in compiled)
|
|
148
156
|
return {
|
|
149
157
|
...legacy,
|
|
158
|
+
items: legacy.items.slice(offset),
|
|
150
159
|
skipped: [...legacy.skipped, { resource: "meetings", code: "validation_error", reason: compiled.reason }],
|
|
151
160
|
};
|
|
152
161
|
let meetings;
|
|
153
162
|
const meetingAccount = request.meetingAccount ?? account;
|
|
154
163
|
try {
|
|
155
|
-
meetings = await collectMeetings(store, meetingAccount, request, compiled.query);
|
|
164
|
+
meetings = await collectMeetings(store, meetingAccount, { ...request, limit: window }, compiled.query);
|
|
156
165
|
}
|
|
157
166
|
catch (error) {
|
|
158
167
|
if (codeOf(error) !== "not_found")
|
|
159
168
|
throw error;
|
|
160
169
|
return {
|
|
161
170
|
...legacy,
|
|
171
|
+
items: legacy.items.slice(offset),
|
|
162
172
|
skipped: [
|
|
163
173
|
...legacy.skipped,
|
|
164
174
|
{ resource: "meetings", code: "not_found", reason: "the selected meeting account is not stored" },
|
|
@@ -176,13 +186,16 @@ export const searchAllIncludingMeetings = async (store, account, request, messen
|
|
|
176
186
|
ranked.set(key, held);
|
|
177
187
|
});
|
|
178
188
|
const ordered = [...ranked.values()].sort((a, b) => b.score - a.score).map(({ item }) => item);
|
|
179
|
-
const items = ordered.slice(
|
|
180
|
-
const returnedMeetings =
|
|
189
|
+
const items = ordered.slice(offset, window);
|
|
190
|
+
const returnedMeetings = ordered.slice(0, window).filter((item) => item.kind === "meeting").length;
|
|
181
191
|
const nextCursor = meetings.items[returnedMeetings]?.before ?? (!meetings.coverage.complete ? meetings.after : undefined);
|
|
192
|
+
const more = legacy.hasMore || ordered.length > window;
|
|
193
|
+
const { nextOffset: _, ...rest } = legacy;
|
|
182
194
|
return {
|
|
183
|
-
...
|
|
195
|
+
...rest,
|
|
184
196
|
items,
|
|
185
|
-
hasMore:
|
|
197
|
+
hasMore: more ? true : meetings.coverage.complete ? false : null,
|
|
198
|
+
...(more && window <= MAX_SEARCH_OFFSET ? { nextOffset: window } : {}),
|
|
186
199
|
searched: [...legacy.searched, "meetings"],
|
|
187
200
|
accounts: [
|
|
188
201
|
...legacy.accounts,
|
|
@@ -5,7 +5,7 @@ import { type TaskCategory } from "./task-categories.js";
|
|
|
5
5
|
export interface RuleResult {
|
|
6
6
|
added: number;
|
|
7
7
|
closed: number;
|
|
8
|
-
/** Opened tasks that may be the same job as another: on the review list (`tasks duplicates`). */
|
|
8
|
+
/** Opened tasks that may be the same job as another: on the review list (`tasks duplicates list`). */
|
|
9
9
|
review: number;
|
|
10
10
|
/** Tasks the rules found but did not open: over the per-chat limit, or a dry run. */
|
|
11
11
|
held?: number;
|
package/dist/services/tasks.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare const TASK_QUERY: {
|
|
|
26
26
|
readonly backend: "sql";
|
|
27
27
|
readonly fields: import("@wirecat/cli-base/query").FieldSpec[];
|
|
28
28
|
};
|
|
29
|
-
/** Which pairs `tasks duplicates` lists: those waiting for the reviewing agent, or those it left to the owner. */
|
|
29
|
+
/** Which pairs `tasks duplicates list` lists: those waiting for the reviewing agent, or those it left to the owner. */
|
|
30
30
|
export declare const PAIR_DECISIONS: readonly ["review", "unsure"];
|
|
31
31
|
export type PairDecision = (typeof PAIR_DECISIONS)[number];
|
|
32
32
|
export declare const TASK_PAIR_QUERY: {
|
|
@@ -168,7 +168,7 @@ export interface TasksService {
|
|
|
168
168
|
/**
|
|
169
169
|
* Links a task to stored things (`msg:`, `chat:`, `person:`, `document:`…) or, for what the store does
|
|
170
170
|
* not hold, to the text as written — a URL, a message not saved yet. After a `msg:` target, `review` counts the
|
|
171
|
-
* pairs the duplicate check sent to `tasks duplicates`.
|
|
171
|
+
* pairs the duplicate check sent to `tasks duplicates list`.
|
|
172
172
|
*/
|
|
173
173
|
link(id: string, targets: {
|
|
174
174
|
references?: readonly string[];
|