jobarbiter 0.3.1 → 0.3.2

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.
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * JobArbiter Observer — Hook installer for coding agent CLIs
3
3
  *
4
- * Detects installed coding agents, installs observation hooks that
5
- * extract proficiency signals from session transcripts.
4
+ * Installs observation hooks that extract proficiency signals from
5
+ * session transcripts. Uses detect-tools.ts for agent detection.
6
6
  */
7
7
 
8
8
  import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from "node:fs";
9
- import { join, dirname } from "node:path";
9
+ import { join } from "node:path";
10
10
  import { homedir } from "node:os";
11
- import { execSync } from "node:child_process";
11
+ import { getObservableTools, type DetectedTool } from "./detect-tools.js";
12
12
 
13
13
  // ── Types ──────────────────────────────────────────────────────────────
14
14
 
@@ -25,107 +25,39 @@ interface HookConfig {
25
25
  [key: string]: unknown;
26
26
  }
27
27
 
28
- // ── Agent Detection ────────────────────────────────────────────────────
29
-
30
- const AGENT_DEFINITIONS = [
31
- {
32
- id: "claude-code",
33
- name: "Claude Code",
34
- configDir: join(homedir(), ".claude"),
35
- hookFormat: "claude" as const,
36
- detectBin: "claude",
37
- },
38
- {
39
- id: "cursor",
40
- name: "Cursor",
41
- configDir: join(homedir(), ".cursor"),
42
- hookFormat: "cursor" as const,
43
- detectBin: null, // Cursor is an app, not a CLI
44
- detectDir: join(homedir(), ".cursor"),
45
- },
46
- {
47
- id: "opencode",
48
- name: "OpenCode",
49
- configDir: join(homedir(), ".config", "opencode"),
50
- hookFormat: "opencode" as const,
51
- detectBin: "opencode",
52
- },
53
- {
54
- id: "codex",
55
- name: "Codex CLI",
56
- configDir: join(homedir(), ".codex"),
57
- hookFormat: "codex" as const,
58
- detectBin: "codex",
59
- },
60
- {
61
- id: "gemini",
62
- name: "Gemini CLI",
63
- configDir: join(homedir(), ".gemini"),
64
- hookFormat: "gemini" as const,
65
- detectBin: "gemini",
66
- },
67
- ];
68
-
69
- function binExists(name: string): boolean {
70
- try {
71
- execSync(`which ${name}`, { stdio: "ignore" });
72
- return true;
73
- } catch {
74
- return false;
75
- }
76
- }
28
+ // ── Agent Config Directories ───────────────────────────────────────────
77
29
 
78
- export function detectAgents(): DetectedAgent[] {
79
- return AGENT_DEFINITIONS.map((def) => {
80
- const installed =
81
- (def.detectBin && binExists(def.detectBin)) ||
82
- existsSync(def.configDir);
83
-
84
- return {
85
- id: def.id,
86
- name: def.name,
87
- configDir: def.configDir,
88
- hookFormat: def.hookFormat,
89
- installed: !!installed,
90
- hookInstalled: installed ? isHookInstalled(def.id, def.configDir, def.hookFormat) : false,
91
- };
92
- });
93
- }
30
+ const AGENT_CONFIG_DIRS: Record<string, string> = {
31
+ "claude-code": join(homedir(), ".claude"),
32
+ "cursor": join(homedir(), ".cursor"),
33
+ "opencode": join(homedir(), ".config", "opencode"),
34
+ "codex": join(homedir(), ".codex"),
35
+ "gemini": join(homedir(), ".gemini"),
36
+ };
94
37
 
95
- // ── Hook Detection ─────────────────────────────────────────────────────
38
+ const AGENT_HOOK_FORMATS: Record<string, "claude" | "cursor" | "opencode" | "codex" | "gemini"> = {
39
+ "claude-code": "claude",
40
+ "cursor": "cursor",
41
+ "opencode": "opencode",
42
+ "codex": "codex",
43
+ "gemini": "gemini",
44
+ };
96
45
 
