@phnx-labs/agents-cli 1.20.6 → 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/dist/commands/computer-actions.d.ts +36 -0
- package/dist/commands/computer-actions.js +328 -0
- package/dist/commands/computer.js +67 -56
- package/dist/commands/inspect.d.ts +38 -7
- package/dist/commands/inspect.js +194 -24
- package/dist/commands/sessions.js +9 -12
- package/dist/index.js +23 -1
- package/dist/lib/daemon.js +4 -7
- package/dist/lib/exec.d.ts +9 -0
- package/dist/lib/exec.js +61 -5
- 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/pty-client.js +13 -5
- package/dist/lib/pty-server.d.ts +24 -1
- package/dist/lib/pty-server.js +102 -25
- package/dist/lib/session/artifacts.js +8 -2
- package/dist/lib/shims.js +45 -0
- package/dist/lib/teams/agents.js +5 -7
- package/package.json +1 -1
- package/scripts/postinstall.js +18 -1
package/dist/commands/inspect.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agents inspect <
|
|
2
|
+
* `agents inspect <target>` — detail view for one agent+version or one DotAgents repo.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* counts, sessions).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Agent targets (`claude`, `claude@2.1.170`) show the per-version header (paths,
|
|
5
|
+
* shim, capabilities, resource counts, sessions). Repo targets (`user`, `system`,
|
|
6
|
+
* `project`, a registered extra-repo alias, or a filesystem path to a repo with a
|
|
7
|
+
* `.agents/` dir or to a DotAgents root itself) show the repo root, git state, and
|
|
8
|
+
* per-kind resource counts. Drill-down flags (`--skills`, `--hooks`, `--mcp`, ...)
|
|
9
|
+
* list one resource kind for either target form; passing a positional query to the
|
|
10
|
+
* same flag fuzzy-searches for a single resource and prints its detail. Resource
|
|
11
|
+
* names render as OSC-8 hyperlinks to the marker file (SKILL.md / WORKFLOW.md /
|
|
12
|
+
* AGENT.md / the file itself) so users can click straight to the source.
|
|
10
13
|
*/
|
|
14
|
+
import { execSync } from 'child_process';
|
|
11
15
|
import * as fs from 'fs';
|
|
12
16
|
import * as os from 'os';
|
|
13
17
|
import * as path from 'path';
|
|
@@ -15,7 +19,7 @@ import chalk from 'chalk';
|
|
|
15
19
|
import * as yaml from 'yaml';
|
|
16
20
|
import { AGENTS, getCliState } from '../lib/agents.js';
|
|
17
21
|
import { supports } from '../lib/capabilities.js';
|
|
18
|
-
import { readMeta } from '../lib/state.js';
|
|
22
|
+
import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
|
|
19
23
|
import { getVersionHomePath } from '../lib/versions.js';
|
|
20
24
|
import { getShimsDir, getVersionedAliasPath } from '../lib/shims.js';
|
|
21
25
|
import { getAgentResources, listResources, } from '../lib/resources.js';
|
|
@@ -39,8 +43,8 @@ const CAPABILITY_NAMES = [
|
|
|
39
43
|
// ─── Command registration ────────────────────────────────────────────────────
|
|
40
44
|
export function registerInspectCommand(program) {
|
|
41
45
|
const cmd = program
|
|
42
|
-
.command('inspect <
|
|
43
|
-
.description('Inspect one installed agent at one version — paths, capabilities, resources, drill into any kind.')
|
|
46
|
+
.command('inspect <target>')
|
|
47
|
+
.description('Inspect one installed agent at one version, or a DotAgents repo (user|system|project|alias|path) — paths, capabilities, resources, drill into any kind.')
|
|
44
48
|
.option('--brief', 'header + capabilities only; skip resources/sessions')
|
|
45
49
|
.option('--json', 'machine-readable JSON output');
|
|
46
50
|
for (const kind of DRILLABLE_KINDS) {
|
|
@@ -52,6 +56,20 @@ export function registerInspectCommand(program) {
|
|
|
52
56
|
}
|
|
53
57
|
// ─── Main dispatcher ─────────────────────────────────────────────────────────
|
|
54
58
|
export async function inspectAction(target, options) {
|
|
59
|
+
const agentKey = target.split('@')[0].toLowerCase();
|
|
60
|
+
if (!(agentKey in AGENTS)) {
|
|
61
|
+
const repo = resolveRepoTarget(target);
|
|
62
|
+
if (repo) {
|
|
63
|
+
await inspectRepo(repo, options);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const extras = getEnabledExtraRepos();
|
|
67
|
+
console.error(chalk.red(`Unknown target: ${target}`));
|
|
68
|
+
console.error(chalk.gray(`Agents: ${Object.keys(AGENTS).join(', ')}`));
|
|
69
|
+
const aliases = extras.length > 0 ? `, ${extras.map(e => e.alias).join(', ')}` : '';
|
|
70
|
+
console.error(chalk.gray(`Repos: user, system, project${aliases} — or a path to a repo with a .agents/ dir`));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
55
73
|
const { agent, version } = parseTarget(target);
|
|
56
74
|
const versionHome = getVersionHomePath(agent, version);
|
|
57
75
|
if (!fs.existsSync(versionHome)) {
|
|
@@ -75,13 +93,7 @@ export async function inspectAction(target, options) {
|
|
|
75
93
|
}
|
|
76
94
|
function parseTarget(target) {
|
|
77
95
|
const [rawAgent, rawVersion] = target.split('@');
|
|
78
|
-
const
|
|
79
|
-
if (!(agentKey in AGENTS)) {
|
|
80
|
-
console.error(chalk.red(`Unknown agent: ${rawAgent}`));
|
|
81
|
-
console.error(chalk.gray(`Known agents: ${Object.keys(AGENTS).join(', ')}`));
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
const agent = agentKey;
|
|
96
|
+
const agent = (rawAgent || '').toLowerCase();
|
|
85
97
|
let version = rawVersion;
|
|
86
98
|
if (!version || version === 'default') {
|
|
87
99
|
const meta = readMeta();
|
|
@@ -110,6 +122,160 @@ function pickDrillKind(options) {
|
|
|
110
122
|
}
|
|
111
123
|
return active[0];
|
|
112
124
|
}
|
|
125
|
+
/** Files at a DotAgents root that mark it as one, beyond the per-kind dirs. */
|
|
126
|
+
const REPO_MARKER_FILES = ['agents.yaml', 'hooks.yaml'];
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a non-agent target as a DotAgents repo: the built-in layer names,
|
|
129
|
+
* a registered extra-repo alias, or a filesystem path. Paths accept either a
|
|
130
|
+
* DotAgents root itself or a repo whose `.agents/` dir should be inspected.
|
|
131
|
+
* Returns null when the target is none of these.
|
|
132
|
+
*/
|
|
133
|
+
export function resolveRepoTarget(target, cwd) {
|
|
134
|
+
if (target === 'user')
|
|
135
|
+
return { label: 'user', root: getUserAgentsDir() };
|
|
136
|
+
if (target === 'system')
|
|
137
|
+
return { label: 'system', root: getSystemAgentsDir() };
|
|
138
|
+
if (target === 'project') {
|
|
139
|
+
const dir = getProjectAgentsDir(cwd);
|
|
140
|
+
if (!dir) {
|
|
141
|
+
console.error(chalk.red('No project .agents/ directory found from the current directory.'));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
return { label: 'project', root: dir };
|
|
145
|
+
}
|
|
146
|
+
for (const extra of getEnabledExtraRepos()) {
|
|
147
|
+
if (extra.alias === target)
|
|
148
|
+
return { label: extra.alias, root: extra.dir };
|
|
149
|
+
}
|
|
150
|
+
const expanded = target.startsWith('~/') ? path.join(os.homedir(), target.slice(2)) : target;
|
|
151
|
+
const abs = path.resolve(cwd ?? process.cwd(), expanded);
|
|
152
|
+
const stat = safeStat(abs);
|
|
153
|
+
if (!stat || !stat.isDirectory())
|
|
154
|
+
return null;
|
|
155
|
+
// A dir that is itself a DotAgents root wins over its nested .agents/ —
|
|
156
|
+
// extra repos like ~/.agents-extras keep resources at the top level and use
|
|
157
|
+
// .agents/ only for worktrees.
|
|
158
|
+
if (isDotAgentsRoot(abs)) {
|
|
159
|
+
const label = path.basename(abs) === '.agents' ? path.basename(path.dirname(abs)) : path.basename(abs);
|
|
160
|
+
return { label, root: abs };
|
|
161
|
+
}
|
|
162
|
+
if (path.basename(abs) !== '.agents') {
|
|
163
|
+
const nested = path.join(abs, '.agents');
|
|
164
|
+
if (safeStat(nested)?.isDirectory()) {
|
|
165
|
+
return { label: path.basename(abs), root: nested };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function isDotAgentsRoot(dir) {
|
|
171
|
+
for (const marker of REPO_MARKER_FILES) {
|
|
172
|
+
if (fs.existsSync(path.join(dir, marker)))
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
for (const kind of DRILLABLE_KINDS) {
|
|
176
|
+
if (safeStat(path.join(dir, kind))?.isDirectory())
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
async function inspectRepo(repo, options) {
|
|
182
|
+
const drill = pickDrillKind(options);
|
|
183
|
+
const jsonHead = { repo: repo.label, root: repo.root };
|
|
184
|
+
if (drill) {
|
|
185
|
+
const items = collectRepoKind(repo, drill.kind);
|
|
186
|
+
if (drill.query === true || drill.query === undefined) {
|
|
187
|
+
renderItemList(repo.label, jsonHead, drill.kind, items, options);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
renderItemDetail(repo.label, jsonHead, drill.kind, String(drill.query), items, options);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
renderRepoSummary(repo, options);
|
|
195
|
+
}
|
|
196
|
+
/** List one resource kind from a single repo root — no layering, no overrides. */
|
|
197
|
+
export function collectRepoKind(repo, kind) {
|
|
198
|
+
const dir = path.join(repo.root, kind);
|
|
199
|
+
let entries;
|
|
200
|
+
try {
|
|
201
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
const items = [];
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
if (entry.name.startsWith('.'))
|
|
209
|
+
continue;
|
|
210
|
+
const p = path.join(dir, entry.name);
|
|
211
|
+
items.push({
|
|
212
|
+
name: entry.name.replace(/\.(md|yaml|yml|toml|json)$/, ''),
|
|
213
|
+
source: repo.label,
|
|
214
|
+
path: p,
|
|
215
|
+
linkTarget: linkTarget(p),
|
|
216
|
+
description: readDescription(p),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return items.sort((a, b) => a.name.localeCompare(b.name));
|
|
220
|
+
}
|
|
221
|
+
function renderRepoSummary(repo, options) {
|
|
222
|
+
const git = repoGitInfo(repo.root);
|
|
223
|
+
const manifests = REPO_MARKER_FILES.filter(m => fs.existsSync(path.join(repo.root, m)));
|
|
224
|
+
const counts = {};
|
|
225
|
+
if (!options.brief) {
|
|
226
|
+
for (const kind of DRILLABLE_KINDS) {
|
|
227
|
+
const items = collectRepoKind(repo, kind);
|
|
228
|
+
counts[kind] = { total: items.length, bySource: { [repo.label]: items.length } };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (options.json) {
|
|
232
|
+
console.log(JSON.stringify({
|
|
233
|
+
repo: repo.label,
|
|
234
|
+
root: repo.root,
|
|
235
|
+
git,
|
|
236
|
+
manifests,
|
|
237
|
+
resources: options.brief ? null : Object.fromEntries(DRILLABLE_KINDS.map(kind => [kind, counts[kind].total])),
|
|
238
|
+
}, null, 2));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
console.log('\n' + chalk.bold(repo.label) + ' ' + chalk.gray('[dotagents repo]') + '\n');
|
|
242
|
+
const rows = [['root', termLink(repo.root, repo.root)]];
|
|
243
|
+
if (git) {
|
|
244
|
+
const dirty = git.dirty > 0 ? ` ${chalk.gray('·')} ${chalk.yellow(`${git.dirty} dirty`)}` : '';
|
|
245
|
+
const url = git.url ? ` ${chalk.gray('·')} ${chalk.gray(git.url)}` : '';
|
|
246
|
+
rows.push(['git', `${git.branch}${dirty}${url}`]);
|
|
247
|
+
}
|
|
248
|
+
if (manifests.length > 0)
|
|
249
|
+
rows.push(['manifests', manifests.join(', ')]);
|
|
250
|
+
for (const [k, v] of rows)
|
|
251
|
+
console.log(` ${k.padEnd(10)} ${v}`);
|
|
252
|
+
if (!options.brief) {
|
|
253
|
+
console.log('\n' + chalk.bold('Resources'));
|
|
254
|
+
for (const kind of DRILLABLE_KINDS) {
|
|
255
|
+
console.log(` ${kind.padEnd(10)} ${String(counts[kind].total).padStart(4)}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(chalk.gray(`Drill in: agents inspect ${repo.label} --skills <query>`));
|
|
260
|
+
console.log('');
|
|
261
|
+
}
|
|
262
|
+
function repoGitInfo(root) {
|
|
263
|
+
const git = (args) => {
|
|
264
|
+
try {
|
|
265
|
+
return execSync(`git -C ${JSON.stringify(root)} ${args}`, { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
266
|
+
.toString().trim();
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const branch = git('rev-parse --abbrev-ref HEAD');
|
|
273
|
+
if (branch === null)
|
|
274
|
+
return null;
|
|
275
|
+
const status = git('status --porcelain');
|
|
276
|
+
const dirty = status ? status.split('\n').filter(Boolean).length : 0;
|
|
277
|
+
return { branch, dirty, url: git('remote get-url origin') };
|
|
278
|
+
}
|
|
113
279
|
// ─── Summary mode ────────────────────────────────────────────────────────────
|
|
114
280
|
async function renderSummary(agent, version, versionHome, options) {
|
|
115
281
|
const meta = readMeta();
|
|
@@ -184,17 +350,19 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
184
350
|
// ─── List mode ───────────────────────────────────────────────────────────────
|
|
185
351
|
async function renderList(agent, version, versionHome, kind, options) {
|
|
186
352
|
const items = collectKind(agent, versionHome, kind);
|
|
353
|
+
renderItemList(`${agent}@${version}`, { agent, version }, kind, items, options);
|
|
354
|
+
}
|
|
355
|
+
function renderItemList(header, jsonHead, kind, items, options) {
|
|
187
356
|
if (options.json) {
|
|
188
357
|
console.log(JSON.stringify({
|
|
189
|
-
|
|
190
|
-
version,
|
|
358
|
+
...jsonHead,
|
|
191
359
|
kind,
|
|
192
360
|
count: items.length,
|
|
193
361
|
items: items.map(i => ({ name: i.name, source: i.source, path: i.path, description: i.description })),
|
|
194
362
|
}, null, 2));
|
|
195
363
|
return;
|
|
196
364
|
}
|
|
197
|
-
console.log('\n' + chalk.bold(
|
|
365
|
+
console.log('\n' + chalk.bold(header) + ' ' + chalk.gray(`${kind} (${items.length})`) + '\n');
|
|
198
366
|
if (items.length === 0) {
|
|
199
367
|
console.log(chalk.gray(` (none installed)`));
|
|
200
368
|
console.log('');
|
|
@@ -212,11 +380,14 @@ async function renderList(agent, version, versionHome, kind, options) {
|
|
|
212
380
|
// ─── Detail mode (fuzzy) ─────────────────────────────────────────────────────
|
|
213
381
|
async function renderDetail(agent, version, versionHome, kind, query, options) {
|
|
214
382
|
const items = collectKind(agent, versionHome, kind);
|
|
383
|
+
renderItemDetail(`${agent}@${version}`, { agent, version }, kind, query, items, options);
|
|
384
|
+
}
|
|
385
|
+
function renderItemDetail(header, jsonHead, kind, query, items, options) {
|
|
215
386
|
const matches = findMatches(items, query);
|
|
216
387
|
if (matches.length === 0) {
|
|
217
388
|
const suggestions = suggestClosest(items, query, 3);
|
|
218
389
|
if (options.json) {
|
|
219
|
-
console.log(JSON.stringify({
|
|
390
|
+
console.log(JSON.stringify({ ...jsonHead, kind, query, match: null, suggestions: suggestions.map(s => s.name) }, null, 2));
|
|
220
391
|
}
|
|
221
392
|
else {
|
|
222
393
|
console.error(chalk.red(`No ${kind} matching '${query}'.`));
|
|
@@ -231,8 +402,7 @@ async function renderDetail(agent, version, versionHome, kind, query, options) {
|
|
|
231
402
|
if (options.json) {
|
|
232
403
|
const detail = buildDetail(best.item, kind);
|
|
233
404
|
console.log(JSON.stringify({
|
|
234
|
-
|
|
235
|
-
version,
|
|
405
|
+
...jsonHead,
|
|
236
406
|
kind,
|
|
237
407
|
query,
|
|
238
408
|
match: { ...detail, matchKind: best.matchKind },
|
|
@@ -240,7 +410,7 @@ async function renderDetail(agent, version, versionHome, kind, query, options) {
|
|
|
240
410
|
}, null, 2));
|
|
241
411
|
return;
|
|
242
412
|
}
|
|
243
|
-
console.log('\n' + chalk.bold(
|
|
413
|
+
console.log('\n' + chalk.bold(header) + ' ' + chalk.gray(`${kind} matching "${query}"`) + '\n');
|
|
244
414
|
const matchTag = best.matchKind === 'exact' ? 'exact' : best.matchKind === 'substring' ? 'substring' : `~${best.distance}`;
|
|
245
415
|
console.log(` ${chalk.green('✓')} ${termLink(chalk.bold.cyan(best.item.name), best.item.linkTarget)} ${chalk.gray(`[${matchTag}, ${best.item.source}]`)}`);
|
|
246
416
|
if (best.item.description) {
|
|
@@ -15,6 +15,7 @@ import chalk from 'chalk';
|
|
|
15
15
|
import ora from 'ora';
|
|
16
16
|
import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
17
17
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
18
|
+
import { looksLikePath, toComparablePath, homeDir } from '../lib/platform/index.js';
|
|
18
19
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
19
20
|
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
|
|
20
21
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
@@ -69,15 +70,6 @@ function createScanProgressTracker(verbs, suffix, spinner) {
|
|
|
69
70
|
}
|
|
70
71
|
const PICKER_RECENT_COUNT = 15;
|
|
71
72
|
const PICKER_POOL_LIMIT = 200;
|
|
72
|
-
/**
|
|
73
|
-
* Detect whether a positional argument looks like a filesystem path.
|
|
74
|
-
* Naked paths (., ./, ../, /, ~) filter sessions by project directory.
|
|
75
|
-
* Everything else is treated as a search query string.
|
|
76
|
-
*/
|
|
77
|
-
function isPathLike(query) {
|
|
78
|
-
return query === '.' || query.startsWith('./') || query.startsWith('../')
|
|
79
|
-
|| query.startsWith('/') || query.startsWith('~');
|
|
80
|
-
}
|
|
81
73
|
/**
|
|
82
74
|
* Resolve a path-like query to an absolute directory path.
|
|
83
75
|
*/
|
|
@@ -153,8 +145,13 @@ function contextColor(context) {
|
|
|
153
145
|
function shortCwd(cwd) {
|
|
154
146
|
if (!cwd)
|
|
155
147
|
return '-';
|
|
156
|
-
const home =
|
|
157
|
-
|
|
148
|
+
const home = homeDir();
|
|
149
|
+
// Compare in normalized form so the `~` shorthand also lands on Windows
|
|
150
|
+
// (case-insensitive, backslash paths); on POSIX this is byte-identical to the
|
|
151
|
+
// previous `cwd.startsWith(home)`. The displayed tail keeps original casing.
|
|
152
|
+
return toComparablePath(cwd).startsWith(toComparablePath(home))
|
|
153
|
+
? '~' + cwd.slice(home.length)
|
|
154
|
+
: cwd;
|
|
158
155
|
}
|
|
159
156
|
function formatStartedAt(startedAtMs) {
|
|
160
157
|
if (!startedAtMs)
|
|
@@ -334,7 +331,7 @@ async function sessionsAction(query, options) {
|
|
|
334
331
|
// Path-like queries filter by project directory instead of text search.
|
|
335
332
|
let pathFilter;
|
|
336
333
|
let searchQuery;
|
|
337
|
-
if (query &&
|
|
334
|
+
if (query && looksLikePath(query)) {
|
|
338
335
|
const resolved = resolvePathFilter(query);
|
|
339
336
|
if (!fs.existsSync(resolved)) {
|
|
340
337
|
console.log(chalk.yellow(`Path not found: ${resolved}`));
|
package/dist/index.js
CHANGED
|
@@ -103,6 +103,21 @@ import { isInteractiveTerminal, isPromptCancelled } from './commands/utils.js';
|
|
|
103
103
|
import { AGENTS } from './lib/agents.js';
|
|
104
104
|
import { getGlobalDefault, listInstalledVersions } from './lib/versions.js';
|
|
105
105
|
import { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, getShimsDir, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
|
|
106
|
+
import { IS_WINDOWS } from './lib/platform/index.js';
|
|
107
|
+
// Transparent shim delegate: the generated Windows `.cmd` shims invoke
|
|
108
|
+
// `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
|
|
109
|
+
// parses anything, so the agent's own flags (`--help`, `--version`, etc.) pass
|
|
110
|
+
// through completely untouched and we skip registering the full command tree.
|
|
111
|
+
if (process.argv[2] === '__shim') {
|
|
112
|
+
const spec = process.argv[3] || '';
|
|
113
|
+
const rawArgs = process.argv.slice(4);
|
|
114
|
+
const atIndex = spec.indexOf('@');
|
|
115
|
+
const agent = atIndex === -1 ? spec : spec.slice(0, atIndex);
|
|
116
|
+
const pinned = atIndex === -1 ? undefined : spec.slice(atIndex + 1);
|
|
117
|
+
const { execShimPassthrough } = await import('./lib/exec.js');
|
|
118
|
+
const code = await execShimPassthrough(agent, rawArgs, process.cwd(), pinned || undefined);
|
|
119
|
+
process.exit(code);
|
|
120
|
+
}
|
|
106
121
|
const program = new Command();
|
|
107
122
|
program
|
|
108
123
|
.name('agents')
|
|
@@ -134,7 +149,7 @@ Agent versions:
|
|
|
134
149
|
prune cleanup [target] Remove orphan resources and older duplicate version installs
|
|
135
150
|
trash Inspect and restore soft-deleted version directories
|
|
136
151
|
view [agent[@version]] List versions, or inspect one in detail
|
|
137
|
-
inspect <
|
|
152
|
+
inspect <target> Deep details for one agent+version, or a DotAgents repo (user|system|project|alias|path)
|
|
138
153
|
|
|
139
154
|
Agent configuration (synced across versions):
|
|
140
155
|
rules Instructions given to agents (CLAUDE.md, etc.)
|
|
@@ -461,6 +476,13 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
|
|
|
461
476
|
for (const agent of installedAgents) {
|
|
462
477
|
removeLegacyUserShim(agent);
|
|
463
478
|
}
|
|
479
|
+
// The remaining flow is rc-file PATH repair, which is POSIX-only. On Windows
|
|
480
|
+
// the shims were just regenerated (incl. `.cmd` companions) above; PATH setup
|
|
481
|
+
// is covered by the install-time guidance, so stop here rather than printing
|
|
482
|
+
// shell-rc instructions that don't apply.
|
|
483
|
+
if (IS_WINDOWS) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
464
486
|
const defaultAgents = installedAgents.filter((agent) => getGlobalDefault(agent));
|
|
465
487
|
const shadowed = defaultAgents
|
|
466
488
|
.map((agent) => ({ agent, shadowedBy: getPathShadowingExecutable(agent) }))
|
package/dist/lib/daemon.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
13
13
|
import { getDaemonDir as getDaemonDirRoot } from './state.js';
|
|
14
|
+
import { isAlive } from './platform/index.js';
|
|
14
15
|
import { listJobs as listAllJobs } from './routines.js';
|
|
15
16
|
import { JobScheduler } from './scheduler.js';
|
|
16
17
|
import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
@@ -114,14 +115,10 @@ export function isDaemonRunning() {
|
|
|
114
115
|
const pid = readDaemonPid();
|
|
115
116
|
if (!pid)
|
|
116
117
|
return false;
|
|
117
|
-
|
|
118
|
-
process.kill(pid, 0);
|
|
118
|
+
if (isAlive(pid))
|
|
119
119
|
return true;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
removeDaemonPid();
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
120
|
+
removeDaemonPid();
|
|
121
|
+
return false;
|
|
125
122
|
}
|
|
126
123
|
/** Redact values that look like tokens or credentials in a log message. */
|
|
127
124
|
function redactSecrets(message) {
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -95,6 +95,15 @@ export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
|
|
|
95
95
|
export declare function buildExecCommand(options: ExecOptions): string[];
|
|
96
96
|
/** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
|
|
97
97
|
export declare function execAgent(options: ExecOptions): Promise<number>;
|
|
98
|
+
/**
|
|
99
|
+
* Transparent passthrough exec for generated shims — the node-side delegate that
|
|
100
|
+
* Windows `.cmd` shims call. Resolves the active version (explicit pin, else
|
|
101
|
+
* project/default) and execs the real binary with the user's RAW args and the
|
|
102
|
+
* per-version env isolation, WITHOUT injecting mode/model/reasoning flags. This
|
|
103
|
+
* mirrors what the POSIX bash shim does inline (`exec $BINARY $launchArgs "$@"`),
|
|
104
|
+
* keeping version resolution in one place instead of reimplementing it in batch.
|
|
105
|
+
*/
|
|
106
|
+
export declare function execShimPassthrough(agent: AgentId, rawArgs: string[], cwd: string, pinnedVersion?: string): Promise<number>;
|
|
98
107
|
/**
|
|
99
108
|
* Patterns that indicate a rate/usage limit. Matching is intentionally broad
|
|
100
109
|
* because providers phrase these differently -- Anthropic uses "5-hour limit"
|
package/dist/lib/exec.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as path from 'path';
|
|
|
11
11
|
import { ALL_MODES } from './types.js';
|
|
12
12
|
import { AGENTS } from './agents.js';
|
|
13
13
|
import { parseTimeout } from './routines.js';
|
|
14
|
-
import { getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
14
|
+
import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
15
15
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
16
16
|
import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
|
|
17
17
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
@@ -367,10 +367,27 @@ export function buildExecCommand(options) {
|
|
|
367
367
|
// Resolve to the absolute path of the shim so spawn doesn't depend on PATH —
|
|
368
368
|
// on Linux installs where the shims dir isn't on PATH, spawning the bare
|
|
369
369
|
// versioned name fails with ENOENT even though `agents view` shows the agent.
|
|
370
|
+
//
|
|
371
|
+
// On Windows, shims are bash scripts and cannot be executed by spawn() directly.
|
|
372
|
+
// buildExecEnv() already sets the isolation env vars (CLAUDE_CONFIG_DIR, CODEX_HOME,
|
|
373
|
+
// etc.) that the bash shim would set, so we can skip the shim entirely and resolve
|
|
374
|
+
// straight to the real binary via getBinaryPath.
|
|
370
375
|
if (options.version && cmd.length > 0) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
376
|
+
if (process.platform === 'win32') {
|
|
377
|
+
const binaryPath = getBinaryPath(options.agent, options.version);
|
|
378
|
+
const binaryPathCmd = binaryPath + '.cmd';
|
|
379
|
+
if (fs.existsSync(binaryPathCmd)) {
|
|
380
|
+
cmd[0] = binaryPathCmd;
|
|
381
|
+
}
|
|
382
|
+
else if (fs.existsSync(binaryPath)) {
|
|
383
|
+
cmd[0] = binaryPath;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
const versionedName = `${cmd[0]}@${options.version}`;
|
|
388
|
+
const absPath = path.join(getShimsDir(), versionedName);
|
|
389
|
+
cmd[0] = fs.existsSync(absPath) ? absPath : versionedName;
|
|
390
|
+
}
|
|
374
391
|
}
|
|
375
392
|
// Add reasoning effort flags (before mode flags for codex -c positioning)
|
|
376
393
|
// For codex, -c must come before 'exec' subcommand, so we insert at position 1
|
|
@@ -458,6 +475,42 @@ export async function execAgent(options) {
|
|
|
458
475
|
const { exitCode } = await spawnAgent(options);
|
|
459
476
|
return exitCode;
|
|
460
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Transparent passthrough exec for generated shims — the node-side delegate that
|
|
480
|
+
* Windows `.cmd` shims call. Resolves the active version (explicit pin, else
|
|
481
|
+
* project/default) and execs the real binary with the user's RAW args and the
|
|
482
|
+
* per-version env isolation, WITHOUT injecting mode/model/reasoning flags. This
|
|
483
|
+
* mirrors what the POSIX bash shim does inline (`exec $BINARY $launchArgs "$@"`),
|
|
484
|
+
* keeping version resolution in one place instead of reimplementing it in batch.
|
|
485
|
+
*/
|
|
486
|
+
export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
487
|
+
const version = pinnedVersion ?? resolveVersion(agent, cwd) ?? undefined;
|
|
488
|
+
if (!version || !isVersionInstalled(agent, version)) {
|
|
489
|
+
process.stderr.write(`agents: no installed default for ${agent}. Set one with: agents use ${agent} <version>\n`);
|
|
490
|
+
return 127;
|
|
491
|
+
}
|
|
492
|
+
let binary = getBinaryPath(agent, version);
|
|
493
|
+
if (process.platform === 'win32') {
|
|
494
|
+
// npm ships <cmd>.cmd alongside the bare script on Windows; that's the runnable form.
|
|
495
|
+
const cmdPath = binary + '.cmd';
|
|
496
|
+
if (fs.existsSync(cmdPath))
|
|
497
|
+
binary = cmdPath;
|
|
498
|
+
}
|
|
499
|
+
// The only flag the bash shim injects (codex); everything else is transparent.
|
|
500
|
+
const launchArgs = agent === 'codex' ? ['-c', 'check_for_update_on_startup=false'] : [];
|
|
501
|
+
// mode/effort are required by ExecOptions but unused by buildExecEnv (which only
|
|
502
|
+
// derives the per-version config-dir env); pass the agent's default to satisfy the type.
|
|
503
|
+
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto' });
|
|
504
|
+
const useShell = process.platform === 'win32' && (!path.isAbsolute(binary) || binary.endsWith('.cmd'));
|
|
505
|
+
return new Promise((resolve) => {
|
|
506
|
+
const child = spawn(binary, [...launchArgs, ...rawArgs], { cwd, stdio: 'inherit', env, shell: useShell });
|
|
507
|
+
child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
508
|
+
child.on('error', (err) => {
|
|
509
|
+
process.stderr.write(`agents: failed to launch ${agent}: ${err.message}\n`);
|
|
510
|
+
resolve(127);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
461
514
|
/**
|
|
462
515
|
* Spawn an agent process and return its exit code plus a tee'd copy of stderr.
|
|
463
516
|
*
|
|
@@ -495,11 +548,14 @@ async function spawnAgent(options) {
|
|
|
495
548
|
const stdio = interactive
|
|
496
549
|
? ['inherit', 'inherit', 'inherit']
|
|
497
550
|
: ['inherit', piped ? 'pipe' : 'inherit', 'pipe'];
|
|
551
|
+
// On Windows, .cmd batch wrappers (npm-installed CLIs) require shell:true
|
|
552
|
+
// whether addressed by name or absolute path.
|
|
553
|
+
const useShell = process.platform === 'win32' && (!path.isAbsolute(executable) || executable.endsWith('.cmd'));
|
|
498
554
|
const child = spawn(executable, args, {
|
|
499
555
|
cwd: options.cwd || process.cwd(),
|
|
500
556
|
stdio,
|
|
501
557
|
env: buildExecEnv(options),
|
|
502
|
-
shell:
|
|
558
|
+
shell: useShell,
|
|
503
559
|
});
|
|
504
560
|
// Mark startup time (time from function call to process spawn)
|
|
505
561
|
timer.mark('startup');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** PATH-search command for the platform: `where` on Windows, else `which`. */
|
|
2
|
+
export declare function whichCommand(platform?: NodeJS.Platform): string;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
5
|
+
* `null` if not found. On Windows `where` can return several lines (one per
|
|
6
|
+
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
7
|
+
* shell would actually run, matching `which` semantics on POSIX.
|
|
8
|
+
*/
|
|
9
|
+
export declare function findExecutable(name: string, platform?: NodeJS.Platform): string | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executable resolution, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
/** PATH-search command for the platform: `where` on Windows, else `which`. */
|
|
6
|
+
export function whichCommand(platform = process.platform) {
|
|
7
|
+
return platform === 'win32' ? 'where' : 'which';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
11
|
+
* `null` if not found. On Windows `where` can return several lines (one per
|
|
12
|
+
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
13
|
+
* shell would actually run, matching `which` semantics on POSIX.
|
|
14
|
+
*/
|
|
15
|
+
export function findExecutable(name, platform = process.platform) {
|
|
16
|
+
try {
|
|
17
|
+
const out = execFileSync(whichCommand(platform), [name], { encoding: 'utf-8' });
|
|
18
|
+
const first = out.trim().split(/\r?\n/)[0]?.trim();
|
|
19
|
+
return first || null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform abstraction — the ONE place OS-divergent behavior is decided.
|
|
3
|
+
*
|
|
4
|
+
* Consumers express intent (`looksLikePath`, `findExecutable`, `isAlive`) instead
|
|
5
|
+
* of scattering `process.platform === 'win32'` checks. Each helper that has an
|
|
6
|
+
* observable branch accepts an explicit `platform` argument (defaulting to
|
|
7
|
+
* `process.platform`), so all three OSes are unit-testable on any host.
|
|
8
|
+
*
|
|
9
|
+
* Modules grow per concern as features land:
|
|
10
|
+
* paths — path classification + normalization
|
|
11
|
+
* exec — executable resolution
|
|
12
|
+
* process — process liveness / control
|
|
13
|
+
* (ipc + shell follow when their consumers migrate off inline branches.)
|
|
14
|
+
*/
|
|
15
|
+
export declare const IS_WINDOWS: boolean;
|
|
16
|
+
export declare const IS_MACOS: boolean;
|
|
17
|
+
export declare const IS_LINUX: boolean;
|
|
18
|
+
export * from './paths.js';
|
|
19
|
+
export * from './exec.js';
|
|
20
|
+
export * from './process.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform abstraction — the ONE place OS-divergent behavior is decided.
|
|
3
|
+
*
|
|
4
|
+
* Consumers express intent (`looksLikePath`, `findExecutable`, `isAlive`) instead
|
|
5
|
+
* of scattering `process.platform === 'win32'` checks. Each helper that has an
|
|
6
|
+
* observable branch accepts an explicit `platform` argument (defaulting to
|
|
7
|
+
* `process.platform`), so all three OSes are unit-testable on any host.
|
|
8
|
+
*
|
|
9
|
+
* Modules grow per concern as features land:
|
|
10
|
+
* paths — path classification + normalization
|
|
11
|
+
* exec — executable resolution
|
|
12
|
+
* process — process liveness / control
|
|
13
|
+
* (ipc + shell follow when their consumers migrate off inline branches.)
|
|
14
|
+
*/
|
|
15
|
+
export const IS_WINDOWS = process.platform === 'win32';
|
|
16
|
+
export const IS_MACOS = process.platform === 'darwin';
|
|
17
|
+
export const IS_LINUX = process.platform === 'linux';
|
|
18
|
+
export * from './paths.js';
|
|
19
|
+
export * from './exec.js';
|
|
20
|
+
export * from './process.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this positional argument look like a filesystem path (vs a search term)?
|
|
3
|
+
*
|
|
4
|
+
* POSIX markers (`.`, `./`, `../`, `/`, `~`) are recognized on every platform —
|
|
5
|
+
* identical to the long-standing behavior. Windows-only shapes (drive-letter
|
|
6
|
+
* `C:\…`, UNC `\\…`, backslash-relative `.\` / `..\`) are recognized ONLY on
|
|
7
|
+
* win32, so a literal `C:\repo` typed on macOS/Linux still resolves as a search
|
|
8
|
+
* term — i.e. no behavior change off Windows.
|
|
9
|
+
*/
|
|
10
|
+
export declare function looksLikePath(query: string, platform?: NodeJS.Platform): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a path for comparison/prefix-matching: backslashes folded to forward
|
|
13
|
+
* slashes and lowercased on Windows (its filesystem is case-insensitive). On
|
|
14
|
+
* POSIX the input is returned unchanged, so callers behave exactly as before.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toComparablePath(p: string, platform?: NodeJS.Platform): string;
|
|
17
|
+
/**
|
|
18
|
+
* Canonical home directory. Use this instead of `process.env.HOME`, which is
|
|
19
|
+
* unset on Windows (where the home is `USERPROFILE`); `os.homedir()` resolves
|
|
20
|
+
* correctly on all three platforms.
|
|
21
|
+
*/
|
|
22
|
+
export declare function homeDir(): string;
|