@phnx-labs/agents-cli 1.20.0 → 1.20.4
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/CHANGELOG.md +81 -0
- package/README.md +4 -4
- package/dist/commands/cli.js +3 -3
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +24 -7
- package/dist/commands/exec.js +36 -16
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +89 -0
- package/dist/commands/helper.d.ts +12 -0
- package/dist/commands/helper.js +87 -0
- package/dist/commands/hooks.js +86 -7
- package/dist/commands/import.js +90 -37
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +117 -4
- package/dist/commands/pull.js +4 -4
- package/dist/commands/routines.js +6 -6
- package/dist/commands/rules.js +8 -4
- package/dist/commands/secrets-migrate.d.ts +24 -0
- package/dist/commands/secrets-migrate.js +198 -0
- package/dist/commands/secrets-sync.d.ts +11 -0
- package/dist/commands/secrets-sync.js +155 -0
- package/dist/commands/secrets.js +74 -39
- package/dist/commands/skills.js +22 -5
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +48 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.js +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +169 -8
- package/dist/commands/workflows.js +29 -6
- package/dist/index.js +4 -0
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +41 -17
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/chrome.js +4 -0
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/profiles.d.ts +3 -3
- package/dist/lib/browser/profiles.js +3 -3
- package/dist/lib/browser/service.js +19 -0
- package/dist/lib/browser/types.d.ts +4 -4
- package/dist/lib/cli-resources.d.ts +36 -8
- package/dist/lib/cli-resources.js +268 -46
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +39 -11
- package/dist/lib/exec.js +90 -31
- package/dist/lib/help.js +11 -5
- package/dist/lib/hooks/cache.d.ts +38 -0
- package/dist/lib/hooks/cache.js +242 -0
- package/dist/lib/hooks/profile.d.ts +33 -0
- package/dist/lib/hooks/profile.js +129 -0
- package/dist/lib/hooks.d.ts +0 -10
- package/dist/lib/hooks.js +68 -15
- package/dist/lib/import.d.ts +21 -0
- package/dist/lib/import.js +55 -2
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +40 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +51 -1
- package/dist/lib/plugin-marketplace.d.ts +10 -0
- package/dist/lib/plugin-marketplace.js +47 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +187 -8
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/pty-server.js +27 -3
- package/dist/lib/routines-format.d.ts +17 -5
- package/dist/lib/routines-format.js +37 -16
- package/dist/lib/routines.d.ts +1 -1
- package/dist/lib/routines.js +2 -2
- package/dist/lib/runner.js +64 -10
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
- package/dist/lib/secrets/bundles.d.ts +18 -22
- package/dist/lib/secrets/bundles.js +75 -99
- package/dist/lib/secrets/index.d.ts +51 -27
- package/dist/lib/secrets/index.js +147 -156
- package/dist/lib/secrets/install-helper.d.ts +45 -0
- package/dist/lib/secrets/install-helper.js +165 -0
- package/dist/lib/secrets/linux.js +4 -4
- package/dist/lib/secrets/sync.d.ts +56 -0
- package/dist/lib/secrets/sync.js +180 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/shims.d.ts +4 -1
- package/dist/lib/shims.js +5 -35
- package/dist/lib/state.d.ts +14 -1
- package/dist/lib/state.js +49 -5
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +47 -21
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/types.d.ts +57 -1
- package/dist/lib/types.js +2 -0
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +35 -1
- package/dist/lib/versions.js +288 -64
- package/package.json +13 -12
- package/scripts/install-helper.js +97 -0
- package/scripts/postinstall.js +16 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
|
@@ -7,12 +7,109 @@
|
|
|
7
7
|
* but unlike skills/commands/hooks, CLI resources are NOT copied into per-agent
|
|
8
8
|
* version homes — they install binaries onto the host PATH. The relationship is
|
|
9
9
|
* "Brewfile-style": declare once in ~/.agents/cli/, install on any new machine.
|
|
10
|
+
*
|
|
11
|
+
* Security: every field that becomes a child-process argument is validated
|
|
12
|
+
* against a strict allowlist and dispatched via spawnSync with an argv array.
|
|
13
|
+
* Nothing here ever runs through a shell — manifests can come from project repos
|
|
14
|
+
* or pulled extras, so anything that would let a manifest author smuggle in
|
|
15
|
+
* `;`, `$(...)`, backticks, redirects, or pipe operators is a remote-code-
|
|
16
|
+
* execution sink.
|
|
10
17
|
*/
|
|
11
18
|
import * as fs from 'fs';
|
|
12
|
-
import
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { spawnSync } from 'child_process';
|
|
13
22
|
import * as yaml from 'yaml';
|
|
14
23
|
import { listResources, resolveResource } from './resources.js';
|
|
24
|
+
// ─── Validation primitives ───────────────────────────────────────────────────
|
|
25
|
+
/** Token allowed inside `check:` strings — letters, digits, underscore, dot, slash, dash. */
|
|
26
|
+
const SAFE_CHECK_TOKEN = /^[a-zA-Z0-9_./-]+$/;
|
|
27
|
+
/** npm package name with optional scope and optional version/tag. */
|
|
28
|
+
const NPM_PACKAGE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(@[a-zA-Z0-9._-]+)?$/;
|
|
29
|
+
/** Homebrew formula name (and optional tap prefix). */
|
|
30
|
+
const BREW_FORMULA = /^([a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*\/)?[a-z0-9][a-z0-9_.+-]*$/;
|
|
31
|
+
/** Path segment inside a tarball — no leading slash, no `..`, no shell metas. */
|
|
32
|
+
const SAFE_PATH_SEGMENT = /^[a-zA-Z0-9_./-]+$/;
|
|
33
|
+
function assertSafeCheckToken(tok) {
|
|
34
|
+
if (!SAFE_CHECK_TOKEN.test(tok)) {
|
|
35
|
+
throw new Error(`check contains unsafe token: ${JSON.stringify(tok)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertNpmPackage(name) {
|
|
39
|
+
if (!NPM_PACKAGE.test(name)) {
|
|
40
|
+
throw new Error(`npm package name is not allowlisted: ${JSON.stringify(name)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function assertBrewFormula(name) {
|
|
44
|
+
if (!BREW_FORMULA.test(name)) {
|
|
45
|
+
throw new Error(`brew formula name is not allowlisted: ${JSON.stringify(name)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function assertHttpsUrl(url) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = new URL(url);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`url is not parseable: ${JSON.stringify(url)}`);
|
|
55
|
+
}
|
|
56
|
+
if (parsed.protocol !== 'https:') {
|
|
57
|
+
throw new Error(`url must use https:// (got ${parsed.protocol}): ${JSON.stringify(url)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function assertSafePathSegment(seg) {
|
|
61
|
+
if (!SAFE_PATH_SEGMENT.test(seg) || seg.startsWith('/') || seg.split('/').includes('..')) {
|
|
62
|
+
throw new Error(`extract path is not allowlisted: ${JSON.stringify(seg)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
15
65
|
// ─── Parsing ─────────────────────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Parse a `check:` field into a CheckSpec. Accepts either a structured object
|
|
68
|
+
* (`{ kind: 'which'|'version', cmd, args? }`) or a legacy whitespace-separated
|
|
69
|
+
* string. String form is split on whitespace and each token is validated against
|
|
70
|
+
* SAFE_CHECK_TOKEN — manifests cannot smuggle in shell metacharacters.
|
|
71
|
+
*/
|
|
72
|
+
export function parseCheckSpec(raw, defaultName) {
|
|
73
|
+
if (raw == null) {
|
|
74
|
+
assertSafeCheckToken(defaultName);
|
|
75
|
+
return { kind: 'version', cmd: defaultName, args: ['--version'] };
|
|
76
|
+
}
|
|
77
|
+
if (typeof raw === 'string') {
|
|
78
|
+
const tokens = raw.trim().split(/\s+/).filter((t) => t.length > 0);
|
|
79
|
+
if (tokens.length === 0) {
|
|
80
|
+
assertSafeCheckToken(defaultName);
|
|
81
|
+
return { kind: 'version', cmd: defaultName, args: ['--version'] };
|
|
82
|
+
}
|
|
83
|
+
for (const tok of tokens)
|
|
84
|
+
assertSafeCheckToken(tok);
|
|
85
|
+
const [cmd, ...args] = tokens;
|
|
86
|
+
return args.length === 0 ? { kind: 'which', cmd } : { kind: 'version', cmd, args };
|
|
87
|
+
}
|
|
88
|
+
if (typeof raw === 'object') {
|
|
89
|
+
const r = raw;
|
|
90
|
+
const kind = r.kind;
|
|
91
|
+
if (kind !== 'which' && kind !== 'version') {
|
|
92
|
+
throw new Error(`check.kind must be "which" or "version" (got ${JSON.stringify(kind)})`);
|
|
93
|
+
}
|
|
94
|
+
if (typeof r.cmd !== 'string' || !r.cmd.trim()) {
|
|
95
|
+
throw new Error('check.cmd must be a non-empty string');
|
|
96
|
+
}
|
|
97
|
+
const cmd = r.cmd.trim();
|
|
98
|
+
assertSafeCheckToken(cmd);
|
|
99
|
+
if (kind === 'which')
|
|
100
|
+
return { kind: 'which', cmd };
|
|
101
|
+
const args = Array.isArray(r.args) ? r.args : [];
|
|
102
|
+
const safeArgs = [];
|
|
103
|
+
for (const a of args) {
|
|
104
|
+
if (typeof a !== 'string')
|
|
105
|
+
throw new Error('check.args entries must be strings');
|
|
106
|
+
assertSafeCheckToken(a);
|
|
107
|
+
safeArgs.push(a);
|
|
108
|
+
}
|
|
109
|
+
return { kind: 'version', cmd, args: safeArgs };
|
|
110
|
+
}
|
|
111
|
+
throw new Error('check must be a string or an object with { kind, cmd, args? }');
|
|
112
|
+
}
|
|
16
113
|
/**
|
|
17
114
|
* Parse a single CLI manifest from its YAML contents.
|
|
18
115
|
* Returns a manifest on success; throws on schema violations so callers can
|
|
@@ -24,11 +121,10 @@ export function parseCliManifest(contents, opts) {
|
|
|
24
121
|
throw new Error('manifest must be a YAML object');
|
|
25
122
|
}
|
|
26
123
|
const name = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : opts.name;
|
|
124
|
+
assertSafeCheckToken(name);
|
|
27
125
|
const description = typeof raw.description === 'string' ? raw.description : undefined;
|
|
28
126
|
const homepage = typeof raw.homepage === 'string' ? raw.homepage : undefined;
|
|
29
|
-
const check =
|
|
30
|
-
? raw.check.trim()
|
|
31
|
-
: `${name} --version`;
|
|
127
|
+
const check = parseCheckSpec(raw.check, name);
|
|
32
128
|
const postInstall = typeof raw.post_install === 'string' ? raw.post_install : undefined;
|
|
33
129
|
if (!Array.isArray(raw.install) || raw.install.length === 0) {
|
|
34
130
|
throw new Error('install must be a non-empty list of methods');
|
|
@@ -44,11 +140,29 @@ export function parseCliManifest(contents, opts) {
|
|
|
44
140
|
}
|
|
45
141
|
const key = keys[0];
|
|
46
142
|
const value = e[key];
|
|
47
|
-
if (key === 'npm'
|
|
143
|
+
if (key === 'npm') {
|
|
144
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
145
|
+
throw new Error(`install[${i}].npm must be a non-empty string`);
|
|
146
|
+
}
|
|
147
|
+
const v = value.trim();
|
|
148
|
+
assertNpmPackage(v);
|
|
149
|
+
return { npm: v };
|
|
150
|
+
}
|
|
151
|
+
if (key === 'brew') {
|
|
152
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
153
|
+
throw new Error(`install[${i}].brew must be a non-empty string`);
|
|
154
|
+
}
|
|
155
|
+
const v = value.trim();
|
|
156
|
+
assertBrewFormula(v);
|
|
157
|
+
return { brew: v };
|
|
158
|
+
}
|
|
159
|
+
if (key === 'script') {
|
|
48
160
|
if (typeof value !== 'string' || !value.trim()) {
|
|
49
|
-
throw new Error(`install[${i}]
|
|
161
|
+
throw new Error(`install[${i}].script must be a non-empty string`);
|
|
50
162
|
}
|
|
51
|
-
|
|
163
|
+
const v = value.trim();
|
|
164
|
+
assertHttpsUrl(v);
|
|
165
|
+
return { script: v };
|
|
52
166
|
}
|
|
53
167
|
if (key === 'binary') {
|
|
54
168
|
if (!value || typeof value !== 'object') {
|
|
@@ -63,10 +177,14 @@ export function parseCliManifest(contents, opts) {
|
|
|
63
177
|
if (typeof s.url !== 'string' || !s.url.trim()) {
|
|
64
178
|
throw new Error(`install[${i}].binary.${platform}.url must be a non-empty string`);
|
|
65
179
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
180
|
+
const url = s.url.trim();
|
|
181
|
+
assertHttpsUrl(url);
|
|
182
|
+
let extract;
|
|
183
|
+
if (typeof s.extract === 'string' && s.extract.length > 0) {
|
|
184
|
+
assertSafePathSegment(s.extract);
|
|
185
|
+
extract = s.extract;
|
|
186
|
+
}
|
|
187
|
+
binary[platform] = { url, extract };
|
|
70
188
|
}
|
|
71
189
|
return { binary };
|
|
72
190
|
}
|
|
@@ -125,25 +243,34 @@ export function resolveCliManifest(name, cwd) {
|
|
|
125
243
|
}
|
|
126
244
|
// ─── Host detection ──────────────────────────────────────────────────────────
|
|
127
245
|
/**
|
|
128
|
-
* Return true if a command resolves on the current PATH. Uses `
|
|
129
|
-
*
|
|
246
|
+
* Return true if a command resolves on the current PATH. Uses POSIX `command -v`
|
|
247
|
+
* via spawn argv (no shell); results are cached for the lifetime of the process.
|
|
130
248
|
*/
|
|
131
249
|
const cmdExistsCache = new Map();
|
|
132
250
|
export function hasCommand(cmd) {
|
|
133
251
|
if (cmdExistsCache.has(cmd))
|
|
134
252
|
return cmdExistsCache.get(cmd);
|
|
135
|
-
|
|
253
|
+
// `command` is a shell builtin on most POSIX shells; invoking `sh -c 'command -v X'`
|
|
254
|
+
// with X as an *argument* (not interpolated) is the safe path. `cmd` may be passed
|
|
255
|
+
// by callers that haven't validated it, so we route via argv to neutralize metas.
|
|
256
|
+
const result = spawnSync('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', '_', cmd], {
|
|
257
|
+
stdio: 'ignore',
|
|
258
|
+
});
|
|
136
259
|
const ok = result.status === 0;
|
|
137
260
|
cmdExistsCache.set(cmd, ok);
|
|
138
261
|
return ok;
|
|
139
262
|
}
|
|
140
|
-
/**
|
|
263
|
+
/**
|
|
264
|
+
* Run the manifest's check. Dispatches on CheckSpec.kind — never invokes a
|
|
265
|
+
* shell, never interpolates strings into a command line.
|
|
266
|
+
*/
|
|
141
267
|
export function isCliInstalled(manifest) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
268
|
+
const c = manifest.check;
|
|
269
|
+
if (c.kind === 'which') {
|
|
270
|
+
cmdExistsCache.delete(c.cmd);
|
|
271
|
+
return hasCommand(c.cmd);
|
|
272
|
+
}
|
|
273
|
+
const result = spawnSync(c.cmd, c.args, { stdio: 'ignore', timeout: 10_000 });
|
|
147
274
|
return result.status === 0;
|
|
148
275
|
}
|
|
149
276
|
// ─── Method selection ────────────────────────────────────────────────────────
|
|
@@ -167,6 +294,10 @@ export function selectInstallMethod(manifest) {
|
|
|
167
294
|
}
|
|
168
295
|
return null;
|
|
169
296
|
}
|
|
297
|
+
/** Render a CheckSpec back to a human-readable command string (display only). */
|
|
298
|
+
export function describeCheck(check) {
|
|
299
|
+
return check.kind === 'which' ? check.cmd : `${check.cmd} ${check.args.join(' ')}`.trim();
|
|
300
|
+
}
|
|
170
301
|
/** Short description of a method for display. */
|
|
171
302
|
export function describeMethod(method) {
|
|
172
303
|
if ('npm' in method)
|
|
@@ -179,6 +310,122 @@ export function describeMethod(method) {
|
|
|
179
310
|
const spec = method.binary[key];
|
|
180
311
|
return spec ? `download ${spec.url}` : 'binary download';
|
|
181
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Display-only rendering of how a method would be run, for `--dry-run` and
|
|
315
|
+
* status output. Not used by installCli — execution goes through runInstallMethod
|
|
316
|
+
* which dispatches to spawnSync with argv arrays.
|
|
317
|
+
*/
|
|
318
|
+
export function buildInstallCommand(method) {
|
|
319
|
+
if ('npm' in method)
|
|
320
|
+
return `npm install -g ${method.npm}`;
|
|
321
|
+
if ('brew' in method)
|
|
322
|
+
return `brew install ${method.brew}`;
|
|
323
|
+
if ('script' in method) {
|
|
324
|
+
return hasCommand('curl')
|
|
325
|
+
? `curl -fsSL ${method.script} | sh`
|
|
326
|
+
: `wget -qO- ${method.script} | sh`;
|
|
327
|
+
}
|
|
328
|
+
const key = `${process.platform}-${process.arch}`;
|
|
329
|
+
const spec = method.binary[key];
|
|
330
|
+
if (!spec)
|
|
331
|
+
return 'binary download';
|
|
332
|
+
return spec.extract
|
|
333
|
+
? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C /usr/local/bin ${spec.extract}`
|
|
334
|
+
: `curl -fsSL ${spec.url} -o /usr/local/bin/agents-cli-downloaded`;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Execute an install method via spawnSync with argv arrays. Each branch
|
|
338
|
+
* re-validates the relevant field — defense in depth, since callers may
|
|
339
|
+
* construct InstallMethod values without going through parseCliManifest
|
|
340
|
+
* (tests, future programmatic use).
|
|
341
|
+
*
|
|
342
|
+
* For `script`, the download is staged to a temp file and then exec'd as
|
|
343
|
+
* `sh <file>` so we never need a shell pipe (`curl | sh`).
|
|
344
|
+
*/
|
|
345
|
+
function runInstallMethod(method) {
|
|
346
|
+
if ('npm' in method) {
|
|
347
|
+
assertNpmPackage(method.npm);
|
|
348
|
+
const r = spawnSync('npm', ['install', '-g', method.npm], { stdio: 'inherit' });
|
|
349
|
+
if (r.status !== 0) {
|
|
350
|
+
throw new Error(`npm install -g ${method.npm} exited with status ${r.status ?? 'unknown'}`);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if ('brew' in method) {
|
|
355
|
+
assertBrewFormula(method.brew);
|
|
356
|
+
const r = spawnSync('brew', ['install', method.brew], { stdio: 'inherit' });
|
|
357
|
+
if (r.status !== 0) {
|
|
358
|
+
throw new Error(`brew install ${method.brew} exited with status ${r.status ?? 'unknown'}`);
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if ('script' in method) {
|
|
363
|
+
assertHttpsUrl(method.script);
|
|
364
|
+
const tmp = path.join(os.tmpdir(), `agents-cli-install-${process.pid}-${Date.now()}.sh`);
|
|
365
|
+
try {
|
|
366
|
+
let dl;
|
|
367
|
+
if (hasCommand('curl')) {
|
|
368
|
+
dl = spawnSync('curl', ['-fsSL', method.script, '-o', tmp], { stdio: 'inherit' });
|
|
369
|
+
}
|
|
370
|
+
else if (hasCommand('wget')) {
|
|
371
|
+
dl = spawnSync('wget', ['-q', '-O', tmp, method.script], { stdio: 'inherit' });
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
throw new Error('neither curl nor wget is available on PATH');
|
|
375
|
+
}
|
|
376
|
+
if (dl.status !== 0) {
|
|
377
|
+
throw new Error(`download of install script failed (status ${dl.status ?? 'unknown'})`);
|
|
378
|
+
}
|
|
379
|
+
const r = spawnSync('sh', [tmp], { stdio: 'inherit' });
|
|
380
|
+
if (r.status !== 0) {
|
|
381
|
+
throw new Error(`install script exited with status ${r.status ?? 'unknown'}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
try {
|
|
386
|
+
fs.unlinkSync(tmp);
|
|
387
|
+
}
|
|
388
|
+
catch { /* best effort */ }
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if ('binary' in method) {
|
|
393
|
+
const key = `${process.platform}-${process.arch}`;
|
|
394
|
+
const spec = method.binary[key];
|
|
395
|
+
if (!spec)
|
|
396
|
+
throw new Error(`no binary declared for ${key}`);
|
|
397
|
+
assertHttpsUrl(spec.url);
|
|
398
|
+
if (spec.extract) {
|
|
399
|
+
assertSafePathSegment(spec.extract);
|
|
400
|
+
const tmp = path.join(os.tmpdir(), `agents-cli-bin-${process.pid}-${Date.now()}.tgz`);
|
|
401
|
+
try {
|
|
402
|
+
const dl = spawnSync('curl', ['-fsSL', spec.url, '-o', tmp], { stdio: 'inherit' });
|
|
403
|
+
if (dl.status !== 0) {
|
|
404
|
+
throw new Error(`binary download failed (status ${dl.status ?? 'unknown'})`);
|
|
405
|
+
}
|
|
406
|
+
const x = spawnSync('tar', ['-xzf', tmp, '-C', '/usr/local/bin', spec.extract], {
|
|
407
|
+
stdio: 'inherit',
|
|
408
|
+
});
|
|
409
|
+
if (x.status !== 0) {
|
|
410
|
+
throw new Error(`tar extract failed (status ${x.status ?? 'unknown'})`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
try {
|
|
415
|
+
fs.unlinkSync(tmp);
|
|
416
|
+
}
|
|
417
|
+
catch { /* best effort */ }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
const r = spawnSync('curl', ['-fsSL', spec.url, '-o', '/usr/local/bin/agents-cli-downloaded'], { stdio: 'inherit' });
|
|
422
|
+
if (r.status !== 0) {
|
|
423
|
+
throw new Error(`binary download failed (status ${r.status ?? 'unknown'})`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
182
429
|
/**
|
|
183
430
|
* Install a single CLI by running its first compatible method. Streams the
|
|
184
431
|
* underlying command's output to the parent terminal so users see brew/npm
|
|
@@ -197,9 +444,8 @@ export function installCli(manifest, opts = {}) {
|
|
|
197
444
|
if (opts.dryRun) {
|
|
198
445
|
return { manifest, method, installed: false, output: `[dry-run] would run: ${describeMethod(method)}` };
|
|
199
446
|
}
|
|
200
|
-
const cmd = buildInstallCommand(method);
|
|
201
447
|
try {
|
|
202
|
-
|
|
448
|
+
runInstallMethod(method);
|
|
203
449
|
}
|
|
204
450
|
catch (err) {
|
|
205
451
|
return {
|
|
@@ -216,30 +462,6 @@ export function installCli(manifest, opts = {}) {
|
|
|
216
462
|
const installed = isCliInstalled(manifest);
|
|
217
463
|
return { manifest, method, installed };
|
|
218
464
|
}
|
|
219
|
-
/**
|
|
220
|
-
* Map a declarative method to a shell command. Centralized so tests and dry-run
|
|
221
|
-
* surface the exact string that would execute.
|
|
222
|
-
*/
|
|
223
|
-
export function buildInstallCommand(method) {
|
|
224
|
-
if ('npm' in method)
|
|
225
|
-
return `npm install -g ${method.npm}`;
|
|
226
|
-
if ('brew' in method)
|
|
227
|
-
return `brew install ${method.brew}`;
|
|
228
|
-
if ('script' in method) {
|
|
229
|
-
// Prefer curl when both are present; fall back to wget.
|
|
230
|
-
return hasCommand('curl')
|
|
231
|
-
? `curl -fsSL ${method.script} | sh`
|
|
232
|
-
: `wget -qO- ${method.script} | sh`;
|
|
233
|
-
}
|
|
234
|
-
const key = `${process.platform}-${process.arch}`;
|
|
235
|
-
const spec = method.binary[key];
|
|
236
|
-
// The downloader is intentionally minimal — binary install is mostly used
|
|
237
|
-
// for pre-built tarballs whose extract path varies per project. We expect
|
|
238
|
-
// the manifest author to document any post-download steps in post_install.
|
|
239
|
-
return spec.extract
|
|
240
|
-
? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C /usr/local/bin ${spec.extract}`
|
|
241
|
-
: `curl -fsSL ${spec.url} -o /usr/local/bin/agents-cli-downloaded`;
|
|
242
|
-
}
|
|
243
465
|
/** Convenience: list all manifests + their installed-on-host status. */
|
|
244
466
|
export function listCliStatus(cwd) {
|
|
245
467
|
const { manifests, errors } = listCliManifests(cwd);
|
|
@@ -8,7 +8,7 @@ import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOpt
|
|
|
8
8
|
/**
|
|
9
9
|
* Factory/Droid cloud provider — stub for Phase 2.
|
|
10
10
|
*
|
|
11
|
-
* Integration path: `droid daemon` running on a remote machine (
|
|
11
|
+
* Integration path: `droid daemon` running on a remote machine (workstation, cloud VM, k8s pod).
|
|
12
12
|
* Dispatch via HTTP to the daemon, stream output, cancel via HTTP DELETE.
|
|
13
13
|
*
|
|
14
14
|
* Not yet implemented because:
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/**
|
|
8
8
|
* Factory/Droid cloud provider — stub for Phase 2.
|
|
9
9
|
*
|
|
10
|
-
* Integration path: `droid daemon` running on a remote machine (
|
|
10
|
+
* Integration path: `droid daemon` running on a remote machine (workstation, cloud VM, k8s pod).
|
|
11
11
|
* Dispatch via HTTP to the daemon, stream output, cancel via HTTP DELETE.
|
|
12
12
|
*
|
|
13
13
|
* Not yet implemented because:
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -32,7 +32,8 @@ export interface EventPayload {
|
|
|
32
32
|
args?: string[];
|
|
33
33
|
input?: string;
|
|
34
34
|
output?: string;
|
|
35
|
-
|
|
35
|
+
prompt_length?: number;
|
|
36
|
+
prompt_sha256?: string;
|
|
36
37
|
durationMs?: number;
|
|
37
38
|
startupMs?: number;
|
|
38
39
|
exitCode?: number;
|
|
@@ -42,6 +43,19 @@ export interface EventPayload {
|
|
|
42
43
|
[key: string]: unknown;
|
|
43
44
|
}
|
|
44
45
|
export type EventRecord = EventMeta & EventPayload;
|
|
46
|
+
/**
|
|
47
|
+
* Replace a prompt string with length + short SHA so we can correlate runs
|
|
48
|
+
* without persisting the raw text. Returns the fields to spread into a payload.
|
|
49
|
+
*/
|
|
50
|
+
export declare function redactPrompt(prompt: string | null | undefined): {
|
|
51
|
+
prompt_length?: number;
|
|
52
|
+
prompt_sha256?: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Mask argv entries that look like tokens or secret paths. Preserves structure
|
|
56
|
+
* for debugging but drops the sensitive substring.
|
|
57
|
+
*/
|
|
58
|
+
export declare function redactArgs(args: string[] | undefined): string[] | undefined;
|
|
45
59
|
/**
|
|
46
60
|
* Truncate a string to maxLength, adding ellipsis if truncated.
|
|
47
61
|
* Returns undefined for null/undefined input.
|
|
@@ -124,7 +138,7 @@ export declare function emitError(err: Error | string, payload?: EventPayload):
|
|
|
124
138
|
* Remove log files older than the retention period.
|
|
125
139
|
* Called lazily on emit or explicitly via CLI.
|
|
126
140
|
*
|
|
127
|
-
* @param retentionDays - Number of days to keep (default
|
|
141
|
+
* @param retentionDays - Number of days to keep (default 7, from DEFAULT_RETENTION_DAYS)
|
|
128
142
|
* @returns Number of files removed
|
|
129
143
|
*/
|
|
130
144
|
export declare function rotate(retentionDays?: number): number;
|
package/dist/lib/events.js
CHANGED
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import * as os from 'os';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
17
18
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
18
19
|
// Logs live under the cache bucket — they're regenerable telemetry.
|
|
19
20
|
const LOGS_DIR = path.join(os.homedir(), '.agents', '.cache', 'logs');
|
|
20
21
|
/** Default retention period in days. */
|
|
21
|
-
const DEFAULT_RETENTION_DAYS =
|
|
22
|
+
const DEFAULT_RETENTION_DAYS = 7;
|
|
22
23
|
/** Default max length for truncated strings. */
|
|
23
24
|
const DEFAULT_TRUNCATE_LENGTH = 500;
|
|
24
25
|
/** Environment variable to disable event logging. */
|
|
@@ -68,6 +69,36 @@ function ensureLogsDir() {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
// ─── Redaction ────────────────────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Replace a prompt string with length + short SHA so we can correlate runs
|
|
75
|
+
* without persisting the raw text. Returns the fields to spread into a payload.
|
|
76
|
+
*/
|
|
77
|
+
export function redactPrompt(prompt) {
|
|
78
|
+
if (prompt == null)
|
|
79
|
+
return {};
|
|
80
|
+
return {
|
|
81
|
+
prompt_length: prompt.length,
|
|
82
|
+
prompt_sha256: createHash('sha256').update(prompt).digest('hex').slice(0, 16),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const TOKEN_LIKE = /(sk_(?:live|test)_|pk_(?:live|test)_|ghp_|gho_|ghu_|ghs_|xox[bpars]-|AKIA|ASIA|AIza|Bearer\s+|eyJ[A-Za-z0-9_-]+\.)/i;
|
|
86
|
+
const SECRET_PATH = /\/(secrets|credentials|\.env|user\.yaml)\b/i;
|
|
87
|
+
/**
|
|
88
|
+
* Mask argv entries that look like tokens or secret paths. Preserves structure
|
|
89
|
+
* for debugging but drops the sensitive substring.
|
|
90
|
+
*/
|
|
91
|
+
export function redactArgs(args) {
|
|
92
|
+
if (!args)
|
|
93
|
+
return undefined;
|
|
94
|
+
return args.map(a => {
|
|
95
|
+
if (typeof a !== 'string')
|
|
96
|
+
return a;
|
|
97
|
+
if (TOKEN_LIKE.test(a) || SECRET_PATH.test(a))
|
|
98
|
+
return '[REDACTED]';
|
|
99
|
+
return a;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
71
102
|
// ─── Truncation ───────────────────────────────────────────────────────────────
|
|
72
103
|
/**
|
|
73
104
|
* Truncate a string to maxLength, adding ellipsis if truncated.
|
|
@@ -324,7 +355,7 @@ export function emitError(err, payload = {}) {
|
|
|
324
355
|
* Remove log files older than the retention period.
|
|
325
356
|
* Called lazily on emit or explicitly via CLI.
|
|
326
357
|
*
|
|
327
|
-
* @param retentionDays - Number of days to keep (default
|
|
358
|
+
* @param retentionDays - Number of days to keep (default 7, from DEFAULT_RETENTION_DAYS)
|
|
328
359
|
* @returns Number of files removed
|
|
329
360
|
*/
|
|
330
361
|
export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
import type { AgentId } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
|
|
1
|
+
import type { AgentId, Mode } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Agent execution modes. Canonical name `skip` (dangerously skip permissions);
|
|
4
|
+
* `full` is accepted as a permanent silent alias via normalizeMode().
|
|
5
|
+
*/
|
|
6
|
+
export type ExecMode = Mode;
|
|
7
|
+
/**
|
|
8
|
+
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
9
|
+
*
|
|
10
|
+
* Accepts the historical `full` spelling and rewrites it to `skip`. Throws on
|
|
11
|
+
* anything outside the four canonical values so bad input fails loud at the
|
|
12
|
+
* boundary rather than silently picking a wrong code path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeMode(input: string | null | undefined): Mode;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a requested mode against an agent's capability table.
|
|
17
|
+
*
|
|
18
|
+
* - `auto` on an agent without auto support silently degrades to `edit`
|
|
19
|
+
* (every agent supports edit-like behavior as its default).
|
|
20
|
+
* - `skip` on an agent without skip support throws with a clear message
|
|
21
|
+
* naming the agent's supported modes. No silent fallback — the user
|
|
22
|
+
* explicitly asked to bypass permissions; pretending we did is unsafe.
|
|
23
|
+
* - `plan` on an agent without plan support throws the same way.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveMode(agent: AgentId, requested: Mode): Mode;
|
|
4
26
|
/** Reasoning effort levels passed to agents that support them. 'auto' defers to the agent's default. */
|
|
5
27
|
export type ExecEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
|
|
6
28
|
/** Options for spawning an agent process. Omitting `prompt` launches the CLI interactively. */
|
|
@@ -32,22 +54,28 @@ export declare function parseExecEnv(entries: string[]): Record<string, string>
|
|
|
32
54
|
* into unrelated invocations.
|
|
33
55
|
*/
|
|
34
56
|
export declare function buildExecEnv(options: ExecOptions): NodeJS.ProcessEnv;
|
|
35
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Describes how to translate ExecOptions into CLI arguments for a specific agent.
|
|
59
|
+
*
|
|
60
|
+
* `modeFlags` only declares modes this agent natively supports. Keys must agree
|
|
61
|
+
* with AGENTS[agent].capabilities.modes — resolveMode() routes a request to a
|
|
62
|
+
* supported mode (or throws), then buildExecCommand looks up the flags here.
|
|
63
|
+
*/
|
|
36
64
|
export interface AgentCommandTemplate {
|
|
37
65
|
base: string[];
|
|
38
66
|
promptFlag: 'positional' | string;
|
|
39
|
-
modeFlags:
|
|
40
|
-
plan: string[];
|
|
41
|
-
edit: string[];
|
|
42
|
-
full: string[];
|
|
43
|
-
auto?: string[];
|
|
44
|
-
};
|
|
67
|
+
modeFlags: Partial<Record<Mode, string[]>>;
|
|
45
68
|
jsonFlags?: string[];
|
|
46
69
|
modelFlag?: string;
|
|
47
70
|
printFlags?: string[];
|
|
48
71
|
verboseFlag?: string;
|
|
49
72
|
}
|
|
50
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* CLI command templates for every supported agent.
|
|
75
|
+
*
|
|
76
|
+
* Each agent's `modeFlags` keys MUST match the modes listed in
|
|
77
|
+
* AGENTS[agent].capabilities.modes. A test in exec.test.ts asserts this.
|
|
78
|
+
*/
|
|
51
79
|
export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
|
|
52
80
|
/** Assemble the full CLI argument array for an agent invocation. */
|
|
53
81
|
export declare function buildExecCommand(options: ExecOptions): string[];
|