primcli 1.48.0 → 1.50.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.
- package/README.md +4 -0
- package/bin/claude-wake.mjs +1 -1
- package/dist/{api-command-DzQ8KxD9.js → api-command-Ci6eyQ8d.js} +142 -7
- package/dist/{connect-skill-DHPc9dVN.js → connect-skill-xtf3Q5IF.js} +4 -3
- package/dist/{machine-session-BaJ1Xmdm.js → machine-session-CY0j2r7S.js} +678 -173
- package/dist/oclif/auto-signal-worker.js +2 -2
- package/dist/oclif/auto-signals.js +1 -1
- package/dist/oclif/index.js +317 -48
- package/dist/oclif/listen-supervisor.js +1 -1
- package/dist/oclif/receiver-heal.js +2 -2
- package/dist/{scoped-chat-DhxtuNXs.js → scoped-chat-DSLrmgWQ.js} +1 -1
- package/dist/{signal-command-BbeV2Nd8.js → signal-command-D9R7d44k.js} +4 -4
- package/dist/skills/primitive-connect/SKILL.md +19 -267
- package/dist/skills/primitive-connect/references/communication.md +7 -0
- package/dist/skills/primitive-connect/references/manual-setup.md +294 -0
- package/dist/skills/primitive-connect.json +5 -4
- package/package.json +1 -1
- /package/dist/{listen-background-CyLc8VBn.js → listen-background-CMfxkeVP.js} +0 -0
- /package/dist/{repeats-D2xtLium-DdiJECLI.js → repeats-D2xtLium-B8Wq41vG.js} +0 -0
- /package/dist/{shared-mail-watch-BUfUaq6j.js → shared-mail-watch-CWhpfJ0N.js} +0 -0
|
@@ -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 {
|
|
3
|
+
import { An as listDomains, Dn as listContacts, Gn as putContact, H as defaultConnectionName, Ln as listSentEmails, Qn as searchEmails, Tn as listAgentContacts, Un as putAgentContact, Wn as putAgentContactPolicy, Wt as refreshStoredCliCredentials, ln as getAgentContactPolicy, mn as getEmail, nn as deleteContact, tn as deleteAgentContact, tr as sendEmail, un as getContact } from "./api-command-Ci6eyQ8d.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
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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-xtf3Q5IF.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
|
-
/**
|
|
300
|
-
|
|
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:
|
|
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
|
-
/**
|
|
692
|
-
|
|
693
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,121 @@ 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
|
|
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_ATTEMPTED_HERE = "An earlier setup on this machine already submitted this invitation and did not finish, so it was not submitted again; it may already be used. Ask the owner to copy a fresh setup instruction from the app. Nothing was changed on this machine.";
|
|
1640
|
+
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.";
|
|
1641
|
+
/**
|
|
1642
|
+
* The server answered the claim with a definite refusal, so nothing was
|
|
1643
|
+
* claimed and no credential exists. Unlike a lost or malformed response, it
|
|
1644
|
+
* is safe to discard every local trace of the attempt.
|
|
1645
|
+
*/
|
|
1646
|
+
var AgentInvitationRejectedError = class extends AgentConnectionSetupError {
|
|
1647
|
+
constructor(message, reason) {
|
|
1648
|
+
super(message);
|
|
1649
|
+
this.reason = reason;
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
const NOTHING_CHANGED = "Nothing was changed on this machine.";
|
|
1653
|
+
/**
|
|
1654
|
+
* Restates a refusal that followed a replacement: the agent being replaced
|
|
1655
|
+
* was disconnected before the claim, so "nothing was changed" is not true.
|
|
1656
|
+
*/
|
|
1657
|
+
function refusalAfterReplacement(error, replaced) {
|
|
1658
|
+
if (!(error instanceof AgentInvitationRejectedError) || !replaced.length) return error;
|
|
1659
|
+
const names = replaced.map((row) => row.address).join(", ");
|
|
1660
|
+
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);
|
|
1661
|
+
}
|
|
1662
|
+
/** The error code of a refused claim, read without keeping the body. */
|
|
1663
|
+
async function claimErrorCode(response) {
|
|
1664
|
+
try {
|
|
1665
|
+
const body = await readClaimResponse(response);
|
|
1666
|
+
return body?.success === false && typeof body.error?.code === "string" ? body.error.code : null;
|
|
1667
|
+
} catch {
|
|
1668
|
+
return null;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
/** A definite refusal of the claim, or null when the outcome is uncertain. */
|
|
1672
|
+
async function definiteClaimRefusal(response) {
|
|
1673
|
+
if (![
|
|
1674
|
+
400,
|
|
1675
|
+
409,
|
|
1676
|
+
429
|
|
1677
|
+
].includes(response.status)) return null;
|
|
1678
|
+
const code = await claimErrorCode(response);
|
|
1679
|
+
if (response.status === 409 && code === "connection_invitation_unavailable") return new AgentInvitationRejectedError(INVITATION_UNAVAILABLE, "invitation_unavailable");
|
|
1680
|
+
if (response.status === 400 && code === "validation_error") return new AgentInvitationRejectedError(INVITATION_INVALID, "invitation_invalid");
|
|
1681
|
+
if (response.status === 429 && code === "rate_limit_exceeded") {
|
|
1682
|
+
const seconds = Number(response.headers.get("retry-after"));
|
|
1683
|
+
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");
|
|
1684
|
+
}
|
|
1685
|
+
return null;
|
|
1686
|
+
}
|
|
1687
|
+
/** The message for an invitation another profile on this machine already claimed. */
|
|
1688
|
+
function invitationUsedHereMessage(address) {
|
|
1689
|
+
return `This invitation was already used on this machine${address ? ` to connect ${address}` : ""} under another profile, so it cannot connect this agent. Ask the owner to copy a fresh setup instruction from the app. Nothing was changed on this machine.`;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* A local record says this invitation was submitted before. It is never
|
|
1693
|
+
* submitted again: the claim is one-use, and a second submission could not
|
|
1694
|
+
* recover a credential lost with an earlier response. The refusal names the
|
|
1695
|
+
* address when the earlier attempt finished, and is a definite local refusal
|
|
1696
|
+
* either way, so a caller may discard whatever it created for this run.
|
|
1697
|
+
*/
|
|
1698
|
+
function earlierAttemptRefusal(configDir, journal, invitationHash) {
|
|
1699
|
+
const record = journal;
|
|
1700
|
+
let address = null;
|
|
1701
|
+
if (record?.status === "claimed" && typeof record.agent_address === "string") address = record.agent_address;
|
|
1702
|
+
else if (typeof record?.profile_name === "string") try {
|
|
1703
|
+
const earlier = loadConnectedAgentProfile(configDir, record.profile_name);
|
|
1704
|
+
if (earlier?.invitation_hash === invitationHash) address = earlier.agent_address;
|
|
1705
|
+
} catch {}
|
|
1706
|
+
return address !== null || record?.status === "claimed" ? new AgentInvitationRejectedError(invitationUsedHereMessage(address), "invitation_used_here") : new AgentInvitationRejectedError(INVITATION_ATTEMPTED_HERE, "invitation_attempted_here");
|
|
1707
|
+
}
|
|
1708
|
+
/** Removes a directory only when it is empty. Best effort. */
|
|
1709
|
+
function removeEmptyDirectory(path) {
|
|
1710
|
+
try {
|
|
1711
|
+
rmdirSync(path);
|
|
1712
|
+
} catch {}
|
|
1713
|
+
}
|
|
1714
|
+
const CONNECTION_NAME_FILE = "connection-name.json";
|
|
1715
|
+
/**
|
|
1716
|
+
* The display name this profile's claim reported, saved beside the profile
|
|
1717
|
+
* so a resumed setup can report it too. Undefined when none was saved.
|
|
1718
|
+
*/
|
|
1719
|
+
function savedConnectionName(configDir, profileName) {
|
|
1720
|
+
try {
|
|
1721
|
+
const saved = readMailJson(join(agentProfileDirectory(configDir, profileName), CONNECTION_NAME_FILE));
|
|
1722
|
+
return saved?.version === 1 ? claimedConnectionName({ data: { connection: { name: saved.name } } }) : void 0;
|
|
1723
|
+
} catch {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Saves the connection's current display name beside its profile: from the
|
|
1729
|
+
* claim, and again after `agent rename`. Best effort: without it a resume
|
|
1730
|
+
* omits the name, as for a claim made by an older version.
|
|
1731
|
+
*/
|
|
1732
|
+
function saveConnectionName(configDir, profileName, name) {
|
|
1733
|
+
try {
|
|
1734
|
+
writeMailJson(join(agentProfileDirectory(configDir, profileName), CONNECTION_NAME_FILE), {
|
|
1735
|
+
version: 1,
|
|
1736
|
+
name
|
|
1737
|
+
});
|
|
1738
|
+
} catch {}
|
|
1739
|
+
}
|
|
1740
|
+
/** The display name a claim response reports, or undefined when unusable. */
|
|
1741
|
+
function claimedConnectionName(value) {
|
|
1742
|
+
const name = value?.data?.connection?.name;
|
|
1743
|
+
if (typeof name !== "string") return void 0;
|
|
1744
|
+
const trimmed = name.trim();
|
|
1745
|
+
const printable = Array.from(trimmed).every((character) => {
|
|
1746
|
+
const code = character.charCodeAt(0);
|
|
1747
|
+
return code >= 32 && !(code >= 127 && code <= 159) && code !== 8232 && code !== 8233;
|
|
1748
|
+
});
|
|
1749
|
+
return trimmed.length > 0 && trimmed.length <= 80 && printable ? trimmed : void 0;
|
|
1750
|
+
}
|
|
1278
1751
|
/** The receiver a saved profile's setup record names. Offline. */
|
|
1279
1752
|
function savedReceiver(configDir, profileName) {
|
|
1280
1753
|
try {
|
|
@@ -1434,7 +1907,7 @@ async function readClaimResponse(response) {
|
|
|
1434
1907
|
const part = await reader.read();
|
|
1435
1908
|
if (part.done) break;
|
|
1436
1909
|
size += part.value.byteLength;
|
|
1437
|
-
if (size > MAX_RESPONSE_BYTES
|
|
1910
|
+
if (size > MAX_RESPONSE_BYTES) throw new Error();
|
|
1438
1911
|
chunks.push(part.value);
|
|
1439
1912
|
}
|
|
1440
1913
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
@@ -1489,11 +1962,18 @@ async function connectAgent(params) {
|
|
|
1489
1962
|
identity: connectedAgentIdentity(profileName, existing)
|
|
1490
1963
|
};
|
|
1491
1964
|
}
|
|
1492
|
-
privateMailDirectory(join(directory, "profiles"), true);
|
|
1493
|
-
privateMailDirectory(agentProfileDirectory(params.configDir, profileName), true);
|
|
1494
1965
|
const journal = join(directory, "claims", `${invitationHash}.json`);
|
|
1495
1966
|
privateMailDirectory(join(directory, "claims"), true);
|
|
1496
|
-
|
|
1967
|
+
let earlier;
|
|
1968
|
+
try {
|
|
1969
|
+
earlier = readMailJson(journal);
|
|
1970
|
+
} catch {
|
|
1971
|
+
throw new AgentConnectionSetupError(UNCERTAIN_CLAIM);
|
|
1972
|
+
}
|
|
1973
|
+
if (earlier !== null) throw earlierAttemptRefusal(params.configDir, earlier, invitationHash);
|
|
1974
|
+
privateMailDirectory(join(directory, "profiles"), true);
|
|
1975
|
+
const profileDirectory = agentProfileDirectory(params.configDir, profileName);
|
|
1976
|
+
privateMailDirectory(profileDirectory, true);
|
|
1497
1977
|
writeMailJson(journal, {
|
|
1498
1978
|
version: 1,
|
|
1499
1979
|
profile_name: profileName,
|
|
@@ -1514,16 +1994,37 @@ async function connectAgent(params) {
|
|
|
1514
1994
|
})
|
|
1515
1995
|
});
|
|
1516
1996
|
if (response.status !== 200) {
|
|
1517
|
-
await response
|
|
1518
|
-
|
|
1997
|
+
const refusal = await definiteClaimRefusal(response);
|
|
1998
|
+
if (!refusal) {
|
|
1999
|
+
await response.body?.cancel().catch(() => void 0);
|
|
2000
|
+
throw new Error();
|
|
2001
|
+
}
|
|
2002
|
+
try {
|
|
2003
|
+
removeMailFile(journal);
|
|
2004
|
+
} catch {}
|
|
2005
|
+
removeEmptyDirectory(profileDirectory);
|
|
2006
|
+
throw refusal;
|
|
1519
2007
|
}
|
|
1520
|
-
const
|
|
2008
|
+
const body = await readClaimResponse(response);
|
|
2009
|
+
const profile = profileFromClaim(body, invitation, invitationHash, params.now ?? Date.now);
|
|
2010
|
+
const name = claimedConnectionName(body);
|
|
1521
2011
|
saveConnectedAgentProfile(params.configDir, profileName, profile);
|
|
2012
|
+
try {
|
|
2013
|
+
writeMailJson(journal, {
|
|
2014
|
+
version: 1,
|
|
2015
|
+
profile_name: profileName,
|
|
2016
|
+
status: "claimed",
|
|
2017
|
+
agent_address: profile.agent_address
|
|
2018
|
+
});
|
|
2019
|
+
} catch {}
|
|
2020
|
+
if (name !== void 0) saveConnectionName(params.configDir, profileName, name);
|
|
1522
2021
|
return {
|
|
1523
2022
|
status: "claimed",
|
|
1524
|
-
identity: connectedAgentIdentity(profileName, profile)
|
|
2023
|
+
identity: connectedAgentIdentity(profileName, profile),
|
|
2024
|
+
...name === void 0 ? {} : { name }
|
|
1525
2025
|
};
|
|
1526
|
-
} catch {
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
if (error instanceof AgentInvitationRejectedError) throw error;
|
|
1527
2028
|
throw new AgentConnectionSetupError(UNCERTAIN_CLAIM);
|
|
1528
2029
|
}
|
|
1529
2030
|
} finally {
|
|
@@ -2350,6 +2851,20 @@ const VERIFICATION_BACKOFF_MS = [
|
|
|
2350
2851
|
3e3,
|
|
2351
2852
|
5e3
|
|
2352
2853
|
];
|
|
2854
|
+
/**
|
|
2855
|
+
* Setup's receiving state for an external receiver: setup itself installs no
|
|
2856
|
+
* hooks, so the caller installs them and restates the outcome with
|
|
2857
|
+
* {@link externalReceivingState}.
|
|
2858
|
+
*/
|
|
2859
|
+
const EXTERNAL_HOOKS_PENDING = "hooks_pending";
|
|
2860
|
+
/**
|
|
2861
|
+
* The printed receiving state for an external receiver, from the hook
|
|
2862
|
+
* install outcome. `hooks_installed` is a finished setup: idle wake is only
|
|
2863
|
+
* proven by real mail, which `externalHook: "installed_unverified"` says.
|
|
2864
|
+
*/
|
|
2865
|
+
function externalReceivingState(hook) {
|
|
2866
|
+
return hook === "installed_unverified" ? "hooks_installed" : hook === "unavailable" ? "hook_unavailable" : hook === "held_for_other_profile" ? "held_for_other_profile" : "pending_verification";
|
|
2867
|
+
}
|
|
2353
2868
|
/** Both states mean the setup reply was accepted for sending. */
|
|
2354
2869
|
function verificationReplySubmitted(state) {
|
|
2355
2870
|
return state === "reply_submitted" || state === "verified";
|
|
@@ -2697,10 +3212,12 @@ async function setupAgent(params) {
|
|
|
2697
3212
|
if ((receiverMode ?? "native") === "native" && params.session) try {
|
|
2698
3213
|
await dependencies.preflight(params.session);
|
|
2699
3214
|
} catch {
|
|
2700
|
-
throw fail("This exact session is not available for native receiving. No invitation was claimed. Open a supported session and retry setup.");
|
|
3215
|
+
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
3216
|
}
|
|
2702
3217
|
privateMailDirectory(directory, true);
|
|
2703
3218
|
const release = acquireListenLock(directory, "setup");
|
|
3219
|
+
let discardStub = false;
|
|
3220
|
+
let claimedName;
|
|
2704
3221
|
try {
|
|
2705
3222
|
const path = join(directory, "setup.json");
|
|
2706
3223
|
const value = readMailJson(path);
|
|
@@ -2712,6 +3229,7 @@ async function setupAgent(params) {
|
|
|
2712
3229
|
const invitation = parseAgentInvitation(params.invitation ?? "");
|
|
2713
3230
|
const hash = createHash("sha256").update(invitation.apiBaseUrl).update("\0").update(invitation.token).digest("hex");
|
|
2714
3231
|
if (state && state.invitationHash !== hash) throw fail(profileAlreadyConnectedMessage(profileName, loadConnectedAgentProfile(params.configDir, profileName)?.agent_address ?? null));
|
|
3232
|
+
const created = !state;
|
|
2715
3233
|
if (!state) {
|
|
2716
3234
|
state = {
|
|
2717
3235
|
version: 1,
|
|
@@ -2726,13 +3244,25 @@ async function setupAgent(params) {
|
|
|
2726
3244
|
};
|
|
2727
3245
|
writeMailJson(path, state);
|
|
2728
3246
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
3247
|
+
try {
|
|
3248
|
+
claimedName = (await connectAgent({
|
|
3249
|
+
configDir: params.configDir,
|
|
3250
|
+
profileName,
|
|
3251
|
+
invitation: params.invitation ?? "",
|
|
3252
|
+
fetch: params.fetch,
|
|
3253
|
+
presence: true
|
|
3254
|
+
})).name;
|
|
3255
|
+
} catch (error) {
|
|
3256
|
+
if (error instanceof AgentInvitationRejectedError && created) {
|
|
3257
|
+
discardStub = true;
|
|
3258
|
+
try {
|
|
3259
|
+
removeMailFile(path);
|
|
3260
|
+
} catch {
|
|
3261
|
+
discardStub = false;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
throw error;
|
|
3265
|
+
}
|
|
2736
3266
|
}
|
|
2737
3267
|
const profile = loadConnectedAgentProfile(params.configDir, profileName);
|
|
2738
3268
|
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.");
|
|
@@ -2763,8 +3293,10 @@ async function setupAgent(params) {
|
|
|
2763
3293
|
"bounced",
|
|
2764
3294
|
"canceled"
|
|
2765
3295
|
].includes(state.receipt?.status ?? "") ? "reply_failed" : ["unknown", "wait_timeout"].includes(state.receipt?.status ?? "") ? "delivery_unknown" : "reply_submitted" : state?.phase === "sending" ? "send_unknown" : "challenge_pending";
|
|
3296
|
+
claimedName ??= savedConnectionName(params.configDir, profileName);
|
|
2766
3297
|
const result = (receiving, ownerNotifications) => ({
|
|
2767
3298
|
identity: context.identity,
|
|
3299
|
+
...claimedName === void 0 ? {} : { connectionName: claimedName },
|
|
2768
3300
|
sessionId: params.session ?? state?.session ?? null,
|
|
2769
3301
|
verification: {
|
|
2770
3302
|
state: confirmed && submittedState() === "reply_submitted" ? "verified" : submittedState(),
|
|
@@ -2817,93 +3349,13 @@ async function setupAgent(params) {
|
|
|
2817
3349
|
const check = await awaitVerification(context, dependencies, verificationTimeoutMs);
|
|
2818
3350
|
if (check.state === "verified") confirmed = { verifiedAt: check.verifiedAt };
|
|
2819
3351
|
}
|
|
2820
|
-
return result(state.receiverMode === "external" ?
|
|
3352
|
+
return result(state.receiverMode === "external" ? EXTERNAL_HOOKS_PENDING : state.receiverMode === "poll" ? "poll" : healthy ? "healthy" : "not_ready", ownerNotifications);
|
|
2821
3353
|
} finally {
|
|
2822
3354
|
release();
|
|
3355
|
+
if (discardStub) removeEmptyDirectory(directory);
|
|
2823
3356
|
}
|
|
2824
3357
|
}
|
|
2825
3358
|
//#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
3359
|
//#region src/oclif/agent-connect-flow.ts
|
|
2908
3360
|
/** What this CLI version can do in one `agent connect --session` call. */
|
|
2909
3361
|
const AGENT_CONNECT_CAPABILITIES = [
|
|
@@ -2921,6 +3373,51 @@ const MAX_NAME_LENGTH = 80;
|
|
|
2921
3373
|
const MAX_INFO_LENGTH = 1e3;
|
|
2922
3374
|
const MAIL_CHECK_WAIT_MS = 2e4;
|
|
2923
3375
|
const PENDING_AGENT_INFO_FILE = "agent-info-pending.json";
|
|
3376
|
+
/**
|
|
3377
|
+
* The first Codex release whose shared app-server daemon exposes the local
|
|
3378
|
+
* session socket that native receiving connects to.
|
|
3379
|
+
*/
|
|
3380
|
+
const CODEX_NATIVE_MIN_VERSION = "0.158.0";
|
|
3381
|
+
/** The `major.minor.patch` triple in a `codex --version` line, if any. */
|
|
3382
|
+
function parseCodexVersion(text) {
|
|
3383
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
3384
|
+
return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
|
|
3385
|
+
}
|
|
3386
|
+
/** True when this Codex version has the session socket native receiving needs. */
|
|
3387
|
+
function codexSupportsNativeReceiving(version) {
|
|
3388
|
+
const have = version.split(".").map(Number);
|
|
3389
|
+
const need = CODEX_NATIVE_MIN_VERSION.split(".").map(Number);
|
|
3390
|
+
for (let index = 0; index < need.length; index++) {
|
|
3391
|
+
const a = have[index] ?? 0;
|
|
3392
|
+
const b = need[index] ?? 0;
|
|
3393
|
+
if (a !== b) return a > b;
|
|
3394
|
+
}
|
|
3395
|
+
return true;
|
|
3396
|
+
}
|
|
3397
|
+
/** True only when connecting to the socket is refused (nothing serves it). */
|
|
3398
|
+
function socketRefusesConnections(path, timeoutMs = 2e3) {
|
|
3399
|
+
return new Promise((resolve) => {
|
|
3400
|
+
const socket = createConnection({ path });
|
|
3401
|
+
const done = (refused) => {
|
|
3402
|
+
socket.destroy();
|
|
3403
|
+
resolve(refused);
|
|
3404
|
+
};
|
|
3405
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
3406
|
+
socket.once("connect", () => done(false));
|
|
3407
|
+
socket.once("error", (error) => done(error.code === "ECONNREFUSED"));
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
async function installedCodexVersion() {
|
|
3411
|
+
try {
|
|
3412
|
+
const { stdout } = await promisify(execFile)("codex", ["--version"], {
|
|
3413
|
+
timeout: 3e3,
|
|
3414
|
+
maxBuffer: 4096
|
|
3415
|
+
});
|
|
3416
|
+
return parseCodexVersion(String(stdout));
|
|
3417
|
+
} catch {
|
|
3418
|
+
return null;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
2924
3421
|
function readPendingAgentInfo(path) {
|
|
2925
3422
|
try {
|
|
2926
3423
|
const saved = readMailJson(path);
|
|
@@ -2958,6 +3455,23 @@ function clearPendingAgentInfo(path) {
|
|
|
2958
3455
|
removeMailFile(path);
|
|
2959
3456
|
} catch {}
|
|
2960
3457
|
}
|
|
3458
|
+
/**
|
|
3459
|
+
* Warns when the connection's own record says the owner has no personal
|
|
3460
|
+
* address in the organization. An unknown value (an older server, or a
|
|
3461
|
+
* failed read) warns nothing.
|
|
3462
|
+
*/
|
|
3463
|
+
function connectWarnings(configDir, profileName) {
|
|
3464
|
+
let profile = null;
|
|
3465
|
+
try {
|
|
3466
|
+
profile = loadConnectedAgentProfile(configDir, profileName);
|
|
3467
|
+
} catch {
|
|
3468
|
+
return [];
|
|
3469
|
+
}
|
|
3470
|
+
return profile?.owner_member_address === null ? [{
|
|
3471
|
+
kind: "owner_member_address_missing",
|
|
3472
|
+
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."
|
|
3473
|
+
}] : [];
|
|
3474
|
+
}
|
|
2961
3475
|
function defaultAgentProfileName(session) {
|
|
2962
3476
|
return `session-${session.toLowerCase()}`;
|
|
2963
3477
|
}
|
|
@@ -2995,6 +3509,9 @@ function defaults(options) {
|
|
|
2995
3509
|
return {
|
|
2996
3510
|
setupAgent,
|
|
2997
3511
|
nativePreflight: nativeSessionPreflight,
|
|
3512
|
+
nativeSocketPresent: () => existsSync(defaultSessionSocket()),
|
|
3513
|
+
nativeSocketRefuses: () => socketRefusesConnections(defaultSessionSocket()),
|
|
3514
|
+
codexVersion: installedCodexVersion,
|
|
2998
3515
|
installClaudeWakeHook,
|
|
2999
3516
|
installSkill(runtime) {
|
|
3000
3517
|
return installConnectSkill({
|
|
@@ -3070,7 +3587,7 @@ async function runAgentConnect(options) {
|
|
|
3070
3587
|
const runtime = session ? detectAgentRuntime(session, env) : null;
|
|
3071
3588
|
const invitation = options.resume || session || options.profileName ? void 0 : await options.readInvitation();
|
|
3072
3589
|
const profileName = agentProfileName(options.profileName ?? (session ? defaultAgentProfileName(session) : invitationProfileName(invitation ?? "")));
|
|
3073
|
-
|
|
3590
|
+
let receiver = options.receiver ?? (options.resume ? savedSetup(options.configDir, profileName)?.receiverMode ?? null : null) ?? (!session ? "poll" : runtime === "claude" ? "external" : "native");
|
|
3074
3591
|
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
3592
|
const agentInfo = agentInfoValue(options.name, options.info);
|
|
3076
3593
|
const skipped = [];
|
|
@@ -3105,15 +3622,31 @@ async function runAgentConnect(options) {
|
|
|
3105
3622
|
reason: skill.reason ?? skill.state
|
|
3106
3623
|
});
|
|
3107
3624
|
const startedAt = Date.now();
|
|
3108
|
-
const
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
await
|
|
3625
|
+
const invocation = options.invocation ?? "primitive";
|
|
3626
|
+
let fallback = null;
|
|
3627
|
+
if (receiver === "native" && session && !options.resume) try {
|
|
3628
|
+
await dependencies.nativePreflight(session);
|
|
3629
|
+
} catch (error) {
|
|
3630
|
+
const socketPresent = dependencies.nativeSocketPresent();
|
|
3631
|
+
const socketDead = socketPresent && error instanceof NativeSessionDisconnectedError && await dependencies.nativeSocketRefuses();
|
|
3632
|
+
const codexVersion = socketPresent && !socketDead || runtime === "claude" ? null : await dependencies.codexVersion();
|
|
3633
|
+
const reason = socketDead ? "session_socket_unavailable" : socketPresent ? null : codexVersion !== null && !codexSupportsNativeReceiving(codexVersion) ? "codex_version_unsupported" : "session_socket_missing";
|
|
3634
|
+
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.`;
|
|
3635
|
+
if (reason && options.receiver === void 0) {
|
|
3636
|
+
receiver = "poll";
|
|
3637
|
+
fallback = {
|
|
3638
|
+
reason,
|
|
3639
|
+
codexVersion,
|
|
3640
|
+
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.`
|
|
3641
|
+
};
|
|
3642
|
+
} 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
3643
|
}
|
|
3644
|
+
if (fallback) skipped.push({
|
|
3645
|
+
step: "native_receiver",
|
|
3646
|
+
reason: fallback.reason
|
|
3647
|
+
});
|
|
3648
|
+
const setupInvitation = options.resume ? void 0 : invitation ?? await options.readInvitation();
|
|
3649
|
+
if (options.beforeSetup) await options.beforeSetup();
|
|
3117
3650
|
const result = await dependencies.setupAgent({
|
|
3118
3651
|
configDir: options.configDir,
|
|
3119
3652
|
profileName,
|
|
@@ -3129,6 +3662,7 @@ async function runAgentConnect(options) {
|
|
|
3129
3662
|
...result.identity,
|
|
3130
3663
|
ownerMemberAddress
|
|
3131
3664
|
};
|
|
3665
|
+
const warnings = connectWarnings(options.configDir, result.identity.profileName);
|
|
3132
3666
|
let externalHook = null;
|
|
3133
3667
|
if (receiver === "external") if (verified) externalHook = dependencies.installClaudeWakeHook({
|
|
3134
3668
|
cliPath: options.cliPath,
|
|
@@ -3181,15 +3715,20 @@ async function runAgentConnect(options) {
|
|
|
3181
3715
|
reason: "already_present"
|
|
3182
3716
|
});
|
|
3183
3717
|
}
|
|
3184
|
-
const invocation = options.invocation ?? "primitive";
|
|
3185
3718
|
const receiving = receiver === "external" ? {
|
|
3186
3719
|
...result.receiving,
|
|
3720
|
+
state: externalReceivingState(externalHook),
|
|
3187
3721
|
mode: "external",
|
|
3188
3722
|
hook: externalHook
|
|
3189
3723
|
} : receiver === "poll" ? {
|
|
3190
3724
|
...result.receiving,
|
|
3191
3725
|
mode: "poll",
|
|
3192
|
-
checkCommand: pollCheckCommand(invocation, result.identity.profileName)
|
|
3726
|
+
checkCommand: pollCheckCommand(invocation, result.identity.profileName),
|
|
3727
|
+
...fallback ? {
|
|
3728
|
+
fallbackReason: fallback.reason,
|
|
3729
|
+
fallbackDetail: fallback.detail,
|
|
3730
|
+
codexVersion: fallback.codexVersion
|
|
3731
|
+
} : {}
|
|
3193
3732
|
} : {
|
|
3194
3733
|
...result.receiving,
|
|
3195
3734
|
mode: "native",
|
|
@@ -3216,6 +3755,10 @@ async function runAgentConnect(options) {
|
|
|
3216
3755
|
ownerAddress: result.identity.ownerAddress,
|
|
3217
3756
|
ownerMemberAddress,
|
|
3218
3757
|
profile: result.identity.profileName,
|
|
3758
|
+
...result.connectionName === void 0 ? {} : {
|
|
3759
|
+
name: result.connectionName,
|
|
3760
|
+
nameIsDefault: defaultConnectionName(result.connectionName, result.identity.agentAddress)
|
|
3761
|
+
},
|
|
3219
3762
|
sessionId: session ?? null,
|
|
3220
3763
|
runtime,
|
|
3221
3764
|
cli: {
|
|
@@ -3228,6 +3771,7 @@ async function runAgentConnect(options) {
|
|
|
3228
3771
|
...result.ownerNotifications ? { ownerNotifications: result.ownerNotifications } : {},
|
|
3229
3772
|
agentInfo: agentInfoState,
|
|
3230
3773
|
skipped,
|
|
3774
|
+
warnings,
|
|
3231
3775
|
selectProfile: `PRIMITIVE_AGENT_PROFILE=${result.identity.profileName}`,
|
|
3232
3776
|
resumeCommand,
|
|
3233
3777
|
guidance: `${result.guidance} ${ownerReportGuidance(identity)}`,
|
|
@@ -3236,45 +3780,6 @@ async function runAgentConnect(options) {
|
|
|
3236
3780
|
};
|
|
3237
3781
|
}
|
|
3238
3782
|
//#endregion
|
|
3239
|
-
//#region src/oclif/agent-identity-suggestions.ts
|
|
3240
|
-
/** The connection name enrollment uses when none is given. */
|
|
3241
|
-
const DEFAULT_ENROLL_NAME = "Coding agent";
|
|
3242
|
-
/** npx runs the CLI from its cache; print follow-up commands the same way. */
|
|
3243
|
-
function cliInvocation(entry) {
|
|
3244
|
-
return entry && /[\\/]_npx[\\/]/.test(entry) ? "npx -y primitive@latest" : "primitive";
|
|
3245
|
-
}
|
|
3246
|
-
/**
|
|
3247
|
-
* Whether an enrollment's connection name is the fallback it chose by itself.
|
|
3248
|
-
* Session registration's `<runtime>-<repository>` names are reported by
|
|
3249
|
-
* registration, which knows the exact name it generated.
|
|
3250
|
-
*/
|
|
3251
|
-
function generatedAgentName(name) {
|
|
3252
|
-
return name === DEFAULT_ENROLL_NAME;
|
|
3253
|
-
}
|
|
3254
|
-
/**
|
|
3255
|
-
* Additive result fields for a connect, enroll or session registration.
|
|
3256
|
-
* `nameIsDefault` appears only when it is known whether the connection name
|
|
3257
|
-
* is one this CLI generated. The runtime note is suggested once, after a
|
|
3258
|
-
* connection this run completed.
|
|
3259
|
-
*/
|
|
3260
|
-
function identitySuggestions(params) {
|
|
3261
|
-
const prefix = `PRIMITIVE_AGENT_PROFILE=${params.profile} ${params.invocation}`;
|
|
3262
|
-
const suggestions = [];
|
|
3263
|
-
const nameIsDefault = params.nameIsDefault;
|
|
3264
|
-
if (nameIsDefault) suggestions.push({
|
|
3265
|
-
kind: "rename",
|
|
3266
|
-
command: `${prefix} agent rename "<new name>"`
|
|
3267
|
-
});
|
|
3268
|
-
if (params.connectedNow) suggestions.push({
|
|
3269
|
-
kind: "runtime_note",
|
|
3270
|
-
command: `${prefix} agent runtime set`
|
|
3271
|
-
});
|
|
3272
|
-
return nameIsDefault === void 0 ? { suggestions } : {
|
|
3273
|
-
nameIsDefault,
|
|
3274
|
-
suggestions
|
|
3275
|
-
};
|
|
3276
|
-
}
|
|
3277
|
-
//#endregion
|
|
3278
3783
|
//#region src/oclif/agent-disconnect.ts
|
|
3279
3784
|
var AgentDisconnectError = class extends Error {};
|
|
3280
3785
|
function credentialDigest(profile) {
|
|
@@ -4751,4 +5256,4 @@ async function runDetached(params) {
|
|
|
4751
5256
|
}
|
|
4752
5257
|
}
|
|
4753
5258
|
//#endregion
|
|
4754
|
-
export { describeSessionHookItem as $,
|
|
5259
|
+
export { describeSessionHookItem as $, evaluateContactPolicy as A, agentInvitationHash as B, RECEIVER_MODES as C, NETWORK_ADMISSION_PENDING_RETRY_MS as D, ContactPolicyReadRetryError as E, ContactsApiError as F, saveConnectionName as G, readAgentInvitation as H, contactAddress as I, recordHookMailCheck as J, savedReceiver as K, contactAgentAddress as L, AddressNotesApiError as M, addressNoteTarget as N, NetworkAdmissionPendingError as O, runAddressNotesRequest as P, describeSessionHookChange as Q, runContactRequest as R, runAgentConnect as S, verificationReplySubmitted as T, receiverStatusCommand as U, connectAgent as V, refusalAfterReplacement as W, claudeConfigDir as X, boundSessionProfiles as Y, currentCliLocation as Z, disconnectAgent as _, withOwnerMemberAddress as _t, endSession as a, readClaudeSettings as at, invitationProfileName as b, pendingSessionDisconnects as c, writeClaudeSettings as ct, registerSession as d, jsonIndentation as dt, ensureHookLauncher as et, retryPendingDisconnects as f, readManagedFile as ft, AgentDisconnectError as g, refreshOwnerMemberAddressPeriodically as gt, enrollmentResumes as h, readHookLauncherWarning as ht, connectedProfilesForSession as i, inspectClaudeHooks as it, canonicalContactSelector as j, createNotificationContactPolicy as k, psProcessInfo as l, MachineFileError as lt, enrollAgent as m, writeManagedFile as mt, OMP_MAIN as n, findOnPath as nt, headlessClaudeRun as o, repairClaudeHooks as ot, runDetached as p, upsertManagedBlock as pt, installClaudeWakeHook as q, agentConnectionState as r, hasMachineSessionHooks as rt, machine_session_exports as s, samePath as st, MACHINE_RUNTIMES as t, ephemeralCliReason as tt, readClaudeHookInput as u, inspectManagedBlock as ut, connectWarnings as v, externalReceivingState as w, pollCheckCommand as x, defaultAgentProfileName as y, agentConnectionStatus as z };
|