@rubytech/create-maxy-code 0.1.163 → 0.1.165
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/package.json +1 -1
- package/payload/platform/neo4j/edge-annotations.json +4 -0
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +1 -6
- package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +12 -15
- package/payload/platform/plugins/admin/mcp/dist/index.js +0 -41
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +8 -12
- package/payload/platform/plugins/email/mcp/dist/index.js +8 -34
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts +39 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js +0 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts +23 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js +131 -3
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts +19 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js +51 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +0 -18
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js +0 -12
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +91 -40
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +204 -169
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js +6 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts +7 -15
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js +206 -86
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +104 -4
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.js +22 -13
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +5 -4
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.js +17 -13
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +8 -8
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +2 -2
- package/payload/platform/plugins/memory/mcp/dist/index.js +4 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js +1 -13
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js +3 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js +10 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +5 -6
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +21 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +6 -8
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +3 -2
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +1 -1
- package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +49 -0
- package/payload/platform/plugins/memory/references/transcript-formats/granola.md +50 -0
- package/payload/platform/plugins/memory/references/transcript-formats/otter.md +50 -0
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +1 -0
- package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +170 -0
- package/payload/platform/scripts/seed-neo4j.sh +1 -3
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +9 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +74 -38
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +16 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +16 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +21 -7
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +10 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +52 -10
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +73 -13
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/platform/templates/specialists/agents/librarian.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/per-turn-graph-pass-gate.test.sh +0 -387
- package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +0 -441
- package/payload/platform/plugins/admin/hooks/per-turn-graph-pass-gate.sh +0 -111
- package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +0 -171
|
@@ -13,18 +13,6 @@ export function stripAngleBrackets(value) {
|
|
|
13
13
|
}
|
|
14
14
|
return trimmed;
|
|
15
15
|
}
|
|
16
|
-
export async function getMessageEnvelope(_messageId, _accountId) {
|
|
17
|
-
// Task 442 — sourcing per-message envelopes from IMAP, not the graph.
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
export async function getThreadInfo(_messageIds, _accountId) {
|
|
21
|
-
// Task 442 — per-message thread-depth lookup requires per-message nodes
|
|
22
|
-
// the new `:ConversationArchive` shape does not store.
|
|
23
|
-
return new Map();
|
|
24
|
-
}
|
|
25
|
-
// Re-export neo4j so consumers depending on the legacy module surface keep
|
|
26
|
-
// working without churn. The actual neo4j-driver usage is now in
|
|
27
|
-
// conversation-archive-dispatch.ts.
|
|
28
16
|
export { neo4j };
|
|
29
17
|
export { getSession };
|
|
30
18
|
//# sourceMappingURL=graph.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,MAAM,cAAc,CAAC;AAuCjC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAoB;IACrD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,OAAO,EAAE,KAAK,EAAE,CAAC;AACjB,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -8,6 +8,7 @@ export interface EmailMessage {
|
|
|
8
8
|
subject: string;
|
|
9
9
|
date: string;
|
|
10
10
|
bodyPreview: string;
|
|
11
|
+
attachments: AttachmentMeta[];
|
|
11
12
|
}
|
|
12
13
|
export interface PaginatedMessages {
|
|
13
14
|
messages: EmailMessage[];
|
|
@@ -28,6 +29,77 @@ export declare function resolveFolderName(client: ImapFlow, folder: "inbox" | "s
|
|
|
28
29
|
* Used for extracting readable text from HTML-only email bodies.
|
|
29
30
|
*/
|
|
30
31
|
export declare function stripHtml(html: string): string;
|
|
32
|
+
/** Hard cap on a single attachment's bytes — over this size, metadata is surfaced
|
|
33
|
+
* but bytes are not downloaded. Matches typical provider send limits. */
|
|
34
|
+
export declare const MAX_ATTACHMENT_BYTES: number;
|
|
35
|
+
/** Attachment metadata as derived from IMAP bodyStructure — no bytes. */
|
|
36
|
+
export interface AttachmentMeta {
|
|
37
|
+
/** IMAP body part key, e.g. "2" or "1.3". Used to fetch bytes later. */
|
|
38
|
+
partKey: string;
|
|
39
|
+
filename: string;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
sizeBytes: number;
|
|
42
|
+
/** RFC 2045 transfer encoding — needed to decode bytes at fetch time. */
|
|
43
|
+
encoding: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Body-structure node shape (subset of ImapFlow's BodyStructure).
|
|
47
|
+
* Defined locally because ImapFlow does not export a public type for this.
|
|
48
|
+
*/
|
|
49
|
+
interface BodyStructureNode {
|
|
50
|
+
part?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
encoding?: string;
|
|
53
|
+
size?: number;
|
|
54
|
+
disposition?: string;
|
|
55
|
+
dispositionParameters?: {
|
|
56
|
+
filename?: string;
|
|
57
|
+
};
|
|
58
|
+
parameters?: {
|
|
59
|
+
name?: string;
|
|
60
|
+
};
|
|
61
|
+
childNodes?: BodyStructureNode[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Walk an IMAP bodyStructure and return metadata for every attachment-shaped
|
|
65
|
+
* part. No bytes are downloaded — cheap, runs at preview time.
|
|
66
|
+
*
|
|
67
|
+
* An attachment-shaped part is one of:
|
|
68
|
+
* - disposition === "attachment" (RFC 2183), OR
|
|
69
|
+
* - has a filename in parameters.name or dispositionParameters.filename AND
|
|
70
|
+
* is not a multipart container.
|
|
71
|
+
*
|
|
72
|
+
* Inline parts without a filename (e.g. the message body) are excluded.
|
|
73
|
+
*/
|
|
74
|
+
export declare function walkAttachmentMetadata(structure: BodyStructureNode | undefined): AttachmentMeta[];
|
|
75
|
+
/**
|
|
76
|
+
* Format an AttachmentMeta[] as a single-line operator-readable footer.
|
|
77
|
+
* Returns empty string when the array is empty so callers can skip the
|
|
78
|
+
* line entirely.
|
|
79
|
+
*/
|
|
80
|
+
export declare function formatAttachmentsFooter(attachments: AttachmentMeta[]): string;
|
|
81
|
+
/**
|
|
82
|
+
* Result of attempting to fetch one attachment's bytes from IMAP.
|
|
83
|
+
* `bytes === null` indicates a non-fatal failure (oversize, short buffer,
|
|
84
|
+
* fetch error) — the caller surfaces metadata-only graph attach instead.
|
|
85
|
+
*/
|
|
86
|
+
export interface AttachmentBytesResult {
|
|
87
|
+
uid: number;
|
|
88
|
+
partKey: string;
|
|
89
|
+
bytes: Buffer | null;
|
|
90
|
+
failureReason: "oversize" | "short-buffer" | "fetch-error" | null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Download decoded bytes for a list of (uid, partKey) attachments from IMAP.
|
|
94
|
+
* Opens one connection, reuses it across all requests. Per-attachment
|
|
95
|
+
* failures are isolated — one bad part does not abort the batch.
|
|
96
|
+
*/
|
|
97
|
+
export declare function fetchAttachmentBytesForMessages(config: EmailConfig, password: string, requests: Array<{
|
|
98
|
+
uid: number;
|
|
99
|
+
partKey: string;
|
|
100
|
+
expectedSize: number;
|
|
101
|
+
encoding: string | null;
|
|
102
|
+
}>): Promise<AttachmentBytesResult[]>;
|
|
31
103
|
/**
|
|
32
104
|
* Decode a raw IMAP body part buffer according to its transfer encoding.
|
|
33
105
|
* Returns a UTF-8 string. Falls back to raw UTF-8 for unknown encodings.
|
|
@@ -148,6 +220,8 @@ export interface FetchedEmail {
|
|
|
148
220
|
subject: string;
|
|
149
221
|
date: string;
|
|
150
222
|
bodyPreview: string;
|
|
223
|
+
/** Attachment metadata derived from bodyStructure; bytes downloaded later. */
|
|
224
|
+
attachments: AttachmentMeta[];
|
|
151
225
|
}
|
|
152
226
|
/**
|
|
153
227
|
* Fetch emails from INBOX since a given UID (exclusive) for incremental polling.
|
|
@@ -165,51 +239,28 @@ export declare function fetchSinceUid(config: EmailConfig, password: string, sin
|
|
|
165
239
|
maxUid: number;
|
|
166
240
|
}>;
|
|
167
241
|
/**
|
|
168
|
-
*
|
|
242
|
+
* Resolve an envelope by Message-ID via IMAP. Searches INBOX first, then
|
|
243
|
+
* the \Sent special-use folder. Returns null when no folder yields a hit.
|
|
244
|
+
*
|
|
245
|
+
* Uses `header: { "message-id": ... }` IMAP SEARCH — sufficient on every
|
|
246
|
+
* server that honours RFC 3501 header search. The Message-ID is sent with
|
|
247
|
+
* angle brackets restored because some servers (notably Gmail) only match
|
|
248
|
+
* the raw header value, not the bare identifier.
|
|
169
249
|
*/
|
|
170
|
-
export declare function
|
|
171
|
-
export interface AutoRespondMessage {
|
|
250
|
+
export declare function fetchEnvelopeByMessageId(config: EmailConfig, password: string, messageId: string): Promise<{
|
|
172
251
|
uid: number;
|
|
252
|
+
mailbox: string;
|
|
173
253
|
messageId: string;
|
|
174
|
-
|
|
175
|
-
|
|
254
|
+
fromAddress: string;
|
|
255
|
+
fromName: string | null;
|
|
256
|
+
toAddresses: string[];
|
|
257
|
+
ccAddresses: string[];
|
|
176
258
|
subject: string;
|
|
177
259
|
date: string;
|
|
178
|
-
|
|
179
|
-
references: string | null;
|
|
180
|
-
body: string;
|
|
181
|
-
/** True if the message has auto-reply indicators and should be skipped. */
|
|
182
|
-
isAutoReply: boolean;
|
|
183
|
-
}
|
|
260
|
+
} | null>;
|
|
184
261
|
/**
|
|
185
|
-
*
|
|
186
|
-
* Handles multi-line continuation (lines starting with whitespace).
|
|
187
|
-
*/
|
|
188
|
-
export declare function parseRawHeaders(raw: string): Map<string, string[]>;
|
|
189
|
-
/**
|
|
190
|
-
* Detect whether a message is an auto-reply based on RFC 3834 headers.
|
|
191
|
-
*
|
|
192
|
-
* Checks:
|
|
193
|
-
* - Auto-Submitted: any value other than "no"
|
|
194
|
-
* - X-Auto-Response-Suppress: present
|
|
195
|
-
* - Precedence: bulk, junk, or list
|
|
196
|
-
* - List-Unsubscribe or List-Id: present (mailing list)
|
|
197
|
-
*/
|
|
198
|
-
export declare function detectAutoReply(headers: Map<string, string[]>): boolean;
|
|
199
|
-
/**
|
|
200
|
-
* Fetch new messages for auto-respond processing.
|
|
201
|
-
*
|
|
202
|
-
* Returns messages with UIDs greater than `sinceUid`, filtered by the
|
|
203
|
-
* agent's address (TO header). Includes envelope, body, and auto-reply
|
|
204
|
-
* detection headers in a single IMAP FETCH.
|
|
205
|
-
*
|
|
206
|
-
* Unlike fetchRecent (optimised for tool output with pagination),
|
|
207
|
-
* this function fetches everything needed for auto-respond in one pass:
|
|
208
|
-
* envelope for threading, body for agent input, headers for loop guards.
|
|
262
|
+
* Get the inbox message count.
|
|
209
263
|
*/
|
|
210
|
-
export declare function
|
|
211
|
-
|
|
212
|
-
agentAddress: string;
|
|
213
|
-
limit?: number;
|
|
214
|
-
}): Promise<AutoRespondMessage[]>;
|
|
264
|
+
export declare function getInboxCount(config: EmailConfig, password: string): Promise<number>;
|
|
265
|
+
export {};
|
|
215
266
|
//# sourceMappingURL=imap.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imap.d.ts","sourceRoot":"","sources":["../../src/lib/imap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAA2B,MAAM,UAAU,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAuBpD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"imap.d.ts","sourceRoot":"","sources":["../../src/lib/imap.ts"],"names":[],"mappings":"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"}
|
|
@@ -81,6 +81,142 @@ export function stripHtml(html) {
|
|
|
81
81
|
const BODY_MAX_BYTES = 4096;
|
|
82
82
|
/** Max characters for body preview in tool output. */
|
|
83
83
|
const BODY_PREVIEW_LENGTH = 500;
|
|
84
|
+
/** Hard cap on a single attachment's bytes — over this size, metadata is surfaced
|
|
85
|
+
* but bytes are not downloaded. Matches typical provider send limits. */
|
|
86
|
+
export const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
87
|
+
/**
|
|
88
|
+
* Walk an IMAP bodyStructure and return metadata for every attachment-shaped
|
|
89
|
+
* part. No bytes are downloaded — cheap, runs at preview time.
|
|
90
|
+
*
|
|
91
|
+
* An attachment-shaped part is one of:
|
|
92
|
+
* - disposition === "attachment" (RFC 2183), OR
|
|
93
|
+
* - has a filename in parameters.name or dispositionParameters.filename AND
|
|
94
|
+
* is not a multipart container.
|
|
95
|
+
*
|
|
96
|
+
* Inline parts without a filename (e.g. the message body) are excluded.
|
|
97
|
+
*/
|
|
98
|
+
export function walkAttachmentMetadata(structure) {
|
|
99
|
+
if (!structure)
|
|
100
|
+
return [];
|
|
101
|
+
const out = [];
|
|
102
|
+
const visit = (node) => {
|
|
103
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
104
|
+
for (const child of node.childNodes)
|
|
105
|
+
visit(child);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const filename = node.dispositionParameters?.filename?.trim() ||
|
|
109
|
+
node.parameters?.name?.trim() ||
|
|
110
|
+
"";
|
|
111
|
+
const isAttachmentDisposition = typeof node.disposition === "string" &&
|
|
112
|
+
node.disposition.toLowerCase() === "attachment";
|
|
113
|
+
const isMultipart = typeof node.type === "string" && node.type.toLowerCase().startsWith("multipart/");
|
|
114
|
+
if (isMultipart)
|
|
115
|
+
return;
|
|
116
|
+
if (!isAttachmentDisposition && !filename)
|
|
117
|
+
return;
|
|
118
|
+
if (!node.part)
|
|
119
|
+
return; // can't fetch bytes without a part key
|
|
120
|
+
out.push({
|
|
121
|
+
partKey: node.part,
|
|
122
|
+
filename: filename || `unnamed-${node.part}`,
|
|
123
|
+
mimeType: (node.type ?? "application/octet-stream").toLowerCase(),
|
|
124
|
+
sizeBytes: typeof node.size === "number" ? node.size : 0,
|
|
125
|
+
encoding: node.encoding ?? null,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
visit(structure);
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function formatBytes(n) {
|
|
132
|
+
if (n < 1024)
|
|
133
|
+
return `${n} B`;
|
|
134
|
+
if (n < 1024 * 1024)
|
|
135
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
136
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Format an AttachmentMeta[] as a single-line operator-readable footer.
|
|
140
|
+
* Returns empty string when the array is empty so callers can skip the
|
|
141
|
+
* line entirely.
|
|
142
|
+
*/
|
|
143
|
+
export function formatAttachmentsFooter(attachments) {
|
|
144
|
+
if (attachments.length === 0)
|
|
145
|
+
return "";
|
|
146
|
+
const parts = attachments
|
|
147
|
+
.map((a) => `${a.filename} (${a.mimeType}, ${formatBytes(a.sizeBytes)})`)
|
|
148
|
+
.join("; ");
|
|
149
|
+
return `Attachments (${attachments.length}): ${parts}`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Download decoded bytes for a list of (uid, partKey) attachments from IMAP.
|
|
153
|
+
* Opens one connection, reuses it across all requests. Per-attachment
|
|
154
|
+
* failures are isolated — one bad part does not abort the batch.
|
|
155
|
+
*/
|
|
156
|
+
export async function fetchAttachmentBytesForMessages(config, password, requests) {
|
|
157
|
+
if (requests.length === 0)
|
|
158
|
+
return [];
|
|
159
|
+
const client = createClient(config, password);
|
|
160
|
+
const results = [];
|
|
161
|
+
try {
|
|
162
|
+
await client.connect();
|
|
163
|
+
const lock = await client.getMailboxLock("INBOX");
|
|
164
|
+
try {
|
|
165
|
+
for (const req of requests) {
|
|
166
|
+
if (req.expectedSize > MAX_ATTACHMENT_BYTES) {
|
|
167
|
+
diag("fetchAttachmentBytes", "oversize", `uid=${req.uid} part=${req.partKey} size=${req.expectedSize} cap=${MAX_ATTACHMENT_BYTES}`);
|
|
168
|
+
results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "oversize" });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const { content } = await client.download(String(req.uid), req.partKey, { uid: true });
|
|
173
|
+
if (!content) {
|
|
174
|
+
diag("fetchAttachmentBytes", "no-stream", `uid=${req.uid} part=${req.partKey}`);
|
|
175
|
+
results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "fetch-error" });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const chunks = [];
|
|
179
|
+
let total = 0;
|
|
180
|
+
let overflow = false;
|
|
181
|
+
for await (const chunk of content) {
|
|
182
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
183
|
+
total += buf.length;
|
|
184
|
+
if (total > MAX_ATTACHMENT_BYTES) {
|
|
185
|
+
overflow = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
chunks.push(buf);
|
|
189
|
+
}
|
|
190
|
+
if (overflow) {
|
|
191
|
+
diag("fetchAttachmentBytes", "stream-oversize", `uid=${req.uid} part=${req.partKey} streamed=${total}`);
|
|
192
|
+
results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "oversize" });
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// ImapFlow's download() returns already-decoded bytes when given a
|
|
196
|
+
// partKey, so we do not re-decode against `encoding` here.
|
|
197
|
+
const bytes = Buffer.concat(chunks);
|
|
198
|
+
if (req.expectedSize > 0 && bytes.length === 0) {
|
|
199
|
+
diag("fetchAttachmentBytes", "short-buffer", `uid=${req.uid} part=${req.partKey} expected=${req.expectedSize} got=0`);
|
|
200
|
+
results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "short-buffer" });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
results.push({ uid: req.uid, partKey: req.partKey, bytes, failureReason: null });
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
diag("fetchAttachmentBytes", "fetch-error", `uid=${req.uid} part=${req.partKey} ${err instanceof Error ? err.message : String(err)}`);
|
|
207
|
+
results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "fetch-error" });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
lock.release();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
await safeLogout(client);
|
|
217
|
+
}
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
84
220
|
/**
|
|
85
221
|
* Decode a raw IMAP body part buffer according to its transfer encoding.
|
|
86
222
|
* Returns a UTF-8 string. Falls back to raw UTF-8 for unknown encodings.
|
|
@@ -328,6 +464,7 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
328
464
|
break;
|
|
329
465
|
const bodyPreview = extractBodyFromFetch(msg);
|
|
330
466
|
diag("fetchRecent", `body #${envelopeCount}`, `uid=${msg.uid} preview=${bodyPreview.length}chars type=${msg.bodyStructure?.type ?? "?"}`);
|
|
467
|
+
const attachments = walkAttachmentMetadata(msg.bodyStructure);
|
|
331
468
|
messages.push({
|
|
332
469
|
uid: msg.uid,
|
|
333
470
|
messageId,
|
|
@@ -336,6 +473,7 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
336
473
|
subject,
|
|
337
474
|
date,
|
|
338
475
|
bodyPreview,
|
|
476
|
+
attachments,
|
|
339
477
|
});
|
|
340
478
|
}
|
|
341
479
|
diag("fetchRecent", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
|
|
@@ -423,6 +561,7 @@ export async function searchMessages(config, password, query) {
|
|
|
423
561
|
if (messages.length >= limit)
|
|
424
562
|
break;
|
|
425
563
|
const bodyPreview = extractBodyFromFetch(msg);
|
|
564
|
+
const attachments = walkAttachmentMetadata(msg.bodyStructure);
|
|
426
565
|
messages.push({
|
|
427
566
|
uid: msg.uid,
|
|
428
567
|
messageId,
|
|
@@ -431,6 +570,7 @@ export async function searchMessages(config, password, query) {
|
|
|
431
570
|
subject: subj,
|
|
432
571
|
date,
|
|
433
572
|
bodyPreview,
|
|
573
|
+
attachments,
|
|
434
574
|
});
|
|
435
575
|
}
|
|
436
576
|
diag("searchMessages", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
|
|
@@ -528,7 +668,8 @@ export async function fetchSinceUid(config, password, sinceUid, recipientAddress
|
|
|
528
668
|
const toAddresses = (msg.envelope?.to ?? []).map((a) => a.address).filter((a) => !!a);
|
|
529
669
|
const ccAddresses = (msg.envelope?.cc ?? []).map((a) => a.address).filter((a) => !!a);
|
|
530
670
|
const bodyPreview = extractBodyFromFetch(msg);
|
|
531
|
-
|
|
671
|
+
const attachments = walkAttachmentMetadata(msg.bodyStructure);
|
|
672
|
+
emails.push({ uid, messageId, inReplyTo, toAddresses, ccAddresses, fromAddress: fromAddr, fromName, subject, date, bodyPreview, attachments });
|
|
532
673
|
}
|
|
533
674
|
diag("fetchSinceUid", "fetch done", `${emails.length} emails (maxUid=${maxUid}) in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
|
|
534
675
|
return { uidValidity, emails, maxUid };
|
|
@@ -542,191 +683,85 @@ export async function fetchSinceUid(config, password, sinceUid, recipientAddress
|
|
|
542
683
|
}
|
|
543
684
|
}
|
|
544
685
|
/**
|
|
545
|
-
*
|
|
686
|
+
* Resolve an envelope by Message-ID via IMAP. Searches INBOX first, then
|
|
687
|
+
* the \Sent special-use folder. Returns null when no folder yields a hit.
|
|
688
|
+
*
|
|
689
|
+
* Uses `header: { "message-id": ... }` IMAP SEARCH — sufficient on every
|
|
690
|
+
* server that honours RFC 3501 header search. The Message-ID is sent with
|
|
691
|
+
* angle brackets restored because some servers (notably Gmail) only match
|
|
692
|
+
* the raw header value, not the bare identifier.
|
|
546
693
|
*/
|
|
547
|
-
export async function
|
|
694
|
+
export async function fetchEnvelopeByMessageId(config, password, messageId) {
|
|
695
|
+
const cleanId = messageId.replace(/^<|>$/g, "");
|
|
696
|
+
const needle = `<${cleanId}>`;
|
|
548
697
|
const client = createClient(config, password);
|
|
698
|
+
const t0 = Date.now();
|
|
699
|
+
diag("fetchEnvelopeByMessageId", "start", `messageId=${cleanId}`);
|
|
549
700
|
try {
|
|
550
701
|
await client.connect();
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
// ─── Auto-respond types and fetch ────────────────────────────────────
|
|
559
|
-
/** Headers used to detect auto-replies (RFC 3834) and mailing lists. */
|
|
560
|
-
const AUTO_REPLY_HEADER_FIELDS = [
|
|
561
|
-
"Auto-Submitted",
|
|
562
|
-
"X-Auto-Response-Suppress",
|
|
563
|
-
"Precedence",
|
|
564
|
-
"List-Unsubscribe",
|
|
565
|
-
"List-Id",
|
|
566
|
-
];
|
|
567
|
-
/**
|
|
568
|
-
* Parse raw IMAP header text into a map of lowercased header names → values.
|
|
569
|
-
* Handles multi-line continuation (lines starting with whitespace).
|
|
570
|
-
*/
|
|
571
|
-
export function parseRawHeaders(raw) {
|
|
572
|
-
const headers = new Map();
|
|
573
|
-
const lines = raw.split(/\r?\n/);
|
|
574
|
-
let currentKey = "";
|
|
575
|
-
let currentValue = "";
|
|
576
|
-
for (const line of lines) {
|
|
577
|
-
if (line === "")
|
|
578
|
-
break; // End of headers
|
|
579
|
-
if (/^\s/.test(line)) {
|
|
580
|
-
// Continuation of previous header
|
|
581
|
-
currentValue += " " + line.trim();
|
|
702
|
+
diag("fetchEnvelopeByMessageId", "connected", `${Date.now() - t0}ms`);
|
|
703
|
+
const folders = ["INBOX"];
|
|
704
|
+
try {
|
|
705
|
+
const sentPath = await resolveFolderName(client, "sent");
|
|
706
|
+
folders.push(sentPath);
|
|
582
707
|
}
|
|
583
|
-
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
708
|
+
catch {
|
|
709
|
+
// No \Sent folder advertised — only search INBOX.
|
|
710
|
+
}
|
|
711
|
+
for (const folderPath of folders) {
|
|
712
|
+
const lock = await client.getMailboxLock(folderPath);
|
|
713
|
+
try {
|
|
714
|
+
const uids = await client.search({ header: { "message-id": needle } }, { uid: true });
|
|
715
|
+
if (!uids || uids.length === 0)
|
|
716
|
+
continue;
|
|
717
|
+
const uid = uids[0];
|
|
718
|
+
for await (const msg of client.fetch(String(uid), { envelope: true, uid: true }, { uid: true })) {
|
|
719
|
+
const env = msg.envelope ?? {};
|
|
720
|
+
const fromAddr = env.from?.[0]?.address ?? "";
|
|
721
|
+
if (!fromAddr)
|
|
722
|
+
continue;
|
|
723
|
+
diag("fetchEnvelopeByMessageId", "match", `folder=${folderPath} uid=${msg.uid} in ${Date.now() - t0}ms`);
|
|
724
|
+
return {
|
|
725
|
+
uid: msg.uid,
|
|
726
|
+
mailbox: folderPath,
|
|
727
|
+
messageId: env.messageId ?? messageId,
|
|
728
|
+
fromAddress: fromAddr,
|
|
729
|
+
fromName: env.from?.[0]?.name ?? null,
|
|
730
|
+
toAddresses: (env.to ?? [])
|
|
731
|
+
.map((a) => a.address)
|
|
732
|
+
.filter((a) => !!a),
|
|
733
|
+
ccAddresses: (env.cc ?? [])
|
|
734
|
+
.map((a) => a.address)
|
|
735
|
+
.filter((a) => !!a),
|
|
736
|
+
subject: env.subject ?? "",
|
|
737
|
+
date: env.date?.toISOString() ?? "",
|
|
738
|
+
};
|
|
739
|
+
}
|
|
594
740
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
currentValue = "";
|
|
741
|
+
finally {
|
|
742
|
+
lock.release();
|
|
598
743
|
}
|
|
599
744
|
}
|
|
745
|
+
diag("fetchEnvelopeByMessageId", "no-match", `searched=${folders.join(",")} in ${Date.now() - t0}ms`);
|
|
746
|
+
return null;
|
|
600
747
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
existing.push(currentValue);
|
|
605
|
-
headers.set(currentKey, existing);
|
|
606
|
-
}
|
|
607
|
-
return headers;
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* Detect whether a message is an auto-reply based on RFC 3834 headers.
|
|
611
|
-
*
|
|
612
|
-
* Checks:
|
|
613
|
-
* - Auto-Submitted: any value other than "no"
|
|
614
|
-
* - X-Auto-Response-Suppress: present
|
|
615
|
-
* - Precedence: bulk, junk, or list
|
|
616
|
-
* - List-Unsubscribe or List-Id: present (mailing list)
|
|
617
|
-
*/
|
|
618
|
-
export function detectAutoReply(headers) {
|
|
619
|
-
const autoSubmitted = headers.get("auto-submitted");
|
|
620
|
-
if (autoSubmitted && autoSubmitted.some((v) => v.trim().toLowerCase() !== "no")) {
|
|
621
|
-
return true;
|
|
748
|
+
catch (err) {
|
|
749
|
+
diag("fetchEnvelopeByMessageId", "FAILED", `${err instanceof Error ? err.message : String(err)} after ${Date.now() - t0}ms`);
|
|
750
|
+
throw err;
|
|
622
751
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const precedence = headers.get("precedence");
|
|
626
|
-
if (precedence) {
|
|
627
|
-
const val = precedence[0]?.trim().toLowerCase();
|
|
628
|
-
if (val === "bulk" || val === "junk" || val === "list")
|
|
629
|
-
return true;
|
|
752
|
+
finally {
|
|
753
|
+
await safeLogout(client);
|
|
630
754
|
}
|
|
631
|
-
if (headers.has("list-unsubscribe") || headers.has("list-id"))
|
|
632
|
-
return true;
|
|
633
|
-
return false;
|
|
634
755
|
}
|
|
635
756
|
/**
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
* Returns messages with UIDs greater than `sinceUid`, filtered by the
|
|
639
|
-
* agent's address (TO header). Includes envelope, body, and auto-reply
|
|
640
|
-
* detection headers in a single IMAP FETCH.
|
|
641
|
-
*
|
|
642
|
-
* Unlike fetchRecent (optimised for tool output with pagination),
|
|
643
|
-
* this function fetches everything needed for auto-respond in one pass:
|
|
644
|
-
* envelope for threading, body for agent input, headers for loop guards.
|
|
757
|
+
* Get the inbox message count.
|
|
645
758
|
*/
|
|
646
|
-
export async function
|
|
647
|
-
const { sinceUid, agentAddress, limit = 20 } = options;
|
|
759
|
+
export async function getInboxCount(config, password) {
|
|
648
760
|
const client = createClient(config, password);
|
|
649
|
-
const t0 = Date.now();
|
|
650
|
-
diag("fetchForAutoRespond", "start", `sinceUid=${sinceUid} agentAddress=${agentAddress} limit=${limit}`);
|
|
651
761
|
try {
|
|
652
762
|
await client.connect();
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
diag("fetchForAutoRespond", "mailbox locked", `${Date.now() - t0}ms`);
|
|
656
|
-
try {
|
|
657
|
-
// Search for messages with UID > sinceUid
|
|
658
|
-
const criteria = {};
|
|
659
|
-
if (sinceUid > 0) {
|
|
660
|
-
criteria.uid = `${sinceUid + 1}:*`;
|
|
661
|
-
}
|
|
662
|
-
else {
|
|
663
|
-
// First poll: only fetch recent messages (last 7 days)
|
|
664
|
-
const since = new Date();
|
|
665
|
-
since.setDate(since.getDate() - DEFAULT_SINCE_DAYS);
|
|
666
|
-
criteria.since = since;
|
|
667
|
-
}
|
|
668
|
-
const t1 = Date.now();
|
|
669
|
-
const searchResult = await client.search(criteria, { uid: true });
|
|
670
|
-
const uids = searchResult || [];
|
|
671
|
-
diag("fetchForAutoRespond", "search done", `${uids.length} UIDs in ${Date.now() - t1}ms`);
|
|
672
|
-
if (uids.length === 0)
|
|
673
|
-
return [];
|
|
674
|
-
// Take the oldest N UIDs (process in chronological order)
|
|
675
|
-
const processUids = uids.slice(0, limit);
|
|
676
|
-
const messages = [];
|
|
677
|
-
const t2 = Date.now();
|
|
678
|
-
const headerSection = `HEADER.FIELDS (${AUTO_REPLY_HEADER_FIELDS.join(" ")})`;
|
|
679
|
-
for await (const msg of client.fetch(processUids.map(String).join(","), {
|
|
680
|
-
envelope: true,
|
|
681
|
-
uid: true,
|
|
682
|
-
bodyStructure: true,
|
|
683
|
-
bodyParts: [
|
|
684
|
-
{ key: "1", start: 0, maxLength: BODY_MAX_BYTES },
|
|
685
|
-
],
|
|
686
|
-
headers: AUTO_REPLY_HEADER_FIELDS.map((h) => h.toLowerCase()),
|
|
687
|
-
}, { uid: true })) {
|
|
688
|
-
// Client-side TO filter: only messages addressed to the agent
|
|
689
|
-
if (!matchesRecipient(msg.envelope ?? {}, agentAddress))
|
|
690
|
-
continue;
|
|
691
|
-
const envelope = msg.envelope ?? {};
|
|
692
|
-
const fromAddr = envelope.from?.[0]?.address ?? "";
|
|
693
|
-
const toAddrs = (envelope.to ?? []).map((a) => a.address ?? "");
|
|
694
|
-
const subject = envelope.subject ?? "(no subject)";
|
|
695
|
-
const date = envelope.date?.toISOString() ?? new Date().toISOString();
|
|
696
|
-
const messageId = envelope.messageId ?? "";
|
|
697
|
-
const inReplyTo = envelope.inReplyTo ?? null;
|
|
698
|
-
// Extract References from envelope (not available in all IMAP servers)
|
|
699
|
-
// Fall back to null — threading still works with just In-Reply-To
|
|
700
|
-
const references = null;
|
|
701
|
-
// Extract body
|
|
702
|
-
const body = extractBodyFromFetch(msg);
|
|
703
|
-
// Detect auto-reply from raw headers (Buffer from ImapFlow)
|
|
704
|
-
const rawHeaders = msg.headers ? msg.headers.toString("utf-8") : "";
|
|
705
|
-
const headerMap = parseRawHeaders(rawHeaders);
|
|
706
|
-
const isAutoReply = detectAutoReply(headerMap);
|
|
707
|
-
messages.push({
|
|
708
|
-
uid: msg.uid,
|
|
709
|
-
messageId,
|
|
710
|
-
from: fromAddr,
|
|
711
|
-
to: toAddrs,
|
|
712
|
-
subject,
|
|
713
|
-
date,
|
|
714
|
-
inReplyTo,
|
|
715
|
-
references,
|
|
716
|
-
body,
|
|
717
|
-
isAutoReply,
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
diag("fetchForAutoRespond", "fetch done", `${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
|
|
721
|
-
return messages;
|
|
722
|
-
}
|
|
723
|
-
finally {
|
|
724
|
-
lock.release();
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
catch (err) {
|
|
728
|
-
diag("fetchForAutoRespond", "FAILED", `${err instanceof Error ? err.message : String(err)} after ${Date.now() - t0}ms`);
|
|
729
|
-
throw err;
|
|
763
|
+
const mailbox = await client.mailboxOpen("INBOX");
|
|
764
|
+
return mailbox.exists;
|
|
730
765
|
}
|
|
731
766
|
finally {
|
|
732
767
|
await safeLogout(client);
|