@drewpayment/mink 0.13.0-beta.3 → 0.13.0-beta.5

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.
Files changed (79) hide show
  1. package/README.md +22 -4
  2. package/dashboard/out/404.html +1 -1
  3. package/dashboard/out/_next/static/chunks/157-7bbe4894a18a8332.js +1 -0
  4. package/dashboard/out/_next/static/chunks/447-8cfdad14e7559c07.js +1 -0
  5. package/dashboard/out/_next/static/chunks/app/(panels)/overview/page-141f456cd7141815.js +1 -0
  6. package/dashboard/out/_next/static/chunks/app/{layout-70a6d18f8e464960.js → layout-99d1a82956bc0f56.js} +1 -1
  7. package/dashboard/out/_next/static/css/0e381ead1d090c3f.css +1 -0
  8. package/dashboard/out/action-log.html +1 -1
  9. package/dashboard/out/action-log.txt +5 -5
  10. package/dashboard/out/activity.html +1 -1
  11. package/dashboard/out/activity.txt +5 -5
  12. package/dashboard/out/bugs.html +1 -1
  13. package/dashboard/out/bugs.txt +5 -5
  14. package/dashboard/out/capture.html +1 -1
  15. package/dashboard/out/capture.txt +5 -5
  16. package/dashboard/out/compression.html +1 -1
  17. package/dashboard/out/compression.txt +5 -5
  18. package/dashboard/out/config.html +1 -1
  19. package/dashboard/out/config.txt +5 -5
  20. package/dashboard/out/daemon.html +1 -1
  21. package/dashboard/out/daemon.txt +5 -5
  22. package/dashboard/out/design.html +1 -1
  23. package/dashboard/out/design.txt +5 -5
  24. package/dashboard/out/discord.html +1 -1
  25. package/dashboard/out/discord.txt +5 -5
  26. package/dashboard/out/file-index.html +1 -1
  27. package/dashboard/out/file-index.txt +5 -5
  28. package/dashboard/out/index.html +1 -1
  29. package/dashboard/out/index.txt +5 -5
  30. package/dashboard/out/insights.html +1 -1
  31. package/dashboard/out/insights.txt +5 -5
  32. package/dashboard/out/learning.html +1 -1
  33. package/dashboard/out/learning.txt +5 -5
  34. package/dashboard/out/overview.html +1 -1
  35. package/dashboard/out/overview.txt +6 -6
  36. package/dashboard/out/scheduler.html +1 -1
  37. package/dashboard/out/scheduler.txt +5 -5
  38. package/dashboard/out/sync.html +1 -1
  39. package/dashboard/out/sync.txt +5 -5
  40. package/dashboard/out/tokens.html +1 -1
  41. package/dashboard/out/tokens.txt +5 -5
  42. package/dashboard/out/waste.html +1 -1
  43. package/dashboard/out/waste.txt +5 -5
  44. package/dashboard/out/wiki.html +1 -1
  45. package/dashboard/out/wiki.txt +5 -5
  46. package/dist/cli.bun.js +3961 -3354
  47. package/dist/cli.node.js +4347 -3535
  48. package/package.json +1 -1
  49. package/src/cli.ts +29 -5
  50. package/src/commands/init.ts +132 -10
  51. package/src/commands/post-read.ts +1 -1
  52. package/src/commands/post-tool.ts +1 -1
  53. package/src/commands/refresh-hooks.ts +42 -0
  54. package/src/commands/retrieve.ts +1 -1
  55. package/src/commands/session-start.ts +11 -0
  56. package/src/core/agent-detect.ts +88 -0
  57. package/src/core/agent-pi.ts +383 -0
  58. package/src/core/code-skeleton.ts +1 -1
  59. package/src/core/compress-tool-output.ts +4 -4
  60. package/src/core/compression.ts +6 -7
  61. package/src/core/dashboard-api.ts +1 -1
  62. package/src/core/hook-output.ts +1 -1
  63. package/src/core/hook-refresh.ts +81 -0
  64. package/src/core/output-compression.ts +2 -2
  65. package/src/core/prompt.ts +27 -0
  66. package/src/core/self-update.ts +15 -0
  67. package/src/repositories/compression-cache-repo.ts +1 -1
  68. package/src/repositories/token-ledger-repo.ts +1 -1
  69. package/src/storage/schema.ts +2 -2
  70. package/src/types/compression.ts +1 -1
  71. package/src/types/config.ts +3 -2
  72. package/src/types/dashboard.ts +2 -2
  73. package/src/types/hook-input.ts +1 -1
  74. package/src/types/token-ledger.ts +2 -2
  75. package/dashboard/out/_next/static/chunks/189-fe789442321eb5eb.js +0 -1
  76. package/dashboard/out/_next/static/chunks/app/(panels)/overview/page-38b8430b5c56e807.js +0 -1
  77. package/dashboard/out/_next/static/css/5e43917ea49c5b3e.css +0 -1
  78. /package/dashboard/out/_next/static/{U9AeObddt4LmJkKRZpEfy → Yov5CTLEIMMDdQaCwuG1a}/_buildManifest.js +0 -0
  79. /package/dashboard/out/_next/static/{U9AeObddt4LmJkKRZpEfy → Yov5CTLEIMMDdQaCwuG1a}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drewpayment/mink",