97
- function isHookInstalled(agentId: string, configDir: string, format: string): boolean {
98
- try {
99
- switch (format) {
100
- case "claude":
101
- case "cursor": {
102
- const hookFile = join(configDir, "hooks.json");
103
- if (!existsSync(hookFile)) return false;
104
- const content = readFileSync(hookFile, "utf-8");
105
- return content.includes("jobarbiter");
106
- }
107
- case "opencode": {
108
- const pluginDir = join(configDir, "plugins");
109
- return existsSync(join(pluginDir, "jobarbiter-observer.ts"));
110
- }
111
- case "codex": {
112
- const configFile = join(configDir, "config.toml");
113
- if (!existsSync(configFile)) return false;
114
- const content = readFileSync(configFile, "utf-8");
115
- return content.includes("jobarbiter");
116
- }
117
- case "gemini": {
118
- const settingsFile = join(configDir, "settings.json");
119
- if (!existsSync(settingsFile)) return false;
120
- const content = readFileSync(settingsFile, "utf-8");
121
- return content.includes("jobarbiter");
122
- }
123
- default:
124
- return false;
125
- }
126
- } catch {
127
- return false;
128
- }
46
+ /**
47
+ * Detect agents that support observation.
48
+ * Uses the shared detect-tools module for detection.
49
+ */
50
+ export function detectAgents(): DetectedAgent[] {
51
+ const observableTools = getObservableTools();
52
+
53
+ return observableTools.map((tool) => ({
54
+ id: tool.id,
55
+ name: tool.name,
56
+ configDir: AGENT_CONFIG_DIRS[tool.id] || tool.configDir || "",
57
+ hookFormat: AGENT_HOOK_FORMATS[tool.id] || "claude",
58
+ installed: tool.installed,
59
+ hookInstalled: tool.observerActive,
60
+ }));
129
61
  }
130
62
 
131
63
  // ── Observer Data Directory ────────────────────────────────────────────
@@ -576,6 +508,53 @@ function installGeminiHook(configDir: string, scriptPath: string): void {
576
508
 
577
509
  // ── Public API ─────────────────────────────────────────────────────────
578
510
 
511
+ // ── Agent Name Mapping ─────────────────────────────────────────────────
512
+
513
+ const AGENT_NAMES: Record<string, string> = {
514
+ "claude-code": "Claude Code",
515
+ "cursor": "Cursor",
516
+ "opencode": "OpenCode",
517
+ "codex": "Codex CLI",
518
+ "gemini": "Gemini CLI",
519
+ };
520
+
521
+ /**
522
+ * Check if observer hook is installed for an agent.
523
+ */
524
+ function isHookInstalled(agentId: string, configDir: string, format: string): boolean {
525
+ try {
526
+ switch (format) {
527
+ case "claude":
528
+ case "cursor": {
529
+ const hookFile = join(configDir, "hooks.json");
530
+ if (!existsSync(hookFile)) return false;
531
+ const content = readFileSync(hookFile, "utf-8");
532
+ return content.includes("jobarbiter");
533
+ }
534
+ case "opencode": {
535
+ const pluginDir = join(configDir, "plugins");
536
+ return existsSync(join(pluginDir, "jobarbiter-observer.js"));
537
+ }
538
+ case "codex": {
539
+ const configFile = join(configDir, "config.toml");
540
+ if (!existsSync(configFile)) return false;
541
+ const content = readFileSync(configFile, "utf-8");
542
+ return content.includes("jobarbiter");
543
+ }
544
+ case "gemini": {
545
+ const settingsFile = join(configDir, "settings.json");
546
+ if (!existsSync(settingsFile)) return false;
547
+ const content = readFileSync(settingsFile, "utf-8");
548
+ return content.includes("jobarbiter");
549
+ }
550
+ default:
551
+ return false;
552
+ }
553
+ } catch {
554
+ return false;
555
+ }
556
+ }
557
+
579
558
  /**
580
559
  * Install observer hooks for the specified agents.
581
560
  * Returns a summary of what was installed.
@@ -591,40 +570,43 @@ export function installObservers(
591
570
  };
592
571
 
593
572
  for (const agentId of agentIds) {
594
- const def = AGENT_DEFINITIONS.find((d) => d.id === agentId);
595
- if (!def) {
573
+ const configDir = AGENT_CONFIG_DIRS[agentId];
574
+ const hookFormat = AGENT_HOOK_FORMATS[agentId];
575
+ const agentName = AGENT_NAMES[agentId] || agentId;
576
+
577
+ if (!configDir || !hookFormat) {
596
578
  result.errors.push({ agent: agentId, error: "Unknown agent" });
597
579
  continue;
598
580
  }
599
581
 
600
582
  // Check if already installed
601
- if (isHookInstalled(def.id, def.configDir, def.hookFormat)) {
602
- result.skipped.push(def.name);
583
+ if (isHookInstalled(agentId, configDir, hookFormat)) {
584
+ result.skipped.push(agentName);
603
585
  continue;
604
586
  }
605
587
 
606
588
  try {
607
- switch (def.hookFormat) {
589
+ switch (hookFormat) {
608
590
  case "claude":
609
- installClaudeCodeHook(def.configDir, scriptPath);
591
+ installClaudeCodeHook(configDir, scriptPath);
610
592
  break;
611
593
  case "cursor":
612
- installCursorHook(def.configDir, scriptPath);
594
+ installCursorHook(configDir, scriptPath);
613
595
  break;
614
596
  case "opencode":
615
- installOpenCodeHook(def.configDir, scriptPath);
597
+ installOpenCodeHook(configDir, scriptPath);
616
598
  break;
617
599
  case "codex":
618
- installCodexHook(def.configDir, scriptPath);
600
+ installCodexHook(configDir, scriptPath);
619
601
  break;
620
602
  case "gemini":
621
- installGeminiHook(def.configDir, scriptPath);
603
+ installGeminiHook(configDir, scriptPath);
622
604
  break;
623
605
  }
624
- result.installed.push(def.name);
606
+ result.installed.push(agentName);
625
607
  } catch (err) {
626
608
  result.errors.push({
627
- agent: def.name,
609
+ agent: agentName,
628
610
  error: err instanceof Error ? err.message : String(err),
629
611
  });
630
612
  }
@@ -640,17 +622,20 @@ export function removeObservers(agentIds: string[]): { removed: string[]; notFou
640
622
  const result = { removed: [] as string[], notFound: [] as string[] };
641
623
 
642
624
  for (const agentId of agentIds) {
643
- const def = AGENT_DEFINITIONS.find((d) => d.id === agentId);
644
- if (!def) {
625
+ const configDir = AGENT_CONFIG_DIRS[agentId];
626
+ const hookFormat = AGENT_HOOK_FORMATS[agentId];
627
+ const agentName = AGENT_NAMES[agentId] || agentId;
628
+
629
+ if (!configDir || !hookFormat) {
645
630
  result.notFound.push(agentId);
646
631
  continue;
647
632
  }
648
633
 
649
634
  try {
650
- switch (def.hookFormat) {
635
+ switch (hookFormat) {
651
636
  case "claude":
652
637
  case "cursor": {
653
- const hookFile = join(def.configDir, "hooks.json");
638
+ const hookFile = join(configDir, "hooks.json");
654
639
  if (existsSync(hookFile)) {
655
640
  const config = JSON.parse(readFileSync(hookFile, "utf-8"));
656
641
  for (const [key, hooks] of Object.entries(config.hooks || {})) {
@@ -661,24 +646,24 @@ export function removeObservers(agentIds: string[]): { removed: string[]; notFou
661
646
  }
662
647
  }
663
648
  writeFileSync(hookFile, JSON.stringify(config, null, 2) + "\n");
664
- result.removed.push(def.name);
649
+ result.removed.push(agentName);
665
650
  } else {
666
- result.notFound.push(def.name);
651
+ result.notFound.push(agentName);
667
652
  }
668
653
  break;
669
654
  }
670
655
  case "opencode": {
671
- const pluginFile = join(def.configDir, "plugins", "jobarbiter-observer.js");
656
+ const pluginFile = join(configDir, "plugins", "jobarbiter-observer.js");
672
657
  if (existsSync(pluginFile)) {
673
- unlinkSync(pluginFile);
674
- result.removed.push(def.name);
658
+ unlinkSync(pluginFile);
659
+ result.removed.push(agentName);
675
660
  } else {
676
- result.notFound.push(def.name);
661
+ result.notFound.push(agentName);
677
662
  }
678
663
  break;
679
664
  }
680
665
  case "codex": {
681
- const configFile = join(def.configDir, "config.toml");
666
+ const configFile = join(configDir, "config.toml");
682
667
  if (existsSync(configFile)) {
683
668
  let content = readFileSync(configFile, "utf-8");
684
669
  content = content
@@ -686,14 +671,14 @@ export function removeObservers(agentIds: string[]): { removed: string[]; notFou
686
671
  .filter((line) => !line.includes("jobarbiter"))
687
672
  .join("\n");
688
673
  writeFileSync(configFile, content);
689
- result.removed.push(def.name);
674
+ result.removed.push(agentName);
690
675
  } else {
691
- result.notFound.push(def.name);
676
+ result.notFound.push(agentName);
692
677
  }
693
678
  break;
694
679
  }
695
680
  case "gemini": {
696
- const settingsFile = join(def.configDir, "settings.json");
681
+ const settingsFile = join(configDir, "settings.json");
697
682
  if (existsSync(settingsFile)) {
698
683
  const settings = JSON.parse(readFileSync(settingsFile, "utf-8"));
699
684
  for (const [key, hookGroups] of Object.entries(settings.hooks || {})) {
@@ -704,15 +689,15 @@ export function removeObservers(agentIds: string[]): { removed: string[]; notFou
704
689
  }
705
690
  }
706
691
  writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
707
- result.removed.push(def.name);
692
+ result.removed.push(agentName);
708
693
  } else {
709
- result.notFound.push(def.name);
694
+ result.notFound.push(agentName);
710
695
  }
711
696
  break;
712
697
  }
713
698
  }
714
699
  } catch {
715
- result.notFound.push(def.name);
700
+ result.notFound.push(agentName);
716
701
  }
717
702
  }
718
703
 
@@ -11,7 +11,15 @@
11
11
  import * as readline from "node:readline";
12
12
  import { loadConfig, saveConfig, getConfigPath, type Config } from "./config.js";
13
13
  import { apiUnauthenticated, api, ApiError } from "./api.js";
14
- import { detectAgents, installObservers, type DetectedAgent } from "./observe.js";
14
+ import { installObservers } from "./observe.js";
15
+ import {
16
+ detectAllTools,
17
+ getInstalledTools,
18
+ getToolsNeedingObserver,
19
+ formatToolDisplay,
20
+ type DetectedTool,
21
+ type ToolCategory,
22
+ } from "./detect-tools.js";
15
23
 
16
24
  // ── ANSI Colors ────────────────────────────────────────────────────────
17
25
 
@@ -216,7 +224,9 @@ async function handleEmailVerification(
216
224
  baseUrl: string,
217
225
  userType: "worker" | "employer"
218
226
  ): Promise<{ email: string; apiKey: string; userId: string }> {
219
- const totalSteps = userType === "worker" ? 5 : 6;
227
+ // Workers: 1) Account, 2) Tool Detection, 3) Domains, 4) GitHub, 5) LinkedIn, 6) Done
228
+ // Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done
229
+ const totalSteps = 6;
220
230
 
221
231
  console.log(`\n${sym.email} ${c.bold(`Step 1/${totalSteps} — Create Your Account`)}\n`);
222
232
 
@@ -303,16 +313,13 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void>
303
313
  userType: "worker",
304
314
  };
305
315
 
306
- // Step 2: Profile Setup (tools + domains)
307
- console.log(`${sym.tools} ${c.bold("Step 2/6 Your AI Stack")}\n`);
308
-
309
- console.log(`What AI tools do you use? ${c.dim("(comma-separated)")}`);
310
- console.log(c.dim("Examples: Claude Code, Cursor, OpenClaw, ChatGPT, Copilot, Midjourney\n"));
311
- const toolsInput = await prompt.question(`${sym.arrow} `);
312
- const tools = toolsInput.split(",").map(s => s.trim()).filter(Boolean);
313
- state.tools = tools;
316
+ // Step 2: Auto-detect AI Tools
317
+ const detectedToolsResult = await runToolDetectionStep(prompt, config);
318
+ state.tools = detectedToolsResult.tools;
314
319
 
315
- console.log(`\nWhat domains do you work in? ${c.dim("(comma-separated)")}`);
320
+ // Step 3: Domains
321
+ console.log(`${sym.target} ${c.bold("Step 3/6 — Your Domains")}\n`);
322
+ console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
316
323
  console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
317
324
  const domainsInput = await prompt.question(`${sym.arrow} `);
318
325
  const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
@@ -325,7 +332,7 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void>
325
332
  await api(config, "POST", "/v1/profile", {
326
333
  domains,
327
334
  tools: {
328
- primary: tools,
335
+ primary: state.tools,
329
336
  },
330
337
  });
331
338
  console.log(`${sym.check} Profile saved\n`);
@@ -333,9 +340,6 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void>
333
340
  console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
334
341
  }
335
342
 
336
- // Step 3: Install Coding Agent Observers
337
- await runObserverStep(prompt, state, 3, 6);
338
-
339
343
  // Step 4: Connect GitHub (optional)
340
344
  console.log(`${sym.link} ${c.bold("Step 4/6 — Connect GitHub")} ${c.dim("(optional)")}\n`);
341
345
  console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
@@ -384,101 +388,133 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void>
384
388
  showWorkerCompletion(state);
385
389
  }
386
390
 
387
- // ── Observer Installation Step ─────────────────────────────────────────
391
+ // ── Tool Detection Step ────────────────────────────────────────────────
388
392
 
389
- async function runObserverStep(
393
+ async function runToolDetectionStep(
390
394
  prompt: Prompt,
391
- state: OnboardState,
392
- stepNum: number,
393
- totalSteps: number,
394
- ): Promise<void> {
395
- console.log(`🔍 ${c.bold(`Step ${stepNum}/${totalSteps} — Coding Agent Observers`)}\n`);
396
- console.log(c.dim("Scanning for coding agents...\n"));
397
-
398
- const agents = detectAgents();
399
- const detected = agents.filter((a) => a.installed);
400
- const notDetected = agents.filter((a) => !a.installed);
401
-
402
- // Show results
403
- if (detected.length === 0) {
404
- console.log(` ${c.dim("No coding agents detected on this system.")}\n`);
405
- console.log(c.dim(" You can install observers later with 'jobarbiter observe install'.\n"));
406
- return;
395
+ config: Config,
396
+ ): Promise<{ tools: string[] }> {
397
+ console.log(`🔍 ${c.bold("Step 2/6 — Detecting AI Tools")}\n`);
398
+ console.log(c.dim(" Scanning your machine...\n"));
399
+
400
+ const allTools = detectAllTools();
401
+ const installed = allTools.filter((t) => t.installed);
402
+ const notInstalled = allTools.filter((t) => !t.installed && t.category === "coding-agent");
403
+
404
+ // Group by category
405
+ const codingAgents = installed.filter((t) => t.category === "coding-agent");
406
+ const chatTools = installed.filter((t) => t.category === "chat");
407
+ const orchestration = installed.filter((t) => t.category === "orchestration");
408
+ const apiProviders = installed.filter((t) => t.category === "api-provider");
409
+
410
+ // Display found tools
411
+ if (installed.length === 0) {
412
+ console.log(` ${c.dim("No AI tools detected on this system.")}\n`);
413
+ console.log(c.dim(" You can add tools later with 'jobarbiter observe install'.\n"));
414
+ return { tools: [] };
407
415
  }
408
416
 
409
- console.log(` Found on your system:`);
410
- for (const agent of detected) {
411
- if (agent.hookInstalled) {
412
- console.log(` ${sym.check} ${agent.name} ${c.dim("(observer already installed)")}`);
417
+ console.log(` ${c.bold("Found:")}`);
418
+
419
+ // Show coding agents with observer status
420
+ for (const tool of codingAgents) {
421
+ const display = formatToolDisplay(tool);
422
+ if (tool.observerAvailable) {
423
+ if (tool.observerActive) {
424
+ console.log(` ${sym.check} ${display} ${c.dim("(observer active)")}`);
425
+ } else {
426
+ console.log(` ${sym.check} ${display} ${c.success("(observer available)")}`);
427
+ }
413
428
  } else {
414
- console.log(` ${c.success("")} ${agent.name}`);
429
+ console.log(` ${sym.check} ${display} ${c.dim("(detected)")}`);
415
430
  }
416
431
  }
417
- for (const agent of notDetected) {
418
- console.log(` ${c.dim("⬚")} ${agent.name} ${c.dim("(not found)")}`);
432
+
433
+ // Show other tools
434
+ for (const tool of chatTools) {
435
+ console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
436
+ }
437
+ for (const tool of orchestration) {
438
+ console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
439
+ }
440
+ for (const tool of apiProviders) {
441
+ console.log(` ${sym.check} ${tool.name} ${c.dim("configured")}`);
419
442
  }
420
443
 
421
- // Filter to agents that need installation
422
- const needsInstall = detected.filter((a) => !a.hookInstalled);
423
-
424
- if (needsInstall.length === 0) {
425
- console.log(`\n ${c.dim("All detected agents already have observers installed.")}\n`);
426
- return;
444
+ // Show not-detected coding agents
445
+ if (notInstalled.length > 0) {
446
+ console.log(`\n ${c.dim("Not detected (install to track):")}`);
447
+ for (const tool of notInstalled.slice(0, 5)) {
448
+ console.log(` ${c.dim("")} ${tool.name}`);
449
+ }
450
+ if (notInstalled.length > 5) {
451
+ console.log(` ${c.dim(`... and ${notInstalled.length - 5} more`)}`);
452
+ }
427
453
  }
428
454
 
429
- console.log(`\n JobArbiter observes your coding sessions to build your`);
430
- console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
431
- console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
432
- console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
433
- console.log(c.dim(` Review anytime: jobarbiter observe status\n`));
455
+ // Collect tool names for profile
456
+ const toolNames = installed.map((t) => t.name);
434
457
 
435
- const installAll = await prompt.confirm(
436
- ` Install observers for all ${needsInstall.length} detected agent${needsInstall.length > 1 ? "s" : ""}?`,
437
- );
458
+ // Observer installation for coding agents
459
+ const needsObserver = codingAgents.filter((t) => t.observerAvailable && !t.observerActive);
438
460
 
439
- let toInstall: string[];
461
+ if (needsObserver.length > 0) {
462
+ console.log(`\n ${c.bold("Observers")}`);
463
+ console.log(` JobArbiter observes your coding sessions to build your`);
464
+ console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
465
+ console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
466
+ console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
467
+ console.log(c.dim(` Review anytime: jobarbiter observe status\n`));
440
468
 
441
- if (installAll) {
442
- toInstall = needsInstall.map((a) => a.id);
443
- } else {
444
- // Let user select individually
445
- console.log(`\n Select which agents to observe:\n`);
446
- const selections: Record<string, boolean> = {};
447
-
448
- for (const agent of needsInstall) {
449
- selections[agent.id] = await prompt.confirm(` ${agent.name}?`, true);
450
- }
469
+ const observerNames = needsObserver.map((t) => t.name).join(", ");
470
+ const installAll = await prompt.confirm(
471
+ ` Install observers for detected tools? (${observerNames})`,
472
+ );
451
473
 
452
- toInstall = Object.entries(selections)
453
- .filter(([, v]) => v)
454
- .map(([k]) => k);
474
+ if (installAll) {
475
+ const toInstall = needsObserver.map((t) => t.id);
476
+ console.log(c.dim("\n Installing observers..."));
477
+ const result = installObservers(toInstall);
455
478
 
456
- if (toInstall.length === 0) {
457
- console.log(`\n ${c.dim("No observers installed. You can add them later with 'jobarbiter observe install'.")}\n`);
458
- return;
459
- }
460
- }
461
-
462
- // Install
463
- console.log(c.dim("\n Installing observers..."));
464
- const result = installObservers(toInstall);
479
+ for (const name of result.installed) {
480
+ console.log(` ${sym.check} ${name}`);
481
+ }
482
+ for (const name of result.skipped) {
483
+ console.log(` ${c.dim("—")} ${name} ${c.dim("(already installed)")}`);
484
+ }
485
+ for (const { agent, error: errMsg } of result.errors) {
486
+ console.log(` ${sym.cross} ${agent}: ${c.error(errMsg)}`);
487
+ }
465
488
 
466
- for (const name of result.installed) {
467
- console.log(` ${sym.check} ${name}`);
468
- }
469
- for (const name of result.skipped) {
470
- console.log(` ${c.dim("—")} ${name} ${c.dim("(already installed)")}`);
471
- }
472
- for (const { agent, error: errMsg } of result.errors) {
473
- console.log(` ${sym.cross} ${agent}: ${c.error(errMsg)}`);
489
+ if (result.installed.length > 0) {
490
+ console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
491
+ console.log(c.dim(` Your proficiency profile will start building automatically.\n`));
492
+ }
493
+ } else {
494
+ console.log(c.dim("\n Skipped — you can install observers later with 'jobarbiter observe install'.\n"));
495
+ }
496
+ } else if (codingAgents.length > 0) {
497
+ const hasActiveObservers = codingAgents.some((t) => t.observerActive);
498
+ if (hasActiveObservers) {
499
+ console.log(`\n ${c.dim("All detected agents already have observers installed.")}\n`);
500
+ } else {
501
+ console.log();
502
+ }
474
503
  }
475
504
 
476
- if (result.installed.length > 0) {
477
- console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
478
- console.log(c.dim(` Your proficiency profile will start building automatically.\n`));
505
+ // "Did we miss anything?" prompt
506
+ console.log(` ${c.dim("Did we miss anything?")}`);
507
+ const additionalTools = await prompt.question(` Other AI tools you use ${c.dim("(comma-separated, or press Enter)")}: `);
508
+
509
+ if (additionalTools.trim()) {
510
+ const additional = additionalTools.split(",").map((s) => s.trim()).filter(Boolean);
511
+ toolNames.push(...additional);
512
+ console.log(` ${sym.check} Added: ${additional.join(", ")}\n`);
479
513
  } else {
480
514
  console.log();
481
515
  }
516
+
517
+ return { tools: toolNames };
482
518
  }
483
519
 
484
520
  function showWorkerCompletion(state: OnboardState): void {