@phnx-labs/agents-cli 1.20.6 → 1.20.8

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.
@@ -3,22 +3,24 @@ import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
5
  import { registerCommandGroups } from '../lib/help.js';
6
- import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, describeTransport, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
6
+ import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
7
+ import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
7
8
  // Help groups — mirror `agents browser` so the mental model carries over.
8
9
  const COMPUTER_HELP_GROUPS = [
9
- { title: 'Installation', names: ['install-helper'] },
10
+ { title: 'Installation', names: ['setup'] },
10
11
  { title: 'Daemon lifecycle', names: ['start', 'stop', 'reload', 'status'] },
11
- { title: 'Capture evidence', names: ['screenshot'] },
12
+ { title: 'Observe', names: ['apps', 'describe', 'screenshot', 'get-text'] },
13
+ { title: 'Interact', names: ['launch', 'raise', 'click', 'right-click', 'type', 'type-text', 'key', 'drag', 'scroll', 'ax-action', 'focus', 'wait'] },
12
14
  ];
13
15
  export function registerComputerCommand(program) {
14
16
  const computer = program
15
- .command('computers')
17
+ .command('computer')
16
18
  .description('Drive macOS apps via Accessibility — list, screenshot, click, type (macOS only)')
17
19
  // The whole subsystem is macOS Accessibility / TCC. Fail fast with a clear
18
20
  // message on other platforms instead of a downstream ENOENT / launchctl error.
19
21
  .hook('preAction', () => {
20
22
  if (process.platform !== 'darwin') {
21
- console.error('agents computers: macOS only — it drives apps via the macOS Accessibility API.');
23
+ console.error('agents computer: macOS only — it drives apps via the macOS Accessibility API.');
22
24
  process.exit(1);
23
25
  }
24
26
  });
@@ -26,18 +28,15 @@ export function registerComputerCommand(program) {
26
28
  registerCommandGroups(computer, COMPUTER_HELP_GROUPS);
27
29
  }
28
30
  export function registerComputerSubcommands(program) {
29
- registerInstallHelperCommand(program);
31
+ registerSetupCommand(program);
30
32
  registerStartCommand(program);
31
33
  registerStopCommand(program);
32
34
  registerReloadCommand(program);
33
35
  registerStatusCommand(program);
34
36
  registerScreenshotCommand(program);
37
+ registerActionCommands(program);
35
38
  registerCommandGroups(program, COMPUTER_HELP_GROUPS);
36
39
  }
37
- function reportMissingHelper() {
38
- console.error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
39
- process.exit(1);
40
- }
41
40
  function registerStatusCommand(program) {
42
41
  program
43
42
  .command('status')
@@ -58,7 +57,7 @@ function registerStatusCommand(program) {
58
57
  console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} (peer-auth on socket)`);
59
58
  if (!installed) {
60
59
  console.log('');
61
- console.log('Run: agents computer install-helper');
60
+ console.log('Run: agents computer setup');
62
61
  return;
63
62
  }
64
63
  if (!socketUp) {
@@ -92,50 +91,55 @@ function registerStatusCommand(program) {
92
91
  function registerScreenshotCommand(program) {
93
92
  program
94
93
  .command('screenshot')
95
- .description('Capture a JPEG of the frontmost window of a bundle id (default: frontmost app)')
96
- .option('--bundle <id>', 'Bundle id to capture (default: bundle id of frontmost app)')
94
+ .description('Capture a window (default: largest), enumerate windows (--list), or the whole display (--display)')
95
+ .option('--bundle <id>', 'Bundle id to capture (default: frontmost allow-listed app)')
96
+ .option('--pid <n>', 'Target pid directly (overrides --bundle)', (v) => parseInt(v, 10))
97
+ .option('--list', 'List the app\'s windows (id/title/layer/bounds) instead of capturing — reveals modals/popups')
98
+ .option('--window-id <n>', 'Capture a specific window by id (from --list)', (v) => parseInt(v, 10))
99
+ .option('--display', 'Capture the whole display the app is on (composites stacked modals)')
97
100
  .option('--out <path>', 'Output JPEG path', './computer-screenshot.jpg')
98
101
  .option('--quality <n>', 'JPEG quality 1-100', (v) => parseInt(v, 10), 85)
102
+ .option('--json', 'Emit JSON (metadata for captures; window list for --list)')
99
103
  .action(async (opts) => {
100
- const transport = describeTransport();
101
- if (transport.kind === 'none')
102
- reportMissingHelper();
103
104
  const quality = Math.max(1, Math.min(100, opts.quality || 85));
104
- const client = openComputerClient();
105
- try {
106
- // Step 1: list_apps to get the candidate set.
107
- const apps = await client.call('list_apps');
108
- if (apps.error) {
109
- console.error(`error: ${apps.error.code}: ${apps.error.message}`);
110
- process.exit(1);
111
- }
112
- const list = apps.result?.apps || [];
113
- let target;
114
- if (opts.bundle) {
115
- target = list.find((a) => a.bundle_id === opts.bundle);
116
- if (!target) {
117
- console.error(`bundle not in allow list (or not running): ${opts.bundle}`);
118
- console.error(`add Computer(${opts.bundle}) to a permissions group, then \`agents computer reload\``);
105
+ await withClient(async (client) => {
106
+ // Resolve the target pid (explicit --pid, else --bundle, else frontmost).
107
+ let pid = opts.pid;
108
+ if (pid == null) {
109
+ const list = unwrap(await client.call('list_apps')).apps || [];
110
+ const picked = pickTarget(list, { bundle: opts.bundle });
111
+ if (!picked.ok) {
112
+ console.error(picked.error);
119
113
  process.exit(1);
120
114
  }
115
+ pid = picked.app.pid;
121
116
  }
122
- else {
123
- target = list.find((a) => a.active);
124
- if (!target) {
125
- console.error('no active app found in allow list');
126
- console.error('add Computer(<bundle-id>) to a permissions group, then `agents computer reload`');
127
- process.exit(1);
117
+ // --list: enumerate windows, no image.
118
+ if (opts.list) {
119
+ const res = unwrap(await client.call('screenshot', { pid, list: true }));
120
+ const windows = res.windows || [];
121
+ if (opts.json) {
122
+ console.log(JSON.stringify(res, null, 2));
128
123
  }
124
+ else if (windows.length === 0) {
125
+ console.log('(no windows)');
126
+ }
127
+ else {
128
+ for (const w of windows) {
129
+ const b = w.bounds || [];
130
+ console.log(`${String(w.window_id).padStart(8)} layer ${w.layer} [${b.join(',')}] ${w.title || '(untitled)'}`);
131
+ }
132
+ }
133
+ return;
129
134
  }
130
- // Step 2: screenshot.
131
- const shot = await client.call('screenshot', { pid: target.pid, quality });
132
- if (shot.error) {
133
- console.error(`error: ${shot.error.code}: ${shot.error.message}`);
134
- process.exit(1);
135
- }
136
- const b64 = shot.result?.image_data;
137
- const width = shot.result?.width;
138
- const height = shot.result?.height;
135
+ // Capture: window (default / --window-id) or full display.
136
+ const params = { pid, quality };
137
+ if (opts.display)
138
+ params.display = true;
139
+ else if (opts.windowId != null)
140
+ params.window_id = opts.windowId;
141
+ const res = unwrap(await client.call('screenshot', params));
142
+ const b64 = res.image_data;
139
143
  if (!b64) {
140
144
  console.error('helper returned no image_data');
141
145
  process.exit(1);
@@ -143,14 +147,20 @@ function registerScreenshotCommand(program) {
143
147
  const buf = Buffer.from(b64, 'base64');
144
148
  const outPath = path.resolve(opts.out);
145
149
  fs.writeFileSync(outPath, buf);
146
- console.log(`saved: ${outPath} (${width ?? '?'}x${height ?? '?'}, ${buf.byteLength} bytes)`);
147
- }
148
- finally {
149
- await client.close();
150
- }
150
+ if (opts.json) {
151
+ // Drop the heavy base64 from the metadata echo; report where it went.
152
+ const meta = { ...res, image_data: `<saved to ${outPath}>` };
153
+ console.log(JSON.stringify(meta, null, 2));
154
+ }
155
+ else {
156
+ const origin = res.origin || [];
157
+ const originStr = origin.length === 2 ? `, origin [${origin.join(',')}], scale ${res.scale ?? '?'}` : '';
158
+ console.log(`saved: ${outPath} (${res.width ?? '?'}x${res.height ?? '?'}, ${buf.byteLength} bytes${originStr})`);
159
+ }
160
+ });
151
161
  });
152
162
  }
153
- // install-helper:
163
+ // setup (alias: install-helper):
154
164
  // 1. resolve dist .app
155
165
  // 2. copy to /Applications/Computer Helper.app
156
166
  // 3. codesign --verify the destination
@@ -167,9 +177,10 @@ const HELPER_BUNDLE_ID = 'com.phnx-labs.computer-helper';
167
177
  const HELPER_APP_NAME = 'Computer Helper.app';
168
178
  const HELPER_APP_DEST = `/Applications/${HELPER_APP_NAME}`;
169
179
  const HELPER_LABEL = HELPER_BUNDLE_ID;
170
- function registerInstallHelperCommand(program) {
180
+ function registerSetupCommand(program) {
171
181
  program
172
- .command('install-helper')
182
+ .command('setup')
183
+ .alias('install-helper')
173
184
  .description('Install ComputerHelper.app to /Applications/ (does NOT activate the daemon — run `start` to enable)')
174
185
  .action(async () => {
175
186
  const srcApp = resolveHelperApp();
@@ -263,12 +274,12 @@ function registerStartCommand(program) {
263
274
  const logPath = resolveLogPath();
264
275
  if (!fs.existsSync(plistPath)) {
265
276
  console.error(`plist not found at ${plistPath}`);
266
- console.error('run: agents computer install-helper');
277
+ console.error('run: agents computer setup');
267
278
  process.exit(1);
268
279
  }
269
280
  if (!fs.existsSync(HELPER_APP_DEST)) {
270
281
  console.error(`helper app not found at ${HELPER_APP_DEST}`);
271
- console.error('run: agents computer install-helper');
282
+ console.error('run: agents computer setup');
272
283
  process.exit(1);
273
284
  }
274
285
  const uid = process.getuid?.();
@@ -1,14 +1,30 @@
1
1
  /**
2
- * `agents inspect <agent>[@version]`single-agent, single-version detail view.
2
+ * `agents inspect <target>`detail view for one agent+version or one DotAgents repo.
3
3
  *
4
- * Summary mode shows the per-version header (paths, shim, capabilities, resource
5
- * counts, sessions). Drill-down flags (`--skills`, `--hooks`, `--mcp`, ...) list
6
- * one resource kind; passing a positional query to the same flag fuzzy-searches
7
- * for a single resource and prints its detail. Resource names render as OSC-8
8
- * hyperlinks to the marker file (SKILL.md / WORKFLOW.md / AGENT.md / the file
9
- * itself) so users can click straight to the source.
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
  */
11
14
  import { Command } from 'commander';
15
+ /** Resource kinds the inspect command can drill into. */
16
+ declare const DRILLABLE_KINDS: readonly ["commands", "skills", "hooks", "mcp", "rules", "plugins", "workflows", "subagents"];
17
+ type DrillableKind = typeof DRILLABLE_KINDS[number];
18
+ interface ResourceItem {
19
+ name: string;
20
+ source: string;
21
+ /** Absolute path to the resource entry (file or directory). */
22
+ path: string;
23
+ /** Path the OSC-8 link should point at — marker file inside bundles, else `path`. */
24
+ linkTarget: string;
25
+ /** One-line description (frontmatter `description:` or first non-frontmatter line). */
26
+ description: string;
27
+ }
12
28
  interface InspectOptions {
13
29
  brief?: boolean;
14
30
  json?: boolean;
@@ -23,4 +39,19 @@ interface InspectOptions {
23
39
  }
24
40
  export declare function registerInspectCommand(program: Command): void;
25
41
  export declare function inspectAction(target: string, options: InspectOptions): Promise<void>;
42
+ export interface RepoTarget {
43
+ /** Display label: 'user' | 'system' | 'project', an extra-repo alias, or a path-derived name. */
44
+ label: string;
45
+ /** Absolute path to the DotAgents root (the dir holding commands/, skills/, ...). */
46
+ root: string;
47
+ }
48
+ /**
49
+ * Resolve a non-agent target as a DotAgents repo: the built-in layer names,
50
+ * a registered extra-repo alias, or a filesystem path. Paths accept either a
51
+ * DotAgents root itself or a repo whose `.agents/` dir should be inspected.
52
+ * Returns null when the target is none of these.
53
+ */
54
+ export declare function resolveRepoTarget(target: string, cwd?: string): RepoTarget | null;
55
+ /** List one resource kind from a single repo root — no layering, no overrides. */
56
+ export declare function collectRepoKind(repo: RepoTarget, kind: DrillableKind): ResourceItem[];
26
57
  export {};
@@ -1,13 +1,17 @@
1
1
  /**
2
- * `agents inspect <agent>[@version]`single-agent, single-version detail view.
2
+ * `agents inspect <target>`detail view for one agent+version or one DotAgents repo.
3
3
  *
4
- * Summary mode shows the per-version header (paths, shim, capabilities, resource
5
- * counts, sessions). Drill-down flags (`--skills`, `--hooks`, `--mcp`, ...) list
6
- * one resource kind; passing a positional query to the same flag fuzzy-searches
7
- * for a single resource and prints its detail. Resource names render as OSC-8
8
- * hyperlinks to the marker file (SKILL.md / WORKFLOW.md / AGENT.md / the file
9
- * itself) so users can click straight to the source.
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 <agent>')
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 agentKey = (rawAgent || '').toLowerCase();
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
- agent,
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(`${agent}@${version}`) + ' ' + chalk.gray(`${kind} (${items.length})`) + '\n');
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({ agent, version, kind, query, match: null, suggestions: suggestions.map(s => s.name) }, null, 2));
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
- agent,
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(`${agent}@${version}`) + ' ' + chalk.gray(`${kind} matching "${query}"`) + '\n');
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 = os.homedir();
157
- return cwd.startsWith(home) ? '~' + cwd.slice(home.length) : cwd;
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 && isPathLike(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}`));
@@ -145,8 +145,8 @@ export async function runSetup(program, options = {}) {
145
145
  if (!isShimsInPath()) {
146
146
  const pathResult = addShimsToPath();
147
147
  if (pathResult.success && !pathResult.alreadyPresent) {
148
- console.log(chalk.green(`\nAdded shims to ~/${pathResult.rcFile}`));
149
- console.log(chalk.gray('Restart your shell or run: source ~/' + pathResult.rcFile));
148
+ console.log(chalk.green(`\nAdded shims to ${pathResult.location}`));
149
+ console.log(chalk.gray(pathResult.reloadHint));
150
150
  }
151
151
  else if (!pathResult.success) {
152
152
  console.log(chalk.yellow('\nTo enable version switching, add shims to PATH:'));
@@ -402,8 +402,8 @@ export function registerVersionsCommands(program) {
402
402
  if (!isShimsInPath()) {
403
403
  const pathResult = addShimsToPath();
404
404
  if (pathResult.success && !pathResult.alreadyPresent) {
405
- console.log(chalk.green(` Added shims to ~/${pathResult.rcFile}`));
406
- console.log(chalk.gray(' Restart your shell or run: source ~/' + pathResult.rcFile));
405
+ console.log(chalk.green(` Added shims to ${pathResult.location}`));
406
+ console.log(chalk.gray(' ' + pathResult.reloadHint));
407
407
  }
408
408
  else if (!pathResult.success) {
409
409
  console.log(chalk.yellow('\nCould not auto-add shims to PATH:'));