@wirecat/cli-core 0.531.0 → 0.532.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 +15 -0
- package/dist/cli/messenger/attachments-command.d.ts +1 -0
- package/dist/cli/messenger/attachments-command.js +1 -1
- package/dist/cli/messenger/download-command.js +37 -10
- package/dist/cli/messenger/search-answers.js +7 -2
- package/dist/domain/attachments.d.ts +9 -1
- package/dist/domain/attachments.js +12 -0
- package/dist/domain/message-link.d.ts +14 -0
- package/dist/domain/message-link.js +8 -0
- package/dist/entities/attachment.d.ts +7 -0
- package/dist/entities/attachment.js +2 -0
- package/dist/entities/hit.d.ts +7 -0
- package/dist/entities/hit.js +3 -0
- package/dist/entities/index.d.ts +20 -0
- package/dist/entities/index.js +3 -1
- package/dist/entities/message-link.d.ts +8 -0
- package/dist/entities/message-link.js +9 -1
- package/dist/services/attachments.d.ts +11 -0
- package/dist/services/attachments.js +45 -2
- package/dist/services/deps.d.ts +1 -1
- package/dist/services/deps.js +1 -1
- package/dist/services/messages.d.ts +2 -3
- package/dist/services/messages.js +27 -8
- package/dist/services/search-all.d.ts +3 -0
- package/dist/services/search-all.js +2 -0
- package/package.json +1 -1
- package/parity.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.532.0 — 01.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A message hit names its files**: `search messages` and `search all` hits carry
|
|
15
|
+
`attachments: [{ position, kind, name, size }]` (files only, absent when there are none; `name` and `size` when the
|
|
16
|
+
store has them). `position` counts every attachment from 1, as `attachments show --attachment` takes it.
|
|
17
|
+
`attachments.name` is listed in the hit's `untrusted` fields.
|
|
18
|
+
- **`messages download --offline`** copies files the local store already holds, by chat and id or by a `msg:`
|
|
19
|
+
locator, and never connects; `--attachment <n>` picks one. A file not saved here fails with `not_found` before
|
|
20
|
+
anything is written, and the store keeps pointing at its own copy.
|
|
21
|
+
- **`messages download` takes a `msg:` locator** in place of chat and id; the chat and id form still works.
|
|
22
|
+
- **`messages link` lists a message's files**: `files: [{ position, kind, name, url, reason }]`. No messenger gives
|
|
23
|
+
a file its own link yet, so `url` is null with reason `unsupported_provider`.
|
|
24
|
+
|
|
10
25
|
## 0.531.0 — 01.11.2026
|
|
11
26
|
|
|
12
27
|
### Added
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { type Messenger } from "./context.js";
|
|
3
|
+
export declare const count: (value: string) => number;
|
|
3
4
|
/** Files of stored messages: what is in them, for `content:` in a search. Nothing here is sent. */
|
|
4
5
|
export declare const attachmentsCommand: (messenger: Messenger) => Command;
|
|
@@ -18,7 +18,7 @@ import { messengerContext, refuseLocalWrite } from "./context.js";
|
|
|
18
18
|
import { addMediaArchiveCommands } from "./media-archive-command.js";
|
|
19
19
|
import { stopOnSignal } from "./patience.js";
|
|
20
20
|
import { readAll } from "./stdin.js";
|
|
21
|
-
const count = (value) => {
|
|
21
|
+
export const count = (value) => {
|
|
22
22
|
const parsed = Number(value);
|
|
23
23
|
if (!Number.isInteger(parsed) || parsed < 1)
|
|
24
24
|
throw new CliError("validation_error", `"${value}" is not a count`);
|
|
@@ -4,6 +4,8 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
4
4
|
import { CliError, writeSecurely } from "@wirecat/cli-base";
|
|
5
5
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
6
6
|
import { NOT_FILES } from "../../domain/attachments.js";
|
|
7
|
+
import { isLocator } from "../../domain/locator.js";
|
|
8
|
+
import { messageLinkTarget } from "../../domain/message-link.js";
|
|
7
9
|
import { FETCHING, keyOf } from "../../services/archive.js";
|
|
8
10
|
import { batchProgress, mergeBatches } from "../../services/batch.js";
|
|
9
11
|
import { saveFiles } from "../../services/file-download.js";
|
|
@@ -15,25 +17,45 @@ import { patiently } from "../../services/patience.js";
|
|
|
15
17
|
import { FETCH } from "../effects.js";
|
|
16
18
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
17
19
|
import { nextStep } from "../steps.js";
|
|
20
|
+
import { count } from "./attachments-command.js";
|
|
18
21
|
import { messengerContext, refuseLocalWrite } from "./context.js";
|
|
19
22
|
import { stopOnSignal } from "./patience.js";
|
|
20
23
|
/** `messages download`: every file of one message, or with `--all` of a whole chat, never over a file already there. */
|
|
21
24
|
export const downloadSubcommand = (messages, messenger) => annotate(messages.command("download"), {
|
|
22
25
|
output: { entity: "message_download" },
|
|
23
|
-
examples: [
|
|
26
|
+
examples: [
|
|
27
|
+
`${messenger.app.command} personal messages download "Book club" 101 --output-dir ./downloads`,
|
|
28
|
+
`${messenger.app.command} personal messages download msg:${messenger.provider}/me/500/101 --attachment 1 --offline`,
|
|
29
|
+
],
|
|
24
30
|
effects: FETCH,
|
|
25
31
|
})
|
|
26
32
|
.summary("download files attached to stored messages")
|
|
27
|
-
.description("save a message's photos, files, videos and voice notes to a folder — or a whole chat's with --all"
|
|
28
|
-
|
|
29
|
-
.argument("
|
|
33
|
+
.description("save a message's photos, files, videos and voice notes to a folder — or a whole chat's with --all; " +
|
|
34
|
+
"with --offline, copy the ones this computer already holds, one by --attachment")
|
|
35
|
+
.argument("<chat>", `${messenger.chatArgument}; or a msg: locator, with no message id after it`)
|
|
36
|
+
.argument("[message]", "the message id; left out with --all or a locator")
|
|
30
37
|
.option("--output-dir <dir>", "where to save them; created if missing", ".")
|
|
38
|
+
.option("--attachment <n>", "which file of the message, from 1; needed when it has more than one", count)
|
|
31
39
|
.option("--all", "every file of the chat, newest first; run it again to continue where it stopped")
|
|
32
40
|
.option("--pause <duration>", "with --all, a pause between pages, to stay under the provider's limits", messenger.fetching?.pause ?? FETCHING.pause)
|
|
33
41
|
.option("--extract", "read text layers from the files this download maps into the local content index")
|
|
34
42
|
.action(async function (chat, messageId) {
|
|
35
43
|
const context = messengerContext(this, messenger);
|
|
36
|
-
const { outputDir: output, all, pause, extract, } = this.opts();
|
|
44
|
+
const { outputDir: output, all, pause, extract, attachment, } = this.opts();
|
|
45
|
+
if (context.settings.offline) {
|
|
46
|
+
if (all || extract)
|
|
47
|
+
throw new CliError("validation_error", "--offline copies one message's saved files; leave out --all and --extract");
|
|
48
|
+
const saved = await context.withServices((services) => services.attachments.saveHeld({
|
|
49
|
+
chat,
|
|
50
|
+
...(messageId === undefined ? {} : { message: messageId.trim() }),
|
|
51
|
+
...(attachment === undefined ? {} : { attachment }),
|
|
52
|
+
output,
|
|
53
|
+
}));
|
|
54
|
+
renderAnswer(this, context, itemOf(snakeKeys({ files: saved })), () => `${saved.map((one) => one.path).join("\n")}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (attachment !== undefined)
|
|
58
|
+
throw new CliError("validation_error", "--attachment picks a file this computer already holds; add --offline");
|
|
37
59
|
if (extract)
|
|
38
60
|
refuseLocalWrite(context, messenger.app.command, "attachments.extract");
|
|
39
61
|
if (all && messageId !== undefined) {
|
|
@@ -43,18 +65,19 @@ export const downloadSubcommand = (messages, messenger) => annotate(messages.com
|
|
|
43
65
|
await downloadChat(this, context, messenger, chat, output, parseDuration(pause, "--pause"), extract);
|
|
44
66
|
return;
|
|
45
67
|
}
|
|
46
|
-
if (messageId === undefined)
|
|
68
|
+
if (messageId === undefined && !isLocator(chat))
|
|
47
69
|
throw new CliError("validation_error", "name a message id, or use --all");
|
|
48
|
-
const
|
|
70
|
+
const target = messageLinkTarget(chat, messageId?.trim(), messenger.provider);
|
|
71
|
+
const id = target.message;
|
|
49
72
|
const result = await context.withServices(async (services) => {
|
|
50
|
-
const { files, skipped } = await services.messages.download(chat,
|
|
73
|
+
const { files, skipped } = await services.messages.download(chat, messageId?.trim());
|
|
51
74
|
if (skipped.length > 0)
|
|
52
75
|
context.renderer.note(`not a file, not downloaded: ${skipped.join(", ")}`);
|
|
53
76
|
if (files.length === 0)
|
|
54
77
|
throw new CliError("not_found", `message ${id} has no file to download`);
|
|
55
78
|
mkdirSync(output, { recursive: true });
|
|
56
79
|
const progress = batchProgress(context.env);
|
|
57
|
-
const downloaded = await saveFiles(services.messages, chat, id, files, output, {
|
|
80
|
+
const downloaded = await saveFiles(services.messages, target.chat, id, files, output, {
|
|
58
81
|
warn: context.renderer.warn,
|
|
59
82
|
batch: progress,
|
|
60
83
|
});
|
|
@@ -62,7 +85,11 @@ export const downloadSubcommand = (messages, messenger) => annotate(messages.com
|
|
|
62
85
|
let extraction;
|
|
63
86
|
if (extract) {
|
|
64
87
|
try {
|
|
65
|
-
extraction = await services.attachments.extract({
|
|
88
|
+
extraction = await services.attachments.extract({
|
|
89
|
+
chat: target.chat,
|
|
90
|
+
message: id,
|
|
91
|
+
paths: done.map((file) => file.path),
|
|
92
|
+
});
|
|
66
93
|
}
|
|
67
94
|
catch (error) {
|
|
68
95
|
const issue = progress.fail(id, "extract", error);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hitFiles } from "../../domain/attachments.js";
|
|
1
2
|
import { formatLocator, parseLocator } from "../../domain/locator.js";
|
|
2
3
|
import { senderIsMe } from "../../domain/models.js";
|
|
3
4
|
import { DEFAULT_TEXT_MODEL } from "../../embeddings/models.js";
|
|
@@ -8,6 +9,7 @@ import { nextStep } from "../steps.js";
|
|
|
8
9
|
import { archiveSummary } from "./archive-summary.js";
|
|
9
10
|
/** What a source tool's `search all` reads: never mail, which is memo's (docs/dev/TOOL-BOUNDARIES.md). */
|
|
10
11
|
export const SOURCE_SEARCHED = ["messages", "notes", "events", "meetings"];
|
|
12
|
+
const nonEmpty = (list) => (list.length > 0 ? list : undefined);
|
|
11
13
|
const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, one]) => one !== undefined));
|
|
12
14
|
/** The page a `--page` asks for, as the offset the search services take. */
|
|
13
15
|
export const offsetOf = (page, limit) => page === undefined || page <= 1 ? undefined : (page - 1) * limit;
|
|
@@ -21,6 +23,7 @@ const MESSAGE_HIT_UNTRUSTED = [
|
|
|
21
23
|
"follow_ups.chat_title",
|
|
22
24
|
"follow_ups.sender_name",
|
|
23
25
|
"follow_ups.excerpt",
|
|
26
|
+
"attachments.name",
|
|
24
27
|
];
|
|
25
28
|
const MAIL_HIT_UNTRUSTED = [
|
|
26
29
|
"email.subject",
|
|
@@ -83,6 +86,7 @@ export const messageHitItem = (hit, query = "") => {
|
|
|
83
86
|
sender_name: one.senderName,
|
|
84
87
|
excerpt: excerpt(one.text, query),
|
|
85
88
|
})),
|
|
89
|
+
attachments: nonEmpty(hitFiles(hit.attachments)),
|
|
86
90
|
untrusted: hit.email ? [...MESSAGE_HIT_UNTRUSTED, ...MAIL_HIT_UNTRUSTED] : MESSAGE_HIT_UNTRUSTED,
|
|
87
91
|
});
|
|
88
92
|
};
|
|
@@ -266,8 +270,8 @@ const DOCUMENT_HIT_UNTRUSTED = ["title", "excerpt"];
|
|
|
266
270
|
export const noteHitItem = (hit) => defined({ ...hit, untrusted: hit.kind === "document" ? DOCUMENT_HIT_UNTRUSTED : undefined });
|
|
267
271
|
/** The text of each kind of `search all` hit that someone other than the owner may have written. */
|
|
268
272
|
const ALL_HIT_UNTRUSTED = {
|
|
269
|
-
message: ["title", "excerpt", "sender_name"],
|
|
270
|
-
mail: ["title", "excerpt", "sender_name"],
|
|
273
|
+
message: ["title", "excerpt", "sender_name", "attachments.name"],
|
|
274
|
+
mail: ["title", "excerpt", "sender_name", "attachments.name"],
|
|
271
275
|
document: DOCUMENT_HIT_UNTRUSTED,
|
|
272
276
|
note: undefined,
|
|
273
277
|
event: ["title", "excerpt"],
|
|
@@ -300,6 +304,7 @@ export const allHitItem = (item) => item.kind === "meeting"
|
|
|
300
304
|
sender_is_me: item.senderIsMe,
|
|
301
305
|
sender_ref: item.from?.ref,
|
|
302
306
|
found_by: item.foundBy,
|
|
307
|
+
attachments: item.attachments === undefined ? undefined : nonEmpty(item.attachments),
|
|
303
308
|
untrusted: ALL_HIT_UNTRUSTED[item.kind],
|
|
304
309
|
});
|
|
305
310
|
/** Meaning was not searched because the model is not downloaded; `--exact` leaving it out is no gap. */
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import type { ProviderMetadata } from "./models.js";
|
|
1
|
+
import type { Attachment, ProviderMetadata } from "./models.js";
|
|
2
2
|
/** Attachments that are never a file, so their messages cost no download request. */
|
|
3
3
|
export declare const NOT_FILES: ReadonlySet<string>;
|
|
4
|
+
/** A file a message carries, as a search hit names it; `position` counts every attachment, from 1. */
|
|
5
|
+
export interface HitFile {
|
|
6
|
+
position: number;
|
|
7
|
+
kind: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
size?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare const hitFiles: (attachments: readonly Attachment[] | undefined) => HitFile[];
|
|
4
12
|
/** The stored kinds `has:<kind>` matches: MAX stores a file as `file`, Telegram as `document`. */
|
|
5
13
|
export declare const storedKinds: (kind: string) => string[];
|
|
6
14
|
/** One file `messages download` saved, as the messenger described it. */
|
|
@@ -9,6 +9,18 @@ export const NOT_FILES = new Set([
|
|
|
9
9
|
"call",
|
|
10
10
|
"inline_keyboard",
|
|
11
11
|
]);
|
|
12
|
+
// The position is taken before the filter: the store numbers every attachment, and `--attachment` takes that number.
|
|
13
|
+
// A server search hit built by an adapter may come without attachments at all.
|
|
14
|
+
export const hitFiles = (attachments) => (attachments ?? []).flatMap(({ kind, name, size }, index) => NOT_FILES.has(kind)
|
|
15
|
+
? []
|
|
16
|
+
: [
|
|
17
|
+
{
|
|
18
|
+
position: index + 1,
|
|
19
|
+
kind,
|
|
20
|
+
...(name === undefined ? {} : { name }),
|
|
21
|
+
...(size === undefined ? {} : { size }),
|
|
22
|
+
},
|
|
23
|
+
]);
|
|
12
24
|
/** The stored kinds `has:<kind>` matches: MAX stores a file as `file`, Telegram as `document`. */
|
|
13
25
|
export const storedKinds = (kind) => (kind === "file" ? ["file", "document"] : [kind]);
|
|
14
26
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Attachment } from "./models.js";
|
|
1
2
|
export type MessagePermalink = {
|
|
2
3
|
url: string;
|
|
3
4
|
access: "public" | "restricted" | "unknown";
|
|
@@ -7,9 +8,22 @@ export type MessagePermalink = {
|
|
|
7
8
|
access: "unavailable";
|
|
8
9
|
reason: "unsupported_chat" | "unsupported_provider" | "offline";
|
|
9
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* A link to one file of the message, apart from the message's own. No messenger gives one yet: Telegram and
|
|
13
|
+
* WhatsApp have none, and MAX's are short-lived download addresses that hand the bytes to whoever holds them.
|
|
14
|
+
*/
|
|
15
|
+
export interface FileLink {
|
|
16
|
+
position: number;
|
|
17
|
+
kind: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
url: null;
|
|
20
|
+
reason: "unsupported_provider";
|
|
21
|
+
}
|
|
10
22
|
export type MessageLink = MessagePermalink & {
|
|
11
23
|
locator: string;
|
|
24
|
+
files?: FileLink[];
|
|
12
25
|
};
|
|
26
|
+
export declare const fileLinks: (attachments: readonly Attachment[]) => FileLink[];
|
|
13
27
|
export declare const messageLinkTarget: (reference: string, message: string | undefined, provider: string) => {
|
|
14
28
|
chat: string;
|
|
15
29
|
message: string;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { hitFiles } from "./attachments.js";
|
|
2
3
|
import { isLocator, parseLocator } from "./locator.js";
|
|
4
|
+
export const fileLinks = (attachments) => hitFiles(attachments).map(({ position, kind, name }) => ({
|
|
5
|
+
position,
|
|
6
|
+
kind,
|
|
7
|
+
...(name === undefined ? {} : { name }),
|
|
8
|
+
url: null,
|
|
9
|
+
reason: "unsupported_provider",
|
|
10
|
+
}));
|
|
3
11
|
export const messageLinkTarget = (reference, message, provider) => {
|
|
4
12
|
if (isLocator(reference)) {
|
|
5
13
|
if (message !== undefined)
|
|
@@ -39,3 +39,10 @@ export declare const attachment: v.StrictObjectSchema<{
|
|
|
39
39
|
readonly url: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
40
40
|
}, undefined>, undefined>, undefined>, undefined>;
|
|
41
41
|
}, undefined>;
|
|
42
|
+
/** A file named on a search hit: `position` is the one `attachments show` and `messages download` take. */
|
|
43
|
+
export declare const hitFile: v.StrictObjectSchema<{
|
|
44
|
+
readonly position: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
45
|
+
readonly kind: v.StringSchema<undefined>;
|
|
46
|
+
readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
47
|
+
readonly size: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
48
|
+
}, undefined>;
|
|
@@ -35,3 +35,5 @@ export const attachment = v.strictObject({
|
|
|
35
35
|
url: v.optional(text),
|
|
36
36
|
})))),
|
|
37
37
|
});
|
|
38
|
+
/** A file named on a search hit: `position` is the one `attachments show` and `messages download` take. */
|
|
39
|
+
export const hitFile = v.strictObject({ position: count, kind: text, name: v.optional(text), size: v.optional(count) });
|
package/dist/entities/hit.d.ts
CHANGED
|
@@ -24,6 +24,13 @@ export declare const hit: v.StrictObjectSchema<{
|
|
|
24
24
|
readonly scope: v.OptionalSchema<v.PicklistSchema<["transcript", "chat", "summary"], undefined>, undefined>;
|
|
25
25
|
/** A meeting passage's stored line, so two passages of one meeting stay apart; `search meetings` gives the same. */
|
|
26
26
|
readonly id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
27
|
+
/** A message's files; absent when it has none. */
|
|
28
|
+
readonly attachments: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
29
|
+
readonly position: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
30
|
+
readonly kind: v.StringSchema<undefined>;
|
|
31
|
+
readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
32
|
+
readonly size: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
33
|
+
}, undefined>, undefined>, undefined>;
|
|
27
34
|
/** Absent on a note hit: the owner wrote it. */
|
|
28
35
|
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
29
36
|
}, undefined>;
|
package/dist/entities/hit.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
+
import { hitFile } from "./attachment.js";
|
|
2
3
|
import { count, reference, text, time, untrusted } from "./fields.js";
|
|
3
4
|
/** One result of `search all`: a message, a mail, a note, a calendar event or a meeting passage, best first. */
|
|
4
5
|
export const hit = v.strictObject({
|
|
@@ -25,6 +26,8 @@ export const hit = v.strictObject({
|
|
|
25
26
|
scope: v.optional(v.picklist(["transcript", "chat", "summary"])),
|
|
26
27
|
/** A meeting passage's stored line, so two passages of one meeting stay apart; `search meetings` gives the same. */
|
|
27
28
|
id: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
29
|
+
/** A message's files; absent when it has none. */
|
|
30
|
+
attachments: v.optional(v.array(hitFile)),
|
|
28
31
|
/** Absent on a note hit: the owner wrote it. */
|
|
29
32
|
untrusted,
|
|
30
33
|
});
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -1231,6 +1231,13 @@ export declare const ENTITIES: {
|
|
|
1231
1231
|
readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
|
|
1232
1232
|
}, undefined>, undefined>;
|
|
1233
1233
|
}, undefined>, undefined>;
|
|
1234
|
+
/** The files the message carries; absent when it has none. */
|
|
1235
|
+
readonly attachments: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
1236
|
+
readonly position: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
1237
|
+
readonly kind: v.StringSchema<undefined>;
|
|
1238
|
+
readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
1239
|
+
readonly size: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
1240
|
+
}, undefined>, undefined>, undefined>;
|
|
1234
1241
|
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
1235
1242
|
}, undefined>;
|
|
1236
1243
|
readonly "message.summary": Omit<v.StrictObjectSchema<{
|
|
@@ -1895,6 +1902,13 @@ export declare const ENTITIES: {
|
|
|
1895
1902
|
readonly url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
1896
1903
|
readonly access: v.PicklistSchema<["public", "restricted", "unknown", "unavailable"], undefined>;
|
|
1897
1904
|
readonly reason: v.NullableSchema<v.PicklistSchema<["unsupported_chat", "unsupported_provider", "offline"], undefined>, undefined>;
|
|
1905
|
+
readonly files: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
1906
|
+
readonly position: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
1907
|
+
readonly kind: v.StringSchema<undefined>;
|
|
1908
|
+
readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
1909
|
+
readonly url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
1910
|
+
readonly reason: v.NullableSchema<v.PicklistSchema<["unsupported_provider"], undefined>, undefined>;
|
|
1911
|
+
}, undefined>, undefined>, undefined>;
|
|
1898
1912
|
}, undefined>;
|
|
1899
1913
|
readonly message_write: v.StrictObjectSchema<{
|
|
1900
1914
|
readonly operation_id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
@@ -4554,6 +4568,12 @@ export declare const ENTITIES: {
|
|
|
4554
4568
|
readonly offset_ms: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>, undefined>;
|
|
4555
4569
|
readonly scope: v.OptionalSchema<v.PicklistSchema<["transcript", "chat", "summary"], undefined>, undefined>;
|
|
4556
4570
|
readonly id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
4571
|
+
readonly attachments: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
4572
|
+
readonly position: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
4573
|
+
readonly kind: v.StringSchema<undefined>;
|
|
4574
|
+
readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
4575
|
+
readonly size: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
4576
|
+
}, undefined>, undefined>, undefined>;
|
|
4557
4577
|
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
4558
4578
|
}, undefined>;
|
|
4559
4579
|
readonly document: v.StrictObjectSchema<{
|
package/dist/entities/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { accountSession } from "./account-session.js";
|
|
|
8
8
|
import { adminReport } from "./admin-report.js";
|
|
9
9
|
import { agenda } from "./agenda.js";
|
|
10
10
|
import { analysisConsent, analysisConsentRevocation } from "./analysis-consent.js";
|
|
11
|
-
import { attachment } from "./attachment.js";
|
|
11
|
+
import { attachment, hitFile } from "./attachment.js";
|
|
12
12
|
import { attachmentBytes, attachmentExtraction, attachmentTextChange } from "./attachment-results.js";
|
|
13
13
|
import { chart } from "./chart.js";
|
|
14
14
|
import { chat } from "./chat.js";
|
|
@@ -120,6 +120,8 @@ export const ENTITIES = {
|
|
|
120
120
|
"follow_ups",
|
|
121
121
|
"email",
|
|
122
122
|
]).entries,
|
|
123
|
+
/** The files the message carries; absent when it has none. */
|
|
124
|
+
attachments: v.optional(v.array(hitFile)),
|
|
123
125
|
untrusted,
|
|
124
126
|
}),
|
|
125
127
|
"message.summary": v.pick(message, [
|
|
@@ -6,4 +6,12 @@ export declare const messageLink: v.StrictObjectSchema<{
|
|
|
6
6
|
readonly access: v.PicklistSchema<["public", "restricted", "unknown", "unavailable"], undefined>;
|
|
7
7
|
/** Why there is no link. */
|
|
8
8
|
readonly reason: v.NullableSchema<v.PicklistSchema<["unsupported_chat", "unsupported_provider", "offline"], undefined>, undefined>;
|
|
9
|
+
/** The message's files, each with its own link where the messenger gives one; absent when it has none. */
|
|
10
|
+
readonly files: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
11
|
+
readonly position: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
12
|
+
readonly kind: v.StringSchema<undefined>;
|
|
13
|
+
readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
14
|
+
readonly url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
15
|
+
readonly reason: v.NullableSchema<v.PicklistSchema<["unsupported_provider"], undefined>, undefined>;
|
|
16
|
+
}, undefined>, undefined>, undefined>;
|
|
9
17
|
}, undefined>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
-
import { reference, text } from "./fields.js";
|
|
2
|
+
import { count, reference, text } from "./fields.js";
|
|
3
3
|
/** Where a message can be opened: its locator always, a provider link when there is one and who that link reaches. */
|
|
4
4
|
export const messageLink = v.strictObject({
|
|
5
5
|
ref: reference,
|
|
@@ -7,4 +7,12 @@ export const messageLink = v.strictObject({
|
|
|
7
7
|
access: v.picklist(["public", "restricted", "unknown", "unavailable"]),
|
|
8
8
|
/** Why there is no link. */
|
|
9
9
|
reason: v.nullable(v.picklist(["unsupported_chat", "unsupported_provider", "offline"])),
|
|
10
|
+
/** The message's files, each with its own link where the messenger gives one; absent when it has none. */
|
|
11
|
+
files: v.optional(v.array(v.strictObject({
|
|
12
|
+
position: count,
|
|
13
|
+
kind: text,
|
|
14
|
+
name: v.optional(text),
|
|
15
|
+
url: v.nullable(text),
|
|
16
|
+
reason: v.nullable(v.picklist(["unsupported_provider"])),
|
|
17
|
+
}))),
|
|
10
18
|
});
|
|
@@ -9,6 +9,7 @@ import { attachment as attachmentEntity } from "../entities/attachment.js";
|
|
|
9
9
|
import { type TextOrigin } from "../store/store.js";
|
|
10
10
|
import { batchProgress } from "./batch.js";
|
|
11
11
|
import { type ServiceDeps } from "./deps.js";
|
|
12
|
+
import { type Saved } from "./file-download.js";
|
|
12
13
|
export type ExtractStatus = "extracted" | "needs-agent" | "unreadable" | "engine-missing" | "too-large" | "missing";
|
|
13
14
|
/** One file looked at. Never its text: the text stays in the local store. */
|
|
14
15
|
export interface ExtractItem {
|
|
@@ -114,6 +115,16 @@ export interface AttachmentsService {
|
|
|
114
115
|
items: AttachmentItem[];
|
|
115
116
|
has_more: boolean;
|
|
116
117
|
}>;
|
|
118
|
+
/**
|
|
119
|
+
* Copies files the store already holds to `output`, never over a file there, and never asks the messenger: one
|
|
120
|
+
* not held locally is an error before anything is written. The store keeps pointing at its own copy.
|
|
121
|
+
*/
|
|
122
|
+
saveHeld(target: {
|
|
123
|
+
chat: string;
|
|
124
|
+
message?: string;
|
|
125
|
+
attachment?: number;
|
|
126
|
+
output: string;
|
|
127
|
+
}): Promise<Saved[]>;
|
|
117
128
|
/** Keeps text an agent read from a file — a scan, a photo — replacing whatever was there. Sends nothing. */
|
|
118
129
|
setText(target: {
|
|
119
130
|
chat: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { constants } from "node:fs";
|
|
3
|
-
import { open, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { constants, mkdirSync } from "node:fs";
|
|
3
|
+
import { lstat, open, readFile, stat } from "node:fs/promises";
|
|
4
4
|
import { CliError } from "@wirecat/cli-base";
|
|
5
5
|
import { queryFieldsOf, walkQuery } from "@wirecat/cli-base/query";
|
|
6
6
|
import { directoryPaths } from "../attachments/directory.js";
|
|
@@ -18,6 +18,7 @@ import { refusedPlace } from "../sends/upload.js";
|
|
|
18
18
|
import { attachmentColumns, queryFilter, } from "../store/store.js";
|
|
19
19
|
import { batchProgress } from "./batch.js";
|
|
20
20
|
import { accountStore } from "./deps.js";
|
|
21
|
+
import { save } from "./file-download.js";
|
|
21
22
|
import { chatIdIn } from "./message-resolution.js";
|
|
22
23
|
export const ATTACHMENT_QUERY = {
|
|
23
24
|
entity: "attachment",
|
|
@@ -407,6 +408,48 @@ export const attachmentsService = (deps) => ({
|
|
|
407
408
|
...preview,
|
|
408
409
|
};
|
|
409
410
|
},
|
|
411
|
+
saveHeld: async ({ chat, message, attachment, output }) => {
|
|
412
|
+
if (attachment !== undefined && (!Number.isSafeInteger(attachment) || attachment < 1))
|
|
413
|
+
throw new CliError("validation_error", "attachment must be a position from 1");
|
|
414
|
+
const store = await accountStore(deps);
|
|
415
|
+
const { account } = store;
|
|
416
|
+
const { chatId, messageId } = await messageOf(deps, store, chat, message);
|
|
417
|
+
const ref = formatLocator({ ...account, chat: chatId, message: messageId });
|
|
418
|
+
const files = (await store.attachments.attachments({ chatId, messageId, limit: 1000 })).filter(({ kind }) => !NOT_FILES.has(kind));
|
|
419
|
+
if (files.length === 0)
|
|
420
|
+
throw new CliError("not_found", `the local store holds no file of ${ref}`);
|
|
421
|
+
const chosen = attachment === undefined ? files : files.filter(({ position }) => position === attachment - 1);
|
|
422
|
+
if (chosen.length === 0)
|
|
423
|
+
throw new CliError("not_found", `${ref} has no file at position ${attachment}; its files are at ${files.map(({ position }) => position + 1).join(", ")}`);
|
|
424
|
+
const env = deps.env ?? process.env;
|
|
425
|
+
const held = await Promise.all(chosen.map(async (file) => {
|
|
426
|
+
const missing = new CliError("not_found", `file ${file.position + 1} of ${ref} is not saved on this computer; run messages download without --offline`);
|
|
427
|
+
if (file.localPath === null)
|
|
428
|
+
throw missing;
|
|
429
|
+
if (refusedPlace(file.localPath, deps.messenger.app, env))
|
|
430
|
+
throw new CliError("validation_error", "cannot copy known credential files, the WireCat apps' own files or the message store");
|
|
431
|
+
const found = await lstat(file.localPath).catch(() => undefined);
|
|
432
|
+
if (!found)
|
|
433
|
+
throw missing;
|
|
434
|
+
if (!found.isFile())
|
|
435
|
+
throw new CliError("validation_error", "the saved attachment must be a regular file");
|
|
436
|
+
return { file, path: file.localPath };
|
|
437
|
+
}));
|
|
438
|
+
mkdirSync(output, { recursive: true });
|
|
439
|
+
const saved = [];
|
|
440
|
+
for (const { file, path } of held)
|
|
441
|
+
saved.push(await save({
|
|
442
|
+
kind: file.kind,
|
|
443
|
+
...(file.name === null ? {} : { name: file.name }),
|
|
444
|
+
...(file.mime === null ? {} : { mime: file.mime }),
|
|
445
|
+
position: file.position,
|
|
446
|
+
bytes: async function* () {
|
|
447
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
448
|
+
yield* handle.createReadStream();
|
|
449
|
+
},
|
|
450
|
+
}, output, `${messageId}-${file.position + 1}`));
|
|
451
|
+
return saved;
|
|
452
|
+
},
|
|
410
453
|
setText: async ({ chat, message, attachment, text }) => {
|
|
411
454
|
if (text.trim() === "")
|
|
412
455
|
throw new CliError("validation_error", "the text is empty — nothing to keep");
|
package/dist/services/deps.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export declare const fromStore: (deps: Pick<ServiceDeps, "offline" | "reads">) =
|
|
|
72
72
|
export declare const nothingStored: (messenger: MessengerTraits) => string;
|
|
73
73
|
/** Where a store-mode messenger's history comes from, for what it cannot do. */
|
|
74
74
|
export declare const PUSHED = "this messenger's history is read from the local store";
|
|
75
|
-
export declare const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context`, `contacts list|show` and `review`";
|
|
75
|
+
export declare const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context|download`, `contacts list|show` and `review`";
|
|
76
76
|
/** Over a connection that is already open — an MCP session holds one for minutes. */
|
|
77
77
|
export declare const onlineDeps: (messenger: ServiceMessenger, adapter: MessengerAdapter, guard: SendGuard, where?: {
|
|
78
78
|
profile?: string;
|
package/dist/services/deps.js
CHANGED
|
@@ -55,7 +55,7 @@ export const fromStore = (deps) => deps.offline || deps.reads === "store";
|
|
|
55
55
|
export const nothingStored = (messenger) => `nothing stored for this chat yet — keep \`${typedCommand(messenger.app.command, "serve")}\` running`;
|
|
56
56
|
/** Where a store-mode messenger's history comes from, for what it cannot do. */
|
|
57
57
|
export const PUSHED = "this messenger's history is read from the local store";
|
|
58
|
-
export const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context`, `contacts list|show` and `review`";
|
|
58
|
+
export const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context|download`, `contacts list|show` and `review`";
|
|
59
59
|
/** Over a connection that is already open — an MCP session holds one for minutes. */
|
|
60
60
|
export const onlineDeps = (messenger, adapter, guard, where = {}) => ({
|
|
61
61
|
messenger,
|
|
@@ -197,7 +197,6 @@ export interface Reacted {
|
|
|
197
197
|
}
|
|
198
198
|
/** max-cli's: many at once is what a ban for automation looks like. */
|
|
199
199
|
export declare const DELETE_AT_ONCE = 10;
|
|
200
|
-
/** Every write goes through the guard: asked before it goes, told after, on every outcome. */
|
|
201
200
|
export interface MessagesService {
|
|
202
201
|
/** The account these messages are read as, which names each one's locator. */
|
|
203
202
|
reader(): Promise<AccountKey>;
|
|
@@ -212,8 +211,8 @@ export interface MessagesService {
|
|
|
212
211
|
}): Promise<Page<Message> & {
|
|
213
212
|
discussion: Discussion;
|
|
214
213
|
}>;
|
|
215
|
-
/** The files of one message
|
|
216
|
-
download(chat: string, message
|
|
214
|
+
/** The files of one message, `chat` and its id or a `msg:` locator alone. Always from the messenger. */
|
|
215
|
+
download(chat: string, message?: Id): Promise<Download>;
|
|
217
216
|
/**
|
|
218
217
|
* Records in the local store where the files went, so their text can be read later. Only a
|
|
219
218
|
* message the store holds; answers how many attachments it recorded.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { validateFormattedText, visibleFormattedText } from "../domain/formatting.js";
|
|
3
3
|
import { formatLocator } from "../domain/locator.js";
|
|
4
|
-
import { messageLinkTarget, validatePermalink } from "../domain/message-link.js";
|
|
4
|
+
import { fileLinks, messageLinkTarget, validatePermalink } from "../domain/message-link.js";
|
|
5
5
|
import { pinDurationsText } from "../domain/pin-duration.js";
|
|
6
6
|
import { threadIdOf } from "../domain/thread.js";
|
|
7
7
|
import { capability } from "../ports/messenger.js";
|
|
@@ -22,6 +22,12 @@ import { searchServer, sourceKey, } from "./server-search.js";
|
|
|
22
22
|
import { readThreadContext, threadBounds } from "./thread-context.js";
|
|
23
23
|
/** max-cli's: many at once is what a ban for automation looks like. */
|
|
24
24
|
export const DELETE_AT_ONCE = 10;
|
|
25
|
+
/** Every write goes through the guard: asked before it goes, told after, on every outcome. */
|
|
26
|
+
/** Only a message with files says what can be linked of them. */
|
|
27
|
+
const filesOf = ({ attachments }) => {
|
|
28
|
+
const files = fileLinks(attachments);
|
|
29
|
+
return files.length > 0 ? { files } : {};
|
|
30
|
+
};
|
|
25
31
|
export const messagesService = (deps) => {
|
|
26
32
|
const { guard } = deps;
|
|
27
33
|
const inStore = async (read) => read(await deps.store(), await deps.account());
|
|
@@ -185,13 +191,15 @@ export const messagesService = (deps) => {
|
|
|
185
191
|
const store = await deps.store();
|
|
186
192
|
const chat = await readChatId(deps, target.chat, store, account);
|
|
187
193
|
const found = await store.messages.around(account, chat, target.message, { before: 0, after: 0 });
|
|
188
|
-
|
|
194
|
+
const held = found.find((item) => item.id === target.message && item.chatId === chat);
|
|
195
|
+
if (!held)
|
|
189
196
|
throw new CliError("not_found", "no such message in this account's stored chat");
|
|
190
197
|
return {
|
|
191
198
|
locator: formatLocator({ ...account, chat, message: target.message }),
|
|
192
199
|
url: null,
|
|
193
200
|
access: "unavailable",
|
|
194
201
|
reason: deps.offline ? "offline" : "unsupported_provider",
|
|
202
|
+
...filesOf(held),
|
|
195
203
|
};
|
|
196
204
|
}
|
|
197
205
|
const connection = await deps.connection();
|
|
@@ -199,20 +207,31 @@ export const messagesService = (deps) => {
|
|
|
199
207
|
throw new CliError("authentication_error", "the connected account differs from the recorded profile; refresh the session before linking");
|
|
200
208
|
const { id: chat } = await connection.resolve(target.chat);
|
|
201
209
|
const locator = formatLocator({ ...account, chat, message: target.message });
|
|
202
|
-
if (connection.permalink)
|
|
203
|
-
|
|
210
|
+
if (connection.permalink) {
|
|
211
|
+
const permalink = validatePermalink(await connection.permalink(chat, target.message));
|
|
212
|
+
const held = await deps
|
|
213
|
+
.store()
|
|
214
|
+
.then((store) => store.messages.around(account, chat, target.message, { before: 0, after: 0 }))
|
|
215
|
+
.then((found) => found.find((item) => item.id === target.message && item.chatId === chat))
|
|
216
|
+
.catch(() => undefined);
|
|
217
|
+
return { locator, ...permalink, ...(held ? filesOf(held) : {}) };
|
|
218
|
+
}
|
|
204
219
|
const found = await capability(connection, "around", "read a message")(chat, target.message, {
|
|
205
220
|
before: 0,
|
|
206
221
|
after: 0,
|
|
207
222
|
});
|
|
208
|
-
|
|
223
|
+
const held = found.find((item) => item.id === target.message && item.chatId === chat);
|
|
224
|
+
if (!held)
|
|
209
225
|
throw new CliError("not_found", "no such message in this chat");
|
|
210
|
-
return { locator, url: null, access: "unavailable", reason: "unsupported_provider" };
|
|
226
|
+
return { locator, url: null, access: "unavailable", reason: "unsupported_provider", ...filesOf(held) };
|
|
211
227
|
},
|
|
212
|
-
download: async (
|
|
228
|
+
download: async (reference, message) => {
|
|
229
|
+
const target = messageLinkTarget(reference, message, deps.messenger.provider);
|
|
230
|
+
if (target.account !== undefined && target.account !== (await deps.account()).account)
|
|
231
|
+
throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
|
|
213
232
|
const connection = await deps.connection();
|
|
214
233
|
const download = capability(connection, "download", "download attachments");
|
|
215
|
-
return download((await connection.resolve(chat)).id, message);
|
|
234
|
+
return download((await connection.resolve(target.chat)).id, target.message);
|
|
216
235
|
},
|
|
217
236
|
keepDownloaded: (chat, message, files) => inStore(async (store, account) => {
|
|
218
237
|
const chatId = (await storedChatId(deps.messenger, chat, store, account).catch(() => undefined)) ??
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ErrorCode } from "@wirecat/cli-base";
|
|
2
|
+
import { type HitFile } from "../domain/attachments.js";
|
|
2
3
|
import { type MailFields } from "../domain/models.js";
|
|
3
4
|
import type { MessengerTraits } from "../ports/traits.js";
|
|
4
5
|
import type { AccountKey, MessageStore } from "../store/store.js";
|
|
@@ -30,6 +31,8 @@ export interface SearchAllItem {
|
|
|
30
31
|
senderIsMe?: boolean;
|
|
31
32
|
/** A mail's people, labels and thread, as `search mail` gives them. */
|
|
32
33
|
email?: MailFields;
|
|
34
|
+
/** A message's or a mail's files, as `search messages` names them. */
|
|
35
|
+
attachments?: HitFile[];
|
|
33
36
|
/** Which search found it: words, meaning or both. */
|
|
34
37
|
foundBy: ("title" | "words" | "meaning")[];
|
|
35
38
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { parseLucene } from "@wirecat/cli-base/query";
|
|
3
|
+
import { hitFiles } from "../domain/attachments.js";
|
|
3
4
|
import { parseLocator } from "../domain/locator.js";
|
|
4
5
|
import { senderIsMe } from "../domain/models.js";
|
|
5
6
|
import { INTERNAL_NOTES, kindIn } from "../domain/providers.js";
|
|
@@ -102,6 +103,7 @@ export const searchAll = async (store, account, request, messenger = {}, searchM
|
|
|
102
103
|
: { name: hit.senderName, ref: null },
|
|
103
104
|
senderIsMe: senderIsMe(hit, at.account),
|
|
104
105
|
...(hit.email ? { email: hit.email } : {}),
|
|
106
|
+
attachments: hitFiles(hit.attachments),
|
|
105
107
|
foundBy,
|
|
106
108
|
};
|
|
107
109
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.532.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",
|