primcli 1.36.0 → 1.38.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.
@@ -0,0 +1,4514 @@
1
+ import { h as sendPreparedSignal, p as prepareSignalEmail } from "./repeats-CQmyvUGQ-CMnOmWgL.js";
2
+ import { d as requireDefaultLoginProfile } from "./connected-agent-profile-8tpOMdq3.js";
3
+ import { C as getEmail, K as listSentEmails, V as listEndpoints, a as createEndpoint, ct as searchEmails, dt as sendEmail, st as sdk_gen_exports } from "./sdk.gen-CugwPoCf.js";
4
+ import { C as resolveCliAuth, S as normalizeApiBaseUrl, T as saveSignupCredentials, _ as deleteCliCredentials, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, i as loadCliConfig, j as PrimitiveApiClient, m as cliAccessTokenExpiresAt, p as acquireCliCredentialsLock, w as saveCliCredentials, x as loadCliCredentials, y as detectPrimitiveKeyEnvMisname } from "./cli-config-UOlrfcPz.js";
5
+ import { _ as acquireListenLock, a as mailLockBusy, b as listenProcessIdentity, c as mailTime, d as readMailJson, f as removeMailFile, h as writeMailJson, i as mailId, l as privateMailDirectory, m as withMailLock, n as invalidSharedMail, o as mailObject, p as syncMailDirectory, r as mailAddress, s as mailString, t as SharedMailStateError, v as compareListenProcessIdentity, y as listenIdentity } from "./shared-mail-files-CZqI1kj6.js";
6
+ import { F as readBeforeDeadline, I as scopedChatSenderTrust, N as isPlainChatReply, S as recordSentSignal } from "./auto-signals-UPcJmV3j.js";
7
+ import { Command, Errors, Flags } from "@oclif/core";
8
+ import { opendirSync, readFileSync, watch, writeFileSync } from "node:fs";
9
+ import { createHash, randomUUID, scryptSync } from "node:crypto";
10
+ import { dirname, join } from "node:path";
11
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
12
+ //#region src/oclif/api-client.ts
13
+ const API_HEADERS_ENV = "PRIMITIVE_API_HEADERS";
14
+ const OAUTH_REFRESH_SKEW_MS = 60 * 1e3;
15
+ const SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE = "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
16
+ function mergeHeaders$1(...headers) {
17
+ const merged = {};
18
+ for (const headerSet of headers) {
19
+ if (!headerSet) continue;
20
+ for (const [name, value] of Object.entries(headerSet)) merged[name] = value;
21
+ }
22
+ return Object.keys(merged).length > 0 ? merged : void 0;
23
+ }
24
+ function parseHeadersJson(raw) {
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ } catch (error) {
29
+ const detail = error instanceof Error ? error.message : String(error);
30
+ throw new Errors.CLIError(`${API_HEADERS_ENV} must be valid JSON object syntax: ${detail}`, { exit: 1 });
31
+ }
32
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Errors.CLIError(`${API_HEADERS_ENV} must be a JSON object.`, { exit: 1 });
33
+ const headers = {};
34
+ for (const [rawName, rawValue] of Object.entries(parsed)) {
35
+ const name = validateCliHeaderName(rawName);
36
+ if (typeof rawValue !== "string") throw new Errors.CLIError(`${API_HEADERS_ENV}.${name} must be a string.`, { exit: 1 });
37
+ headers[name] = validateCliHeaderValue(rawValue, name);
38
+ }
39
+ return headers;
40
+ }
41
+ function cliApiHeadersFromEnv(env = process.env) {
42
+ const rawGenericHeaders = env[API_HEADERS_ENV]?.trim();
43
+ return rawGenericHeaders ? parseHeadersJson(rawGenericHeaders) : void 0;
44
+ }
45
+ function resolveCliApiRequestConfig(params) {
46
+ if ((params.env ?? process.env)["PRIMITIVE_AGENT_PROFILE"]?.trim()) {
47
+ const auth = resolveCliAuth(params);
48
+ return {
49
+ apiBaseUrl: auth.apiBaseUrl,
50
+ resolvedApiBaseUrl: auth.apiBaseUrl,
51
+ baseUrlOverridden: params.apiBaseUrl !== void 0,
52
+ environmentName: null
53
+ };
54
+ }
55
+ const currentEnvironment = resolveConfigEnvironment(loadCliConfig(params.configDir));
56
+ const configuredApiBaseUrl = currentEnvironment?.config.api_base_url;
57
+ if (currentEnvironment !== null && currentEnvironment.name !== "default" && params.apiBaseUrl === void 0 && configuredApiBaseUrl === void 0) throw new Errors.CLIError(`The active Primitive CLI environment \`${currentEnvironment.name}\` does not specify an api_base_url. Set one with \`primitive config set --environment ${currentEnvironment.name} --api-base-url https://...\`, or switch to a different environment with \`primitive config use <name>\`. Refusing to fall back to the production default for a non-default environment.`, { exit: 1 });
58
+ const apiBaseUrl = params.apiBaseUrl !== void 0 ? normalizeApiBaseUrl(params.apiBaseUrl) : configuredApiBaseUrl;
59
+ return {
60
+ apiBaseUrl,
61
+ baseUrlOverridden: apiBaseUrl !== void 0,
62
+ environmentName: currentEnvironment?.name ?? null,
63
+ headers: mergeHeaders$1(currentEnvironment?.config.headers, cliApiHeadersFromEnv(params.env)),
64
+ resolvedApiBaseUrl: normalizeApiBaseUrl(apiBaseUrl)
65
+ };
66
+ }
67
+ function createCliApiClient(params) {
68
+ const requestConfig = resolveCliApiRequestConfig(params);
69
+ return {
70
+ apiClient: new PrimitiveApiClient({
71
+ apiKey: params.apiKey,
72
+ apiBaseUrl: requestConfig.resolvedApiBaseUrl,
73
+ headers: requestConfig.headers
74
+ }),
75
+ requestConfig
76
+ };
77
+ }
78
+ function oauthTokenEndpoint(apiBaseUrl) {
79
+ const url = new URL(apiBaseUrl);
80
+ url.pathname = "/oauth/token";
81
+ url.search = "";
82
+ url.hash = "";
83
+ return url.toString();
84
+ }
85
+ function shouldRefresh(credentials, now) {
86
+ const expiresAt = Date.parse(credentials.expires_at);
87
+ if (!Number.isFinite(expiresAt)) return true;
88
+ return expiresAt <= now() + OAUTH_REFRESH_SKEW_MS;
89
+ }
90
+ function isOAuthRefreshSuccess(value) {
91
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
92
+ const row = value;
93
+ return typeof row.access_token === "string" && typeof row.refresh_token === "string" && row.token_type === "Bearer" && typeof row.expires_in === "number" && Number.isFinite(row.expires_in) && row.expires_in > 0;
94
+ }
95
+ function oauthErrorCode(value) {
96
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
97
+ const raw = value.error;
98
+ return typeof raw === "string" ? raw : null;
99
+ }
100
+ function oauthErrorDescription(value) {
101
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
102
+ const raw = value.error_description;
103
+ return typeof raw === "string" && raw.trim() ? raw : null;
104
+ }
105
+ async function refreshStoredCliCredentials(params) {
106
+ const now = params.now ?? Date.now;
107
+ if (!shouldRefresh(params.credentials, now)) return params.credentials;
108
+ let releaseLock;
109
+ try {
110
+ if (!params.credentialsLockHeld) try {
111
+ releaseLock = acquireCliCredentialsLock(params.configDir);
112
+ } catch (error) {
113
+ const detail = error instanceof Error ? error.message : String(error);
114
+ throw new Errors.CLIError(detail, { exit: 1 });
115
+ }
116
+ const current = loadCliCredentials(params.configDir);
117
+ if (!current) throw new Errors.CLIError("Saved Primitive CLI OAuth session is no longer available. Run `primitive signin` to authenticate again.", { exit: 1 });
118
+ if (!shouldRefresh(current, now)) return current;
119
+ const fetchImpl = params.fetch ?? fetch;
120
+ const body = new URLSearchParams({
121
+ client_id: current.oauth_client_id,
122
+ grant_type: "refresh_token",
123
+ refresh_token: current.refresh_token
124
+ });
125
+ let response;
126
+ try {
127
+ response = await fetchImpl(oauthTokenEndpoint(params.apiBaseUrl), {
128
+ body,
129
+ headers: {
130
+ ...params.headers ?? {},
131
+ "content-type": "application/x-www-form-urlencoded"
132
+ },
133
+ method: "POST"
134
+ });
135
+ } catch (error) {
136
+ const detail = error instanceof Error ? error.message : String(error);
137
+ throw new Errors.CLIError(`Could not refresh saved Primitive CLI OAuth credentials: ${detail}`, { exit: 1 });
138
+ }
139
+ const payload = await response.json().catch(() => null);
140
+ if (!response.ok) {
141
+ const code = oauthErrorCode(payload);
142
+ const description = oauthErrorDescription(payload);
143
+ if (code === "invalid_grant") {
144
+ deleteCliCredentials(params.configDir);
145
+ throw new Errors.CLIError(SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE, { exit: 1 });
146
+ }
147
+ throw new Errors.CLIError(`Could not refresh saved Primitive CLI OAuth credentials${description ? `: ${description}` : "."}`, { exit: 1 });
148
+ }
149
+ if (!isOAuthRefreshSuccess(payload)) throw new Errors.CLIError("Primitive OAuth token endpoint returned an unexpected refresh response.", { exit: 1 });
150
+ const next = {
151
+ ...current,
152
+ access_token: payload.access_token,
153
+ expires_at: cliAccessTokenExpiresAt(payload.expires_in, now),
154
+ refresh_token: payload.refresh_token,
155
+ token_type: payload.token_type
156
+ };
157
+ saveCliCredentials(params.configDir, next);
158
+ return next;
159
+ } finally {
160
+ releaseLock?.();
161
+ }
162
+ }
163
+ async function createAuthenticatedCliApiClient(params) {
164
+ const requestConfig = resolveCliApiRequestConfig(params);
165
+ let auth = resolveCliAuth({
166
+ apiKey: params.apiKey,
167
+ apiBaseUrl: requestConfig.apiBaseUrl,
168
+ configDir: params.configDir,
169
+ env: params.env
170
+ });
171
+ if (auth.apiKey === void 0) {
172
+ const hint = detectPrimitiveKeyEnvMisname(params.env ?? process.env);
173
+ if (hint) process.stderr.write(`${hint}\n`);
174
+ }
175
+ if (auth.source === "stored" && auth.credentials) {
176
+ const refreshed = await refreshStoredCliCredentials({
177
+ apiBaseUrl: auth.apiBaseUrl,
178
+ configDir: params.configDir,
179
+ credentials: auth.credentials,
180
+ credentialsLockHeld: params.credentialsLockHeld,
181
+ fetch: params.fetch,
182
+ headers: requestConfig.headers,
183
+ now: params.now
184
+ });
185
+ auth = {
186
+ ...auth,
187
+ apiKey: refreshed.access_token,
188
+ credentials: refreshed
189
+ };
190
+ }
191
+ return {
192
+ apiClient: new PrimitiveApiClient({
193
+ apiKey: auth.apiKey,
194
+ apiBaseUrl: auth.apiBaseUrl,
195
+ headers: auth.connectedAgent ? void 0 : requestConfig.headers
196
+ }),
197
+ auth,
198
+ baseUrlOverridden: requestConfig.baseUrlOverridden,
199
+ requestConfig
200
+ };
201
+ }
202
+ //#endregion
203
+ //#region src/oclif/attachment-wait.ts
204
+ /** Generated download commands that wait for attachment content by default. */
205
+ const ATTACHMENT_WAIT_OPERATIONS = new Set([
206
+ "downloadAttachments",
207
+ "downloadEmailAttachmentPart",
208
+ "downloadSentAttachmentPart"
209
+ ]);
210
+ /** Longest a download command waits for attachment content, in total. */
211
+ const ATTACHMENT_WAIT_MAX_MS = 6e4;
212
+ const INITIAL_DELAY_MS = 1e3;
213
+ const MAX_DELAY_MS = 8e3;
214
+ const ATTACHMENT_WAIT_FLAG_DESCRIPTION = `Wait and retry while the attachment content is not ready yet (attachment_not_ready), for up to ${ATTACHMENT_WAIT_MAX_MS / 1e3}s. Pass --no-wait to return the error immediately.`;
215
+ /** Seconds from a Retry-After header, or undefined when absent or not a number of seconds. */
216
+ function retryAfterMs(response) {
217
+ const header = response?.headers?.get("retry-after");
218
+ if (!header || !/^\d+$/.test(header.trim())) return void 0;
219
+ return Number(header.trim()) * 1e3;
220
+ }
221
+ /**
222
+ * Delay before the next attempt: the server's Retry-After when it gives one,
223
+ * otherwise exponential backoff from 1s, capped at 8s either way, and never
224
+ * past the remaining budget.
225
+ */
226
+ function nextAttachmentWaitDelay(attempt, remainingMs, serverHintMs) {
227
+ const backoff = INITIAL_DELAY_MS * 2 ** Math.min(attempt, 10);
228
+ return Math.max(0, Math.min(Math.min(serverHintMs ?? backoff, MAX_DELAY_MS), remainingMs));
229
+ }
230
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
231
+ /**
232
+ * Run `call` and, while it fails with attachment_not_ready, wait and run it
233
+ * again until it succeeds, fails differently, or the budget is spent. The last
234
+ * result is returned either way, so the caller reports it exactly as before.
235
+ */
236
+ async function withAttachmentWait(call, options) {
237
+ const now = options.now ?? Date.now;
238
+ const sleep = options.sleep ?? defaultSleep;
239
+ const maxWaitMs = options.maxWaitMs ?? 6e4;
240
+ const started = now();
241
+ let result = await call();
242
+ for (let attempt = 0; options.wait; attempt++) {
243
+ if (!result.error || options.errorCode(result.error) !== "attachment_not_ready") break;
244
+ const remaining = maxWaitMs - (now() - started);
245
+ if (remaining <= 0) break;
246
+ if (attempt === 0) options.onFirstWait?.();
247
+ await sleep(nextAttachmentWaitDelay(attempt, remaining, retryAfterMs(result.response)));
248
+ result = await call();
249
+ }
250
+ return result;
251
+ }
252
+ //#endregion
253
+ //#region src/oclif/automated-filter.ts
254
+ /**
255
+ * The server-side `automated` verdict on inbound email: `automated`
256
+ * (boolean) and `automated_reasons` (why, in rule order), decided by
257
+ * the API when the email arrives, and the `automated=true|false` filter
258
+ * on GET /emails and GET /emails/search.
259
+ *
260
+ * A server that predates it either rejects the `automated` filter (GET
261
+ * /emails validates its query strictly) or ignores it and returns rows
262
+ * without the fields (GET /emails/search). Both must fail loudly: a CLI
263
+ * that read a missing verdict as "not automated" would hand an agent
264
+ * every bounce and newsletter as mail from a person, and one that fell
265
+ * back to deciding it locally would re-read all unanswered automated
266
+ * mail on every call.
267
+ */
268
+ const AUTOMATED_FILTER_UNSUPPORTED_CODE = "automated_filter_unsupported";
269
+ const AUTOMATED_VALUES = ["true", "false"];
270
+ const AUTOMATED_FLAG_DESCRIPTION = "Only emails whose server-decided `automated` verdict is this value: `false` = sent by a person, `true` = bounces and delivery reports, auto-replies, list and newsletter mail, and mail from the address it was sent to. Fails if the server does not support the automated filter yet.";
271
+ var AutomatedFilterUnsupportedError = class extends Error {
272
+ code = AUTOMATED_FILTER_UNSUPPORTED_CODE;
273
+ constructor(detail) {
274
+ super(`The server does not support the \`automated\` filter yet: ${detail} Nothing was treated as sent by a person. Upgrade to a server that returns \`automated\` and \`automated_reasons\`, or pass --include-automated.`);
275
+ this.name = "AutomatedFilterUnsupportedError";
276
+ }
277
+ };
278
+ /** Like AutomatedFilterUnsupportedError, for a probe that failed. */
279
+ var AutomatedFilterUnverifiedError = class extends AutomatedFilterUnsupportedError {
280
+ constructor(detail) {
281
+ super(detail);
282
+ this.message = `Could not verify that the server supports the \`automated\` filter: ${detail} Nothing was treated as matching. Retry the command.`;
283
+ this.name = "AutomatedFilterUnverifiedError";
284
+ this.code = "automated_filter_unverified";
285
+ }
286
+ };
287
+ function hasAutomatedVerdict(row) {
288
+ if (row === null || typeof row !== "object") return false;
289
+ const candidate = row;
290
+ return typeof candidate.automated === "boolean" && Array.isArray(candidate.automated_reasons) && candidate.automated_reasons.every((reason) => typeof reason === "string");
291
+ }
292
+ /**
293
+ * Throw unless every row carries the verdict, and, for each `expected`
294
+ * value given, unless every row has that value: a server that accepted
295
+ * the filter but did not apply it must not pass for one that did.
296
+ */
297
+ function assertAutomatedVerdict(rows, surface, expected) {
298
+ const missing = rows.filter((row) => !hasAutomatedVerdict(row)).length;
299
+ if (missing > 0) throw new AutomatedFilterUnsupportedError(`${surface} returned ${missing} of ${rows.length} email${rows.length === 1 ? "" : "s"} without the \`automated\` and \`automated_reasons\` fields.`);
300
+ const verdicts = expected === void 0 ? [] : typeof expected === "boolean" ? [expected] : expected;
301
+ for (const verdict of verdicts) {
302
+ const wrong = rows.filter((row) => row.automated !== verdict).length;
303
+ if (wrong > 0) throw new AutomatedFilterUnsupportedError(`${surface} ignored \`automated=${verdict}\` and returned ${wrong} email${wrong === 1 ? "" : "s"} with \`automated=${!verdict}\`.`);
304
+ }
305
+ }
306
+ /**
307
+ * The verdicts the `automated:` terms in a search DSL query require.
308
+ * The DSL joins its terms with AND, so every `automated:true|false`
309
+ * term must hold for every returned row. Quoted text is skipped, so a
310
+ * phrase that merely contains "automated:false" does not count.
311
+ */
312
+ function automatedVerdictsFromQuery(q) {
313
+ if (typeof q !== "string") return [];
314
+ const verdicts = [];
315
+ const tokens = q.match(/(?:[^\s"]|"(?:[^"\\]|\\.)*"?)+/g) ?? [];
316
+ for (const token of tokens) {
317
+ const match = /^automated:"?(true|false)"?$/i.exec(token);
318
+ if (match) verdicts.push(match[1].toLowerCase() === "true");
319
+ }
320
+ return verdicts;
321
+ }
322
+ /**
323
+ * Every verdict a list or search call asked for: the `automated`
324
+ * parameter plus any `automated:` terms in the search query.
325
+ */
326
+ function expectedAutomatedVerdicts(automated, q) {
327
+ return [...automated === void 0 ? [] : [automated], ...automatedVerdictsFromQuery(q)];
328
+ }
329
+ /** True when a search DSL query uses the `automated:` term. */
330
+ function queryUsesAutomated(q) {
331
+ return typeof q === "string" && /(^|[\s(])-?automated:/i.test(q);
332
+ }
333
+ function errorBody$1(payload) {
334
+ if (payload === null || typeof payload !== "object") return {};
335
+ const inner = payload.error;
336
+ return inner !== null && typeof inner === "object" ? inner : payload;
337
+ }
338
+ /**
339
+ * True when an API error is an older server rejecting the `automated`
340
+ * query parameter or the `automated:` search term as unknown.
341
+ */
342
+ function isAutomatedRejectedError(payload) {
343
+ const body = errorBody$1(payload);
344
+ if (body.code !== "validation_error") return false;
345
+ const message = typeof body.message === "string" ? body.message : JSON.stringify(payload);
346
+ return /automated/i.test(message) && /unrecogni[sz]ed|unknown/i.test(message);
347
+ }
348
+ function automatedRejectedError(surface) {
349
+ return new AutomatedFilterUnsupportedError(`${surface} rejected the \`automated\` filter as unknown.`);
350
+ }
351
+ const AUTOMATED_SURFACES = {
352
+ listEmails: "GET /emails",
353
+ searchEmails: "GET /emails/search"
354
+ };
355
+ /**
356
+ * For the generated `emails list` / `emails search` commands: the
357
+ * surface name when this call filtered on `automated` (the parameter or
358
+ * an `automated:` search term), else null.
359
+ */
360
+ function automatedSurfaceForOperation(sdkName, query) {
361
+ const surface = AUTOMATED_SURFACES[sdkName];
362
+ if (!surface || !query) return null;
363
+ const q = typeof query.q === "string" ? query.q : void 0;
364
+ return query.automated !== void 0 || queryUsesAutomated(q) ? surface : null;
365
+ }
366
+ const searchSupportProbes$1 = /* @__PURE__ */ new WeakMap();
367
+ /**
368
+ * GET /emails/search ignores unknown parameters, so an empty page for
369
+ * `automated=...` does not show that the server applied the filter.
370
+ * Read one unfiltered row: if it lacks the verdict the server does not
371
+ * support it. No rows at all means the mailbox is empty, so the empty
372
+ * answer stands. One probe per API client per process.
373
+ */
374
+ function ensureSearchReportsAutomated(apiClient) {
375
+ const key = apiClient;
376
+ let probe = searchSupportProbes$1.get(key);
377
+ if (!probe) {
378
+ probe = (async () => {
379
+ const result = await searchEmails({
380
+ client: apiClient.client,
381
+ query: {
382
+ limit: 1,
383
+ include_facets: "false",
384
+ snippet: "false"
385
+ },
386
+ responseStyle: "fields"
387
+ });
388
+ if (result.error) throw new AutomatedFilterUnverifiedError("the empty result could not be checked: reading one unfiltered row from GET /emails/search failed.");
389
+ const rows = result.data?.data;
390
+ assertAutomatedVerdict(rows ?? [], "GET /emails/search");
391
+ })();
392
+ searchSupportProbes$1.set(key, probe);
393
+ probe.catch(() => {
394
+ if (searchSupportProbes$1.get(key) === probe) searchSupportProbes$1.delete(key);
395
+ });
396
+ }
397
+ return probe;
398
+ }
399
+ //#endregion
400
+ //#region src/oclif/endpoints-test-redirect.ts
401
+ async function detectFunctionEndpoint(endpointId, listEndpoints) {
402
+ let response;
403
+ try {
404
+ response = await listEndpoints();
405
+ } catch {
406
+ return null;
407
+ }
408
+ if (response.error) return null;
409
+ const rows = response.data?.data;
410
+ if (!Array.isArray(rows)) return null;
411
+ for (const row of rows) {
412
+ if (!row || typeof row !== "object") continue;
413
+ if (row.id !== endpointId) continue;
414
+ if (row.kind !== "function") return null;
415
+ const functionId = row.function_id;
416
+ if (typeof functionId !== "string" || functionId.length === 0) return null;
417
+ return {
418
+ endpointId,
419
+ functionId
420
+ };
421
+ }
422
+ return null;
423
+ }
424
+ function formatFunctionEndpointRedirect(match) {
425
+ return [
426
+ "This is a function endpoint. Function endpoints are tested differently. Run:",
427
+ "",
428
+ ` primitive functions test --id ${match.functionId}`,
429
+ "",
430
+ `(pass the function id, not the endpoint id. endpoint_id=${match.endpointId} function_id=${match.functionId})`
431
+ ].join("\n");
432
+ }
433
+ async function maybeWriteFunctionEndpointRedirect(inputs) {
434
+ if (inputs.sdkName !== "testEndpoint") return null;
435
+ if (inputs.errorCode !== "not_found") return null;
436
+ if (!inputs.endpointId) return null;
437
+ const match = await detectFunctionEndpoint(inputs.endpointId, inputs.listEndpoints);
438
+ if (!match) return null;
439
+ inputs.writeStderr(`${formatFunctionEndpointRedirect(match)}\n`);
440
+ return match;
441
+ }
442
+ //#endregion
443
+ //#region src/oclif/idempotent-replay-banner.ts
444
+ /**
445
+ * If `data` carries `idempotent_replay: true`, write a stderr notice
446
+ * that the message already went out and nothing new was sent.
447
+ * Otherwise no-op.
448
+ *
449
+ * The notice deliberately gives no advice on how to force a fresh
450
+ * copy. Whoever sees a replay has already sent this message; telling
451
+ * them to vary the content is an instruction to double-send.
452
+ */
453
+ function writeIdempotentReplayBannerIfReplay(data, options) {
454
+ if (!isReplay(data)) return;
455
+ options.write(`${formatAlreadySentNotice(data)}\n`);
456
+ }
457
+ function isReplay(data) {
458
+ if (data === null || typeof data !== "object") return false;
459
+ return data.idempotent_replay === true;
460
+ }
461
+ function formatAlreadySentNotice(sent) {
462
+ const details = [];
463
+ if (typeof sent.id === "string" && sent.id) details.push(`sent id ${sent.id}`);
464
+ const status = typeof sent.status === "string" && sent.status ? sent.status : null;
465
+ const deliveryStatus = typeof sent.delivery_status === "string" && sent.delivery_status ? sent.delivery_status : null;
466
+ if (status) details.push(`status ${status}`);
467
+ if (deliveryStatus && deliveryStatus !== status) details.push(`delivery status ${deliveryStatus}`);
468
+ return `Already sent: this exact message went out earlier${details.length > 0 ? ` (${details.join(", ")})` : ""}. Nothing new was sent.`;
469
+ }
470
+ //#endregion
471
+ //#region src/oclif/reply-state.ts
472
+ const AWAITING_VALUES = ["you", "them"];
473
+ const AWAITING_FLAG_DESCRIPTION = "Only emails whose reply state is this value: `you` = waiting on your reply (the thread's latest message is inbound), `them` = you replied last. Queued and scheduled replies count as replied. Delivered mail only: rejected mail is never returned. Fails if the server does not report reply state yet.";
474
+ const REPLY_STATE_UNSUPPORTED_CODE = "reply_state_unsupported";
475
+ /** True when a search DSL query uses the `awaiting:` term. */
476
+ function queryUsesAwaiting(q) {
477
+ return typeof q === "string" && /(^|[\s(])-?awaiting:/i.test(q);
478
+ }
479
+ var ReplyStateUnsupportedError = class extends Error {
480
+ code = REPLY_STATE_UNSUPPORTED_CODE;
481
+ constructor(detail) {
482
+ super(`The server does not support reply state yet: ${detail} Nothing was treated as awaiting your reply. Upgrade to a server that returns \`awaiting\` and \`reply_count\`, or run without reply-state options.`);
483
+ this.name = "ReplyStateUnsupportedError";
484
+ }
485
+ };
486
+ /**
487
+ * The server's reply-state support could not be established (a probe
488
+ * failed). Reported like ReplyStateUnsupportedError so callers never
489
+ * read an unverified empty answer as "nothing matched".
490
+ */
491
+ var ReplyStateUnverifiedError = class extends ReplyStateUnsupportedError {
492
+ constructor(detail) {
493
+ super(detail);
494
+ this.message = `Could not verify that the server supports reply state: ${detail} Nothing was treated as matching. Retry the command.`;
495
+ this.name = "ReplyStateUnverifiedError";
496
+ this.code = "reply_state_unverified";
497
+ }
498
+ };
499
+ function hasReplyState(row) {
500
+ if (row === null || typeof row !== "object") return false;
501
+ const candidate = row;
502
+ return (candidate.awaiting === "you" || candidate.awaiting === "them") && typeof candidate.reply_count === "number" && (candidate.last_replied_at === null || typeof candidate.last_replied_at === "string");
503
+ }
504
+ /**
505
+ * Throw unless every row carries reply state. An empty page proves
506
+ * nothing either way; callers that asked for reply state follow an
507
+ * empty search page with ensureSearchReportsReplyState(). `surface` names the
508
+ * response for the error, e.g. "GET /emails".
509
+ */
510
+ function assertReplyState(rows, surface) {
511
+ const missing = rows.filter((row) => !hasReplyState(row)).length;
512
+ if (missing > 0) throw new ReplyStateUnsupportedError(`${surface} returned ${missing} of ${rows.length} email${rows.length === 1 ? "" : "s"} without the \`awaiting\` and \`reply_count\` fields.`);
513
+ }
514
+ const AWAITING_REJECTED_UNSUPPORTED_CODE = "awaiting_rejected_unsupported";
515
+ /**
516
+ * The `awaiting` filter matches delivered mail only: a rejected email
517
+ * (for example refused over the storage limit) was never delivered, so it
518
+ * awaits no one. A server that returns one through the filter predates
519
+ * that, and an agent would be handed mail nobody can answer.
520
+ */
521
+ var AwaitingIncludesRejectedError = class extends ReplyStateUnsupportedError {
522
+ constructor(detail) {
523
+ super(detail);
524
+ this.message = `The server's \`awaiting\` filter does not exclude undelivered mail yet: ${detail} Upgrade to a server whose \`awaiting\` filter matches delivered mail only.`;
525
+ this.name = "AwaitingIncludesRejectedError";
526
+ this.code = AWAITING_REJECTED_UNSUPPORTED_CODE;
527
+ }
528
+ };
529
+ /**
530
+ * For rows returned BY an `awaiting` filter (not the unfiltered probe):
531
+ * every row carries reply state, and none has status `rejected`.
532
+ */
533
+ function assertAwaitingFilterRows(rows, surface) {
534
+ assertReplyState(rows, surface);
535
+ const rejected = rows.filter((row) => row !== null && typeof row === "object" && row.status === "rejected").length;
536
+ if (rejected > 0) throw new AwaitingIncludesRejectedError(`${surface} returned ${rejected} email${rejected === 1 ? "" : "s"} with status \`rejected\`.`);
537
+ }
538
+ function errorBody(payload) {
539
+ if (payload === null || typeof payload !== "object") return {};
540
+ const inner = payload.error;
541
+ return inner !== null && typeof inner === "object" ? inner : payload;
542
+ }
543
+ /**
544
+ * True when an API error is an older server rejecting the `awaiting`
545
+ * query parameter or the `awaiting:` search term as unknown. A current
546
+ * server accepts both, and the CLI only ever sends valid values.
547
+ */
548
+ function isAwaitingRejectedError(payload) {
549
+ const body = errorBody(payload);
550
+ if (body.code !== "validation_error") return false;
551
+ const message = typeof body.message === "string" ? body.message : JSON.stringify(payload);
552
+ return /awaiting/i.test(message) && /unrecogni[sz]ed|unknown/i.test(message);
553
+ }
554
+ function awaitingRejectedError(surface) {
555
+ return new ReplyStateUnsupportedError(`${surface} rejected the \`awaiting\` filter as unknown.`);
556
+ }
557
+ /** Short column value for tables: "you", "them", or "-" when absent. */
558
+ function formatAwaitingCell(row) {
559
+ if (!hasReplyState(row)) return "-";
560
+ return row.awaiting;
561
+ }
562
+ /** Reply count for tables, or "-" when the server did not report it. */
563
+ function formatRepliesCell(row) {
564
+ if (!hasReplyState(row)) return "-";
565
+ return String(row.reply_count);
566
+ }
567
+ const REPLY_STATE_SURFACES = {
568
+ listEmails: "GET /emails",
569
+ searchEmails: "GET /emails/search"
570
+ };
571
+ /**
572
+ * For the generated `emails list` / `emails search` commands: the
573
+ * surface name when this call asked for reply state (an `awaiting`
574
+ * parameter or an `awaiting:` search term), else null.
575
+ */
576
+ function replyStateSurfaceForOperation(sdkName, query) {
577
+ const surface = REPLY_STATE_SURFACES[sdkName];
578
+ if (!surface || !query) return null;
579
+ const q = typeof query.q === "string" ? query.q : void 0;
580
+ return query.awaiting !== void 0 || queryUsesAwaiting(q) ? surface : null;
581
+ }
582
+ const searchSupportProbes = /* @__PURE__ */ new WeakMap();
583
+ /**
584
+ * GET /emails/search ignores unknown parameters, so an empty page for
585
+ * `awaiting=...` does not show that the server applied the filter.
586
+ * Read one unfiltered row: if it lacks reply state the server does not
587
+ * support it and this throws ReplyStateUnsupportedError. No rows at all
588
+ * means the mailbox is empty, so the empty answer stands.
589
+ */
590
+ function ensureSearchReportsReplyState(apiClient) {
591
+ const key = apiClient;
592
+ let probe = searchSupportProbes.get(key);
593
+ if (!probe) {
594
+ probe = (async () => {
595
+ const result = await searchEmails({
596
+ client: apiClient.client,
597
+ query: {
598
+ limit: 1,
599
+ include_facets: "false",
600
+ snippet: "false"
601
+ },
602
+ responseStyle: "fields"
603
+ });
604
+ if (result.error) throw new ReplyStateUnverifiedError("the empty result could not be checked: reading one unfiltered row from GET /emails/search failed.");
605
+ const rows = result.data?.data;
606
+ assertReplyState(rows ?? [], "GET /emails/search");
607
+ })();
608
+ searchSupportProbes.set(key, probe);
609
+ probe.catch(() => {
610
+ if (searchSupportProbes.get(key) === probe) searchSupportProbes.delete(key);
611
+ });
612
+ }
613
+ return probe;
614
+ }
615
+ //#endregion
616
+ //#region src/oclif/api-command.ts
617
+ const API_ERROR_CODES = {
618
+ accessDenied: "access_denied",
619
+ authorizationPending: "authorization_pending",
620
+ expiredToken: "expired_token",
621
+ invalidDeviceCode: "invalid_device_code",
622
+ notFound: "not_found",
623
+ slowDown: "slow_down",
624
+ unauthorized: "unauthorized"
625
+ };
626
+ function flagName(parameterName) {
627
+ return parameterName.replace(/_/g, "-");
628
+ }
629
+ function flagDescription(parameter) {
630
+ return parameter.description ?? parameter.name;
631
+ }
632
+ const numberFlag = Flags.custom({ async parse(input, _context, options) {
633
+ const trimmed = input.trim();
634
+ if (trimmed === "") throw new Errors.CLIError(`Expected a number but received: ${input}`);
635
+ const value = Number(trimmed);
636
+ if (!Number.isFinite(value)) throw new Errors.CLIError(`Expected a number but received: ${input}`);
637
+ if (options.min !== void 0 && value < options.min) throw new Errors.CLIError(`Expected a number greater than or equal to ${options.min} but received: ${input}`);
638
+ if (options.max !== void 0 && value > options.max) throw new Errors.CLIError(`Expected a number less than or equal to ${options.max} but received: ${input}`);
639
+ return value;
640
+ } });
641
+ function numericFlagOptions(parameter) {
642
+ return {
643
+ ...typeof parameter.default === "number" ? { default: parameter.default } : {},
644
+ ...typeof parameter.maximum === "number" ? { max: parameter.maximum } : {},
645
+ ...typeof parameter.minimum === "number" ? { min: parameter.minimum } : {}
646
+ };
647
+ }
648
+ function extractBodyFields(schema) {
649
+ if (!schema || typeof schema !== "object") return [];
650
+ const properties = schema.properties;
651
+ if (!properties || typeof properties !== "object") return [];
652
+ const requiredArr = Array.isArray(schema.required) ? schema.required.filter((k) => typeof k === "string") : [];
653
+ const required = new Set(requiredArr);
654
+ const fields = [];
655
+ for (const [name, raw] of Object.entries(properties)) {
656
+ const propSchema = raw && typeof raw === "object" ? raw : {};
657
+ const t = propSchema.type;
658
+ let displayType = "any";
659
+ let kind = "complex";
660
+ if (typeof t === "string") {
661
+ displayType = t;
662
+ if (t === "string") kind = "string";
663
+ else if (t === "integer") kind = "integer";
664
+ else if (t === "number") kind = "number";
665
+ else if (t === "boolean") kind = "boolean";
666
+ else if (t === "array") {
667
+ const items = propSchema.items;
668
+ if (items && typeof items === "object") {
669
+ const itemType = items.type;
670
+ if (typeof itemType === "string") displayType = `array<${itemType}>`;
671
+ }
672
+ kind = "complex";
673
+ } else kind = "complex";
674
+ } else if (Array.isArray(t)) {
675
+ const nonNull = t.filter((s) => s !== "null");
676
+ if (nonNull.length === 1) {
677
+ const single = nonNull[0];
678
+ displayType = `${single}?`;
679
+ if (single === "string") kind = "string";
680
+ else if (single === "integer") kind = "integer";
681
+ else if (single === "number") kind = "number";
682
+ else if (single === "boolean") kind = "boolean";
683
+ else kind = "complex";
684
+ } else {
685
+ displayType = nonNull.join("|");
686
+ kind = "complex";
687
+ }
688
+ }
689
+ const description = typeof propSchema.description === "string" ? propSchema.description.split(/\n\s*\n/)[0].replace(/\s*\n\s*/g, " ").trim() : "";
690
+ const enumRaw = propSchema.enum;
691
+ const enumValues = kind === "string" && Array.isArray(enumRaw) ? enumRaw.filter((e) => typeof e === "string") : void 0;
692
+ fields.push({
693
+ name,
694
+ description,
695
+ required: required.has(name),
696
+ displayType,
697
+ kind,
698
+ ...enumValues && enumValues.length > 0 ? { enumValues } : {},
699
+ ...typeof propSchema.maximum === "number" ? { maximum: propSchema.maximum } : {},
700
+ ...typeof propSchema.minimum === "number" ? { minimum: propSchema.minimum } : {}
701
+ });
702
+ }
703
+ return fields.sort((a, b) => {
704
+ if (a.required !== b.required) return a.required ? -1 : 1;
705
+ return a.name.localeCompare(b.name);
706
+ });
707
+ }
708
+ /**
709
+ * Render a "Body fields" summary for the per-command help.
710
+ *
711
+ * Most scalar fields are exposed as individual `--flag` flags,
712
+ * which oclif auto-renders in the FLAGS section above. To avoid
713
+ * duplicating that, the summary here only documents fields that
714
+ * MUST go through `--raw-body` (complex types: arrays, objects,
715
+ * mixed-non-nullable). When an operation has only scalars, the
716
+ * summary is omitted entirely and oclif's FLAGS section is the
717
+ * full story.
718
+ *
719
+ * For operations with mixed scalar and complex fields, we also
720
+ * include a short header pointing the agent at the flag form so
721
+ * the natural reading is "use the flags above; --raw-body for
722
+ * the leftovers below."
723
+ */
724
+ function renderRequestSchemaSummary(schema) {
725
+ const fields = extractBodyFields(schema);
726
+ if (fields.length === 0) return null;
727
+ const complex = fields.filter((f) => f.kind === "complex");
728
+ if (complex.length === 0) return null;
729
+ const nameWidth = Math.min(24, Math.max(...complex.map((f) => f.name.length)));
730
+ const descMax = 78;
731
+ const lines = ["Body fields requiring --raw-body JSON (these are not exposed as flags):"];
732
+ for (const f of complex) {
733
+ const marker = f.required ? " *" : " ";
734
+ const padName = f.name.padEnd(nameWidth);
735
+ const trimmedDesc = f.description.length > descMax ? `${f.description.slice(0, descMax - 3)}...` : f.description;
736
+ const desc = trimmedDesc ? ` ${trimmedDesc}` : "";
737
+ lines.push(`${marker} ${padName} ${f.displayType}${desc}`);
738
+ }
739
+ lines.push("(* = required. Scalar body fields are exposed as individual --flag-name flags; see FLAGS above.)");
740
+ return lines.join("\n");
741
+ }
742
+ function flagForParameter(parameter) {
743
+ const common = {
744
+ description: flagDescription(parameter),
745
+ required: parameter.required
746
+ };
747
+ if (parameter.type === "boolean") return Flags.boolean(common);
748
+ if (parameter.type === "integer") return Flags.integer({
749
+ ...common,
750
+ ...numericFlagOptions(parameter)
751
+ });
752
+ if (parameter.type === "number") return numberFlag({
753
+ ...common,
754
+ ...numericFlagOptions(parameter)
755
+ });
756
+ if (parameter.enum && parameter.enum.length > 0) return Flags.string({
757
+ ...common,
758
+ options: parameter.enum
759
+ });
760
+ return Flags.string(common);
761
+ }
762
+ function coerceParameterValue(parameter, value) {
763
+ if (value === void 0) return;
764
+ if (parameter.type === "number") {
765
+ if (typeof value === "number") return value;
766
+ const parsed = Number(value);
767
+ if (Number.isNaN(parsed)) throw new Errors.CLIError(`Invalid number for --${parameter.name}: ${value}`);
768
+ return parsed;
769
+ }
770
+ if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
771
+ throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
772
+ }
773
+ function cliError(message) {
774
+ return new Errors.CLIError(message, { exit: 1 });
775
+ }
776
+ function parseJson(source, flagLabel) {
777
+ try {
778
+ return JSON.parse(source);
779
+ } catch (error) {
780
+ throw cliError(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
781
+ }
782
+ }
783
+ function readJsonBody(flags) {
784
+ const bodyFile = flags["body-file"];
785
+ const rawBody = flags["raw-body"];
786
+ if (bodyFile && rawBody) throw cliError("Use either --raw-body or --body-file, not both");
787
+ if (typeof bodyFile === "string") {
788
+ let contents;
789
+ try {
790
+ contents = readFileSync(bodyFile, "utf8");
791
+ } catch (error) {
792
+ throw cliError(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
793
+ }
794
+ return parseJson(contents, `--body-file ${bodyFile}`);
795
+ }
796
+ if (typeof rawBody === "string") return parseJson(rawBody, "--raw-body");
797
+ }
798
+ function readTextFileFlag(path, flagLabel) {
799
+ try {
800
+ return readFileSync(path, "utf8");
801
+ } catch (error) {
802
+ throw cliError(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
803
+ }
804
+ }
805
+ function extractErrorPayload(raw) {
806
+ if (raw && typeof raw === "object" && !(raw instanceof Error) && "error" in raw) {
807
+ const inner = raw.error;
808
+ if (inner !== null && inner !== void 0) return inner;
809
+ }
810
+ return raw;
811
+ }
812
+ function extractCauseDetails(cause) {
813
+ const details = {};
814
+ let code;
815
+ if (!cause || typeof cause !== "object") return { details };
816
+ for (const [key, value] of Object.entries(cause)) if (typeof value === "string" || typeof value === "number") {
817
+ details[key] = value;
818
+ if (key === "code" && typeof value === "string") code = value;
819
+ }
820
+ return {
821
+ code,
822
+ details
823
+ };
824
+ }
825
+ function formatErrorPayload(payload) {
826
+ if (payload instanceof Error) {
827
+ const { code, details } = extractCauseDetails(payload.cause);
828
+ const body = {
829
+ code: code ?? "client_error",
830
+ message: payload.message || payload.name || String(payload)
831
+ };
832
+ if (Object.keys(details).length > 0) body.cause = details;
833
+ return JSON.stringify(body, null, 2);
834
+ }
835
+ return JSON.stringify(payload, null, 2);
836
+ }
837
+ function extractErrorCode(payload) {
838
+ if (payload instanceof Error) {
839
+ const { code } = extractCauseDetails(payload.cause);
840
+ return code;
841
+ }
842
+ if (payload && typeof payload === "object") {
843
+ const inner = payload.error;
844
+ if (inner && typeof inner === "object" && typeof inner.code === "string") return inner.code;
845
+ const direct = payload.code;
846
+ if (typeof direct === "string") return direct;
847
+ }
848
+ }
849
+ const ERROR_CODE_HINTS = { [API_ERROR_CODES.unauthorized]: "Hint: run `primitive signin`, pass --api-key explicitly, or set PRIMITIVE_API_KEY in your environment. `primitive whoami` is the fastest way to verify auth is live." };
850
+ const NETWORK_ERROR_HINTS = {
851
+ ENETUNREACH: "Hint: the network is unreachable. If you're behind a proxy and set HTTP(S)_PROXY, re-run with NODE_USE_ENV_PROXY=1 (Node 22+ ignores those env vars by default). `primitive doctor` reports the local environment in one shot.",
852
+ ECONNREFUSED: "Hint: the server refused the connection. Check that your firewall allows egress to *.primitive.dev, that your PRIMITIVE_API_BASE_URL_* overrides (if any) point at a reachable host, and re-run with NODE_USE_ENV_PROXY=1 if you're behind a proxy. `primitive doctor` reports the local environment in one shot.",
853
+ ETIMEDOUT: "Hint: the connection timed out. Check egress rules and proxy configuration; if you're behind a proxy, re-run with NODE_USE_ENV_PROXY=1 and HTTPS_PROXY set. `primitive doctor` reports the local environment in one shot.",
854
+ EAI_AGAIN: "Hint: DNS lookup failed. Check /etc/resolv.conf inside containers, and try `curl -v https://api.primitive.dev/v1/account` to confirm the host resolves. `primitive doctor` reports the local environment in one shot."
855
+ };
856
+ function writeErrorWithHints(payload) {
857
+ process.stderr.write(`${formatErrorPayload(payload)}\n`);
858
+ const code = extractErrorCode(payload);
859
+ if (!code) return;
860
+ if (code in ERROR_CODE_HINTS) {
861
+ const hint = ERROR_CODE_HINTS[code];
862
+ process.stderr.write(`${hint}\n`);
863
+ return;
864
+ }
865
+ if (code in NETWORK_ERROR_HINTS) process.stderr.write(`${NETWORK_ERROR_HINTS[code]}\n`);
866
+ }
867
+ /**
868
+ * Surface a user-facing hint when a request comes back unauthorized.
869
+ *
870
+ * Deliberately does NOT mutate the saved credentials.json. Auto-
871
+ * deleting on any 401 made transient rejections look like permanent
872
+ * credential failures and forced unnecessary re-login cycles; we
873
+ * surface a hint and let the user decide whether to re-authenticate.
874
+ *
875
+ * The one legitimate auto-delete case lives in `primitive login`'s
876
+ * `checkExistingLogin`: the user has explicitly asked to log in,
877
+ * existing credentials are probed, and if they fail we clean up
878
+ * before minting a new key. That path calls `deleteCliCredentials`
879
+ * directly rather than going through this function.
880
+ */
881
+ function surfaceUnauthorizedHint(params) {
882
+ if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return;
883
+ if (params.baseUrlOverridden && params.auth.credentials !== null && params.auth.apiBaseUrl !== params.auth.credentials.api_base_url) {
884
+ process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The saved credential is preserved; unset PRIMITIVE_API_BASE_URL, run `primitive config reset` to clear configured URL overrides, or run `primitive logout` to remove the stored credential.\n");
885
+ return;
886
+ }
887
+ process.stderr.write("Your saved Primitive CLI OAuth session was rejected. If the command was working a moment ago, please retry; brief retries often clear transient rejections. If it keeps failing, run `primitive logout && primitive signin` to mint a fresh session.\n");
888
+ }
889
+ function formatElapsed(ms) {
890
+ const seconds = ms / 1e3;
891
+ if (seconds < 60) return `${seconds.toFixed(2)}s`;
892
+ const minutes = Math.floor(seconds / 60);
893
+ return `${minutes}m ${(seconds - minutes * 60).toFixed(2)}s`;
894
+ }
895
+ async function runWithTiming(enabled, fn) {
896
+ if (!enabled) return fn();
897
+ const start = Date.now();
898
+ try {
899
+ return await fn();
900
+ } finally {
901
+ process.stderr.write(`[time: ${formatElapsed(Date.now() - start)}]\n`);
902
+ }
903
+ }
904
+ const TIME_FLAG_DESCRIPTION = "Print the wall-clock duration of this command to stderr after it completes (e.g. `[time: 1.34s]`). Useful for measuring `--wait` send latency, comparing CLI overhead, or capturing timing in scripts.";
905
+ const API_BASE_URL_FLAG_DESCRIPTION = "Override the API base URL. Internal testing only; not documented to customers.";
906
+ const RESERVED_FLAG_NAMES = new Set([
907
+ "api-key",
908
+ "api-base-url",
909
+ "raw-body",
910
+ "body-file",
911
+ "envelope",
912
+ "output"
913
+ ]);
914
+ function bodyFieldFlag(field, aliases) {
915
+ const common = {
916
+ description: field.description || field.name,
917
+ ...aliases && aliases.length > 0 ? { aliases } : {}
918
+ };
919
+ if (field.kind === "boolean") return Flags.boolean({
920
+ ...common,
921
+ allowNo: true
922
+ });
923
+ if (field.kind === "integer") return Flags.integer({
924
+ ...common,
925
+ ...numericFlagOptions(field)
926
+ });
927
+ if (field.kind === "number") return numberFlag({
928
+ ...common,
929
+ ...numericFlagOptions(field)
930
+ });
931
+ if (field.enumValues) return Flags.string({
932
+ ...common,
933
+ options: field.enumValues
934
+ });
935
+ return Flags.string(common);
936
+ }
937
+ function buildFlags(operation) {
938
+ const flags = {
939
+ "api-key": Flags.string({
940
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
941
+ env: "PRIMITIVE_API_KEY"
942
+ }),
943
+ "api-base-url": Flags.string({
944
+ description: "Override the API base URL. Internal testing only; not documented to customers.",
945
+ env: "PRIMITIVE_API_BASE_URL",
946
+ hidden: true
947
+ }),
948
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
949
+ };
950
+ if (!operation.binaryResponse) {
951
+ flags.json = Flags.boolean({ description: "Print exactly one JSON document on stdout, on success and on failure, and nothing on stderr. Stdout keeps its usual shape: the data payload, or with --envelope the full response envelope (data plus meta, including meta.cursor, and empty-result hints in summary). The next-cursor line is not printed; use --envelope to page. A failure prints the error with exit_code. Without --json, the cursor and notices go to stderr." });
952
+ flags.envelope = Flags.boolean({ description: "Print the full response envelope, including pagination metadata such as meta.cursor. Defaults to printing only the data payload for backward compatibility." });
953
+ }
954
+ for (const parameter of [...operation.pathParams, ...operation.queryParams]) flags[flagName(parameter.name)] = flagForParameter(parameter);
955
+ const bodyFieldFlagToProperty = /* @__PURE__ */ new Map();
956
+ if (operation.hasJsonBody) {
957
+ flags["raw-body"] = Flags.string({ description: "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available." });
958
+ flags["body-file"] = Flags.string({ description: "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload." });
959
+ const bodyFields = extractBodyFields(operation.requestSchema);
960
+ const aliasesForOperation = OPERATION_FLAG_ALIASES[operation.sdkName];
961
+ for (const field of bodyFields) {
962
+ if (field.kind === "complex") continue;
963
+ const name = flagName(field.name);
964
+ if (RESERVED_FLAG_NAMES.has(name)) continue;
965
+ if (flags[name] !== void 0) continue;
966
+ flags[name] = bodyFieldFlag(field, aliasesForOperation?.[field.name]);
967
+ bodyFieldFlagToProperty.set(name, field.name);
968
+ }
969
+ }
970
+ if (operation.binaryResponse) flags.output = Flags.string({ description: "Write binary response bytes to a file" });
971
+ if (ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName)) flags.wait = Flags.boolean({
972
+ allowNo: true,
973
+ default: true,
974
+ description: ATTACHMENT_WAIT_FLAG_DESCRIPTION
975
+ });
976
+ return {
977
+ flags,
978
+ bodyFieldFlagToProperty
979
+ };
980
+ }
981
+ function collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty) {
982
+ const result = {};
983
+ for (const [flag, property] of bodyFieldFlagToProperty) {
984
+ const value = parsedFlags[flag];
985
+ if (value === void 0) continue;
986
+ result[property] = value;
987
+ }
988
+ return result;
989
+ }
990
+ function collectValues(parameters, flags) {
991
+ const values = {};
992
+ for (const parameter of parameters) {
993
+ const value = coerceParameterValue(parameter, flags[flagName(parameter.name)]);
994
+ if (value !== void 0) values[parameter.name] = value;
995
+ }
996
+ return values;
997
+ }
998
+ function operationOutputPayload(envelope, includeEnvelope) {
999
+ return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
1000
+ }
1001
+ /**
1002
+ * The `--json` document for a generated command: the full response
1003
+ * envelope, so pagination metadata travels with the data, plus any
1004
+ * notes the command would otherwise print on stderr.
1005
+ */
1006
+ function operationJsonDocument(envelope, summary = []) {
1007
+ const document = envelope !== null && typeof envelope === "object" ? { ...envelope } : { data: null };
1008
+ if (summary.length > 0) document.summary = summary.join("\n");
1009
+ return document;
1010
+ }
1011
+ function isIncompleteDomainVerification(operation, envelope) {
1012
+ if (operation.sdkName !== "verifyDomain") return false;
1013
+ const data = envelope?.data;
1014
+ if (!data || typeof data !== "object") return false;
1015
+ return data.verified === false;
1016
+ }
1017
+ function writeIncompleteDomainVerificationHint() {
1018
+ process.stderr.write("Domain verification is incomplete. Add or fix the DNS records shown above, or run `primitive domains zone-file --id <domain-id>` to download the complete zone file, then retry `primitive domains verify --id <domain-id>`.\n");
1019
+ }
1020
+ const OPERATION_HINTS = {
1021
+ addDomain: "Tip: after this returns a domain id, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` when the user wants an importable DNS zone file.",
1022
+ verifyDomain: "Tip: if DNS is still missing, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` to give the user an importable DNS zone file.",
1023
+ downloadDomainZoneFile: "Tip: prefer `primitive domains zone-file --id <domain-id> --output <domain>.zone` for CLI-friendly file output.",
1024
+ getInboxStatus: "Tip: prefer `primitive inbox status` for a compact readiness summary and next-step commands.",
1025
+ getSendPermissions: "Tip: this command answers where you may send mail to. To find usable sender domains for --from, run `primitive domains list` or `primitive inbox status` and use an address at an active verified domain.",
1026
+ sendEmail: "Tip: prefer `primitive send --to <address> --body <text> --attachment <file>` for file attachments. This raw command exists for callers passing JSON.",
1027
+ createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
1028
+ updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
1029
+ createFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
1030
+ setFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
1031
+ setMemory: "Tip: prefer `primitive memories set <key> <json-value> [--function <function-id>]` for JSON value parsing and scope flags. This raw command exists for callers passing full request JSON.",
1032
+ getMemory: "Tip: prefer `primitive memories get <key> [--function <function-id>]` for the common read path.",
1033
+ deleteMemory: "Tip: prefer `primitive memories delete <key> [--function <function-id>]` for the common delete path.",
1034
+ searchMemories: "Tip: prefer `primitive memories search [prefix] [--metadata-only] [--function <function-id>]` for the common search path.",
1035
+ startAgentSignup: "Tip: pass --terms-accepted, and optionally --signup-code <code> if you have one. Capture the signup_token from the response and feed it to `primitive agent verify-agent-signup --signup-token <token> --verification-code <6-digit-code>` (the verify flag accepts --code as an alias). The high-level `primitive signup <email>` command walks an interactive user through both steps with friendlier prompts.",
1036
+ verifyAgentSignup: "Tip: pass --verification-code <code> (or --code; both work). The response carries OAuth tokens but not your assigned inbox domain; run `primitive domains list` (or `primitive whoami`) after success to see the managed *.primitive.email address that routes to this account."
1037
+ };
1038
+ const OPERATION_FLAG_ALIASES = { verifyAgentSignup: { verification_code: ["code"] } };
1039
+ const OPERATION_SUCCESS_HOOKS = { verifyAgentSignup: ({ envelope, configDir, apiBaseUrl, writeStderr }) => {
1040
+ const data = envelope?.data;
1041
+ if (!data?.access_token || !data?.refresh_token) return;
1042
+ saveSignupCredentials({
1043
+ apiBaseUrl,
1044
+ configDir,
1045
+ signup: data
1046
+ });
1047
+ writeStderr("Credentials saved to the CLI config; `primitive whoami` will work on the next call.\n");
1048
+ } };
1049
+ function createOperationCommand(operation) {
1050
+ const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
1051
+ const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
1052
+ const schemaSummary = operation.hasJsonBody ? renderRequestSchemaSummary(operation.requestSchema) : null;
1053
+ const hint = OPERATION_HINTS[operation.sdkName];
1054
+ const descriptionWithSchema = schemaSummary ? `${baseDescription}\n\n${schemaSummary}` : baseDescription;
1055
+ const fullDescription = hint ? `${descriptionWithSchema}\n\n${hint}` : descriptionWithSchema;
1056
+ class OperationCommand extends Command {
1057
+ static description = fullDescription;
1058
+ static flags = flags;
1059
+ static summary = operation.summary ?? `${operation.method} ${operation.path}`;
1060
+ async run() {
1061
+ const { flags } = await this.parse(OperationCommand);
1062
+ const parsedFlags = flags;
1063
+ if ([
1064
+ "startAgentSignup",
1065
+ "verifyAgentSignup",
1066
+ "resendAgentSignupVerification"
1067
+ ].includes(operation.operationId)) requireDefaultLoginProfile();
1068
+ await runWithTiming(parsedFlags.time === true, async () => {
1069
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
1070
+ apiKey: typeof parsedFlags["api-key"] === "string" ? parsedFlags["api-key"] : void 0,
1071
+ apiBaseUrl: typeof parsedFlags["api-base-url"] === "string" ? parsedFlags["api-base-url"] : void 0,
1072
+ configDir: this.config.configDir
1073
+ });
1074
+ let body;
1075
+ if (operation.hasJsonBody) {
1076
+ const explicit = readJsonBody(parsedFlags);
1077
+ const overrides = collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty);
1078
+ if (Object.keys(overrides).length > 0) if (explicit === void 0) body = overrides;
1079
+ else if (explicit !== null && typeof explicit === "object" && !Array.isArray(explicit)) body = {
1080
+ ...explicit,
1081
+ ...overrides
1082
+ };
1083
+ else {
1084
+ const explicitKind = explicit === null ? "null" : Array.isArray(explicit) ? "array" : typeof explicit;
1085
+ const overrideFlags = Object.keys(overrides).map((p) => `--${flagName(p)}`).join(", ");
1086
+ throw new Errors.CLIError(`--raw-body must be a JSON object when also passing per-field flags (got ${explicitKind}); supplied per-field flags: ${overrideFlags}. Either drop --raw-body and rely on the per-field flags, or move every field into the JSON --raw-body and drop the flags.`);
1087
+ }
1088
+ else body = explicit;
1089
+ }
1090
+ if (operation.bodyRequired && body === void 0) throw new Errors.CLIError(`Operation ${operation.operationId} requires a body. Pass each field as a --flag (see --help) or supply JSON via --raw-body / --body-file.`);
1091
+ const operationFn = sdk_gen_exports[operation.sdkName];
1092
+ const query = collectValues(operation.queryParams, parsedFlags);
1093
+ const replyStateSurface = replyStateSurfaceForOperation(operation.sdkName, query);
1094
+ const automatedSurface = automatedSurfaceForOperation(operation.sdkName, query);
1095
+ const callOperation = () => operationFn({
1096
+ body,
1097
+ client: apiClient.client,
1098
+ parseAs: operation.binaryResponse ? "blob" : "auto",
1099
+ path: collectValues(operation.pathParams, parsedFlags),
1100
+ query,
1101
+ responseStyle: "fields"
1102
+ });
1103
+ const result = ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName) ? await withAttachmentWait(callOperation, {
1104
+ wait: parsedFlags.wait !== false,
1105
+ errorCode: (error) => extractErrorCode(extractErrorPayload(error)),
1106
+ onFirstWait: () => {
1107
+ process.stderr.write(`Attachment content is not ready yet; waiting up to ${ATTACHMENT_WAIT_MAX_MS / 1e3}s. Pass --no-wait to return immediately.\n`);
1108
+ }
1109
+ }) : await callOperation();
1110
+ if (result.error) {
1111
+ const errorPayload = extractErrorPayload(result.error);
1112
+ writeErrorWithHints(errorPayload);
1113
+ if (replyStateSurface && isAwaitingRejectedError(errorPayload)) process.stderr.write(`${awaitingRejectedError(replyStateSurface).message}\n`);
1114
+ if (automatedSurface && isAutomatedRejectedError(errorPayload)) process.stderr.write(`${automatedRejectedError(automatedSurface).message}\n`);
1115
+ surfaceUnauthorizedHint({
1116
+ auth,
1117
+ baseUrlOverridden,
1118
+ configDir: this.config.configDir,
1119
+ payload: errorPayload
1120
+ });
1121
+ const listClient = apiClient.client;
1122
+ const listEndpointsFn = () => listEndpoints({
1123
+ client: listClient,
1124
+ responseStyle: "fields"
1125
+ });
1126
+ await maybeWriteFunctionEndpointRedirect({
1127
+ sdkName: operation.sdkName,
1128
+ errorCode: extractErrorCode(errorPayload),
1129
+ endpointId: typeof parsedFlags.id === "string" ? parsedFlags.id : void 0,
1130
+ listEndpoints: listEndpointsFn,
1131
+ writeStderr: (chunk) => {
1132
+ process.stderr.write(chunk);
1133
+ }
1134
+ });
1135
+ process.exitCode = 1;
1136
+ return;
1137
+ }
1138
+ if (operation.binaryResponse) {
1139
+ const blob = result.data;
1140
+ const bytes = Buffer.from(await blob.arrayBuffer());
1141
+ const output = parsedFlags.output;
1142
+ if (typeof output === "string") {
1143
+ writeFileSync(output, bytes);
1144
+ return;
1145
+ }
1146
+ process.stdout.write(bytes);
1147
+ return;
1148
+ }
1149
+ const envelope = result.data;
1150
+ if (replyStateSurface) try {
1151
+ const rows = Array.isArray(envelope?.data) ? envelope.data : [];
1152
+ assertAwaitingFilterRows(rows, replyStateSurface);
1153
+ if (rows.length === 0 && operation.sdkName === "searchEmails") await ensureSearchReportsReplyState(apiClient);
1154
+ } catch (error) {
1155
+ if (!(error instanceof ReplyStateUnsupportedError)) throw error;
1156
+ process.stderr.write(`${error.message}\n`);
1157
+ process.exitCode = 1;
1158
+ return;
1159
+ }
1160
+ if (automatedSurface) try {
1161
+ const rows = Array.isArray(envelope?.data) ? envelope.data : [];
1162
+ assertAutomatedVerdict(rows, automatedSurface, expectedAutomatedVerdicts(query?.automated === "true" ? true : query?.automated === "false" ? false : void 0, typeof query?.q === "string" ? query.q : void 0));
1163
+ if (rows.length === 0 && operation.sdkName === "searchEmails") await ensureSearchReportsAutomated(apiClient);
1164
+ } catch (error) {
1165
+ if (!(error instanceof AutomatedFilterUnsupportedError)) throw error;
1166
+ process.stderr.write(`${error.message}\n`);
1167
+ process.exitCode = 1;
1168
+ return;
1169
+ }
1170
+ const jsonOutput = parsedFlags.json === true;
1171
+ const withEnvelope = parsedFlags.envelope === true;
1172
+ const cursor = envelope?.meta?.cursor;
1173
+ if (cursor && !jsonOutput) process.stderr.write(`next cursor: ${cursor}\n`);
1174
+ const summary = [];
1175
+ if (Array.isArray(envelope?.data) && envelope.data.length === 0) {
1176
+ const hint = EMPTY_RESULT_HINTS[operation.sdkName];
1177
+ if (hint && jsonOutput) {
1178
+ if (withEnvelope) summary.push(hint);
1179
+ } else if (hint) process.stderr.write(`${hint}\n`);
1180
+ }
1181
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
1182
+ process.stderr.write(chunk);
1183
+ } });
1184
+ const successHook = OPERATION_SUCCESS_HOOKS[operation.sdkName];
1185
+ if (successHook) try {
1186
+ successHook({
1187
+ envelope,
1188
+ configDir: this.config.configDir,
1189
+ apiBaseUrl: auth.apiBaseUrl,
1190
+ writeStderr: (chunk) => {
1191
+ process.stderr.write(chunk);
1192
+ }
1193
+ });
1194
+ } catch (hookError) {
1195
+ const detail = hookError instanceof Error ? hookError.message : String(hookError);
1196
+ process.stderr.write(`Warning: ${operation.sdkName} succeeded but its post-success hook threw (${detail}). The response below is still valid; act on it manually.\n`);
1197
+ }
1198
+ this.log(JSON.stringify(jsonOutput && withEnvelope ? operationJsonDocument(envelope, summary) : operationOutputPayload(envelope, withEnvelope), null, 2));
1199
+ if (isIncompleteDomainVerification(operation, envelope)) {
1200
+ writeIncompleteDomainVerificationHint();
1201
+ process.exitCode = 1;
1202
+ }
1203
+ });
1204
+ }
1205
+ }
1206
+ return OperationCommand;
1207
+ }
1208
+ const EMPTY_RESULT_HINTS = {
1209
+ listDeliveries: "(no results) No webhook deliveries logged yet. If you have an endpoint configured but expected to see test fires here: test deliveries from `primitive endpoints test` are NOT logged in this list, they're synchronous and visible only in the test-endpoint command's response. Real deliveries are logged when an inbound `email.received` event fans out to your endpoints. If you have no endpoints, run `primitive endpoints list` to check.",
1210
+ listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
1211
+ listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails latest`.",
1212
+ listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
1213
+ listFilters: "(no results) No filter rules configured.",
1214
+ listFunctions: "(no results) No Functions configured yet. Start with `primitive functions templates`, then `primitive functions init --template <template>` and `primitive functions deploy --name <name> --file <bundle>`."
1215
+ };
1216
+ function canonicalizeCliReferences(description) {
1217
+ return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
1218
+ }
1219
+ //#endregion
1220
+ //#region ../sdk-node/dist/payloads-CXgCzM4Y.js
1221
+ /**
1222
+ * Load a Node builtin at call time. Exported for direct testing of the
1223
+ * unavailable-module error path; production code goes through the typed
1224
+ * wrappers below.
1225
+ */
1226
+ async function loadBuiltin(specifier, operation) {
1227
+ let builtin;
1228
+ if (typeof process !== "undefined" && typeof process.getBuiltinModule === "function") try {
1229
+ builtin = process.getBuiltinModule(specifier);
1230
+ } catch {
1231
+ builtin = void 0;
1232
+ }
1233
+ if (builtin !== void 0 && builtin !== null) return builtin;
1234
+ try {
1235
+ return await import(specifier);
1236
+ } catch (cause) {
1237
+ throw new Error(`${operation} requires file system access, which this runtime does not provide. Pass in-memory bytes instead of a file path when running outside Node.js.`, { cause });
1238
+ }
1239
+ }
1240
+ /** `node:fs`, loaded lazily so Workers-style bundles never see the import. */
1241
+ function loadNodeFs(operation) {
1242
+ return loadBuiltin("node:fs", operation);
1243
+ }
1244
+ /** `node:fs/promises`, loaded lazily for the same reason as {@link loadNodeFs}. */
1245
+ function loadNodeFsPromises(operation) {
1246
+ return loadBuiltin("node:fs/promises", operation);
1247
+ }
1248
+ const CHUNK_SIZE = 64 * 1024 * 1024;
1249
+ const MANIFEST_VERSION = 1;
1250
+ const CHUNK_KDF_INFO = "payloads-chunk";
1251
+ const OBJECT_ID_BYTES = 16;
1252
+ const CEK_BYTES = 32;
1253
+ function subtle() {
1254
+ return globalThis.crypto.subtle;
1255
+ }
1256
+ const textEncoder = new TextEncoder();
1257
+ function randomBytes(length) {
1258
+ const out = new Uint8Array(length);
1259
+ globalThis.crypto.getRandomValues(out);
1260
+ return out;
1261
+ }
1262
+ function toHex(bytes) {
1263
+ let out = "";
1264
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
1265
+ return out;
1266
+ }
1267
+ function fromHex(hex) {
1268
+ if (hex.length % 2 !== 0) throw new Error("invalid hex: odd length");
1269
+ if (hex.length > 0 && !/^[0-9a-fA-F]+$/.test(hex)) throw new Error("invalid hex: non-hex character");
1270
+ const out = new Uint8Array(hex.length / 2);
1271
+ for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1272
+ return out;
1273
+ }
1274
+ async function contentHash(bytes) {
1275
+ return new Uint8Array(await subtle().digest("SHA-256", bytes));
1276
+ }
1277
+ async function contentHashHex(bytes) {
1278
+ return toHex(await contentHash(bytes));
1279
+ }
1280
+ /** Binary Merkle root over ordered hex leaf hashes (odd node promoted). */
1281
+ async function merkleRoot(leafHashesHex) {
1282
+ if (leafHashesHex.length === 0) return contentHashHex(new Uint8Array(0));
1283
+ let level = leafHashesHex.map(fromHex);
1284
+ while (level.length > 1) {
1285
+ const next = [];
1286
+ for (let i = 0; i < level.length; i += 2) {
1287
+ const left = level[i];
1288
+ const right = i + 1 < level.length ? level[i + 1] : level[i];
1289
+ const combined = new Uint8Array(left.length + right.length);
1290
+ combined.set(left, 0);
1291
+ combined.set(right, left.length);
1292
+ next.push(await contentHash(combined));
1293
+ }
1294
+ level = next;
1295
+ }
1296
+ return toHex(level[0]);
1297
+ }
1298
+ async function deriveChunkKey(cek, index) {
1299
+ const baseKey = await subtle().importKey("raw", cek, "HKDF", false, ["deriveKey"]);
1300
+ const info = textEncoder.encode(`${CHUNK_KDF_INFO}:${index}`);
1301
+ return subtle().deriveKey({
1302
+ name: "HKDF",
1303
+ hash: "SHA-256",
1304
+ salt: new Uint8Array(32),
1305
+ info
1306
+ }, baseKey, {
1307
+ name: "AES-GCM",
1308
+ length: 256
1309
+ }, false, ["encrypt", "decrypt"]);
1310
+ }
1311
+ function chunkNonce(index) {
1312
+ const nonce = new Uint8Array(12);
1313
+ new DataView(nonce.buffer).setUint32(8, index >>> 0, false);
1314
+ return nonce;
1315
+ }
1316
+ function chunkAad(objectId, index) {
1317
+ const aad = new Uint8Array(objectId.length + 4);
1318
+ aad.set(objectId, 0);
1319
+ new DataView(aad.buffer).setUint32(objectId.length, index >>> 0, false);
1320
+ return aad;
1321
+ }
1322
+ async function encryptChunk(cek, objectId, index, plaintext) {
1323
+ const key = await deriveChunkKey(cek, index);
1324
+ const ct = await subtle().encrypt({
1325
+ name: "AES-GCM",
1326
+ iv: chunkNonce(index),
1327
+ additionalData: chunkAad(objectId, index)
1328
+ }, key, plaintext);
1329
+ return new Uint8Array(ct);
1330
+ }
1331
+ async function decryptChunk(cek, objectId, index, ciphertext) {
1332
+ const key = await deriveChunkKey(cek, index);
1333
+ const pt = await subtle().decrypt({
1334
+ name: "AES-GCM",
1335
+ iv: chunkNonce(index),
1336
+ additionalData: chunkAad(objectId, index)
1337
+ }, key, ciphertext);
1338
+ return new Uint8Array(pt);
1339
+ }
1340
+ function authHeaders(apiKey) {
1341
+ return { authorization: `Bearer ${apiKey}` };
1342
+ }
1343
+ function apiRoot(baseUrl) {
1344
+ let end = baseUrl.length;
1345
+ while (end > 0 && baseUrl.charCodeAt(end - 1) === 47) end--;
1346
+ return `${baseUrl.slice(0, end)}/v1/payloads`;
1347
+ }
1348
+ const DEFAULT_MAX_RETRIES = 6;
1349
+ const RETRYABLE_STATUS = new Set([
1350
+ 429,
1351
+ 500,
1352
+ 502,
1353
+ 503,
1354
+ 504
1355
+ ]);
1356
+ function sleep(ms) {
1357
+ return new Promise((resolve) => setTimeout(resolve, ms));
1358
+ }
1359
+ /**
1360
+ * fetch with exponential backoff on transient failures. Request bodies here are
1361
+ * in-memory buffers/strings (never streams), so a retry can safely resend them.
1362
+ */
1363
+ async function retryingFetch(url, init, label, maxRetries = DEFAULT_MAX_RETRIES) {
1364
+ for (let attempt = 0;; attempt++) {
1365
+ let res;
1366
+ let networkError;
1367
+ try {
1368
+ res = await fetch(url, init);
1369
+ } catch (err) {
1370
+ networkError = err;
1371
+ }
1372
+ if (res && !RETRYABLE_STATUS.has(res.status)) return res;
1373
+ if (attempt >= maxRetries) {
1374
+ if (res) return res;
1375
+ throw new Error(`${label}: network error after ${attempt} retries: ${networkError?.message ?? networkError}`);
1376
+ }
1377
+ if (res) await res.text().catch(() => {});
1378
+ await sleep(Math.min(1e3 * 2 ** attempt, 15e3) + Math.floor(Math.random() * 250));
1379
+ }
1380
+ }
1381
+ async function initiate(opts, manifest) {
1382
+ const res = await retryingFetch(apiRoot(opts.baseUrl), {
1383
+ method: "POST",
1384
+ headers: {
1385
+ ...authHeaders(opts.apiKey),
1386
+ "content-type": "application/json"
1387
+ },
1388
+ body: JSON.stringify({ manifest })
1389
+ }, "initiate");
1390
+ if (!res.ok) throw new Error(`initiate failed: HTTP ${res.status} ${await res.text()}`);
1391
+ await res.text();
1392
+ }
1393
+ async function putChunk(opts, root, d, bytes) {
1394
+ const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/chunks/${d.ciphertextHash}`, {
1395
+ method: "PUT",
1396
+ headers: {
1397
+ ...authHeaders(opts.apiKey),
1398
+ "content-type": "application/octet-stream"
1399
+ },
1400
+ body: bytes
1401
+ }, `chunk ${d.index} upload`);
1402
+ if (!res.ok) throw new Error(`chunk ${d.index} upload failed: HTTP ${res.status} ${await res.text()}`);
1403
+ await res.text();
1404
+ }
1405
+ async function finalize(opts, root) {
1406
+ const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/finalize`, {
1407
+ method: "POST",
1408
+ headers: authHeaders(opts.apiKey)
1409
+ }, "finalize");
1410
+ if (!res.ok) throw new Error(`finalize failed: HTTP ${res.status} ${await res.text()}`);
1411
+ await res.text();
1412
+ }
1413
+ async function fetchManifest(opts, root) {
1414
+ const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/manifest`, { headers: authHeaders(opts.apiKey) }, "get manifest");
1415
+ if (!res.ok) throw new Error(`get manifest failed: HTTP ${res.status} ${await res.text()}`);
1416
+ return (await res.json()).data.manifest;
1417
+ }
1418
+ async function getChunkBytes(opts, root, d) {
1419
+ const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/chunks/${d.ciphertextHash}`, { headers: authHeaders(opts.apiKey) }, `get chunk ${d.index}`);
1420
+ if (!res.ok) throw new Error(`get chunk ${d.index} failed: HTTP ${res.status}`);
1421
+ return new Uint8Array(await res.arrayBuffer());
1422
+ }
1423
+ async function readWindow(fh, position, length) {
1424
+ const buf = Buffer.allocUnsafe(length);
1425
+ let read = 0;
1426
+ while (read < length) {
1427
+ const { bytesRead } = await fh.read(buf, read, length - read, position + read);
1428
+ if (bytesRead === 0) break;
1429
+ read += bytesRead;
1430
+ }
1431
+ return read === length ? buf : buf.subarray(0, read);
1432
+ }
1433
+ async function mapLimit(items, limit, fn) {
1434
+ let next = 0;
1435
+ let firstError;
1436
+ const worker = async () => {
1437
+ while (next < items.length && firstError === void 0) {
1438
+ const i = next++;
1439
+ try {
1440
+ await fn(items[i], i);
1441
+ } catch (err) {
1442
+ if (firstError === void 0) firstError = err;
1443
+ return;
1444
+ }
1445
+ }
1446
+ };
1447
+ await Promise.all(Array.from({ length: Math.min(limit, items.length || 1) }, () => worker()));
1448
+ if (firstError !== void 0) throw firstError;
1449
+ }
1450
+ /**
1451
+ * Stream a file up as a Primitive Payload. Two passes over the file (encrypt to
1452
+ * build the manifest, then encrypt again to upload) keep memory bounded to a few
1453
+ * chunks — the whole object is never resident. Returns the content address
1454
+ * (merkleRoot) and the hex CEK needed to download it.
1455
+ */
1456
+ async function pushFile(filePath, opts) {
1457
+ const { open, stat } = await loadNodeFsPromises("pushFile");
1458
+ const chunkSize = opts.chunkSize ?? CHUNK_SIZE;
1459
+ const concurrency = opts.concurrency ?? 3;
1460
+ const { size } = await stat(filePath);
1461
+ const cek = randomBytes(CEK_BYTES);
1462
+ const objectId = randomBytes(OBJECT_ID_BYTES);
1463
+ const chunkCount = size === 0 ? 0 : Math.ceil(size / chunkSize);
1464
+ const fh = await open(filePath, "r");
1465
+ try {
1466
+ const descriptors = [];
1467
+ for (let index = 0; index < chunkCount; index++) {
1468
+ const position = index * chunkSize;
1469
+ const length = Math.min(chunkSize, size - position);
1470
+ const plaintext = await readWindow(fh, position, length);
1471
+ const ciphertext = await encryptChunk(cek, objectId, index, plaintext);
1472
+ descriptors.push({
1473
+ index,
1474
+ ciphertextHash: await contentHashHex(ciphertext),
1475
+ plaintextSize: length,
1476
+ ciphertextSize: ciphertext.length
1477
+ });
1478
+ opts.onProgress?.("encrypt", index + 1, chunkCount);
1479
+ }
1480
+ const root = await merkleRoot(descriptors.map((d) => d.ciphertextHash));
1481
+ await initiate(opts, {
1482
+ version: MANIFEST_VERSION,
1483
+ objectId: toHex(objectId),
1484
+ chunkSize,
1485
+ totalPlaintextSize: size,
1486
+ chunkCount,
1487
+ chunks: descriptors,
1488
+ merkleRoot: root
1489
+ });
1490
+ let uploaded = 0;
1491
+ await mapLimit(descriptors, concurrency, async (d) => {
1492
+ const plaintext = await readWindow(fh, d.index * chunkSize, d.plaintextSize);
1493
+ await putChunk(opts, root, d, await encryptChunk(cek, objectId, d.index, plaintext));
1494
+ opts.onProgress?.("upload", ++uploaded, chunkCount);
1495
+ });
1496
+ await finalize(opts, root);
1497
+ return {
1498
+ merkleRoot: root,
1499
+ cek: toHex(cek),
1500
+ chunkCount,
1501
+ totalBytes: size
1502
+ };
1503
+ } finally {
1504
+ await fh.close();
1505
+ }
1506
+ }
1507
+ /**
1508
+ * Stream a Primitive Payload down to a file, verifying and decrypting one chunk
1509
+ * at a time (bounded memory). Every chunk is content-address-checked before
1510
+ * decryption, so a corrupt or substituted chunk throws.
1511
+ */
1512
+ async function pullFile(root, outPath, opts) {
1513
+ const { createWriteStream } = await loadNodeFs("pullFile");
1514
+ const { rm } = await loadNodeFsPromises("pullFile");
1515
+ const manifest = await fetchManifest(opts, root);
1516
+ const computedRoot = await merkleRoot(manifest.chunks.map((c) => c.ciphertextHash));
1517
+ if (computedRoot !== root) throw new Error(`manifest does not match the requested content address (got ${computedRoot})`);
1518
+ const cek = fromHex(opts.cek);
1519
+ const objectId = fromHex(manifest.objectId);
1520
+ const out = createWriteStream(outPath);
1521
+ let onWriteError = () => {};
1522
+ const writeErrored = new Promise((_, reject) => {
1523
+ onWriteError = reject;
1524
+ });
1525
+ writeErrored.catch(() => {});
1526
+ out.on("error", (err) => onWriteError(err));
1527
+ try {
1528
+ let done = 0;
1529
+ for (const d of manifest.chunks) {
1530
+ const ciphertext = await getChunkBytes(opts, root, d);
1531
+ if (await contentHashHex(ciphertext) !== d.ciphertextHash) throw new Error(`chunk ${d.index} failed integrity check`);
1532
+ const plaintext = await decryptChunk(cek, objectId, d.index, ciphertext);
1533
+ if (!out.write(plaintext)) await Promise.race([new Promise((resolve) => out.once("drain", resolve)), writeErrored]);
1534
+ opts.onProgress?.("download", ++done, manifest.chunkCount);
1535
+ }
1536
+ await Promise.race([new Promise((resolve) => out.end(resolve)), writeErrored]);
1537
+ } catch (err) {
1538
+ out.destroy();
1539
+ await rm(outPath, { force: true });
1540
+ throw err;
1541
+ }
1542
+ return manifest;
1543
+ }
1544
+ //#endregion
1545
+ //#region ../sdk-node/dist/api-DIgE1sxv.js
1546
+ const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
1547
+ function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
1548
+ let lastEventId;
1549
+ const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1550
+ const createStream = async function* () {
1551
+ let retryDelay = sseDefaultRetryDelay ?? 3e3;
1552
+ let attempt = 0;
1553
+ const signal = options.signal ?? new AbortController().signal;
1554
+ while (true) {
1555
+ if (signal.aborted) break;
1556
+ attempt++;
1557
+ const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
1558
+ if (lastEventId !== void 0) headers.set("Last-Event-ID", lastEventId);
1559
+ try {
1560
+ const requestInit = {
1561
+ redirect: "follow",
1562
+ ...options,
1563
+ body: options.serializedBody,
1564
+ headers,
1565
+ signal
1566
+ };
1567
+ let request = new Request(url, requestInit);
1568
+ if (onRequest) request = await onRequest(url, requestInit);
1569
+ const response = await (options.fetch ?? globalThis.fetch)(request);
1570
+ if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
1571
+ if (!response.body) throw new Error("No body in SSE response");
1572
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
1573
+ let buffer = "";
1574
+ const abortHandler = () => {
1575
+ try {
1576
+ reader.cancel();
1577
+ } catch {}
1578
+ };
1579
+ signal.addEventListener("abort", abortHandler);
1580
+ try {
1581
+ while (true) {
1582
+ const { done, value } = await reader.read();
1583
+ if (done) break;
1584
+ buffer += value;
1585
+ buffer = buffer.replace(/\r\n?/g, "\n");
1586
+ const chunks = buffer.split("\n\n");
1587
+ buffer = chunks.pop() ?? "";
1588
+ for (const chunk of chunks) {
1589
+ const lines = chunk.split("\n");
1590
+ const dataLines = [];
1591
+ let eventName;
1592
+ for (const line of lines) if (line.startsWith("data:")) dataLines.push(line.replace(/^data:\s*/, ""));
1593
+ else if (line.startsWith("event:")) eventName = line.replace(/^event:\s*/, "");
1594
+ else if (line.startsWith("id:")) lastEventId = line.replace(/^id:\s*/, "");
1595
+ else if (line.startsWith("retry:")) {
1596
+ const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
1597
+ if (!Number.isNaN(parsed)) retryDelay = parsed;
1598
+ }
1599
+ let data;
1600
+ let parsedJson = false;
1601
+ if (dataLines.length) {
1602
+ const rawData = dataLines.join("\n");
1603
+ try {
1604
+ data = JSON.parse(rawData);
1605
+ parsedJson = true;
1606
+ } catch {
1607
+ data = rawData;
1608
+ }
1609
+ }
1610
+ if (parsedJson) {
1611
+ if (responseValidator) await responseValidator(data);
1612
+ if (responseTransformer) data = await responseTransformer(data);
1613
+ }
1614
+ onSseEvent?.({
1615
+ data,
1616
+ event: eventName,
1617
+ id: lastEventId,
1618
+ retry: retryDelay
1619
+ });
1620
+ if (dataLines.length) yield data;
1621
+ }
1622
+ }
1623
+ } finally {
1624
+ signal.removeEventListener("abort", abortHandler);
1625
+ reader.releaseLock();
1626
+ }
1627
+ break;
1628
+ } catch (error) {
1629
+ onSseError?.(error);
1630
+ if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) break;
1631
+ await sleep(Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4));
1632
+ }
1633
+ }
1634
+ };
1635
+ return { stream: createStream() };
1636
+ }
1637
+ const separatorArrayExplode = (style) => {
1638
+ switch (style) {
1639
+ case "label": return ".";
1640
+ case "matrix": return ";";
1641
+ case "simple": return ",";
1642
+ default: return "&";
1643
+ }
1644
+ };
1645
+ const separatorArrayNoExplode = (style) => {
1646
+ switch (style) {
1647
+ case "form": return ",";
1648
+ case "pipeDelimited": return "|";
1649
+ case "spaceDelimited": return "%20";
1650
+ default: return ",";
1651
+ }
1652
+ };
1653
+ const separatorObjectExplode = (style) => {
1654
+ switch (style) {
1655
+ case "label": return ".";
1656
+ case "matrix": return ";";
1657
+ case "simple": return ",";
1658
+ default: return "&";
1659
+ }
1660
+ };
1661
+ const serializeArrayParam = ({ allowReserved, explode, name, style, value }) => {
1662
+ if (!explode) {
1663
+ const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
1664
+ switch (style) {
1665
+ case "label": return `.${joinedValues}`;
1666
+ case "matrix": return `;${name}=${joinedValues}`;
1667
+ case "simple": return joinedValues;
1668
+ default: return `${name}=${joinedValues}`;
1669
+ }
1670
+ }
1671
+ const separator = separatorArrayExplode(style);
1672
+ const joinedValues = value.map((v) => {
1673
+ if (style === "label" || style === "simple") return allowReserved ? v : encodeURIComponent(v);
1674
+ return serializePrimitiveParam({
1675
+ allowReserved,
1676
+ name,
1677
+ value: v
1678
+ });
1679
+ }).join(separator);
1680
+ return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
1681
+ };
1682
+ const serializePrimitiveParam = ({ allowReserved, name, value }) => {
1683
+ if (value === void 0 || value === null) return "";
1684
+ if (typeof value === "object") throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");
1685
+ return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
1686
+ };
1687
+ const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => {
1688
+ if (value instanceof Date) return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
1689
+ if (style !== "deepObject" && !explode) {
1690
+ let values = [];
1691
+ Object.entries(value).forEach(([key, v]) => {
1692
+ values = [
1693
+ ...values,
1694
+ key,
1695
+ allowReserved ? v : encodeURIComponent(v)
1696
+ ];
1697
+ });
1698
+ const joinedValues = values.join(",");
1699
+ switch (style) {
1700
+ case "form": return `${name}=${joinedValues}`;
1701
+ case "label": return `.${joinedValues}`;
1702
+ case "matrix": return `;${name}=${joinedValues}`;
1703
+ default: return joinedValues;
1704
+ }
1705
+ }
1706
+ const separator = separatorObjectExplode(style);
1707
+ const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({
1708
+ allowReserved,
1709
+ name: style === "deepObject" ? `${name}[${key}]` : key,
1710
+ value: v
1711
+ })).join(separator);
1712
+ return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
1713
+ };
1714
+ const PATH_PARAM_RE = /\{[^{}]+\}/g;
1715
+ const defaultPathSerializer = ({ path, url: _url }) => {
1716
+ let url = _url;
1717
+ const matches = _url.match(PATH_PARAM_RE);
1718
+ if (matches) for (const match of matches) {
1719
+ let explode = false;
1720
+ let name = match.substring(1, match.length - 1);
1721
+ let style = "simple";
1722
+ if (name.endsWith("*")) {
1723
+ explode = true;
1724
+ name = name.substring(0, name.length - 1);
1725
+ }
1726
+ if (name.startsWith(".")) {
1727
+ name = name.substring(1);
1728
+ style = "label";
1729
+ } else if (name.startsWith(";")) {
1730
+ name = name.substring(1);
1731
+ style = "matrix";
1732
+ }
1733
+ const value = path[name];
1734
+ if (value === void 0 || value === null) continue;
1735
+ if (Array.isArray(value)) {
1736
+ url = url.replace(match, serializeArrayParam({
1737
+ explode,
1738
+ name,
1739
+ style,
1740
+ value
1741
+ }));
1742
+ continue;
1743
+ }
1744
+ if (typeof value === "object") {
1745
+ url = url.replace(match, serializeObjectParam({
1746
+ explode,
1747
+ name,
1748
+ style,
1749
+ value,
1750
+ valueOnly: true
1751
+ }));
1752
+ continue;
1753
+ }
1754
+ if (style === "matrix") {
1755
+ url = url.replace(match, `;${serializePrimitiveParam({
1756
+ name,
1757
+ value
1758
+ })}`);
1759
+ continue;
1760
+ }
1761
+ const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value);
1762
+ url = url.replace(match, replaceValue);
1763
+ }
1764
+ return url;
1765
+ };
1766
+ const getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => {
1767
+ const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
1768
+ let url = (baseUrl ?? "") + pathUrl;
1769
+ if (path) url = defaultPathSerializer({
1770
+ path,
1771
+ url
1772
+ });
1773
+ let search = query ? querySerializer(query) : "";
1774
+ if (search.startsWith("?")) search = search.substring(1);
1775
+ if (search) url += `?${search}`;
1776
+ return url;
1777
+ };
1778
+ function getValidRequestBody(options) {
1779
+ const hasBody = options.body !== void 0;
1780
+ if (hasBody && options.bodySerializer) {
1781
+ if ("serializedBody" in options) return options.serializedBody !== void 0 && options.serializedBody !== "" ? options.serializedBody : null;
1782
+ return options.body !== "" ? options.body : null;
1783
+ }
1784
+ if (hasBody) return options.body;
1785
+ }
1786
+ const getAuthToken = async (auth, callback) => {
1787
+ const token = typeof callback === "function" ? await callback(auth) : callback;
1788
+ if (!token) return;
1789
+ if (auth.scheme === "bearer") return `Bearer ${token}`;
1790
+ if (auth.scheme === "basic") return `Basic ${btoa(token)}`;
1791
+ return token;
1792
+ };
1793
+ const createQuerySerializer = ({ parameters = {}, ...args } = {}) => {
1794
+ const querySerializer = (queryParams) => {
1795
+ const search = [];
1796
+ if (queryParams && typeof queryParams === "object") for (const name in queryParams) {
1797
+ const value = queryParams[name];
1798
+ if (value === void 0 || value === null) continue;
1799
+ const options = parameters[name] || args;
1800
+ if (Array.isArray(value)) {
1801
+ const serializedArray = serializeArrayParam({
1802
+ allowReserved: options.allowReserved,
1803
+ explode: true,
1804
+ name,
1805
+ style: "form",
1806
+ value,
1807
+ ...options.array
1808
+ });
1809
+ if (serializedArray) search.push(serializedArray);
1810
+ } else if (typeof value === "object") {
1811
+ const serializedObject = serializeObjectParam({
1812
+ allowReserved: options.allowReserved,
1813
+ explode: true,
1814
+ name,
1815
+ style: "deepObject",
1816
+ value,
1817
+ ...options.object
1818
+ });
1819
+ if (serializedObject) search.push(serializedObject);
1820
+ } else {
1821
+ const serializedPrimitive = serializePrimitiveParam({
1822
+ allowReserved: options.allowReserved,
1823
+ name,
1824
+ value
1825
+ });
1826
+ if (serializedPrimitive) search.push(serializedPrimitive);
1827
+ }
1828
+ }
1829
+ return search.join("&");
1830
+ };
1831
+ return querySerializer;
1832
+ };
1833
+ /**
1834
+ * Infers parseAs value from provided Content-Type header.
1835
+ */
1836
+ const getParseAs = (contentType) => {
1837
+ if (!contentType) return "stream";
1838
+ const cleanContent = contentType.split(";")[0]?.trim();
1839
+ if (!cleanContent) return;
1840
+ if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) return "json";
1841
+ if (cleanContent === "multipart/form-data") return "formData";
1842
+ if ([
1843
+ "application/",
1844
+ "audio/",
1845
+ "image/",
1846
+ "video/"
1847
+ ].some((type) => cleanContent.startsWith(type))) return "blob";
1848
+ if (cleanContent.startsWith("text/")) return "text";
1849
+ };
1850
+ const checkForExistence = (options, name) => {
1851
+ if (!name) return false;
1852
+ if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) return true;
1853
+ return false;
1854
+ };
1855
+ async function setAuthParams(options) {
1856
+ for (const auth of options.security ?? []) {
1857
+ if (checkForExistence(options, auth.name)) continue;
1858
+ const token = await getAuthToken(auth, options.auth);
1859
+ if (!token) continue;
1860
+ const name = auth.name ?? "Authorization";
1861
+ switch (auth.in) {
1862
+ case "query":
1863
+ if (!options.query) options.query = {};
1864
+ options.query[name] = token;
1865
+ break;
1866
+ case "cookie":
1867
+ options.headers.append("Cookie", `${name}=${token}`);
1868
+ break;
1869
+ default:
1870
+ options.headers.set(name, token);
1871
+ break;
1872
+ }
1873
+ }
1874
+ }
1875
+ const buildUrl = (options) => getUrl({
1876
+ baseUrl: options.baseUrl,
1877
+ path: options.path,
1878
+ query: options.query,
1879
+ querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
1880
+ url: options.url
1881
+ });
1882
+ const mergeConfigs = (a, b) => {
1883
+ const config = {
1884
+ ...a,
1885
+ ...b
1886
+ };
1887
+ if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
1888
+ config.headers = mergeHeaders(a.headers, b.headers);
1889
+ return config;
1890
+ };
1891
+ const headersEntries = (headers) => {
1892
+ const entries = [];
1893
+ headers.forEach((value, key) => {
1894
+ entries.push([key, value]);
1895
+ });
1896
+ return entries;
1897
+ };
1898
+ const mergeHeaders = (...headers) => {
1899
+ const mergedHeaders = new Headers();
1900
+ for (const header of headers) {
1901
+ if (!header) continue;
1902
+ const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
1903
+ for (const [key, value] of iterator) if (value === null) mergedHeaders.delete(key);
1904
+ else if (Array.isArray(value)) for (const v of value) mergedHeaders.append(key, v);
1905
+ else if (value !== void 0) mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value);
1906
+ }
1907
+ return mergedHeaders;
1908
+ };
1909
+ var Interceptors = class {
1910
+ fns = [];
1911
+ clear() {
1912
+ this.fns = [];
1913
+ }
1914
+ eject(id) {
1915
+ const index = this.getInterceptorIndex(id);
1916
+ if (this.fns[index]) this.fns[index] = null;
1917
+ }
1918
+ exists(id) {
1919
+ const index = this.getInterceptorIndex(id);
1920
+ return Boolean(this.fns[index]);
1921
+ }
1922
+ getInterceptorIndex(id) {
1923
+ if (typeof id === "number") return this.fns[id] ? id : -1;
1924
+ return this.fns.indexOf(id);
1925
+ }
1926
+ update(id, fn) {
1927
+ const index = this.getInterceptorIndex(id);
1928
+ if (this.fns[index]) {
1929
+ this.fns[index] = fn;
1930
+ return id;
1931
+ }
1932
+ return false;
1933
+ }
1934
+ use(fn) {
1935
+ this.fns.push(fn);
1936
+ return this.fns.length - 1;
1937
+ }
1938
+ };
1939
+ const createInterceptors = () => ({
1940
+ error: new Interceptors(),
1941
+ request: new Interceptors(),
1942
+ response: new Interceptors()
1943
+ });
1944
+ const defaultQuerySerializer = createQuerySerializer({
1945
+ allowReserved: false,
1946
+ array: {
1947
+ explode: true,
1948
+ style: "form"
1949
+ },
1950
+ object: {
1951
+ explode: true,
1952
+ style: "deepObject"
1953
+ }
1954
+ });
1955
+ const defaultHeaders = { "Content-Type": "application/json" };
1956
+ const createConfig = (override = {}) => ({
1957
+ ...jsonBodySerializer,
1958
+ headers: defaultHeaders,
1959
+ parseAs: "auto",
1960
+ querySerializer: defaultQuerySerializer,
1961
+ ...override
1962
+ });
1963
+ const createClient = (config = {}) => {
1964
+ let _config = mergeConfigs(createConfig(), config);
1965
+ const getConfig = () => ({ ..._config });
1966
+ const setConfig = (config) => {
1967
+ _config = mergeConfigs(_config, config);
1968
+ return getConfig();
1969
+ };
1970
+ const interceptors = createInterceptors();
1971
+ const beforeRequest = async (options) => {
1972
+ const opts = {
1973
+ ..._config,
1974
+ ...options,
1975
+ fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
1976
+ headers: mergeHeaders(_config.headers, options.headers),
1977
+ serializedBody: void 0
1978
+ };
1979
+ if (opts.security) await setAuthParams(opts);
1980
+ if (opts.requestValidator) await opts.requestValidator(opts);
1981
+ if (opts.body !== void 0 && opts.bodySerializer) opts.serializedBody = opts.bodySerializer(opts.body);
1982
+ if (opts.body === void 0 || opts.serializedBody === "") opts.headers.delete("Content-Type");
1983
+ const resolvedOpts = opts;
1984
+ return {
1985
+ opts: resolvedOpts,
1986
+ url: buildUrl(resolvedOpts)
1987
+ };
1988
+ };
1989
+ const request = async (options) => {
1990
+ const throwOnError = options.throwOnError ?? _config.throwOnError;
1991
+ const responseStyle = options.responseStyle ?? _config.responseStyle;
1992
+ let request;
1993
+ let response;
1994
+ try {
1995
+ const { opts, url } = await beforeRequest(options);
1996
+ const requestInit = {
1997
+ redirect: "follow",
1998
+ ...opts,
1999
+ body: getValidRequestBody(opts)
2000
+ };
2001
+ request = new Request(url, requestInit);
2002
+ for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
2003
+ const _fetch = opts.fetch;
2004
+ response = await _fetch(request);
2005
+ for (const fn of interceptors.response.fns) if (fn) response = await fn(response, request, opts);
2006
+ const result = {
2007
+ request,
2008
+ response
2009
+ };
2010
+ if (response.ok) {
2011
+ const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
2012
+ if (response.status === 204 || response.headers.get("Content-Length") === "0") {
2013
+ let emptyData;
2014
+ switch (parseAs) {
2015
+ case "arrayBuffer":
2016
+ case "blob":
2017
+ case "text":
2018
+ emptyData = await response[parseAs]();
2019
+ break;
2020
+ case "formData":
2021
+ emptyData = new FormData();
2022
+ break;
2023
+ case "stream":
2024
+ emptyData = response.body;
2025
+ break;
2026
+ default:
2027
+ emptyData = {};
2028
+ break;
2029
+ }
2030
+ return opts.responseStyle === "data" ? emptyData : {
2031
+ data: emptyData,
2032
+ ...result
2033
+ };
2034
+ }
2035
+ let data;
2036
+ switch (parseAs) {
2037
+ case "arrayBuffer":
2038
+ case "blob":
2039
+ case "formData":
2040
+ case "text":
2041
+ data = await response[parseAs]();
2042
+ break;
2043
+ case "json": {
2044
+ const text = await response.text();
2045
+ data = text ? JSON.parse(text) : {};
2046
+ break;
2047
+ }
2048
+ case "stream": return opts.responseStyle === "data" ? response.body : {
2049
+ data: response.body,
2050
+ ...result
2051
+ };
2052
+ }
2053
+ if (parseAs === "json") {
2054
+ if (opts.responseValidator) await opts.responseValidator(data);
2055
+ if (opts.responseTransformer) data = await opts.responseTransformer(data);
2056
+ }
2057
+ return opts.responseStyle === "data" ? data : {
2058
+ data,
2059
+ ...result
2060
+ };
2061
+ }
2062
+ const textError = await response.text();
2063
+ let jsonError;
2064
+ try {
2065
+ jsonError = JSON.parse(textError);
2066
+ } catch {}
2067
+ throw jsonError ?? textError;
2068
+ } catch (error) {
2069
+ let finalError = error;
2070
+ for (const fn of interceptors.error.fns) if (fn) finalError = await fn(finalError, response, request, options);
2071
+ finalError = finalError || {};
2072
+ if (throwOnError) throw finalError;
2073
+ return responseStyle === "data" ? void 0 : {
2074
+ error: finalError,
2075
+ request,
2076
+ response
2077
+ };
2078
+ }
2079
+ };
2080
+ const makeMethodFn = (method) => (options) => request({
2081
+ ...options,
2082
+ method
2083
+ });
2084
+ const makeSseFn = (method) => async (options) => {
2085
+ const { opts, url } = await beforeRequest(options);
2086
+ return createSseClient({
2087
+ ...opts,
2088
+ body: opts.body,
2089
+ method,
2090
+ onRequest: async (url, init) => {
2091
+ let request = new Request(url, init);
2092
+ for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
2093
+ return request;
2094
+ },
2095
+ serializedBody: getValidRequestBody(opts),
2096
+ url
2097
+ });
2098
+ };
2099
+ const _buildUrl = (options) => buildUrl({
2100
+ ..._config,
2101
+ ...options
2102
+ });
2103
+ return {
2104
+ buildUrl: _buildUrl,
2105
+ connect: makeMethodFn("CONNECT"),
2106
+ delete: makeMethodFn("DELETE"),
2107
+ get: makeMethodFn("GET"),
2108
+ getConfig,
2109
+ head: makeMethodFn("HEAD"),
2110
+ interceptors,
2111
+ options: makeMethodFn("OPTIONS"),
2112
+ patch: makeMethodFn("PATCH"),
2113
+ post: makeMethodFn("POST"),
2114
+ put: makeMethodFn("PUT"),
2115
+ request,
2116
+ setConfig,
2117
+ sse: {
2118
+ connect: makeSseFn("CONNECT"),
2119
+ delete: makeSseFn("DELETE"),
2120
+ get: makeSseFn("GET"),
2121
+ head: makeSseFn("HEAD"),
2122
+ options: makeSseFn("OPTIONS"),
2123
+ patch: makeSseFn("PATCH"),
2124
+ post: makeSseFn("POST"),
2125
+ put: makeSseFn("PUT"),
2126
+ trace: makeSseFn("TRACE")
2127
+ },
2128
+ trace: makeMethodFn("TRACE")
2129
+ };
2130
+ };
2131
+ const client$1 = createClient(createConfig({ baseUrl: "https://api.primitive.dev/v1" }));
2132
+ /**
2133
+ * Receive a pending webhook event without a public endpoint
2134
+ *
2135
+ * Wait up to 30 seconds for one existing event. The body string preserves the exact webhook serialization; headers and canonical occurrence/type metadata travel alongside it. A fixed lease protects the attempt while a short handler accepts input. Retry may redeliver an occurrence: deduplicate event_id. Empty delivery means no offer now, not proof all input was delivered; inspect backlog and persistent gap_count/last_gap_reason. Queue retention is 24 hours and does not extend source-content retention. Disconnecting preserves pending work. Same named destination shares consumption; different destinations get independent copies.
2136
+ */
2137
+ const pullWebhookEvent = (options) => (options.client ?? client$1).post({
2138
+ security: [{
2139
+ scheme: "bearer",
2140
+ type: "http"
2141
+ }],
2142
+ url: "/endpoints/{id}/pull",
2143
+ ...options,
2144
+ headers: {
2145
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
2146
+ ...options.headers
2147
+ }
2148
+ });
2149
+ /**
2150
+ * Report the outcome of a local webhook delivery attempt
2151
+ *
2152
+ * Report normalized handling evidence for the queue, attempt and lease. Required JSON fields: queue_id, delivery_id, lease_token, mode, duration_ms. For mode=exec include exit_code; stdout includes write_succeeded; http includes status_code and optional transport_error, error_code, confirmed. Supply this discriminated request with --raw-body in the generated CLI. The server classifies success and retry. Duplicate identical completion is idempotent; a stale lease cannot finish a newer attempt. Exec success means durable input acceptance, not completion of agent reasoning. Only successful HTTP handling with recognized confirmation evidence can confirm content discard. Never send arbitrary handler output or HTTP response bodies.
2153
+ */
2154
+ const completeWebhookEvent = (options) => (options.client ?? client$1).post({
2155
+ security: [{
2156
+ scheme: "bearer",
2157
+ type: "http"
2158
+ }],
2159
+ url: "/endpoints/{id}/complete",
2160
+ ...options,
2161
+ headers: {
2162
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
2163
+ ...options.headers
2164
+ }
2165
+ });
2166
+ var EventReceiverError = class extends Error {
2167
+ constructor(message, code, status = 0, retryAfterMs = 0) {
2168
+ super(message);
2169
+ this.code = code;
2170
+ this.status = status;
2171
+ this.retryAfterMs = retryAfterMs;
2172
+ this.name = "EventReceiverError";
2173
+ }
2174
+ };
2175
+ function record$2(value) {
2176
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2177
+ }
2178
+ function validateOffer(value) {
2179
+ if (!record$2(value) || !Number.isSafeInteger(value.backlog) || Number(value.backlog) < 0 || !Number.isSafeInteger(value.gap_count) || Number(value.gap_count) < 0 || value.last_gap_reason !== null && typeof value.last_gap_reason !== "string" || value.retention_seconds !== 86400 || value.handler_timeout_seconds !== 30) throw new EventReceiverError("Invalid event status response", "invalid_response");
2180
+ const d = value.delivery;
2181
+ if (d !== null && (!record$2(d) || ![
2182
+ "queue_id",
2183
+ "event_id",
2184
+ "delivery_id",
2185
+ "lease_token",
2186
+ "event_type",
2187
+ "body",
2188
+ "lease_expires_at"
2189
+ ].every((key) => typeof d[key] === "string") || !Number.isFinite(Date.parse(String(d.lease_expires_at))) || !record$2(d.headers) || !Object.values(d.headers).every((header) => typeof header === "string"))) throw new EventReceiverError("Invalid event delivery response", "invalid_response");
2190
+ return value;
2191
+ }
2192
+ function retryAfter(value) {
2193
+ if (!value) return 0;
2194
+ const ms = /^\d+(\.\d+)?$/.test(value) ? Number(value) * 1e3 : Date.parse(value) - Date.now();
2195
+ return Number.isFinite(ms) ? Math.max(0, ms) : 0;
2196
+ }
2197
+ function unwrap(result) {
2198
+ if (result.data !== void 0 && !result.error) return result.data;
2199
+ const code = record$2(result.error) && record$2(result.error.error) && typeof result.error.error.code === "string" ? result.error.error.code : "request_failed";
2200
+ throw new EventReceiverError(`Event request failed (${result.response?.status ?? 0}, ${code})`, code, result.response?.status ?? 0, retryAfter(result.response?.headers.get("retry-after") ?? null));
2201
+ }
2202
+ /** Shared by the SDK and CLI. Completion retries preserve the original evidence. */
2203
+ var EventConnection = class {
2204
+ socket;
2205
+ pending;
2206
+ origin;
2207
+ touch;
2208
+ constructor(client, endpointId, options = {}) {
2209
+ this.client = client;
2210
+ this.endpointId = endpointId;
2211
+ this.options = options;
2212
+ }
2213
+ close() {
2214
+ const socket = this.socket;
2215
+ this.socket = void 0;
2216
+ this.pending?.reject(new EventReceiverError("Event connection closed", "disconnected"));
2217
+ this.pending = void 0;
2218
+ socket?.close();
2219
+ }
2220
+ async open(signal) {
2221
+ if (this.options.transport === "poll" || this.socket?.readyState === 1) return;
2222
+ const config = this.client.getConfig();
2223
+ if (this.origin !== void 0 && this.origin !== config.baseUrl) throw new EventReceiverError("The API environment changed; restart the receiver", "identity_changed", 403);
2224
+ this.origin = config.baseUrl;
2225
+ const auth = typeof config.auth === "function" ? await config.auth({
2226
+ type: "http",
2227
+ scheme: "bearer"
2228
+ }) : config.auth;
2229
+ const authorization = new Headers(config.headers).get("authorization");
2230
+ const token = typeof auth === "string" ? auth.replace(/^Bearer /i, "") : authorization?.replace(/^Bearer /i, "");
2231
+ if (!token) throw new EventReceiverError("Event receiver requires bearer credentials", "unauthorized", 401);
2232
+ const url = new URL(`${config.baseUrl?.replace(/\/+$/, "")}/endpoints/${encodeURIComponent(this.endpointId)}/stream`);
2233
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && [
2234
+ "localhost",
2235
+ "127.0.0.1",
2236
+ "[::1]"
2237
+ ].includes(url.hostname))) throw new EventReceiverError("Event connections require HTTPS (except loopback development)", "unsupported");
2238
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2239
+ const socket = (this.options.webSocketFactory ?? ((address, protocol) => new WebSocket(address, protocol)))(url.href, "primitive.events.v1");
2240
+ this.socket = socket;
2241
+ socket.addEventListener("message", (message) => {
2242
+ if (this.socket !== socket) return;
2243
+ let frame;
2244
+ try {
2245
+ frame = JSON.parse(String(message.data));
2246
+ } catch {
2247
+ this.fail(new EventReceiverError("Invalid event frame", "invalid_response"));
2248
+ return;
2249
+ }
2250
+ this.touch?.();
2251
+ if (!record$2(frame)) {
2252
+ this.fail(new EventReceiverError("Invalid event frame", "invalid_response"));
2253
+ return;
2254
+ }
2255
+ if (frame.type === "ping") {
2256
+ socket.send(JSON.stringify({ type: "pong" }));
2257
+ try {
2258
+ this.options.onHeartbeat?.();
2259
+ } catch (error) {
2260
+ this.fail(error);
2261
+ }
2262
+ return;
2263
+ }
2264
+ if (frame.type === "status") {
2265
+ try {
2266
+ this.options.onStatus?.(validateOffer(frame.data));
2267
+ } catch (error) {
2268
+ this.fail(error);
2269
+ }
2270
+ return;
2271
+ }
2272
+ if (frame.type === "error") {
2273
+ this.fail(new EventReceiverError("Event stream request failed", typeof frame.code === "string" ? frame.code : "request_failed", typeof frame.status === "number" ? frame.status : 0, retryAfter(typeof frame.retry_after === "string" ? frame.retry_after : null)));
2274
+ return;
2275
+ }
2276
+ this.pending?.resolve(frame);
2277
+ });
2278
+ socket.addEventListener("close", () => {
2279
+ if (this.socket === socket) this.fail(new EventReceiverError("Event stream disconnected", "disconnected"));
2280
+ });
2281
+ socket.addEventListener("error", () => {
2282
+ if (this.socket === socket) this.fail(new EventReceiverError("Event stream connection failed", "disconnected"));
2283
+ });
2284
+ const ready = await this.exchange(signal, void 0, () => {
2285
+ socket.addEventListener("open", () => socket.send(JSON.stringify({
2286
+ type: "authenticate",
2287
+ token
2288
+ })), { once: true });
2289
+ }, 15e3);
2290
+ if (!record$2(ready) || ready.type !== "ready" || ready.protocol !== "primitive.events.v1") {
2291
+ this.close();
2292
+ throw new EventReceiverError("Unsupported event stream protocol", "unsupported");
2293
+ }
2294
+ }
2295
+ fail(error) {
2296
+ this.pending?.reject(error);
2297
+ this.pending = void 0;
2298
+ this.close();
2299
+ }
2300
+ async exchange(signal, frame, start, timeoutMs = 6e4) {
2301
+ signal.throwIfAborted();
2302
+ if (this.pending) throw new EventReceiverError("Concurrent event operation", "busy", 409);
2303
+ let cleanup = () => {};
2304
+ try {
2305
+ return await new Promise((resolve, reject) => {
2306
+ const abort = () => {
2307
+ reject(signal.reason);
2308
+ this.close();
2309
+ };
2310
+ let timer;
2311
+ this.touch = () => {
2312
+ clearTimeout(timer);
2313
+ timer = setTimeout(() => this.fail(new EventReceiverError("Event stream timed out", "disconnected")), timeoutMs);
2314
+ };
2315
+ this.touch();
2316
+ cleanup = () => {
2317
+ this.touch = void 0;
2318
+ clearTimeout(timer);
2319
+ signal.removeEventListener("abort", abort);
2320
+ this.pending = void 0;
2321
+ };
2322
+ this.pending = {
2323
+ resolve,
2324
+ reject
2325
+ };
2326
+ signal.addEventListener("abort", abort, { once: true });
2327
+ if (frame) this.socket?.send(JSON.stringify(frame));
2328
+ start?.();
2329
+ });
2330
+ } finally {
2331
+ cleanup();
2332
+ }
2333
+ }
2334
+ async receive(signal) {
2335
+ if (this.options.transport === "poll") return validateOffer(unwrap(await pullWebhookEvent({
2336
+ client: this.client,
2337
+ path: { id: this.endpointId },
2338
+ body: { wait_seconds: 25 },
2339
+ responseStyle: "fields",
2340
+ throwOnError: false,
2341
+ signal: AbortSignal.any([signal, AbortSignal.timeout(45e3)])
2342
+ })).data);
2343
+ await this.open(signal);
2344
+ const frame = await this.exchange(signal, { type: "receive" }, void 0, 9e4);
2345
+ if (!record$2(frame) || frame.type !== "event") throw new EventReceiverError("Invalid event frame", "invalid_response");
2346
+ return validateOffer(frame.data);
2347
+ }
2348
+ async complete(body, signal) {
2349
+ let receipt;
2350
+ if (this.options.transport === "poll") receipt = unwrap(await completeWebhookEvent({
2351
+ client: this.client,
2352
+ path: { id: this.endpointId },
2353
+ body,
2354
+ responseStyle: "fields",
2355
+ throwOnError: false,
2356
+ signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)])
2357
+ })).data;
2358
+ else {
2359
+ await this.open(signal);
2360
+ const frame = await this.exchange(signal, {
2361
+ type: "complete",
2362
+ body
2363
+ }, void 0, 15e3);
2364
+ if (!record$2(frame) || frame.type !== "receipt") throw new EventReceiverError("Invalid event receipt", "invalid_response");
2365
+ receipt = frame.data;
2366
+ }
2367
+ if (!record$2(receipt) || !["completed", "already_completed"].includes(String(receipt.result))) throw new EventReceiverError("Invalid event receipt", "invalid_response");
2368
+ }
2369
+ };
2370
+ //#endregion
2371
+ //#region src/oclif/shared-mail-watch.ts
2372
+ function ownerRecord(value) {
2373
+ const o = mailObject(value, [
2374
+ "generation",
2375
+ "pid",
2376
+ "identity",
2377
+ "ready",
2378
+ "gapCount",
2379
+ "lastGapReason",
2380
+ ...value && typeof value === "object" && Object.hasOwn(value, "lastMailCheckAt") ? ["lastMailCheckAt"] : []
2381
+ ]);
2382
+ if (typeof o.pid !== "number" || !Number.isSafeInteger(o.pid) || o.pid < 1 || typeof o.ready !== "boolean" || typeof o.gapCount !== "number" || !Number.isSafeInteger(o.gapCount) || o.gapCount < 0) throw invalidSharedMail();
2383
+ return {
2384
+ generation: mailId(o.generation),
2385
+ pid: o.pid,
2386
+ identity: mailString(o.identity),
2387
+ ready: o.ready,
2388
+ gapCount: o.gapCount,
2389
+ lastGapReason: o.lastGapReason === null ? null : mailString(o.lastGapReason, 100),
2390
+ lastMailCheckAt: o.lastMailCheckAt === void 0 || o.lastMailCheckAt === null ? null : mailTime(o.lastMailCheckAt)
2391
+ };
2392
+ }
2393
+ function readSharedMailOwner(store) {
2394
+ const raw = readMailJson(join(store.directory, "owner.json"));
2395
+ if (raw === null) return null;
2396
+ const owner = ownerRecord(raw);
2397
+ return {
2398
+ ...owner,
2399
+ alive: compareListenProcessIdentity(owner.identity, listenProcessIdentity(owner.pid)) === true
2400
+ };
2401
+ }
2402
+ /** The caller stays in the foreground and owns the only remote stream until close. */
2403
+ function tryOwnSharedMail(store) {
2404
+ let release;
2405
+ try {
2406
+ release = acquireListenLock(store.directory, "shared-mail-stream");
2407
+ } catch (error) {
2408
+ if (mailLockBusy(error)) return null;
2409
+ throw error;
2410
+ }
2411
+ const path = join(store.directory, "owner.json"), identity = listenProcessIdentity(process.pid);
2412
+ if (identity === null) {
2413
+ release();
2414
+ throw invalidSharedMail();
2415
+ }
2416
+ let current = {
2417
+ generation: randomUUID(),
2418
+ pid: process.pid,
2419
+ identity,
2420
+ ready: false,
2421
+ gapCount: 0,
2422
+ lastGapReason: null,
2423
+ lastMailCheckAt: null
2424
+ };
2425
+ let closed = false;
2426
+ try {
2427
+ writeMailJson(path, current);
2428
+ } catch (error) {
2429
+ release();
2430
+ throw error;
2431
+ }
2432
+ const markStatus = (status) => {
2433
+ if (closed) throw invalidSharedMail();
2434
+ const existing = readMailJson(path);
2435
+ if (existing === null || ownerRecord(existing).generation !== current.generation) throw invalidSharedMail();
2436
+ const next = ownerRecord({
2437
+ ...current,
2438
+ ...status
2439
+ });
2440
+ if (next.gapCount < current.gapCount) throw invalidSharedMail();
2441
+ writeMailJson(path, next);
2442
+ current = next;
2443
+ };
2444
+ return {
2445
+ generation: current.generation,
2446
+ markReady(status = {}) {
2447
+ markStatus({
2448
+ ...status,
2449
+ ready: true
2450
+ });
2451
+ },
2452
+ markStatus,
2453
+ markChecked() {
2454
+ markStatus({ lastMailCheckAt: (/* @__PURE__ */ new Date()).toISOString() });
2455
+ },
2456
+ close() {
2457
+ if (closed) return;
2458
+ closed = true;
2459
+ try {
2460
+ const existing = readMailJson(path);
2461
+ if (existing !== null && ownerRecord(existing).generation === current.generation) removeMailFile(path);
2462
+ } finally {
2463
+ release();
2464
+ }
2465
+ }
2466
+ };
2467
+ }
2468
+ /** Install before reading state. Watches are hints; timeout reconciles local records only. */
2469
+ function waitForSharedMailChange(store, options) {
2470
+ const timeoutMs = options.timeoutMs ?? 1e3;
2471
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e4) throw new RangeError("Shared mail reconciliation delay must be between 1 and 60000 milliseconds.");
2472
+ return new Promise((resolve, reject) => {
2473
+ const watchers = [];
2474
+ let timer;
2475
+ let done = false;
2476
+ const finish = (error) => {
2477
+ if (done) return;
2478
+ done = true;
2479
+ clearTimeout(timer);
2480
+ options.signal.removeEventListener("abort", abort);
2481
+ for (const watcher of watchers) watcher.close();
2482
+ if (error !== void 0) reject(error);
2483
+ else resolve();
2484
+ };
2485
+ const abort = () => finish(options.signal.reason ?? /* @__PURE__ */ new Error("Shared mail wait aborted."));
2486
+ options.signal.addEventListener("abort", abort, { once: true });
2487
+ if (options.signal.aborted) {
2488
+ abort();
2489
+ return;
2490
+ }
2491
+ try {
2492
+ for (const path of [
2493
+ store.directory,
2494
+ join(store.directory, "emails"),
2495
+ join(store.directory, "waits")
2496
+ ]) {
2497
+ const watcher = watch(path, (_event, filename) => {
2498
+ const name = filename?.toString();
2499
+ if (name && (path === store.directory ? name === "owner.json" : name.endsWith(".json") && !name.startsWith(".write-"))) finish();
2500
+ });
2501
+ watchers.push(watcher);
2502
+ watcher.on("error", () => finish());
2503
+ }
2504
+ timer = setTimeout(() => finish(), timeoutMs);
2505
+ } catch (error) {
2506
+ finish(error);
2507
+ }
2508
+ });
2509
+ }
2510
+ //#endregion
2511
+ //#region src/oclif/wake-context.ts
2512
+ const security = [{
2513
+ scheme: "bearer",
2514
+ type: "http"
2515
+ }];
2516
+ const UUID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
2517
+ const WAKE_ADDRESS = /^[a-z0-9._%+-]{1,64}@[a-z0-9.-]{1,253}$/;
2518
+ const afterUnsupported = /* @__PURE__ */ new WeakSet();
2519
+ /** The bare lowercase address in `Name <addr>` or `addr` form. */
2520
+ function bareAddress(value) {
2521
+ if (typeof value !== "string") return null;
2522
+ const address = (/<([^<>\s]+@[^<>\s]+)>\s*$/.exec(value)?.[1] ?? value).trim().toLowerCase();
2523
+ return /^[^\s<>@]+@[^\s<>@]+$/.test(address) ? address : null;
2524
+ }
2525
+ function record$1(value) {
2526
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
2527
+ }
2528
+ function newerRows(value) {
2529
+ if (!Array.isArray(value)) return void 0;
2530
+ const rows = [];
2531
+ for (const item of value.slice(-20)) {
2532
+ const row = record$1(item);
2533
+ const from = bareAddress(row?.from);
2534
+ if (!row || typeof row.id !== "string" || !UUID.test(row.id) || !from || typeof row.received_at !== "string" || !Number.isFinite(Date.parse(row.received_at))) return void 0;
2535
+ rows.push({
2536
+ id: row.id.toLowerCase(),
2537
+ from,
2538
+ received_at: row.received_at
2539
+ });
2540
+ }
2541
+ return rows;
2542
+ }
2543
+ /**
2544
+ * Read one thread, asking for mail newer than `afterEmailId`. The newer-mail
2545
+ * fields are feature-detected: an API that does not return them yields a
2546
+ * context without `newerInboundCount`. Returns null when the thread cannot be
2547
+ * read; callers treat that as "unknown", never as an error.
2548
+ */
2549
+ async function readThreadContext(client, threadId, afterEmailId, signal) {
2550
+ if (!UUID.test(threadId)) return null;
2551
+ const bounded = AbortSignal.any([signal, AbortSignal.timeout(5e3)]);
2552
+ const read = (after) => client.get({
2553
+ security,
2554
+ url: "/threads/{id}",
2555
+ path: { id: threadId },
2556
+ ...after ? { query: { after } } : {},
2557
+ signal: bounded,
2558
+ responseStyle: "fields"
2559
+ });
2560
+ try {
2561
+ const askAfter = afterEmailId !== void 0 && UUID.test(afterEmailId) && !afterUnsupported.has(client);
2562
+ let result = await read(askAfter ? afterEmailId : void 0);
2563
+ if (askAfter && result.response?.status === 400) {
2564
+ afterUnsupported.add(client);
2565
+ result = await read();
2566
+ }
2567
+ if (result.error !== void 0) return null;
2568
+ const body = record$1(result.data);
2569
+ const data = record$1(body?.data);
2570
+ if (body?.success !== true || !data || !Array.isArray(data.messages)) return null;
2571
+ const messages = data.messages.filter((message) => Boolean(record$1(message)) && typeof message.id === "string" && ["inbound", "outbound"].includes(message.direction));
2572
+ const count = data.newer_inbound_count;
2573
+ const newerInboundCount = typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
2574
+ const newerInbound = newerInboundCount === void 0 ? void 0 : newerRows(data.newer_inbound);
2575
+ return {
2576
+ threadId: threadId.toLowerCase(),
2577
+ messages,
2578
+ truncated: typeof data.message_count === "number" && data.message_count > messages.length,
2579
+ ...newerInboundCount === void 0 ? {} : { newerInboundCount },
2580
+ ...newerInbound === void 0 ? {} : { newerInbound }
2581
+ };
2582
+ } catch {
2583
+ signal.throwIfAborted();
2584
+ return null;
2585
+ }
2586
+ }
2587
+ /** Whether `self` has an outbound message in the thread; undefined if unknowable. */
2588
+ function sentInThread(context, self) {
2589
+ if (!context) return void 0;
2590
+ const address = self.toLowerCase();
2591
+ if (context.messages.some((message) => message.direction === "outbound" && bareAddress(message.from) === address)) return true;
2592
+ return context.truncated ? void 0 : false;
2593
+ }
2594
+ /** The latest outbound message from `self` in the thread, if listed. */
2595
+ function latestOwnOutbound(context, self) {
2596
+ const address = self.toLowerCase();
2597
+ return [...context?.messages ?? []].reverse().find((message) => message.direction === "outbound" && bareAddress(message.from) === address);
2598
+ }
2599
+ function hasAttachments(detail) {
2600
+ return (detail.parsed?.attachments?.length ?? 0) > 0;
2601
+ }
2602
+ /**
2603
+ * Map server-provided sender facts to one relationship label. The owner and
2604
+ * member labels come only from server admission; `agent` from the server's
2605
+ * connected-agent verification or network admission; `contact` from an
2606
+ * explicit contact allowance.
2607
+ */
2608
+ function wakeRelationship(input) {
2609
+ if (input.senderRelation) return input.senderRelation;
2610
+ if (input.connectedAgentVerified || input.network) return "agent";
2611
+ if (input.contact) return "contact";
2612
+ return "other";
2613
+ }
2614
+ const SERVER_RELATIONSHIPS = {
2615
+ owner: "owner",
2616
+ org_agent: "agent",
2617
+ member: "member",
2618
+ contact: "contact",
2619
+ other: "other"
2620
+ };
2621
+ /** The `collaboration` object of an email read, when the server sends one. */
2622
+ function collaborationOf(detail) {
2623
+ return record$1(record$1(detail)?.collaboration);
2624
+ }
2625
+ /**
2626
+ * The sender relationship the server computed for this email
2627
+ * (`collaboration.sender_relationship`), mapped to the CLI's labels:
2628
+ * `org_agent` reads as `agent`. When present it is authoritative, `other`
2629
+ * included, so a stale local classification cannot override it. Returns
2630
+ * undefined only for an older server that sends no such field, or an
2631
+ * unrecognized value; the caller then keeps its own derivation.
2632
+ */
2633
+ function serverRelationship(detail) {
2634
+ const value = collaborationOf(detail)?.sender_relationship;
2635
+ return typeof value === "string" && Object.hasOwn(SERVER_RELATIONSHIPS, value) ? SERVER_RELATIONSHIPS[value] : void 0;
2636
+ }
2637
+ /**
2638
+ * True when the server reports this email's thread as muted for the
2639
+ * receiving address (`collaboration.muted`, or `muted` on the email). An
2640
+ * older server sends neither and nothing is muted by it.
2641
+ */
2642
+ function serverMuted(detail) {
2643
+ return collaborationOf(detail)?.muted === true || record$1(detail)?.muted === true;
2644
+ }
2645
+ /**
2646
+ * An address fit to print as trusted metadata, or `unavailable`. The
2647
+ * sender's address is parsed from a header the sender wrote, so anything
2648
+ * outside a plain charset (quotes, spaces, line breaks) is withheld.
2649
+ */
2650
+ function displayAddress(value) {
2651
+ return WAKE_ADDRESS.test(value) ? value : "unavailable";
2652
+ }
2653
+ /** The metadata clause of a wake line. Addresses outside a plain charset are withheld. */
2654
+ function formatWakeContext(context) {
2655
+ const sender = displayAddress(context.sender);
2656
+ const thread = context.threadId && UUID.test(context.threadId) ? context.threadId.toLowerCase() : "none";
2657
+ const newer = context.newer !== void 0 && Number.isSafeInteger(context.newer) && context.newer >= 0 ? ` newer=${Math.min(context.newer, 9999)}` : "";
2658
+ return `from=${sender} relationship=${context.relationship} thread=${thread} in_thread=${context.inThread ? "yes" : "no"} attachments=${context.attachments ? "yes" : "no"}${newer}`;
2659
+ }
2660
+ /** Build a wake context, reading the thread once. Never throws for API failures. */
2661
+ async function describeWake(input) {
2662
+ const { detail } = input;
2663
+ const threadId = typeof detail.thread_id === "string" && UUID.test(detail.thread_id) ? detail.thread_id.toLowerCase() : null;
2664
+ const thread = threadId ? await readThreadContext(input.client, threadId, detail.id, input.signal) : null;
2665
+ return {
2666
+ sender: detail.from_email.trim().toLowerCase(),
2667
+ relationship: serverRelationship(detail) ?? input.relationship,
2668
+ threadId,
2669
+ inThread: input.localInThread || sentInThread(thread, input.self) === true,
2670
+ attachments: hasAttachments(detail),
2671
+ ...thread?.newerInboundCount === void 0 ? {} : { newer: thread.newerInboundCount }
2672
+ };
2673
+ }
2674
+ //#endregion
2675
+ //#region src/oclif/email-participants.ts
2676
+ /**
2677
+ * Everyone else an email was addressed to: its parsed To and Cc, as bare
2678
+ * lowercase addresses, without `self` (the mailbox reading it) or the sender,
2679
+ * in header order and de-duplicated. Bcc is never read. These are the
2680
+ * addresses `primitive reply --all` copies, and a non-empty list is what makes
2681
+ * an email a group email.
2682
+ *
2683
+ * Empty unless To or Cc names `self`. Mail that reached you without naming you
2684
+ * (a blind copy, an alias, a list) is not treated as a group: a blind
2685
+ * recipient who replies to everyone reveals the blind copy.
2686
+ *
2687
+ * To and Cc are written by the sender, so treat the list as the sender's
2688
+ * claim about who else received it, not as proof. Display them through
2689
+ * displayAddress, which withholds anything outside a plain charset.
2690
+ */
2691
+ function otherParticipants(detail, self) {
2692
+ const parsed = detail.parsed;
2693
+ const me = bareAddress(self);
2694
+ const excluded = new Set([me, bareAddress(detail.from_email)].filter((value) => value !== null));
2695
+ const out = [];
2696
+ let named = false;
2697
+ for (const list of [parsed?.to_addresses, parsed?.cc]) {
2698
+ if (!Array.isArray(list)) continue;
2699
+ for (const entry of list) {
2700
+ const address = bareAddress(typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.address : null);
2701
+ if (address !== null && address === me) named = true;
2702
+ if (!address || excluded.has(address)) continue;
2703
+ excluded.add(address);
2704
+ out.push(address);
2705
+ }
2706
+ }
2707
+ return named ? out : [];
2708
+ }
2709
+ //#endregion
2710
+ //#region src/oclif/conversation-follow.ts
2711
+ function directory(context) {
2712
+ const key = createHash("sha256").update(JSON.stringify([context.scope, mailAddress(context.recipient)])).digest("hex");
2713
+ return join(context.configDir, "conversation-follows", key);
2714
+ }
2715
+ function session(value) {
2716
+ const separator = value.indexOf(":");
2717
+ const runtime = value.slice(0, separator);
2718
+ if (!["codex", "claude"].includes(runtime)) throw new Error("Conversation receiving requires an exact runtime session.");
2719
+ return `${runtime}:${mailId(value.slice(separator + 1))}`;
2720
+ }
2721
+ /** Read one exact server thread, never scan mail or infer a thread from a subject. */
2722
+ function readConversationFollow(context, threadId) {
2723
+ const id = mailId(threadId), dir = directory(context);
2724
+ try {
2725
+ privateMailDirectory(dir);
2726
+ } catch (error) {
2727
+ if (error.code === "ENOENT") return null;
2728
+ throw error;
2729
+ }
2730
+ const value = readMailJson(join(dir, `${id}.json`));
2731
+ if (value === null) return null;
2732
+ const row = mailObject(value, [
2733
+ "threadId",
2734
+ "recipient",
2735
+ "peer",
2736
+ "sessionKey",
2737
+ "since"
2738
+ ]);
2739
+ const follow = {
2740
+ threadId: mailId(row.threadId),
2741
+ recipient: mailAddress(row.recipient),
2742
+ peer: mailAddress(row.peer),
2743
+ sessionKey: session(String(row.sessionKey)),
2744
+ since: mailTime(row.since)
2745
+ };
2746
+ if (follow.threadId !== id || follow.recipient !== mailAddress(context.recipient)) throw new Error("Saved conversation identity does not match this connection.");
2747
+ return follow;
2748
+ }
2749
+ /** Local receiving interest has no active waiter and never holds unrelated peer mail. */
2750
+ async function followEmailConversation(context, detail) {
2751
+ if (detail.thread_id == null || !isPlainChatReply(detail)) return null;
2752
+ const recipient = mailAddress(context.recipient), peer = mailAddress(context.peer);
2753
+ if (otherParticipants(detail, recipient).length > 0) return null;
2754
+ if (!["accepted", "completed"].includes(detail.status) || mailAddress(detail.recipient) !== recipient || mailAddress(detail.to_email) !== recipient || mailAddress(detail.from_email) !== peer || !scopedChatSenderTrust(detail, peer).trusted) throw new Error("Conversation receiving requires an authenticated email addressed to this connected agent.");
2755
+ const follow = {
2756
+ threadId: mailId(detail.thread_id),
2757
+ recipient,
2758
+ peer,
2759
+ sessionKey: session(context.sessionKey),
2760
+ since: mailTime(context.since ?? (/* @__PURE__ */ new Date()).toISOString())
2761
+ };
2762
+ const dir = directory(context);
2763
+ privateMailDirectory(dir, true);
2764
+ return withMailLock(dir, () => {
2765
+ const existing = readConversationFollow(context, follow.threadId);
2766
+ if (existing) {
2767
+ if (existing.peer !== peer || existing.sessionKey !== follow.sessionKey) throw new Error("This conversation is already followed by another peer or native session. No receiving ownership was changed.");
2768
+ return existing;
2769
+ }
2770
+ writeMailJson(join(dir, `${follow.threadId}.json`), follow);
2771
+ return follow;
2772
+ });
2773
+ }
2774
+ //#endregion
2775
+ //#region src/oclif/shared-mail-state.ts
2776
+ function createSharedMailWaiter() {
2777
+ return {
2778
+ token: randomUUID(),
2779
+ pid: process.pid,
2780
+ identity: listenProcessIdentity(process.pid)
2781
+ };
2782
+ }
2783
+ function waiter(value) {
2784
+ const owner = mailObject(value, [
2785
+ "token",
2786
+ "pid",
2787
+ "identity"
2788
+ ]);
2789
+ if (!Number.isSafeInteger(owner.pid) || Number(owner.pid) < 1) throw invalidSharedMail();
2790
+ return {
2791
+ token: mailId(owner.token),
2792
+ pid: Number(owner.pid),
2793
+ identity: owner.identity === null ? null : mailString(owner.identity)
2794
+ };
2795
+ }
2796
+ function mayBeWaiting(owner) {
2797
+ const identity = listenProcessIdentity(owner.pid);
2798
+ const matches = compareListenProcessIdentity(owner.identity, identity);
2799
+ if (matches !== null) return matches;
2800
+ try {
2801
+ process.kill(owner.pid, 0);
2802
+ } catch (error) {
2803
+ if (error.code === "ESRCH") return false;
2804
+ }
2805
+ return true;
2806
+ }
2807
+ function addWaiters(previous, owners) {
2808
+ if (!previous.waiters) return previous;
2809
+ const merged = new Map(previous.waiters.filter(mayBeWaiting).map((owner) => [owner.token, owner]));
2810
+ for (const input of owners) {
2811
+ const owner = waiter(input), existing = merged.get(owner.token);
2812
+ if (existing && !same(existing, owner)) throw invalidSharedMail();
2813
+ merged.set(owner.token, owner);
2814
+ }
2815
+ return {
2816
+ ...previous,
2817
+ waiters: [...merged.values()]
2818
+ };
2819
+ }
2820
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
2821
+ const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
2822
+ function details(value) {
2823
+ const hasThread = Boolean(value && typeof value === "object" && Object.hasOwn(value, "threadId"));
2824
+ const d = mailObject(value, [
2825
+ ...hasThread ? ["threadId"] : [],
2826
+ "recipient",
2827
+ "peer",
2828
+ "replyToSentEmailId",
2829
+ "receivedAt",
2830
+ "authorization"
2831
+ ]);
2832
+ if (d.authorization !== "pending" && d.authorization !== "trusted" && d.authorization !== "rejected") throw invalidSharedMail();
2833
+ return {
2834
+ ...hasThread ? { threadId: mailId(d.threadId) } : {},
2835
+ recipient: mailAddress(d.recipient),
2836
+ peer: mailAddress(d.peer),
2837
+ replyToSentEmailId: d.replyToSentEmailId === null ? null : mailId(d.replyToSentEmailId),
2838
+ receivedAt: mailTime(d.receivedAt),
2839
+ authorization: d.authorization
2840
+ };
2841
+ }
2842
+ function wait(value) {
2843
+ const hasWaiters = Boolean(value && typeof value === "object" && Object.hasOwn(value, "waiters"));
2844
+ const hasControl = Boolean(value && typeof value === "object" && Object.hasOwn(value, "contactRequest"));
2845
+ const w = mailObject(value, [
2846
+ ...hasWaiters ? ["waiters"] : [],
2847
+ ...hasControl ? ["contactRequest"] : [],
2848
+ "requestId",
2849
+ "peer",
2850
+ "sessionKey",
2851
+ "idempotencyKey",
2852
+ "createdAt",
2853
+ "status",
2854
+ "sentEmailId"
2855
+ ]);
2856
+ if (w.status !== "unbound" && w.status !== "uncertain" && w.status !== "bound" && w.status !== "completed" && w.status !== "cancelled") throw invalidSharedMail();
2857
+ if (w.status !== "cancelled" && (w.status === "bound" || w.status === "completed") !== (w.sentEmailId !== null)) throw invalidSharedMail();
2858
+ let owners;
2859
+ if (hasWaiters) {
2860
+ if (!Array.isArray(w.waiters) || w.waiters.length > 32) throw invalidSharedMail();
2861
+ owners = w.waiters.map(waiter);
2862
+ if (new Set(owners.map((owner) => owner.token)).size !== owners.length) throw invalidSharedMail();
2863
+ }
2864
+ return {
2865
+ ...owners ? { waiters: owners } : {},
2866
+ ...hasControl ? { contactRequest: contactRequestReference(w.contactRequest) } : {},
2867
+ requestId: mailId(w.requestId),
2868
+ peer: mailAddress(w.peer),
2869
+ sessionKey: w.sessionKey === null ? null : mailString(w.sessionKey),
2870
+ idempotencyKey: mailString(w.idempotencyKey),
2871
+ createdAt: mailTime(w.createdAt),
2872
+ status: w.status,
2873
+ sentEmailId: w.sentEmailId === null ? null : mailId(w.sentEmailId)
2874
+ };
2875
+ }
2876
+ function contactRequestReference(value) {
2877
+ const r = mailObject(value, [
2878
+ "interactionId",
2879
+ "stepId",
2880
+ "expiresAt"
2881
+ ]);
2882
+ const interactionId = mailString(r.interactionId);
2883
+ const at = interactionId.indexOf("@");
2884
+ mailId(interactionId.slice(0, at));
2885
+ mailAddress(interactionId);
2886
+ return {
2887
+ interactionId,
2888
+ stepId: mailId(r.stepId),
2889
+ expiresAt: mailTime(r.expiresAt)
2890
+ };
2891
+ }
2892
+ function email(value) {
2893
+ const e = mailObject(value, [
2894
+ "emailId",
2895
+ "eventId",
2896
+ "receivedAt",
2897
+ "firstSeenAt",
2898
+ "details",
2899
+ "route"
2900
+ ]);
2901
+ let route = null;
2902
+ if (e.route !== null) {
2903
+ const raw = e.route;
2904
+ if (raw.kind === "wait") {
2905
+ const r = mailObject(raw, [
2906
+ "kind",
2907
+ "requestId",
2908
+ "observed"
2909
+ ]);
2910
+ if (typeof r.observed !== "boolean") throw invalidSharedMail();
2911
+ route = {
2912
+ kind: "wait",
2913
+ requestId: mailId(r.requestId),
2914
+ observed: r.observed
2915
+ };
2916
+ } else {
2917
+ const r = mailObject(raw, [
2918
+ "kind",
2919
+ "sessionKey",
2920
+ "state"
2921
+ ]);
2922
+ if (r.kind !== "notification" || r.state !== "selected" && r.state !== "submitting" && r.state !== "accepted" && r.state !== "skipped" && r.state !== "unknown") throw invalidSharedMail();
2923
+ route = {
2924
+ kind: "notification",
2925
+ sessionKey: mailString(r.sessionKey),
2926
+ state: r.state
2927
+ };
2928
+ }
2929
+ }
2930
+ return {
2931
+ emailId: mailId(e.emailId),
2932
+ eventId: mailId(e.eventId),
2933
+ receivedAt: mailTime(e.receivedAt),
2934
+ firstSeenAt: mailTime(e.firstSeenAt),
2935
+ details: e.details === null ? null : details(e.details),
2936
+ route
2937
+ };
2938
+ }
2939
+ function validate(path, value) {
2940
+ const parts = path.split("/");
2941
+ const file = parts.at(-1);
2942
+ if (!file?.endsWith(".json")) throw invalidSharedMail();
2943
+ const id = mailId(file.slice(0, -5));
2944
+ if (parts.length === 2 && parts[0] === "emails") {
2945
+ const e = email(value);
2946
+ if (e.emailId !== id) throw invalidSharedMail();
2947
+ return e;
2948
+ }
2949
+ if (parts.length === 2 && parts[0] === "waits") {
2950
+ const w = wait(value);
2951
+ if (w.requestId !== id) throw invalidSharedMail();
2952
+ return w;
2953
+ }
2954
+ if (parts.length === 2 && parts[0] === "events") {
2955
+ const r = mailObject(value, ["eventId", "emailId"]);
2956
+ if (mailId(r.eventId) !== id) throw invalidSharedMail();
2957
+ return {
2958
+ eventId: id,
2959
+ emailId: mailId(r.emailId)
2960
+ };
2961
+ }
2962
+ if (parts.length === 2 && parts[0] === "parents") {
2963
+ const r = mailObject(value, ["sentEmailId", "requestId"]);
2964
+ if (mailId(r.sentEmailId) !== id) throw invalidSharedMail();
2965
+ return {
2966
+ sentEmailId: id,
2967
+ requestId: mailId(r.requestId)
2968
+ };
2969
+ }
2970
+ if (parts.length === 3 && parts[0] === "unbound" && /^[a-f0-9]{64}$/.test(parts[1] ?? "")) {
2971
+ if (mailId(mailObject(value, ["requestId"]).requestId) !== id) throw invalidSharedMail();
2972
+ return { requestId: id };
2973
+ }
2974
+ if (parts.length === 3 && parts[0] === "matches") {
2975
+ const r = mailObject(value, ["emailId", "requestId"]);
2976
+ if (mailId(r.emailId) !== id || mailId(r.requestId) !== mailId(parts[1])) throw invalidSharedMail();
2977
+ return {
2978
+ emailId: id,
2979
+ requestId: mailId(parts[1])
2980
+ };
2981
+ }
2982
+ throw invalidSharedMail();
2983
+ }
2984
+ function fileIds(directory, options) {
2985
+ const limit = options.limit ?? 100;
2986
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new SharedMailStateError("Shared mail page limit must be between 1 and 1000.");
2987
+ const cursor = options.cursor === void 0 ? "" : mailId(options.cursor);
2988
+ const selected = [];
2989
+ let entries;
2990
+ try {
2991
+ privateMailDirectory(directory);
2992
+ entries = opendirSync(directory);
2993
+ } catch (error) {
2994
+ if (error.code === "ENOENT") return {
2995
+ ids: [],
2996
+ nextCursor: null
2997
+ };
2998
+ throw error;
2999
+ }
3000
+ try {
3001
+ for (let item = entries.readSync(); item !== null; item = entries.readSync()) {
3002
+ if (item.name.startsWith(".write-")) continue;
3003
+ if (!item.isFile() || !item.name.endsWith(".json")) throw invalidSharedMail();
3004
+ const id = mailId(item.name.slice(0, -5));
3005
+ if (item.name !== `${id}.json`) throw invalidSharedMail();
3006
+ if (id <= cursor) continue;
3007
+ selected.push(id);
3008
+ selected.sort();
3009
+ if (selected.length > limit + 1) selected.pop();
3010
+ }
3011
+ } finally {
3012
+ entries.closeSync();
3013
+ }
3014
+ const more = selected.length > limit, ids = selected.slice(0, limit);
3015
+ return {
3016
+ ids,
3017
+ nextCursor: more ? ids.at(-1) ?? null : null
3018
+ };
3019
+ }
3020
+ /** Reserve the one local subscription before its server-verified recipient is known. */
3021
+ async function reserveSharedMailSubscription(options) {
3022
+ options.signal?.throwIfAborted();
3023
+ const directory = join(options.configDir, "shared-mail", hash(mailString(options.scope, 1024)));
3024
+ privateMailDirectory(directory, true);
3025
+ privateMailDirectory(dirname(directory));
3026
+ return withMailLock(directory, () => {
3027
+ const path = join(directory, "subscription.json"), raw = readMailJson(path);
3028
+ const requested = options.recipient === void 0 ? null : mailAddress(options.recipient);
3029
+ let name = `local-mail-${randomUUID()}`, recipient = requested;
3030
+ if (raw !== null) {
3031
+ const saved = mailObject(raw, ["name", "recipient"]);
3032
+ if (typeof saved.name !== "string" || !saved.name.startsWith("local-mail-")) throw invalidSharedMail();
3033
+ mailId(saved.name.slice(11));
3034
+ name = saved.name;
3035
+ const prior = saved.recipient === null ? null : mailAddress(saved.recipient);
3036
+ if (prior !== null && requested !== null && prior !== requested) throw invalidSharedMail();
3037
+ recipient = prior ?? requested;
3038
+ if (recipient !== prior) writeMailJson(path, {
3039
+ name,
3040
+ recipient
3041
+ });
3042
+ } else writeMailJson(path, {
3043
+ name,
3044
+ recipient
3045
+ });
3046
+ for (const current of [
3047
+ directory,
3048
+ dirname(directory),
3049
+ options.configDir
3050
+ ]) syncMailDirectory(current);
3051
+ return {
3052
+ directory,
3053
+ name,
3054
+ recipient
3055
+ };
3056
+ }, options.signal);
3057
+ }
3058
+ /** Metadata only. Callers validate exact GET recipient, authentication and ancestry before hydration. */
3059
+ async function openSharedMailStore(options) {
3060
+ options.signal?.throwIfAborted();
3061
+ const recipient = mailAddress(options.recipient);
3062
+ const reserved = await reserveSharedMailSubscription(options);
3063
+ const directory = reserved.directory;
3064
+ for (const kind of [
3065
+ "emails",
3066
+ "events",
3067
+ "waits",
3068
+ "parents",
3069
+ "unbound",
3070
+ "matches"
3071
+ ]) privateMailDirectory(join(directory, kind), true);
3072
+ const pathFor = (kind, id) => `${kind}/${mailId(id)}.json`;
3073
+ const read = (path) => {
3074
+ const parent = dirname(join(directory, path));
3075
+ try {
3076
+ privateMailDirectory(parent);
3077
+ } catch (error) {
3078
+ if (error.code === "ENOENT") return null;
3079
+ throw error;
3080
+ }
3081
+ const raw = readMailJson(join(directory, path));
3082
+ return raw === null ? null : validate(path, raw);
3083
+ };
3084
+ const readEmail = (id) => read(pathFor("emails", id));
3085
+ const readWait = (id) => read(pathFor("waits", id));
3086
+ const requiredEmail = (id) => {
3087
+ const e = readEmail(id);
3088
+ if (!e) throw invalidSharedMail();
3089
+ return e;
3090
+ };
3091
+ const requiredWait = (id) => {
3092
+ const w = readWait(id);
3093
+ if (!w) throw invalidSharedMail();
3094
+ return w;
3095
+ };
3096
+ function recover() {
3097
+ const raw = readMailJson(join(directory, "pending.json"), 65536);
3098
+ if (raw === null) return;
3099
+ const pending = mailObject(raw, ["writes"]);
3100
+ if (!Array.isArray(pending.writes) || !pending.writes.length || pending.writes.length > 8) throw invalidSharedMail();
3101
+ const writes = pending.writes.map((value) => {
3102
+ const w = mailObject(value, [
3103
+ "path",
3104
+ "before",
3105
+ "after"
3106
+ ]), path = mailString(w.path, 200);
3107
+ const before = w.before === null ? null : validate(path, w.before), after = w.after === null ? null : validate(path, w.after);
3108
+ if (before === null && after === null) throw invalidSharedMail();
3109
+ return {
3110
+ path,
3111
+ before,
3112
+ after
3113
+ };
3114
+ });
3115
+ if (new Set(writes.map((w) => w.path)).size !== writes.length) throw invalidSharedMail();
3116
+ for (const w of writes) {
3117
+ const current = read(w.path);
3118
+ if (!same(current, w.before) && !same(current, w.after)) throw invalidSharedMail();
3119
+ }
3120
+ for (const w of writes) if (w.after === null) removeMailFile(join(directory, w.path));
3121
+ else writeMailJson(join(directory, w.path), w.after);
3122
+ removeMailFile(join(directory, "pending.json"));
3123
+ }
3124
+ function commit(changes) {
3125
+ const writes = changes.map(({ path, value }) => ({
3126
+ path,
3127
+ before: read(path),
3128
+ after: value === null ? null : validate(path, value)
3129
+ })).filter((w) => !same(w.before, w.after));
3130
+ if (!writes.length) return;
3131
+ writeMailJson(join(directory, "pending.json"), { writes });
3132
+ recover();
3133
+ }
3134
+ const transaction = (action) => withMailLock(directory, () => {
3135
+ recover();
3136
+ return action();
3137
+ }, options.signal);
3138
+ await transaction(() => {});
3139
+ const subscriptionName = reserved.name;
3140
+ const unboundPath = (w) => `unbound/${hash(w.peer)}/${w.requestId}.json`;
3141
+ return {
3142
+ directory,
3143
+ subscriptionName,
3144
+ recipient,
3145
+ readEmail: (id) => transaction(() => readEmail(id)),
3146
+ readWait: (id) => transaction(() => readWait(id)),
3147
+ ingest(input) {
3148
+ return transaction(() => {
3149
+ const emailId = mailId(input.emailId), eventId = mailId(input.eventId), previous = readEmail(emailId);
3150
+ const eventPath = pathFor("events", eventId), indexed = read(eventPath);
3151
+ if (indexed && (indexed.emailId !== emailId || !previous)) throw invalidSharedMail();
3152
+ const record = previous ?? {
3153
+ emailId,
3154
+ eventId,
3155
+ receivedAt: mailTime(input.receivedAt),
3156
+ firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
3157
+ details: null,
3158
+ route: null
3159
+ };
3160
+ commit([{
3161
+ path: pathFor("emails", emailId),
3162
+ value: record
3163
+ }, {
3164
+ path: eventPath,
3165
+ value: {
3166
+ eventId,
3167
+ emailId
3168
+ }
3169
+ }]);
3170
+ return record;
3171
+ });
3172
+ },
3173
+ hydrate(emailId, input) {
3174
+ return transaction(() => {
3175
+ const record = requiredEmail(emailId), verified = details(input);
3176
+ if (verified.threadId === void 0 && record.details?.threadId) verified.threadId = record.details.threadId;
3177
+ if (verified.recipient !== recipient) throw invalidSharedMail();
3178
+ if (record.details && record.details.authorization !== "pending" && !same(details({
3179
+ ...record.details,
3180
+ ...record.details.replyToSentEmailId === null ? { replyToSentEmailId: verified.replyToSentEmailId } : {},
3181
+ ...record.details.threadId === void 0 && verified.threadId ? { threadId: verified.threadId } : {}
3182
+ }), details(verified))) throw invalidSharedMail();
3183
+ const next = {
3184
+ ...record,
3185
+ receivedAt: verified.receivedAt,
3186
+ details: verified
3187
+ };
3188
+ commit([{
3189
+ path: pathFor("emails", emailId),
3190
+ value: next
3191
+ }]);
3192
+ return next;
3193
+ });
3194
+ },
3195
+ registerWait(input) {
3196
+ return transaction(() => {
3197
+ const { waiter: owner, ...registration } = input;
3198
+ const requested = wait({
3199
+ ...registration,
3200
+ ...owner ? { waiters: [owner] } : {},
3201
+ sessionKey: input.sessionKey ?? null,
3202
+ status: "unbound",
3203
+ sentEmailId: null
3204
+ }), previous = readWait(requested.requestId);
3205
+ if (previous) {
3206
+ if (!same({
3207
+ ...previous,
3208
+ waiters: void 0,
3209
+ status: "unbound",
3210
+ sentEmailId: null
3211
+ }, {
3212
+ ...requested,
3213
+ waiters: void 0
3214
+ })) throw invalidSharedMail();
3215
+ const next = addWaiters(previous, owner ? [owner] : []);
3216
+ commit([{
3217
+ path: pathFor("waits", next.requestId),
3218
+ value: next
3219
+ }]);
3220
+ return next;
3221
+ }
3222
+ commit([{
3223
+ path: pathFor("waits", requested.requestId),
3224
+ value: requested
3225
+ }, {
3226
+ path: unboundPath(requested),
3227
+ value: { requestId: requested.requestId }
3228
+ }]);
3229
+ return requested;
3230
+ });
3231
+ },
3232
+ joinWait(requestId, owner) {
3233
+ return transaction(() => {
3234
+ const previous = requiredWait(requestId);
3235
+ if (previous.status !== "bound" && previous.status !== "completed") throw invalidSharedMail();
3236
+ const next = addWaiters(previous, [owner]);
3237
+ commit([{
3238
+ path: pathFor("waits", requestId),
3239
+ value: next
3240
+ }]);
3241
+ return next;
3242
+ });
3243
+ },
3244
+ releaseWaiter(requestId, token) {
3245
+ return transaction(() => {
3246
+ const previous = requiredWait(requestId), owner = mailId(token);
3247
+ const records = [previous];
3248
+ if (previous.status === "cancelled" && previous.sentEmailId) {
3249
+ const parent = read(pathFor("parents", previous.sentEmailId));
3250
+ if (parent && parent.requestId !== previous.requestId) records.push(requiredWait(parent.requestId));
3251
+ }
3252
+ commit(records.filter((record) => record.waiters).map((record) => ({
3253
+ path: pathFor("waits", record.requestId),
3254
+ value: {
3255
+ ...record,
3256
+ waiters: record.waiters?.filter((entry) => entry.token !== owner)
3257
+ }
3258
+ })));
3259
+ return requiredWait(requestId);
3260
+ });
3261
+ },
3262
+ findWaitByParent(sentEmailId) {
3263
+ return transaction(() => {
3264
+ const parent = read(pathFor("parents", sentEmailId));
3265
+ return parent === null ? null : requiredWait(parent.requestId);
3266
+ });
3267
+ },
3268
+ wakeDisposition(emailId, sentEmailId) {
3269
+ return transaction(() => {
3270
+ const email = readEmail(mailId(emailId));
3271
+ if (email?.route?.kind === "wait" && email.route.observed) return "observed";
3272
+ const parent = read(pathFor("parents", mailId(sentEmailId)));
3273
+ if (!parent) return "available";
3274
+ const w = requiredWait(parent.requestId);
3275
+ if (w.status === "bound" && w.waiters?.some(mayBeWaiting)) return "waiting";
3276
+ return "available";
3277
+ });
3278
+ },
3279
+ bindWait(requestId, sentEmailId) {
3280
+ return transaction(() => {
3281
+ const previous = requiredWait(requestId), parent = mailId(sentEmailId), parentPath = pathFor("parents", parent);
3282
+ const occupied = read(parentPath);
3283
+ if (previous.sentEmailId !== null && previous.sentEmailId !== parent) throw invalidSharedMail();
3284
+ if (occupied && occupied.requestId !== previous.requestId) {
3285
+ const canonical = requiredWait(occupied.requestId);
3286
+ if (canonical.sentEmailId !== parent || canonical.peer !== previous.peer || canonical.sessionKey !== null && previous.sessionKey !== null && canonical.sessionKey !== previous.sessionKey || !same(canonical.contactRequest ?? null, previous.contactRequest ?? null)) throw invalidSharedMail();
3287
+ if (canonical.status === "bound") {
3288
+ if (previous.status !== "unbound" && previous.status !== "cancelled") throw invalidSharedMail();
3289
+ const joined = previous.waiters ? addWaiters(canonical, previous.waiters) : {
3290
+ ...canonical,
3291
+ waiters: void 0
3292
+ };
3293
+ if (joined.waiters === void 0) delete joined.waiters;
3294
+ commit([
3295
+ {
3296
+ path: pathFor("waits", canonical.requestId),
3297
+ value: joined
3298
+ },
3299
+ {
3300
+ path: pathFor("waits", requestId),
3301
+ value: {
3302
+ ...previous,
3303
+ ...previous.waiters ? { waiters: [] } : {},
3304
+ status: "cancelled",
3305
+ sentEmailId: parent
3306
+ }
3307
+ },
3308
+ {
3309
+ path: unboundPath(previous),
3310
+ value: null
3311
+ }
3312
+ ]);
3313
+ return joined;
3314
+ }
3315
+ if (canonical.status !== "completed") throw invalidSharedMail();
3316
+ previous.sessionKey ??= canonical.sessionKey;
3317
+ }
3318
+ if (previous.status === "cancelled" || previous.status === "completed") throw invalidSharedMail();
3319
+ const next = {
3320
+ ...previous,
3321
+ status: "bound",
3322
+ sentEmailId: parent
3323
+ };
3324
+ commit([
3325
+ {
3326
+ path: pathFor("waits", requestId),
3327
+ value: next
3328
+ },
3329
+ {
3330
+ path: parentPath,
3331
+ value: {
3332
+ sentEmailId: parent,
3333
+ requestId: previous.requestId
3334
+ }
3335
+ },
3336
+ {
3337
+ path: unboundPath(previous),
3338
+ value: null
3339
+ }
3340
+ ]);
3341
+ return next;
3342
+ });
3343
+ },
3344
+ cancelWaitBeforeSend(requestId) {
3345
+ return transaction(() => {
3346
+ const previous = requiredWait(requestId);
3347
+ if (previous.status !== "unbound" && !(previous.status === "cancelled" && previous.sentEmailId === null)) throw invalidSharedMail();
3348
+ const next = {
3349
+ ...previous,
3350
+ status: "cancelled"
3351
+ };
3352
+ commit([{
3353
+ path: pathFor("waits", requestId),
3354
+ value: next
3355
+ }, {
3356
+ path: unboundPath(previous),
3357
+ value: null
3358
+ }]);
3359
+ return next;
3360
+ });
3361
+ },
3362
+ cancelRejectedSend(requestId) {
3363
+ return transaction(() => {
3364
+ const previous = requiredWait(requestId);
3365
+ if (previous.sentEmailId !== null || ![
3366
+ "unbound",
3367
+ "uncertain",
3368
+ "cancelled"
3369
+ ].includes(previous.status)) throw invalidSharedMail();
3370
+ const next = {
3371
+ ...previous,
3372
+ status: "cancelled"
3373
+ };
3374
+ commit([{
3375
+ path: pathFor("waits", requestId),
3376
+ value: next
3377
+ }, {
3378
+ path: unboundPath(previous),
3379
+ value: null
3380
+ }]);
3381
+ return next;
3382
+ });
3383
+ },
3384
+ finishWait(requestId) {
3385
+ return transaction(() => {
3386
+ const previous = requiredWait(requestId);
3387
+ if (previous.status === "completed") return previous;
3388
+ if (previous.status !== "bound") throw invalidSharedMail();
3389
+ let observed = 0, cursor;
3390
+ do {
3391
+ const page = fileIds(join(directory, "matches", previous.requestId), { cursor });
3392
+ for (const id of page.ids) {
3393
+ if (read(`matches/${previous.requestId}/${id}.json`) === null) throw invalidSharedMail();
3394
+ const r = requiredEmail(id).route;
3395
+ if (r?.kind !== "wait" || r.requestId !== previous.requestId || !r.observed) throw invalidSharedMail();
3396
+ observed++;
3397
+ }
3398
+ cursor = page.nextCursor ?? void 0;
3399
+ } while (cursor);
3400
+ if (!observed) throw invalidSharedMail();
3401
+ const next = {
3402
+ ...previous,
3403
+ status: "completed"
3404
+ };
3405
+ commit([{
3406
+ path: pathFor("waits", requestId),
3407
+ value: next
3408
+ }]);
3409
+ return next;
3410
+ });
3411
+ },
3412
+ markWaitUncertain(requestId) {
3413
+ return transaction(() => {
3414
+ const previous = requiredWait(requestId);
3415
+ if (previous.status !== "unbound" && previous.status !== "uncertain") throw invalidSharedMail();
3416
+ const next = {
3417
+ ...previous,
3418
+ status: "uncertain"
3419
+ };
3420
+ commit([{
3421
+ path: pathFor("waits", requestId),
3422
+ value: next
3423
+ }]);
3424
+ return next;
3425
+ });
3426
+ },
3427
+ claimForWait(emailId, requestId, token) {
3428
+ return transaction(() => {
3429
+ const record = requiredEmail(emailId), w = requiredWait(requestId);
3430
+ if (w.waiters && !w.waiters.some((owner) => owner.token === token && mayBeWaiting(owner))) return {
3431
+ status: "held",
3432
+ email: record
3433
+ };
3434
+ if (record.route?.kind === "notification" && ["accepted", "skipped"].includes(record.route.state) && w.contactRequest && w.waiters && w.status === "bound" && record.details?.authorization === "trusted" && record.details.peer === w.peer && record.details.replyToSentEmailId === w.sentEmailId) return {
3435
+ status: "claimed",
3436
+ email: record
3437
+ };
3438
+ if (record.route) return {
3439
+ status: record.route.kind === "wait" && record.route.requestId === w.requestId ? record.route.observed ? "already_observed" : "claimed" : "held",
3440
+ email: record
3441
+ };
3442
+ if (!record.details || record.details.authorization === "pending" || w.status !== "bound") return {
3443
+ status: "held",
3444
+ email: record
3445
+ };
3446
+ if (record.details.authorization !== "trusted" || record.details.peer !== w.peer || record.details.replyToSentEmailId !== w.sentEmailId) return {
3447
+ status: "unmatched",
3448
+ email: record
3449
+ };
3450
+ const next = {
3451
+ ...record,
3452
+ route: {
3453
+ kind: "wait",
3454
+ requestId: w.requestId,
3455
+ observed: false
3456
+ }
3457
+ };
3458
+ commit([{
3459
+ path: pathFor("emails", emailId),
3460
+ value: next
3461
+ }, {
3462
+ path: `matches/${w.requestId}/${record.emailId}.json`,
3463
+ value: {
3464
+ emailId: record.emailId,
3465
+ requestId: w.requestId
3466
+ }
3467
+ }]);
3468
+ return {
3469
+ status: "claimed",
3470
+ email: next
3471
+ };
3472
+ });
3473
+ },
3474
+ markWaitObserved(emailId, requestId) {
3475
+ return transaction(() => {
3476
+ const record = requiredEmail(emailId);
3477
+ if (record.route?.kind !== "wait" || record.route.requestId !== mailId(requestId)) throw invalidSharedMail();
3478
+ const next = {
3479
+ ...record,
3480
+ route: {
3481
+ ...record.route,
3482
+ observed: true
3483
+ }
3484
+ };
3485
+ commit([{
3486
+ path: pathFor("emails", emailId),
3487
+ value: next
3488
+ }]);
3489
+ return next;
3490
+ });
3491
+ },
3492
+ observeNotifiedContactReply(emailId, requestId, token) {
3493
+ return transaction(() => {
3494
+ const record = requiredEmail(emailId), w = requiredWait(requestId);
3495
+ if (record.route?.kind !== "notification" || !["accepted", "skipped"].includes(record.route.state)) return false;
3496
+ if (!w.contactRequest || !["bound", "completed"].includes(w.status) || !w.waiters?.some((owner) => owner.token === token && mayBeWaiting(owner)) || record.details?.authorization !== "trusted" || record.details.peer !== w.peer || record.details.replyToSentEmailId !== w.sentEmailId) throw invalidSharedMail();
3497
+ commit([{
3498
+ path: pathFor("waits", requestId),
3499
+ value: {
3500
+ ...w,
3501
+ status: "completed"
3502
+ }
3503
+ }]);
3504
+ return true;
3505
+ });
3506
+ },
3507
+ claimForNotification(emailId, sessionKey) {
3508
+ return transaction(() => {
3509
+ const record = requiredEmail(emailId), session = mailString(sessionKey), d = record.details;
3510
+ if (record.route) return {
3511
+ status: record.route.kind === "notification" && record.route.state === "skipped" ? "already_observed" : record.route.kind === "notification" && record.route.sessionKey === session ? record.route.state === "selected" ? "claimed" : record.route.state === "accepted" ? "already_observed" : "held" : "held",
3512
+ email: record
3513
+ };
3514
+ if (!d || d.authorization === "pending") return {
3515
+ status: "held",
3516
+ email: record
3517
+ };
3518
+ if (d.authorization !== "trusted") return {
3519
+ status: "unmatched",
3520
+ email: record
3521
+ };
3522
+ if (d.threadId) {
3523
+ const followed = readConversationFollow({
3524
+ configDir: options.configDir,
3525
+ scope: options.scope,
3526
+ recipient
3527
+ }, d.threadId);
3528
+ if (followed && followed.sessionKey !== session) return {
3529
+ status: "held",
3530
+ email: record
3531
+ };
3532
+ }
3533
+ if (d.replyToSentEmailId) {
3534
+ const parent = read(pathFor("parents", d.replyToSentEmailId));
3535
+ if (parent) {
3536
+ const w = requiredWait(parent.requestId);
3537
+ if (w.sentEmailId !== d.replyToSentEmailId) throw invalidSharedMail();
3538
+ if (w.sessionKey !== null && w.sessionKey !== session) return {
3539
+ status: "held",
3540
+ email: record
3541
+ };
3542
+ if (w.status === "bound" && w.peer === d.peer && (!w.waiters || w.waiters.some(mayBeWaiting))) return {
3543
+ status: "held",
3544
+ email: record
3545
+ };
3546
+ }
3547
+ }
3548
+ let cursor;
3549
+ do {
3550
+ const page = fileIds(join(directory, "unbound", hash(d.peer)), { cursor });
3551
+ for (const id of page.ids) {
3552
+ if (read(`unbound/${hash(d.peer)}/${id}.json`) === null) throw invalidSharedMail();
3553
+ const w = requiredWait(id);
3554
+ if (w.peer !== d.peer || w.status !== "unbound" && w.status !== "uncertain") throw invalidSharedMail();
3555
+ return {
3556
+ status: "held",
3557
+ email: record
3558
+ };
3559
+ }
3560
+ cursor = page.nextCursor ?? void 0;
3561
+ } while (cursor);
3562
+ const next = {
3563
+ ...record,
3564
+ route: {
3565
+ kind: "notification",
3566
+ sessionKey: session,
3567
+ state: "selected"
3568
+ }
3569
+ };
3570
+ commit([{
3571
+ path: pathFor("emails", emailId),
3572
+ value: next
3573
+ }]);
3574
+ return {
3575
+ status: "claimed",
3576
+ email: next
3577
+ };
3578
+ });
3579
+ },
3580
+ releaseNotification(emailId, sessionKey) {
3581
+ return transaction(() => {
3582
+ const record = requiredEmail(emailId), route = record.route;
3583
+ if (route?.kind !== "notification" || route.sessionKey !== mailString(sessionKey) || route.state !== "selected") throw invalidSharedMail();
3584
+ const next = {
3585
+ ...record,
3586
+ route: null
3587
+ };
3588
+ commit([{
3589
+ path: pathFor("emails", emailId),
3590
+ value: next
3591
+ }]);
3592
+ return next;
3593
+ });
3594
+ },
3595
+ skipNotification(emailId, sessionKey) {
3596
+ return transaction(() => {
3597
+ const record = requiredEmail(emailId), route = record.route;
3598
+ if (route?.kind !== "notification" || route.sessionKey !== mailString(sessionKey) || route.state !== "selected" && route.state !== "skipped") throw invalidSharedMail();
3599
+ const next = {
3600
+ ...record,
3601
+ route: {
3602
+ ...route,
3603
+ state: "skipped"
3604
+ }
3605
+ };
3606
+ commit([{
3607
+ path: pathFor("emails", emailId),
3608
+ value: next
3609
+ }]);
3610
+ return next;
3611
+ });
3612
+ },
3613
+ markNotification(emailId, state) {
3614
+ return transaction(() => {
3615
+ const record = requiredEmail(emailId), route = record.route;
3616
+ if (route?.kind !== "notification" || ![
3617
+ "submitting",
3618
+ "accepted",
3619
+ "unknown"
3620
+ ].includes(state) || route.state === "selected" && state !== "submitting" || route.state !== "selected" && route.state !== "submitting" && route.state !== state) throw invalidSharedMail();
3621
+ const next = {
3622
+ ...record,
3623
+ route: {
3624
+ ...route,
3625
+ state
3626
+ }
3627
+ };
3628
+ commit([{
3629
+ path: pathFor("emails", emailId),
3630
+ value: next
3631
+ }]);
3632
+ return next;
3633
+ });
3634
+ },
3635
+ listEmails(options = {}) {
3636
+ return transaction(() => {
3637
+ const page = fileIds(join(directory, "emails"), options);
3638
+ return {
3639
+ emails: page.ids.map(requiredEmail),
3640
+ nextCursor: page.nextCursor
3641
+ };
3642
+ });
3643
+ },
3644
+ listWaitEmails(requestId, options = {}) {
3645
+ return transaction(() => {
3646
+ const request = mailId(requestId), page = fileIds(join(directory, "matches", request), options);
3647
+ return {
3648
+ emails: page.ids.map((id) => {
3649
+ if (read(`matches/${request}/${id}.json`) === null) throw invalidSharedMail();
3650
+ const e = requiredEmail(id);
3651
+ if (e.route?.kind !== "wait" || e.route.requestId !== request) throw invalidSharedMail();
3652
+ return e;
3653
+ }),
3654
+ nextCursor: page.nextCursor
3655
+ };
3656
+ });
3657
+ }
3658
+ };
3659
+ }
3660
+ //#endregion
3661
+ //#region src/oclif/shared-mail-transport.ts
3662
+ function invalid$1(message) {
3663
+ return new EventReceiverError(message, "invalid_response");
3664
+ }
3665
+ function record(value) {
3666
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3667
+ }
3668
+ async function ensureSharedMailSubscription(options) {
3669
+ const signal = options.signal;
3670
+ const result = await createEndpoint({
3671
+ client: options.apiClient.client,
3672
+ body: {
3673
+ kind: "pull",
3674
+ name: options.subscription,
3675
+ rules: { event_types: ["email.received"] }
3676
+ },
3677
+ signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)]),
3678
+ responseStyle: "fields"
3679
+ });
3680
+ if (result.error || !result.data?.data) throw new EventReceiverError("Could not open the shared inbound subscription.", "subscription_failed", result.response?.status ?? 0);
3681
+ const created = result.data.data;
3682
+ if (typeof created.id !== "string" || !created.id || created.kind !== "pull" || created.enabled !== true || !Array.isArray(created.rules?.event_types) || created.rules.event_types.length !== 1 || created.rules.event_types[0] !== "email.received" || typeof created.recipient !== "string" || !/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/.test(created.recipient) || options.recipient !== void 0 && created.recipient.trim().toLowerCase() !== options.recipient.trim().toLowerCase() || !created.receiver_capabilities?.stream_protocols.includes("primitive.events.v1") || !created.receiver_capabilities.completion_modes.includes("sdk")) throw invalid$1("The API did not return a compatible subscription for the connected address.");
3683
+ return {
3684
+ endpointId: created.id,
3685
+ recipient: created.recipient.trim().toLowerCase()
3686
+ };
3687
+ }
3688
+ /** The elected foreground owner journals IDs before accepting remote delivery. */
3689
+ async function runSharedMailTransport(options) {
3690
+ const failed = new AbortController();
3691
+ const signal = AbortSignal.any([options.signal, failed.signal]);
3692
+ let connected = false;
3693
+ async function retry(operation, deadline) {
3694
+ for (let attempt = 0;; attempt++) {
3695
+ signal.throwIfAborted();
3696
+ if (deadline !== void 0 && Date.now() >= deadline) throw invalid$1("The shared inbound completion lease expired; durable ingress remains available for redelivery.");
3697
+ try {
3698
+ return await operation();
3699
+ } catch (error) {
3700
+ signal.throwIfAborted();
3701
+ if (!(error instanceof EventReceiverError) || ["invalid_response", "unsupported"].includes(error.code) || !([
3702
+ 0,
3703
+ 408,
3704
+ 429
3705
+ ].includes(error.status) || error.status >= 500)) throw error;
3706
+ connected = false;
3707
+ await options.status({ ready: false });
3708
+ await setTimeout$1(Math.max(error.retryAfterMs, Math.min(1e4, 250 * 2 ** Math.min(attempt, 6))), void 0, { signal });
3709
+ }
3710
+ }
3711
+ }
3712
+ const created = await retry(() => ensureSharedMailSubscription({
3713
+ ...options,
3714
+ signal
3715
+ }));
3716
+ let statusWork = Promise.resolve();
3717
+ const publishChecked = () => {
3718
+ const checked = options.checked;
3719
+ if (!checked) return;
3720
+ statusWork = statusWork.then(() => checked());
3721
+ statusWork.catch((error) => failed.abort(error));
3722
+ };
3723
+ const publishStatus = (value) => {
3724
+ publishChecked();
3725
+ statusWork = statusWork.then(() => options.status({
3726
+ gapCount: value.gap_count,
3727
+ lastGapReason: value.last_gap_reason
3728
+ }));
3729
+ statusWork.catch((error) => failed.abort(error));
3730
+ };
3731
+ const stream = new EventConnection(options.apiClient.client, created.endpointId, { onStatus: publishStatus });
3732
+ const ensureOpen = async () => {
3733
+ if (connected) return;
3734
+ await stream.open(signal);
3735
+ await statusWork;
3736
+ await options.ready();
3737
+ connected = true;
3738
+ };
3739
+ try {
3740
+ await retry(ensureOpen);
3741
+ while (!signal.aborted) {
3742
+ const offer = await retry(async () => {
3743
+ await ensureOpen();
3744
+ return stream.receive(signal);
3745
+ });
3746
+ publishStatus(offer);
3747
+ await statusWork;
3748
+ const delivery = offer.delivery;
3749
+ if (!delivery) continue;
3750
+ const started = performance.now();
3751
+ let body;
3752
+ try {
3753
+ body = JSON.parse(delivery.body);
3754
+ } catch {
3755
+ throw invalid$1("The shared subscription returned invalid JSON.");
3756
+ }
3757
+ if (delivery.event_type !== "email.received" || !record(body) || !record(body.email) || typeof body.email.id !== "string" || !body.email.id || typeof body.email.received_at !== "string" || !Number.isFinite(Date.parse(body.email.received_at)) || !record(body.email.smtp) || !Array.isArray(body.email.smtp.rcpt_to) || body.email.smtp.rcpt_to.length !== 1 || typeof body.email.smtp.rcpt_to[0] !== "string" || body.email.smtp.rcpt_to[0].trim().toLowerCase() !== options.recipient.trim().toLowerCase()) throw invalid$1("The shared subscription returned an invalid or out-of-scope inbound event.");
3758
+ await options.ingest({
3759
+ eventId: delivery.event_id,
3760
+ emailId: body.email.id,
3761
+ receivedAt: body.email.received_at
3762
+ });
3763
+ const elapsed = Math.round(performance.now() - started);
3764
+ if (elapsed > 3e4) throw invalid$1("Shared inbound persistence exceeded the handler deadline; durable ingress remains available for redelivery.");
3765
+ const completion = {
3766
+ queue_id: delivery.queue_id,
3767
+ delivery_id: delivery.delivery_id,
3768
+ lease_token: delivery.lease_token,
3769
+ mode: "sdk",
3770
+ accepted: true,
3771
+ duration_ms: elapsed
3772
+ };
3773
+ const leaseDeadline = Date.parse(delivery.lease_expires_at);
3774
+ if (Date.now() >= leaseDeadline) throw invalid$1("The shared inbound lease expired after durable recording; it remains eligible for redelivery.");
3775
+ await retry(async () => {
3776
+ await ensureOpen();
3777
+ if (Date.now() >= leaseDeadline) throw invalid$1("The shared inbound completion lease expired; durable ingress remains available for redelivery.");
3778
+ return stream.complete(completion, AbortSignal.any([signal, AbortSignal.timeout(Math.max(1, leaseDeadline - Date.now()))]));
3779
+ }, leaseDeadline);
3780
+ }
3781
+ } finally {
3782
+ stream.close();
3783
+ }
3784
+ }
3785
+ //#endregion
3786
+ //#region src/oclif/shared-mail-receiver.ts
3787
+ function sharedMailScope(apiKey, baseUrl) {
3788
+ if (!apiKey?.startsWith("pconn_")) throw new Error("Shared mail receiving requires connected credentials.");
3789
+ return listenIdentity(baseUrl, `connection:${scryptSync(apiKey, "primitive-listener-identity-v1", 32).toString("hex")}`);
3790
+ }
3791
+ /** Join locally, or own the stable remote subscription while this command lives. */
3792
+ async function openSharedMailReceiver(options) {
3793
+ const scope = sharedMailScope(options.apiKey, options.baseUrl);
3794
+ const stopping = new AbortController();
3795
+ let timer;
3796
+ const arm = () => {
3797
+ if (options.deadline == null) return;
3798
+ const remaining = Math.max(0, options.deadline - Date.now());
3799
+ timer = setTimeout(remaining > 2 ** 31 - 1 ? arm : () => stopping.abort(new DOMException("Mail wait timed out", "TimeoutError")), Math.min(remaining, 2 ** 31 - 1));
3800
+ };
3801
+ if (options.deadline != null && Date.now() >= options.deadline) stopping.abort(new DOMException("Mail wait timed out", "TimeoutError"));
3802
+ else arm();
3803
+ const signal = AbortSignal.any([stopping.signal, ...options.signal ? [options.signal] : []]);
3804
+ const store = await openSharedMailStore({
3805
+ configDir: options.configDir,
3806
+ scope,
3807
+ recipient: options.recipient,
3808
+ signal
3809
+ }).catch((error) => {
3810
+ clearTimeout(timer);
3811
+ throw error;
3812
+ });
3813
+ let failure;
3814
+ let cleanupFailure;
3815
+ let active;
3816
+ function ensureOwner() {
3817
+ signal.throwIfAborted();
3818
+ if (failure !== void 0) throw failure;
3819
+ const current = readSharedMailOwner(store);
3820
+ if (current?.alive || active) return current;
3821
+ const owner = tryOwnSharedMail(store);
3822
+ if (!owner) return readSharedMailOwner(store);
3823
+ active = runSharedMailTransport({
3824
+ apiClient: options.apiClient,
3825
+ subscription: store.subscriptionName,
3826
+ recipient: store.recipient,
3827
+ signal,
3828
+ ready: async () => {
3829
+ owner.markReady();
3830
+ },
3831
+ checked: async () => {
3832
+ owner.markChecked();
3833
+ },
3834
+ status: async (status) => {
3835
+ owner.markStatus(status);
3836
+ },
3837
+ ingest: async (event) => {
3838
+ await store.ingest(event);
3839
+ }
3840
+ }).catch((error) => {
3841
+ if (!signal.aborted) failure = error;
3842
+ }).finally(() => {
3843
+ try {
3844
+ owner.close();
3845
+ } catch (error) {
3846
+ cleanupFailure = error;
3847
+ failure ??= error;
3848
+ } finally {
3849
+ active = void 0;
3850
+ }
3851
+ });
3852
+ return readSharedMailOwner(store);
3853
+ }
3854
+ function watch(deadline) {
3855
+ const stop = new AbortController();
3856
+ const promise = waitForSharedMailChange(store, {
3857
+ signal: AbortSignal.any([signal, stop.signal]),
3858
+ timeoutMs: deadline == null ? 1e3 : Math.max(1, Math.min(1e3, deadline - Date.now()))
3859
+ });
3860
+ promise.catch(() => {});
3861
+ return {
3862
+ promise,
3863
+ close: () => stop.abort()
3864
+ };
3865
+ }
3866
+ return {
3867
+ store,
3868
+ signal,
3869
+ status: () => readSharedMailOwner(store),
3870
+ async ready(deadline) {
3871
+ while (deadline == null || Date.now() < deadline) {
3872
+ const watching = watch(deadline);
3873
+ try {
3874
+ const owner = ensureOwner();
3875
+ if (owner?.alive && owner.ready) return owner;
3876
+ await watching.promise;
3877
+ } finally {
3878
+ watching.close();
3879
+ }
3880
+ }
3881
+ return null;
3882
+ },
3883
+ async changed(deadline) {
3884
+ const watching = watch(deadline);
3885
+ try {
3886
+ ensureOwner();
3887
+ await watching.promise;
3888
+ } finally {
3889
+ watching.close();
3890
+ }
3891
+ if (failure !== void 0) throw failure;
3892
+ },
3893
+ async close() {
3894
+ clearTimeout(timer);
3895
+ stopping.abort();
3896
+ await active;
3897
+ if (cleanupFailure !== void 0) throw cleanupFailure;
3898
+ }
3899
+ };
3900
+ }
3901
+ //#endregion
3902
+ //#region src/oclif/reconcile-chat-send.ts
3903
+ /** Lookup only. An empty result never authorizes another send. */
3904
+ async function reconcileChatSend(params) {
3905
+ const result = await readBeforeDeadline(params.deadline, (signal) => listSentEmails({
3906
+ client: params.apiClient.client,
3907
+ query: {
3908
+ idempotency_key: params.idempotencyKey,
3909
+ limit: 2
3910
+ },
3911
+ signal,
3912
+ responseStyle: "fields"
3913
+ }));
3914
+ if (result === null) return null;
3915
+ const page = result.data;
3916
+ if (result.error || !page || !Array.isArray(page.data) || !page.meta || page.meta.cursor !== null || page.data.length > 1) throw new Errors.CLIError("Could not reconcile the saved send intent. Its outcome remains unknown; do not send again.", { exit: 1 });
3917
+ const sent = page.data[0];
3918
+ if (!sent) return null;
3919
+ if (sent.client_idempotency_key !== params.idempotencyKey || typeof sent.from_address !== "string" || sent.from_address.trim().toLowerCase() !== params.from.trim().toLowerCase() || typeof sent.to_address !== "string" || sent.to_address.trim().toLowerCase() !== params.recipient.trim().toLowerCase() || typeof sent.id !== "string" || !sent.id) throw new Errors.CLIError("The saved send lookup returned conflicting identity metadata. Inspect the existing send before retrying.", { exit: 1 });
3920
+ return sent;
3921
+ }
3922
+ //#endregion
3923
+ //#region src/oclif/send-outcome.ts
3924
+ const SEND_OUTCOME_EXIT_CODES = {
3925
+ replied: 0,
3926
+ sent: 0,
3927
+ already_sent: 0,
3928
+ not_sent: 1,
3929
+ sent_awaiting_reply: 3,
3930
+ uncertain: 4
3931
+ };
3932
+ const DEFINITIVE_SEND_REJECTION_STATUSES = [
3933
+ 400,
3934
+ 401,
3935
+ 402,
3936
+ 403,
3937
+ 404,
3938
+ 413,
3939
+ 422,
3940
+ 429
3941
+ ];
3942
+ const SEND_OUTCOME_HELP = `Outcomes and exit codes, shared by chat, chat reply, send and reply (JSON output carries the name as "outcome"):
3943
+ - exit 0 replied: chat only. The message was sent and a reply arrived.
3944
+ - exit 0 sent: accepted for delivery. A queued status counts as sent.
3945
+ - exit 0 already_sent: an identical earlier send exists (or was deleted: HTTP 410 sent_email_deleted). Nothing new went out. Do not resend.
3946
+ - exit 1 not_sent: the API rejected the request (HTTP 400, 401, 402, 403, 404, 413, 422 or 429), the command failed before sending, or the send record has status agent_failed, gate_denied or canceled. Nothing went out.
3947
+ - exit 2: invalid flags or arguments. Nothing went out.
3948
+ - exit 3 sent_awaiting_reply: chat only. Sent, but no reply before the timeout. Wait; do not resend.
3949
+ - exit 4 uncertain: transport error, conflict, server error, or a send record with status unknown. It may or may not have gone out. Reconcile with primitive sent get --idempotency-key <key> (the key is in the --json envelope) before retrying.`;
3950
+ function sendOutcomeExitCode(outcome) {
3951
+ return SEND_OUTCOME_EXIT_CODES[outcome];
3952
+ }
3953
+ function sendFailureOutcome(httpStatus) {
3954
+ return httpStatus !== void 0 && DEFINITIVE_SEND_REJECTION_STATUSES.includes(httpStatus) ? "not_sent" : "uncertain";
3955
+ }
3956
+ const SENT_EMAIL_DELETED_CODE = "sent_email_deleted";
3957
+ function errorPayloadCode(payload) {
3958
+ if (payload === null || typeof payload !== "object") return void 0;
3959
+ const code = payload.code;
3960
+ return typeof code === "string" ? code : void 0;
3961
+ }
3962
+ function isDeletedEarlierSend(httpStatus, errorPayload) {
3963
+ return httpStatus === 410 && errorPayloadCode(errorPayload) === SENT_EMAIL_DELETED_CODE;
3964
+ }
3965
+ /** Classify an API error from a send request. */
3966
+ function classifySendError(httpStatus, errorPayload) {
3967
+ return isDeletedEarlierSend(httpStatus, errorPayload) ? "already_sent" : sendFailureOutcome(httpStatus);
3968
+ }
3969
+ function formatDeletedEarlierSendNotice() {
3970
+ return "Already sent: an earlier send matching this request went out and its record was later deleted, so the API refused this one (HTTP 410 sent_email_deleted). Nothing new was sent. Do not resend or change the idempotency key to get around this.";
3971
+ }
3972
+ /** Read the HTTP status from a `responseStyle: "fields"` API result. */
3973
+ function apiResultHttpStatus(result) {
3974
+ if (result === null || typeof result !== "object") return void 0;
3975
+ if (!("response" in result)) return void 0;
3976
+ const response = result.response;
3977
+ return typeof response?.status === "number" ? response.status : void 0;
3978
+ }
3979
+ const SEND_STATUSES_THAT_DID_NOT_GO_OUT = new Set([
3980
+ "agent_failed",
3981
+ "canceled",
3982
+ "gate_denied"
3983
+ ]);
3984
+ const SEND_STATUSES_WITH_UNKNOWN_OUTCOME = new Set(["unknown"]);
3985
+ /**
3986
+ * Classify a send record the API returned with a 2xx. A 2xx alone does
3987
+ * not mean a message went out: the record's status can show the
3988
+ * attempt was rejected or that its result is indeterminate. The same
3989
+ * holds for an idempotent replay, which returns the earlier attempt's
3990
+ * record as it stands.
3991
+ */
3992
+ function successfulSendOutcome(sent) {
3993
+ if (SEND_STATUSES_THAT_DID_NOT_GO_OUT.has(sent.status)) return "not_sent";
3994
+ if (SEND_STATUSES_WITH_UNKNOWN_OUTCOME.has(sent.status)) return "uncertain";
3995
+ return sent.idempotent_replay === true ? "already_sent" : "sent";
3996
+ }
3997
+ /**
3998
+ * Summary for a 2xx send record whose status shows it did not go out
3999
+ * (`not_sent`) or that its result is indeterminate (`uncertain`).
4000
+ */
4001
+ function formatSendRecordFailureSummary(noun, outcome, sent) {
4002
+ const record = sent.idempotent_replay === true ? `the earlier identical attempt (sent id ${sent.id}) has status ${sent.status}` : `the send record (id ${sent.id}) has status ${sent.status}`;
4003
+ if (outcome === "not_sent") return `${noun} not sent: ${record}. Nothing went out; fix the problem before retrying.`;
4004
+ return `${noun} send outcome uncertain: ${record}. It may or may not have gone out. Do not resend blindly; check sent history first.`;
4005
+ }
4006
+ function describeSentStatus(status) {
4007
+ if (status === "queued") return "queued for delivery";
4008
+ if (status === "delivered") return "delivered";
4009
+ return `status ${status}`;
4010
+ }
4011
+ function formatSentSummary(noun, sent) {
4012
+ return `${noun} sent (${describeSentStatus(sent.status)}, id ${sent.id}). Do not resend.`;
4013
+ }
4014
+ function formatSendFailureSummary(noun, outcome, httpStatus) {
4015
+ if (outcome === "not_sent") return `${noun} not sent: ${httpStatus === void 0 ? "the request failed before sending" : `the API rejected the request (HTTP ${httpStatus})`}. Nothing went out; fix the problem before retrying.`;
4016
+ return `${noun} send outcome uncertain (${httpStatus === void 0 ? "no response from the API" : `HTTP ${httpStatus}`}): it may or may not have gone out. Do not resend blindly; check sent history first.`;
4017
+ }
4018
+ function priorRepliesThatWentOut(replies, options = {}) {
4019
+ return (replies ?? []).filter((reply) => !SEND_STATUSES_THAT_DID_NOT_GO_OUT.has(reply.status) && reply.id !== options.excludeSentId);
4020
+ }
4021
+ function formatPriorRepliesWarning(prior) {
4022
+ const latest = prior.at(-1);
4023
+ if (!latest) return null;
4024
+ return `This email already has ${prior.length === 1 ? "1 outgoing email" : `${prior.length} outgoing emails`}, most recently at ${latest.created_at} (sent id ${latest.id}). These may include activity updates and do not prove a completed answer. Sending this reply.`;
4025
+ }
4026
+ function formatPriorRepliesCheckSkipped(emailId, reason) {
4027
+ return `Could not check whether you already replied to email ${emailId} (${reason}). Prior-reply check skipped; sending the reply anyway.`;
4028
+ }
4029
+ /**
4030
+ * Look up the inbound email and report replies to it that already
4031
+ * went out. Never throws: the check is advisory and must not block a
4032
+ * send, so every failure comes back as `skipped` with a reason the
4033
+ * caller surfaces.
4034
+ */
4035
+ async function checkPriorReplies(params) {
4036
+ try {
4037
+ const result = await getEmail({
4038
+ client: params.client,
4039
+ path: { id: params.emailId },
4040
+ responseStyle: "fields"
4041
+ });
4042
+ if (result.error) {
4043
+ const status = apiResultHttpStatus(result);
4044
+ return {
4045
+ status: "skipped",
4046
+ reason: status === void 0 ? "the email lookup failed" : `the email lookup returned HTTP ${status}`
4047
+ };
4048
+ }
4049
+ const envelope = result.data;
4050
+ const detail = envelope?.data ?? envelope;
4051
+ if (!detail || !Array.isArray(detail.replies)) return {
4052
+ status: "skipped",
4053
+ reason: "the email lookup returned no reply history"
4054
+ };
4055
+ return {
4056
+ status: "checked",
4057
+ prior: priorRepliesThatWentOut(detail.replies),
4058
+ detail
4059
+ };
4060
+ } catch (error) {
4061
+ return {
4062
+ status: "skipped",
4063
+ reason: `the email lookup failed: ${error instanceof Error ? error.message : String(error)}`
4064
+ };
4065
+ }
4066
+ }
4067
+ function shellQuote(value) {
4068
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
4069
+ return `'${value.replaceAll("'", "'\\''")}'`;
4070
+ }
4071
+ function commandFromArgv(argv) {
4072
+ return argv.map(shellQuote).join(" ");
4073
+ }
4074
+ function buildFollowUpCommand(kind, description, argv, options = {}) {
4075
+ const requiresMessage = options.requiresMessage ?? false;
4076
+ return {
4077
+ argv,
4078
+ description,
4079
+ command: commandFromArgv(argv),
4080
+ kind,
4081
+ placeholders: requiresMessage ? [{
4082
+ description: "Replace with the message body before running.",
4083
+ token: "<message>"
4084
+ }] : [],
4085
+ requires_message: requiresMessage
4086
+ };
4087
+ }
4088
+ const SENT_HISTORY_CLOCK_SKEW_MS = 300 * 1e3;
4089
+ function sentHistoryWindowStart(attemptStartedAtIso) {
4090
+ const started = Date.parse(attemptStartedAtIso);
4091
+ if (!Number.isFinite(started)) return attemptStartedAtIso;
4092
+ return new Date(started - SENT_HISTORY_CLOCK_SKEW_MS).toISOString();
4093
+ }
4094
+ /** Header rule for Idempotency-Key: 1 to 255 printable ASCII characters. */
4095
+ const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
4096
+ const IDEMPOTENCY_KEY_FLAG_DESCRIPTION = "Idempotency key for this send (1 to 255 printable ASCII characters). Retrying with the same key returns the original send instead of sending twice. Defaults to a key derived from the request content and the current five-minute window, so an identical retry within that window is deduplicated and a deliberate repeat later still sends. To retry an uncertain send after that, pass the key it reported. Look a send up later with `primitive sent get --idempotency-key <key>`.";
4097
+ function assertValidIdempotencyKey(key) {
4098
+ if (!IDEMPOTENCY_KEY_PATTERN.test(key)) throw new Errors.CLIError("--idempotency-key must be 1 to 255 printable ASCII characters with no spaces.", { exit: 2 });
4099
+ return key;
4100
+ }
4101
+ const NON_CONTENT_SEND_FIELDS = new Set(["wait", "wait_timeout_ms"]);
4102
+ function canonicalJson(value) {
4103
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item ?? null)).join(",")}]`;
4104
+ if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
4105
+ return JSON.stringify(value ?? null);
4106
+ }
4107
+ /**
4108
+ * How long a derived key stays the same. It matches the window the API
4109
+ * uses for the key it derives itself when a request carries none, so an
4110
+ * identical send repeated inside this window is deduplicated and a
4111
+ * deliberate repeat later (a digest, a heartbeat, the same short reply
4112
+ * on another day) still goes out, exactly as without a key. An explicit
4113
+ * --idempotency-key never expires.
4114
+ */
4115
+ const DERIVED_IDEMPOTENCY_WINDOW_MS = 300 * 1e3;
4116
+ /**
4117
+ * The idempotency key a send uses when the caller passes none. It is a
4118
+ * hash of what is sent (and, for a reply, of the email replied to) and
4119
+ * of the current five-minute window, so dedup matches what the API did
4120
+ * before the CLI sent a key, while the CLI knows the key before the
4121
+ * request starts. That lets an uncertain outcome still report the key to
4122
+ * reconcile with, and to retry under with --idempotency-key.
4123
+ */
4124
+ function deriveSendIdempotencyKey(kind, request, now = Date.now()) {
4125
+ const content = Object.fromEntries(Object.entries(request).filter(([key]) => !NON_CONTENT_SEND_FIELDS.has(key)));
4126
+ return `primitive-${kind}-${createHash("sha256").update(canonicalJson(content)).update(`:t${Math.floor(now / DERIVED_IDEMPOTENCY_WINDOW_MS)}`).digest("hex")}`;
4127
+ }
4128
+ /**
4129
+ * Identity fields every send, reply and chat `--json` envelope carries,
4130
+ * whatever the outcome. `sent_email_id` is null until a send record is
4131
+ * known; `idempotency_key` is known before the request starts, so an
4132
+ * uncertain outcome can be reconciled with
4133
+ * `primitive sent get --idempotency-key <key>`.
4134
+ */
4135
+ function sendIdentityFields(params) {
4136
+ return {
4137
+ sent_email_id: params.sent?.id ?? null,
4138
+ idempotency_key: params.idempotencyKey ?? params.sent?.client_idempotency_key ?? null
4139
+ };
4140
+ }
4141
+ /**
4142
+ * Commands that inspect an existing send. None of them send again:
4143
+ * a caller that follows them after a failure cannot double-send.
4144
+ */
4145
+ function buildSendCommandFollowUps(params) {
4146
+ const commands = [];
4147
+ if (params.sentId === null && params.outcome === "uncertain" && params.idempotencyKey) commands.push(buildFollowUpCommand("find_sent_email_by_idempotency_key", "Look up this attempt by its idempotency key before retrying", [
4148
+ "primitive",
4149
+ "sent",
4150
+ "get",
4151
+ "--idempotency-key",
4152
+ params.idempotencyKey
4153
+ ]));
4154
+ if (params.sentId !== null) commands.push(buildFollowUpCommand("inspect_sent_email", "Inspect the send and its delivery status", [
4155
+ "primitive",
4156
+ "sent",
4157
+ "get",
4158
+ "--id",
4159
+ params.sentId
4160
+ ]));
4161
+ if (params.outcome === "uncertain") commands.push(buildFollowUpCommand("list_recent_sent_emails", "Check sent history for this attempt before retrying", [
4162
+ "primitive",
4163
+ "sent",
4164
+ "list",
4165
+ "--date-from",
4166
+ sentHistoryWindowStart(params.attemptStartedAtIso)
4167
+ ]));
4168
+ return commands;
4169
+ }
4170
+ /**
4171
+ * Classify a `send` / `reply` API result, write the stderr summary,
4172
+ * and print stdout. Without `--json` stdout stays exactly what these
4173
+ * commands always printed (the SendMailResult, or `null`), so
4174
+ * existing `| jq` pipelines keep working; the outcome is on stderr
4175
+ * and in the exit code. With `--json` stdout is always an envelope,
4176
+ * whatever the outcome, and the summary is only its outcome_message.
4177
+ */
4178
+ function reportSendCommandResult(params) {
4179
+ const httpStatus = apiResultHttpStatus(params.result);
4180
+ let outcome;
4181
+ let outcomeMessage;
4182
+ let sent = null;
4183
+ let errorPayload = null;
4184
+ if (params.result.error) {
4185
+ errorPayload = extractErrorPayload(params.result.error);
4186
+ params.onApiError(errorPayload);
4187
+ const failure = classifySendError(httpStatus, errorPayload);
4188
+ outcome = failure;
4189
+ outcomeMessage = failure === "already_sent" ? formatDeletedEarlierSendNotice() : formatSendFailureSummary(params.noun, failure, httpStatus);
4190
+ if (!params.json) params.writeStderr(`${outcomeMessage}\n`);
4191
+ } else {
4192
+ sent = params.result.data?.data ?? null;
4193
+ if (sent === null) {
4194
+ outcome = "uncertain";
4195
+ outcomeMessage = `${params.noun} send outcome uncertain: the API accepted the request but returned no send record. Do not resend blindly; check sent history first.`;
4196
+ errorPayload = { message: "The API returned no send record." };
4197
+ } else {
4198
+ outcome = successfulSendOutcome(sent);
4199
+ outcomeMessage = outcome === "not_sent" || outcome === "uncertain" ? formatSendRecordFailureSummary(params.noun, outcome, sent) : outcome === "already_sent" ? formatAlreadySentNotice(sent) : formatSentSummary(params.noun, sent);
4200
+ }
4201
+ if (!params.json) params.writeStderr(`${outcomeMessage}\n`);
4202
+ }
4203
+ if (params.json) {
4204
+ const envelope = {
4205
+ outcome,
4206
+ exit_code: sendOutcomeExitCode(outcome),
4207
+ outcome_message: outcomeMessage,
4208
+ ...sendIdentityFields({
4209
+ idempotencyKey: params.idempotencyKey,
4210
+ sent
4211
+ }),
4212
+ sent,
4213
+ http_status: httpStatus ?? null,
4214
+ error: serializeErrorPayload(errorPayload),
4215
+ follow_up_commands: buildSendCommandFollowUps({
4216
+ attemptStartedAtIso: params.attemptStartedAtIso,
4217
+ idempotencyKey: params.idempotencyKey,
4218
+ outcome,
4219
+ sentId: sent?.id ?? null
4220
+ }),
4221
+ ...params.extraEnvelopeFields
4222
+ };
4223
+ params.log(JSON.stringify(envelope, null, 2));
4224
+ } else if (!params.result.error) params.log(JSON.stringify(sent, null, 2));
4225
+ return outcome;
4226
+ }
4227
+ /** JSON-safe form of an API error payload; `Error` objects stringify to `{}`. */
4228
+ function serializeErrorPayload(payload) {
4229
+ if (!(payload instanceof Error)) return payload ?? null;
4230
+ const serialized = {
4231
+ name: payload.name,
4232
+ message: payload.message
4233
+ };
4234
+ const cause = payload.cause;
4235
+ if (cause && typeof cause.code === "string") serialized.code = cause.code;
4236
+ return serialized;
4237
+ }
4238
+ /**
4239
+ * Envelope for a `send` / `reply` that threw instead of returning an
4240
+ * API result. Printed in --json mode so stdout carries an envelope for
4241
+ * every outcome. Before the request starts (bad input, missing auth,
4242
+ * unreadable attachment) nothing went out; after it starts the
4243
+ * outcome is unknown.
4244
+ */
4245
+ function buildThrownSendFailureEnvelope(params) {
4246
+ const message = params.error instanceof Error ? params.error.message : String(params.error);
4247
+ const outcome = params.requestStarted ? "uncertain" : "not_sent";
4248
+ return {
4249
+ outcome,
4250
+ exit_code: params.requestStarted ? sendOutcomeExitCode("uncertain") : thrownErrorExitCode(params.error),
4251
+ outcome_message: formatSendFailureSummary(params.noun, outcome, void 0),
4252
+ ...sendIdentityFields({
4253
+ idempotencyKey: params.idempotencyKey,
4254
+ sent: null
4255
+ }),
4256
+ sent: null,
4257
+ http_status: null,
4258
+ error: { message },
4259
+ follow_up_commands: params.requestStarted && params.attemptStartedAtIso ? buildSendCommandFollowUps({
4260
+ attemptStartedAtIso: params.attemptStartedAtIso,
4261
+ idempotencyKey: params.idempotencyKey,
4262
+ outcome,
4263
+ sentId: null
4264
+ }) : [],
4265
+ ...params.extraEnvelopeFields
4266
+ };
4267
+ }
4268
+ /** Exit code oclif will use for a thrown error (2 for usage errors). */
4269
+ function thrownErrorExitCode(error) {
4270
+ const exit = error?.oclif?.exit;
4271
+ return typeof exit === "number" ? exit : 1;
4272
+ }
4273
+ //#endregion
4274
+ //#region src/oclif/signal-command.ts
4275
+ const sendStatuses = new Set([
4276
+ "queued",
4277
+ "submitted_to_agent",
4278
+ "agent_failed",
4279
+ "gate_denied",
4280
+ "unknown",
4281
+ "delivered",
4282
+ "bounced",
4283
+ "deferred",
4284
+ "wait_timeout",
4285
+ "scheduled",
4286
+ "canceled"
4287
+ ]);
4288
+ const validSendStatus = (value) => typeof value === "string" && sendStatuses.has(value);
4289
+ const invalid = () => /* @__PURE__ */ new Error("Saved signal state is invalid. Preserve it; do not resend.");
4290
+ function input(options, parent, now, expires = now + (options.expiresIn ?? 30) * 1e3) {
4291
+ if (options.kind === "ack") return {
4292
+ kind: "ack",
4293
+ parent,
4294
+ status: options.status
4295
+ };
4296
+ if (options.kind === "read") return {
4297
+ kind: "read",
4298
+ parent
4299
+ };
4300
+ return {
4301
+ kind: options.kind,
4302
+ parent,
4303
+ expiresAtMs: expires
4304
+ };
4305
+ }
4306
+ function parseState(raw, options, parent) {
4307
+ try {
4308
+ const row = mailObject(raw, [
4309
+ "version",
4310
+ "prepared",
4311
+ "phase",
4312
+ "sentId"
4313
+ ]);
4314
+ if (row.version !== 1 || ![
4315
+ "prepared",
4316
+ "submitting",
4317
+ "sent",
4318
+ "not_sent",
4319
+ "uncertain",
4320
+ "expired"
4321
+ ].includes(String(row.phase))) throw invalid();
4322
+ const p = mailObject(row.prepared, [
4323
+ "accountScope",
4324
+ "preparedAtMs",
4325
+ "expiresAtMs",
4326
+ "idempotencyKey",
4327
+ "requestJson"
4328
+ ]);
4329
+ if (typeof p.requestJson !== "string" || typeof p.preparedAtMs !== "number" || p.expiresAtMs !== null && typeof p.expiresAtMs !== "number") throw invalid();
4330
+ const body = JSON.parse(p.requestJson);
4331
+ const control = JSON.parse(Buffer.from(body.attachments[0].content_base64, "base64").toString("utf8"));
4332
+ const ids = [control.interaction_id.split("@")[0], control.step_id];
4333
+ const rebuilt = prepareSignalEmail(input(options, parent, p.preparedAtMs, p.expiresAtMs ?? void 0), {
4334
+ now: () => p.preparedAtMs,
4335
+ uuid: () => ids.shift()
4336
+ });
4337
+ if (rebuilt.status !== "prepared" || JSON.stringify(rebuilt.prepared) !== JSON.stringify(row.prepared)) throw invalid();
4338
+ return {
4339
+ version: 1,
4340
+ prepared: rebuilt.prepared,
4341
+ phase: row.phase,
4342
+ sentId: row.sentId === null ? null : mailId(row.sentId)
4343
+ };
4344
+ } catch {
4345
+ throw invalid();
4346
+ }
4347
+ }
4348
+ function report(state, repeated = false) {
4349
+ const outcome = state.phase === "sent" ? repeated ? "already_sent" : "sent" : state.phase === "submitting" ? "uncertain" : state.phase;
4350
+ return {
4351
+ exitCode: outcome === "uncertain" ? 4 : outcome === "not_sent" ? 1 : 0,
4352
+ data: {
4353
+ outcome,
4354
+ sent_id: state.sentId,
4355
+ expires_at: state.prepared.expiresAtMs === null ? null : new Date(state.prepared.expiresAtMs).toISOString(),
4356
+ guidance: outcome === "uncertain" ? "Outcome unknown. Repeat this exact command to reconcile the saved intent; never resend with a new key or profile." : outcome === "expired" ? "The saved activity expired without dispatch. A new explicit invocation may prepare fresh activity." : "Signals report communication state only; they do not grant task or tool authority."
4357
+ }
4358
+ };
4359
+ }
4360
+ /** Explicit one-shot signal send. The complete intent is durable before POST. */
4361
+ async function sendSignal(context, options) {
4362
+ const id = mailId(options.id);
4363
+ if (![
4364
+ "read",
4365
+ "ack",
4366
+ "working",
4367
+ "typing"
4368
+ ].includes(options.kind)) throw new Error("Unknown signal kind.");
4369
+ if (options.kind === "ack" ? ![
4370
+ "received",
4371
+ "will_process",
4372
+ "will_not_process"
4373
+ ].includes(options.status ?? "") : options.status !== void 0) throw new Error("Only ack requires --status received, will_process, or will_not_process.");
4374
+ if (options.expiresIn !== void 0 && (!["working", "typing"].includes(options.kind) || !Number.isInteger(options.expiresIn) || options.expiresIn < 1 || options.expiresIn > (options.kind === "typing" ? 30 : 60))) throw new Error("--expires-in applies only to working/typing and must be 1-30 seconds for typing or 1-60 for working.");
4375
+ const result = await getEmail({
4376
+ client: context.apiClient.client,
4377
+ path: { id },
4378
+ responseStyle: "fields",
4379
+ signal: AbortSignal.timeout(5e3)
4380
+ });
4381
+ const detail = result.data?.data;
4382
+ if (result.error || !detail || detail.id !== id || detail.recipient.toLowerCase() !== context.identity.agentAddress.toLowerCase() || detail.to_email.toLowerCase() !== context.identity.agentAddress.toLowerCase() || !["accepted", "completed"].includes(detail.status)) throw new Error("The parent email is unavailable for this connected profile.");
4383
+ const trust = scopedChatSenderTrust(detail, detail.from_email);
4384
+ if (!trust.trusted) throw new Error(`Parent sender authentication rejected (reason: ${trust.reason}; retryable: ${trust.retryable}).`);
4385
+ if (detail.from_email.trim().toLowerCase() === context.identity.agentAddress.toLowerCase()) throw new Error("Do not send a signal in response to this profile's own email.");
4386
+ if (!isPlainChatReply(detail)) throw new Error("Signals require a fully parsed plain email. Signals, interactions, and unknown content cannot trigger another signal.");
4387
+ if (!detail.message_id) throw new Error("The parent Message-ID is not available. Nothing was sent; retry this exact email after processing completes.");
4388
+ const scope = sharedMailScope(context.apiKey, context.identity.apiBaseUrl);
4389
+ const parent = {
4390
+ accountScope: scope,
4391
+ from: detail.from_email.trim().toLowerCase(),
4392
+ to: context.identity.agentAddress,
4393
+ messageId: detail.message_id,
4394
+ subject: detail.subject ?? null,
4395
+ references: detail.parsed.references ?? []
4396
+ };
4397
+ const key = createHash("sha256").update(JSON.stringify([
4398
+ scope,
4399
+ context.identity.orgId,
4400
+ parent.to,
4401
+ id,
4402
+ options.kind,
4403
+ options.status ?? null,
4404
+ ...options.slot === void 0 ? [] : [options.slot]
4405
+ ])).digest("hex");
4406
+ const directory = join(context.configDir, "signals", key);
4407
+ privateMailDirectory(directory, true);
4408
+ const release = acquireListenLock(directory, "signal-send");
4409
+ const path = join(directory, "intent.json"), now = context.now ?? Date.now;
4410
+ try {
4411
+ const raw = readMailJson(path, 256 * 1024);
4412
+ let state = raw === null ? null : parseState(raw, options, parent);
4413
+ if (state?.phase === "submitting" || state?.phase === "uncertain") {
4414
+ let prior;
4415
+ try {
4416
+ prior = await reconcileChatSend({
4417
+ apiClient: context.apiClient,
4418
+ idempotencyKey: state.prepared.idempotencyKey,
4419
+ from: parent.to,
4420
+ recipient: parent.from,
4421
+ deadline: Date.now() + 5e3
4422
+ });
4423
+ } catch {
4424
+ return report(state);
4425
+ }
4426
+ if (!prior || !validSendStatus(prior.status)) return report(state);
4427
+ const outcome = successfulSendOutcome({
4428
+ status: prior.status,
4429
+ idempotent_replay: true
4430
+ });
4431
+ state.phase = outcome === "uncertain" ? "uncertain" : outcome === "not_sent" ? "not_sent" : "sent";
4432
+ try {
4433
+ state.sentId = mailId(prior.id);
4434
+ } catch {
4435
+ state.phase = "uncertain";
4436
+ writeMailJson(path, state);
4437
+ return report(state);
4438
+ }
4439
+ writeMailJson(path, state);
4440
+ if (state.sentId) recordSentSignal(context.configDir, state.sentId);
4441
+ if (state.phase === "uncertain") return report(state);
4442
+ }
4443
+ const expired = state?.prepared.expiresAtMs !== null && state?.prepared.expiresAtMs !== void 0 && now() >= state.prepared.expiresAtMs;
4444
+ if (state && [
4445
+ "sent",
4446
+ "not_sent",
4447
+ "expired"
4448
+ ].includes(state.phase)) {
4449
+ if (state.sentId) recordSentSignal(context.configDir, state.sentId);
4450
+ if (!expired) return report(state, true);
4451
+ state = null;
4452
+ }
4453
+ if (!state) {
4454
+ const prepared = prepareSignalEmail(input(options, parent, now()), {
4455
+ now,
4456
+ uuid: randomUUID
4457
+ });
4458
+ if (prepared.status !== "prepared") throw new Error("The parent is not ready. Nothing was sent.");
4459
+ state = {
4460
+ version: 1,
4461
+ prepared: prepared.prepared,
4462
+ phase: "prepared",
4463
+ sentId: null
4464
+ };
4465
+ writeMailJson(path, state);
4466
+ }
4467
+ if (state.prepared.expiresAtMs !== null && now() >= state.prepared.expiresAtMs) {
4468
+ state.phase = "expired";
4469
+ writeMailJson(path, state);
4470
+ return report(state);
4471
+ }
4472
+ if (options.shouldSend && !options.shouldSend()) return report(state);
4473
+ state.phase = "submitting";
4474
+ writeMailJson(path, state);
4475
+ let outcome;
4476
+ try {
4477
+ outcome = await sendPreparedSignal(async (body, idempotencyKey) => sendEmail({
4478
+ client: context.apiClient.client,
4479
+ body,
4480
+ headers: { "Idempotency-Key": idempotencyKey },
4481
+ responseStyle: "fields",
4482
+ signal: AbortSignal.timeout(5e3)
4483
+ }), state.prepared, {
4484
+ accountScope: scope,
4485
+ now
4486
+ });
4487
+ } catch {
4488
+ state.phase = "uncertain";
4489
+ writeMailJson(path, state);
4490
+ return report(state);
4491
+ }
4492
+ let alreadySent = false;
4493
+ if (outcome.status === "expired") state.phase = "expired";
4494
+ else try {
4495
+ const result = outcome.result, sent = result.data?.data;
4496
+ if (sent && !validSendStatus(sent.status)) throw invalid();
4497
+ const sentId = sent ? mailId(sent.id) : null;
4498
+ const classification = result.error ? classifySendError(result.response?.status, extractErrorPayload(result.error)) : sent ? successfulSendOutcome(sent) : "uncertain";
4499
+ alreadySent = classification === "already_sent";
4500
+ state.phase = classification === "not_sent" ? "not_sent" : classification === "uncertain" ? "uncertain" : "sent";
4501
+ state.sentId = sentId;
4502
+ } catch {
4503
+ state.phase = "uncertain";
4504
+ state.sentId = null;
4505
+ }
4506
+ writeMailJson(path, state);
4507
+ if (state.sentId) recordSentSignal(context.configDir, state.sentId);
4508
+ return report(state, alreadySent);
4509
+ } finally {
4510
+ release();
4511
+ }
4512
+ }
4513
+ //#endregion
4514
+ export { extractErrorCode as $, reserveSharedMailSubscription as A, queryUsesAutomated as At, sentInThread as B, thrownErrorExitCode as C, AutomatedFilterUnsupportedError as Ct, ensureSharedMailSubscription as D, expectedAutomatedVerdicts as Dt, sharedMailScope as E, ensureSearchReportsAutomated as Et, describeWake as F, resolveCliApiRequestConfig as Ft, EventConnection as G, serverRelationship as H, displayAddress as I, pushFile as J, EventReceiverError as K, formatWakeContext as L, readConversationFollow as M, createAuthenticatedCliApiClient as Mt, otherParticipants as N, createCliApiClient as Nt, createSharedMailWaiter as O, hasAutomatedVerdict as Ot, bareAddress as P, refreshStoredCliCredentials as Pt, createOperationCommand as Q, latestOwnOutbound as R, successfulSendOutcome as S, AUTOMATED_VALUES as St, openSharedMailReceiver as T, automatedRejectedError as Tt, wakeRelationship as U, serverMuted as V, readSharedMailOwner as W, API_ERROR_CODES as X, API_BASE_URL_FLAG_DESCRIPTION as Y, TIME_FLAG_DESCRIPTION as Z, reportSendCommandResult as _, queryUsesAwaiting as _t, assertValidIdempotencyKey as a, writeErrorWithHints as at, sentHistoryWindowStart as b, AUTOMATED_FILTER_UNSUPPORTED_CODE as bt, checkPriorReplies as c, REPLY_STATE_UNSUPPORTED_CODE as ct, formatDeletedEarlierSendNotice as d, awaitingRejectedError as dt, extractErrorPayload as et, formatPriorRepliesCheckSkipped as f, ensureSearchReportsReplyState as ft, priorRepliesThatWentOut as g, isAwaitingRejectedError as gt, formatSendRecordFailureSummary as h, hasReplyState as ht, apiResultHttpStatus as i, surfaceUnauthorizedHint as it, followEmailConversation as j, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as jt, openSharedMailStore as k, isAutomatedRejectedError as kt, classifySendError as l, ReplyStateUnsupportedError as lt, formatSendFailureSummary as m, formatRepliesCell as mt, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as n, readTextFileFlag as nt, buildFollowUpCommand as o, AWAITING_FLAG_DESCRIPTION as ot, formatPriorRepliesWarning as p, formatAwaitingCell as pt, pullFile as q, SEND_OUTCOME_HELP as r, runWithTiming as rt, buildThrownSendFailureEnvelope as s, AWAITING_VALUES as st, sendSignal as t, formatErrorPayload as tt, deriveSendIdempotencyKey as u, assertAwaitingFilterRows as ut, sendIdentityFields as v, formatAlreadySentNotice as vt, reconcileChatSend as w, assertAutomatedVerdict as wt, serializeErrorPayload as x, AUTOMATED_FLAG_DESCRIPTION as xt, sendOutcomeExitCode as y, writeIdempotentReplayBannerIfReplay as yt, readThreadContext as z };