3
- "version": "0.13.0-beta.3",
3
+ "version": "0.13.0-beta.5",
4
4
  "description": "A hidden presence that moves alongside the developer — token efficiency and cross-project wiki for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -16,8 +16,24 @@ switch (command) {
16
16
  break;
17
17
 
18
18
  case "init": {
19
- const { init } = await import("./commands/init");
20
- await init(cwd);
19
+ const { init, resolveTargetsFromFlag } = await import("./commands/init");
20
+ const args = process.argv.slice(3);
21
+ const agentFlagIndex = args.findIndex(
22
+ (a) => a === "--agent" || a.startsWith("--agent=")
23
+ );
24
+ let agentValue: string | undefined;
25
+ if (agentFlagIndex !== -1) {
26
+ const a = args[agentFlagIndex];
27
+ agentValue = a.includes("=") ? a.split("=").slice(1).join("=") : args[agentFlagIndex + 1];
28
+ }
29
+ const yes = args.includes("--yes") || args.includes("-y");
30
+ const targets = agentValue ? resolveTargetsFromFlag(agentValue) : undefined;
31
+ if (agentValue && (!targets || targets.length === 0)) {
32
+ console.error(`[mink] unknown --agent value: ${agentValue}`);
33
+ console.error(" Valid: claude, pi, all (or a comma-separated list)");
34
+ process.exit(1);
35
+ }
36
+ await init(cwd, { targets, interactive: !yes });
21
37
  break;
22
38
  }
23
39
 
@@ -59,6 +75,12 @@ switch (command) {
59
75
  break;
60
76
  }
61
77
 
