primcli 1.33.0 → 1.35.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 +237 -20
- package/bin/claude-pending-mail.mjs +342 -0
- package/bin/claude-wake.mjs +204 -0
- package/bin/run.js +20 -0
- package/dist/{cli-config-Cv2-NAOd.js → cli-config-DmhlcS3s.js} +30 -350
- package/dist/json-output-KAIuP9rG.js +254 -0
- package/dist/listen-background-D6UhIbkO.js +647 -0
- package/dist/oclif/index.js +17087 -9256
- package/dist/oclif/listen-supervisor.js +2 -0
- package/dist/oclif/proxy-auto-detect.js +2 -1
- package/dist/oclif/root-signup-hint.js +1 -1
- package/dist/shared-mail-files-Cbwnf79a.js +351 -0
- package/man/primitive.1 +51 -0
- package/package.json +13 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as jsonOutputRequested } from "../json-output-KAIuP9rG.js";
|
|
1
2
|
import { spawnSync } from "node:child_process";
|
|
2
3
|
//#region src/oclif/proxy-auto-detect.ts
|
|
3
4
|
const PROXY_ENV_VARS = [
|
|
@@ -44,7 +45,7 @@ function restartWithProxyEnvIfNeeded(options = {}) {
|
|
|
44
45
|
process.exit(code);
|
|
45
46
|
throw new Error("process.exit returned unexpectedly");
|
|
46
47
|
});
|
|
47
|
-
if (!hintPrinted) {
|
|
48
|
+
if (!hintPrinted && !jsonOutputRequested(argv.slice(2))) {
|
|
48
49
|
hintPrinted = true;
|
|
49
50
|
const names = detectedVars.join("/");
|
|
50
51
|
stderr.write(`primitive: proxy detected via ${names}, restarting with NODE_USE_ENV_PROXY=1\n`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as normalizeApiBaseUrl, c as resolveConfigEnvironment, i as loadCliConfig } from "../cli-config-
|
|
1
|
+
import { S as normalizeApiBaseUrl, c as resolveConfigEnvironment, i as loadCliConfig } from "../cli-config-DmhlcS3s.js";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { chmodSync, closeSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname, join, win32 } from "node:path";
|
|
4
|
+
import { setTimeout } from "node:timers/promises";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
//#region src/oclif/listen-state.ts
|
|
7
|
+
var ListenStateError = class extends Error {};
|
|
8
|
+
function normalizeListenOrigin(value) {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new ListenStateError("The API base URL must be an HTTP(S) URL without credentials, query, or fragment.");
|
|
11
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
12
|
+
}
|
|
13
|
+
function listenIdentity(origin, accountId) {
|
|
14
|
+
return createHash("sha256").update(JSON.stringify([normalizeListenOrigin(origin), accountId])).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
function privateDirectory(path) {
|
|
17
|
+
mkdirSync(path, {
|
|
18
|
+
recursive: true,
|
|
19
|
+
mode: 448
|
|
20
|
+
});
|
|
21
|
+
if (!lstatSync(path).isDirectory() || lstatSync(path).isSymbolicLink()) throw new ListenStateError("Listener state must be a private directory.");
|
|
22
|
+
chmodSync(path, 448);
|
|
23
|
+
}
|
|
24
|
+
function processAlive(pid) {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return error.code !== "ESRCH";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function listenProcessIdentity(pid) {
|
|
33
|
+
if (!Number.isSafeInteger(pid) || pid < 1) return null;
|
|
34
|
+
try {
|
|
35
|
+
if (process.platform === "linux") {
|
|
36
|
+
const boot = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
37
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
38
|
+
const end = stat.lastIndexOf(")");
|
|
39
|
+
const started = stat.slice(end + 1).trim().split(/\s+/)[19];
|
|
40
|
+
if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(boot) || !stat.startsWith(`${pid} (`) || end < 0 || !started || !/^\d+$/.test(started)) return null;
|
|
41
|
+
return `linux:${boot}:${started}`;
|
|
42
|
+
}
|
|
43
|
+
if (process.platform === "darwin") {
|
|
44
|
+
const options = {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
timeout: 1e3,
|
|
47
|
+
maxBuffer: 4096,
|
|
48
|
+
stdio: [
|
|
49
|
+
"ignore",
|
|
50
|
+
"pipe",
|
|
51
|
+
"ignore"
|
|
52
|
+
],
|
|
53
|
+
env: {
|
|
54
|
+
LC_ALL: "C",
|
|
55
|
+
TZ: "UTC",
|
|
56
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin"
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const bootId = execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], options).trim().toLowerCase();
|
|
60
|
+
const started = execFileSync("/bin/ps", [
|
|
61
|
+
"-p",
|
|
62
|
+
String(pid),
|
|
63
|
+
"-o",
|
|
64
|
+
"lstart="
|
|
65
|
+
], options).trim().replace(/\s+/g, " ");
|
|
66
|
+
if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(bootId) || !/^[A-Z][a-z]{2} [A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}:\d{2} \d{4}$/.test(started)) return null;
|
|
67
|
+
return `darwin-boot:${bootId}:${started}`;
|
|
68
|
+
}
|
|
69
|
+
if (process.platform === "win32") {
|
|
70
|
+
const root = process.env.SystemRoot;
|
|
71
|
+
if (!root || !win32.isAbsolute(root)) return null;
|
|
72
|
+
const started = execFileSync(win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), [
|
|
73
|
+
"-NoLogo",
|
|
74
|
+
"-NoProfile",
|
|
75
|
+
"-NonInteractive",
|
|
76
|
+
"-Command",
|
|
77
|
+
`$ErrorActionPreference='Stop'; (Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
|
|
78
|
+
], {
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
timeout: 2e3,
|
|
81
|
+
maxBuffer: 4096,
|
|
82
|
+
windowsHide: true,
|
|
83
|
+
stdio: [
|
|
84
|
+
"ignore",
|
|
85
|
+
"pipe",
|
|
86
|
+
"ignore"
|
|
87
|
+
]
|
|
88
|
+
}).trim();
|
|
89
|
+
return /^\d{1,20}$/.test(started) ? `win32:${started}` : null;
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/** Legacy macOS timestamps cannot prove that a live PID changed owners. */
|
|
95
|
+
function compareListenProcessIdentity(saved, current) {
|
|
96
|
+
if (saved === null || current === null) return null;
|
|
97
|
+
if (saved === current) return true;
|
|
98
|
+
if (saved.startsWith("darwin:") || current.startsWith("darwin:")) return null;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
function ownerIdentity(path) {
|
|
102
|
+
try {
|
|
103
|
+
const stat = lstatSync(path);
|
|
104
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024) return null;
|
|
105
|
+
const saved = JSON.parse(readFileSync(path, "utf8"));
|
|
106
|
+
if (!saved || typeof saved !== "object" || !("version" in saved) || saved.version !== 1 || !("identity" in saved) || typeof saved.identity !== "string") return null;
|
|
107
|
+
return /^(?:linux:[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}:\d+|darwin:\d+:\d+:[A-Z][a-z]{2} [A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}:\d{2} \d{4}|darwin-boot:[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}:[A-Z][a-z]{2} [A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}:\d{2} \d{4}|win32:\d{1,20})$/.test(saved.identity) ? saved.identity : null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function removeOwner(path, owner) {
|
|
113
|
+
try {
|
|
114
|
+
unlinkSync(join(path, owner));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code !== "ENOENT") throw error;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
rmdirSync(path);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (![
|
|
122
|
+
"ENOENT",
|
|
123
|
+
"ENOTEMPTY",
|
|
124
|
+
"EEXIST"
|
|
125
|
+
].includes(error.code ?? "")) throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function acquireListenLock(directory, scope) {
|
|
129
|
+
privateDirectory(directory);
|
|
130
|
+
const path = join(directory, `${createHash("sha256").update(scope).digest("hex")}.lock`);
|
|
131
|
+
const owner = `${process.pid}-${randomUUID()}`;
|
|
132
|
+
const candidate = mkdtempSync(join(directory, ".claim-"));
|
|
133
|
+
chmodSync(candidate, 448);
|
|
134
|
+
writeFileSync(join(candidate, owner), JSON.stringify({
|
|
135
|
+
version: 1,
|
|
136
|
+
identity: listenProcessIdentity(process.pid)
|
|
137
|
+
}), {
|
|
138
|
+
mode: 384,
|
|
139
|
+
flag: "wx"
|
|
140
|
+
});
|
|
141
|
+
let installed = false;
|
|
142
|
+
try {
|
|
143
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
144
|
+
try {
|
|
145
|
+
renameSync(candidate, path);
|
|
146
|
+
installed = true;
|
|
147
|
+
break;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (!["EEXIST", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
|
|
150
|
+
}
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
entries = readdirSync(path);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error.code === "ENOENT") continue;
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
if (entries.length === 0) continue;
|
|
159
|
+
const match = entries.length === 1 ? /^(\d+)-[a-f0-9-]{36}$/.exec(entries[0] ?? "") : null;
|
|
160
|
+
const pid = match ? Number(match[1]) : null;
|
|
161
|
+
let stale = pid !== null && !processAlive(pid);
|
|
162
|
+
if (!stale && pid !== null) {
|
|
163
|
+
const saved = ownerIdentity(join(path, entries[0] ?? ""));
|
|
164
|
+
stale = compareListenProcessIdentity(saved, saved === null ? null : listenProcessIdentity(pid)) === false;
|
|
165
|
+
}
|
|
166
|
+
if (!stale) throw new ListenStateError("Another listener is using this subscription, or its owner cannot be verified. Stop the original listener before reconnecting. To use a separate subscription instead, run: primitive listen --subscription local-" + randomUUID());
|
|
167
|
+
removeOwner(path, entries[0] ?? "");
|
|
168
|
+
}
|
|
169
|
+
if (!installed) throw new ListenStateError("Subscription lock changed concurrently. Retry the command.");
|
|
170
|
+
} finally {
|
|
171
|
+
if (!installed) removeOwner(candidate, owner);
|
|
172
|
+
}
|
|
173
|
+
let released = false;
|
|
174
|
+
return () => {
|
|
175
|
+
if (!released) {
|
|
176
|
+
released = true;
|
|
177
|
+
removeOwner(path, owner);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function resolveListenSubscription(configDir, identity, explicit) {
|
|
182
|
+
const directory = join(configDir, "listen", identity);
|
|
183
|
+
privateDirectory(directory);
|
|
184
|
+
let name = explicit;
|
|
185
|
+
if (name === void 0) {
|
|
186
|
+
const unlock = acquireListenLock(directory, "default-name");
|
|
187
|
+
try {
|
|
188
|
+
const file = join(directory, "default.json");
|
|
189
|
+
try {
|
|
190
|
+
if (!lstatSync(file).isFile() || lstatSync(file).isSymbolicLink()) throw new ListenStateError("Invalid listener identity file.");
|
|
191
|
+
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
192
|
+
if (typeof saved.name !== "string" || !/^local-[a-f0-9-]{36}$/.test(saved.name)) throw new ListenStateError("Invalid listener identity file.");
|
|
193
|
+
name = saved.name;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error.code !== "ENOENT") throw error;
|
|
196
|
+
name = `local-${randomUUID()}`;
|
|
197
|
+
const temporary = join(directory, `.state-${randomUUID()}`);
|
|
198
|
+
const fd = openSync(temporary, "wx", 384);
|
|
199
|
+
try {
|
|
200
|
+
writeFileSync(fd, `${JSON.stringify({ name })}\n`);
|
|
201
|
+
fsyncSync(fd);
|
|
202
|
+
} finally {
|
|
203
|
+
closeSync(fd);
|
|
204
|
+
}
|
|
205
|
+
renameSync(temporary, file);
|
|
206
|
+
}
|
|
207
|
+
if (process.platform !== "win32") for (const path of [
|
|
208
|
+
directory,
|
|
209
|
+
join(configDir, "listen"),
|
|
210
|
+
configDir
|
|
211
|
+
]) {
|
|
212
|
+
const directoryFd = openSync(path, "r");
|
|
213
|
+
try {
|
|
214
|
+
fsyncSync(directoryFd);
|
|
215
|
+
} finally {
|
|
216
|
+
closeSync(directoryFd);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
unlock();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!name || name.length > 64 || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(name)) throw new ListenStateError("Use a subscription name of 1–64 letters, digits, underscores, or hyphens.");
|
|
224
|
+
return {
|
|
225
|
+
name,
|
|
226
|
+
release: acquireListenLock(directory, `subscription:${name}`)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/oclif/shared-mail-files.ts
|
|
231
|
+
var SharedMailStateError = class extends ListenStateError {};
|
|
232
|
+
const invalidSharedMail = () => new SharedMailStateError("Shared mail state is invalid or inconsistent. Preserve it before retrying to avoid duplicate delivery.");
|
|
233
|
+
function privateMailPermissions(info, platform = process.platform) {
|
|
234
|
+
return platform === "win32" || info.uid === process.getuid?.() && (info.mode & 63) === 0;
|
|
235
|
+
}
|
|
236
|
+
function privateMailDirectory(path, create = false) {
|
|
237
|
+
if (create) {
|
|
238
|
+
const created = mkdirSync(path, {
|
|
239
|
+
recursive: true,
|
|
240
|
+
mode: 448
|
|
241
|
+
});
|
|
242
|
+
if (created !== void 0) {
|
|
243
|
+
let current = path;
|
|
244
|
+
for (;;) {
|
|
245
|
+
syncMailDirectory(current);
|
|
246
|
+
syncMailDirectory(dirname(current));
|
|
247
|
+
if (current === created) break;
|
|
248
|
+
current = dirname(current);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const stat = lstatSync(path);
|
|
253
|
+
if (!stat.isDirectory() || !privateMailPermissions(stat)) throw invalidSharedMail();
|
|
254
|
+
}
|
|
255
|
+
function syncMailDirectory(path, platform = process.platform) {
|
|
256
|
+
if (platform === "win32") return;
|
|
257
|
+
const fd = openSync(path, "r");
|
|
258
|
+
try {
|
|
259
|
+
fsyncSync(fd);
|
|
260
|
+
} finally {
|
|
261
|
+
closeSync(fd);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function readMailJson(path, maxBytes = 16384) {
|
|
265
|
+
try {
|
|
266
|
+
const info = lstatSync(path);
|
|
267
|
+
if (!info.isFile() || !privateMailPermissions(info) || info.size > maxBytes) throw invalidSharedMail();
|
|
268
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
269
|
+
if (value === null) throw invalidSharedMail();
|
|
270
|
+
return value;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error.code === "ENOENT") return null;
|
|
273
|
+
throw invalidSharedMail();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function writeMailJson(path, value) {
|
|
277
|
+
privateMailDirectory(dirname(path), true);
|
|
278
|
+
const temporary = join(dirname(path), `.write-${randomUUID()}`);
|
|
279
|
+
try {
|
|
280
|
+
const fd = openSync(temporary, "wx", 384);
|
|
281
|
+
try {
|
|
282
|
+
writeFileSync(fd, `${JSON.stringify(value)}\n`);
|
|
283
|
+
fsyncSync(fd);
|
|
284
|
+
} finally {
|
|
285
|
+
closeSync(fd);
|
|
286
|
+
}
|
|
287
|
+
renameSync(temporary, path);
|
|
288
|
+
syncMailDirectory(dirname(path));
|
|
289
|
+
} finally {
|
|
290
|
+
try {
|
|
291
|
+
unlinkSync(temporary);
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function removeMailFile(path) {
|
|
296
|
+
try {
|
|
297
|
+
unlinkSync(path);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error.code !== "ENOENT") throw error;
|
|
300
|
+
}
|
|
301
|
+
syncMailDirectory(dirname(path));
|
|
302
|
+
}
|
|
303
|
+
function mailLockBusy(error) {
|
|
304
|
+
return error instanceof ListenStateError && /^(Another listener|Subscription lock changed)/.test(error.message);
|
|
305
|
+
}
|
|
306
|
+
async function withMailLock(directory, action, signal) {
|
|
307
|
+
const deadline = performance.now() + 5e3;
|
|
308
|
+
for (;;) {
|
|
309
|
+
signal?.throwIfAborted();
|
|
310
|
+
let release;
|
|
311
|
+
try {
|
|
312
|
+
release = acquireListenLock(directory, "shared-mail-state");
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (!mailLockBusy(error) || performance.now() >= deadline) throw error;
|
|
315
|
+
await setTimeout(25, void 0, { signal });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
signal?.throwIfAborted();
|
|
320
|
+
return action();
|
|
321
|
+
} finally {
|
|
322
|
+
release();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function mailObject(value, keys) {
|
|
327
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidSharedMail();
|
|
328
|
+
const result = value;
|
|
329
|
+
if (Object.keys(result).length !== keys.length || keys.some((key) => !Object.hasOwn(result, key))) throw invalidSharedMail();
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
function mailId(value) {
|
|
333
|
+
if (typeof value !== "string" || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value)) throw invalidSharedMail();
|
|
334
|
+
return value.toLowerCase();
|
|
335
|
+
}
|
|
336
|
+
function mailString(value, max = 256) {
|
|
337
|
+
if (typeof value !== "string" || !value || value.length > max || Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) throw invalidSharedMail();
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
function mailAddress(value) {
|
|
341
|
+
const address = mailString(value, 254).toLowerCase();
|
|
342
|
+
if (!/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/.test(address)) throw invalidSharedMail();
|
|
343
|
+
return address;
|
|
344
|
+
}
|
|
345
|
+
function mailTime(value) {
|
|
346
|
+
const time = mailString(value, 40);
|
|
347
|
+
if (!Number.isFinite(Date.parse(time))) throw invalidSharedMail();
|
|
348
|
+
return new Date(time).toISOString();
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
export { resolveListenSubscription as S, acquireListenLock as _, mailLockBusy as a, listenProcessIdentity as b, mailTime as c, readMailJson as d, removeMailFile as f, ListenStateError as g, writeMailJson as h, mailId as i, privateMailDirectory as l, withMailLock as m, invalidSharedMail as n, mailObject as o, syncMailDirectory as p, mailAddress as r, mailString as s, SharedMailStateError as t, privateMailPermissions as u, compareListenProcessIdentity as v, normalizeListenOrigin as x, listenIdentity as y };
|
package/man/primitive.1
CHANGED
|
@@ -20,6 +20,29 @@ Most commands print human-readable output by default. Commands that expose JSON
|
|
|
20
20
|
.B --json
|
|
21
21
|
flag or use generated operation output that can be piped into tools such as
|
|
22
22
|
.BR jq (1).
|
|
23
|
+
.PP
|
|
24
|
+
With
|
|
25
|
+
.BR --json ,
|
|
26
|
+
stdout is exactly one JSON document, on success and on failure, and stderr
|
|
27
|
+
stays empty, so output merged with 2>&1 still parses. Notices the command
|
|
28
|
+
would print on stderr go in the document's
|
|
29
|
+
.B warnings
|
|
30
|
+
array; a failure adds
|
|
31
|
+
.B error
|
|
32
|
+
and
|
|
33
|
+
.BR exit_code .
|
|
34
|
+
Generated API commands keep their usual stdout shape with
|
|
35
|
+
.BR --json :
|
|
36
|
+
the data payload only. They do not print the
|
|
37
|
+
.B next cursor:
|
|
38
|
+
line then; add
|
|
39
|
+
.B --envelope
|
|
40
|
+
to get the full response envelope, with the pagination cursor in
|
|
41
|
+
.B meta.cursor
|
|
42
|
+
and empty-result hints in
|
|
43
|
+
.BR summary .
|
|
44
|
+
.B primitive listen
|
|
45
|
+
streams JSONL and is not covered by this rule.
|
|
23
46
|
.SH LOCAL WEBHOOK EVENTS
|
|
24
47
|
.PP
|
|
25
48
|
.B primitive listen
|
|
@@ -34,11 +57,17 @@ credential. This mode requires WebSocket and the shared saved subscription;
|
|
|
34
57
|
--transport poll and a custom --subscription are rejected. The local-mail-*
|
|
35
58
|
subscription namespace is reserved for shared receiving. No runtime is launched. Repeat --sender or use comma-separated exact
|
|
36
59
|
addresses. Routine authenticated status interactions do not notify the session.
|
|
60
|
+
Verified receiver presence controls are answered deterministically while the
|
|
61
|
+
exact session binding is eligible. They do not start model turns, wake Claude,
|
|
62
|
+
complete chat waits, or consume --once or --number. Pending verification remains
|
|
63
|
+
durably deferred and reconciles independently of ordinary mail.
|
|
37
64
|
.PP
|
|
38
65
|
.B primitive listen --status --notify-session UUID
|
|
39
66
|
inspects local notification receipts without receiving mail or connecting to a
|
|
40
67
|
session. Accepted means queued; unknown outcomes are held across restarts and are
|
|
41
68
|
never automatically resent. Inspect the exact session before a manual resend.
|
|
69
|
+
An explicit Codex Review or Compact refusal is recorded as not_submitted and
|
|
70
|
+
retried after a short delay; a timeout remains unknown.
|
|
42
71
|
Use --limit (default 100, maximum 1000) and --cursor with the returned nextCursor
|
|
43
72
|
to page through receipts. Notification subscriptions accept email.received only;
|
|
44
73
|
--once counts a candidate processed during this invocation, including filtered
|
|
@@ -233,6 +262,15 @@ stable idempotency key. After a lost send response, repeating the same command
|
|
|
233
262
|
looks up that key without sending again. Timed-out exact-parent waits retain
|
|
234
263
|
their claim for resume. A plain reply does not prove task completion.
|
|
235
264
|
|
|
265
|
+
.SS Organization agent network
|
|
266
|
+
.B primitive network peers --owner NAME --json
|
|
267
|
+
finds listed agents owned by a coworker in the private default organization
|
|
268
|
+
network. Results include address, owner name and last recorded API activity;
|
|
269
|
+
activity is not live presence. Use the returned cursor for further pages.
|
|
270
|
+
The owner can separately allow an agent to see peers or be seen. Contacts are
|
|
271
|
+
saved addresses, not the network roster. Network listing and contact acceptance
|
|
272
|
+
do not grant task authority. Known addresses can still exchange ordinary email.
|
|
273
|
+
|
|
236
274
|
.SS Explicit communication signals
|
|
237
275
|
.B primitive signal read --id EMAIL_ID --json
|
|
238
276
|
reports that you read one authenticated plain email.
|
|
@@ -297,6 +335,19 @@ chat sent the message but no reply arrived before the timeout. Wait; do not rese
|
|
|
297
335
|
.B 4
|
|
298
336
|
.BR uncertain :
|
|
299
337
|
transport error, conflict, server error, or a send record with status unknown. The message may have gone out; check sent history before retrying.
|
|
338
|
+
.PP
|
|
339
|
+
Every
|
|
340
|
+
.B --json
|
|
341
|
+
envelope from these commands carries
|
|
342
|
+
.B sent_email_id
|
|
343
|
+
(null until a send record is known) and
|
|
344
|
+
.BR idempotency_key ,
|
|
345
|
+
including on an uncertain outcome. Reconcile with
|
|
346
|
+
.BR "primitive sent get --idempotency-key KEY" ;
|
|
347
|
+
not_found means no record is visible yet, not that nothing was created, so
|
|
348
|
+
retry only with the same
|
|
349
|
+
.BR --idempotency-key ,
|
|
350
|
+
never a new one.
|
|
300
351
|
.SH ENVIRONMENT
|
|
301
352
|
.TP
|
|
302
353
|
.B PRIMITIVE_API_KEY
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primcli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"description": "Manage your account settings, storage, and webhook secret"
|
|
51
51
|
},
|
|
52
52
|
"agent-connections": {
|
|
53
|
-
"description": "
|
|
53
|
+
"description": "Create, list, invite, revoke, and remove connected agents; claim private invitations through stdin."
|
|
54
54
|
},
|
|
55
55
|
"agent": {
|
|
56
|
-
"description": "Connect an invited session, enroll a new session using a saved
|
|
56
|
+
"description": "Connect an invited session, enroll a new session using a saved member login, disconnect one saved profile, or manage contacts and signup."
|
|
57
57
|
},
|
|
58
58
|
"credits": {
|
|
59
59
|
"description": "Redeem credit codes and check the organization credit balance"
|
|
@@ -64,6 +64,12 @@
|
|
|
64
64
|
"registries": {
|
|
65
65
|
"description": "Create and manage agent registries, publish and resolve agents by handle, and moderate publication requests"
|
|
66
66
|
},
|
|
67
|
+
"network": {
|
|
68
|
+
"description": "Discover same-organization agents by email address and manage default network visibility. Use `primitive network` to see the commands."
|
|
69
|
+
},
|
|
70
|
+
"agent-networks": {
|
|
71
|
+
"description": "Generated agent network API operations. Prefer `primitive network` for discovery and visibility."
|
|
72
|
+
},
|
|
67
73
|
"inbox": {
|
|
68
74
|
"description": "Check inbound email setup and readiness, and work through mail awaiting your reply. Prefer `primitive inbox setup` for a guided setup path, `primitive inbox status` for the readiness table, and `primitive inbox next` for the oldest email awaiting your reply."
|
|
69
75
|
},
|
|
@@ -129,7 +135,7 @@
|
|
|
129
135
|
"description": "Schedule and send wake commands to your own functions over signed email, and manage which senders may wake them. Use `primitive wake schedules list|create|get|update|delete|run`, `primitive wake authorizations list|create|update|delete`, and `primitive wake dispatches list`."
|
|
130
136
|
},
|
|
131
137
|
"listen": {
|
|
132
|
-
"description": "Receive events locally as JSONL with JSON status responses by default; --json is accepted explicitly. Use --exec or --forward-to, or deliver external mail events as tool output, never user messages, to an exact loaded native session using --notify-session with --contacts or approved --sender addresses. Add --email-id to --status to explain one received email. Add --contact-requests for owner-enabled structured first-contact intake. Use --background for independent receiving, --status --notify-session for current receiver health and durable receipts, or --stop --notify-session to stop only that receiver."
|
|
138
|
+
"description": "Receive events locally as JSONL with JSON status responses by default; --json is accepted explicitly. Use --exec or --forward-to, or deliver external mail events as tool output, never user messages, to an exact loaded native session using --notify-session with --contacts or approved --sender addresses. Add --email-id to --status to explain one received email. Add --contact-requests for owner-enabled structured first-contact intake. Use --background for independent receiving, --status --notify-session for current receiver health and durable receipts, or --stop --notify-session to stop only that receiver. Verified presence controls use no model turns and do not consume --once or --number."
|
|
133
139
|
},
|
|
134
140
|
"contacts": {
|
|
135
141
|
"description": "Shared organization contacts plus structured request, accept, and wait commands for connected agents. Directory list, get, add, update, and remove use conditional versions. Commands return JSON by default and accept explicit --json."
|
|
@@ -139,6 +145,9 @@
|
|
|
139
145
|
},
|
|
140
146
|
"agent:notes": {
|
|
141
147
|
"description": "Read and maintain organization address notes. Connected profiles default to their own address and can write only their own notes. Use agent notes list|get|set|delete; new notes are private, and updates preserve visibility unless explicitly changed. Commands return JSON by default."
|
|
148
|
+
},
|
|
149
|
+
"agent:working": {
|
|
150
|
+
"description": "Short, expiring work claims stored in the AGENT_WORKING address note. Set a claim when work starts, clear it when work ends, and check a peer's claim with get --address before editing a shared file. Claims are advisory, not locks."
|
|
142
151
|
}
|
|
143
152
|
},
|
|
144
153
|
"topicSeparator": " "
|