@phnx-labs/agents-cli 1.20.5 → 1.20.7
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 +13 -0
- package/README.md +1 -1
- package/dist/commands/browser.js +31 -4
- package/dist/commands/computer-actions.d.ts +36 -0
- package/dist/commands/computer-actions.js +328 -0
- package/dist/commands/computer.js +74 -55
- package/dist/commands/defaults.d.ts +7 -0
- package/dist/commands/defaults.js +89 -0
- package/dist/commands/exec.js +24 -6
- package/dist/commands/inspect.d.ts +38 -7
- package/dist/commands/inspect.js +194 -24
- package/dist/commands/rules.js +3 -3
- package/dist/commands/secrets.js +46 -9
- package/dist/commands/sessions.js +9 -12
- package/dist/commands/setup.js +2 -2
- package/dist/commands/teams.js +108 -11
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +121 -38
- package/dist/index.js +61 -22
- package/dist/lib/agents.d.ts +10 -6
- package/dist/lib/agents.js +23 -14
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +84 -3
- package/dist/lib/daemon.js +4 -7
- package/dist/lib/exec.d.ts +9 -0
- package/dist/lib/exec.js +85 -9
- package/dist/lib/migrate.js +6 -4
- package/dist/lib/permissions.d.ts +23 -0
- package/dist/lib/permissions.js +89 -7
- package/dist/lib/platform/exec.d.ts +9 -0
- package/dist/lib/platform/exec.js +24 -0
- package/dist/lib/platform/index.d.ts +20 -0
- package/dist/lib/platform/index.js +20 -0
- package/dist/lib/platform/paths.d.ts +22 -0
- package/dist/lib/platform/paths.js +49 -0
- package/dist/lib/platform/process.d.ts +12 -0
- package/dist/lib/platform/process.js +22 -0
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/project-launch.d.ts +5 -0
- package/dist/lib/project-launch.js +37 -0
- package/dist/lib/pty-client.js +13 -5
- package/dist/lib/pty-server.d.ts +24 -1
- package/dist/lib/pty-server.js +109 -29
- package/dist/lib/resources/rules.js +1 -1
- package/dist/lib/resources/skills.js +1 -1
- package/dist/lib/resources.d.ts +2 -0
- package/dist/lib/resources.js +2 -1
- package/dist/lib/rotate.js +6 -18
- package/dist/lib/run-config.d.ts +9 -0
- package/dist/lib/run-config.js +35 -0
- package/dist/lib/run-defaults.d.ts +42 -0
- package/dist/lib/run-defaults.js +180 -0
- 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/install-helper.d.ts +11 -3
- package/dist/lib/secrets/install-helper.js +48 -6
- package/dist/lib/secrets/linux.d.ts +12 -0
- package/dist/lib/secrets/linux.js +30 -16
- package/dist/lib/session/artifacts.js +8 -2
- package/dist/lib/shims.d.ts +9 -1
- package/dist/lib/shims.js +80 -3
- package/dist/lib/staleness/detectors/hooks.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/teams/agents.js +5 -7
- package/dist/lib/teams/api.d.ts +67 -0
- package/dist/lib/teams/api.js +78 -0
- package/dist/lib/types.d.ts +15 -6
- package/dist/lib/versions.js +4 -4
- package/package.json +5 -2
- package/scripts/postinstall.js +18 -1
package/dist/lib/pty-server.js
CHANGED
|
@@ -15,6 +15,7 @@ import * as crypto from 'crypto';
|
|
|
15
15
|
import { execFileSync } from 'child_process';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
17
|
import { getPtyDir as getPtyDirRoot } from './state.js';
|
|
18
|
+
import { isAlive } from './platform/index.js';
|
|
18
19
|
/**
|
|
19
20
|
* Capture a stable identifier for a process at the moment it was started.
|
|
20
21
|
* Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
|
|
@@ -52,6 +53,7 @@ export function captureProcessStartTime(pid) {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
// --- Constants ---
|
|
56
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
55
57
|
const SENTINEL = '__AGENTS_PTY_DONE__';
|
|
56
58
|
const SOCKET_NAME = 'pty.sock';
|
|
57
59
|
const PID_FILE = 'pty.pid';
|
|
@@ -72,24 +74,81 @@ const PTY_ENV_ALLOWLIST = [
|
|
|
72
74
|
'EDITOR', 'VISUAL', 'PAGER', 'LESS',
|
|
73
75
|
'NO_COLOR', 'FORCE_COLOR',
|
|
74
76
|
];
|
|
77
|
+
/**
|
|
78
|
+
* Windows allowlist. cmd.exe / PowerShell refuse to start (or misbehave) without
|
|
79
|
+
* SystemRoot, ComSpec, PATHEXT and the USERPROFILE/APPDATA family, so a Unix-style
|
|
80
|
+
* allowlist would spawn a broken shell. PATH/TERM/color/NODE vars are shared with
|
|
81
|
+
* the Unix list; the rest are Windows-specific.
|
|
82
|
+
*/
|
|
83
|
+
const PTY_ENV_ALLOWLIST_WIN = [
|
|
84
|
+
'SystemRoot', 'SystemDrive', 'windir', 'ComSpec', 'PATH', 'PATHEXT',
|
|
85
|
+
'TEMP', 'TMP', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'HOME',
|
|
86
|
+
'APPDATA', 'LOCALAPPDATA', 'PROGRAMFILES', 'PROGRAMDATA',
|
|
87
|
+
'USERNAME', 'USERDOMAIN', 'COMPUTERNAME', 'OS',
|
|
88
|
+
'PROCESSOR_ARCHITECTURE', 'NUMBER_OF_PROCESSORS',
|
|
89
|
+
'TERM', 'COLORTERM', 'NO_COLOR', 'FORCE_COLOR',
|
|
90
|
+
'NODE_PATH', 'BUN_INSTALL',
|
|
91
|
+
];
|
|
75
92
|
function buildPtyEnv() {
|
|
76
93
|
const env = {};
|
|
77
|
-
|
|
94
|
+
const allowlist = IS_WINDOWS ? PTY_ENV_ALLOWLIST_WIN : PTY_ENV_ALLOWLIST;
|
|
95
|
+
for (const key of allowlist) {
|
|
78
96
|
const v = process.env[key];
|
|
79
97
|
if (v !== undefined)
|
|
80
98
|
env[key] = v;
|
|
81
99
|
}
|
|
82
100
|
return env;
|
|
83
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Wrap a user command so a `__SENTINEL__:<exit>` line is printed after it
|
|
104
|
+
* finishes — that line drives completion detection in the exec/read flow.
|
|
105
|
+
* The separator and exit-code variable are shell-family specific:
|
|
106
|
+
* POSIX sh/zsh/bash : `cmd; echo "S:$?"`
|
|
107
|
+
* PowerShell : `cmd; echo "S:$LASTEXITCODE"`
|
|
108
|
+
* cmd.exe : `cmd & echo S:%errorlevel%` (`&` always runs the echo)
|
|
109
|
+
* Only the completion marker matters; the numeric exit code is informational
|
|
110
|
+
* (the authoritative code comes from node-pty's onExit).
|
|
111
|
+
*/
|
|
112
|
+
export function buildSentinelCommand(shell, command) {
|
|
113
|
+
// Split on both separators: a Windows shell path (`C:\…\cmd.exe`) must be
|
|
114
|
+
// recognized even when this code runs under POSIX path.basename, which does
|
|
115
|
+
// not treat `\` as a separator.
|
|
116
|
+
const name = (shell.split(/[\\/]/).pop() || shell).toLowerCase();
|
|
117
|
+
if (name === 'cmd.exe' || name === 'cmd') {
|
|
118
|
+
return `${command} & echo ${SENTINEL}:%errorlevel%`;
|
|
119
|
+
}
|
|
120
|
+
if (name === 'powershell.exe' || name === 'powershell' || name === 'pwsh.exe' || name === 'pwsh') {
|
|
121
|
+
return `${command}; echo "${SENTINEL}:$LASTEXITCODE"`;
|
|
122
|
+
}
|
|
123
|
+
return `${command}; echo "${SENTINEL}:$?"`;
|
|
124
|
+
}
|
|
84
125
|
/** Get the PTY helper directory, creating it if needed. */
|
|
85
126
|
function getPtyDir() {
|
|
86
127
|
const dir = getPtyDirRoot();
|
|
87
128
|
fs.mkdirSync(dir, { recursive: true });
|
|
88
129
|
return dir;
|
|
89
130
|
}
|
|
90
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the IPC endpoint for a given platform + PTY scratch dir. Pure so both
|
|
133
|
+
* branches are testable without stubbing process.platform.
|
|
134
|
+
*
|
|
135
|
+
* Unix: an AF_UNIX socket file inside the scratch dir.
|
|
136
|
+
* Windows: a named pipe (`\\.\pipe\…`). Named pipes are NOT filesystem objects,
|
|
137
|
+
* so the name is derived from a hash of the (per-user) scratch dir to keep it
|
|
138
|
+
* stable across invocations and isolated per user — and callers must never probe
|
|
139
|
+
* it with fs.existsSync (it always reports false). Both forms are accepted by
|
|
140
|
+
* net.createServer/createConnection.
|
|
141
|
+
*/
|
|
142
|
+
export function derivePtyEndpoint(platform, ptyDir) {
|
|
143
|
+
if (platform === 'win32') {
|
|
144
|
+
const hash = crypto.createHash('sha1').update(ptyDir).digest('hex').slice(0, 16);
|
|
145
|
+
return `\\\\.\\pipe\\agents-pty-${hash}`;
|
|
146
|
+
}
|
|
147
|
+
return path.join(ptyDir, SOCKET_NAME);
|
|
148
|
+
}
|
|
149
|
+
/** Get the IPC endpoint the PTY server listens on / clients connect to. */
|
|
91
150
|
export function getSocketPath() {
|
|
92
|
-
return
|
|
151
|
+
return derivePtyEndpoint(process.platform, getPtyDir());
|
|
93
152
|
}
|
|
94
153
|
/** Get the path to the PTY server PID file. */
|
|
95
154
|
export function getPtyPidPath() {
|
|
@@ -109,16 +168,17 @@ export function isPtyServerRunning() {
|
|
|
109
168
|
const pid = parseInt(fs.readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
110
169
|
if (isNaN(pid))
|
|
111
170
|
return false;
|
|
112
|
-
|
|
113
|
-
|
|
171
|
+
if (isAlive(pid))
|
|
172
|
+
return true;
|
|
114
173
|
}
|
|
115
174
|
catch {
|
|
116
|
-
|
|
117
|
-
fs.unlinkSync(pidPath);
|
|
118
|
-
}
|
|
119
|
-
catch { }
|
|
120
|
-
return false;
|
|
175
|
+
// read failed — fall through and treat the pid file as stale
|
|
121
176
|
}
|
|
177
|
+
try {
|
|
178
|
+
fs.unlinkSync(pidPath);
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
181
|
+
return false;
|
|
122
182
|
}
|
|
123
183
|
// --- Logging ---
|
|
124
184
|
function rotateLogsIfNeeded(logPath) {
|
|
@@ -155,14 +215,17 @@ export async function runPtyServer() {
|
|
|
155
215
|
let nodePty;
|
|
156
216
|
let XtermTerminal;
|
|
157
217
|
try {
|
|
158
|
-
|
|
218
|
+
// The Homebridge multiarch fork of node-pty: API-identical (same 1.x N-API
|
|
219
|
+
// codebase) but ships prebuilt binaries for Linux glibc + musl, x64 + arm64
|
|
220
|
+
// (plus macOS/Windows), so no compiler is needed on Linux/Alpine/arm64.
|
|
221
|
+
nodePty = await import('@homebridge/node-pty-prebuilt-multiarch');
|
|
159
222
|
// Handle ESM default export
|
|
160
223
|
if (nodePty.default?.spawn)
|
|
161
224
|
nodePty = nodePty.default;
|
|
162
225
|
// Ensure spawn-helper is executable (bun install doesn't set +x on prebuilds)
|
|
163
226
|
try {
|
|
164
227
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
165
|
-
const ptyBase = path.resolve(__dirname, '..', '..', 'node_modules', 'node-pty');
|
|
228
|
+
const ptyBase = path.resolve(__dirname, '..', '..', 'node_modules', '@homebridge', 'node-pty-prebuilt-multiarch');
|
|
166
229
|
const helpers = [
|
|
167
230
|
path.join(ptyBase, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper'),
|
|
168
231
|
path.join(ptyBase, 'build', 'Release', 'spawn-helper'),
|
|
@@ -176,8 +239,8 @@ export async function runPtyServer() {
|
|
|
176
239
|
catch { }
|
|
177
240
|
}
|
|
178
241
|
catch (err) {
|
|
179
|
-
console.error('node-pty is required for PTY support.');
|
|
180
|
-
console.error('Install:
|
|
242
|
+
console.error('node-pty (@homebridge/node-pty-prebuilt-multiarch) is required for PTY support.');
|
|
243
|
+
console.error('Install: bun add @homebridge/node-pty-prebuilt-multiarch');
|
|
181
244
|
process.exit(1);
|
|
182
245
|
}
|
|
183
246
|
try {
|
|
@@ -218,8 +281,10 @@ export async function runPtyServer() {
|
|
|
218
281
|
fs.unlinkSync(pidPath);
|
|
219
282
|
}
|
|
220
283
|
catch { } });
|
|
221
|
-
// Remove stale socket from a prior crashed server. Safe now that we hold the PID
|
|
222
|
-
|
|
284
|
+
// Remove stale socket from a prior crashed server. Safe now that we hold the PID
|
|
285
|
+
// slot. Windows named pipes are not filesystem inodes — they vanish with their
|
|
286
|
+
// owning process, so there's nothing to unlink (and existsSync always reports false).
|
|
287
|
+
if (!IS_WINDOWS && fs.existsSync(socketPath)) {
|
|
223
288
|
try {
|
|
224
289
|
fs.unlinkSync(socketPath);
|
|
225
290
|
}
|
|
@@ -283,8 +348,10 @@ export async function runPtyServer() {
|
|
|
283
348
|
case 'start': {
|
|
284
349
|
const rows = req.params?.rows || 24;
|
|
285
350
|
const cols = req.params?.cols || 120;
|
|
286
|
-
const shell = req.params?.shell
|
|
287
|
-
|
|
351
|
+
const shell = req.params?.shell
|
|
352
|
+
|| (IS_WINDOWS ? (process.env.ComSpec || 'powershell.exe') : (process.env.SHELL || 'zsh'));
|
|
353
|
+
const cwd = req.params?.cwd
|
|
354
|
+
|| (IS_WINDOWS ? (process.env.USERPROFILE || process.env.HOME || process.cwd()) : (process.env.HOME || '/'));
|
|
288
355
|
const id = generateId();
|
|
289
356
|
let ptyProcess;
|
|
290
357
|
try {
|
|
@@ -355,7 +422,9 @@ export async function runPtyServer() {
|
|
|
355
422
|
session.appActive = true;
|
|
356
423
|
session.activeCommand = command;
|
|
357
424
|
session.pendingOutput = '';
|
|
358
|
-
|
|
425
|
+
// Windows conpty submits on CR; POSIX line discipline expects LF.
|
|
426
|
+
const submit = IS_WINDOWS ? '\r' : '\n';
|
|
427
|
+
session.pty.write(`${buildSentinelCommand(session.shell, command)}${submit}`);
|
|
359
428
|
session.lastActivity = Date.now();
|
|
360
429
|
return { ok: true, submitted: true };
|
|
361
430
|
}
|
|
@@ -522,20 +591,28 @@ export async function runPtyServer() {
|
|
|
522
591
|
// any local user with execute on the parent dir could connect to the socket
|
|
523
592
|
// during the listen()-to-chmod() window. macOS BSD AF_UNIX semantics make
|
|
524
593
|
// socket mode advisory only, so the parent dir is the real boundary.
|
|
594
|
+
//
|
|
595
|
+
// On Windows the transport is a named pipe, not a filesystem inode: chmod/umask
|
|
596
|
+
// are no-ops (and umask throws in some Node builds), and pipe ACLs default to
|
|
597
|
+
// the creating user. So we skip the Unix hardening entirely there.
|
|
525
598
|
const agentsDir = getPtyDirRoot();
|
|
526
599
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
600
|
+
if (!IS_WINDOWS) {
|
|
601
|
+
fs.chmodSync(agentsDir, 0o700);
|
|
602
|
+
// umask covers any inherited group/other bits while listen() is creating
|
|
603
|
+
// the socket inode — it only matters for the unobservable instant before
|
|
604
|
+
// we can chmod the inode itself.
|
|
605
|
+
process.umask(0o077);
|
|
606
|
+
}
|
|
532
607
|
await new Promise((resolve) => {
|
|
533
608
|
server.listen(socketPath, () => resolve());
|
|
534
609
|
});
|
|
535
610
|
// Surface chmod failures: a 0o600 socket is a load-bearing security
|
|
536
611
|
// assumption, not a nice-to-have. If we can't lock it down, refuse to
|
|
537
|
-
// start so the caller learns immediately.
|
|
538
|
-
|
|
612
|
+
// start so the caller learns immediately. (No-op on Windows named pipes.)
|
|
613
|
+
if (!IS_WINDOWS) {
|
|
614
|
+
fs.chmodSync(socketPath, 0o600);
|
|
615
|
+
}
|
|
539
616
|
log('INFO', `PTY server started (PID: ${process.pid}, socket: ${socketPath})`);
|
|
540
617
|
// Shutdown handler
|
|
541
618
|
function shutdown() {
|
|
@@ -546,10 +623,13 @@ export async function runPtyServer() {
|
|
|
546
623
|
sessions.clear();
|
|
547
624
|
clearInterval(cleanupInterval);
|
|
548
625
|
server.close();
|
|
549
|
-
|
|
550
|
-
|
|
626
|
+
// Named pipes are reclaimed by the OS on close; only Unix sockets leave a file.
|
|
627
|
+
if (!IS_WINDOWS) {
|
|
628
|
+
try {
|
|
629
|
+
fs.unlinkSync(socketPath);
|
|
630
|
+
}
|
|
631
|
+
catch { }
|
|
551
632
|
}
|
|
552
|
-
catch { }
|
|
553
633
|
try {
|
|
554
634
|
fs.unlinkSync(getPtyPidPath());
|
|
555
635
|
}
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* - All unique subrules across layers are unioned
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
|
+
import { agentConfigDirName } from '../agents.js';
|
|
12
13
|
import * as path from 'path';
|
|
13
14
|
import { getSystemRulesDir, getUserRulesDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
|
|
14
|
-
import { agentConfigDirName } from '../agents.js';
|
|
15
15
|
const SUBRULES_DIR = 'subrules';
|
|
16
16
|
const SUBRULES_README = 'README.md';
|
|
17
17
|
/**
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Format is the same for all agents. Resolution order: project > user > system.
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
|
+
import { agentConfigDirName } from '../agents.js';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import * as yaml from 'yaml';
|
|
10
11
|
import { getSystemSkillsDir, getUserSkillsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
|
|
11
|
-
import { agentConfigDirName } from '../agents.js';
|
|
12
12
|
/** Default provider uses the real state module. */
|
|
13
13
|
const defaultProvider = {
|
|
14
14
|
getSystemSkillsDir,
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface ResourceEntry {
|
|
|
36
36
|
name: string;
|
|
37
37
|
path: string;
|
|
38
38
|
scope: 'user' | 'project';
|
|
39
|
+
/** One-line description pulled from frontmatter; not all resource kinds have one. */
|
|
40
|
+
description?: string;
|
|
39
41
|
}
|
|
40
42
|
/** A skill resource entry with optional rule count. */
|
|
41
43
|
export interface SkillResourceEntry extends ResourceEntry {
|
package/dist/lib/resources.js
CHANGED
|
@@ -106,7 +106,7 @@ export function getAgentResources(agentId, options = {}) {
|
|
|
106
106
|
const commands = [];
|
|
107
107
|
for (const cmd of listInstalledCommandsWithScope(agentId, cwd, { home })) {
|
|
108
108
|
if (shouldInclude(cmd.scope)) {
|
|
109
|
-
commands.push({ name: cmd.name, path: cmd.path, scope: cmd.scope });
|
|
109
|
+
commands.push({ name: cmd.name, path: cmd.path, scope: cmd.scope, description: cmd.description });
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
// Skills
|
|
@@ -119,6 +119,7 @@ export function getAgentResources(agentId, options = {}) {
|
|
|
119
119
|
path: skill.path,
|
|
120
120
|
scope: skill.scope,
|
|
121
121
|
ruleCount: skill.ruleCount,
|
|
122
|
+
description: skill.metadata.description || undefined,
|
|
122
123
|
});
|
|
123
124
|
}
|
|
124
125
|
}
|
package/dist/lib/rotate.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import * as yaml from 'yaml';
|
|
10
9
|
import { getAccountInfo } from './agents.js';
|
|
11
|
-
import { readMeta, writeMeta, getHelpersDir
|
|
10
|
+
import { readMeta, writeMeta, getHelpersDir } from './state.js';
|
|
12
11
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
|
|
12
|
+
import { getProjectRunConfigs } from './run-config.js';
|
|
13
13
|
import { getUsageInfoByIdentity, getUsageLookupKey, } from './usage.js';
|
|
14
14
|
function getRotateDir() {
|
|
15
15
|
const dir = path.join(getHelpersDir(), 'rotate');
|
|
@@ -33,22 +33,10 @@ export function normalizeRunStrategy(value) {
|
|
|
33
33
|
}
|
|
34
34
|
/** Read project-local run strategy from the nearest agents.yaml, if present. */
|
|
35
35
|
export function getProjectRunStrategy(agent, startPath) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (manifestPath !== userAgentsYaml && fs.existsSync(manifestPath)) {
|
|
41
|
-
try {
|
|
42
|
-
const parsed = yaml.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
43
|
-
const strategy = normalizeRunStrategy(parsed?.run?.[agent]?.strategy);
|
|
44
|
-
if (strategy)
|
|
45
|
-
return strategy;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// Ignore malformed project config and keep walking, matching version resolution.
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
dir = path.dirname(dir);
|
|
36
|
+
for (const runConfig of getProjectRunConfigs(startPath)) {
|
|
37
|
+
const strategy = normalizeRunStrategy(runConfig[agent]?.strategy);
|
|
38
|
+
if (strategy)
|
|
39
|
+
return strategy;
|
|
52
40
|
}
|
|
53
41
|
return null;
|
|
54
42
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-local `run:` config discovery.
|
|
3
|
+
*
|
|
4
|
+
* The user/system `agents.yaml` is read through state.ts. Project-local
|
|
5
|
+
* agents.yaml files are discovered from the current working directory upward.
|
|
6
|
+
*/
|
|
7
|
+
import type { RunConfig } from './types.js';
|
|
8
|
+
/** Return project-local run configs from nearest directory upward. */
|
|
9
|
+
export declare function getProjectRunConfigs(startPath?: string): RunConfig[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-local `run:` config discovery.
|
|
3
|
+
*
|
|
4
|
+
* The user/system `agents.yaml` is read through state.ts. Project-local
|
|
5
|
+
* agents.yaml files are discovered from the current working directory upward.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as yaml from 'yaml';
|
|
10
|
+
import { getUserAgentsDir } from './state.js';
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
/** Return project-local run configs from nearest directory upward. */
|
|
15
|
+
export function getProjectRunConfigs(startPath = process.cwd()) {
|
|
16
|
+
const configs = [];
|
|
17
|
+
let dir = path.resolve(startPath);
|
|
18
|
+
const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
19
|
+
while (dir !== path.dirname(dir)) {
|
|
20
|
+
const manifestPath = path.join(dir, 'agents.yaml');
|
|
21
|
+
if (manifestPath !== userAgentsYaml && fs.existsSync(manifestPath)) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = yaml.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
24
|
+
if (isRecord(parsed?.run)) {
|
|
25
|
+
configs.push(parsed.run);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore malformed project config and keep walking.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
dir = path.dirname(dir);
|
|
33
|
+
}
|
|
34
|
+
return configs;
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector-based defaults for `agents run`.
|
|
3
|
+
*
|
|
4
|
+
* Stored under agents.yaml:
|
|
5
|
+
*
|
|
6
|
+
* run:
|
|
7
|
+
* defaults:
|
|
8
|
+
* "claude:*":
|
|
9
|
+
* mode: auto
|
|
10
|
+
* model: opus
|
|
11
|
+
* "claude:2.1.45":
|
|
12
|
+
* mode: plan
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentId, Mode, RunConfig, RunDefaults } from './types.js';
|
|
15
|
+
export interface ParsedRunDefaultSelector {
|
|
16
|
+
agent: AgentId;
|
|
17
|
+
version: string;
|
|
18
|
+
selector: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ResolvedRunDefaults extends RunDefaults {
|
|
21
|
+
sources: {
|
|
22
|
+
mode?: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface RunDefaultEntry {
|
|
27
|
+
selector: string;
|
|
28
|
+
defaults: RunDefaults;
|
|
29
|
+
}
|
|
30
|
+
type RunDefaultsInput = {
|
|
31
|
+
mode?: unknown;
|
|
32
|
+
model?: unknown;
|
|
33
|
+
};
|
|
34
|
+
export declare function normalizeRunDefaultMode(input: string): Mode;
|
|
35
|
+
export declare function parseRunDefaultSelector(input: string): ParsedRunDefaultSelector;
|
|
36
|
+
export declare function resolveRunDefaultsFromConfig(runConfig: RunConfig | undefined, agent: AgentId, version?: string | null): ResolvedRunDefaults;
|
|
37
|
+
export declare function resolveRunDefaultsFromConfigs(runConfigs: Array<RunConfig | undefined>, agent: AgentId, version?: string | null): ResolvedRunDefaults;
|
|
38
|
+
export declare function resolveRunDefaults(agent: AgentId, version?: string | null, startPath?: string): ResolvedRunDefaults;
|
|
39
|
+
export declare function listRunDefaults(): RunDefaultEntry[];
|
|
40
|
+
export declare function setRunDefault(selectorInput: string, defaultsInput: RunDefaultsInput): RunDefaultEntry;
|
|
41
|
+
export declare function unsetRunDefault(selectorInput: string): boolean;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector-based defaults for `agents run`.
|
|
3
|
+
*
|
|
4
|
+
* Stored under agents.yaml:
|
|
5
|
+
*
|
|
6
|
+
* run:
|
|
7
|
+
* defaults:
|
|
8
|
+
* "claude:*":
|
|
9
|
+
* mode: auto
|
|
10
|
+
* model: opus
|
|
11
|
+
* "claude:2.1.45":
|
|
12
|
+
* mode: plan
|
|
13
|
+
*/
|
|
14
|
+
import { ALL_MODES } from './types.js';
|
|
15
|
+
import { AGENTS } from './agents.js';
|
|
16
|
+
import { readMeta, updateMeta } from './state.js';
|
|
17
|
+
import { getProjectRunConfigs } from './run-config.js';
|
|
18
|
+
const VERSION_RE = /^(?:\*|latest|(?!.*\.\.)[A-Za-z0-9._+-]{1,64})$/;
|
|
19
|
+
function isAgentId(value) {
|
|
20
|
+
return value in AGENTS;
|
|
21
|
+
}
|
|
22
|
+
export function normalizeRunDefaultMode(input) {
|
|
23
|
+
const mode = input.trim().toLowerCase();
|
|
24
|
+
if (mode === 'full')
|
|
25
|
+
return 'skip';
|
|
26
|
+
if (ALL_MODES.includes(mode))
|
|
27
|
+
return mode;
|
|
28
|
+
throw new Error(`Invalid mode '${input}'. Use one of: ${ALL_MODES.join(', ')} (or 'full' as an alias for 'skip').`);
|
|
29
|
+
}
|
|
30
|
+
function normalizeRunDefaults(defaults, selector) {
|
|
31
|
+
const out = {};
|
|
32
|
+
if (defaults.mode !== undefined) {
|
|
33
|
+
if (typeof defaults.mode !== 'string') {
|
|
34
|
+
throw new Error(`Invalid mode in run.defaults.${selector}: expected a string.`);
|
|
35
|
+
}
|
|
36
|
+
out.mode = normalizeRunDefaultMode(defaults.mode);
|
|
37
|
+
}
|
|
38
|
+
if (defaults.model !== undefined) {
|
|
39
|
+
if (typeof defaults.model !== 'string' || defaults.model.trim() === '') {
|
|
40
|
+
throw new Error(`Invalid model in run.defaults.${selector}: expected a non-empty string.`);
|
|
41
|
+
}
|
|
42
|
+
out.model = defaults.model.trim();
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export function parseRunDefaultSelector(input) {
|
|
47
|
+
const raw = input.trim();
|
|
48
|
+
if (!raw)
|
|
49
|
+
throw new Error('Selector is required. Use <agent>:<version>, <agent>@<version>, or <agent>:*.');
|
|
50
|
+
let agentPart;
|
|
51
|
+
let versionPart;
|
|
52
|
+
if (raw.includes('@')) {
|
|
53
|
+
const parts = raw.split('@');
|
|
54
|
+
if (parts.length !== 2)
|
|
55
|
+
throw new Error(`Invalid selector '${input}'. Use <agent>@<version>.`);
|
|
56
|
+
[agentPart, versionPart] = parts;
|
|
57
|
+
}
|
|
58
|
+
else if (raw.includes(':')) {
|
|
59
|
+
const idx = raw.indexOf(':');
|
|
60
|
+
agentPart = raw.slice(0, idx);
|
|
61
|
+
versionPart = raw.slice(idx + 1);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
agentPart = raw;
|
|
65
|
+
versionPart = '*';
|
|
66
|
+
}
|
|
67
|
+
const agent = agentPart.toLowerCase();
|
|
68
|
+
if (!isAgentId(agent)) {
|
|
69
|
+
throw new Error(`Invalid agent '${agentPart}'. Available agents: ${Object.keys(AGENTS).join(', ')}.`);
|
|
70
|
+
}
|
|
71
|
+
if (!VERSION_RE.test(versionPart)) {
|
|
72
|
+
throw new Error(`Invalid selector version '${versionPart}'. Use *, latest, or [A-Za-z0-9._+-]{1,64}.`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
agent,
|
|
76
|
+
version: versionPart,
|
|
77
|
+
selector: `${agent}:${versionPart}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function sortedDefaults(defaults) {
|
|
81
|
+
return Object.fromEntries(Object.entries(defaults).sort(([a], [b]) => a.localeCompare(b)));
|
|
82
|
+
}
|
|
83
|
+
export function resolveRunDefaultsFromConfig(runConfig, agent, version) {
|
|
84
|
+
const defaults = runConfig?.defaults ?? {};
|
|
85
|
+
const wildcardSelector = `${agent}:*`;
|
|
86
|
+
const exactSelector = version ? `${agent}:${version}` : null;
|
|
87
|
+
const resolved = { sources: {} };
|
|
88
|
+
const wildcard = defaults[wildcardSelector]
|
|
89
|
+
? normalizeRunDefaults(defaults[wildcardSelector], wildcardSelector)
|
|
90
|
+
: null;
|
|
91
|
+
if (wildcard?.mode) {
|
|
92
|
+
resolved.mode = wildcard.mode;
|
|
93
|
+
resolved.sources.mode = wildcardSelector;
|
|
94
|
+
}
|
|
95
|
+
if (wildcard?.model) {
|
|
96
|
+
resolved.model = wildcard.model;
|
|
97
|
+
resolved.sources.model = wildcardSelector;
|
|
98
|
+
}
|
|
99
|
+
if (exactSelector && defaults[exactSelector]) {
|
|
100
|
+
const exact = normalizeRunDefaults(defaults[exactSelector], exactSelector);
|
|
101
|
+
if (exact.mode) {
|
|
102
|
+
resolved.mode = exact.mode;
|
|
103
|
+
resolved.sources.mode = exactSelector;
|
|
104
|
+
}
|
|
105
|
+
if (exact.model) {
|
|
106
|
+
resolved.model = exact.model;
|
|
107
|
+
resolved.sources.model = exactSelector;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return resolved;
|
|
111
|
+
}
|
|
112
|
+
export function resolveRunDefaultsFromConfigs(runConfigs, agent, version) {
|
|
113
|
+
const resolved = { sources: {} };
|
|
114
|
+
for (const runConfig of runConfigs) {
|
|
115
|
+
const next = resolveRunDefaultsFromConfig(runConfig, agent, version);
|
|
116
|
+
if (next.mode) {
|
|
117
|
+
resolved.mode = next.mode;
|
|
118
|
+
resolved.sources.mode = next.sources.mode;
|
|
119
|
+
}
|
|
120
|
+
if (next.model) {
|
|
121
|
+
resolved.model = next.model;
|
|
122
|
+
resolved.sources.model = next.sources.model;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return resolved;
|
|
126
|
+
}
|
|
127
|
+
export function resolveRunDefaults(agent, version, startPath = process.cwd()) {
|
|
128
|
+
const projectRunConfigs = getProjectRunConfigs(startPath).reverse();
|
|
129
|
+
return resolveRunDefaultsFromConfigs([readMeta().run, ...projectRunConfigs], agent, version);
|
|
130
|
+
}
|
|
131
|
+
export function listRunDefaults() {
|
|
132
|
+
const defaults = readMeta().run?.defaults ?? {};
|
|
133
|
+
return Object.entries(defaults)
|
|
134
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
135
|
+
.map(([selector, value]) => ({
|
|
136
|
+
selector,
|
|
137
|
+
defaults: normalizeRunDefaults(value, selector),
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
export function setRunDefault(selectorInput, defaultsInput) {
|
|
141
|
+
const parsed = parseRunDefaultSelector(selectorInput);
|
|
142
|
+
const defaults = normalizeRunDefaults(defaultsInput, parsed.selector);
|
|
143
|
+
if (!defaults.mode && !defaults.model) {
|
|
144
|
+
throw new Error('Set at least one default: --mode <mode> or --model <model>.');
|
|
145
|
+
}
|
|
146
|
+
updateMeta((meta) => {
|
|
147
|
+
const run = { ...(meta.run ?? {}) };
|
|
148
|
+
const currentDefaults = { ...(run.defaults ?? {}) };
|
|
149
|
+
currentDefaults[parsed.selector] = {
|
|
150
|
+
...(currentDefaults[parsed.selector] ?? {}),
|
|
151
|
+
...defaults,
|
|
152
|
+
};
|
|
153
|
+
run.defaults = sortedDefaults(currentDefaults);
|
|
154
|
+
return { ...meta, run };
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
selector: parsed.selector,
|
|
158
|
+
defaults: {
|
|
159
|
+
...(readMeta().run?.defaults?.[parsed.selector] ?? {}),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export function unsetRunDefault(selectorInput) {
|
|
164
|
+
const parsed = parseRunDefaultSelector(selectorInput);
|
|
165
|
+
let removed = false;
|
|
166
|
+
updateMeta((meta) => {
|
|
167
|
+
const run = { ...(meta.run ?? {}) };
|
|
168
|
+
const currentDefaults = { ...(run.defaults ?? {}) };
|
|
169
|
+
removed = Object.prototype.hasOwnProperty.call(currentDefaults, parsed.selector);
|
|
170
|
+
delete currentDefaults[parsed.selector];
|
|
171
|
+
if (Object.keys(currentDefaults).length > 0) {
|
|
172
|
+
run.defaults = sortedDefaults(currentDefaults);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
delete run.defaults;
|
|
176
|
+
}
|
|
177
|
+
return { ...meta, run };
|
|
178
|
+
});
|
|
179
|
+
return removed;
|
|
180
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -12,10 +12,14 @@
|
|
|
12
12
|
* modules in `src/lib/secrets/` must import `getKeychainHelperPath()` rather
|
|
13
13
|
* than recomputing it.
|
|
14
14
|
*/
|
|
15
|
+
/** Redirect the install root (test only). Returns the previous override so callers can restore. */
|
|
16
|
+
export declare function setInstallRootForTest(dir: string | null): string | null;
|
|
15
17
|
/**
|
|
16
18
|
* Idempotent install. Copies the bundled `.app` to the stable user path. Skips
|
|
17
|
-
* if the destination already exists
|
|
18
|
-
*
|
|
19
|
+
* if the destination already exists, `codesign --verify` passes, AND the
|
|
20
|
+
* installed executable matches the bundled one byte-for-byte — a valid
|
|
21
|
+
* signature alone is not enough, because an outdated helper signs clean too.
|
|
22
|
+
* `forceReinstall=true` skips all checks and always copies.
|
|
19
23
|
*
|
|
20
24
|
* Notarization is checked via `spctl --assess` after install — a failure is
|
|
21
25
|
* logged as a warning but does NOT throw. Notarization checks require network
|
|
@@ -27,7 +31,11 @@ export declare function ensureKeychainHelperInstalled(opts?: {
|
|
|
27
31
|
}): void;
|
|
28
32
|
/**
|
|
29
33
|
* Return the absolute path to the helper executable. If the installed bundle
|
|
30
|
-
* is missing,
|
|
34
|
+
* is missing, or is stale relative to the bundled source helper, performs a
|
|
35
|
+
* lazy (re)install first. The staleness check is what lets an upgraded CLI
|
|
36
|
+
* replace a helper a previous version installed — `agents helper install`
|
|
37
|
+
* never runs on `npm i -g`, so this call site is the only one every machine
|
|
38
|
+
* is guaranteed to pass through.
|
|
31
39
|
*
|
|
32
40
|
* Throws on non-darwin.
|
|
33
41
|
*/
|