primcli 1.36.0 → 1.37.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,702 @@
1
+ import { _ as isTrustedSender, c as classifySignalContent } from "./repeats-CQmyvUGQ-CMnOmWgL.js";
2
+ import { d as readMailJson, h as writeMailJson, i as mailId, l as privateMailDirectory, r as mailAddress } from "./shared-mail-files-CZqI1kj6.js";
3
+ import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { randomUUID } from "node:crypto";
5
+ import { join } from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ //#region src/oclif/presence-provenance.ts
9
+ function readPresenceProjection(detail) {
10
+ if (!detail || typeof detail !== "object") return null;
11
+ const value = detail.presence_control;
12
+ if (value === void 0 || value === null) return null;
13
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {
14
+ status: "pending",
15
+ valid_for_ms: 0
16
+ };
17
+ const row = value;
18
+ if (![
19
+ "verified",
20
+ "pending",
21
+ "rejected"
22
+ ].includes(String(row.status)) || typeof row.valid_for_ms !== "number" || !Number.isSafeInteger(row.valid_for_ms) || row.valid_for_ms < 0 || row.valid_for_ms > 6e5) return {
23
+ status: "pending",
24
+ valid_for_ms: 0
25
+ };
26
+ return row;
27
+ }
28
+ /** Only the authenticated detail projection identifies control mail. */
29
+ function presenceDisposition(detail) {
30
+ const proof = readPresenceProjection(detail);
31
+ return !proof || proof.status === "rejected" ? "ordinary" : proof.status === "verified" ? "quiet" : "pending";
32
+ }
33
+ //#endregion
34
+ //#region src/oclif/scoped-chat.ts
35
+ function isConnectedChatCredential(apiKey) {
36
+ return apiKey?.startsWith("pconn_") ?? false;
37
+ }
38
+ async function readBeforeDeadline(deadline, read) {
39
+ if (deadline == null) return read(void 0);
40
+ if (Date.now() >= deadline) return null;
41
+ const controller = new AbortController();
42
+ let timer;
43
+ const arm = () => {
44
+ const remaining = Math.max(0, deadline - Date.now());
45
+ const maximumDelay = 2 ** 31 - 1;
46
+ timer = setTimeout(remaining > maximumDelay ? arm : () => controller.abort(), Math.min(remaining, maximumDelay));
47
+ };
48
+ arm();
49
+ try {
50
+ const result = await read(controller.signal);
51
+ return controller.signal.aborted || Date.now() >= deadline ? null : result;
52
+ } catch (error) {
53
+ if (controller.signal.aborted) return null;
54
+ throw error;
55
+ } finally {
56
+ clearTimeout(timer);
57
+ }
58
+ }
59
+ function address(value) {
60
+ return value.trim().toLowerCase();
61
+ }
62
+ function isScopedChatReply(detail, params) {
63
+ if (presenceDisposition(detail) !== "ordinary") return false;
64
+ if (!["accepted", "completed"].includes(detail.status) || params.sentId !== void 0 && detail.reply_to_sent_email_id !== params.sentId || address(detail.recipient) !== address(params.from) || address(detail.to_email) !== address(params.from)) return false;
65
+ return scopedChatSenderTrust(detail, params.recipient).trusted;
66
+ }
67
+ function scopedChatSenderTrust(detail, peer) {
68
+ const sender = address(peer);
69
+ return isTrustedSender({ email: {
70
+ auth: detail.auth,
71
+ headers: { from: detail.from_header }
72
+ } }, {
73
+ sender,
74
+ domain: sender.slice(sender.lastIndexOf("@") + 1)
75
+ });
76
+ }
77
+ function isPlainChatReply(detail) {
78
+ if (presenceDisposition(detail) !== "ordinary") return false;
79
+ if (detail.parsed?.status !== "complete" || !Array.isArray(detail.parsed.attachments)) return false;
80
+ return classifySignalContent({
81
+ inventory: {
82
+ status: "complete",
83
+ parts: detail.parsed.attachments.map((part) => ({
84
+ filename: part.filename ?? null,
85
+ contentType: part.content_type ?? null
86
+ }))
87
+ },
88
+ bodies: {
89
+ status: "complete",
90
+ text: detail.body_text ?? null,
91
+ html: detail.body_html ?? null
92
+ },
93
+ canonicalPartBytes: null
94
+ }).classification === "plain";
95
+ }
96
+ //#endregion
97
+ //#region src/oclif/auto-signals.ts
98
+ /**
99
+ * Automatic communication signals. When mail from the verified owner or a
100
+ * verified same-organization peer is surfaced to an agent session, the CLI
101
+ * reports `read` once; when the session opens it with `emails get --brief`, the
102
+ * CLI reports `working` and renews it until the agent answers. Every step is
103
+ * best effort: nothing here may block, delay or fail the wake or the read.
104
+ */
105
+ const AUTO_SIGNALS_OPT_OUT_ENV = "PRIMITIVE_NO_AUTO_SIGNALS";
106
+ const AUTO_SIGNAL_WORKER_ENV = "PRIMITIVE_AUTO_SIGNAL_WORKER";
107
+ const AUTO_SIGNAL_EMAIL_ENV = "PRIMITIVE_AUTO_SIGNAL_EMAIL";
108
+ const AUTO_SIGNAL_KIND_ENV = "PRIMITIVE_AUTO_SIGNAL_KIND";
109
+ /**
110
+ * Receivers accept a working signal only when it expires at most 60 seconds
111
+ * after the server accepted it. A little under 60 tolerates clock skew.
112
+ */
113
+ const AUTO_WORKING_EXPIRES_SECONDS = 55;
114
+ /** Renew well before expiry so the indicator does not flicker off. */
115
+ const AUTO_WORKING_RENEW_MS = 4e4;
116
+ /** Stop renewing after this long even if no answer was seen. */
117
+ const AUTO_WORKING_CAP_MS = 15 * 6e4;
118
+ /**
119
+ * A renewal in flight is never older than this: the signal send and its
120
+ * reconciliation each abort after 5 seconds. Past it the marker is stale.
121
+ */
122
+ const SENDING_STALE_MS = 15e3;
123
+ function autoSignalsDisabled(env = process.env) {
124
+ const value = env[AUTO_SIGNALS_OPT_OUT_ENV]?.trim().toLowerCase();
125
+ return value !== void 0 && value !== "" && value !== "0" && value !== "false" && value !== "no";
126
+ }
127
+ const profilePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/;
128
+ const lower = (value) => typeof value === "string" ? value.trim().toLowerCase() : "";
129
+ /**
130
+ * Whether one admitted email may carry automatic signals. The sender must be
131
+ * authenticated (aligned DMARC) and either be this agent's pinned owner or
132
+ * have been admitted by the organization network policy (an active member or
133
+ * a same-organization agent). The email must be a fully parsed plain email
134
+ * addressed only to this agent, which did not send it. Signals, fyi
135
+ * acknowledgements, interactions, presence checks, contact requests and
136
+ * unverified or external senders never qualify.
137
+ */
138
+ function autoSignalEligible(detail, identity, admission) {
139
+ try {
140
+ if (!admission || admission.kind === "request") return false;
141
+ const agent = lower(identity.agentAddress);
142
+ const owner = lower(identity.ownerAddress);
143
+ const sender = lower(detail.from_email);
144
+ if (!agent || !sender || sender === agent) return false;
145
+ if (admission.source !== "network" && !(owner && sender === owner)) return false;
146
+ if (!scopedChatSenderTrust(detail, sender).trusted) return false;
147
+ if (lower(detail.recipient) !== agent || lower(detail.to_email) !== agent) return false;
148
+ if (!["accepted", "completed"].includes(detail.status)) return false;
149
+ if (!detail.message_id) return false;
150
+ const parsed = detail.parsed;
151
+ const to = parsed?.to_addresses;
152
+ if (!Array.isArray(to) || to.length !== 1 || lower(to[0]?.address) !== agent || (parsed.cc?.length ?? 0) > 0 || (parsed.bcc?.length ?? 0) > 0) return false;
153
+ return isPlainChatReply(detail);
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+ function sentSignalPath(configDir, sentId) {
159
+ return join(configDir, "signals", "sent-ids", mailId(sentId));
160
+ }
161
+ /**
162
+ * Remember a signal this profile sent. An automated responder that answers
163
+ * a signal with plain mail must not trigger another automatic signal, or the
164
+ * two would acknowledge each other forever. Never throws.
165
+ */
166
+ function recordSentSignal(configDir, sentId) {
167
+ try {
168
+ const path = sentSignalPath(configDir, sentId);
169
+ privateMailDirectory(join(configDir, "signals", "sent-ids"), true);
170
+ writeFileSync(path, "", {
171
+ flag: "w",
172
+ mode: 384
173
+ });
174
+ } catch {}
175
+ }
176
+ function isSentSignal(configDir, sentId) {
177
+ try {
178
+ return Boolean(sentId) && existsSync(sentSignalPath(configDir, sentId ?? ""));
179
+ } catch {
180
+ return false;
181
+ }
182
+ }
183
+ function root(configDir) {
184
+ return join(configDir, "auto-signals");
185
+ }
186
+ function emailDirectory(configDir, emailId) {
187
+ return join(root(configDir), mailId(emailId));
188
+ }
189
+ function parseClaim(value) {
190
+ const row = value;
191
+ if (!row || row.version !== 1 || typeof row.profile !== "string" || !profilePattern.test(row.profile) || row.thread_id !== null && typeof row.thread_id !== "string") return null;
192
+ try {
193
+ return {
194
+ version: 1,
195
+ email_id: mailId(row.email_id),
196
+ profile: row.profile,
197
+ sender: mailAddress(row.sender),
198
+ thread_id: row.thread_id === null ? null : mailId(row.thread_id),
199
+ claimed_at: String(row.claimed_at)
200
+ };
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ /**
206
+ * Record, once per email, that a session surfaced it and it qualified for
207
+ * automatic signals. Returns true only for the first caller, so repeated wakes,
208
+ * redeliveries and concurrent receivers dispatch at most one `read`.
209
+ */
210
+ function claimAutoRead(configDir, claim, now = Date.now) {
211
+ if (!profilePattern.test(claim.profileName)) return false;
212
+ const directory = emailDirectory(configDir, claim.emailId);
213
+ privateMailDirectory(directory, true);
214
+ const value = {
215
+ version: 1,
216
+ email_id: mailId(claim.emailId),
217
+ profile: claim.profileName,
218
+ sender: mailAddress(claim.sender),
219
+ thread_id: claim.threadId ? mailId(claim.threadId) : null,
220
+ claimed_at: new Date(now()).toISOString()
221
+ };
222
+ try {
223
+ writeFileSync(join(directory, "read.json"), `${JSON.stringify(value)}\n`, {
224
+ flag: "wx",
225
+ mode: 384
226
+ });
227
+ return true;
228
+ } catch (error) {
229
+ if (error.code === "EEXIST") return false;
230
+ throw error;
231
+ }
232
+ }
233
+ function readAutoClaim(configDir, emailId) {
234
+ try {
235
+ const claim = parseClaim(readMailJson(join(emailDirectory(configDir, emailId), "read.json")));
236
+ return claim && claim.email_id === mailId(emailId) ? claim : null;
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+ function leasePath(configDir, emailId) {
242
+ return join(emailDirectory(configDir, emailId), "working.json");
243
+ }
244
+ /**
245
+ * The stop lives in its own exclusively created file, so a renewer that
246
+ * rewrites working.json with new signal IDs can never undo it.
247
+ */
248
+ function stopPath(configDir, emailId) {
249
+ return join(emailDirectory(configDir, emailId), "stopped.json");
250
+ }
251
+ /**
252
+ * Index of active leases, so the startup backstop and peer matching read only
253
+ * live leases instead of every email that was ever acknowledged.
254
+ */
255
+ function activeIndex(configDir) {
256
+ return join(root(configDir), "active");
257
+ }
258
+ function markActive(configDir, emailId) {
259
+ privateMailDirectory(activeIndex(configDir), true);
260
+ writeFileSync(join(activeIndex(configDir), mailId(emailId)), "", { mode: 384 });
261
+ }
262
+ function unmarkActive(configDir, emailId) {
263
+ try {
264
+ unlinkSync(join(activeIndex(configDir), mailId(emailId)));
265
+ } catch {}
266
+ }
267
+ function readStop(configDir, emailId) {
268
+ try {
269
+ const row = readMailJson(stopPath(configDir, emailId), 4096);
270
+ if (!row || typeof row.at !== "number") return null;
271
+ return {
272
+ at: row.at,
273
+ reason: typeof row.reason === "string" ? row.reason : "stopped",
274
+ token: typeof row.token === "string" ? row.token : ""
275
+ };
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+ function readWorkingLease(configDir, emailId) {
281
+ try {
282
+ const row = readMailJson(leasePath(configDir, emailId), 64 * 1024);
283
+ if (!row || row.version !== 1 || typeof row.profile !== "string" || !profilePattern.test(row.profile) || typeof row.started_at !== "number" || !Number.isFinite(row.started_at) || row.stopped_at !== null && typeof row.stopped_at !== "number" || !Array.isArray(row.signal_sent_ids)) return null;
284
+ const stop = readStop(configDir, emailId);
285
+ return {
286
+ version: 1,
287
+ email_id: mailId(row.email_id),
288
+ profile: row.profile,
289
+ sender: mailAddress(row.sender),
290
+ thread_id: row.thread_id ? mailId(row.thread_id) : null,
291
+ started_at: row.started_at,
292
+ stopped_at: stop?.at ?? row.stopped_at,
293
+ stop_reason: stop?.reason ?? (typeof row.stop_reason === "string" ? row.stop_reason : null),
294
+ signal_sent_ids: row.signal_sent_ids.slice(-200).map(mailId)
295
+ };
296
+ } catch {
297
+ return null;
298
+ }
299
+ }
300
+ function writeWorkingLease(configDir, lease) {
301
+ writeMailJson(leasePath(configDir, lease.email_id), {
302
+ ...lease,
303
+ stopped_at: null,
304
+ stop_reason: null
305
+ });
306
+ }
307
+ /**
308
+ * Start the working lease for an email that already qualified for automatic
309
+ * signals. Only the first caller succeeds, so working is started once per
310
+ * email however many times the session reads it.
311
+ */
312
+ function startWorkingLease(configDir, emailId, now = Date.now) {
313
+ const claim = readAutoClaim(configDir, emailId);
314
+ if (!claim) return null;
315
+ const lease = {
316
+ version: 1,
317
+ email_id: claim.email_id,
318
+ profile: claim.profile,
319
+ sender: claim.sender,
320
+ thread_id: claim.thread_id,
321
+ started_at: now(),
322
+ stopped_at: null,
323
+ stop_reason: null,
324
+ signal_sent_ids: []
325
+ };
326
+ try {
327
+ writeFileSync(leasePath(configDir, emailId), `${JSON.stringify(lease)}\n`, {
328
+ flag: "wx",
329
+ mode: 384
330
+ });
331
+ } catch {
332
+ return null;
333
+ }
334
+ try {
335
+ markActive(configDir, emailId);
336
+ } catch {}
337
+ return lease;
338
+ }
339
+ /**
340
+ * Answers that rely on a stop someone else created. While any exist, the
341
+ * creator of that stop may not undo it, even if its own answer was refused.
342
+ */
343
+ function holdsDirectory(configDir, emailId) {
344
+ return join(emailDirectory(configDir, emailId), "stop-holds");
345
+ }
346
+ function stopHeld(configDir, emailId) {
347
+ try {
348
+ return readdirSync(holdsDirectory(configDir, emailId)).length > 0;
349
+ } catch {
350
+ return false;
351
+ }
352
+ }
353
+ /** Stop a lease, or hold an existing stop. Null when there is no lease. */
354
+ function stopWorkingLease(configDir, emailId, reason, now = Date.now) {
355
+ if (!readWorkingLease(configDir, emailId)) return null;
356
+ const token = randomUUID();
357
+ const hold = join(holdsDirectory(configDir, emailId), token);
358
+ privateMailDirectory(holdsDirectory(configDir, emailId), true);
359
+ writeFileSync(hold, "", { mode: 384 });
360
+ try {
361
+ writeFileSync(stopPath(configDir, emailId), `${JSON.stringify({
362
+ at: now(),
363
+ reason,
364
+ token
365
+ })}\n`, {
366
+ flag: "wx",
367
+ mode: 384
368
+ });
369
+ } catch (error) {
370
+ if (error.code === "EEXIST") return {
371
+ token,
372
+ owner: false
373
+ };
374
+ throw error;
375
+ }
376
+ try {
377
+ unlinkSync(hold);
378
+ } catch {}
379
+ return {
380
+ token,
381
+ owner: true
382
+ };
383
+ }
384
+ function pendingPath(configDir, emailId) {
385
+ return join(emailDirectory(configDir, emailId), "restore-pending.json");
386
+ }
387
+ /**
388
+ * Undo a stop this process made, for an answer known not to have been sent.
389
+ * A stop made by anyone else (cap, answered, another reply) is left alone.
390
+ * While a later answer holds the stop it stays in place, and the restore is
391
+ * left pending: the last holder to release finishes it if its answer was
392
+ * refused too (see releaseStopHold).
393
+ */
394
+ function restoreWorkingLease(configDir, emailId, token) {
395
+ try {
396
+ const stop = readStop(configDir, emailId);
397
+ if (!stop || stop.token !== token) return false;
398
+ writeFileSync(pendingPath(configDir, emailId), JSON.stringify({ token }), { mode: 384 });
399
+ if (stopHeld(configDir, emailId)) return false;
400
+ markActive(configDir, emailId);
401
+ unlinkSync(stopPath(configDir, emailId));
402
+ if (stopHeld(configDir, emailId)) {
403
+ try {
404
+ writeFileSync(stopPath(configDir, emailId), `${JSON.stringify(stop)}\n`, {
405
+ flag: "wx",
406
+ mode: 384
407
+ });
408
+ } catch {}
409
+ return false;
410
+ }
411
+ try {
412
+ unlinkSync(pendingPath(configDir, emailId));
413
+ } catch {}
414
+ return true;
415
+ } catch {
416
+ return false;
417
+ }
418
+ }
419
+ /**
420
+ * Release a hold for an answer known not to have been sent. If the stop's
421
+ * owner was refused too and is waiting on holds, the last release restores
422
+ * the lease. Returns true when it restored it.
423
+ */
424
+ function releaseStopHold(configDir, emailId, token) {
425
+ try {
426
+ try {
427
+ unlinkSync(join(holdsDirectory(configDir, emailId), token));
428
+ } catch {
429
+ return false;
430
+ }
431
+ if (stopHeld(configDir, emailId)) return false;
432
+ let pending = null;
433
+ try {
434
+ const row = JSON.parse(readFileSync(pendingPath(configDir, emailId), "utf8"));
435
+ pending = typeof row.token === "string" ? row.token : null;
436
+ } catch {
437
+ return false;
438
+ }
439
+ return pending !== null && restoreWorkingLease(configDir, emailId, pending);
440
+ } catch {
441
+ return false;
442
+ }
443
+ }
444
+ function sendingPath(configDir, emailId) {
445
+ return join(emailDirectory(configDir, emailId), "sending");
446
+ }
447
+ /** Mark a renewal in flight so a reply can wait for it to settle. */
448
+ function markWorkingSending(configDir, emailId) {
449
+ writeFileSync(sendingPath(configDir, emailId), JSON.stringify({
450
+ pid: process.pid,
451
+ at: Date.now()
452
+ }), { mode: 384 });
453
+ }
454
+ function clearWorkingSending(configDir, emailId) {
455
+ try {
456
+ unlinkSync(sendingPath(configDir, emailId));
457
+ } catch {}
458
+ }
459
+ function sendingInFlight(configDir, emailId) {
460
+ try {
461
+ const value = JSON.parse(readFileSync(sendingPath(configDir, emailId), "utf8"));
462
+ if (typeof value.at !== "number" || Date.now() - value.at > SENDING_STALE_MS) return false;
463
+ if (typeof value.pid === "number") process.kill(value.pid, 0);
464
+ return true;
465
+ } catch {
466
+ return false;
467
+ }
468
+ }
469
+ /**
470
+ * A stopped lease stays indexed this long, so a later answer to the same
471
+ * sender can still find it and hold its stop against a refused earlier one.
472
+ */
473
+ const STOPPED_INDEX_MS = 10 * 6e4;
474
+ function indexedLeases(configDir, now) {
475
+ let entries;
476
+ try {
477
+ entries = readdirSync(activeIndex(configDir));
478
+ } catch {
479
+ return [];
480
+ }
481
+ return entries.flatMap((entry) => {
482
+ if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(entry)) return [];
483
+ const lease = readWorkingLease(configDir, entry);
484
+ if (!lease || lease.stopped_at !== null && now() - lease.stopped_at >= STOPPED_INDEX_MS || now() - lease.started_at >= 9e5 * 2) {
485
+ unmarkActive(configDir, entry);
486
+ return [];
487
+ }
488
+ return [lease];
489
+ });
490
+ }
491
+ /**
492
+ * Active, unstopped leases in this config directory, read from the lease
493
+ * index rather than every email ever acknowledged. Old entries are pruned.
494
+ */
495
+ function activeWorkingLeases(configDir, now = Date.now) {
496
+ return indexedLeases(configDir, now).filter((lease) => lease.stopped_at === null);
497
+ }
498
+ /**
499
+ * Stop automatic working for the emails an outgoing message answers, before
500
+ * that message is sent, then wait for any renewal already in flight to land
501
+ * first. A renewal that reached receivers after the answer would show the
502
+ * agent as working again. The wait ends when the renewal settles, which is
503
+ * normally immediate and is bounded by the send timeouts. Never throws.
504
+ */
505
+ async function haltAutoWorking(configDir, match, reason, options = {}) {
506
+ const halted = {
507
+ configDir,
508
+ stops: []
509
+ };
510
+ try {
511
+ const ids = /* @__PURE__ */ new Set();
512
+ for (const id of match.emailIds ?? []) try {
513
+ ids.add(mailId(id));
514
+ } catch {}
515
+ const peers = new Set((match.peers ?? []).map(lower).filter(Boolean));
516
+ const candidates = /* @__PURE__ */ new Map();
517
+ for (const lease of indexedLeases(configDir, Date.now)) candidates.set(lease.email_id, lease);
518
+ for (const id of ids) {
519
+ const lease = readWorkingLease(configDir, id);
520
+ if (lease) candidates.set(id, lease);
521
+ }
522
+ const stopped = [];
523
+ for (const lease of candidates.values()) {
524
+ const byPeer = peers.has(lease.sender) && match.profileName !== void 0 && match.profileName === lease.profile;
525
+ if (!ids.has(lease.email_id) && !byPeer) continue;
526
+ try {
527
+ const stop = stopWorkingLease(configDir, lease.email_id, reason);
528
+ if (stop) halted.stops.push({
529
+ emailId: lease.email_id,
530
+ ...stop
531
+ });
532
+ stopped.push(lease.email_id);
533
+ } catch {}
534
+ }
535
+ const deadline = Date.now() + (options.settleMs ?? SENDING_STALE_MS);
536
+ while (stopped.some((id) => sendingInFlight(configDir, id)) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50));
537
+ } catch {}
538
+ return halted;
539
+ }
540
+ /**
541
+ * The answer was definitively refused, so nothing reached the sender: put
542
+ * back the working indication this answer stopped. Never throws.
543
+ */
544
+ function restoreAutoWorking(halted, options = {}) {
545
+ try {
546
+ let restored = 0;
547
+ for (const stop of halted.stops) {
548
+ if (!(stop.owner ? restoreWorkingLease(halted.configDir, stop.emailId, stop.token) : releaseStopHold(halted.configDir, stop.emailId, stop.token))) continue;
549
+ restored++;
550
+ const lease = readWorkingLease(halted.configDir, stop.emailId);
551
+ if (lease) spawnAutoSignalWorker({
552
+ configDir: halted.configDir,
553
+ emailId: lease.email_id,
554
+ kind: "working",
555
+ profileName: lease.profile,
556
+ env: options.env ?? process.env,
557
+ spawnImpl: options.spawnImpl
558
+ });
559
+ }
560
+ return restored;
561
+ } catch {
562
+ return 0;
563
+ }
564
+ }
565
+ /** The CLI entrypoint that runs a detached signal worker. */
566
+ function autoSignalEntrypoint() {
567
+ const candidates = ["../../bin/run.js", "../bin/run.js"].map((path) => fileURLToPath(new URL(path, import.meta.url)));
568
+ return candidates.find((path) => existsSync(path)) ?? candidates[0] ?? "";
569
+ }
570
+ /**
571
+ * Run one automatic signal in a detached process with no inherited stdio, so
572
+ * the calling command's output, exit status and timing are unchanged.
573
+ */
574
+ function spawnAutoSignalWorker(options) {
575
+ try {
576
+ const env = { ...options.env ?? process.env };
577
+ if (autoSignalsDisabled(env)) return false;
578
+ if (!profilePattern.test(options.profileName)) return false;
579
+ const entry = options.entry ?? autoSignalEntrypoint();
580
+ if (!options.spawnImpl && !existsSync(entry)) return false;
581
+ delete env.PRIMITIVE_API_KEY;
582
+ delete env.PRIMITIVE_KEY;
583
+ const child = (options.spawnImpl ?? spawn)(process.execPath, [entry], {
584
+ detached: true,
585
+ stdio: "ignore",
586
+ windowsHide: true,
587
+ env: {
588
+ ...env,
589
+ PRIMITIVE_CONFIG_DIR: options.configDir,
590
+ PRIMITIVE_AGENT_PROFILE: options.profileName,
591
+ PRIMITIVE_SKIP_NEW_VERSION_CHECK: "true",
592
+ [AUTO_SIGNAL_WORKER_ENV]: "1",
593
+ [AUTO_SIGNAL_EMAIL_ENV]: mailId(options.emailId),
594
+ [AUTO_SIGNAL_KIND_ENV]: options.kind
595
+ }
596
+ });
597
+ child.on("error", () => void 0);
598
+ child.unref();
599
+ return true;
600
+ } catch {
601
+ return false;
602
+ }
603
+ }
604
+ /**
605
+ * Called when a receiver surfaces mail to a session: claim the email once and
606
+ * send `read` in the background. Never throws and never writes output.
607
+ */
608
+ function dispatchAutoRead(options) {
609
+ try {
610
+ const env = options.env ?? process.env;
611
+ if (autoSignalsDisabled(env) || !options.profileName) return false;
612
+ if (isSentSignal(options.configDir, options.replyToSentEmailId)) return false;
613
+ if (!claimAutoRead(options.configDir, {
614
+ emailId: options.emailId,
615
+ profileName: options.profileName,
616
+ sender: options.sender,
617
+ threadId: options.threadId
618
+ })) return false;
619
+ return spawnAutoSignalWorker({
620
+ configDir: options.configDir,
621
+ emailId: options.emailId,
622
+ kind: "read",
623
+ profileName: options.profileName,
624
+ env,
625
+ spawnImpl: options.spawnImpl
626
+ });
627
+ } catch {
628
+ return false;
629
+ }
630
+ }
631
+ /**
632
+ * Called after the session reads an email with `emails get --brief`: start
633
+ * working once for mail that already qualified for automatic signals.
634
+ */
635
+ function dispatchAutoWorking(options) {
636
+ try {
637
+ const env = options.env ?? process.env;
638
+ if (autoSignalsDisabled(env)) return false;
639
+ const lease = startWorkingLease(options.configDir, options.emailId);
640
+ if (!lease) return false;
641
+ return spawnAutoSignalWorker({
642
+ configDir: options.configDir,
643
+ emailId: lease.email_id,
644
+ kind: "working",
645
+ profileName: lease.profile,
646
+ env,
647
+ spawnImpl: options.spawnImpl
648
+ });
649
+ } catch {
650
+ return false;
651
+ }
652
+ }
653
+ function renewerPath(configDir, emailId) {
654
+ return join(emailDirectory(configDir, emailId), "renewer.json");
655
+ }
656
+ function recordRenewer(configDir, emailId) {
657
+ writeFileSync(renewerPath(configDir, emailId), JSON.stringify({
658
+ pid: process.pid,
659
+ at: Date.now()
660
+ }), { mode: 384 });
661
+ }
662
+ function renewerAlive(configDir, emailId) {
663
+ try {
664
+ const value = JSON.parse(readFileSync(renewerPath(configDir, emailId), "utf8"));
665
+ if (typeof value.pid !== "number" || typeof value.at !== "number") return false;
666
+ if (Date.now() - value.at > 4e4 * 2) return false;
667
+ process.kill(value.pid, 0);
668
+ return true;
669
+ } catch {
670
+ return false;
671
+ }
672
+ }
673
+ /**
674
+ * Backstop for runtimes that end background processes between tool calls:
675
+ * any CLI invocation restarts a renewer for an active lease whose renewer is
676
+ * gone. Cheap when there are no leases. Never throws.
677
+ */
678
+ function resumeAutoWorking(configDir, options = {}) {
679
+ try {
680
+ const env = options.env ?? process.env;
681
+ if (autoSignalsDisabled(env) || env["PRIMITIVE_AUTO_SIGNAL_WORKER"] === "1") return 0;
682
+ const now = (options.now ?? Date.now)();
683
+ let started = 0;
684
+ for (const lease of activeWorkingLeases(configDir, () => now)) {
685
+ if (now - lease.started_at >= 9e5) continue;
686
+ if (renewerAlive(configDir, lease.email_id)) continue;
687
+ if (spawnAutoSignalWorker({
688
+ configDir,
689
+ emailId: lease.email_id,
690
+ kind: "working",
691
+ profileName: lease.profile,
692
+ env,
693
+ spawnImpl: options.spawnImpl
694
+ })) started++;
695
+ }
696
+ return started;
697
+ } catch {
698
+ return 0;
699
+ }
700
+ }
701
+ //#endregion
702
+ export { stopWorkingLease as A, releaseStopHold as C, resumeAutoWorking as D, restoreWorkingLease as E, readBeforeDeadline as F, scopedChatSenderTrust as I, presenceDisposition as L, isConnectedChatCredential as M, isPlainChatReply as N, spawnAutoSignalWorker as O, isScopedChatReply as P, readPresenceProjection as R, recordSentSignal as S, restoreAutoWorking as T, isSentSignal as _, AUTO_WORKING_CAP_MS as a, readWorkingLease as b, activeWorkingLeases as c, autoSignalsDisabled as d, claimAutoRead as f, haltAutoWorking as g, dispatchAutoWorking as h, AUTO_SIGNAL_WORKER_ENV as i, writeWorkingLease as j, startWorkingLease as k, autoSignalEligible as l, dispatchAutoRead as m, AUTO_SIGNAL_EMAIL_ENV as n, AUTO_WORKING_EXPIRES_SECONDS as o, clearWorkingSending as p, AUTO_SIGNAL_KIND_ENV as r, AUTO_WORKING_RENEW_MS as s, AUTO_SIGNALS_OPT_OUT_ENV as t, autoSignalEntrypoint as u, markWorkingSending as v, renewerAlive as w, recordRenewer as x, readAutoClaim as y };