78
+ case "refresh-hooks": {
79
+ const { refreshHooks } = await import("./commands/refresh-hooks");
80
+ refreshHooks(cwd, process.argv.slice(3));
81
+ break;
82
+ }
83
+
62
84
  case "pre-write": {
63
85
  const { preWrite } = await import("./commands/pre-write");
64
86
  await preWrite(cwd);
@@ -233,7 +255,9 @@ switch (command) {
233
255
  console.log("Usage: mink <command> [options]");
234
256
  console.log();
235
257
  console.log("Commands:");
236
- console.log(" init Initialize Mink in the current project");
258
+ console.log(" init [--agent X] [--yes] Initialize Mink in the current project");
259
+ console.log(" --agent claude|pi|all (default: detect & prompt)");
260
+ console.log(" refresh-hooks [--all] Regenerate hook wiring after an upgrade (--all: every project)");
237
261
  console.log(" status Display project health at a glance");
238
262
  console.log(" scan [--check] Force a full file index rescan");
239
263
  console.log(" config [key] [value] Manage global user settings");
@@ -275,7 +299,7 @@ switch (command) {
275
299
  console.log(" restore [backup] Restore state from a backup");
276
300
  console.log(" bug search <term> Search the bug log");
277
301
  console.log(" detect-waste Detect and flag wasteful patterns");
278
- console.log(" retrieve <token> Return a compressed tool output's original (spec 21)");
302
+ console.log(" retrieve <token> Return a compressed tool output's original (spec 22)");
279
303
  console.log(" reflect Generate learning memory reflections");
280
304
  console.log(" designqc [target] Capture design screenshots (spec 13)");
281
305
  console.log(" framework-advisor Generate framework advisor knowledge file (spec 14)");
@@ -285,7 +309,7 @@ switch (command) {
285
309
  console.log(" session-stop Finalize session and log data");
286
310
  console.log(" pre-read / post-read File read hooks");
287
311
  console.log(" pre-write / post-write File write hooks");
288
- console.log(" post-tool Tool-output compression hook (Bash/Grep/MCP, spec 21)");
312
+ console.log(" post-tool Tool-output compression hook (Bash/Grep/MCP, spec 22)");
289
313
  break;
290
314
 
291
315
  default:
@@ -12,6 +12,23 @@ import {
12
12
  isInsideVault,
13
13
  vaultProjects,
14
14
  } from "../core/vault";
15
+ import {
16
+ type AgentId,
17
+ AGENTS,
18
+ detectAgents,
19
+ resolveTargetsFromFlag,
20
+ } from "../core/agent-detect";
21
+ import { installPi } from "../core/agent-pi";
22
+ import { ask, stdinIsInteractive } from "../core/prompt";
23
+
24
+ export { resolveTargetsFromFlag };
25
+
26
+ export interface InitOptions {
27
+ /** Explicit set of hosts to wire. When omitted, Mink detects and/or prompts. */
28
+ targets?: AgentId[];
29
+ /** Allow an interactive agent-selection prompt when stdin is a TTY. */
30
+ interactive?: boolean;
31
+ }
15
32
 
16
33
  interface HookCommand {
17
34
  type: "command";
@@ -91,7 +108,7 @@ export function buildHooksConfig(cliPath: string): HooksConfig {
91
108
  { matcher: "Read", hooks: hook(`${prefix} post-read`) },
92
109
  { matcher: "Edit", hooks: hook(`${prefix} post-write`) },
93
110
  { matcher: "Write", hooks: hook(`${prefix} post-write`) },
94
- // Tool-output compression (spec 21) — a no-op until enabled via config.
111
+ // Tool-output compression (spec 22) — on by default; a no-op when disabled via config.
95
112
  { matcher: "Bash", hooks: hook(`${prefix} post-tool`) },
96
113
  { matcher: "Grep", hooks: hook(`${prefix} post-tool`) },
97
114
  ],
@@ -172,17 +189,80 @@ export function mergeHooksIntoSettings(
172
189
  atomicWriteJson(settingsPath, existing);
173
190
  }
174
191
 
192
+ /** Wire Mink into Claude Code: settings.json hooks + the project rule file. */
193
+ export function installClaude(
194
+ cwd: string,
195
+ cliPath: string
196
+ ): { settingsPath: string; rulePath: string } {
197
+ const settingsPath = resolve(cwd, ".claude", "settings.json");
198
+ mergeHooksIntoSettings(settingsPath, buildHooksConfig(cliPath));
199
+ const rulePath = writeMinkRule(cwd);
200
+ return { settingsPath, rulePath };
201
+ }
202
+
203
+ /**
204
+ * Decide which hosts to wire. Explicit `targets` win; otherwise Mink detects
205
+ * installed hosts and — only when running interactively at a TTY — asks the
206
+ * user to confirm. Non-interactive runs fall back to the detected set, and to
207
+ * Claude Code when nothing is detected (preserving Mink's original behavior).
208
+ */
209
+ export async function resolveTargets(
210
+ cwd: string,
211
+ opts: InitOptions
212
+ ): Promise<AgentId[]> {
213
+ if (opts.targets && opts.targets.length > 0) return opts.targets;
214
+
215
+ const detected = detectAgents(cwd);
216
+ const detectedIds = detected.filter((a) => a.detected).map((a) => a.id);
217
+
218
+ if (opts.interactive && stdinIsInteractive()) {
219
+ return promptForAgents(detected, detectedIds);
220
+ }
221
+
222
+ return detectedIds.length > 0 ? detectedIds : ["claude"];
223
+ }
224
+
225
+ async function promptForAgents(
226
+ detected: ReturnType<typeof detectAgents>,
227
+ defaults: AgentId[]
228
+ ): Promise<AgentId[]> {
229
+ const fallback: AgentId[] = defaults.length > 0 ? defaults : ["claude"];
230
+
231
+ console.log("Which assistant(s) should Mink work with?");
232
+ detected.forEach((a, i) => {
233
+ const tag = a.detected ? ` (detected — ${a.signals.join(", ")})` : "";
234
+ console.log(` ${i + 1}) ${a.label}${tag}`);
235
+ });
236
+
237
+ const answer = (
238
+ await ask(
239
+ `Enter numbers (comma-separated), 'a' for all [default: ${fallback.join(", ")}]: `
240
+ )
241
+ )
242
+ .trim()
243
+ .toLowerCase();
244
+
245
+ if (answer === "") return fallback;
246
+ if (answer === "a" || answer === "all") return AGENTS.map((a) => a.id);
247
+
248
+ const picked = answer
249
+ .split(",")
250
+ .map((s) => parseInt(s.trim(), 10))
251
+ .filter((n) => Number.isInteger(n) && n >= 1 && n <= detected.length)
252
+ .map((n) => detected[n - 1].id);
253
+
254
+ return picked.length > 0 ? picked : fallback;
255
+ }
256
+
175
257
  function isExistingInstallation(cwd: string): boolean {
176
258
  const dir = projectDir(cwd);
177
259
  if (!existsSync(dir)) return false;
178
260
  return existsSync(join(dir, "file-index.json"));
179
261
  }
180
262
 
181
- export async function init(cwd: string): Promise<void> {
263
+ export async function init(cwd: string, opts: InitOptions = {}): Promise<void> {
182
264
  const runtime = detectRuntime();
183
265
  const cliPath = resolveCliPath();
184
- const hooks = buildHooksConfig(cliPath);
185
- const settingsPath = resolve(cwd, ".claude", "settings.json");
186
266
  const dir = projectDir(cwd);
187
267
  const upgrading = isExistingInstallation(cwd);
188
268
 
@@ -193,8 +273,25 @@ export async function init(cwd: string): Promise<void> {
193
273
  console.log(` backup: ${backupName}`);
194
274
  }
195
275
 
196
- mergeHooksIntoSettings(settingsPath, hooks);
197
- const rulePath = writeMinkRule(cwd);
276
+ const targets = await resolveTargets(cwd, opts);
277
+
278
+ // Wire each selected host. Each installer is idempotent — re-running replaces
279
+ // Mink's prior entries rather than duplicating them — and touches only that
280
+ // host's configuration.
281
+ const wired: Record<string, string[]> = {};
282
+ for (const target of targets) {
283
+ if (target === "claude") {
284
+ const { settingsPath, rulePath } = installClaude(cwd, cliPath);
285
+ wired.claude = [`hooks: ${settingsPath}`, `rule: ${rulePath}`];
286
+ } else if (target === "pi") {
287
+ const r = installPi(cwd, cliPath);
288
+ wired.pi = [
289
+ `extension: ${r.extensionPath}`,
290
+ `guidance: ${r.guidancePath}`,
291
+ ...(r.notePath ? [`note skill: ${r.notePath}`] : []),
292
+ ];
293
+ }
294
+ }
198
295
 
199
296
  mkdirSync(dir, { recursive: true });
200
297
 
@@ -217,28 +314,53 @@ export async function init(cwd: string): Promise<void> {
217
314
  !Array.isArray(existingMeta.pathsByDevice)
218
315
  ? (existingMeta.pathsByDevice as Record<string, string>)
219
316
  : {};
317
+ // Record the set of wired hosts as the authoritative source of truth, unioned
318
+ // with any previously wired host so a single-target re-init never silently
319
+ // unwires the other.
320
+ const priorAgents = Array.isArray(existingMeta?.agents)
321
+ ? (existingMeta!.agents as string[])
322
+ : [];
323
+ const agents = Array.from(new Set([...priorAgents, ...targets]));
324
+ // Stamp the Mink version that generated these hooks so session-start (and
325
+ // `mink refresh-hooks`) can self-heal stale wiring after an upgrade.
326
+ let hooksVersion = "0.0.0";
327
+ try {
328
+ hooksVersion = require("../core/self-update").getInstallInfo().currentVersion;
329
+ } catch {
330
+ // Fall through with a sentinel; a missing/old stamp just forces one refresh.
331
+ }
220
332
  atomicWriteJson(metaPath, {
221
333
  ...(existingMeta ?? {}),
222
334
  cwd,
223
335
  name: basename(cwd),
224
336
  initTimestamp: existingMeta?.initTimestamp ?? new Date().toISOString(),
225
337
  version: "0.1.0",
338
+ hooksVersion,
226
339
  pathsByDevice: { ...existingPathsByDevice, [deviceId]: cwd },
340
+ agents,
227
341
  ...(isNotesProject ? { projectType: "notes" } : {}),
228
342
  });
229
343
 
344
+ const printWiring = () => {
345
+ for (const id of Object.keys(wired)) {
346
+ const label = AGENTS.find((a) => a.id === id)?.label ?? id;
347
+ console.log(` ${label}:`);
348
+ for (const line of wired[id]) console.log(` ${line}`);
349
+ }
350
+ };
351
+
230
352
  if (upgrading) {
231
353
  console.log(`[mink] upgrade complete`);
232
354
  console.log(` project: ${projectId}`);
233
- console.log(` hooks: ${settingsPath}`);
234
- console.log(` rule: ${rulePath}`);
355
+ console.log(` agents: ${agents.join(", ")}`);
356
+ printWiring();
235
357
  } else {
236
358
  console.log(`[mink] initialized`);
237
359
  console.log(` project: ${projectId} (${identity.source})`);
238
360
  console.log(` state: ${dir}`);
239
361
  console.log(` runtime: ${runtime}`);
240
- console.log(` hooks: ${settingsPath}`);
241
- console.log(` rule: ${rulePath}`);
362
+ console.log(` agents: ${agents.join(", ")}`);
363
+ printWiring();
242
364
  }
243
365
 
244
366
  // Surface a one-time hint when the project is in a git repo with no remote
@@ -205,7 +205,7 @@ export async function postRead(cwd: string): Promise<void> {
205
205
  // Persist state
206
206
  atomicWriteJson(sessionPath(cwd), state);
207
207
 
208
- // Tool-output compression (spec 21). Substitute a compact, reversible
208
+ // Tool-output compression (spec 22). Substitute a compact, reversible
209
209
  // summary for a large whole-file read. Skipped for ranged reads (their
210
210
  // output is only a slice) and a no-op unless compression is enabled. Uses
211
211
  // the on-disk content as the canonical original so signature extraction
@@ -1,4 +1,4 @@
1
- // Generic PostToolUse compression hook (spec 21) for tools that produce large,
1
+ // Generic PostToolUse compression hook (spec 22) for tools that produce large,
2
2
  // non-file output — Bash, Grep/Glob, and MCP tools. The Read tool is handled by
3
3
  // post-read (which has the on-disk content and ranged-read awareness); this hook
4
4
  // compresses the payload text directly.
@@ -0,0 +1,42 @@
1
+ // `mink refresh-hooks [--all]` — regenerate Mink's generated hook wiring after an
2
+ // upgrade so it matches the installed version's templates.
3
+ //
4
+ // (default) Refresh the current project.
5
+ // --all Refresh every registered project that exists on this device.
6
+ //
7
+ // `--all` is what `runSelfUpgrade` spawns (as the freshly-installed binary) right
8
+ // after a successful upgrade, so every project is refreshed eagerly rather than
9
+ // waiting for its next session-start.
10
+
11
+ import { existsSync } from "fs";
12
+ import { refreshProjectHooks } from "../core/hook-refresh";
13
+
14
+ export function refreshHooks(cwd: string, args: string[]): void {
15
+ const all = args.includes("--all");
16
+
17
+ if (!all) {
18
+ const r = refreshProjectHooks(cwd, { force: true });
19
+ console.log(
20
+ r.refreshed
21
+ ? `[mink] refreshed hooks (${r.agents.join(", ")}) → ${r.version}`
22
+ : "[mink] nothing to refresh — run `mink init` first."
23
+ );
24
+ return;
25
+ }
26
+
27
+ const { listRegisteredProjects } = require("../core/project-registry");
28
+ const { getOrCreateDeviceId } = require("../core/device");
29
+ const deviceId = getOrCreateDeviceId();
30
+
31
+ let refreshed = 0;
32
+ for (const p of listRegisteredProjects()) {
33
+ const local = p.pathsByDevice?.[deviceId] ?? p.cwd;
34
+ if (!local || !existsSync(local)) continue; // not present on this device
35
+ const r = refreshProjectHooks(local, { force: true });
36
+ if (r.refreshed) {
37
+ refreshed++;
38
+ console.log(` ${p.name} (${r.agents.join(", ")})`);
39
+ }
40
+ }
41
+ console.log(`[mink] refreshed hooks for ${refreshed} project(s) → installed version.`);
42
+ }
@@ -1,5 +1,5 @@
1
1
  // `mink retrieve <token>` — return the byte-exact original of a previously
2
- // compressed tool output (spec 21 §Reversibility). Prints the original to
2
+ // compressed tool output (spec 22 §Reversibility). Prints the original to
3
3
  // stdout on a hit; on a miss (unknown or expired token) it prints a short,
4
4
  // non-fatal notice to stderr and exits 0 so the assistant is never stranded by
5
5
  // an error.
@@ -41,6 +41,17 @@ export function sessionStart(cwd: string): void {
41
41
  // Migration is best-effort; never block session-start
42
42
  }
43
43
 
44
+ // Self-heal stale hook wiring after a Mink upgrade. Regenerates this project's
45
+ // configured hooks (Claude settings, Pi extension) when the generating version
46
+ // changed, so a `mink upgrade` never leaves the user re-running `mink init`.
47
+ // Idempotent, version-gated, and silent — never blocks session-start.
48
+ try {
49
+ const { refreshHooksIfStale } = require("../core/hook-refresh");
50
+ refreshHooksIfStale(cwd);
51
+ } catch {
52
+ // Never crash hooks
53
+ }
54
+
44
55
  // Sync pull before session begins (if enabled)
45
56
  try {
46
57
  const { isSyncInitialized, syncPull } = require("../core/sync");
@@ -0,0 +1,88 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ // Supported host coding assistants Mink can attach to. Adding a new host is a
7
+ // matter of appending an entry here plus an installer in init.ts.
8
+ export type AgentId = "claude" | "pi";
9
+
10
+ export interface AgentMeta {
11
+ id: AgentId;
12
+ label: string;
13
+ /** Project-local config directory that signals the host is used here. */
14
+ projectDir: string;
15
+ /** Per-user global config directory that signals the host is installed. */
16
+ globalDir: string;
17
+ /** Executable name to probe on PATH. */
18
+ bin: string;
19
+ }
20
+
21
+ export const AGENTS: AgentMeta[] = [
22
+ {
23
+ id: "claude",
24
+ label: "Claude Code",
25
+ projectDir: ".claude",
26
+ globalDir: join(homedir(), ".claude"),
27
+ bin: "claude",
28
+ },
29
+ {
30
+ id: "pi",
31
+ label: "Pi",
32
+ projectDir: ".pi",
33
+ globalDir: join(homedir(), ".pi"),
34
+ bin: "pi",
35
+ },
36
+ ];
37
+
38
+ export interface AgentInfo extends AgentMeta {
39
+ detected: boolean;
40
+ /** Human-readable reasons the host was (or was not) detected. */
41
+ signals: string[];
42
+ }
43
+
44
+ function commandExists(bin: string): boolean {
45
+ try {
46
+ const probe = process.platform === "win32" ? `where ${bin}` : `command -v ${bin}`;
47
+ execSync(probe, { stdio: "ignore" });
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Inspect a single host's footprint relative to `cwd`. Detection is best-effort
56
+ * and layered, strongest signal first: a project-local config directory is the
57
+ * clearest sign the host is actually used here; a global config directory or a
58
+ * binary on PATH only prove the host is installed somewhere.
59
+ */
60
+ export function detectAgent(meta: AgentMeta, cwd: string): AgentInfo {
61
+ const signals: string[] = [];
62
+ if (existsSync(join(cwd, meta.projectDir))) {
63
+ signals.push(`project config (${meta.projectDir}/)`);
64
+ }
65
+ if (existsSync(meta.globalDir)) {
66
+ signals.push("global config");
67
+ }
68
+ if (commandExists(meta.bin)) {
69
+ signals.push("on PATH");
70
+ }
71
+ return { ...meta, detected: signals.length > 0, signals };
72
+ }
73
+
74
+ export function detectAgents(cwd: string): AgentInfo[] {
75
+ return AGENTS.map((m) => detectAgent(m, cwd));
76
+ }
77
+
78
+ export function resolveTargetsFromFlag(flag: string): AgentId[] {
79
+ const normalized = flag.trim().toLowerCase();
80
+ if (normalized === "all") return AGENTS.map((a) => a.id);
81
+ const ids = normalized
82
+ .split(",")
83
+ .map((s) => s.trim())
84
+ .filter(Boolean);
85
+ const valid = AGENTS.map((a) => a.id) as string[];
86
+ const resolved = ids.filter((id): id is AgentId => valid.includes(id));
87
+ return resolved;
88
+ }