primcli 1.47.0 → 1.49.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.
@@ -1,18 +1,334 @@
1
1
  import { n as __exportAll } from "./rolldown-runtime-CNZpIYwj.js";
2
2
  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, j as AGENT_PROFILE_ENV, x as loadCliCredentials, z as parseConnectedAgentProfile } from "./cli-config-DGrZCUfw.js";
3
- import { $t as getAgentContactPolicy, Dn as listSentEmails, Fn as putContact, Gn as sendEmail, Gt as deleteAgentContact, Hn as searchEmails, Kt as deleteContact, Nn as putAgentContact, Pn as putAgentContactPolicy, Pt as refreshStoredCliCredentials, _n as listContacts, bn as listDomains, en as getContact, hn as listAgentContacts, in as getEmail } from "./api-command-C1JvJqV0.js";
3
+ import { $t as getAgentContactPolicy, Dn as listSentEmails, Fn as putContact, Gn as sendEmail, Gt as deleteAgentContact, Hn as searchEmails, Kt as deleteContact, Nn as putAgentContact, Pn as putAgentContactPolicy, Pt as refreshStoredCliCredentials, _n as listContacts, bn as listDomains, en as getContact, hn as listAgentContacts, in as getEmail } from "./api-command-BixHVNwQ.js";
4
4
  import { _ as acquireListenLock, c as mailTime, d as readMailJson, f as removeMailFile, g as ListenStateError, h as writeMailJson, i as mailId, l as privateMailDirectory, o as mailObject, r as mailAddress, s as mailString } from "./shared-mail-files-SPPcis9g.js";
5
- import { a as readBundledConnectSkill, h as connectNativeSession, m as SESSION_UUID, n as detectAgentRuntime, o as notificationScope, r as installConnectSkill, u as NotificationRetryError } from "./connect-skill-CH2pq436.js";
6
- import { l as stopBackgroundListen, r as backgroundListenStatus } from "./listen-background-CyLc8VBn.js";
7
- import { t as readSharedMailOwner } from "./shared-mail-watch-BUfUaq6j.js";
8
- import { a as scopedChatSenderTrust } from "./scoped-chat-DhxtuNXs.js";
9
- import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { a as scopedChatSenderTrust } from "./scoped-chat-DSLrmgWQ.js";
6
+ import { _ as NativeSessionDisconnectedError, a as readBundledConnectSkill, g as defaultSessionSocket, h as connectNativeSession, m as SESSION_UUID, n as detectAgentRuntime, o as notificationScope, r as installConnectSkill, u as NotificationRetryError } from "./connect-skill-DBkhFAkO.js";
7
+ import { l as stopBackgroundListen, r as backgroundListenStatus } from "./listen-background-CMfxkeVP.js";
8
+ import { t as readSharedMailOwner } from "./shared-mail-watch-CWhpfJ0N.js";
9
+ import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
10
10
  import { createHash, randomUUID } from "node:crypto";
11
11
  import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
12
12
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
13
13
  import { execFile, execFileSync, spawn } from "node:child_process";
14
14
  import { homedir } from "node:os";
15
15
  import { promisify } from "node:util";
