counselors 0.1.0

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/dist/cli.js ADDED
@@ -0,0 +1,2585 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/ui/logger.ts
7
+ function isDebug() {
8
+ return process.env.DEBUG === "1" || process.env.DEBUG === "counselors";
9
+ }
10
+ function debug(msg) {
11
+ if (isDebug()) {
12
+ process.stderr.write(`[debug] ${msg}
13
+ `);
14
+ }
15
+ }
16
+ function warn(msg) {
17
+ process.stderr.write(`\u26A0 ${msg}
18
+ `);
19
+ }
20
+ function error(msg) {
21
+ process.stderr.write(`\u2717 ${msg}
22
+ `);
23
+ }
24
+ function info(msg) {
25
+ process.stdout.write(`${msg}
26
+ `);
27
+ }
28
+ function success(msg) {
29
+ process.stdout.write(`\u2713 ${msg}
30
+ `);
31
+ }
32
+
33
+ // src/commands/agent.ts
34
+ function registerAgentCommand(program2) {
35
+ program2.command("agent").description("Print setup and skill installation instructions").action(async () => {
36
+ const instructions = `# Counselors \u2014 Setup & Skill Installation
37
+
38
+ ## 1. Install the CLI
39
+
40
+ \`\`\`bash
41
+ npm install -g counselors
42
+ \`\`\`
43
+
44
+ Requires Node 20+.
45
+
46
+ ## 2. Configure tools
47
+
48
+ Auto-discover and configure all installed AI coding agents:
49
+
50
+ \`\`\`bash
51
+ counselors init --auto
52
+ \`\`\`
53
+
54
+ This detects installed agents (Claude, Codex, Gemini, Amp), configures them with recommended models, and writes your config to \`~/.config/counselors/config.json\`. The output is JSON listing what was configured.
55
+
56
+ You can also manage tools individually:
57
+
58
+ \`\`\`bash
59
+ counselors tools discover # Find available agents
60
+ counselors tools add # Add a tool (interactive)
61
+ counselors tools remove <id> # Remove a tool
62
+ counselors tools rename <old> <new> # Rename a tool
63
+ counselors ls # List configured tools
64
+ counselors doctor # Verify tools are working
65
+ \`\`\`
66
+
67
+ ## 3. Install the skill
68
+
69
+ The \`/counselors\` skill lets AI coding agents invoke counselors directly via a slash command. To install it, copy the skill template into your agent's skill directory.
70
+
71
+ ### Claude Code
72
+
73
+ \`\`\`bash
74
+ # Print the skill template
75
+ counselors skill
76
+
77
+ # Save it to Claude Code's skill directory
78
+ counselors skill > ~/.claude/commands/counselors.md
79
+ \`\`\`
80
+
81
+ ### Other agents
82
+
83
+ Save the output of \`counselors skill\` to the appropriate skill/command directory for your agent.
84
+
85
+ ## 4. Verify
86
+
87
+ \`\`\`bash
88
+ counselors doctor
89
+ \`\`\`
90
+
91
+ Then use \`/counselors\` from your AI coding agent to fan out a prompt for parallel review.
92
+ `;
93
+ info(instructions);
94
+ });
95
+ }
96
+
97
+ // src/commands/doctor.ts
98
+ import { existsSync as existsSync4 } from "fs";
99
+
100
+ // src/adapters/amp.ts
101
+ import { existsSync } from "fs";
102
+
103
+ // src/constants.ts
104
+ import { homedir } from "os";
105
+ import { join } from "path";
106
+ var xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
107
+ var CONFIG_DIR = join(xdgConfig, "counselors");
108
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
109
+ var AMP_SETTINGS_FILE = join(CONFIG_DIR, "amp-readonly-settings.json");
110
+ var AMP_DEEP_SETTINGS_FILE = join(
111
+ CONFIG_DIR,
112
+ "amp-deep-settings.json"
113
+ );
114
+ var KILL_GRACE_PERIOD = 15e3;
115
+ var TEST_TIMEOUT = 3e4;
116
+ var DISCOVERY_TIMEOUT = 5e3;
117
+ var VERSION_TIMEOUT = 1e4;
118
+ var DEFAULT_MAX_CONTEXT_KB = 50;
119
+ function getExtendedSearchPaths() {
120
+ const home = homedir();
121
+ const paths = [
122
+ join(home, ".local", "bin"),
123
+ "/usr/local/bin",
124
+ "/opt/homebrew/bin",
125
+ join(home, ".npm-global", "bin"),
126
+ join(home, ".volta", "bin"),
127
+ join(home, ".bun", "bin")
128
+ ];
129
+ const nvmBin = process.env.NVM_BIN;
130
+ if (nvmBin) paths.push(nvmBin);
131
+ const fnmMultishell = process.env.FNM_MULTISHELL_PATH;
132
+ if (fnmMultishell) paths.push(join(fnmMultishell, "bin"));
133
+ return paths;
134
+ }
135
+ var MAX_SLUG_LENGTH = 40;
136
+ var CONFIG_FILE_MODE = 384;
137
+ function sanitizeId(id) {
138
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
139
+ }
140
+ var SAFE_ID_RE = /^[a-zA-Z0-9._-]+$/;
141
+ var VERSION = true ? "0.1.0" : "0.0.0-dev";
142
+
143
+ // src/adapters/base.ts
144
+ var BaseAdapter = class {
145
+ parseResult(result) {
146
+ return {
147
+ status: result.timedOut ? "timeout" : result.exitCode === 0 ? "success" : "error",
148
+ exitCode: result.exitCode,
149
+ durationMs: result.durationMs,
150
+ wordCount: result.stdout.split(/\s+/).filter(Boolean).length
151
+ };
152
+ }
153
+ };
154
+
155
+ // src/adapters/amp.ts
156
+ var AmpAdapter = class extends BaseAdapter {
157
+ id = "amp";
158
+ displayName = "Amp CLI";
159
+ commands = ["amp"];
160
+ installUrl = "https://ampcode.com";
161
+ readOnly = { level: "enforced" };
162
+ models = [
163
+ {
164
+ id: "smart",
165
+ name: "Smart \u2014 Opus 4.6, most capable",
166
+ recommended: true,
167
+ extraFlags: ["-m", "smart"]
168
+ },
169
+ {
170
+ id: "deep",
171
+ name: "Deep \u2014 GPT-5.2 Codex, extended thinking",
172
+ extraFlags: ["-m", "deep"]
173
+ }
174
+ ];
175
+ buildInvocation(req) {
176
+ const args = ["-x"];
177
+ if (req.extraFlags) {
178
+ args.push(...req.extraFlags);
179
+ }
180
+ const isDeep = req.extraFlags?.includes("deep") && req.extraFlags?.[req.extraFlags.indexOf("deep") - 1] === "-m";
181
+ const settingsFile = isDeep ? AMP_DEEP_SETTINGS_FILE : AMP_SETTINGS_FILE;
182
+ if (req.readOnlyPolicy !== "none" && existsSync(settingsFile)) {
183
+ args.push("--settings-file", settingsFile);
184
+ }
185
+ const deepSafetyPrompt = isDeep ? "\n\nMANDATORY: Do not change any files. You are in read-only mode." : "";
186
+ const stdinContent = req.prompt + deepSafetyPrompt + "\n\nUse the oracle tool to provide deeper reasoning and analysis on the most complex or critical aspects of this review.";
187
+ return {
188
+ cmd: req.binary ?? "amp",
189
+ args,
190
+ stdin: stdinContent,
191
+ cwd: req.cwd
192
+ };
193
+ }
194
+ parseResult(result) {
195
+ return {
196
+ ...super.parseResult(result)
197
+ };
198
+ }
199
+ };
200
+ function parseAmpUsage(output) {
201
+ const freeMatch = output.match(/Amp Free: \$([0-9.]+)\/\$([0-9.]+)/);
202
+ const creditsMatch = output.match(/Individual credits: \$([0-9.]+)/);
203
+ return {
204
+ freeRemaining: freeMatch ? parseFloat(freeMatch[1]) : 0,
205
+ freeTotal: freeMatch ? parseFloat(freeMatch[2]) : 0,
206
+ creditsRemaining: creditsMatch ? parseFloat(creditsMatch[1]) : 0
207
+ };
208
+ }
209
+ function computeAmpCost(before, after) {
210
+ const freeUsed = Math.max(0, before.freeRemaining - after.freeRemaining);
211
+ const creditsUsed = Math.max(
212
+ 0,
213
+ before.creditsRemaining - after.creditsRemaining
214
+ );
215
+ const totalCost = freeUsed + creditsUsed;
216
+ const source = creditsUsed > 0 ? "credits" : "free";
217
+ return {
218
+ cost_usd: Math.round(totalCost * 100) / 100,
219
+ free_used_usd: Math.round(freeUsed * 100) / 100,
220
+ credits_used_usd: Math.round(creditsUsed * 100) / 100,
221
+ source,
222
+ free_remaining_usd: after.freeRemaining,
223
+ free_total_usd: after.freeTotal,
224
+ credits_remaining_usd: after.creditsRemaining
225
+ };
226
+ }
227
+
228
+ // src/adapters/claude.ts
229
+ var ClaudeAdapter = class extends BaseAdapter {
230
+ id = "claude";
231
+ displayName = "Claude Code";
232
+ commands = ["claude"];
233
+ installUrl = "https://docs.anthropic.com/en/docs/claude-code";
234
+ readOnly = { level: "enforced" };
235
+ models = [
236
+ {
237
+ id: "opus",
238
+ name: "Opus 4.6 \u2014 most capable",
239
+ recommended: true,
240
+ extraFlags: ["--model", "opus"]
241
+ },
242
+ {
243
+ id: "sonnet",
244
+ name: "Sonnet 4.5 \u2014 fast and capable",
245
+ extraFlags: ["--model", "sonnet"]
246
+ },
247
+ {
248
+ id: "haiku",
249
+ name: "Haiku 4.5 \u2014 fastest, most affordable",
250
+ extraFlags: ["--model", "haiku"]
251
+ }
252
+ ];
253
+ buildInvocation(req) {
254
+ const instruction = `Read the file at ${req.promptFilePath} and follow the instructions within it.`;
255
+ const args = ["-p", "--output-format", "text"];
256
+ if (req.extraFlags) {
257
+ args.push(...req.extraFlags);
258
+ }
259
+ if (req.readOnlyPolicy !== "none") {
260
+ args.push(
261
+ "--tools",
262
+ "Read,Glob,Grep,WebFetch,WebSearch",
263
+ "--allowedTools",
264
+ "Read,Glob,Grep,WebFetch,WebSearch",
265
+ "--strict-mcp-config"
266
+ );
267
+ }
268
+ args.push(instruction);
269
+ return { cmd: req.binary ?? "claude", args, cwd: req.cwd };
270
+ }
271
+ };
272
+
273
+ // src/adapters/codex.ts
274
+ var CodexAdapter = class extends BaseAdapter {
275
+ id = "codex";
276
+ displayName = "OpenAI Codex";
277
+ commands = ["codex"];
278
+ installUrl = "https://github.com/openai/codex";
279
+ readOnly = { level: "enforced" };
280
+ models = [
281
+ {
282
+ id: "gpt-5.3-codex",
283
+ compoundId: "codex-5.3-high",
284
+ name: "GPT-5.3 Codex \u2014 high reasoning",
285
+ recommended: true,
286
+ extraFlags: ["-m", "gpt-5.3-codex", "-c", "model_reasoning_effort=high"]
287
+ },
288
+ {
289
+ id: "gpt-5.3-codex",
290
+ compoundId: "codex-5.3-xhigh",
291
+ name: "GPT-5.3 Codex \u2014 xhigh reasoning",
292
+ extraFlags: ["-m", "gpt-5.3-codex", "-c", "model_reasoning_effort=xhigh"]
293
+ },
294
+ {
295
+ id: "gpt-5.3-codex",
296
+ compoundId: "codex-5.3-medium",
297
+ name: "GPT-5.3 Codex \u2014 medium reasoning",
298
+ extraFlags: [
299
+ "-m",
300
+ "gpt-5.3-codex",
301
+ "-c",
302
+ "model_reasoning_effort=medium"
303
+ ]
304
+ }
305
+ ];
306
+ buildInvocation(req) {
307
+ const instruction = `Read the file at ${req.promptFilePath} and follow the instructions within it.`;
308
+ const args = ["exec"];
309
+ if (req.readOnlyPolicy !== "none") {
310
+ args.push("--sandbox", "read-only");
311
+ }
312
+ args.push("-c", "web_search=live", "--skip-git-repo-check");
313
+ if (req.extraFlags) {
314
+ args.push(...req.extraFlags);
315
+ }
316
+ args.push(instruction);
317
+ return { cmd: req.binary ?? "codex", args, cwd: req.cwd };
318
+ }
319
+ };
320
+
321
+ // src/adapters/custom.ts
322
+ var CustomAdapter = class extends BaseAdapter {
323
+ id;
324
+ displayName;
325
+ commands;
326
+ installUrl = "";
327
+ readOnly;
328
+ models = [];
329
+ config;
330
+ constructor(id, config) {
331
+ super();
332
+ this.id = id;
333
+ this.displayName = id;
334
+ this.commands = [config.binary];
335
+ this.readOnly = { level: config.readOnly.level };
336
+ this.config = config;
337
+ }
338
+ buildInvocation(req) {
339
+ const args = [];
340
+ if (req.extraFlags) {
341
+ args.push(...req.extraFlags);
342
+ }
343
+ if (req.readOnlyPolicy !== "none" && this.config.readOnly.flags) {
344
+ args.push(...this.config.readOnly.flags);
345
+ }
346
+ const cmd = req.binary ?? this.config.binary;
347
+ if (this.config.stdin === true) {
348
+ return { cmd, args, stdin: req.prompt, cwd: req.cwd };
349
+ }
350
+ const instruction = `Read the file at ${req.promptFilePath} and follow the instructions within it.`;
351
+ args.push(instruction);
352
+ return { cmd, args, cwd: req.cwd };
353
+ }
354
+ };
355
+
356
+ // src/adapters/gemini.ts
357
+ var GeminiAdapter = class extends BaseAdapter {
358
+ id = "gemini";
359
+ displayName = "Gemini CLI";
360
+ commands = ["gemini"];
361
+ installUrl = "https://github.com/google-gemini/gemini-cli";
362
+ readOnly = { level: "bestEffort" };
363
+ models = [
364
+ {
365
+ id: "gemini-3-pro-preview",
366
+ name: "Gemini 3 Pro Preview \u2014 latest",
367
+ recommended: true,
368
+ extraFlags: ["-m", "gemini-3-pro-preview"]
369
+ },
370
+ {
371
+ id: "gemini-2.5-pro",
372
+ name: "Gemini 2.5 Pro \u2014 stable GA",
373
+ extraFlags: ["-m", "gemini-2.5-pro"]
374
+ },
375
+ {
376
+ id: "gemini-3-flash-preview",
377
+ name: "Gemini 3 Flash Preview \u2014 fast",
378
+ extraFlags: ["-m", "gemini-3-flash-preview"]
379
+ },
380
+ {
381
+ id: "gemini-2.5-flash",
382
+ name: "Gemini 2.5 Flash \u2014 fast GA",
383
+ extraFlags: ["-m", "gemini-2.5-flash"]
384
+ }
385
+ ];
386
+ buildInvocation(req) {
387
+ const args = ["-p", ""];
388
+ if (req.extraFlags) {
389
+ args.push(...req.extraFlags);
390
+ }
391
+ if (req.readOnlyPolicy !== "none") {
392
+ args.push(
393
+ "--extensions",
394
+ "",
395
+ "--allowed-tools",
396
+ "read_file",
397
+ "list_directory",
398
+ "search_file_content",
399
+ "glob",
400
+ "google_web_search",
401
+ "codebase_investigator"
402
+ );
403
+ }
404
+ args.push("--output-format", "text");
405
+ return {
406
+ cmd: req.binary ?? "gemini",
407
+ args,
408
+ stdin: req.prompt,
409
+ cwd: req.cwd
410
+ };
411
+ }
412
+ };
413
+
414
+ // src/adapters/index.ts
415
+ var builtInAdapters = {
416
+ claude: () => new ClaudeAdapter(),
417
+ codex: () => new CodexAdapter(),
418
+ gemini: () => new GeminiAdapter(),
419
+ amp: () => new AmpAdapter()
420
+ };
421
+ function getAdapter(id, config) {
422
+ if (builtInAdapters[id]) {
423
+ return builtInAdapters[id]();
424
+ }
425
+ if (config) {
426
+ return new CustomAdapter(id, config);
427
+ }
428
+ throw new Error(
429
+ `Unknown tool: ${id}. Use "counselors tools add" to configure it.`
430
+ );
431
+ }
432
+ function getAllBuiltInAdapters() {
433
+ return Object.values(builtInAdapters).map((fn) => fn());
434
+ }
435
+ function isBuiltInTool(id) {
436
+ return id in builtInAdapters;
437
+ }
438
+ function resolveAdapter(id, toolConfig) {
439
+ const baseId = toolConfig.adapter ?? id;
440
+ return isBuiltInTool(baseId) ? getAdapter(baseId) : new CustomAdapter(id, toolConfig);
441
+ }
442
+
443
+ // src/core/config.ts
444
+ import { existsSync as existsSync2, mkdirSync, readFileSync } from "fs";
445
+ import { dirname, resolve } from "path";
446
+ import { z as z2 } from "zod";
447
+
448
+ // src/types.ts
449
+ import { z } from "zod";
450
+ var ToolConfigSchema = z.object({
451
+ binary: z.string(),
452
+ adapter: z.string().optional(),
453
+ readOnly: z.object({
454
+ level: z.enum(["enforced", "bestEffort", "none"]),
455
+ flags: z.array(z.string()).optional()
456
+ }),
457
+ extraFlags: z.array(z.string()).optional(),
458
+ timeout: z.number().optional(),
459
+ stdin: z.boolean().optional(),
460
+ custom: z.boolean().optional()
461
+ });
462
+ var ConfigSchema = z.object({
463
+ version: z.literal(1),
464
+ defaults: z.object({
465
+ timeout: z.number().default(540),
466
+ outputDir: z.string().default("./agents/counselors"),
467
+ readOnly: z.enum(["enforced", "bestEffort", "none"]).default("bestEffort"),
468
+ maxContextKb: z.number().default(50),
469
+ maxParallel: z.number().default(4)
470
+ }).default({}),
471
+ tools: z.record(z.string(), ToolConfigSchema).default({})
472
+ });
473
+
474
+ // src/core/fs-utils.ts
475
+ import { renameSync, unlinkSync, writeFileSync } from "fs";
476
+ function safeWriteFile(path, content, options) {
477
+ const tmp = `${path}.tmp.${process.pid}`;
478
+ try {
479
+ writeFileSync(tmp, content, { encoding: "utf-8", mode: options?.mode });
480
+ renameSync(tmp, path);
481
+ } catch (e) {
482
+ try {
483
+ unlinkSync(tmp);
484
+ } catch {
485
+ }
486
+ throw e;
487
+ }
488
+ }
489
+
490
+ // src/core/config.ts
491
+ var DEFAULT_CONFIG = {
492
+ version: 1,
493
+ defaults: {
494
+ timeout: 540,
495
+ outputDir: "./agents/counselors",
496
+ readOnly: "bestEffort",
497
+ maxContextKb: 50,
498
+ maxParallel: 4
499
+ },
500
+ tools: {}
501
+ };
502
+ function loadConfig(globalPath) {
503
+ const path = globalPath ?? CONFIG_FILE;
504
+ if (!existsSync2(path)) return { ...DEFAULT_CONFIG };
505
+ let raw;
506
+ try {
507
+ raw = JSON.parse(readFileSync(path, "utf-8"));
508
+ } catch (e) {
509
+ throw new Error(
510
+ `Invalid JSON in ${path}: ${e instanceof Error ? e.message : e}`
511
+ );
512
+ }
513
+ return ConfigSchema.parse(raw);
514
+ }
515
+ var ProjectConfigSchema = z2.object({
516
+ defaults: z2.object({
517
+ timeout: z2.number().optional(),
518
+ outputDir: z2.string().optional(),
519
+ readOnly: z2.enum(["enforced", "bestEffort", "none"]).optional(),
520
+ maxContextKb: z2.number().optional(),
521
+ maxParallel: z2.number().optional()
522
+ }).optional()
523
+ });
524
+ function loadProjectConfig(cwd) {
525
+ const path = resolve(cwd, ".counselors.json");
526
+ if (!existsSync2(path)) return null;
527
+ let raw;
528
+ try {
529
+ raw = JSON.parse(readFileSync(path, "utf-8"));
530
+ } catch (e) {
531
+ throw new Error(
532
+ `Invalid JSON in ${path}: ${e instanceof Error ? e.message : e}`
533
+ );
534
+ }
535
+ return ProjectConfigSchema.parse(raw);
536
+ }
537
+ function mergeConfigs(global, project, cliFlags) {
538
+ const merged = {
539
+ version: 1,
540
+ defaults: { ...global.defaults },
541
+ tools: { ...global.tools }
542
+ };
543
+ if (project) {
544
+ if (project.defaults) {
545
+ merged.defaults = { ...merged.defaults, ...project.defaults };
546
+ }
547
+ }
548
+ if (cliFlags) {
549
+ merged.defaults = { ...merged.defaults, ...cliFlags };
550
+ }
551
+ return merged;
552
+ }
553
+ function saveConfig(config, path) {
554
+ const filePath = path ?? CONFIG_FILE;
555
+ mkdirSync(dirname(filePath), { recursive: true });
556
+ safeWriteFile(filePath, `${JSON.stringify(config, null, 2)}
557
+ `, {
558
+ mode: CONFIG_FILE_MODE
559
+ });
560
+ }
561
+ function addToolToConfig(config, id, tool) {
562
+ return {
563
+ ...config,
564
+ tools: { ...config.tools, [id]: tool }
565
+ };
566
+ }
567
+ function removeToolFromConfig(config, id) {
568
+ const tools2 = { ...config.tools };
569
+ delete tools2[id];
570
+ return { ...config, tools: tools2 };
571
+ }
572
+ function renameToolInConfig(config, oldId, newId) {
573
+ const tools2 = { ...config.tools };
574
+ tools2[newId] = tools2[oldId];
575
+ delete tools2[oldId];
576
+ return { ...config, tools: tools2 };
577
+ }
578
+
579
+ // src/core/discovery.ts
580
+ import { execFileSync } from "child_process";
581
+ import {
582
+ accessSync,
583
+ constants,
584
+ existsSync as existsSync3,
585
+ readdirSync,
586
+ readFileSync as readFileSync2,
587
+ statSync
588
+ } from "fs";
589
+ import { homedir as homedir2 } from "os";
590
+ import { join as join2 } from "path";
591
+ function findBinary(command) {
592
+ const lookupCmd = process.platform === "win32" ? "where" : "which";
593
+ try {
594
+ const result = execFileSync(lookupCmd, [command], {
595
+ timeout: DISCOVERY_TIMEOUT,
596
+ stdio: ["pipe", "pipe", "pipe"],
597
+ encoding: "utf-8"
598
+ }).trim().split("\n")[0].trim();
599
+ if (result) return result;
600
+ } catch {
601
+ }
602
+ const searchPaths = [
603
+ ...getExtendedSearchPaths(),
604
+ ...getNvmPaths(),
605
+ ...getFnmPaths()
606
+ ];
607
+ for (const dir of searchPaths) {
608
+ const fullPath = join2(dir, command);
609
+ try {
610
+ accessSync(fullPath, constants.X_OK);
611
+ return fullPath;
612
+ } catch {
613
+ }
614
+ }
615
+ return null;
616
+ }
617
+ function getNvmPaths() {
618
+ const home = homedir2();
619
+ const nvmDir = join2(home, ".nvm");
620
+ const aliasFile = join2(nvmDir, "alias", "default");
621
+ if (!existsSync3(aliasFile)) return [];
622
+ try {
623
+ let alias = readFileSync2(aliasFile, "utf-8").trim();
624
+ if (alias.startsWith("lts/")) {
625
+ const ltsName = alias.slice(4);
626
+ const ltsFile = join2(nvmDir, "alias", "lts", ltsName);
627
+ if (existsSync3(ltsFile)) {
628
+ alias = readFileSync2(ltsFile, "utf-8").trim();
629
+ }
630
+ }
631
+ const versionsDir = join2(nvmDir, "versions", "node");
632
+ if (!existsSync3(versionsDir)) return [];
633
+ const versions = readdirSync(versionsDir);
634
+ const match = versions.find((v) => v.startsWith(`v${alias}`));
635
+ if (match) {
636
+ return [join2(versionsDir, match, "bin")];
637
+ }
638
+ } catch {
639
+ }
640
+ return [];
641
+ }
642
+ function getFnmPaths() {
643
+ const home = homedir2();
644
+ const multishellDir = join2(home, ".local", "state", "fnm_multishells");
645
+ const paths = [];
646
+ const fnmDir = join2(home, ".local", "share", "fnm");
647
+ if (existsSync3(fnmDir)) {
648
+ const aliasDir = join2(fnmDir, "aliases");
649
+ if (existsSync3(aliasDir)) {
650
+ try {
651
+ for (const alias of readdirSync(aliasDir)) {
652
+ const binDir = join2(aliasDir, alias, "bin");
653
+ if (existsSync3(binDir)) paths.push(binDir);
654
+ }
655
+ } catch {
656
+ }
657
+ }
658
+ }
659
+ if (!existsSync3(multishellDir)) return paths;
660
+ try {
661
+ const entries = readdirSync(multishellDir).map((name) => {
662
+ const full = join2(multishellDir, name);
663
+ try {
664
+ return { name: full, mtime: statSync(full).mtimeMs };
665
+ } catch {
666
+ return null;
667
+ }
668
+ }).filter((e) => e !== null).sort((a, b) => b.mtime - a.mtime).slice(0, 5);
669
+ for (const entry of entries) {
670
+ const binDir = join2(entry.name, "bin");
671
+ if (existsSync3(binDir)) {
672
+ paths.push(binDir);
673
+ }
674
+ }
675
+ } catch {
676
+ }
677
+ return paths;
678
+ }
679
+ function getBinaryVersion(binaryPath) {
680
+ try {
681
+ const output = execFileSync(binaryPath, ["--version"], {
682
+ timeout: VERSION_TIMEOUT,
683
+ stdio: ["pipe", "pipe", "pipe"],
684
+ encoding: "utf-8"
685
+ }).trim();
686
+ const firstLine = output.split("\n")[0].trim();
687
+ return firstLine || null;
688
+ } catch {
689
+ return null;
690
+ }
691
+ }
692
+ function discoverTool(commands) {
693
+ for (const cmd of commands) {
694
+ const path = findBinary(cmd);
695
+ if (path) {
696
+ const version = getBinaryVersion(path);
697
+ return { toolId: cmd, found: true, path, version, command: cmd };
698
+ }
699
+ }
700
+ return {
701
+ toolId: commands[0],
702
+ found: false,
703
+ path: null,
704
+ version: null,
705
+ command: commands[0]
706
+ };
707
+ }
708
+
709
+ // src/ui/output.ts
710
+ import ora from "ora";
711
+ function createSpinner(text) {
712
+ return ora({ text, stream: process.stderr });
713
+ }
714
+ function formatDiscoveryResults(results) {
715
+ const lines = ["", "Discovered tools:", ""];
716
+ for (const r of results) {
717
+ const name = r.displayName || r.toolId;
718
+ if (r.found) {
719
+ lines.push(` \u2713 ${name}`);
720
+ lines.push(` Path: ${r.path}`);
721
+ if (r.version) lines.push(` Version: ${r.version}`);
722
+ } else {
723
+ lines.push(` \u2717 ${name} \u2014 not found`);
724
+ }
725
+ }
726
+ lines.push("");
727
+ return lines.join("\n");
728
+ }
729
+ function formatDoctorResults(checks) {
730
+ const lines = ["", "Doctor results:", ""];
731
+ for (const c of checks) {
732
+ const icon = c.status === "pass" ? "\u2713" : c.status === "warn" ? "\u26A0" : "\u2717";
733
+ lines.push(` ${icon} ${c.name}: ${c.message}`);
734
+ }
735
+ const failures = checks.filter((c) => c.status === "fail").length;
736
+ const warnings = checks.filter((c) => c.status === "warn").length;
737
+ lines.push("");
738
+ if (failures > 0) {
739
+ lines.push(`${failures} check(s) failed.`);
740
+ } else if (warnings > 0) {
741
+ lines.push(`All checks passed with ${warnings} warning(s).`);
742
+ } else {
743
+ lines.push("All checks passed.");
744
+ }
745
+ lines.push("");
746
+ return lines.join("\n");
747
+ }
748
+ function formatToolList(tools2, verbose) {
749
+ if (tools2.length === 0) {
750
+ return '\nNo tools configured. Run "counselors init" to get started.\n';
751
+ }
752
+ const lines = ["", "Configured tools:", ""];
753
+ for (const t of tools2) {
754
+ if (!verbose) {
755
+ lines.push(` \x1B[1m${t.id}\x1B[0m (${t.binary})`);
756
+ continue;
757
+ }
758
+ const bold = "\x1B[1m";
759
+ const reset = "\x1B[0m";
760
+ lines.push(` ${bold}${t.id}${reset}`);
761
+ const raw = t.args ?? [];
762
+ const quote = (a) => a.includes(" ") ? `"${a}"` : a;
763
+ const allParts = [t.binary, ...raw].map(quote);
764
+ let line = " ";
765
+ for (const part of allParts) {
766
+ if (part.startsWith("-") && line.trim().length > 0) {
767
+ lines.push(line);
768
+ line = ` ${part}`;
769
+ } else {
770
+ line += (line.trim().length > 0 ? " " : "") + part;
771
+ }
772
+ }
773
+ if (line.trim().length > 0) lines.push(line);
774
+ }
775
+ if (!verbose) {
776
+ const dim = "\x1B[2m";
777
+ const reset = "\x1B[0m";
778
+ lines.push("");
779
+ lines.push(`${dim}(Use -v to show flags)${reset}`);
780
+ }
781
+ lines.push("");
782
+ return lines.join("\n");
783
+ }
784
+ function formatTestResults(results) {
785
+ const lines = ["", "Test results:", ""];
786
+ for (const r of results) {
787
+ const icon = r.passed ? "\u2713" : "\u2717";
788
+ lines.push(` ${icon} ${r.toolId} (${r.durationMs}ms)`);
789
+ if (!r.passed && r.error) {
790
+ lines.push(` Error: ${r.error}`);
791
+ }
792
+ }
793
+ lines.push("");
794
+ return lines.join("\n");
795
+ }
796
+ function formatRunSummary(manifest) {
797
+ const lines = ["", `Run complete: ${manifest.slug}`, ""];
798
+ for (const r of manifest.tools) {
799
+ const icon = r.status === "success" ? "\u2713" : r.status === "timeout" ? "\u23F1" : "\u2717";
800
+ const duration = (r.durationMs / 1e3).toFixed(1);
801
+ lines.push(` ${icon} ${r.toolId} \u2014 ${r.wordCount} words, ${duration}s`);
802
+ if (r.cost) {
803
+ lines.push(` Cost: $${r.cost.cost_usd.toFixed(2)} (${r.cost.source})`);
804
+ }
805
+ if (r.status === "error" && r.error) {
806
+ lines.push(` Error: ${r.error}`);
807
+ }
808
+ }
809
+ lines.push("");
810
+ lines.push(
811
+ `Reports saved to: ${manifest.tools[0]?.outputFile ? manifest.tools[0].outputFile.replace(/\/[^/]+$/, "/") : "output dir"}`
812
+ );
813
+ lines.push("");
814
+ return lines.join("\n");
815
+ }
816
+ function formatDryRun(invocations) {
817
+ const lines = ["", "Dry run \u2014 would dispatch:", ""];
818
+ for (const inv of invocations) {
819
+ lines.push(` ${inv.toolId}`);
820
+ lines.push(` $ ${inv.cmd} ${inv.args.join(" ")}`);
821
+ }
822
+ lines.push("");
823
+ return lines.join("\n");
824
+ }
825
+
826
+ // src/commands/doctor.ts
827
+ function registerDoctorCommand(program2) {
828
+ program2.command("doctor").description("Check tool configuration and health").action(async () => {
829
+ const checks = [];
830
+ if (existsSync4(CONFIG_FILE)) {
831
+ checks.push({
832
+ name: "Config file",
833
+ status: "pass",
834
+ message: CONFIG_FILE
835
+ });
836
+ } else {
837
+ checks.push({
838
+ name: "Config file",
839
+ status: "warn",
840
+ message: 'Not found. Run "counselors init" to create one.'
841
+ });
842
+ }
843
+ let config;
844
+ try {
845
+ config = loadConfig();
846
+ } catch (e) {
847
+ checks.push({
848
+ name: "Config parse",
849
+ status: "fail",
850
+ message: `Invalid config: ${e}`
851
+ });
852
+ info(formatDoctorResults(checks));
853
+ process.exitCode = 1;
854
+ return;
855
+ }
856
+ const toolIds = Object.keys(config.tools);
857
+ if (toolIds.length === 0) {
858
+ checks.push({
859
+ name: "Tools configured",
860
+ status: "warn",
861
+ message: 'No tools configured. Run "counselors init".'
862
+ });
863
+ }
864
+ for (const id of toolIds) {
865
+ const toolConfig = config.tools[id];
866
+ const binaryPath = findBinary(toolConfig.binary);
867
+ if (binaryPath) {
868
+ checks.push({
869
+ name: `${id}: binary`,
870
+ status: "pass",
871
+ message: binaryPath
872
+ });
873
+ } else {
874
+ checks.push({
875
+ name: `${id}: binary`,
876
+ status: "fail",
877
+ message: `"${toolConfig.binary}" not found in PATH`
878
+ });
879
+ continue;
880
+ }
881
+ const version = getBinaryVersion(binaryPath);
882
+ if (version) {
883
+ checks.push({
884
+ name: `${id}: version`,
885
+ status: "pass",
886
+ message: version
887
+ });
888
+ } else {
889
+ checks.push({
890
+ name: `${id}: version`,
891
+ status: "warn",
892
+ message: "Could not determine version"
893
+ });
894
+ }
895
+ const adapter = resolveAdapter(id, toolConfig);
896
+ checks.push({
897
+ name: `${id}: read-only`,
898
+ status: adapter.readOnly.level === "enforced" ? "pass" : adapter.readOnly.level === "bestEffort" ? "warn" : "fail",
899
+ message: adapter.readOnly.level
900
+ });
901
+ }
902
+ const hasAmp = Object.entries(config.tools).some(
903
+ ([id, t]) => (t.adapter ?? id) === "amp"
904
+ );
905
+ if (hasAmp) {
906
+ if (existsSync4(AMP_SETTINGS_FILE)) {
907
+ checks.push({
908
+ name: "Amp settings file",
909
+ status: "pass",
910
+ message: AMP_SETTINGS_FILE
911
+ });
912
+ } else {
913
+ checks.push({
914
+ name: "Amp settings file",
915
+ status: "warn",
916
+ message: "Not found. Amp read-only mode may not work."
917
+ });
918
+ }
919
+ if (existsSync4(AMP_DEEP_SETTINGS_FILE)) {
920
+ checks.push({
921
+ name: "Amp deep settings file",
922
+ status: "pass",
923
+ message: AMP_DEEP_SETTINGS_FILE
924
+ });
925
+ } else {
926
+ checks.push({
927
+ name: "Amp deep settings file",
928
+ status: "warn",
929
+ message: "Not found. Amp deep mode may not work."
930
+ });
931
+ }
932
+ }
933
+ info(formatDoctorResults(checks));
934
+ if (checks.some((c) => c.status === "fail")) {
935
+ process.exitCode = 1;
936
+ }
937
+ });
938
+ }
939
+
940
+ // src/core/amp-utils.ts
941
+ import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
942
+ import { dirname as dirname2, resolve as resolve2 } from "path";
943
+ import { fileURLToPath } from "url";
944
+ function copyAmpSettings() {
945
+ mkdirSync2(CONFIG_DIR, { recursive: true });
946
+ const assetsDir = resolve2(
947
+ dirname2(fileURLToPath(import.meta.url)),
948
+ "..",
949
+ "assets"
950
+ );
951
+ const bundledSettings = resolve2(assetsDir, "amp-readonly-settings.json");
952
+ if (existsSync5(bundledSettings)) {
953
+ copyFileSync(bundledSettings, AMP_SETTINGS_FILE);
954
+ }
955
+ const bundledDeepSettings = resolve2(assetsDir, "amp-deep-settings.json");
956
+ if (existsSync5(bundledDeepSettings)) {
957
+ copyFileSync(bundledDeepSettings, AMP_DEEP_SETTINGS_FILE);
958
+ }
959
+ }
960
+
961
+ // src/core/executor.ts
962
+ import { execFile, spawn } from "child_process";
963
+ import { promisify } from "util";
964
+ import stripAnsi from "strip-ansi";
965
+ var execFileAsync = promisify(execFile);
966
+ var MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
967
+ var activeChildren = /* @__PURE__ */ new Set();
968
+ process.on("SIGINT", () => {
969
+ for (const child of activeChildren) {
970
+ child.kill("SIGTERM");
971
+ }
972
+ setTimeout(() => process.exit(1), 2e3);
973
+ });
974
+ var ENV_ALLOWLIST = [
975
+ "PATH",
976
+ "HOME",
977
+ "USER",
978
+ "TERM",
979
+ "LANG",
980
+ "SHELL",
981
+ "TMPDIR",
982
+ "XDG_CONFIG_HOME",
983
+ "XDG_DATA_HOME",
984
+ // Node version managers
985
+ "NVM_BIN",
986
+ "NVM_DIR",
987
+ "FNM_MULTISHELL_PATH",
988
+ // API keys for adapters
989
+ "ANTHROPIC_API_KEY",
990
+ "OPENAI_API_KEY",
991
+ "OPENAI_ORG_ID",
992
+ "GEMINI_API_KEY",
993
+ "GOOGLE_API_KEY",
994
+ "GOOGLE_APPLICATION_CREDENTIALS",
995
+ "AMP_API_KEY",
996
+ // Proxy
997
+ "HTTP_PROXY",
998
+ "HTTPS_PROXY",
999
+ "NO_PROXY",
1000
+ "http_proxy",
1001
+ "https_proxy",
1002
+ "no_proxy",
1003
+ // Node runtime
1004
+ "NODE_OPTIONS"
1005
+ ];
1006
+ function buildSafeEnv(extra) {
1007
+ const env = {};
1008
+ for (const key of ENV_ALLOWLIST) {
1009
+ if (process.env[key]) env[key] = process.env[key];
1010
+ }
1011
+ if (extra) Object.assign(env, extra);
1012
+ env.CI = "true";
1013
+ env.NO_COLOR = "1";
1014
+ return env;
1015
+ }
1016
+ function execute(invocation, timeoutMs) {
1017
+ return new Promise((resolve6) => {
1018
+ const start = Date.now();
1019
+ let stdout = "";
1020
+ let stderr = "";
1021
+ let timedOut = false;
1022
+ let killed = false;
1023
+ let killTimer;
1024
+ let truncated = false;
1025
+ debug(`Executing: ${invocation.cmd} ${invocation.args.join(" ")}`);
1026
+ const child = spawn(invocation.cmd, invocation.args, {
1027
+ cwd: invocation.cwd,
1028
+ env: buildSafeEnv(invocation.env),
1029
+ stdio: ["pipe", "pipe", "pipe"]
1030
+ });
1031
+ activeChildren.add(child);
1032
+ child.stdout.on("data", (data) => {
1033
+ if (!truncated && stdout.length < MAX_OUTPUT_BYTES) {
1034
+ stdout += data.toString();
1035
+ if (stdout.length >= MAX_OUTPUT_BYTES) {
1036
+ truncated = true;
1037
+ stdout = `${stdout.slice(0, MAX_OUTPUT_BYTES)}
1038
+ [output truncated at 10MB]`;
1039
+ }
1040
+ }
1041
+ });
1042
+ child.stderr.on("data", (data) => {
1043
+ if (stderr.length < MAX_OUTPUT_BYTES) {
1044
+ stderr += data.toString();
1045
+ }
1046
+ });
1047
+ if (invocation.stdin) {
1048
+ child.stdin.write(invocation.stdin);
1049
+ child.stdin.end();
1050
+ } else {
1051
+ child.stdin.end();
1052
+ }
1053
+ const timer = setTimeout(() => {
1054
+ timedOut = true;
1055
+ child.kill("SIGTERM");
1056
+ killTimer = setTimeout(() => {
1057
+ if (!killed) {
1058
+ child.kill("SIGKILL");
1059
+ }
1060
+ }, KILL_GRACE_PERIOD);
1061
+ }, timeoutMs);
1062
+ child.on("close", (code) => {
1063
+ killed = true;
1064
+ clearTimeout(timer);
1065
+ if (killTimer) clearTimeout(killTimer);
1066
+ activeChildren.delete(child);
1067
+ resolve6({
1068
+ exitCode: code ?? 1,
1069
+ stdout: stripAnsi(stdout),
1070
+ stderr: stripAnsi(stderr),
1071
+ timedOut,
1072
+ durationMs: Date.now() - start
1073
+ });
1074
+ });
1075
+ child.on("error", (err) => {
1076
+ killed = true;
1077
+ clearTimeout(timer);
1078
+ if (killTimer) clearTimeout(killTimer);
1079
+ activeChildren.delete(child);
1080
+ resolve6({
1081
+ exitCode: 1,
1082
+ stdout: "",
1083
+ stderr: err.message,
1084
+ timedOut: false,
1085
+ durationMs: Date.now() - start
1086
+ });
1087
+ });
1088
+ });
1089
+ }
1090
+ async function captureAmpUsage() {
1091
+ try {
1092
+ const { stdout } = await execFileAsync("amp", ["usage"], {
1093
+ timeout: 1e4,
1094
+ encoding: "utf-8"
1095
+ });
1096
+ return stdout;
1097
+ } catch {
1098
+ return null;
1099
+ }
1100
+ }
1101
+ function computeAmpCostFromSnapshots(before, after) {
1102
+ try {
1103
+ const beforeParsed = parseAmpUsage(before);
1104
+ const afterParsed = parseAmpUsage(after);
1105
+ return computeAmpCost(beforeParsed, afterParsed);
1106
+ } catch {
1107
+ return null;
1108
+ }
1109
+ }
1110
+ async function executeTest(adapter, toolConfig, toolName) {
1111
+ const prompt = "Reply with exactly: OK";
1112
+ const start = Date.now();
1113
+ const invocation = adapter.buildInvocation({
1114
+ prompt,
1115
+ promptFilePath: "",
1116
+ toolId: adapter.id,
1117
+ outputDir: "",
1118
+ readOnlyPolicy: "none",
1119
+ timeout: TEST_TIMEOUT / 1e3,
1120
+ cwd: process.cwd(),
1121
+ extraFlags: toolConfig.extraFlags
1122
+ });
1123
+ if (invocation.stdin != null) {
1124
+ invocation.stdin = prompt;
1125
+ invocation.args = invocation.args.filter((a, i, arr) => {
1126
+ if (a === "--settings-file") return false;
1127
+ if (i > 0 && arr[i - 1] === "--settings-file") return false;
1128
+ return true;
1129
+ });
1130
+ } else {
1131
+ const lastArgIdx = invocation.args.length - 1;
1132
+ invocation.args[lastArgIdx] = prompt;
1133
+ }
1134
+ const result = await execute(invocation, TEST_TIMEOUT);
1135
+ const passed = result.stdout.includes("OK");
1136
+ return {
1137
+ toolId: toolName ?? adapter.id,
1138
+ passed,
1139
+ output: result.stdout.slice(0, 500),
1140
+ error: !passed ? result.stderr.slice(0, 500) || 'Output did not contain "OK"' : void 0,
1141
+ durationMs: Date.now() - start
1142
+ };
1143
+ }
1144
+
1145
+ // src/ui/prompts.ts
1146
+ import { checkbox, confirm, input, select } from "@inquirer/prompts";
1147
+ async function selectModelDetails(toolId, models) {
1148
+ const choices = models.map((m, i) => ({
1149
+ name: m.recommended ? `${m.name} (Recommended)` : m.name,
1150
+ value: String(i)
1151
+ }));
1152
+ const idx = await select({
1153
+ message: `Select model for ${toolId}:`,
1154
+ choices
1155
+ });
1156
+ const model = models[Number(idx)];
1157
+ return {
1158
+ id: model.id,
1159
+ compoundId: model.compoundId,
1160
+ extraFlags: model.extraFlags
1161
+ };
1162
+ }
1163
+ async function selectModels(toolId, models) {
1164
+ const choices = models.map((m) => ({
1165
+ name: m.recommended ? `${m.name} (Recommended)` : m.name,
1166
+ value: { id: m.id, compoundId: m.compoundId, extraFlags: m.extraFlags },
1167
+ checked: m.recommended
1168
+ }));
1169
+ return checkbox({
1170
+ message: `Select models for ${toolId}:`,
1171
+ choices
1172
+ });
1173
+ }
1174
+ async function selectTools(tools2) {
1175
+ const choices = tools2.map((t) => ({
1176
+ name: t.found ? `${t.name} \u2014 found` : `${t.name} \u2014 not found`,
1177
+ value: t.id,
1178
+ checked: t.found,
1179
+ disabled: !t.found ? "(not installed)" : void 0
1180
+ }));
1181
+ return checkbox({
1182
+ message: "Which tools should be configured?",
1183
+ choices
1184
+ });
1185
+ }
1186
+ async function confirmOverwrite(toolId) {
1187
+ return confirm({
1188
+ message: `Tool "${toolId}" already exists. Overwrite?`,
1189
+ default: false
1190
+ });
1191
+ }
1192
+ async function selectRunTools(tools2) {
1193
+ const choices = tools2.map((id) => ({
1194
+ name: id,
1195
+ value: id,
1196
+ checked: true
1197
+ }));
1198
+ return checkbox({
1199
+ message: "Select tools to dispatch:",
1200
+ choices
1201
+ });
1202
+ }
1203
+ async function confirmAction(message) {
1204
+ return confirm({ message, default: true });
1205
+ }
1206
+ async function promptInput(message, defaultValue) {
1207
+ return input({ message, default: defaultValue });
1208
+ }
1209
+ async function promptSelect(message, choices) {
1210
+ return select({ message, choices });
1211
+ }
1212
+
1213
+ // src/commands/init.ts
1214
+ function buildToolConfig(id, adapter, binaryPath) {
1215
+ return {
1216
+ binary: binaryPath,
1217
+ readOnly: { level: adapter.readOnly.level },
1218
+ ...id === "gemini" || id === "codex" ? { timeout: 900 } : {}
1219
+ };
1220
+ }
1221
+ function compoundId(adapterId, modelId) {
1222
+ if (modelId.startsWith(`${adapterId}-`)) return modelId;
1223
+ return `${adapterId}-${modelId}`;
1224
+ }
1225
+ function registerInitCommand(program2) {
1226
+ program2.command("init").description("Interactive setup wizard").option(
1227
+ "--auto",
1228
+ "Non-interactive mode: discover tools, use recommended models, output JSON"
1229
+ ).action(async (opts) => {
1230
+ if (opts.auto) {
1231
+ const adapters2 = getAllBuiltInAdapters();
1232
+ const discoveries2 = adapters2.map((adapter) => {
1233
+ const result = discoverTool(adapter.commands);
1234
+ return { adapter, discovery: result };
1235
+ });
1236
+ const foundTools2 = discoveries2.filter((d) => d.discovery.found);
1237
+ if (foundTools2.length === 0) {
1238
+ info(
1239
+ JSON.stringify(
1240
+ {
1241
+ configured: [],
1242
+ notFound: adapters2.map((a) => a.id),
1243
+ configPath: CONFIG_DIR
1244
+ },
1245
+ null,
1246
+ 2
1247
+ )
1248
+ );
1249
+ return;
1250
+ }
1251
+ let config2 = loadConfig();
1252
+ const configured = [];
1253
+ const notFound = [];
1254
+ for (const { adapter, discovery } of discoveries2) {
1255
+ if (!discovery.found) {
1256
+ notFound.push(adapter.id);
1257
+ continue;
1258
+ }
1259
+ for (const model of adapter.models) {
1260
+ const cid = model.compoundId ?? compoundId(adapter.id, model.id);
1261
+ const toolConfig = {
1262
+ ...buildToolConfig(adapter.id, adapter, discovery.path),
1263
+ adapter: adapter.id,
1264
+ ...model.extraFlags ? { extraFlags: model.extraFlags } : {}
1265
+ };
1266
+ config2 = addToolToConfig(config2, cid, toolConfig);
1267
+ configured.push({
1268
+ id: cid,
1269
+ adapter: adapter.id,
1270
+ binary: discovery.path,
1271
+ version: discovery.version
1272
+ });
1273
+ }
1274
+ }
1275
+ if (configured.some((t) => t.adapter === "amp")) {
1276
+ copyAmpSettings();
1277
+ }
1278
+ saveConfig(config2);
1279
+ info(
1280
+ JSON.stringify(
1281
+ { configured, notFound, configPath: CONFIG_DIR },
1282
+ null,
1283
+ 2
1284
+ )
1285
+ );
1286
+ return;
1287
+ }
1288
+ info("\nCounselors \u2014 setup wizard\n");
1289
+ const existingConfig = loadConfig();
1290
+ const existingTools = Object.keys(existingConfig.tools);
1291
+ if (existingTools.length > 0) {
1292
+ warn(
1293
+ `Existing config has ${existingTools.length} tool(s). Re-running init will overwrite any tools with the same name.`
1294
+ );
1295
+ }
1296
+ const spinner = createSpinner("Discovering installed tools...").start();
1297
+ const adapters = getAllBuiltInAdapters();
1298
+ const discoveries = adapters.map((adapter) => {
1299
+ const result = discoverTool(adapter.commands);
1300
+ return { adapter, discovery: result };
1301
+ });
1302
+ spinner.stop();
1303
+ info(
1304
+ formatDiscoveryResults(
1305
+ discoveries.map((d) => ({
1306
+ ...d.discovery,
1307
+ toolId: d.adapter.id,
1308
+ displayName: d.adapter.displayName
1309
+ }))
1310
+ )
1311
+ );
1312
+ const foundTools = discoveries.filter((d) => d.discovery.found);
1313
+ if (foundTools.length === 0) {
1314
+ warn(
1315
+ "No AI CLI tools found. Install at least one before running init."
1316
+ );
1317
+ return;
1318
+ }
1319
+ const selectedIds = await selectTools(
1320
+ discoveries.map((d) => ({
1321
+ id: d.adapter.id,
1322
+ name: d.adapter.displayName,
1323
+ found: d.discovery.found
1324
+ }))
1325
+ );
1326
+ if (selectedIds.length === 0) {
1327
+ info("No tools selected. Exiting.");
1328
+ return;
1329
+ }
1330
+ let config = loadConfig();
1331
+ const configuredIds = [];
1332
+ for (const id of selectedIds) {
1333
+ const d = discoveries.find((x) => x.adapter.id === id);
1334
+ const models = await selectModels(id, d.adapter.models);
1335
+ for (const model of models) {
1336
+ const cid = model.compoundId ?? compoundId(id, model.id);
1337
+ const toolConfig = {
1338
+ ...buildToolConfig(id, d.adapter, d.discovery.path),
1339
+ adapter: id,
1340
+ ...model.extraFlags ? { extraFlags: model.extraFlags } : {}
1341
+ };
1342
+ config = addToolToConfig(config, cid, toolConfig);
1343
+ configuredIds.push(cid);
1344
+ }
1345
+ }
1346
+ if (selectedIds.includes("amp")) {
1347
+ copyAmpSettings();
1348
+ success(`Copied amp settings to ${AMP_SETTINGS_FILE}`);
1349
+ }
1350
+ saveConfig(config);
1351
+ success(`Config saved to ${CONFIG_DIR}`);
1352
+ const runTests = await confirmAction("Run tool tests now?");
1353
+ if (runTests) {
1354
+ const testResults = [];
1355
+ for (const id of configuredIds) {
1356
+ const toolConfig = config.tools[id];
1357
+ const adapter = resolveAdapter(id, toolConfig);
1358
+ const spinner2 = createSpinner(`Testing ${id}...`).start();
1359
+ const result = await executeTest(adapter, toolConfig, id);
1360
+ spinner2.stop();
1361
+ testResults.push(result);
1362
+ }
1363
+ info(formatTestResults(testResults));
1364
+ }
1365
+ });
1366
+ }
1367
+
1368
+ // src/commands/run.ts
1369
+ import { copyFileSync as copyFileSync2, readFileSync as readFileSync5 } from "fs";
1370
+ import { basename as basename2, join as join6, resolve as resolve4 } from "path";
1371
+
1372
+ // src/core/context.ts
1373
+ import { execFileSync as execFileSync2 } from "child_process";
1374
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
1375
+ import { resolve as resolve3 } from "path";
1376
+ function gatherContext(cwd, paths, maxKb = DEFAULT_MAX_CONTEXT_KB) {
1377
+ const parts = [];
1378
+ let totalBytes = 0;
1379
+ const maxBytes = maxKb * 1024;
1380
+ if (paths.length > 0) {
1381
+ parts.push("### Files Referenced", "");
1382
+ for (const p of paths) {
1383
+ if (totalBytes >= maxBytes) {
1384
+ debug(`Context limit reached (${maxKb}KB), skipping remaining files`);
1385
+ break;
1386
+ }
1387
+ const fullPath = resolve3(cwd, p);
1388
+ try {
1389
+ const stat = statSync2(fullPath);
1390
+ if (!stat.isFile()) continue;
1391
+ if (stat.size > maxBytes - totalBytes) {
1392
+ debug(`Skipping ${p} \u2014 too large (${stat.size} bytes)`);
1393
+ continue;
1394
+ }
1395
+ const content = readFileSync3(fullPath, "utf-8");
1396
+ parts.push(`#### ${p}`, "", "```", content, "```", "");
1397
+ totalBytes += Buffer.byteLength(content);
1398
+ } catch {
1399
+ debug(`Could not read ${p}`);
1400
+ }
1401
+ }
1402
+ }
1403
+ if (totalBytes < maxBytes) {
1404
+ const diff = getGitDiff(cwd);
1405
+ if (diff) {
1406
+ const diffBytes = Buffer.byteLength(diff);
1407
+ if (totalBytes + diffBytes <= maxBytes) {
1408
+ parts.push(
1409
+ "### Recent Changes (Git Diff)",
1410
+ "",
1411
+ "```diff",
1412
+ diff,
1413
+ "```",
1414
+ ""
1415
+ );
1416
+ totalBytes += diffBytes;
1417
+ } else {
1418
+ const remaining = maxBytes - totalBytes;
1419
+ const truncated = Buffer.from(diff).subarray(0, remaining).toString("utf-8");
1420
+ parts.push(
1421
+ "### Recent Changes (Git Diff) [truncated]",
1422
+ "",
1423
+ "```diff",
1424
+ truncated,
1425
+ "```",
1426
+ ""
1427
+ );
1428
+ totalBytes = maxBytes;
1429
+ }
1430
+ }
1431
+ }
1432
+ return parts.join("\n");
1433
+ }
1434
+ function getGitDiff(cwd) {
1435
+ try {
1436
+ const staged = execFileSync2("git", ["diff", "--staged"], {
1437
+ cwd,
1438
+ encoding: "utf-8",
1439
+ timeout: 1e4,
1440
+ maxBuffer: 10 * 1024 * 1024,
1441
+ stdio: ["pipe", "pipe", "pipe"]
1442
+ }).trim();
1443
+ const unstaged = execFileSync2("git", ["diff"], {
1444
+ cwd,
1445
+ encoding: "utf-8",
1446
+ timeout: 1e4,
1447
+ maxBuffer: 10 * 1024 * 1024,
1448
+ stdio: ["pipe", "pipe", "pipe"]
1449
+ }).trim();
1450
+ const parts = [];
1451
+ if (staged) parts.push(staged);
1452
+ if (unstaged) parts.push(unstaged);
1453
+ return parts.length > 0 ? parts.join("\n") : null;
1454
+ } catch {
1455
+ return null;
1456
+ }
1457
+ }
1458
+
1459
+ // src/core/dispatcher.ts
1460
+ import { join as join3 } from "path";
1461
+ import pLimit from "p-limit";
1462
+ async function dispatch(options) {
1463
+ const {
1464
+ config,
1465
+ toolIds,
1466
+ promptFilePath,
1467
+ promptContent,
1468
+ outputDir,
1469
+ readOnlyPolicy,
1470
+ cwd,
1471
+ onProgress
1472
+ } = options;
1473
+ const limit = pLimit(config.defaults.maxParallel);
1474
+ const eligibleTools = toolIds.filter((id) => {
1475
+ const toolConfig = config.tools[id];
1476
+ if (!toolConfig) {
1477
+ warn(`Tool "${id}" not configured, skipping.`);
1478
+ return false;
1479
+ }
1480
+ if (readOnlyPolicy === "enforced") {
1481
+ const adapter = resolveAdapter(id, toolConfig);
1482
+ if (adapter.readOnly.level !== "enforced") {
1483
+ warn(
1484
+ `Skipping "${id}" \u2014 read-only level is "${adapter.readOnly.level}", policy requires "enforced".`
1485
+ );
1486
+ return false;
1487
+ }
1488
+ }
1489
+ return true;
1490
+ });
1491
+ if (eligibleTools.length === 0) {
1492
+ throw new Error("No eligible tools after read-only policy filtering.");
1493
+ }
1494
+ const tasks = eligibleTools.map(
1495
+ (id) => limit(async () => {
1496
+ const toolConfig = config.tools[id];
1497
+ const adapter = resolveAdapter(id, toolConfig);
1498
+ const toolTimeout = toolConfig.timeout ?? config.defaults.timeout;
1499
+ const toolTimeoutMs = toolTimeout * 1e3;
1500
+ const req = {
1501
+ prompt: promptContent,
1502
+ promptFilePath,
1503
+ toolId: id,
1504
+ outputDir,
1505
+ readOnlyPolicy,
1506
+ timeout: toolTimeout,
1507
+ cwd,
1508
+ binary: toolConfig.binary,
1509
+ extraFlags: toolConfig.extraFlags
1510
+ };
1511
+ const invocation = adapter.buildInvocation(req);
1512
+ const isAmp = (toolConfig.adapter ?? id) === "amp";
1513
+ const usageBefore = isAmp ? await captureAmpUsage() : null;
1514
+ debug(`Dispatching ${id}`);
1515
+ onProgress?.({ toolId: id, event: "started" });
1516
+ const result = await execute(invocation, toolTimeoutMs);
1517
+ const usageAfter = isAmp ? await captureAmpUsage() : null;
1518
+ const cost = isAmp && usageBefore && usageAfter ? computeAmpCostFromSnapshots(usageBefore, usageAfter) : void 0;
1519
+ const safeId = sanitizeId(id);
1520
+ const outputFile = join3(outputDir, `${safeId}.md`);
1521
+ const stderrFile = join3(outputDir, `${safeId}.stderr`);
1522
+ safeWriteFile(outputFile, result.stdout);
1523
+ safeWriteFile(stderrFile, result.stderr);
1524
+ if (cost) {
1525
+ const statsFile = join3(outputDir, `${safeId}.stats.json`);
1526
+ safeWriteFile(statsFile, JSON.stringify({ cost }, null, 2));
1527
+ }
1528
+ const parsed = adapter.parseResult?.(result) ?? {};
1529
+ const report = {
1530
+ toolId: id,
1531
+ status: result.timedOut ? "timeout" : result.exitCode === 0 ? "success" : "error",
1532
+ exitCode: result.exitCode,
1533
+ durationMs: result.durationMs,
1534
+ wordCount: result.stdout.split(/\s+/).filter(Boolean).length,
1535
+ outputFile,
1536
+ stderrFile,
1537
+ cost: cost ?? void 0,
1538
+ error: result.exitCode !== 0 ? result.stderr.slice(0, 500) : void 0,
1539
+ ...parsed
1540
+ };
1541
+ onProgress?.({ toolId: id, event: "completed", report });
1542
+ return report;
1543
+ })
1544
+ );
1545
+ const results = await Promise.allSettled(tasks);
1546
+ return results.map((r, i) => {
1547
+ if (r.status === "fulfilled") return r.value;
1548
+ return {
1549
+ toolId: eligibleTools[i],
1550
+ status: "error",
1551
+ exitCode: 1,
1552
+ durationMs: 0,
1553
+ wordCount: 0,
1554
+ outputFile: "",
1555
+ stderrFile: "",
1556
+ error: r.reason?.message ?? "Unknown error"
1557
+ };
1558
+ });
1559
+ }
1560
+
1561
+ // src/core/prompt-builder.ts
1562
+ import { mkdirSync as mkdirSync3 } from "fs";
1563
+ import { basename, dirname as dirname3, join as join4 } from "path";
1564
+ function generateSlug(text) {
1565
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, MAX_SLUG_LENGTH);
1566
+ }
1567
+ function generateSlugFromFile(filePath) {
1568
+ const dir = dirname3(filePath);
1569
+ const dirName = basename(dir);
1570
+ if (dirName && dirName !== "." && dirName !== "..") {
1571
+ return generateSlug(dirName);
1572
+ }
1573
+ return generateSlug(basename(filePath, ".md"));
1574
+ }
1575
+ function resolveOutputDir(baseDir, slug) {
1576
+ let outputDir = join4(baseDir, slug);
1577
+ try {
1578
+ mkdirSync3(outputDir, { recursive: false });
1579
+ } catch (e) {
1580
+ if (e.code === "EEXIST") {
1581
+ outputDir = `${outputDir}-${Date.now()}`;
1582
+ mkdirSync3(outputDir, { recursive: true });
1583
+ } else {
1584
+ mkdirSync3(outputDir, { recursive: true });
1585
+ }
1586
+ }
1587
+ return outputDir;
1588
+ }
1589
+ function buildPrompt(question, context) {
1590
+ const parts = [
1591
+ "# Second Opinion Request",
1592
+ "",
1593
+ "## Question",
1594
+ question,
1595
+ ""
1596
+ ];
1597
+ if (context) {
1598
+ parts.push("## Context", "", context, "");
1599
+ }
1600
+ parts.push(
1601
+ "## Instructions",
1602
+ "You are providing an independent second opinion. Be critical and thorough.",
1603
+ "- Analyze the question in the context provided",
1604
+ "- Identify risks, tradeoffs, and blind spots",
1605
+ "- Suggest alternatives if you see better approaches",
1606
+ "- Be direct and opinionated \u2014 don't hedge",
1607
+ "- Structure your response with clear headings",
1608
+ "- Keep your response focused and actionable",
1609
+ ""
1610
+ );
1611
+ return parts.join("\n");
1612
+ }
1613
+
1614
+ // src/core/synthesis.ts
1615
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
1616
+ import { join as join5 } from "path";
1617
+ function synthesize(manifest, outputDir) {
1618
+ const parts = [
1619
+ "# Run Summary",
1620
+ "",
1621
+ `**Prompt:** ${manifest.prompt.slice(0, 100)}${manifest.prompt.length > 100 ? "..." : ""}`,
1622
+ `**Tools:** ${manifest.tools.map((t) => t.toolId).join(", ")}`,
1623
+ `**Policy:** read-only=${manifest.readOnlyPolicy}`,
1624
+ ""
1625
+ ];
1626
+ parts.push("## Results", "");
1627
+ for (const report of manifest.tools) {
1628
+ const icon = report.status === "success" ? "\u2713" : report.status === "timeout" ? "\u23F1" : "\u2717";
1629
+ const duration = (report.durationMs / 1e3).toFixed(1);
1630
+ parts.push(`### ${icon} ${report.toolId}`);
1631
+ parts.push("");
1632
+ parts.push(`- Status: ${report.status}`);
1633
+ parts.push(`- Duration: ${duration}s`);
1634
+ parts.push(`- Word count: ${report.wordCount}`);
1635
+ if (report.cost) {
1636
+ parts.push(
1637
+ `- Cost: $${report.cost.cost_usd.toFixed(2)} (${report.cost.source})`
1638
+ );
1639
+ }
1640
+ if (report.status === "error" && report.error) {
1641
+ parts.push(`- Error: ${report.error}`);
1642
+ }
1643
+ if (report.status === "success") {
1644
+ const headings = extractHeadings(outputDir, report);
1645
+ if (headings.length > 0) {
1646
+ parts.push("- Key sections:");
1647
+ for (const h of headings) {
1648
+ parts.push(` - ${h}`);
1649
+ }
1650
+ }
1651
+ }
1652
+ parts.push("");
1653
+ }
1654
+ const costsAvailable = manifest.tools.filter((t) => t.cost);
1655
+ if (costsAvailable.length > 0) {
1656
+ parts.push("## Cost Summary", "");
1657
+ parts.push("| Tool | Cost | Source | Remaining |");
1658
+ parts.push("|------|------|--------|-----------|");
1659
+ for (const t of costsAvailable) {
1660
+ const c = t.cost;
1661
+ parts.push(
1662
+ `| ${t.toolId} | $${c.cost_usd.toFixed(2)} | ${c.source} | $${c.source === "credits" ? c.credits_remaining_usd.toFixed(2) : c.free_remaining_usd.toFixed(2)} |`
1663
+ );
1664
+ }
1665
+ parts.push("");
1666
+ }
1667
+ return parts.join("\n");
1668
+ }
1669
+ function extractHeadings(outputDir, report) {
1670
+ const filePath = join5(outputDir, `${sanitizeId(report.toolId)}.md`);
1671
+ if (!existsSync6(filePath)) return [];
1672
+ try {
1673
+ const content = readFileSync4(filePath, "utf-8");
1674
+ const headings = [];
1675
+ for (const line of content.split("\n")) {
1676
+ const match = line.match(/^#{1,3}\s+(.+)/);
1677
+ if (match) {
1678
+ headings.push(match[1].trim());
1679
+ if (headings.length >= 10) break;
1680
+ }
1681
+ }
1682
+ return headings;
1683
+ } catch {
1684
+ return [];
1685
+ }
1686
+ }
1687
+
1688
+ // src/ui/progress.ts
1689
+ var SPINNER_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
1690
+ var TICK_INTERVAL = 200;
1691
+ var RED = "\x1B[31m";
1692
+ var DIM = "\x1B[2m";
1693
+ var RESET = "\x1B[0m";
1694
+ var ProgressDisplay = class {
1695
+ tools;
1696
+ order;
1697
+ outputDir;
1698
+ timer = null;
1699
+ frame = 0;
1700
+ lineCount = 0;
1701
+ isTTY;
1702
+ constructor(toolIds, outputDir) {
1703
+ this.isTTY = Boolean(process.stderr.isTTY);
1704
+ this.outputDir = outputDir;
1705
+ this.tools = /* @__PURE__ */ new Map();
1706
+ this.order = [];
1707
+ for (const id of toolIds) {
1708
+ this.tools.set(id, {
1709
+ toolId: id,
1710
+ status: "pending"
1711
+ });
1712
+ this.order.push(id);
1713
+ }
1714
+ if (this.isTTY) {
1715
+ this.render();
1716
+ this.timer = setInterval(() => {
1717
+ this.frame++;
1718
+ this.render();
1719
+ }, TICK_INTERVAL);
1720
+ } else {
1721
+ process.stderr.write(` Output: ${this.outputDir}
1722
+ `);
1723
+ }
1724
+ }
1725
+ start(toolId) {
1726
+ const tool = this.tools.get(toolId);
1727
+ if (!tool) return;
1728
+ tool.status = "running";
1729
+ tool.startedAt = Date.now();
1730
+ if (!this.isTTY) {
1731
+ process.stderr.write(` \u25B8 ${toolId} started
1732
+ `);
1733
+ }
1734
+ }
1735
+ complete(toolId, report) {
1736
+ const tool = this.tools.get(toolId);
1737
+ if (!tool) return;
1738
+ tool.status = "done";
1739
+ tool.report = report;
1740
+ if (!this.isTTY) {
1741
+ const duration = (report.durationMs / 1e3).toFixed(1);
1742
+ const icon = report.status === "success" ? "\u2713" : report.status === "timeout" ? "\u23F1" : "\u2717";
1743
+ process.stderr.write(
1744
+ ` ${icon} ${toolId} done ${duration}s ${report.wordCount.toLocaleString()} words
1745
+ `
1746
+ );
1747
+ if (report.status !== "success" && report.error) {
1748
+ process.stderr.write(
1749
+ ` \u2514 ${report.error.split("\n")[0].slice(0, 120)}
1750
+ `
1751
+ );
1752
+ }
1753
+ }
1754
+ }
1755
+ stop() {
1756
+ if (this.timer) {
1757
+ clearInterval(this.timer);
1758
+ this.timer = null;
1759
+ }
1760
+ if (this.isTTY) {
1761
+ this.render();
1762
+ }
1763
+ }
1764
+ render() {
1765
+ const lines = [];
1766
+ lines.push(` ${DIM}Output: ${this.outputDir}${RESET}`);
1767
+ for (const id of this.order) {
1768
+ const tool = this.tools.get(id);
1769
+ lines.push(this.formatLine(tool));
1770
+ if (tool.status === "done" && tool.report?.status !== "success" && tool.report?.error) {
1771
+ const msg = tool.report.error.split("\n")[0].slice(0, 120);
1772
+ lines.push(` ${RED}\u2514 ${msg}${RESET}`);
1773
+ }
1774
+ }
1775
+ if (this.lineCount > 0) {
1776
+ process.stderr.write(`\x1B[${this.lineCount}A`);
1777
+ }
1778
+ for (const line of lines) {
1779
+ process.stderr.write(`\x1B[K${line}
1780
+ `);
1781
+ }
1782
+ this.lineCount = lines.length;
1783
+ }
1784
+ formatLine(tool) {
1785
+ const label = tool.toolId;
1786
+ switch (tool.status) {
1787
+ case "pending": {
1788
+ const pad = " ".repeat(Math.max(0, 40 - label.length));
1789
+ return ` \u23F3 ${label}${pad}pending`;
1790
+ }
1791
+ case "running": {
1792
+ const spinner = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
1793
+ const elapsed = tool.startedAt ? ((Date.now() - tool.startedAt) / 1e3).toFixed(1) : "0.0";
1794
+ const pad = " ".repeat(Math.max(0, 40 - label.length));
1795
+ return ` ${spinner} ${label}${pad}running ${elapsed.padStart(6)}s`;
1796
+ }
1797
+ case "done": {
1798
+ const r = tool.report;
1799
+ const icon = r.status === "success" ? "\u2713" : r.status === "timeout" ? "\u23F1" : "\u2717";
1800
+ const duration = (r.durationMs / 1e3).toFixed(1);
1801
+ const pad = " ".repeat(Math.max(0, 40 - label.length));
1802
+ return ` ${icon} ${label}${pad}done ${duration.padStart(6)}s ${r.wordCount.toLocaleString()} words`;
1803
+ }
1804
+ }
1805
+ }
1806
+ };
1807
+
1808
+ // src/commands/run.ts
1809
+ function registerRunCommand(program2) {
1810
+ program2.command("run [prompt]").description("Dispatch prompt to configured AI tools in parallel").option("-f, --file <path>", "Use a pre-built prompt file (no wrapping)").option("-t, --tools <tools>", "Comma-separated list of tools to use").option(
1811
+ "--context <paths>",
1812
+ 'Gather context from paths (comma-separated, or "." for git diff)'
1813
+ ).option("--read-only <level>", "Read-only policy: strict, best-effort, off").option("--dry-run", "Show what would be dispatched without running").option("--json", "Output manifest as JSON").option("-o, --output-dir <dir>", "Base output directory").action(
1814
+ async (promptArg, opts) => {
1815
+ const cwd = process.cwd();
1816
+ const globalConfig = loadConfig();
1817
+ const projectConfig = loadProjectConfig(cwd);
1818
+ const config = mergeConfigs(globalConfig, projectConfig);
1819
+ let toolIds;
1820
+ const explicitTools = Boolean(opts.tools);
1821
+ if (opts.tools) {
1822
+ toolIds = opts.tools.split(",").map((t) => t.trim());
1823
+ } else {
1824
+ toolIds = Object.keys(config.tools);
1825
+ }
1826
+ if (toolIds.length === 0) {
1827
+ error('No tools configured. Run "counselors init" first.');
1828
+ process.exitCode = 1;
1829
+ return;
1830
+ }
1831
+ for (const id of toolIds) {
1832
+ if (!config.tools[id]) {
1833
+ error(
1834
+ `Tool "${id}" not configured. Run "counselors tools add ${id}".`
1835
+ );
1836
+ process.exitCode = 1;
1837
+ return;
1838
+ }
1839
+ }
1840
+ if (!explicitTools && !opts.dryRun && process.stderr.isTTY && toolIds.length > 1) {
1841
+ const selected = await selectRunTools(toolIds);
1842
+ if (selected.length === 0) {
1843
+ error("No tools selected.");
1844
+ process.exitCode = 1;
1845
+ return;
1846
+ }
1847
+ toolIds = selected;
1848
+ }
1849
+ const internalToCliMap = {
1850
+ enforced: "strict",
1851
+ bestEffort: "best-effort",
1852
+ none: "off"
1853
+ };
1854
+ const readOnlyInput = opts.readOnly ?? internalToCliMap[config.defaults.readOnly] ?? "best-effort";
1855
+ const readOnlyMap = {
1856
+ strict: "enforced",
1857
+ "best-effort": "bestEffort",
1858
+ off: "none"
1859
+ };
1860
+ const readOnlyPolicy = readOnlyMap[readOnlyInput];
1861
+ if (!readOnlyPolicy) {
1862
+ error(
1863
+ `Invalid --read-only value "${readOnlyInput}". Must be: strict, best-effort, or off.`
1864
+ );
1865
+ process.exitCode = 1;
1866
+ return;
1867
+ }
1868
+ let promptContent;
1869
+ let promptSource;
1870
+ let slug;
1871
+ if (opts.file) {
1872
+ const filePath = resolve4(cwd, opts.file);
1873
+ try {
1874
+ promptContent = readFileSync5(filePath, "utf-8");
1875
+ } catch {
1876
+ error(`Cannot read prompt file: ${filePath}`);
1877
+ process.exitCode = 1;
1878
+ return;
1879
+ }
1880
+ promptSource = "file";
1881
+ slug = generateSlugFromFile(filePath);
1882
+ } else if (promptArg) {
1883
+ promptSource = "inline";
1884
+ slug = generateSlug(promptArg);
1885
+ const context = opts.context ? gatherContext(
1886
+ cwd,
1887
+ opts.context === "." ? [] : opts.context.split(","),
1888
+ config.defaults.maxContextKb
1889
+ ) : void 0;
1890
+ promptContent = buildPrompt(promptArg, context);
1891
+ } else {
1892
+ if (process.stdin.isTTY) {
1893
+ error(
1894
+ "No prompt provided. Pass as argument, use -f <file>, or pipe via stdin."
1895
+ );
1896
+ process.exitCode = 1;
1897
+ return;
1898
+ }
1899
+ const chunks = [];
1900
+ for await (const chunk of process.stdin) {
1901
+ chunks.push(chunk);
1902
+ }
1903
+ const stdinContent = Buffer.concat(chunks).toString("utf-8").trim();
1904
+ if (!stdinContent) {
1905
+ error("Empty prompt from stdin.");
1906
+ process.exitCode = 1;
1907
+ return;
1908
+ }
1909
+ promptSource = "stdin";
1910
+ slug = generateSlug(stdinContent);
1911
+ const context = opts.context ? gatherContext(
1912
+ cwd,
1913
+ opts.context === "." ? [] : opts.context.split(","),
1914
+ config.defaults.maxContextKb
1915
+ ) : void 0;
1916
+ promptContent = buildPrompt(stdinContent, context);
1917
+ }
1918
+ if (!slug) slug = `run-${Date.now()}`;
1919
+ if (opts.dryRun) {
1920
+ const baseDir2 = opts.outputDir || config.defaults.outputDir;
1921
+ const dryOutputDir = join6(baseDir2, slug);
1922
+ const dryPromptFile = resolve4(dryOutputDir, "prompt.md");
1923
+ const invocations = toolIds.map((id) => {
1924
+ const toolConfig = config.tools[id];
1925
+ const adapter = resolveAdapter(id, toolConfig);
1926
+ const inv = adapter.buildInvocation({
1927
+ prompt: promptContent,
1928
+ promptFilePath: dryPromptFile,
1929
+ toolId: id,
1930
+ outputDir: dryOutputDir,
1931
+ readOnlyPolicy,
1932
+ timeout: config.defaults.timeout,
1933
+ cwd,
1934
+ binary: toolConfig.binary,
1935
+ extraFlags: toolConfig.extraFlags
1936
+ });
1937
+ return {
1938
+ toolId: id,
1939
+ cmd: inv.cmd,
1940
+ args: inv.args
1941
+ };
1942
+ });
1943
+ info(formatDryRun(invocations));
1944
+ return;
1945
+ }
1946
+ const baseDir = opts.outputDir || config.defaults.outputDir;
1947
+ const outputDir = resolveOutputDir(baseDir, slug);
1948
+ const promptFilePath = resolve4(outputDir, "prompt.md");
1949
+ if (opts.file) {
1950
+ copyFileSync2(resolve4(cwd, opts.file), promptFilePath);
1951
+ } else {
1952
+ safeWriteFile(promptFilePath, promptContent);
1953
+ }
1954
+ const display = new ProgressDisplay(toolIds, outputDir);
1955
+ let reports;
1956
+ try {
1957
+ reports = await dispatch({
1958
+ config,
1959
+ toolIds,
1960
+ promptFilePath,
1961
+ promptContent,
1962
+ outputDir,
1963
+ readOnlyPolicy,
1964
+ cwd,
1965
+ onProgress: (event) => {
1966
+ if (event.event === "started") display.start(event.toolId);
1967
+ if (event.event === "completed")
1968
+ display.complete(event.toolId, event.report);
1969
+ }
1970
+ });
1971
+ } finally {
1972
+ display.stop();
1973
+ }
1974
+ const manifest = {
1975
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1976
+ slug,
1977
+ prompt: promptArg || (opts.file ? `file:${basename2(opts.file)}` : "stdin"),
1978
+ promptSource,
1979
+ readOnlyPolicy,
1980
+ tools: reports
1981
+ };
1982
+ safeWriteFile(
1983
+ resolve4(outputDir, "run.json"),
1984
+ JSON.stringify(manifest, null, 2)
1985
+ );
1986
+ const summary = synthesize(manifest, outputDir);
1987
+ safeWriteFile(resolve4(outputDir, "summary.md"), summary);
1988
+ if (opts.json) {
1989
+ info(JSON.stringify(manifest, null, 2));
1990
+ } else {
1991
+ info(formatRunSummary(manifest));
1992
+ }
1993
+ }
1994
+ );
1995
+ }
1996
+
1997
+ // src/commands/skill.ts
1998
+ function registerSkillCommand(program2) {
1999
+ program2.command("skill").description("Print a skill/slash-command template for coding agents").action(async () => {
2000
+ const template = `---
2001
+ name: counselors
2002
+ description: Get parallel second opinions from multiple AI coding agents. Use when the user wants independent reviews, architecture feedback, or a sanity check from other AI models.
2003
+ ---
2004
+
2005
+ # Counselors \u2014 Multi-Agent Review Skill
2006
+
2007
+ > **Note:** This is a reference skill template. Your agent system may use a different skill/command format. Adapt the structure and frontmatter below to match your system's conventions \u2014 the workflow and phases are what matter.
2008
+
2009
+ Fan out a prompt to multiple AI coding agents in parallel and synthesize their responses.
2010
+
2011
+ Arguments: $ARGUMENTS
2012
+
2013
+ **If no arguments provided**, ask the user what they want reviewed.
2014
+
2015
+ ---
2016
+
2017
+ ## Phase 1: Context Gathering
2018
+
2019
+ Parse \`$ARGUMENTS\` to understand what the user wants reviewed. Then auto-gather relevant context:
2020
+
2021
+ 1. **Files mentioned in the prompt**: Use Glob/Grep to find files referenced by name, class, function, or keyword
2022
+ 2. **Recent changes**: Run \`git diff HEAD\` and \`git diff --staged\` to capture recent work
2023
+ 3. **Related code**: Search for key terms from the prompt and read the most relevant files (up to 5 files, ~50KB total cap)
2024
+
2025
+ Be selective \u2014 don't dump the entire codebase. Pick the most relevant code sections.
2026
+
2027
+ ---
2028
+
2029
+ ## Phase 2: Agent Selection
2030
+
2031
+ 1. **Discover available agents** by running via Bash:
2032
+ \`\`\`bash
2033
+ counselors ls
2034
+ \`\`\`
2035
+ This lists all configured agents with their IDs and binaries.
2036
+
2037
+ 2. **MANDATORY: Print the full agent list, then ask the user which to use.**
2038
+
2039
+ **Always print the full \`counselors ls\` output as inline text** (not inside AskUserQuestion). Just show the raw output from the command so the user sees every agent with its ID and binary. Do NOT reformat or abbreviate it.
2040
+
2041
+ Then ask the user to pick:
2042
+
2043
+ **If 4 or fewer agents**: Use AskUserQuestion with \`multiSelect: true\`, one option per agent.
2044
+
2045
+ **If more than 4 agents**: AskUserQuestion only supports 4 options. Use these fixed options:
2046
+ - Option 1: "All [N] agents" \u2014 sends to every configured agent
2047
+ - Option 2-4: The first 3 individual agents by ID
2048
+ - The user can always select "Other" to type a comma-separated list of agent IDs from the printed list above
2049
+
2050
+ Do NOT combine agents into preset groups (e.g. "claude + codex + gemini"). Each option must be a single agent or "All".
2051
+
2052
+ 3. Wait for the user's selection before proceeding.
2053
+
2054
+ 4. **MANDATORY: Confirm the selection before continuing.** After the user picks agents, echo back the exact list you will dispatch to:
2055
+
2056
+ > Dispatching to: **claude-opus**, **codex-5.3-high**, **gemini-pro**
2057
+
2058
+ Then ask the user to confirm (e.g. "Look good?") before proceeding to Phase 3. This prevents silent tool omissions. If the user corrects the list, update your selection accordingly.
2059
+
2060
+ ---
2061
+
2062
+ ## Phase 3: Prompt Assembly
2063
+
2064
+ 1. **Generate a slug** from the topic (lowercase, hyphens, max 40 chars)
2065
+ - "review the auth flow" \u2192 \`auth-flow-review\`
2066
+ - "is this migration safe" \u2192 \`migration-safety-review\`
2067
+
2068
+ 2. **Create the output directory** via Bash. The directory name MUST always be prefixed with a millisecond-precision UNIX timestamp so runs are lexically sortable and never collide:
2069
+ \`\`\`
2070
+ ./agents/counselors/TIMESTAMP-[slug]
2071
+ \`\`\`
2072
+ For example: \`./agents/counselors/1770676882780-auth-flow-review\`
2073
+
2074
+ 3. **Write the prompt file** using the Write tool to \`./agents/counselors/TIMESTAMP-[slug]/prompt.md\`:
2075
+
2076
+ \`\`\`markdown
2077
+ # Review Request
2078
+
2079
+ ## Question
2080
+ [User's original prompt/question from $ARGUMENTS]
2081
+
2082
+ ## Context
2083
+
2084
+ ### Files Referenced
2085
+ [Contents of the most relevant files found in Phase 1]
2086
+
2087
+ ### Recent Changes
2088
+ [git diff output, if any]
2089
+
2090
+ ### Related Code
2091
+ [Related files discovered via search]
2092
+
2093
+ ## Instructions
2094
+ You are providing an independent review. Be critical and thorough.
2095
+ - Analyze the question in the context provided
2096
+ - Identify risks, tradeoffs, and blind spots
2097
+ - Suggest alternatives if you see better approaches
2098
+ - Be direct and opinionated \u2014 don't hedge
2099
+ - Structure your response with clear headings
2100
+ \`\`\`
2101
+
2102
+ ---
2103
+
2104
+ ## Phase 4: Dispatch
2105
+
2106
+ Run counselors via Bash with the prompt file, passing the user's selected agents:
2107
+
2108
+ \`\`\`bash
2109
+ counselors run -f ./agents/counselors/[slug]/prompt.md --tools [comma-separated-selections] --json
2110
+ \`\`\`
2111
+
2112
+ Example: \`--tools claude,codex,gemini\`
2113
+
2114
+ Use \`timeout: 600000\` (10 minutes). Counselors dispatches to the selected agents in parallel and writes results to the output directory shown in the JSON output.
2115
+
2116
+ **Important**: Use \`-f\` (file mode) so the prompt is sent as-is without wrapping. Use \`--json\` to get structured output for parsing.
2117
+
2118
+ ---
2119
+
2120
+ ## Phase 5: Read Results
2121
+
2122
+ 1. **Parse the JSON output** from stdout \u2014 it contains the run manifest with status, duration, word count, and output file paths for each agent
2123
+ 2. **Read each agent's response** from the \`outputFile\` path in the manifest
2124
+ 3. **Check \`stderrFile\` paths** for any agent that failed or returned empty output
2125
+ 4. **Skip empty or error-only reports** \u2014 note which agents failed
2126
+
2127
+ ---
2128
+
2129
+ ## Phase 6: Synthesize and Present
2130
+
2131
+ Combine all agent responses into a synthesis:
2132
+
2133
+ \`\`\`markdown
2134
+ ## Counselors Review
2135
+
2136
+ **Agents consulted:** [list of agents that responded]
2137
+
2138
+ **Consensus:** [What most agents agree on \u2014 key takeaways]
2139
+
2140
+ **Disagreements:** [Where they differ, and reasoning behind each position]
2141
+
2142
+ **Key Risks:** [Risks or concerns flagged by any agent]
2143
+
2144
+ **Blind Spots:** [Things none of the agents addressed that seem important]
2145
+
2146
+ **Recommendation:** [Your synthesized recommendation based on all inputs]
2147
+
2148
+ ---
2149
+ Reports saved to: [output directory from manifest]
2150
+ \`\`\`
2151
+
2152
+ Present this synthesis to the user. Be concise \u2014 the individual reports are saved for deep reading.
2153
+
2154
+ ---
2155
+
2156
+ ## Phase 7: Action (Optional)
2157
+
2158
+ After presenting the synthesis, ask the user what they'd like to address. Offer the top 2-3 actionable items from the synthesis as options. If the user wants to act on findings, plan the implementation before making changes.
2159
+
2160
+ ---
2161
+
2162
+ ## Error Handling
2163
+
2164
+ - **counselors not installed**: Tell the user to install it (\`npm install -g counselors\`)
2165
+ - **No tools configured**: Tell the user to run \`counselors init\` or \`counselors add\`
2166
+ - **Agent fails**: Note it in the synthesis and continue with other agents' results
2167
+ - **All agents fail**: Report errors from stderr files and suggest checking \`counselors doctor\`
2168
+ `;
2169
+ info(template);
2170
+ });
2171
+ }
2172
+
2173
+ // src/commands/tools/add.ts
2174
+ import { accessSync as accessSync2, constants as constants2 } from "fs";
2175
+ import { resolve as resolve5 } from "path";
2176
+ var CUSTOM_TOOL_VALUE = "__custom__";
2177
+ async function runAddWizard() {
2178
+ const spinner = createSpinner("Discovering installed tools...").start();
2179
+ const adapters = getAllBuiltInAdapters();
2180
+ const discovered = [];
2181
+ for (const adapter of adapters) {
2182
+ const result = discoverTool(adapter.commands);
2183
+ discovered.push({
2184
+ id: adapter.id,
2185
+ name: adapter.displayName,
2186
+ found: result.found,
2187
+ version: result.version
2188
+ });
2189
+ }
2190
+ spinner.stop();
2191
+ const choices = discovered.map((d) => ({
2192
+ name: d.found ? `${d.name} (${d.id})${d.version ? ` \u2014 ${d.version}` : ""}` : `${d.name} (${d.id}) \u2014 not installed`,
2193
+ value: d.id,
2194
+ disabled: !d.found ? "(not installed)" : void 0
2195
+ }));
2196
+ choices.push({
2197
+ name: "Custom tool \u2014 provide a binary path",
2198
+ value: CUSTOM_TOOL_VALUE,
2199
+ disabled: void 0
2200
+ });
2201
+ const selected = await promptSelect(
2202
+ "Which tool would you like to add?",
2203
+ choices
2204
+ );
2205
+ if (selected === CUSTOM_TOOL_VALUE) {
2206
+ return { toolId: "", isCustom: true };
2207
+ }
2208
+ return { toolId: selected, isCustom: false };
2209
+ }
2210
+ function validateBinary(input2) {
2211
+ const resolved = resolve5(input2);
2212
+ try {
2213
+ accessSync2(resolved, constants2.X_OK);
2214
+ return resolved;
2215
+ } catch {
2216
+ }
2217
+ const found = findBinary(input2);
2218
+ if (found) return found;
2219
+ return null;
2220
+ }
2221
+ async function addBuiltInTool(toolId, config, nameOverride) {
2222
+ const adapter = getAdapter(toolId);
2223
+ const discovery = discoverTool(adapter.commands);
2224
+ if (!discovery.found) {
2225
+ error(
2226
+ `"${toolId}" binary not found. Install it from: ${adapter.installUrl}`
2227
+ );
2228
+ process.exitCode = 1;
2229
+ return;
2230
+ }
2231
+ const selectedModel = await selectModelDetails(toolId, adapter.models);
2232
+ const defaultName = nameOverride ?? selectedModel.compoundId ?? `${toolId}-${selectedModel.id}`;
2233
+ let name = nameOverride ?? await promptInput("Tool name:", defaultName);
2234
+ if (!SAFE_ID_RE.test(name)) {
2235
+ error(
2236
+ `Invalid tool name "${name}". Use only letters, numbers, dots, hyphens, and underscores.`
2237
+ );
2238
+ process.exitCode = 1;
2239
+ return;
2240
+ }
2241
+ if (config.tools[name]) {
2242
+ const overwrite = await confirmOverwrite(name);
2243
+ if (!overwrite) {
2244
+ name = await promptInput("Pick a different name:");
2245
+ if (!SAFE_ID_RE.test(name)) {
2246
+ error(
2247
+ `Invalid tool name "${name}". Use only letters, numbers, dots, hyphens, and underscores.`
2248
+ );
2249
+ process.exitCode = 1;
2250
+ return;
2251
+ }
2252
+ if (config.tools[name]) {
2253
+ error(`"${name}" also exists. Run "counselors tools add" again.`);
2254
+ process.exitCode = 1;
2255
+ return;
2256
+ }
2257
+ }
2258
+ }
2259
+ const toolConfig = {
2260
+ binary: discovery.path,
2261
+ readOnly: { level: adapter.readOnly.level },
2262
+ adapter: toolId,
2263
+ ...selectedModel.extraFlags ? { extraFlags: selectedModel.extraFlags } : {}
2264
+ };
2265
+ const updated = addToolToConfig(config, name, toolConfig);
2266
+ saveConfig(updated);
2267
+ if (toolId === "amp") {
2268
+ copyAmpSettings();
2269
+ }
2270
+ success(`Added "${name}" to config.`);
2271
+ }
2272
+ async function collectCustomConfig(config, presetId) {
2273
+ let binaryPath = null;
2274
+ while (!binaryPath) {
2275
+ const binaryInput = await promptInput("Binary path or command:");
2276
+ binaryPath = validateBinary(binaryInput);
2277
+ if (!binaryPath) {
2278
+ warn(`"${binaryInput}" not found or not executable. Please try again.`);
2279
+ }
2280
+ }
2281
+ const useStdin = await confirmAction(
2282
+ "Does this tool receive prompts via stdin?"
2283
+ );
2284
+ info("");
2285
+ info(" Counselors runs tools non-interactively. Your flags MUST include:");
2286
+ info(
2287
+ " 1. Headless/non-interactive mode (e.g. -p, --non-interactive, --headless)"
2288
+ );
2289
+ info(" 2. Model selection if needed (e.g. --model gpt-4o)");
2290
+ info(" 3. Output format if needed (e.g. --output-format text)");
2291
+ info("");
2292
+ if (!useStdin) {
2293
+ info(" Counselors will append the prompt as the last CLI argument:");
2294
+ info(
2295
+ ' "Read the file at <path> and follow the instructions within it."'
2296
+ );
2297
+ } else {
2298
+ info(" Counselors will pipe the prompt text to stdin.");
2299
+ }
2300
+ info("");
2301
+ info(" Example: -p --model gpt-4o --output-format text");
2302
+ info("");
2303
+ let extraFlags;
2304
+ const flagsInput = await promptInput("Flags (space-separated):");
2305
+ if (flagsInput.trim()) {
2306
+ extraFlags = flagsInput.trim().split(/\s+/);
2307
+ }
2308
+ const readOnlyLevel = await promptSelect(
2309
+ "Read-only capability:",
2310
+ [
2311
+ { name: "Enforced \u2014 tool guarantees read-only", value: "enforced" },
2312
+ {
2313
+ name: "Best effort \u2014 tool tries but may not guarantee",
2314
+ value: "bestEffort"
2315
+ },
2316
+ { name: "None \u2014 tool has full access", value: "none" }
2317
+ ]
2318
+ );
2319
+ const defaultId = presetId ?? binaryPath.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "custom";
2320
+ const toolId = await promptInput(
2321
+ "Tool name (used in config and output filenames):",
2322
+ defaultId
2323
+ );
2324
+ if (!SAFE_ID_RE.test(toolId)) {
2325
+ error(
2326
+ `Invalid tool name "${toolId}". Use only letters, numbers, dots, hyphens, and underscores.`
2327
+ );
2328
+ process.exitCode = 1;
2329
+ return;
2330
+ }
2331
+ info("");
2332
+ info(" Tool will be invoked as:");
2333
+ const previewArgs = [
2334
+ ...extraFlags ?? [],
2335
+ useStdin ? "< prompt.md" : '"Read the file at <path> and follow the instructions..."'
2336
+ ];
2337
+ info(` ${binaryPath} ${previewArgs.join(" ")}`);
2338
+ info("");
2339
+ if (config.tools[toolId]) {
2340
+ const overwrite = await confirmOverwrite(toolId);
2341
+ if (!overwrite) {
2342
+ const newId = await promptInput("Pick a different name:");
2343
+ if (!SAFE_ID_RE.test(newId)) {
2344
+ error(
2345
+ `Invalid tool name "${newId}". Use only letters, numbers, dots, hyphens, and underscores.`
2346
+ );
2347
+ process.exitCode = 1;
2348
+ return;
2349
+ }
2350
+ if (config.tools[newId]) {
2351
+ error(`"${newId}" also exists. Run "counselors tools add" again.`);
2352
+ process.exitCode = 1;
2353
+ return;
2354
+ }
2355
+ const toolConfig2 = {
2356
+ binary: binaryPath,
2357
+ readOnly: { level: readOnlyLevel },
2358
+ ...useStdin ? { stdin: true } : {},
2359
+ extraFlags,
2360
+ custom: true
2361
+ };
2362
+ const updated2 = addToolToConfig(config, newId, toolConfig2);
2363
+ saveConfig(updated2);
2364
+ success(`Added "${newId}" to config.`);
2365
+ return;
2366
+ }
2367
+ }
2368
+ const toolConfig = {
2369
+ binary: binaryPath,
2370
+ readOnly: { level: readOnlyLevel },
2371
+ ...useStdin ? { stdin: true } : {},
2372
+ extraFlags,
2373
+ custom: true
2374
+ };
2375
+ const updated = addToolToConfig(config, toolId, toolConfig);
2376
+ saveConfig(updated);
2377
+ success(`Added "${toolId}" to config.`);
2378
+ }
2379
+ function registerAddCommand(program2) {
2380
+ program2.command("add [tool]").description("Add a tool (claude, codex, gemini, amp, or custom)").action(async (toolId) => {
2381
+ const config = loadConfig();
2382
+ if (!toolId) {
2383
+ const result = await runAddWizard();
2384
+ if (result.isCustom) {
2385
+ await collectCustomConfig(config);
2386
+ } else {
2387
+ await addBuiltInTool(result.toolId, config);
2388
+ }
2389
+ return;
2390
+ }
2391
+ if (isBuiltInTool(toolId)) {
2392
+ await addBuiltInTool(toolId, config, toolId);
2393
+ } else {
2394
+ await collectCustomConfig(config, toolId);
2395
+ }
2396
+ });
2397
+ }
2398
+
2399
+ // src/commands/tools/discover.ts
2400
+ function registerDiscoverCommand(program2) {
2401
+ program2.command("discover").description("Discover installed AI CLI tools").action(async () => {
2402
+ const spinner = createSpinner("Scanning for AI CLI tools...").start();
2403
+ const adapters = getAllBuiltInAdapters();
2404
+ const results = [];
2405
+ for (const adapter of adapters) {
2406
+ const result = discoverTool(adapter.commands);
2407
+ results.push({
2408
+ ...result,
2409
+ toolId: adapter.id,
2410
+ displayName: adapter.displayName
2411
+ });
2412
+ }
2413
+ spinner.stop();
2414
+ info(formatDiscoveryResults(results));
2415
+ });
2416
+ }
2417
+
2418
+ // src/commands/tools/list.ts
2419
+ function registerListCommand(program2) {
2420
+ program2.command("list").alias("ls").description("List configured tools").option("-v, --verbose", "Show full tool configuration including flags").action(async (opts) => {
2421
+ const config = loadConfig();
2422
+ const tools2 = Object.entries(config.tools).map(([id, t]) => {
2423
+ const entry = {
2424
+ id,
2425
+ binary: t.binary
2426
+ };
2427
+ if (opts.verbose) {
2428
+ const adapter = resolveAdapter(id, t);
2429
+ const inv = adapter.buildInvocation({
2430
+ prompt: "<prompt>",
2431
+ promptFilePath: "<prompt-file>",
2432
+ toolId: id,
2433
+ outputDir: ".",
2434
+ readOnlyPolicy: t.readOnly.level,
2435
+ timeout: t.timeout ?? config.defaults.timeout,
2436
+ cwd: process.cwd(),
2437
+ binary: t.binary,
2438
+ extraFlags: t.extraFlags
2439
+ });
2440
+ entry.args = inv.args;
2441
+ }
2442
+ return entry;
2443
+ });
2444
+ info(formatToolList(tools2, opts.verbose));
2445
+ });
2446
+ }
2447
+
2448
+ // src/commands/tools/remove.ts
2449
+ import { checkbox as checkbox2 } from "@inquirer/prompts";
2450
+ function registerRemoveCommand(program2) {
2451
+ program2.command("remove [tool]").description("Remove a configured tool").action(async (toolId) => {
2452
+ const config = loadConfig();
2453
+ const toolIds = Object.keys(config.tools);
2454
+ if (toolIds.length === 0) {
2455
+ error("No tools configured.");
2456
+ process.exitCode = 1;
2457
+ return;
2458
+ }
2459
+ let toRemove;
2460
+ if (toolId) {
2461
+ if (!config.tools[toolId]) {
2462
+ error(`Tool "${toolId}" is not configured.`);
2463
+ process.exitCode = 1;
2464
+ return;
2465
+ }
2466
+ toRemove = [toolId];
2467
+ } else {
2468
+ toRemove = await checkbox2({
2469
+ message: "Select tools to remove:",
2470
+ choices: toolIds.map((id) => ({
2471
+ name: `${id} (${config.tools[id].binary})`,
2472
+ value: id
2473
+ }))
2474
+ });
2475
+ if (toRemove.length === 0) {
2476
+ info("No tools selected.");
2477
+ return;
2478
+ }
2479
+ }
2480
+ const confirmed = await confirmAction(
2481
+ toRemove.length === 1 ? `Remove "${toRemove[0]}" from config?` : `Remove ${toRemove.length} tools from config?`
2482
+ );
2483
+ if (!confirmed) return;
2484
+ let updated = config;
2485
+ for (const id of toRemove) {
2486
+ updated = removeToolFromConfig(updated, id);
2487
+ }
2488
+ saveConfig(updated);
2489
+ success(`Removed ${toRemove.join(", ")}.`);
2490
+ });
2491
+ }
2492
+
2493
+ // src/commands/tools/rename.ts
2494
+ function registerRenameCommand(program2) {
2495
+ program2.command("rename <old> <new>").description("Rename a configured tool").action(async (oldId, newId) => {
2496
+ const config = loadConfig();
2497
+ if (!config.tools[oldId]) {
2498
+ error(`Tool "${oldId}" is not configured.`);
2499
+ process.exitCode = 1;
2500
+ return;
2501
+ }
2502
+ if (config.tools[newId]) {
2503
+ error(`Tool "${newId}" already exists.`);
2504
+ process.exitCode = 1;
2505
+ return;
2506
+ }
2507
+ if (!SAFE_ID_RE.test(newId)) {
2508
+ error(
2509
+ `Invalid tool name "${newId}". Use only letters, numbers, dots, hyphens, and underscores.`
2510
+ );
2511
+ process.exitCode = 1;
2512
+ return;
2513
+ }
2514
+ const updated = renameToolInConfig(config, oldId, newId);
2515
+ saveConfig(updated);
2516
+ success(`Renamed "${oldId}" \u2192 "${newId}".`);
2517
+ });
2518
+ }
2519
+
2520
+ // src/commands/tools/test.ts
2521
+ function registerTestCommand(program2) {
2522
+ program2.command("test [tools...]").description('Test configured tools with a "reply OK" prompt').action(async (toolIds) => {
2523
+ const config = loadConfig();
2524
+ const idsToTest = toolIds.length > 0 ? toolIds : Object.keys(config.tools);
2525
+ if (idsToTest.length === 0) {
2526
+ error('No tools configured. Run "counselors init" first.');
2527
+ process.exitCode = 1;
2528
+ return;
2529
+ }
2530
+ const results = [];
2531
+ for (const id of idsToTest) {
2532
+ const toolConfig = config.tools[id];
2533
+ if (!toolConfig) {
2534
+ results.push({
2535
+ toolId: id,
2536
+ passed: false,
2537
+ output: "",
2538
+ error: "Not configured",
2539
+ durationMs: 0
2540
+ });
2541
+ continue;
2542
+ }
2543
+ const spinner = createSpinner(`Testing ${id}...`).start();
2544
+ const adapter = resolveAdapter(id, toolConfig);
2545
+ const result = await executeTest(adapter, toolConfig, id);
2546
+ spinner.stop();
2547
+ results.push(result);
2548
+ }
2549
+ info(formatTestResults(results));
2550
+ if (results.some((r) => !r.passed)) {
2551
+ process.exitCode = 1;
2552
+ }
2553
+ });
2554
+ }
2555
+
2556
+ // src/cli.ts
2557
+ var program = new Command();
2558
+ program.name("counselors").description("Fan out prompts to multiple AI coding agents in parallel").version(VERSION);
2559
+ registerRunCommand(program);
2560
+ registerDoctorCommand(program);
2561
+ registerInitCommand(program);
2562
+ registerAgentCommand(program);
2563
+ registerSkillCommand(program);
2564
+ var tools = program.command("tools").description("Manage AI tool configurations");
2565
+ registerDiscoverCommand(tools);
2566
+ registerAddCommand(tools);
2567
+ registerRemoveCommand(tools);
2568
+ registerRenameCommand(tools);
2569
+ registerListCommand(tools);
2570
+ registerTestCommand(tools);
2571
+ program.command("add [tool]").description('Alias for "tools add"').action(async (tool) => {
2572
+ const args = tool ? ["add", tool] : ["add"];
2573
+ await tools.parseAsync(args, { from: "user" });
2574
+ });
2575
+ program.command("ls").description('Alias for "tools list"').option("-v, --verbose", "Show full tool configuration including flags").action(async (opts) => {
2576
+ const args = ["list"];
2577
+ if (opts.verbose) args.push("--verbose");
2578
+ await tools.parseAsync(args, { from: "user" });
2579
+ });
2580
+ program.parseAsync(process.argv).catch((err) => {
2581
+ process.stderr.write(`\u2717 ${err.message}
2582
+ `);
2583
+ process.exitCode = 1;
2584
+ });
2585
+ //# sourceMappingURL=cli.js.map