primcli 1.75.0 → 3.0.0-preview.1

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.
@@ -391,6 +391,7 @@ var LegacyApiKeyCredentialFormatError = class extends Error {
391
391
  };
392
392
  function parseCredentials(raw) {
393
393
  if (!isRecord$1(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
394
+ if (raw.auth_method === "user_grant") return parseUserGrantCredentials(raw);
394
395
  if (raw.auth_method !== "oauth") {
395
396
  if (typeof raw.api_key === "string" || typeof raw.key_id === "string" || typeof raw.base_url === "string") throw new LegacyApiKeyCredentialFormatError();
396
397
  throw new Error(`Stored Primitive CLI credentials are malformed: auth_method must be oauth. ${MALFORMED_CREDENTIALS_HINT}`);
@@ -412,6 +413,63 @@ function parseCredentials(raw) {
412
413
  created_at: requireString(raw, "created_at")
413
414
  };
414
415
  }
416
+ function malformed(detail) {
417
+ return /* @__PURE__ */ new Error(`Stored Primitive CLI credentials are malformed: ${detail}. ${MALFORMED_CREDENTIALS_HINT}`);
418
+ }
419
+ function requireText(value, key, options = {}) {
420
+ const raw = value[key];
421
+ if (typeof raw !== "string" || !options.allowEmpty && raw.trim().length === 0) throw malformed(`${key} must be a non-empty string`);
422
+ return raw;
423
+ }
424
+ function nullableText(value, key) {
425
+ const raw = value[key];
426
+ if (raw === void 0 || raw === null) return null;
427
+ if (typeof raw !== "string") throw malformed(`${key} must be a string or null`);
428
+ return raw;
429
+ }
430
+ function parseOrgToken(raw) {
431
+ if (!isRecord$1(raw)) throw malformed("user_grant.org_tokens entries must be objects");
432
+ return {
433
+ access_token: requireText(raw, "access_token"),
434
+ expires_at: requireText(raw, "expires_at"),
435
+ oauth_grant_id: requireText(raw, "oauth_grant_id"),
436
+ org_id: requireText(raw, "org_id"),
437
+ org_name: nullableText(raw, "org_name"),
438
+ role: nullableText(raw, "role")
439
+ };
440
+ }
441
+ function parseUserGrantCredentials(raw) {
442
+ const grantRaw = raw.user_grant;
443
+ if (!isRecord$1(grantRaw)) throw malformed("user_grant must be an object");
444
+ if (raw.token_type !== "Bearer") throw malformed("token_type must be Bearer");
445
+ const tokensRaw = grantRaw.org_tokens ?? {};
446
+ if (!isRecord$1(tokensRaw)) throw malformed("user_grant.org_tokens must be an object");
447
+ const orgTokens = {};
448
+ for (const [orgId, token] of Object.entries(tokensRaw)) orgTokens[orgId] = parseOrgToken(token);
449
+ const orgId = requireText(raw, "org_id", { allowEmpty: true });
450
+ return {
451
+ access_token: requireText(raw, "access_token", { allowEmpty: true }),
452
+ api_base_url: readStoredApiBaseUrl(raw),
453
+ auth_method: "user_grant",
454
+ created_at: requireText(raw, "created_at"),
455
+ expires_at: requireText(raw, "expires_at"),
456
+ oauth_client_id: requireText(raw, "oauth_client_id"),
457
+ oauth_grant_id: requireText(raw, "oauth_grant_id", { allowEmpty: true }),
458
+ org_id: orgId,
459
+ org_name: nullableText(raw, "org_name"),
460
+ refresh_token: requireText(raw, "refresh_token"),
461
+ token_type: "Bearer",
462
+ user_grant: {
463
+ access_token: requireText(grantRaw, "access_token"),
464
+ expires_at: requireText(grantRaw, "expires_at"),
465
+ org_role: nullableText(grantRaw, "org_role"),
466
+ org_tokens: orgTokens,
467
+ requested_grant: nullableText(grantRaw, "requested_grant"),
468
+ user_grant_id: requireText(grantRaw, "user_grant_id"),
469
+ user_id: requireText(grantRaw, "user_id")
470
+ }
471
+ };
472
+ }
415
473
  function credentialsPath(configDir) {
416
474
  return join(configDir, CREDENTIALS_FILE);
417
475
  }
@@ -674,21 +732,6 @@ function detectPrimitiveKeyEnvMisname(env = process.env) {
674
732
  if ((primitiveKey?.length ?? 0) > 0 && (primitiveApiKey?.length ?? 0) === 0) return "PRIMITIVE_KEY is set but the CLI reads PRIMITIVE_API_KEY. Rename your env var, or re-run with PRIMITIVE_API_KEY=$PRIMITIVE_KEY.";
675
733
  return null;
676
734
  }
677
- /**
678
- * True when a usable OAuth login session is saved on disk. Used by `pay-email`
679
- * to decide whether it can safely ignore an env-only `PRIMITIVE_API_KEY` in
680
- * favor of the payer's logged-in account (which owns the inbound challenge
681
- * inbox). Returns false when nothing is stored or the stored credential is
682
- * unreadable/legacy, so the caller falls back to whatever key did resolve
683
- * rather than silently dropping the user's only auth.
684
- */
685
- function hasStoredCliLogin(configDir) {
686
- try {
687
- return loadCliCredentials(configDir) !== null;
688
- } catch {
689
- return false;
690
- }
691
- }
692
735
  function resolveCliAuth(params) {
693
736
  const apiKey = params.apiKey?.trim();
694
737
  const apiBaseUrl = normalizeApiBaseUrl(params.apiBaseUrl);
@@ -903,4 +946,4 @@ function redactCliEnvironment(environment) {
903
946
  ...headers ? { headers } : {}
904
947
  };
905
948
  }
906
- export { saveActiveChatState as A, parseConnectedAgentProfile as B, resolveCliAuth as C, deleteChatState as D, chatStatePath as E, agentProfileName as F, PrimitiveApiClient as G, profileAlreadyConnectedMessage as H, agentProfilesDirectory as I, connectedAgentIdentity as L, AGENT_PROFILE_ENV as M, AgentConnectionSetupError as N, loadActiveChatState as O, agentProfileDirectory as P, connectedApiBaseUrl as R, normalizeApiBaseUrl as S, saveSignupCredentials as T, requireDefaultLoginProfile as U, parseOwnerMemberAddress as V, saveConnectedAgentProfile as W, deleteCliCredentials as _, normalizeCliEnvironmentName as a, hasStoredCliLogin as b, resolveConfigEnvironment as c, validateCliHeaderName as d, validateCliHeaderValue as f, credentialsPath as g, credentialsLockPath as h, loadCliConfig as i, AGENT_ADDRESS_ENV as j, loadChatConversationByLocalId as k, saveCliConfig as l, cliAccessTokenExpiresAt as m, deleteCliConfig as n, redactCliEnvironment as o, acquireCliCredentialsLock as p, emptyCliConfig as r, removeCliEnvironment as s, DEFAULT_ENVIRONMENT as t, upsertCliEnvironment as u, deleteCliCredentialsLock as v, saveCliCredentials as w, loadCliCredentials as x, detectPrimitiveKeyEnvMisname as y, loadConnectedAgentProfile as z };
949
+ export { AGENT_ADDRESS_ENV as A, parseOwnerMemberAddress as B, saveCliCredentials as C, loadActiveChatState as D, deleteChatState as E, agentProfilesDirectory as F, requireDefaultLoginProfile as H, connectedAgentIdentity as I, connectedApiBaseUrl as L, AgentConnectionSetupError as M, agentProfileDirectory as N, loadChatConversationByLocalId as O, agentProfileName as P, loadConnectedAgentProfile as R, resolveCliAuth as S, chatStatePath as T, saveConnectedAgentProfile as U, profileAlreadyConnectedMessage as V, PrimitiveApiClient as W, deleteCliCredentials as _, normalizeCliEnvironmentName as a, loadCliCredentials as b, resolveConfigEnvironment as c, validateCliHeaderName as d, validateCliHeaderValue as f, credentialsPath as g, credentialsLockPath as h, loadCliConfig as i, AGENT_PROFILE_ENV as j, saveActiveChatState as k, saveCliConfig as l, cliAccessTokenExpiresAt as m, deleteCliConfig as n, redactCliEnvironment as o, acquireCliCredentialsLock as p, emptyCliConfig as r, removeCliEnvironment as s, DEFAULT_ENVIRONMENT as t, upsertCliEnvironment as u, deleteCliCredentialsLock as v, saveSignupCredentials as w, normalizeApiBaseUrl as x, detectPrimitiveKeyEnvMisname as y, parseConnectedAgentProfile as z };
@@ -1,11 +1,12 @@
1
- import { B as parseConnectedAgentProfile, F as agentProfileName, G as PrimitiveApiClient, H as profileAlreadyConnectedMessage, I as agentProfilesDirectory, L as connectedAgentIdentity, M as AGENT_PROFILE_ENV, N as AgentConnectionSetupError, P as agentProfileDirectory, R as connectedApiBaseUrl, V as parseOwnerMemberAddress, W as saveConnectedAgentProfile, x as loadCliCredentials, z as loadConnectedAgentProfile } from "./cli-config-B80p0pLk.js";
2
- import { $t as listDomains, At as getAgentContactPolicy, Dn as sendEmail, Ft as getEmail, Jt as listAgentContacts, Kn as __exportAll, Kt as inviteAgentConnection, Xt as listContacts, cn as listSentEmails, gn as putContact, hn as putAgentContactPolicy, jt as getContact, mn as putAgentContact, qt as listAgentConnections, vt as deleteAgentContact, wn as searchEmails, yt as deleteContact } from "./repeats-Dj_o6c-X-LyCkKYjp.js";
3
- import { Pt as refreshStoredCliCredentials, gt as defaultConnectionName, mt as cliInvocation, xt as detectAgentRuntime } from "./api-command-rlS3eTlZ.js";
1
+ import { B as parseOwnerMemberAddress, F as agentProfilesDirectory, I as connectedAgentIdentity, L as connectedApiBaseUrl, M as AgentConnectionSetupError, N as agentProfileDirectory, P as agentProfileName, R as loadConnectedAgentProfile, U as saveConnectedAgentProfile, V as profileAlreadyConnectedMessage, W as PrimitiveApiClient, b as loadCliCredentials, j as AGENT_PROFILE_ENV, z as parseConnectedAgentProfile } from "./cli-config-Ms_0vNjq.js";
2
+ import { $n as __exportAll, $t as listContacts, Cn as putContact, It as getEmail, Jt as inviteAgentConnection, Mt as getContact, Pn as sendEmail, Sn as putAgentContactPolicy, Xt as listAgentConnections, Zt as listAgentContacts, hn as listSentEmails, jn as searchEmails, jt as getAgentContactPolicy, rn as listDomains, vt as deleteAgentContact, xn as putAgentContact, yt as deleteContact } from "./repeats-Dj_o6c-X-Bf9G0xds.js";
3
+ import { Ft as refuseOrgSelectionForProfile, Pt as refreshStoredCliCredentials, St as detectAgentRuntime, _t as defaultConnectionName, ht as cliInvocation } from "./api-command-ctHBqTL5.js";
4
4
  import { _ as acquireListenLock, b as listenLockHeld, c as mailTime, d as readMailJson, f as removeMailFile, g as ListenStateError, h as writeMailJson, i as mailId, l as privateMailDirectory, o as mailObject, p as syncMailDirectory, r as mailAddress, s as mailString } from "./shared-mail-files-BAQSGxxz.js";
5
- import { _ as installConnectSkill, a as NotificationRetryError, b as readBundledConnectSkill, d as defaultSessionSocket, f as NativeSessionDisconnectedError, l as SESSION_UUID, t as notificationScope, u as connectNativeSession } from "./notify-session-CbY4FYUt.js";
5
+ import { S as readBundledConnectSkill, a as NotificationRetryError, d as SESSION_UUID, f as connectNativeSession, m as NativeSessionDisconnectedError, p as defaultSessionSocket, t as notificationScope, y as installConnectSkill } from "./notify-session-BN0JTPZ3.js";
6
6
  import { l as stopBackgroundListen, r as backgroundListenStatus } from "./listen-background-C0F9gNDD.js";
7
7
  import { t as readSharedMailOwner } from "./shared-mail-watch-BAPMsFNv.js";
8
- import { o as scopedChatSenderTrust } from "./scoped-chat-B0thCwME.js";
8
+ import { withoutOrgSelection } from "./oclif/global-org-flag.js";
9
+ import { o as scopedChatSenderTrust } from "./scoped-chat-DZ42V3jq.js";
9
10
  import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
10
11
  import { createHash, randomUUID } from "node:crypto";
11
12
  import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
@@ -3009,6 +3010,7 @@ async function revoke(profile, fetcher) {
3009
3010
  /** Revoke one selected connected credential; never alter OAuth login or mail evidence. */
3010
3011
  async function disconnectAgent(params) {
3011
3012
  const profileName = agentProfileName(params.profileName);
3013
+ refuseOrgSelectionForProfile(profileName);
3012
3014
  if (!loadConnectedAgentProfile(params.configDir, profileName)) throw new AgentDisconnectError("This agent profile is not configured. Nothing was changed.");
3013
3015
  const directory = agentProfileDirectory(params.configDir, profileName);
3014
3016
  let releaseSetup;
@@ -3448,6 +3450,11 @@ var ContactPolicyReadRetryError = class extends NotificationRetryError {
3448
3450
  super("Contact notification policy is temporarily unavailable. Mail remains pending while permissions are refreshed.");
3449
3451
  }
3450
3452
  };
3453
+ /** A network decision is trusted only when every field agrees with the others. */
3454
+ function consistentNetworkAdmission(decision) {
3455
+ const relation = decision.sender_relation;
3456
+ return typeof decision.allowed === "boolean" && typeof decision.pending === "boolean" && typeof decision.member_policy_required === "boolean" && (relation === void 0 || decision.member_policy_required && (relation === "owner" || relation === "member")) && !(decision.member_policy_required && decision.allowed && !relation) && !(decision.pending && (decision.allowed || decision.allowed_since !== null)) && (decision.allowed ? typeof decision.allowed_since === "string" : decision.allowed_since === null);
3457
+ }
3451
3458
  const unavailable = () => new ListenStateError("Contact notification preferences are unavailable or invalid. No contact notification was submitted; restart after access is restored.");
3452
3459
  const changed = () => new NotificationRetryError("Contact notification permission changed or expired before dispatch.");
3453
3460
  /** Read one connected address's policy. Partial pages never authorize delivery. */
@@ -3561,10 +3568,8 @@ function createNotificationContactPolicy(options) {
3561
3568
  if (!emailId) throw unavailable();
3562
3569
  const network = await networkReader(emailId, sender, signal);
3563
3570
  signal.throwIfAborted();
3564
- if (typeof network.member_policy_required !== "boolean") throw unavailable();
3565
- if (network.sender_relation !== void 0 && (!network.member_policy_required || !["owner", "member"].includes(network.sender_relation))) throw unavailable();
3571
+ if (!consistentNetworkAdmission(network)) throw unavailable();
3566
3572
  if (network.member_policy_required) {
3567
- if (network.allowed && !network.sender_relation) throw unavailable();
3568
3573
  if (network.pending) throw new NetworkAdmissionPendingError();
3569
3574
  if (!network.allowed) return null;
3570
3575
  if (decision.kind === "silent" && decision.source !== "default") return null;
@@ -4192,11 +4197,11 @@ function defaults$1() {
4192
4197
  "--contacts",
4193
4198
  ...contactRequests ? ["--contact-requests"] : []
4194
4199
  ], {
4195
- env: {
4200
+ env: withoutOrgSelection({
4196
4201
  ...process.env,
4197
4202
  [AGENT_PROFILE_ENV]: profile,
4198
4203
  PRIMITIVE_CONFIG_DIR: configDir
4199
- },
4204
+ }),
4200
4205
  timeout: 35e3,
4201
4206
  maxBuffer: 65536
4202
4207
  });
@@ -5058,11 +5063,18 @@ function assertNativeSessionIdentity(session, env) {
5058
5063
  if (env.CODEX_THREAD_ID && env.CODEX_THREAD_ID !== session) throw refuse("The requested session differs from this Codex thread. No address was created.");
5059
5064
  if (!env.CODEX_THREAD_ID && env.CODEX_SESSION_ID && env.CODEX_SESSION_ID !== session) throw refuse("The requested session differs from this Codex session. No address was created.");
5060
5065
  }
5066
+ /**
5067
+ * Preflight for enrollment: the managed domains an address could land on.
5068
+ * Null when the member's grant may not list domains; creating a connection
5069
+ * needs only agent_connections:manage, and the server picks the address and
5070
+ * refuses with connection_domain_unavailable when none is usable.
5071
+ */
5061
5072
  async function managedDomains(client, apiBaseUrl) {
5062
5073
  const response = await listDomains({
5063
5074
  client: client.client,
5064
5075
  responseStyle: "fields"
5065
5076
  });
5077
+ if (response.response?.status === 403) return null;
5066
5078
  const rows = response.data?.data;
5067
5079
  if (response.error || !Array.isArray(rows)) throw refuse("Could not list the owner's managed domains. No address was created.");
5068
5080
  const suffix = apiBaseUrl === "https://api.primitive-staging-1.com/v1" ? ".primitive-staging.email" : ".primitive.email";
@@ -5579,11 +5591,12 @@ async function enrollAgent(params) {
5579
5591
  if (state?.continueAttempted) throw refuse(stuckEnrollmentStep(resolvedEnrollment(state).address));
5580
5592
  if (!state) {
5581
5593
  if (params.continueSetup) throw refuse("There is no saved enrollment to continue. Start enrollment normally first.");
5582
- if (!(await managedDomains(new PrimitiveApiClient({
5594
+ const domains = await managedDomains(new PrimitiveApiClient({
5583
5595
  apiKey: credentials.access_token,
5584
5596
  apiBaseUrl,
5585
5597
  fetch: params.fetch
5586
- }), apiBaseUrl)).length) throw refuse("No managed domain is available for enrollment.");
5598
+ }), apiBaseUrl);
5599
+ if (domains && !domains.length) throw refuse("No managed domain is available for enrollment.");
5587
5600
  state = {
5588
5601
  version: 2,
5589
5602
  session: params.session,
@@ -1,13 +1,17 @@
1
- import { Et as downloadEmailAttachmentPart, J as parseWebhookEvent, K as isEmailReceivedEvent, c as classifySignalContent, q as isTrustedSender } from "./repeats-Dj_o6c-X-LyCkKYjp.js";
2
- import { B as wakeInteractionSentence, T as wakeReadCommand, w as skillFirstLine } from "./api-command-rlS3eTlZ.js";
1
+ import { Et as downloadEmailAttachmentPart, J as parseWebhookEvent, K as isEmailReceivedEvent, c as classifySignalContent, q as isTrustedSender } from "./repeats-Dj_o6c-X-Bf9G0xds.js";
2
+ import { E as wakeReadCommand, T as skillFirstLine, V as wakeInteractionSentence } from "./api-command-ctHBqTL5.js";
3
3
  import { _ as acquireListenLock, g as ListenStateError, y as listenIdentity } from "./shared-mail-files-BAQSGxxz.js";
4
4
  import { closeSync, copyFileSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, opendirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { createHash, randomUUID, scryptSync } from "node:crypto";
6
6
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
7
+ import { spawn } from "node:child_process";
7
8
  import { homedir } from "node:os";
8
9
  import { lstat, realpath } from "node:fs/promises";
9
10
  import { isDeepStrictEqual } from "node:util";
10
11
  import WebSocket from "ws";
12
+ import { request } from "node:http";
13
+ import { request as request$1 } from "node:https";
14
+ import { performance } from "node:perf_hooks";
11
15
  // src/oclif/connect-skill.ts
12
16
  const CONNECT_SKILL_NAME = "primitive-connect";
13
17
  function connectSkillTarget(options) {
@@ -564,6 +568,331 @@ async function connectNativeSession(options) {
564
568
  }
565
569
  };
566
570
  }
571
+ // src/oclif/listen-handlers.ts
572
+ const HANDLER_TIMEOUT_MS = 3e4;
573
+ const MAX_RESPONSE_BYTES = 1048576;
574
+ const FORWARDED_HEADERS = /* @__PURE__ */ new Set([
575
+ "content-type",
576
+ "user-agent",
577
+ "primitive-signature",
578
+ "mymx-signature",
579
+ "x-webhook-event",
580
+ "x-webhook-id",
581
+ "x-primitive-webhook-delivery-id",
582
+ "x-primitive-email-id",
583
+ "x-primitive-endpoint-id"
584
+ ]);
585
+ function normalizeForwardTarget(target) {
586
+ if (/^\d+$/.test(target)) return `http://localhost:${target}`;
587
+ if (/^(localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/.test(target)) return `http://${target}`;
588
+ return target;
589
+ }
590
+ function validateListenHandlerOptions(options) {
591
+ if (options.exec !== void 0 && process.platform === "win32") throw new Error("--exec requires a POSIX shell and process groups. On Windows, use --forward-to with a local HTTP handler.");
592
+ if (options.exec !== void 0 && options.forwardTo !== void 0) throw new Error("Choose either --exec or --forward-to.");
593
+ if (options.exec !== void 0 && (!options.exec.trim() || options.exec.includes("\0"))) throw new Error("--exec requires a nonempty shell command.");
594
+ if (options.forwardTo !== void 0) {
595
+ let url;
596
+ try {
597
+ url = new URL(normalizeForwardTarget(options.forwardTo));
598
+ } catch {
599
+ throw new Error("--forward-to requires an HTTP or HTTPS URL.");
600
+ }
601
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || url.hash) throw new Error("--forward-to requires HTTP or HTTPS without userinfo or a fragment.");
602
+ }
603
+ }
604
+ /** User hooks retain their configuration, but not the CLI's API credentials. */
605
+ function hookEnvironment(delivery) {
606
+ const env = { ...process.env };
607
+ for (const name of Object.keys(env)) if (/^PRIMITIVE_(?:(?:API_|OAUTH_|AUTH_)?(?:KEY|TOKEN|ACCESS_TOKEN|REFRESH_TOKEN)|API_HEADERS|LEASE_TOKEN)$/i.test(name)) delete env[name];
608
+ return {
609
+ ...env,
610
+ PRIMITIVE_EVENT_ID: delivery.event_id,
611
+ PRIMITIVE_DELIVERY_ID: delivery.delivery_id,
612
+ PRIMITIVE_EVENT_TYPE: delivery.event_type
613
+ };
614
+ }
615
+ async function writeChunk(output, chunk, signal) {
616
+ signal.throwIfAborted();
617
+ let onError = () => {};
618
+ let onAbort = () => {};
619
+ let onDrain = () => {};
620
+ let onClose = () => {};
621
+ try {
622
+ await new Promise((resolve, reject) => {
623
+ let callbackDone = false;
624
+ let returned = false;
625
+ let accepted = false;
626
+ let drained = false;
627
+ const finish = () => {
628
+ if (returned && callbackDone && (accepted || drained)) resolve();
629
+ };
630
+ onError = reject;
631
+ onAbort = () => reject(signal.reason);
632
+ onClose = () => reject(/* @__PURE__ */ new Error("Output closed before accepting the event."));
633
+ onDrain = () => {
634
+ drained = true;
635
+ finish();
636
+ };
637
+ output.on("error", onError);
638
+ output.on("close", onClose);
639
+ output.on("drain", onDrain);
640
+ signal.addEventListener("abort", onAbort, { once: true });
641
+ accepted = output.write(chunk, (error) => {
642
+ if (error) reject(error);
643
+ else {
644
+ callbackDone = true;
645
+ finish();
646
+ }
647
+ });
648
+ returned = true;
649
+ finish();
650
+ });
651
+ } finally {
652
+ output.off("error", onError);
653
+ output.off("close", onClose);
654
+ output.off("drain", onDrain);
655
+ signal.removeEventListener("abort", onAbort);
656
+ }
657
+ }
658
+ function stopProcessGroup(child) {
659
+ if (!child.pid) return;
660
+ try {
661
+ process.kill(-child.pid, "SIGKILL");
662
+ } catch {}
663
+ }
664
+ var ChildCleanupError = class extends Error {};
665
+ async function reapChild(child, closed) {
666
+ stopProcessGroup(child);
667
+ child.stdin?.destroy();
668
+ child.stdout?.destroy();
669
+ child.stderr?.destroy();
670
+ let timer;
671
+ try {
672
+ await Promise.race([closed, new Promise((_resolve, reject) => {
673
+ timer = setTimeout(() => reject(new ChildCleanupError("The hook did not close after termination. Its delivery remains uncompleted.")), 1e3);
674
+ })]);
675
+ } finally {
676
+ clearTimeout(timer);
677
+ }
678
+ }
679
+ async function execute(command, delivery, stderr, signal) {
680
+ signal.throwIfAborted();
681
+ const child = spawn(command, {
682
+ shell: true,
683
+ detached: true,
684
+ env: hookEnvironment(delivery),
685
+ stdio: [
686
+ "pipe",
687
+ "pipe",
688
+ "pipe"
689
+ ],
690
+ windowsHide: true
691
+ });
692
+ const closed = new Promise((resolve) => child.once("close", () => resolve()));
693
+ const stop = () => stopProcessGroup(child);
694
+ signal.addEventListener("abort", stop, { once: true });
695
+ const consume = async (stream) => {
696
+ for await (const chunk of stream) await writeChunk(stderr, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), signal);
697
+ };
698
+ child.once("exit", stop);
699
+ try {
700
+ const exited = new Promise((resolve, reject) => {
701
+ child.once("error", reject);
702
+ child.once("close", (code) => resolve(code));
703
+ });
704
+ const stdin = writeChunk(child.stdin, Buffer.from(delivery.body, "utf8"), signal).then(() => child.stdin.end());
705
+ const [code] = await Promise.all([
706
+ exited,
707
+ stdin,
708
+ consume(child.stdout),
709
+ consume(child.stderr)
710
+ ]);
711
+ signal.throwIfAborted();
712
+ return code;
713
+ } finally {
714
+ try {
715
+ await reapChild(child, closed);
716
+ } finally {
717
+ signal.removeEventListener("abort", stop);
718
+ }
719
+ }
720
+ }
721
+ function structuredErrorCode(body) {
722
+ try {
723
+ const value = JSON.parse(body);
724
+ if (typeof value !== "object" || value === null) return void 0;
725
+ const object = value;
726
+ const nested = typeof object.error === "object" && object.error !== null ? object.error.code : void 0;
727
+ const code = typeof nested === "string" ? nested : object.code;
728
+ return typeof code === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(code) ? code : void 0;
729
+ } catch {
730
+ return;
731
+ }
732
+ }
733
+ var ForwardFailure = class extends Error {
734
+ statusCode;
735
+ transportError;
736
+ constructor(statusCode, transportError) {
737
+ super("Webhook transport did not complete.");
738
+ this.statusCode = statusCode;
739
+ this.transportError = transportError;
740
+ }
741
+ };
742
+ async function forward(target, delivery, signal) {
743
+ signal.throwIfAborted();
744
+ const url = new URL(normalizeForwardTarget(target));
745
+ const body = Buffer.from(delivery.body, "utf8");
746
+ const headers = {};
747
+ for (const [name, value] of Object.entries(delivery.headers)) if (FORWARDED_HEADERS.has(name.toLowerCase())) headers[name.toLowerCase()] = value;
748
+ headers["content-length"] = String(body.length);
749
+ return new Promise((resolve, reject) => {
750
+ let observedStatus = null;
751
+ let settled = false;
752
+ const fail = (kind = observedStatus === null ? "network" : "io") => {
753
+ if (settled) return;
754
+ settled = true;
755
+ reject(new ForwardFailure(observedStatus, kind));
756
+ };
757
+ const request$2 = (url.protocol === "https:" ? request$1 : request)(url, {
758
+ method: "POST",
759
+ headers,
760
+ signal,
761
+ rejectUnauthorized: true
762
+ }, (response) => {
763
+ observedStatus = response.statusCode ?? null;
764
+ const chunks = [];
765
+ let size = 0;
766
+ response.on("error", () => fail());
767
+ response.on("aborted", () => fail());
768
+ response.on("data", (chunk) => {
769
+ size += chunk.length;
770
+ if (size > MAX_RESPONSE_BYTES) {
771
+ fail("response_too_large");
772
+ request$2.destroy();
773
+ } else chunks.push(chunk);
774
+ });
775
+ response.on("end", () => {
776
+ if (settled) return;
777
+ if (!response.complete || size > MAX_RESPONSE_BYTES) {
778
+ fail(size > MAX_RESPONSE_BYTES ? "response_too_large" : "io");
779
+ return;
780
+ }
781
+ settled = true;
782
+ const status = response.statusCode ?? 0;
783
+ const succeeded = status >= 200 && status < 300;
784
+ resolve({
785
+ status,
786
+ confirmed: succeeded && (response.headers["x-primitive-confirmed"] === "true" || response.headers["x-mymx-confirmed"] === "true"),
787
+ errorCode: succeeded ? void 0 : status >= 300 && status < 400 ? "handler_redirect" : structuredErrorCode(Buffer.concat(chunks).toString("utf8")) ?? (status >= 400 && status < 500 ? "handler_4xx" : status >= 500 ? "handler_5xx" : "unknown_error")
788
+ });
789
+ });
790
+ });
791
+ request$2.on("error", () => fail());
792
+ request$2.end(body);
793
+ });
794
+ }
795
+ function jsonLine(body) {
796
+ try {
797
+ return JSON.stringify(JSON.parse(body));
798
+ } catch {
799
+ throw new Error("Webhook body is not valid JSON.");
800
+ }
801
+ }
802
+ /**
803
+ * The API withholds an event's body from a credential that cannot read the
804
+ * mail it carries. Such an event is never handed on as if it were the event:
805
+ * listening stops with an explanation and the delivery stays uncompleted.
806
+ */
807
+ function readableDelivery(delivery) {
808
+ if (delivery.body === null) throw new ListenStateError("The API withheld this event's content from the listening credential; the delivery remains uncompleted. Listen with a credential that can read this mail.");
809
+ return {
810
+ ...delivery,
811
+ body: delivery.body
812
+ };
813
+ }
814
+ function createListenHandler(options = {}) {
815
+ validateListenHandlerOptions(options);
816
+ return async (received, signal) => {
817
+ signal.throwIfAborted();
818
+ const delivery = readableDelivery(received);
819
+ const start = performance.now();
820
+ const work = new AbortController();
821
+ let timedOut = false;
822
+ const cancel = () => work.abort(signal.reason);
823
+ signal.addEventListener("abort", cancel, { once: true });
824
+ const timer = setTimeout(() => {
825
+ timedOut = true;
826
+ work.abort(/* @__PURE__ */ new Error("Event handler timed out."));
827
+ }, HANDLER_TIMEOUT_MS);
828
+ const duration = () => Math.min(HANDLER_TIMEOUT_MS, Math.max(0, Math.round(performance.now() - start)));
829
+ try {
830
+ if (options.exec !== void 0) {
831
+ const code = await execute(options.exec, delivery, options.stderr ?? process.stderr, work.signal);
832
+ signal.throwIfAborted();
833
+ return {
834
+ succeeded: code === 0,
835
+ outcome: {
836
+ mode: "exec",
837
+ exit_code: code,
838
+ duration_ms: duration()
839
+ }
840
+ };
841
+ }
842
+ if (options.forwardTo !== void 0) {
843
+ const response = await forward(options.forwardTo, delivery, work.signal);
844
+ signal.throwIfAborted();
845
+ return {
846
+ succeeded: response.status >= 200 && response.status < 300,
847
+ outcome: {
848
+ mode: "http",
849
+ status_code: response.status,
850
+ confirmed: response.confirmed,
851
+ duration_ms: duration(),
852
+ ...response.errorCode ? { error_code: response.errorCode } : {}
853
+ }
854
+ };
855
+ }
856
+ await writeChunk(options.stdout ?? process.stdout, Buffer.from(`${jsonLine(delivery.body)}\n`, "utf8"), work.signal);
857
+ signal.throwIfAborted();
858
+ return {
859
+ succeeded: true,
860
+ outcome: {
861
+ mode: "stdout",
862
+ write_succeeded: true,
863
+ duration_ms: duration()
864
+ }
865
+ };
866
+ } catch (error) {
867
+ signal.throwIfAborted();
868
+ if (error instanceof ChildCleanupError) throw error;
869
+ if (options.exec !== void 0) return {
870
+ succeeded: false,
871
+ outcome: {
872
+ mode: "exec",
873
+ exit_code: null,
874
+ transport_error: timedOut ? "timeout" : "io",
875
+ duration_ms: duration()
876
+ }
877
+ };
878
+ if (options.forwardTo !== void 0) return {
879
+ succeeded: false,
880
+ outcome: {
881
+ mode: "http",
882
+ status_code: error instanceof ForwardFailure ? error.statusCode : null,
883
+ confirmed: false,
884
+ transport_error: timedOut ? "timeout" : error instanceof ForwardFailure ? error.transportError : "network",
885
+ duration_ms: duration()
886
+ }
887
+ };
888
+ throw error;
889
+ } finally {
890
+ clearTimeout(timer);
891
+ signal.removeEventListener("abort", cancel);
892
+ work.abort();
893
+ }
894
+ };
895
+ }
567
896
  // src/oclif/notify-session-content.ts
568
897
  var NotificationRetryError = class extends ListenStateError {};
569
898
  function notificationPartReader(client) {
@@ -951,9 +1280,10 @@ async function openSessionNotifications(options) {
951
1280
  }
952
1281
  });
953
1282
  if (delivery.event_type !== "email.received") throw new ListenStateError("Notification mode received an unrelated event. It remains uncompleted; use an email.received-only subscription.");
1283
+ const { body } = readableDelivery(delivery);
954
1284
  let event;
955
1285
  try {
956
- event = parseWebhookEvent(JSON.parse(delivery.body), delivery.event_type);
1286
+ event = parseWebhookEvent(JSON.parse(body), delivery.event_type);
957
1287
  } catch {
958
1288
  throw new ListenStateError("Invalid email event; the delivery remains uncompleted.");
959
1289
  }
@@ -1114,4 +1444,4 @@ async function openSessionNotifications(options) {
1114
1444
  }
1115
1445
  var ContactNoticeSuppressed = class extends Error {};
1116
1446
  var ContactNoticeDeferred = class extends Error {};
1117
- export { installConnectSkill as _, NotificationRetryError as a, readBundledConnectSkill as b, readConversationStatusContent as c, defaultSessionSocket as d, NativeSessionDisconnectedError as f, connectSkillTarget as g, NotificationOutcomeUnknownError as h, notificationReceiptPage as i, SESSION_UUID as l, NativeSessionNotLoadedError as m, notificationSenders as n, isRoutineNotificationContent as o, NativeSessionError as p, openSessionNotifications as r, notificationPartReader as s, notificationScope as t, connectNativeSession as u, installedConnectSkillFile as v, installedVersion as y };
1447
+ export { readBundledConnectSkill as S, NotificationOutcomeUnknownError as _, NotificationRetryError as a, installedConnectSkillFile as b, readConversationStatusContent as c, SESSION_UUID as d, connectNativeSession as f, NativeSessionNotLoadedError as g, NativeSessionError as h, notificationReceiptPage as i, createListenHandler as l, NativeSessionDisconnectedError as m, notificationSenders as n, isRoutineNotificationContent as o, defaultSessionSocket as p, openSessionNotifications as r, notificationPartReader as s, notificationScope as t, readableDelivery as u, connectSkillTarget as v, installedVersion as x, installConnectSkill as y };
@@ -1,8 +1,8 @@
1
- import { Ut as getSentEmail } from "../repeats-Dj_o6c-X-LyCkKYjp.js";
2
- import { Mt as createAuthenticatedCliApiClient, b as readThreadContext } from "../api-command-rlS3eTlZ.js";
1
+ import { Gt as getSentEmail } from "../repeats-Dj_o6c-X-Bf9G0xds.js";
2
+ import { Mt as createAuthenticatedCliApiClient, x as readThreadContext } from "../api-command-ctHBqTL5.js";
3
3
  import { AUTO_SIGNAL_KIND_ENV, AUTO_WORKING_RENEW_MS, autoSignalsDisabled, clearWorkingSending, markWorkingSending, readAutoClaim, readWorkingLease, recordRenewer, stopWorkingLease, writeWorkingLease } from "./auto-signals.js";
4
4
  import { _ as acquireListenLock } from "../shared-mail-files-BAQSGxxz.js";
5
- import { t as sendSignal } from "../signal-command-DCPsJBdz.js";
5
+ import { t as sendSignal } from "../signal-command-CRUMUCNa.js";
6
6
  import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  // src/oclif/auto-signal-worker.ts
@@ -1,4 +1,4 @@
1
- import { n as isPlainChatReply, o as scopedChatSenderTrust } from "../scoped-chat-B0thCwME.js";
1
+ import { n as isPlainChatReply, o as scopedChatSenderTrust } from "../scoped-chat-DZ42V3jq.js";
2
2
  import { d as readMailJson, h as writeMailJson, i as mailId, l as privateMailDirectory, r as mailAddress } from "../shared-mail-files-BAQSGxxz.js";
3
3
  import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { randomUUID } from "node:crypto";
@@ -63,7 +63,7 @@ function autoSignalEligible(detail, identity, admission) {
63
63
  if (!detail.message_id) return false;
64
64
  const parsed = detail.parsed;
65
65
  const to = parsed?.to_addresses;
66
- if (!Array.isArray(to) || to.length !== 1 || lower(to[0]?.address) !== agent || (parsed.cc?.length ?? 0) > 0 || (parsed.bcc?.length ?? 0) > 0) return false;
66
+ if (!Array.isArray(to) || to.length !== 1 || lower(to[0]?.address) !== agent || (parsed?.cc?.length ?? 0) > 0 || (parsed?.bcc?.length ?? 0) > 0) return false;
67
67
  return isPlainChatReply(detail);
68
68
  } catch {
69
69
  return false;