16
+ import { createConnection } from "node:net";
17
+ //#region src/oclif/owner-member-address.ts
18
+ const REFRESH_TIMEOUT_MS = 5e3;
19
+ const MAX_RESPONSE_BYTES$1 = 64 * 1024;
20
+ /** Reads at most MAX_RESPONSE_BYTES, stopping as soon as the body exceeds it. */
21
+ async function boundedText(response) {
22
+ const reader = response.body?.getReader();
23
+ if (!reader) return null;
24
+ const chunks = [];
25
+ let size = 0;
26
+ try {
27
+ for (;;) {
28
+ const part = await reader.read();
29
+ if (part.done) break;
30
+ size += part.value.byteLength;
31
+ if (size > MAX_RESPONSE_BYTES$1) return null;
32
+ chunks.push(part.value);
33
+ }
34
+ return Buffer.concat(chunks).toString("utf8");
35
+ } finally {
36
+ await reader.cancel().catch(() => void 0);
37
+ }
38
+ }
39
+ /**
40
+ * Reads the owner's personal mailbox from the connection's own record
41
+ * (GET /agent-connections/me) and saves it in the profile, so an agent paired
42
+ * before the server reported it, or before its owner set one up, learns where
43
+ * to send reports. Best effort: any failure keeps the saved value. Returns the
44
+ * address to report to, or null when there is none or it is unknown.
45
+ */
46
+ async function refreshOwnerMemberAddress(params) {
47
+ const profile = loadConnectedAgentProfile(params.configDir, params.profileName);
48
+ if (!profile) return null;
49
+ const saved = profile.owner_member_address ?? null;
50
+ try {
51
+ const response = await (params.fetch ?? fetch)(`${profile.api_base_url}/agent-connections/me`, {
52
+ headers: { authorization: `Bearer ${profile.api_key}` },
53
+ redirect: "error",
54
+ signal: AbortSignal.timeout(params.timeoutMs ?? REFRESH_TIMEOUT_MS)
55
+ });
56
+ if (!response.ok) return saved;
57
+ const text = await boundedText(response);
58
+ if (text === null) return saved;
59
+ const body = JSON.parse(text);
60
+ const connection = body?.data?.connection;
61
+ if (body?.success !== true || !connection || !Object.hasOwn(connection, "owner_member_address") || mailAddress(connection.address) !== profile.agent_address || mailAddress(connection.owner_address) !== profile.owner_address) return saved;
62
+ const current = parseOwnerMemberAddress(connection.owner_member_address, profile.owner_address, profile.agent_address);
63
+ const latest = loadConnectedAgentProfile(params.configDir, params.profileName);
64
+ if (!latest || latest.api_key !== profile.api_key || latest.agent_address !== profile.agent_address || latest.owner_address !== profile.owner_address) return latest?.owner_member_address ?? null;
65
+ if (latest.owner_member_address !== current) saveConnectedAgentProfile(params.configDir, params.profileName, {
66
+ ...latest,
67
+ owner_member_address: current
68
+ });
69
+ return current;
70
+ } catch {
71
+ return saved;
72
+ }
73
+ }
74
+ const REFRESH_STAMP_FILE = "owner-member-refresh.json";
75
+ function lastRefreshAttempt(path) {
76
+ try {
77
+ const saved = readMailJson(path);
78
+ if (saved?.version !== 1 || typeof saved.at !== "string") return null;
79
+ const at = Date.parse(saved.at);
80
+ return Number.isFinite(at) ? at : null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+ /**
86
+ * refreshOwnerMemberAddress for routine paths (each mail check, each start of
87
+ * a wake receiver): reads the connection's record at most once per interval,
88
+ * so an owner who sets up a personal address after pairing is learned without
89
+ * a reconnect. Between reads it returns the saved value without a request.
90
+ * The time of each attempt is saved with the profile, so a failing server is
91
+ * not asked again on every turn. Never throws.
92
+ */
93
+ async function refreshOwnerMemberAddressPeriodically(params) {
94
+ let saved = null;
95
+ let release;
96
+ try {
97
+ const profile = loadConnectedAgentProfile(params.configDir, params.profileName);
98
+ if (!profile) return null;
99
+ saved = profile.owner_member_address ?? null;
100
+ const directory = agentProfileDirectory(params.configDir, params.profileName);
101
+ try {
102
+ release = acquireListenLock(directory, "owner-member-refresh");
103
+ } catch {
104
+ return saved;
105
+ }
106
+ const now = (params.now ?? Date.now)();
107
+ const stamp = join(directory, REFRESH_STAMP_FILE);
108
+ const last = lastRefreshAttempt(stamp);
109
+ if (last !== null && last <= now && now - last < (params.intervalMs ?? 6e5)) return saved;
110
+ try {
111
+ writeMailJson(stamp, {
112
+ version: 1,
113
+ at: new Date(now).toISOString()
114
+ });
115
+ } catch {}
116
+ return await refreshOwnerMemberAddress(params);
117
+ } catch {
118
+ return saved;
119
+ } finally {
120
+ release?.();
121
+ }
122
+ }
123
+ /** One sentence telling the agent where its reports go. */
124
+ function ownerReportGuidance(identity) {
125
+ return identity.ownerMemberAddress ? `Send reports and questions to your owner's personal address ${identity.ownerMemberAddress}; ${identity.ownerAddress} is the setup and presence control address and nobody reads it.` : `No personal owner address is known, so reply to the member who wrote to you; never send reports to ${identity.ownerAddress}, the setup and presence control address.`;
126
+ }
127
+ /**
128
+ * Adds the current personal address and report guidance to a result that
129
+ * carries a connected identity, refreshing the saved profile first.
130
+ */
131
+ async function withOwnerMemberAddress(result, params) {
132
+ const saved = loadConnectedAgentProfile(params.configDir, result.identity.profileName);
133
+ const ownerMemberAddress = params.onlyIfUnknown && typeof saved?.owner_member_address === "string" ? saved.owner_member_address : await refreshOwnerMemberAddress({
134
+ configDir: params.configDir,
135
+ profileName: result.identity.profileName,
136
+ fetch: params.fetch
137
+ });
138
+ const identity = {
139
+ ...result.identity,
140
+ ownerMemberAddress
141
+ };
142
+ return {
143
+ ...result,
144
+ identity,
145
+ ownerReportGuidance: ownerReportGuidance(identity)
146
+ };
147
+ }
148
+ //#endregion
149
+ //#region src/oclif/hook-launcher.ts
150
+ /**
151
+ * Claude Code hooks run Primitive through a small POSIX launcher instead of a
152
+ * pinned Node binary. A version manager (nvm, n, asdf) or a package-runner
153
+ * cache (npx) can remove the Node or CLI files a hook names at any time; the
154
+ * launcher then falls back to another Node and another copy of the CLI, and
155
+ * when it finds no Node at all it records a warning instead of failing
156
+ * silently. Windows keeps hooks that name Node directly.
157
+ */
158
+ const HOOK_LAUNCHER_MARKER = "primitive-hook-launcher-v1";
159
+ function hookLauncherSupported(platform = process.platform) {
160
+ return platform !== "win32";
161
+ }
162
+ function hookLauncherPath(configDir) {
163
+ return join(resolve(configDir), "bin", "primitive-node");
164
+ }
165
+ /** Written by the launcher when no Node (or no CLI copy) can be found. */
166
+ function hookLauncherWarningPath(configDir) {
167
+ return join(resolve(configDir), "bin", "primitive-node.warning");
168
+ }
169
+ function shellQuote(value) {
170
+ return `'${value.replace(/'/g, `'\\''`)}'`;
171
+ }
172
+ const NO_NODE_WARNING = `Primitive wake hooks could not find Node.js 22 or later, so new mail will not wake this session. Install it, then run \`primitive agent connect --resume\` for each connected session or \`primitive machine doctor --fix\`.`;
173
+ const NO_CLI_WARNING = "Primitive wake hooks could not find the Primitive CLI (its package cache was removed and it could not be fetched again), so new mail will not wake this session. Run `npm install -g primitive`, then `primitive machine doctor --fix`.";
174
+ /** The read version of the CLI package whose bin/run.js is `entry`. */
175
+ function cliPackageVersion(entry) {
176
+ try {
177
+ const manifest = JSON.parse(readFileSync(join(dirname(entry), "..", "package.json"), "utf8"));
178
+ return manifest.name === "primitive" && typeof manifest.version === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version) ? manifest.version : null;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+ /**
184
+ * The launcher script. It takes the hook's own arguments (the script to run,
185
+ * then its arguments) and:
186
+ *
187
+ * 1. runs the pinned Node when it still exists, else the first `node` on
188
+ * PATH, else a sibling version of the pinned one, else a common install
189
+ * location;
190
+ * 2. when the script the hook names is gone (a cleaned npx cache), runs the
191
+ * same script from the `primitive` on PATH, or from
192
+ * `npx -y -p primitive@<version>`;
193
+ * 3. when neither can be found, writes a one-line warning, shows it once to
194
+ * the session through the PostToolUse hook, and exits 0 so Claude Code is
195
+ * never blocked.
196
+ */
197
+ function hookLauncherScript(params) {
198
+ const warning = hookLauncherWarningPath(params.configDir);
199
+ const common = params.commonLocations === false ? "" : ` "$nvm_root/current/bin/node" "$nvm_root"/versions/node/*/bin/node /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node "$HOME/.volta/bin/node"`;
200
+ return `#!/bin/sh
201
+ # ${HOOK_LAUNCHER_MARKER}
202
+ # Written by the Primitive CLI. Claude Code hooks run Primitive through this
203
+ # file so they keep working when the Node.js or CLI copy they were installed
204
+ # with is removed. \`primitive agent connect --resume\` and
205
+ # \`primitive machine doctor --fix\` rewrite it.
206
+ pinned_node=${shellQuote(params.node)}
207
+ pinned_version=${shellQuote(params.version ?? "")}
208
+ warning_file=${shellQuote(warning)}
209
+
210
+ # Record why hooks cannot run, show it once to each session through its
211
+ # PostToolUse hook (the session id is the hook's sixth argument), and exit 0
212
+ # so Claude Code is never blocked.
213
+ fail_open() {
214
+ mkdir -p "\${warning_file%/*}" 2>/dev/null
215
+ printf '%s\\n' "$1" > "$warning_file" 2>/dev/null
216
+ case "\${hook_script##*/}" in
217
+ claude-pending-mail.mjs)
218
+ session=$hook_session
219
+ case "$session" in
220
+ '' | *[!0-9A-Fa-f-]*) session=unknown ;;
221
+ esac
222
+ # mkdir is atomic, so of several hooks running at once only one
223
+ # shows the warning.
224
+ if mkdir "$warning_file.shown-$session" 2>/dev/null; then
225
+ printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}' "$1"
226
+ fi
227
+ ;;
228
+ esac
229
+ exit 0
230
+ }
231
+
232
+ # A fallback Node must be new enough to run this CLI.
233
+ supported() {
234
+ [ -x "$1" ] && "$1" -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' >/dev/null 2>&1
235
+ }
236
+
237
+ pick_node() {
238
+ if [ -x "$pinned_node" ]; then node=$pinned_node; return 0; fi
239
+ node=$(command -v node 2>/dev/null)
240
+ if [ -n "$node" ] && supported "$node"; then return 0; fi
241
+ versions=\${pinned_node%/*/bin/node}
242
+ nvm_root=\${NVM_DIR:-$HOME/.nvm}
243
+ for node in "$versions"/*/bin/node${common}; do
244
+ if supported "$node"; then return 0; fi
245
+ done
246
+ node=
247
+ return 1
248
+ }
249
+
250
+ package_dir() {
251
+ "$node" -e 'process.stdout.write(require("path").dirname(require("fs").realpathSync(process.argv[1])))' "$1" 2>/dev/null
252
+ }
253
+
254
+ # A CLI copy whose bin directory holds both the hook script and run.js.
255
+ complete_copy() {
256
+ [ -n "$1" ] && [ -f "$1/$name" ] && [ -f "$1/run.js" ]
257
+ }
258
+
259
+ hook_script=$1
260
+ hook_session=$6
261
+ pick_node || fail_open ${shellQuote(NO_NODE_WARNING)}
262
+ PATH=\${node%/*}:$PATH
263
+ export PATH
264
+
265
+ # The hook names a script and, after it, the CLI's run.js. When either is
266
+ # gone (a cleaned npx cache), both come from another copy of the CLI.
267
+ need_cli=
268
+ if [ $# -ge 1 ] && [ ! -f "$1" ]; then need_cli=1; fi
269
+ if [ $# -ge 2 ] && [ "\${2##*/}" = run.js ] && [ ! -f "$2" ]; then need_cli=1; fi
270
+ if [ -n "$need_cli" ]; then
271
+ name=\${1##*/}
272
+ dir=
273
+ if [ -f "$1" ] && complete_copy "\${1%/*}"; then dir=\${1%/*}; fi
274
+ if ! complete_copy "$dir"; then
275
+ found=$(command -v primitive 2>/dev/null)
276
+ if [ -n "$found" ]; then dir=$(package_dir "$found"); fi
277
+ fi
278
+ if ! complete_copy "$dir" && [ -n "$pinned_version" ]; then
279
+ npx=\${node%/*}/npx
280
+ [ -x "$npx" ] || npx=$(command -v npx 2>/dev/null)
281
+ if [ -n "$npx" ]; then
282
+ found=$("$npx" -y -p "primitive@$pinned_version" -c 'command -v primitive' 2>/dev/null | tail -n 1)
283
+ if [ -n "$found" ]; then dir=$(package_dir "$found"); fi
284
+ fi
285
+ fi
286
+ complete_copy "$dir" || fail_open ${shellQuote(NO_CLI_WARNING)}
287
+ shift
288
+ if [ $# -ge 1 ] && [ "\${1##*/}" = run.js ] && [ ! -f "$1" ]; then
289
+ shift
290
+ set -- "$dir/$name" "$dir/run.js" "$@"
291
+ else
292
+ set -- "$dir/$name" "$@"
293
+ fi
294
+ fi
295
+
296
+ if [ -f "$warning_file" ]; then rm -rf "$warning_file" "$warning_file".shown-* 2>/dev/null; fi
297
+ exec "$node" "$@"
298
+ `;
299
+ }
300
+ /**
301
+ * Write the launcher when its content differs, atomically and executable.
302
+ * Returns true when the file changed. Throws when it cannot be written.
303
+ */
304
+ function writeHookLauncher(path, content) {
305
+ try {
306
+ if (readFileSync(path, "utf8") === content) return false;
307
+ } catch {}
308
+ mkdirSync(dirname(path), {
309
+ recursive: true,
310
+ mode: 448
311
+ });
312
+ const temporary = `${path}.${randomUUID()}.tmp`;
313
+ try {
314
+ writeFileSync(temporary, content, { mode: 493 });
315
+ chmodSync(temporary, 493);
316
+ renameSync(temporary, path);
317
+ } catch (error) {
318
+ rmSync(temporary, { force: true });
319
+ throw error;
320
+ }
321
+ return true;
322
+ }
323
+ /** The warning the launcher last wrote, or null when it found what it needed. */
324
+ function readHookLauncherWarning(configDir) {
325
+ try {
326
+ return readFileSync(hookLauncherWarningPath(configDir), "utf8").trim() || null;
327
+ } catch {
328
+ return null;
329
+ }
330
+ }
331
+ //#endregion
16
332
  //#region src/oclif/machine-files.ts
17
333
  /** Hand-edited files larger than this are reported, never rewritten. */
18
334
  const MAX_MANAGED_FILE_BYTES = 1048576;
@@ -296,20 +612,39 @@ function stableNodePath(env, execPath = process.execPath) {
296
612
  if (onPath && isAbsolute(onPath) && onPath !== execPath && samePath(onPath, execPath)) return onPath;
297
613
  return execPath;
298
614
  }
299
- /** Locate the running CLI. Null when its files are not where a package install puts them. */
300
- function currentCliLocation(entryArg, env, execPath = process.execPath) {
615
+ /**
616
+ * Locate the running CLI. Null when its files are not where a package install
617
+ * puts them. With a config directory, Claude hooks run through the hook
618
+ * launcher in it (except on Windows).
619
+ */
620
+ function currentCliLocation(entryArg, env, execPath = process.execPath, options = {}) {
301
621
  const entry = realpathOrNull(entryArg);
302
622
  if (!entry) return null;
303
623
  const wake = realpathOrNull(join(dirname(entry), "claude-wake.mjs"));
304
624
  const pending = realpathOrNull(join(dirname(entry), "claude-pending-mail.mjs"));
305
625
  if (!wake || !pending) return null;
626
+ const runtimeNode = stableNodePath(env, execPath);
627
+ const launcher = options.configDir !== void 0 && hookLauncherSupported(options.platform) ? {
628
+ path: hookLauncherPath(options.configDir),
629
+ content: hookLauncherScript({
630
+ node: runtimeNode,
631
+ version: cliPackageVersion(entry),
632
+ configDir: options.configDir
633
+ })
634
+ } : null;
306
635
  return {
307
- node: stableNodePath(env, execPath),
636
+ node: launcher?.path ?? runtimeNode,
637
+ runtimeNode,
638
+ launcher,
308
639
  entry,
309
640
  wake,
310
641
  pending
311
642
  };
312
643
  }
644
+ /** Write the hook launcher a CLI location names. True when it changed. */
645
+ function ensureHookLauncher(cli) {
646
+ return cli.launcher ? writeHookLauncher(cli.launcher.path, cli.launcher.content) : false;
647
+ }
313
648
  /** Package-runner caches are cleaned up behind the user's back. */
314
649
  function ephemeralCliReason(entry) {
315
650
  if (/[\\/]_npx[\\/]/.test(entry)) return "an npx cache";
@@ -438,6 +773,17 @@ function isGlobalManaged(hook, spec) {
438
773
  const args = hook.args;
439
774
  return args.length === 6 && typeof args[0] === "string" && args[1] === "agent" && args[2] === spec.subcommand && args[3] === "--runtime" && args[4] === "claude" && args[5] === "--hook";
440
775
  }
776
+ /**
777
+ * Whether settings carry either machine-wide session hook (SessionStart
778
+ * session-register or SessionEnd session-end), from any CLI path or version.
779
+ */
780
+ function hasMachineSessionHooks(settings) {
781
+ const hooks = record$2(settings.hooks) ? settings.hooks : {};
782
+ return Object.values(GLOBAL_HOOKS).some((spec) => {
783
+ const entries = eventEntries(hooks, spec.event);
784
+ return entries !== "invalid" && entries.some((entry) => record$2(entry) && Array.isArray(entry.hooks) && entry.hooks.some((hook) => isGlobalManaged(hook, spec)));
785
+ });
786
+ }
441
787
  function globalHookArgs(cli, spec) {
442
788
  return [
443
789
  cli.entry,
@@ -480,7 +826,7 @@ function hasSessionReceiveHook(hooks, event, target, node) {
480
826
  return entries.some((entry) => record$2(entry) && Array.isArray(entry.hooks) && entry.hooks.some((hook) => {
481
827
  if (!record$2(hook) || hook.type !== "command") return false;
482
828
  const args = hook.args;
483
- return Array.isArray(args) && args.length === 7 && args.every((value) => typeof value === "string") && basename(args[0]) === (pending ? "claude-pending-mail.mjs" : "claude-wake.mjs") && resolve(args[2]) === configDir && args[3] === target.profile && args[4] === target.address && args[5].toLowerCase() === session && args[6] === (pending ? PENDING_MARKER$1 : WAKE_MARKER) && samePath(hook.command, node);
829
+ return Array.isArray(args) && args.length === 7 && args.every((value) => typeof value === "string") && basename(args[0]) === (pending ? "claude-pending-mail.mjs" : "claude-wake.mjs") && resolve(args[2]) === configDir && args[3] === target.profile && args[4] === target.address && args[5].toLowerCase() === session && args[6] === (pending ? PENDING_MARKER$1 : WAKE_MARKER) && (node === null || samePath(hook.command, node));
484
830
  }));
485
831
  }
486
832
  /** A per-session receive hook written by `agent connect` or `agent enroll`, any version. */
@@ -688,9 +1034,13 @@ function heldSessionProfiles(hooks, bound, restore) {
688
1034
  }
689
1035
  return held;
690
1036
  }
691
- /** Receive hooks each bound profile should have and does not. */
692
- function missingSessionHooks(hooks, bound, node) {
693
- return bound.flatMap((item) => SESSION_EVENTS.filter((event) => !hasSessionReceiveHook(hooks, event, item, node)).map((event) => ({
1037
+ /**
1038
+ * Receive hooks each bound profile has in no form at all. A hook that exists
1039
+ * but names an old Node or CLI path is outdated, not missing, so one hook is
1040
+ * never reported (or repaired) as both.
1041
+ */
1042
+ function missingSessionHooks(hooks, bound) {
1043
+ return bound.flatMap((item) => SESSION_EVENTS.filter((event) => !hasSessionReceiveHook(hooks, event, item, null)).map((event) => ({
694
1044
  event,
695
1045
  bound: item
696
1046
  })));
@@ -808,7 +1158,7 @@ function inspectClaudeHooks(settings, context) {
808
1158
  };
809
1159
  const verdicts = judgeSessionHooks(hooks, context.cli, bound);
810
1160
  for (const verdict of verdicts.values()) counts[verdict]++;
811
- const missingHooks = missingSessionHooks(hooks, bound, context.cli?.node ?? process.execPath);
1161
+ const missingHooks = missingSessionHooks(hooks, bound);
812
1162
  const missing = missingHooks.length;
813
1163
  const items = sessionHookItems(hooks, verdicts, missingHooks);
814
1164
  const problems = [
@@ -936,7 +1286,7 @@ function repairClaudeHooks(settings, context, checks, options = {}) {
936
1286
  changed.add("claude.hook.stop");
937
1287
  }
938
1288
  }
939
- if (cli) for (const missing of missingSessionHooks(hooks, bound, cli.node)) {
1289
+ if (cli) for (const missing of missingSessionHooks(hooks, bound)) {
940
1290
  if (!selected(missing.bound.profile)) continue;
941
1291
  const entries = eventEntries(hooks, missing.event);
942
1292
  if (entries === "invalid") continue;
@@ -1023,12 +1373,12 @@ function claudeWakeHookStatus(options) {
1023
1373
  address: agentAddress,
1024
1374
  session: sessionId
1025
1375
  };
1026
- const node = stableNodePath(env);
1376
+ const commands = [...hookLauncherSupported(options.platform) ? [hookLauncherPath(configDir)] : [], stableNodePath(env)];
1027
1377
  const installed = [
1028
1378
  "Stop",
1029
1379
  "SessionStart",
1030
1380
  "PostToolUse"
1031
- ].every((event) => hasSessionReceiveHook(hooks, event, target, node));
1381
+ ].every((event) => commands.some((node) => hasSessionReceiveHook(hooks, event, target, node)));
1032
1382
  const lastFiredAt = recordedAt(readMailJson(join(agentProfileDirectory(configDir, profileName), `pending-mail-${sessionId}.fired.json`)));
1033
1383
  const lastMailCheckAt = recordedAt(readMailJson(hookMailCheckPath(configDir, profileName, sessionId)));
1034
1384
  const now = options.now ?? Date.now();
@@ -1099,17 +1449,27 @@ function editClaudeSettings(claudeDir, edit) {
1099
1449
  /** Add exact-session fail-open receiving hooks while preserving unrelated settings. */
1100
1450
  function installClaudeWakeHook(options) {
1101
1451
  try {
1102
- const cliPath = realpathSync(options.cliPath);
1103
- const wrapperPath = realpathSync(join(dirname(cliPath), "claude-wake.mjs"));
1104
- const pendingPath = realpathSync(join(dirname(cliPath), "claude-pending-mail.mjs"));
1105
1452
  const configDir = resolve(options.configDir);
1453
+ const env = options.env ?? process.env;
1454
+ const cli = currentCliLocation(options.cliPath, env, process.execPath, {
1455
+ configDir,
1456
+ platform: options.platform
1457
+ });
1458
+ if (!cli) return "unavailable";
1459
+ const cliPath = cli.entry;
1460
+ const wrapperPath = cli.wake;
1461
+ const pendingPath = cli.pending;
1106
1462
  const profileName = agentProfileName(options.profileName);
1107
1463
  const agentAddress = mailAddress(options.agentAddress);
1108
1464
  if (!SESSION_UUID.test(options.sessionId)) return "unavailable";
1109
1465
  const sessionId = options.sessionId.toLowerCase();
1110
- const env = options.env ?? process.env;
1111
1466
  const claudeDir = resolve(env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"));
1112
- const node = stableNodePath(env);
1467
+ let node = cli.node;
1468
+ try {
1469
+ ensureHookLauncher(cli);
1470
+ } catch {
1471
+ node = cli.runtimeNode;
1472
+ }
1113
1473
  let held = false;
1114
1474
  if (!editClaudeSettings(claudeDir, (settings) => {
1115
1475
  const hooks = settings.hooks ?? {};
@@ -1273,8 +1633,62 @@ function uninstallClaudeWakeHook(options) {
1273
1633
  //#endregion
1274
1634
  //#region src/oclif/agent-connect.ts
1275
1635
  const MAX_INVITATION_BYTES = 4096;
1276
- const MAX_RESPONSE_BYTES$1 = 32768;
1636
+ const MAX_RESPONSE_BYTES = 32768;
1277
1637
  const UNCERTAIN_CLAIM = "Setup did not complete safely. The invitation may have been consumed. Request a fresh invitation from the owner; do not retry this invitation.";
1638
+ const INVITATION_UNAVAILABLE = "This invitation was already used, has expired, or was revoked, so it cannot connect this agent. Ask the owner to copy a fresh setup instruction from the app. Nothing was changed on this machine.";
1639
+ const INVITATION_INVALID = "Primitive rejected this invitation as invalid. Ask the owner to copy a fresh setup instruction from the app. Nothing was changed on this machine.";
1640
+ /**
1641
+ * The server answered the claim with a definite refusal, so nothing was
1642
+ * claimed and no credential exists. Unlike a lost or malformed response, it
1643
+ * is safe to discard every local trace of the attempt.
1644
+ */
1645
+ var AgentInvitationRejectedError = class extends AgentConnectionSetupError {
1646
+ constructor(message, reason) {
1647
+ super(message);
1648
+ this.reason = reason;
1649
+ }
1650
+ };
1651
+ const NOTHING_CHANGED = "Nothing was changed on this machine.";
1652
+ /**
1653
+ * Restates a refusal that followed a replacement: the agent being replaced
1654
+ * was disconnected before the claim, so "nothing was changed" is not true.
1655
+ */
1656
+ function refusalAfterReplacement(error, replaced) {
1657
+ if (!(error instanceof AgentInvitationRejectedError) || !replaced.length) return error;
1658
+ const names = replaced.map((row) => row.address).join(", ");
1659
+ return new AgentInvitationRejectedError(error.message.replace(NOTHING_CHANGED, `The agent this was replacing (${names}) was already disconnected before the claim and stays disconnected.`), error.reason);
1660
+ }
1661
+ /** The error code of a refused claim, read without keeping the body. */
1662
+ async function claimErrorCode(response) {
1663
+ try {
1664
+ const body = await readClaimResponse(response);
1665
+ return body?.success === false && typeof body.error?.code === "string" ? body.error.code : null;
1666
+ } catch {
1667
+ return null;
1668
+ }
1669
+ }
1670
+ /** A definite refusal of the claim, or null when the outcome is uncertain. */
1671
+ async function definiteClaimRefusal(response) {
1672
+ if (![
1673
+ 400,
1674
+ 409,
1675
+ 429
1676
+ ].includes(response.status)) return null;
1677
+ const code = await claimErrorCode(response);
1678
+ if (response.status === 409 && code === "connection_invitation_unavailable") return new AgentInvitationRejectedError(INVITATION_UNAVAILABLE, "invitation_unavailable");
1679
+ if (response.status === 400 && code === "validation_error") return new AgentInvitationRejectedError(INVITATION_INVALID, "invitation_invalid");
1680
+ if (response.status === 429 && code === "rate_limit_exceeded") {
1681
+ const seconds = Number(response.headers.get("retry-after"));
1682
+ return new AgentInvitationRejectedError(`Primitive is limiting connection attempts from this network. The invitation was not claimed; wait ${Number.isSafeInteger(seconds) && seconds > 0 && seconds <= 3600 ? `${seconds} seconds` : "a minute"} and rerun the same command with the same invitation. Nothing was changed on this machine.`, "rate_limited");
1683
+ }
1684
+ return null;
1685
+ }
1686
+ /** Removes a directory only when it is empty. Best effort. */
1687
+ function removeEmptyDirectory(path) {
1688
+ try {
1689
+ rmdirSync(path);
1690
+ } catch {}
1691
+ }
1278
1692
  /** The receiver a saved profile's setup record names. Offline. */
1279
1693
  function savedReceiver(configDir, profileName) {
1280
1694
  try {
@@ -1434,7 +1848,7 @@ async function readClaimResponse(response) {
1434
1848
  const part = await reader.read();
1435
1849
  if (part.done) break;
1436
1850
  size += part.value.byteLength;
1437
- if (size > MAX_RESPONSE_BYTES$1) throw new Error();
1851
+ if (size > MAX_RESPONSE_BYTES) throw new Error();
1438
1852
  chunks.push(part.value);
1439
1853
  }
1440
1854
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
@@ -1490,7 +1904,8 @@ async function connectAgent(params) {
1490
1904
  };
1491
1905
  }
1492
1906
  privateMailDirectory(join(directory, "profiles"), true);
1493
- privateMailDirectory(agentProfileDirectory(params.configDir, profileName), true);
1907
+ const profileDirectory = agentProfileDirectory(params.configDir, profileName);
1908
+ privateMailDirectory(profileDirectory, true);
1494
1909
  const journal = join(directory, "claims", `${invitationHash}.json`);
1495
1910
  privateMailDirectory(join(directory, "claims"), true);
1496
1911
  if (readMailJson(journal) !== null) throw new AgentConnectionSetupError(UNCERTAIN_CLAIM);
@@ -1514,8 +1929,16 @@ async function connectAgent(params) {
1514
1929
  })
1515
1930
  });
1516
1931
  if (response.status !== 200) {
1517
- await response.body?.cancel();
1518
- throw new Error();
1932
+ const refusal = await definiteClaimRefusal(response);
1933
+ if (!refusal) {
1934
+ await response.body?.cancel().catch(() => void 0);
1935
+ throw new Error();
1936
+ }
1937
+ try {
1938
+ removeMailFile(journal);
1939
+ } catch {}
1940
+ removeEmptyDirectory(profileDirectory);
1941
+ throw refusal;
1519
1942
  }
1520
1943
  const profile = profileFromClaim(await readClaimResponse(response), invitation, invitationHash, params.now ?? Date.now);
1521
1944
  saveConnectedAgentProfile(params.configDir, profileName, profile);
@@ -1523,7 +1946,8 @@ async function connectAgent(params) {
1523
1946
  status: "claimed",
1524
1947
  identity: connectedAgentIdentity(profileName, profile)
1525
1948
  };
1526
- } catch {
1949
+ } catch (error) {
1950
+ if (error instanceof AgentInvitationRejectedError) throw error;
1527
1951
  throw new AgentConnectionSetupError(UNCERTAIN_CLAIM);
1528
1952
  }
1529
1953
  } finally {
@@ -2350,6 +2774,20 @@ const VERIFICATION_BACKOFF_MS = [
2350
2774
  3e3,
2351
2775
  5e3
2352
2776
  ];
2777
+ /**
2778
+ * Setup's receiving state for an external receiver: setup itself installs no
2779
+ * hooks, so the caller installs them and restates the outcome with
2780
+ * {@link externalReceivingState}.
2781
+ */
2782
+ const EXTERNAL_HOOKS_PENDING = "hooks_pending";
2783
+ /**
2784
+ * The printed receiving state for an external receiver, from the hook
2785
+ * install outcome. `hooks_installed` is a finished setup: idle wake is only
2786
+ * proven by real mail, which `externalHook: "installed_unverified"` says.
2787
+ */
2788
+ function externalReceivingState(hook) {
2789
+ return hook === "installed_unverified" ? "hooks_installed" : hook === "unavailable" ? "hook_unavailable" : hook === "held_for_other_profile" ? "held_for_other_profile" : "pending_verification";
2790
+ }
2353
2791
  /** Both states mean the setup reply was accepted for sending. */
2354
2792
  function verificationReplySubmitted(state) {
2355
2793
  return state === "reply_submitted" || state === "verified";
@@ -2697,10 +3135,11 @@ async function setupAgent(params) {
2697
3135
  if ((receiverMode ?? "native") === "native" && params.session) try {
2698
3136
  await dependencies.preflight(params.session);
2699
3137
  } catch {
2700
- throw fail("This exact session is not available for native receiving. No invitation was claimed. Open a supported session and retry setup.");
3138
+ throw fail("This exact session is not available for native receiving. No invitation was claimed. Open a supported session and retry setup, or rerun with --receiver poll, which needs no session socket.");
2701
3139
  }
2702
3140
  privateMailDirectory(directory, true);
2703
3141
  const release = acquireListenLock(directory, "setup");
3142
+ let discardStub = false;
2704
3143
  try {
2705
3144
  const path = join(directory, "setup.json");
2706
3145
  const value = readMailJson(path);
@@ -2712,6 +3151,7 @@ async function setupAgent(params) {
2712
3151
  const invitation = parseAgentInvitation(params.invitation ?? "");
2713
3152
  const hash = createHash("sha256").update(invitation.apiBaseUrl).update("\0").update(invitation.token).digest("hex");
2714
3153
  if (state && state.invitationHash !== hash) throw fail(profileAlreadyConnectedMessage(profileName, loadConnectedAgentProfile(params.configDir, profileName)?.agent_address ?? null));
3154
+ const created = !state;
2715
3155
  if (!state) {
2716
3156
  state = {
2717
3157
  version: 1,
@@ -2726,13 +3166,25 @@ async function setupAgent(params) {
2726
3166
  };
2727
3167
  writeMailJson(path, state);
2728
3168
  }
2729
- await connectAgent({
2730
- configDir: params.configDir,
2731
- profileName,
2732
- invitation: params.invitation ?? "",
2733
- fetch: params.fetch,
2734
- presence: true
2735
- });
3169
+ try {
3170
+ await connectAgent({
3171
+ configDir: params.configDir,
3172
+ profileName,
3173
+ invitation: params.invitation ?? "",
3174
+ fetch: params.fetch,
3175
+ presence: true
3176
+ });
3177
+ } catch (error) {
3178
+ if (error instanceof AgentInvitationRejectedError && created) {
3179
+ discardStub = true;
3180
+ try {
3181
+ removeMailFile(path);
3182
+ } catch {
3183
+ discardStub = false;
3184
+ }
3185
+ }
3186
+ throw error;
3187
+ }
2736
3188
  }
2737
3189
  const profile = loadConnectedAgentProfile(params.configDir, profileName);
2738
3190
  if (!profile || !state || profile.invitation_hash !== state.invitationHash) throw fail("The claim has no matching saved credential. Preserve private state and request a fresh invitation; do not replay this one.");
@@ -2817,93 +3269,13 @@ async function setupAgent(params) {
2817
3269
  const check = await awaitVerification(context, dependencies, verificationTimeoutMs);
2818
3270
  if (check.state === "verified") confirmed = { verifiedAt: check.verifiedAt };
2819
3271
  }
2820
- return result(state.receiverMode === "external" ? "external_setup_required" : state.receiverMode === "poll" ? "poll" : healthy ? "healthy" : "not_ready", ownerNotifications);
3272
+ return result(state.receiverMode === "external" ? EXTERNAL_HOOKS_PENDING : state.receiverMode === "poll" ? "poll" : healthy ? "healthy" : "not_ready", ownerNotifications);
2821
3273
  } finally {
2822
3274
  release();
3275
+ if (discardStub) removeEmptyDirectory(directory);
2823
3276
  }
2824
3277
  }
2825
3278
  //#endregion
2826
- //#region src/oclif/owner-member-address.ts
2827
- const REFRESH_TIMEOUT_MS = 5e3;
2828
- const MAX_RESPONSE_BYTES = 64 * 1024;
2829
- /** Reads at most MAX_RESPONSE_BYTES, stopping as soon as the body exceeds it. */
2830
- async function boundedText(response) {
2831
- const reader = response.body?.getReader();
2832
- if (!reader) return null;
2833
- const chunks = [];
2834
- let size = 0;
2835
- try {
2836
- for (;;) {
2837
- const part = await reader.read();
2838
- if (part.done) break;
2839
- size += part.value.byteLength;
2840
- if (size > MAX_RESPONSE_BYTES) return null;
2841
- chunks.push(part.value);
2842
- }
2843
- return Buffer.concat(chunks).toString("utf8");
2844
- } finally {
2845
- await reader.cancel().catch(() => void 0);
2846
- }
2847
- }
2848
- /**
2849
- * Reads the owner's personal mailbox from the connection's own record
2850
- * (GET /agent-connections/me) and saves it in the profile, so an agent paired
2851
- * before the server reported it, or before its owner set one up, learns where
2852
- * to send reports. Best effort: any failure keeps the saved value. Returns the
2853
- * address to report to, or null when there is none or it is unknown.
2854
- */
2855
- async function refreshOwnerMemberAddress(params) {
2856
- const profile = loadConnectedAgentProfile(params.configDir, params.profileName);
2857
- if (!profile) return null;
2858
- const saved = profile.owner_member_address ?? null;
2859
- try {
2860
- const response = await (params.fetch ?? fetch)(`${profile.api_base_url}/agent-connections/me`, {
2861
- headers: { authorization: `Bearer ${profile.api_key}` },
2862
- redirect: "error",
2863
- signal: AbortSignal.timeout(params.timeoutMs ?? REFRESH_TIMEOUT_MS)
2864
- });
2865
- if (!response.ok) return saved;
2866
- const text = await boundedText(response);
2867
- if (text === null) return saved;
2868
- const body = JSON.parse(text);
2869
- const connection = body?.data?.connection;
2870
- if (body?.success !== true || !connection || !Object.hasOwn(connection, "owner_member_address") || mailAddress(connection.address) !== profile.agent_address || mailAddress(connection.owner_address) !== profile.owner_address) return saved;
2871
- const current = parseOwnerMemberAddress(connection.owner_member_address, profile.owner_address, profile.agent_address);
2872
- if (profile.owner_member_address !== current) saveConnectedAgentProfile(params.configDir, params.profileName, {
2873
- ...profile,
2874
- owner_member_address: current
2875
- });
2876
- return current;
2877
- } catch {
2878
- return saved;
2879
- }
2880
- }
2881
- /** One sentence telling the agent where its reports go. */
2882
- function ownerReportGuidance(identity) {
2883
- return identity.ownerMemberAddress ? `Send reports and questions to your owner's personal address ${identity.ownerMemberAddress}; ${identity.ownerAddress} is the setup and presence control address and nobody reads it.` : `No personal owner address is known, so reply to the member who wrote to you; never send reports to ${identity.ownerAddress}, the setup and presence control address.`;
2884
- }
2885
- /**
2886
- * Adds the current personal address and report guidance to a result that
2887
- * carries a connected identity, refreshing the saved profile first.
2888
- */
2889
- async function withOwnerMemberAddress(result, params) {
2890
- const saved = loadConnectedAgentProfile(params.configDir, result.identity.profileName);
2891
- const ownerMemberAddress = params.onlyIfUnknown && typeof saved?.owner_member_address === "string" ? saved.owner_member_address : await refreshOwnerMemberAddress({
2892
- configDir: params.configDir,
2893
- profileName: result.identity.profileName,
2894
- fetch: params.fetch
2895
- });
2896
- const identity = {
2897
- ...result.identity,
2898
- ownerMemberAddress
2899
- };
2900
- return {
2901
- ...result,
2902
- identity,
2903
- ownerReportGuidance: ownerReportGuidance(identity)
2904
- };
2905
- }
2906
- //#endregion
2907
3279
  //#region src/oclif/agent-connect-flow.ts
2908
3280
  /** What this CLI version can do in one `agent connect --session` call. */
2909
3281
  const AGENT_CONNECT_CAPABILITIES = [
@@ -2921,6 +3293,51 @@ const MAX_NAME_LENGTH = 80;
2921
3293
  const MAX_INFO_LENGTH = 1e3;
2922
3294
  const MAIL_CHECK_WAIT_MS = 2e4;
2923
3295
  const PENDING_AGENT_INFO_FILE = "agent-info-pending.json";
3296
+ /**
3297
+ * The first Codex release whose shared app-server daemon exposes the local
3298
+ * session socket that native receiving connects to.
3299
+ */
3300
+ const CODEX_NATIVE_MIN_VERSION = "0.158.0";
3301
+ /** The `major.minor.patch` triple in a `codex --version` line, if any. */
3302
+ function parseCodexVersion(text) {
3303
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(text);
3304
+ return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
3305
+ }
3306
+ /** True when this Codex version has the session socket native receiving needs. */
3307
+ function codexSupportsNativeReceiving(version) {
3308
+ const have = version.split(".").map(Number);
3309
+ const need = CODEX_NATIVE_MIN_VERSION.split(".").map(Number);
3310
+ for (let index = 0; index < need.length; index++) {
3311
+ const a = have[index] ?? 0;
3312
+ const b = need[index] ?? 0;
3313
+ if (a !== b) return a > b;
3314
+ }
3315
+ return true;
3316
+ }
3317
+ /** True only when connecting to the socket is refused (nothing serves it). */
3318
+ function socketRefusesConnections(path, timeoutMs = 2e3) {
3319
+ return new Promise((resolve) => {
3320
+ const socket = createConnection({ path });
3321
+ const done = (refused) => {
3322
+ socket.destroy();
3323
+ resolve(refused);
3324
+ };
3325
+ socket.setTimeout(timeoutMs, () => done(false));
3326
+ socket.once("connect", () => done(false));
3327
+ socket.once("error", (error) => done(error.code === "ECONNREFUSED"));
3328
+ });
3329
+ }
3330
+ async function installedCodexVersion() {
3331
+ try {
3332
+ const { stdout } = await promisify(execFile)("codex", ["--version"], {
3333
+ timeout: 3e3,
3334
+ maxBuffer: 4096
3335
+ });
3336
+ return parseCodexVersion(String(stdout));
3337
+ } catch {
3338
+ return null;
3339
+ }
3340
+ }
2924
3341
  function readPendingAgentInfo(path) {
2925
3342
  try {
2926
3343
  const saved = readMailJson(path);
@@ -2958,6 +3375,23 @@ function clearPendingAgentInfo(path) {
2958
3375
  removeMailFile(path);
2959
3376
  } catch {}
2960
3377
  }
3378
+ /**
3379
+ * Warns when the connection's own record says the owner has no personal
3380
+ * address in the organization. An unknown value (an older server, or a
3381
+ * failed read) warns nothing.
3382
+ */
3383
+ function connectWarnings(configDir, profileName) {
3384
+ let profile = null;
3385
+ try {
3386
+ profile = loadConnectedAgentProfile(configDir, profileName);
3387
+ } catch {
3388
+ return [];
3389
+ }
3390
+ return profile?.owner_member_address === null ? [{
3391
+ kind: "owner_member_address_missing",
3392
+ message: "Your owner has no personal address in this organization, so mail they send from their own address will not wake this agent and reports have nowhere to go. Ask them to open the Primitive app once (it sets one), or to run `primitive account provision-member-address --address <their address>` signed in as themselves."
3393
+ }] : [];
3394
+ }
2961
3395
  function defaultAgentProfileName(session) {
2962
3396
  return `session-${session.toLowerCase()}`;
2963
3397
  }
@@ -2995,6 +3429,9 @@ function defaults(options) {
2995
3429
  return {
2996
3430
  setupAgent,
2997
3431
  nativePreflight: nativeSessionPreflight,
3432
+ nativeSocketPresent: () => existsSync(defaultSessionSocket()),
3433
+ nativeSocketRefuses: () => socketRefusesConnections(defaultSessionSocket()),
3434
+ codexVersion: installedCodexVersion,
2998
3435
  installClaudeWakeHook,
2999
3436
  installSkill(runtime) {
3000
3437
  return installConnectSkill({
@@ -3070,7 +3507,7 @@ async function runAgentConnect(options) {
3070
3507
  const runtime = session ? detectAgentRuntime(session, env) : null;
3071
3508
  const invitation = options.resume || session || options.profileName ? void 0 : await options.readInvitation();
3072
3509
  const profileName = agentProfileName(options.profileName ?? (session ? defaultAgentProfileName(session) : invitationProfileName(invitation ?? "")));
3073
- const receiver = options.receiver ?? (options.resume ? savedSetup(options.configDir, profileName)?.receiverMode ?? null : null) ?? (!session ? "poll" : runtime === "claude" ? "external" : "native");
3510
+ let receiver = options.receiver ?? (options.resume ? savedSetup(options.configDir, profileName)?.receiverMode ?? null : null) ?? (!session ? "poll" : runtime === "claude" ? "external" : "native");
3074
3511
  if (receiver === "external" && env.CLAUDE_CODE_SESSION_ID?.trim().toLowerCase() !== session?.toLowerCase()) throw new AgentConnectionSetupError("External receiving requires this exact Claude session ID. No invitation was claimed.");
3075
3512
  const agentInfo = agentInfoValue(options.name, options.info);
3076
3513
  const skipped = [];
@@ -3105,15 +3542,31 @@ async function runAgentConnect(options) {
3105
3542
  reason: skill.reason ?? skill.state
3106
3543
  });
3107
3544
  const startedAt = Date.now();
3108
- const setupInvitation = options.resume ? void 0 : invitation ?? await options.readInvitation();
3109
- if (options.beforeSetup) {
3110
- if (receiver === "native" && session) try {
3111
- await dependencies.nativePreflight(session);
3112
- } catch {
3113
- throw new AgentConnectionSetupError("This exact session is not available for native receiving. No invitation was claimed and nothing was changed. Open a supported session and retry setup.");
3114
- }
3115
- await options.beforeSetup();
3545
+ const invocation = options.invocation ?? "primitive";
3546
+ let fallback = null;
3547
+ if (receiver === "native" && session && !options.resume) try {
3548
+ await dependencies.nativePreflight(session);
3549
+ } catch (error) {
3550
+ const socketPresent = dependencies.nativeSocketPresent();
3551
+ const socketDead = socketPresent && error instanceof NativeSessionDisconnectedError && await dependencies.nativeSocketRefuses();
3552
+ const codexVersion = socketPresent && !socketDead || runtime === "claude" ? null : await dependencies.codexVersion();
3553
+ const reason = socketDead ? "session_socket_unavailable" : socketPresent ? null : codexVersion !== null && !codexSupportsNativeReceiving(codexVersion) ? "codex_version_unsupported" : "session_socket_missing";
3554
+ const why = reason === "codex_version_unsupported" ? `Native receiving needs Codex ${CODEX_NATIVE_MIN_VERSION} or newer; this machine has Codex ${codexVersion}.` : reason === "session_socket_unavailable" ? "The local Codex session socket exists but nothing accepted the connection, so its app-server is not running." : reason === "session_socket_missing" ? `Native receiving needs the local Codex session socket (Codex ${CODEX_NATIVE_MIN_VERSION} or newer with its shared app-server running), and none was found${codexVersion ? ` for Codex ${codexVersion}` : ""}.` : `The local session socket exists but did not accept session ${session}: it may not be loaded in this terminal, or the ID may not be this session's.`;
3555
+ if (reason && options.receiver === void 0) {
3556
+ receiver = "poll";
3557
+ fallback = {
3558
+ reason,
3559
+ codexVersion,
3560
+ detail: `${why} Connected with poll receiving instead: nothing wakes this session, so check for mail with receiving.checkCommand at the start of each turn and after sending.`
3561
+ };
3562
+ } else throw new AgentConnectionSetupError(`This exact session is not available for native receiving. ${why} No invitation was claimed and nothing was changed. To connect with poll receiving instead, pipe the same invitation to: ${invocation} agent connect --session ${session} --receiver poll --json`);
3116
3563
  }
3564
+ if (fallback) skipped.push({
3565
+ step: "native_receiver",
3566
+ reason: fallback.reason
3567
+ });
3568
+ const setupInvitation = options.resume ? void 0 : invitation ?? await options.readInvitation();
3569
+ if (options.beforeSetup) await options.beforeSetup();
3117
3570
  const result = await dependencies.setupAgent({
3118
3571
  configDir: options.configDir,
3119
3572
  profileName,
@@ -3129,6 +3582,7 @@ async function runAgentConnect(options) {
3129
3582
  ...result.identity,
3130
3583
  ownerMemberAddress
3131
3584
  };
3585
+ const warnings = connectWarnings(options.configDir, result.identity.profileName);
3132
3586
  let externalHook = null;
3133
3587
  if (receiver === "external") if (verified) externalHook = dependencies.installClaudeWakeHook({
3134
3588
  cliPath: options.cliPath,
@@ -3181,15 +3635,20 @@ async function runAgentConnect(options) {
3181
3635
  reason: "already_present"
3182
3636
  });
3183
3637
  }
3184
- const invocation = options.invocation ?? "primitive";
3185
3638
  const receiving = receiver === "external" ? {
3186
3639
  ...result.receiving,
3640
+ state: externalReceivingState(externalHook),
3187
3641
  mode: "external",
3188
3642
  hook: externalHook
3189
3643
  } : receiver === "poll" ? {
3190
3644
  ...result.receiving,
3191
3645
  mode: "poll",
3192
- checkCommand: pollCheckCommand(invocation, result.identity.profileName)
3646
+ checkCommand: pollCheckCommand(invocation, result.identity.profileName),
3647
+ ...fallback ? {
3648
+ fallbackReason: fallback.reason,
3649
+ fallbackDetail: fallback.detail,
3650
+ codexVersion: fallback.codexVersion
3651
+ } : {}
3193
3652
  } : {
3194
3653
  ...result.receiving,
3195
3654
  mode: "native",
@@ -3228,6 +3687,7 @@ async function runAgentConnect(options) {
3228
3687
  ...result.ownerNotifications ? { ownerNotifications: result.ownerNotifications } : {},
3229
3688
  agentInfo: agentInfoState,
3230
3689
  skipped,
3690
+ warnings,
3231
3691
  selectProfile: `PRIMITIVE_AGENT_PROFILE=${result.identity.profileName}`,
3232
3692
  resumeCommand,
3233
3693
  guidance: `${result.guidance} ${ownerReportGuidance(identity)}`,
@@ -4751,4 +5211,4 @@ async function runDetached(params) {
4751
5211
  }
4752
5212
  }
4753
5213
  //#endregion
4754
- export { describeSessionHookItem as $, NetworkAdmissionPendingError as A, runContactRequest as B, pollCheckCommand as C, verificationReplySubmitted as D, RECEIVER_MODES as E, addressNoteTarget as F, receiverStatusCommand as G, agentInvitationHash as H, runAddressNotesRequest as I, recordHookMailCheck as J, savedReceiver as K, ContactsApiError as L, evaluateContactPolicy as M, canonicalContactSelector as N, ContactPolicyReadRetryError as O, AddressNotesApiError as P, describeSessionHookChange as Q, contactAddress as R, invitationProfileName as S, withOwnerMemberAddress as T, connectAgent as U, agentConnectionStatus as V, readAgentInvitation as W, claudeConfigDir as X, boundSessionProfiles as Y, currentCliLocation as Z, disconnectAgent as _, endSession as a, samePath as at, identitySuggestions as b, pendingSessionDisconnects as c, inspectManagedBlock as ct, registerSession as d, upsertManagedBlock as dt, ephemeralCliReason as et, retryPendingDisconnects as f, writeManagedFile as ft, AgentDisconnectError as g, enrollmentResumes as h, connectedProfilesForSession as i, repairClaudeHooks as it, createNotificationContactPolicy as j, NETWORK_ADMISSION_PENDING_RETRY_MS as k, psProcessInfo as l, jsonIndentation as lt, enrollAgent as m, OMP_MAIN as n, inspectClaudeHooks as nt, headlessClaudeRun as o, writeClaudeSettings as ot, runDetached as p, installClaudeWakeHook as q, agentConnectionState as r, readClaudeSettings as rt, machine_session_exports as s, MachineFileError as st, MACHINE_RUNTIMES as t, findOnPath as tt, readClaudeHookInput as u, readManagedFile as ut, cliInvocation as v, runAgentConnect as w, defaultAgentProfileName as x, generatedAgentName as y, contactAgentAddress as z };
5214
+ export { currentCliLocation as $, NETWORK_ADMISSION_PENDING_RETRY_MS as A, contactAgentAddress as B, invitationProfileName as C, externalReceivingState as D, RECEIVER_MODES as E, AddressNotesApiError as F, readAgentInvitation as G, agentConnectionStatus as H, addressNoteTarget as I, savedReceiver as J, receiverStatusCommand as K, runAddressNotesRequest as L, createNotificationContactPolicy as M, evaluateContactPolicy as N, verificationReplySubmitted as O, canonicalContactSelector as P, claudeConfigDir as Q, ContactsApiError as R, defaultAgentProfileName as S, runAgentConnect as T, agentInvitationHash as U, runContactRequest as V, connectAgent as W, recordHookMailCheck as X, installClaudeWakeHook as Y, boundSessionProfiles as Z, disconnectAgent as _, readHookLauncherWarning as _t, endSession as a, hasMachineSessionHooks as at, identitySuggestions as b, pendingSessionDisconnects as c, repairClaudeHooks as ct, registerSession as d, MachineFileError as dt, describeSessionHookChange as et, retryPendingDisconnects as f, inspectManagedBlock as ft, AgentDisconnectError as g, writeManagedFile as gt, enrollmentResumes as h, upsertManagedBlock as ht, connectedProfilesForSession as i, findOnPath as it, NetworkAdmissionPendingError as j, ContactPolicyReadRetryError as k, psProcessInfo as l, samePath as lt, enrollAgent as m, readManagedFile as mt, OMP_MAIN as n, ensureHookLauncher as nt, headlessClaudeRun as o, inspectClaudeHooks as ot, runDetached as p, jsonIndentation as pt, refusalAfterReplacement as q, agentConnectionState as r, ephemeralCliReason as rt, machine_session_exports as s, readClaudeSettings as st, MACHINE_RUNTIMES as t, describeSessionHookItem as tt, readClaudeHookInput as u, writeClaudeSettings as ut, cliInvocation as v, refreshOwnerMemberAddressPeriodically as vt, pollCheckCommand as w, connectWarnings as x, generatedAgentName as y, withOwnerMemberAddress as yt, contactAddress as z };