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,112 +1,42 @@
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
  import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { homedir } from "node:os";
10
- import { execSync } from "node:child_process";
11
- // ── Agent Detection ────────────────────────────────────────────────────
12
- const AGENT_DEFINITIONS = [
13
- {
14
- id: "claude-code",
15
- name: "Claude Code",
16
- configDir: join(homedir(), ".claude"),
17
- hookFormat: "claude",
18
- detectBin: "claude",
19
- },
20
- {
21
- id: "cursor",
22
- name: "Cursor",
23
- configDir: join(homedir(), ".cursor"),
24
- hookFormat: "cursor",
25
- detectBin: null, // Cursor is an app, not a CLI
26
- detectDir: join(homedir(), ".cursor"),
27
- },
28
- {
29
- id: "opencode",
30
- name: "OpenCode",
31
- configDir: join(homedir(), ".config", "opencode"),
32
- hookFormat: "opencode",
33
- detectBin: "opencode",
34
- },
35
- {
36
- id: "codex",
37
- name: "Codex CLI",
38
- configDir: join(homedir(), ".codex"),
39
- hookFormat: "codex",
40
- detectBin: "codex",
41
- },
42
- {
43
- id: "gemini",
44
- name: "Gemini CLI",
45
- configDir: join(homedir(), ".gemini"),
46
- hookFormat: "gemini",
47
- detectBin: "gemini",
48
- },
49
- ];
50
- function binExists(name) {
51
- try {
52
- execSync(`which ${name}`, { stdio: "ignore" });
53
- return true;
54
- }
55
- catch {
56
- return false;
57
- }
58
- }
10
+ import { getObservableTools } from "./detect-tools.js";
11
+ // ── Agent Config Directories ───────────────────────────────────────────
12
+ const AGENT_CONFIG_DIRS = {
13
+ "claude-code": join(homedir(), ".claude"),
14
+ "cursor": join(homedir(), ".cursor"),
15
+ "opencode": join(homedir(), ".config", "opencode"),
16
+ "codex": join(homedir(), ".codex"),
17
+ "gemini": join(homedir(), ".gemini"),
18
+ };
19
+ const AGENT_HOOK_FORMATS = {
20
+ "claude-code": "claude",
21
+ "cursor": "cursor",
22
+ "opencode": "opencode",
23
+ "codex": "codex",
24
+ "gemini": "gemini",
25
+ };
26
+ /**
27
+ * Detect agents that support observation.
28
+ * Uses the shared detect-tools module for detection.
29
+ */
59
30
  export function detectAgents() {
60
- return AGENT_DEFINITIONS.map((def) => {
61
- const installed = (def.detectBin && binExists(def.detectBin)) ||
62
- existsSync(def.configDir);
63
- return {
64
- id: def.id,
65
- name: def.name,
66
- configDir: def.configDir,
67
- hookFormat: def.hookFormat,
68
- installed: !!installed,
69
- hookInstalled: installed ? isHookInstalled(def.id, def.configDir, def.hookFormat) : false,
70
- };
71
- });
72
- }
73
- // ── Hook Detection ─────────────────────────────────────────────────────
74
- function isHookInstalled(agentId, configDir, format) {
75
- try {
76
- switch (format) {
77
- case "claude":
78
- case "cursor": {
79
- const hookFile = join(configDir, "hooks.json");
80
- if (!existsSync(hookFile))
81
- return false;
82
- const content = readFileSync(hookFile, "utf-8");
83
- return content.includes("jobarbiter");
84
- }
85
- case "opencode": {
86
- const pluginDir = join(configDir, "plugins");
87
- return existsSync(join(pluginDir, "jobarbiter-observer.ts"));
88
- }
89
- case "codex": {
90
- const configFile = join(configDir, "config.toml");
91
- if (!existsSync(configFile))
92
- return false;
93
- const content = readFileSync(configFile, "utf-8");
94
- return content.includes("jobarbiter");
95
- }
96
- case "gemini": {
97
- const settingsFile = join(configDir, "settings.json");
98
- if (!existsSync(settingsFile))
99
- return false;
100
- const content = readFileSync(settingsFile, "utf-8");
101
- return content.includes("jobarbiter");
102
- }
103
- default:
104
- return false;
105
- }
106
- }
107
- catch {
108
- return false;
109
- }
31
+ const observableTools = getObservableTools();
32
+ return observableTools.map((tool) => ({
33
+ id: tool.id,
34
+ name: tool.name,
35
+ configDir: AGENT_CONFIG_DIRS[tool.id] || tool.configDir || "",
36
+ hookFormat: AGENT_HOOK_FORMATS[tool.id] || "claude",
37
+ installed: tool.installed,
38
+ hookInstalled: tool.observerActive,
39
+ }));
110
40
  }
111
41
  // ── Observer Data Directory ────────────────────────────────────────────
112
42
  const OBSERVER_DIR = join(homedir(), ".config", "jobarbiter", "observer");
@@ -519,6 +449,54 @@ function installGeminiHook(configDir, scriptPath) {
519
449
  writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
520
450
  }
521
451
  // ── Public API ─────────────────────────────────────────────────────────
452
+ // ── Agent Name Mapping ─────────────────────────────────────────────────
453
+ const AGENT_NAMES = {
454
+ "claude-code": "Claude Code",
455
+ "cursor": "Cursor",
456
+ "opencode": "OpenCode",
457
+ "codex": "Codex CLI",
458
+ "gemini": "Gemini CLI",
459
+ };
460
+ /**
461
+ * Check if observer hook is installed for an agent.
462
+ */
463
+ function isHookInstalled(agentId, configDir, format) {
464
+ try {
465
+ switch (format) {
466
+ case "claude":
467
+ case "cursor": {
468
+ const hookFile = join(configDir, "hooks.json");
469
+ if (!existsSync(hookFile))
470
+ return false;
471
+ const content = readFileSync(hookFile, "utf-8");
472
+ return content.includes("jobarbiter");
473
+ }
474
+ case "opencode": {
475
+ const pluginDir = join(configDir, "plugins");
476
+ return existsSync(join(pluginDir, "jobarbiter-observer.js"));
477
+ }
478
+ case "codex": {
479
+ const configFile = join(configDir, "config.toml");
480
+ if (!existsSync(configFile))
481
+ return false;
482
+ const content = readFileSync(configFile, "utf-8");
483
+ return content.includes("jobarbiter");
484
+ }
485
+ case "gemini": {
486
+ const settingsFile = join(configDir, "settings.json");
487
+ if (!existsSync(settingsFile))
488
+ return false;
489
+ const content = readFileSync(settingsFile, "utf-8");
490
+ return content.includes("jobarbiter");
491
+ }
492
+ default:
493
+ return false;
494
+ }
495
+ }
496
+ catch {
497
+ return false;
498
+ }
499
+ }
522
500
  /**
523
501
  * Install observer hooks for the specified agents.
524
502
  * Returns a summary of what was installed.
@@ -531,39 +509,41 @@ export function installObservers(agentIds) {
531
509
  errors: [],
532
510
  };
533
511
  for (const agentId of agentIds) {
534
- const def = AGENT_DEFINITIONS.find((d) => d.id === agentId);
535
- if (!def) {
512
+ const configDir = AGENT_CONFIG_DIRS[agentId];
513
+ const hookFormat = AGENT_HOOK_FORMATS[agentId];
514
+ const agentName = AGENT_NAMES[agentId] || agentId;
515
+ if (!configDir || !hookFormat) {
536
516
  result.errors.push({ agent: agentId, error: "Unknown agent" });
537
517
  continue;
538
518
  }
539
519
  // Check if already installed
540
- if (isHookInstalled(def.id, def.configDir, def.hookFormat)) {
541
- result.skipped.push(def.name);
520
+ if (isHookInstalled(agentId, configDir, hookFormat)) {
521
+ result.skipped.push(agentName);
542
522
  continue;
543
523
  }
544
524
  try {
545
- switch (def.hookFormat) {
525
+ switch (hookFormat) {
546
526
  case "claude":
547
- installClaudeCodeHook(def.configDir, scriptPath);
527
+ installClaudeCodeHook(configDir, scriptPath);
548
528
  break;
549
529
  case "cursor":
550
- installCursorHook(def.configDir, scriptPath);
530
+ installCursorHook(configDir, scriptPath);
551
531
  break;
552
532
  case "opencode":
553
- installOpenCodeHook(def.configDir, scriptPath);
533
+ installOpenCodeHook(configDir, scriptPath);
554
534
  break;
555
535
  case "codex":
556
- installCodexHook(def.configDir, scriptPath);
536
+ installCodexHook(configDir, scriptPath);
557
537
  break;
558
538
  case "gemini":
559
- installGeminiHook(def.configDir, scriptPath);
539
+ installGeminiHook(configDir, scriptPath);
560
540
  break;
561
541
  }
562
- result.installed.push(def.name);
542
+ result.installed.push(agentName);
563
543
  }
564
544
  catch (err) {
565
545
  result.errors.push({
566
- agent: def.name,
546
+ agent: agentName,
567
547
  error: err instanceof Error ? err.message : String(err),
568
548
  });
569
549
  }
@@ -576,16 +556,18 @@ export function installObservers(agentIds) {
576
556
  export function removeObservers(agentIds) {
577
557
  const result = { removed: [], notFound: [] };
578
558
  for (const agentId of agentIds) {
579
- const def = AGENT_DEFINITIONS.find((d) => d.id === agentId);
580
- if (!def) {
559
+ const configDir = AGENT_CONFIG_DIRS[agentId];
560
+ const hookFormat = AGENT_HOOK_FORMATS[agentId];
561
+ const agentName = AGENT_NAMES[agentId] || agentId;
562
+ if (!configDir || !hookFormat) {
581
563
  result.notFound.push(agentId);
582
564
  continue;
583
565
  }
584
566
  try {
585
- switch (def.hookFormat) {
567
+ switch (hookFormat) {
586
568
  case "claude":
587
569
  case "cursor": {
588
- const hookFile = join(def.configDir, "hooks.json");
570
+ const hookFile = join(configDir, "hooks.json");
589
571
  if (existsSync(hookFile)) {
590
572
  const config = JSON.parse(readFileSync(hookFile, "utf-8"));
591
573
  for (const [key, hooks] of Object.entries(config.hooks || {})) {
@@ -594,26 +576,26 @@ export function removeObservers(agentIds) {
594
576
  }
595
577
  }
596
578
  writeFileSync(hookFile, JSON.stringify(config, null, 2) + "\n");
597
- result.removed.push(def.name);
579
+ result.removed.push(agentName);
598
580
  }
599
581
  else {
600
- result.notFound.push(def.name);
582
+ result.notFound.push(agentName);
601
583
  }
602
584
  break;
603
585
  }
604
586
  case "opencode": {
605
- const pluginFile = join(def.configDir, "plugins", "jobarbiter-observer.js");
587
+ const pluginFile = join(configDir, "plugins", "jobarbiter-observer.js");
606
588
  if (existsSync(pluginFile)) {
607
589
  unlinkSync(pluginFile);
608
- result.removed.push(def.name);
590
+ result.removed.push(agentName);
609
591
  }
610
592
  else {
611
- result.notFound.push(def.name);
593
+ result.notFound.push(agentName);
612
594
  }
613
595
  break;
614
596
  }
615
597
  case "codex": {
616
- const configFile = join(def.configDir, "config.toml");
598
+ const configFile = join(configDir, "config.toml");
617
599
  if (existsSync(configFile)) {
618
600
  let content = readFileSync(configFile, "utf-8");
619
601
  content = content
@@ -621,15 +603,15 @@ export function removeObservers(agentIds) {
621
603
  .filter((line) => !line.includes("jobarbiter"))
622
604
  .join("\n");
623
605
  writeFileSync(configFile, content);
624
- result.removed.push(def.name);
606
+ result.removed.push(agentName);
625
607
  }
626
608
  else {
627
- result.notFound.push(def.name);
609
+ result.notFound.push(agentName);
628
610
  }
629
611
  break;
630
612
  }
631
613
  case "gemini": {
632
- const settingsFile = join(def.configDir, "settings.json");
614
+ const settingsFile = join(configDir, "settings.json");
633
615
  if (existsSync(settingsFile)) {
634
616
  const settings = JSON.parse(readFileSync(settingsFile, "utf-8"));
635
617
  for (const [key, hookGroups] of Object.entries(settings.hooks || {})) {
@@ -638,17 +620,17 @@ export function removeObservers(agentIds) {
638
620
  }
639
621
  }
640
622
  writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
641
- result.removed.push(def.name);
623
+ result.removed.push(agentName);
642
624
  }
643
625
  else {
644
- result.notFound.push(def.name);
626
+ result.notFound.push(agentName);
645
627
  }
646
628
  break;
647
629
  }
648
630
  }
649
631
  }
650
632
  catch {
651
- result.notFound.push(def.name);
633
+ result.notFound.push(agentName);
652
634
  }
653
635
  }
654
636
  return result;
@@ -10,7 +10,8 @@
10
10
  import * as readline from "node:readline";
11
11
  import { loadConfig, saveConfig, getConfigPath } from "./config.js";
12
12
  import { apiUnauthenticated, api, ApiError } from "./api.js";
13
- import { detectAgents, installObservers } from "./observe.js";
13
+ import { installObservers } from "./observe.js";
14
+ import { detectAllTools, formatToolDisplay, } from "./detect-tools.js";
14
15
  // ── ANSI Colors ────────────────────────────────────────────────────────
15
16
  const colors = {
16
17
  reset: "\x1b[0m",
@@ -164,7 +165,9 @@ async function selectUserType(prompt) {
164
165
  }
165
166
  // ── Email & Verification ───────────────────────────────────────────────
166
167
  async function handleEmailVerification(prompt, baseUrl, userType) {
167
- const totalSteps = userType === "worker" ? 5 : 6;
168
+ // Workers: 1) Account, 2) Tool Detection, 3) Domains, 4) GitHub, 5) LinkedIn, 6) Done
169
+ // Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done
170
+ const totalSteps = 6;
168
171
  console.log(`\n${sym.email} ${c.bold(`Step 1/${totalSteps} — Create Your Account`)}\n`);
169
172
  // Get email
170
173
  let email;
@@ -240,14 +243,12 @@ async function runWorkerFlow(prompt, state) {
240
243
  baseUrl: state.baseUrl,
241
244
  userType: "worker",
242
245
  };
243
- // Step 2: Profile Setup (tools + domains)
244
- console.log(`${sym.tools} ${c.bold("Step 2/6 Your AI Stack")}\n`);
245
- console.log(`What AI tools do you use? ${c.dim("(comma-separated)")}`);
246
- console.log(c.dim("Examples: Claude Code, Cursor, OpenClaw, ChatGPT, Copilot, Midjourney\n"));
247
- const toolsInput = await prompt.question(`${sym.arrow} `);
248
- const tools = toolsInput.split(",").map(s => s.trim()).filter(Boolean);
249
- state.tools = tools;
250
- console.log(`\nWhat domains do you work in? ${c.dim("(comma-separated)")}`);
246
+ // Step 2: Auto-detect AI Tools
247
+ const detectedToolsResult = await runToolDetectionStep(prompt, config);
248
+ state.tools = detectedToolsResult.tools;
249
+ // Step 3: Domains
250
+ console.log(`${sym.target} ${c.bold("Step 3/6 — Your Domains")}\n`);
251
+ console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
251
252
  console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
252
253
  const domainsInput = await prompt.question(`${sym.arrow} `);
253
254
  const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
@@ -258,7 +259,7 @@ async function runWorkerFlow(prompt, state) {
258
259
  await api(config, "POST", "/v1/profile", {
259
260
  domains,
260
261
  tools: {
261
- primary: tools,
262
+ primary: state.tools,
262
263
  },
263
264
  });
264
265
  console.log(`${sym.check} Profile saved\n`);
@@ -266,8 +267,6 @@ async function runWorkerFlow(prompt, state) {
266
267
  catch (err) {
267
268
  console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
268
269
  }
269
- // Step 3: Install Coding Agent Observers
270
- await runObserverStep(prompt, state, 3, 6);
271
270
  // Step 4: Connect GitHub (optional)
272
271
  console.log(`${sym.link} ${c.bold("Step 4/6 — Connect GitHub")} ${c.dim("(optional)")}\n`);
273
272
  console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
@@ -313,81 +312,116 @@ async function runWorkerFlow(prompt, state) {
313
312
  // Step 6: Done!
314
313
  showWorkerCompletion(state);
315
314
  }
316
- // ── Observer Installation Step ─────────────────────────────────────────
317
- async function runObserverStep(prompt, state, stepNum, totalSteps) {
318
- console.log(`🔍 ${c.bold(`Step ${stepNum}/${totalSteps}Coding Agent Observers`)}\n`);
319
- console.log(c.dim("Scanning for coding agents...\n"));
320
- const agents = detectAgents();
321
- const detected = agents.filter((a) => a.installed);
322
- const notDetected = agents.filter((a) => !a.installed);
323
- // Show results
324
- if (detected.length === 0) {
325
- console.log(` ${c.dim("No coding agents detected on this system.")}\n`);
326
- console.log(c.dim(" You can install observers later with 'jobarbiter observe install'.\n"));
327
- return;
328
- }
329
- console.log(` Found on your system:`);
330
- for (const agent of detected) {
331
- if (agent.hookInstalled) {
332
- console.log(` ${sym.check} ${agent.name} ${c.dim("(observer already installed)")}`);
315
+ // ── Tool Detection Step ────────────────────────────────────────────────
316
+ async function runToolDetectionStep(prompt, config) {
317
+ console.log(`🔍 ${c.bold("Step 2/6Detecting AI Tools")}\n`);
318
+ console.log(c.dim(" Scanning your machine...\n"));
319
+ const allTools = detectAllTools();
320
+ const installed = allTools.filter((t) => t.installed);
321
+ const notInstalled = allTools.filter((t) => !t.installed && t.category === "coding-agent");
322
+ // Group by category
323
+ const codingAgents = installed.filter((t) => t.category === "coding-agent");
324
+ const chatTools = installed.filter((t) => t.category === "chat");
325
+ const orchestration = installed.filter((t) => t.category === "orchestration");
326
+ const apiProviders = installed.filter((t) => t.category === "api-provider");
327
+ // Display found tools
328
+ if (installed.length === 0) {
329
+ console.log(` ${c.dim("No AI tools detected on this system.")}\n`);
330
+ console.log(c.dim(" You can add tools later with 'jobarbiter observe install'.\n"));
331
+ return { tools: [] };
332
+ }
333
+ console.log(` ${c.bold("Found:")}`);
334
+ // Show coding agents with observer status
335
+ for (const tool of codingAgents) {
336
+ const display = formatToolDisplay(tool);
337
+ if (tool.observerAvailable) {
338
+ if (tool.observerActive) {
339
+ console.log(` ${sym.check} ${display} ${c.dim("(observer active)")}`);
340
+ }
341
+ else {
342
+ console.log(` ${sym.check} ${display} ${c.success("(observer available)")}`);
343
+ }
333
344
  }
334
345
  else {
335
- console.log(` ${c.success("")} ${agent.name}`);
346
+ console.log(` ${sym.check} ${display} ${c.dim("(detected)")}`);
336
347
  }
337
348
  }
338
- for (const agent of notDetected) {
339
- console.log(` ${c.dim("⬚")} ${agent.name} ${c.dim("(not found)")}`);
340
- }
341
- // Filter to agents that need installation
342
- const needsInstall = detected.filter((a) => !a.hookInstalled);
343
- if (needsInstall.length === 0) {
344
- console.log(`\n ${c.dim("All detected agents already have observers installed.")}\n`);
345
- return;
346
- }
347
- console.log(`\n JobArbiter observes your coding sessions to build your`);
348
- console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
349
- console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
350
- console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
351
- console.log(c.dim(` Review anytime: jobarbiter observe status\n`));
352
- const installAll = await prompt.confirm(` Install observers for all ${needsInstall.length} detected agent${needsInstall.length > 1 ? "s" : ""}?`);
353
- let toInstall;
354
- if (installAll) {
355
- toInstall = needsInstall.map((a) => a.id);
349
+ // Show other tools
350
+ for (const tool of chatTools) {
351
+ console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
356
352
  }
357
- else {
358
- // Let user select individually
359
- console.log(`\n Select which agents to observe:\n`);
360
- const selections = {};
361
- for (const agent of needsInstall) {
362
- selections[agent.id] = await prompt.confirm(` ${agent.name}?`, true);
353
+ for (const tool of orchestration) {
354
+ console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
355
+ }
356
+ for (const tool of apiProviders) {
357
+ console.log(` ${sym.check} ${tool.name} ${c.dim("configured")}`);
358
+ }
359
+ // Show not-detected coding agents
360
+ if (notInstalled.length > 0) {
361
+ console.log(`\n ${c.dim("Not detected (install to track):")}`);
362
+ for (const tool of notInstalled.slice(0, 5)) {
363
+ console.log(` ${c.dim("⬚")} ${tool.name}`);
363
364
  }
364
- toInstall = Object.entries(selections)
365
- .filter(([, v]) => v)
366
- .map(([k]) => k);
367
- if (toInstall.length === 0) {
368
- console.log(`\n ${c.dim("No observers installed. You can add them later with 'jobarbiter observe install'.")}\n`);
369
- return;
365
+ if (notInstalled.length > 5) {
366
+ console.log(` ${c.dim(`... and ${notInstalled.length - 5} more`)}`);
370
367
  }
371
368
  }
372
- // Install
373
- console.log(c.dim("\n Installing observers..."));
374
- const result = installObservers(toInstall);
375
- for (const name of result.installed) {
376
- console.log(` ${sym.check} ${name}`);
377
- }
378
- for (const name of result.skipped) {
379
- console.log(` ${c.dim("—")} ${name} ${c.dim("(already installed)")}`);
369
+ // Collect tool names for profile
370
+ const toolNames = installed.map((t) => t.name);
371
+ // Observer installation for coding agents
372
+ const needsObserver = codingAgents.filter((t) => t.observerAvailable && !t.observerActive);
373
+ if (needsObserver.length > 0) {
374
+ console.log(`\n ${c.bold("Observers")}`);
375
+ console.log(` JobArbiter observes your coding sessions to build your`);
376
+ console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
377
+ console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
378
+ console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
379
+ console.log(c.dim(` Review anytime: jobarbiter observe status\n`));
380
+ const observerNames = needsObserver.map((t) => t.name).join(", ");
381
+ const installAll = await prompt.confirm(` Install observers for detected tools? (${observerNames})`);
382
+ if (installAll) {
383
+ const toInstall = needsObserver.map((t) => t.id);
384
+ console.log(c.dim("\n Installing observers..."));
385
+ const result = installObservers(toInstall);
386
+ for (const name of result.installed) {
387
+ console.log(` ${sym.check} ${name}`);
388
+ }
389
+ for (const name of result.skipped) {
390
+ console.log(` ${c.dim("—")} ${name} ${c.dim("(already installed)")}`);
391
+ }
392
+ for (const { agent, error: errMsg } of result.errors) {
393
+ console.log(` ${sym.cross} ${agent}: ${c.error(errMsg)}`);
394
+ }
395
+ if (result.installed.length > 0) {
396
+ console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
397
+ console.log(c.dim(` Your proficiency profile will start building automatically.\n`));
398
+ }
399
+ }
400
+ else {
401
+ console.log(c.dim("\n Skipped — you can install observers later with 'jobarbiter observe install'.\n"));
402
+ }
380
403
  }
381
- for (const { agent, error: errMsg } of result.errors) {
382
- console.log(` ${sym.cross} ${agent}: ${c.error(errMsg)}`);
404
+ else if (codingAgents.length > 0) {
405
+ const hasActiveObservers = codingAgents.some((t) => t.observerActive);
406
+ if (hasActiveObservers) {
407
+ console.log(`\n ${c.dim("All detected agents already have observers installed.")}\n`);
408
+ }
409
+ else {
410
+ console.log();
411
+ }
383
412
  }
384
- if (result.installed.length > 0) {
385
- console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
386
- console.log(c.dim(` Your proficiency profile will start building automatically.\n`));
413
+ // "Did we miss anything?" prompt
414
+ console.log(` ${c.dim("Did we miss anything?")}`);
415
+ const additionalTools = await prompt.question(` Other AI tools you use ${c.dim("(comma-separated, or press Enter)")}: `);
416
+ if (additionalTools.trim()) {
417
+ const additional = additionalTools.split(",").map((s) => s.trim()).filter(Boolean);
418
+ toolNames.push(...additional);
419
+ console.log(` ${sym.check} Added: ${additional.join(", ")}\n`);
387
420
  }
388
421
  else {
389
422
  console.log();
390
423
  }
424
+ return { tools: toolNames };
391
425
  }
392
426
  function showWorkerCompletion(state) {
393
427
  console.log(`${sym.done} ${c.bold("Step 6/6 — You're In!")}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobarbiter",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "CLI for JobArbiter — the first AI Proficiency Marketplace",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import { api, apiUnauthenticated, ApiError } from "./lib/api.js";
6
6
  import { output, outputList, success, error, setJsonMode } from "./lib/output.js";
7
7
  import { runOnboardWizard } from "./lib/onboard.js";
8
8
  import { detectAgents, installObservers, removeObservers, getObservationStatus } from "./lib/observe.js";
9
+ import { getObservableTools, formatToolDisplay } from "./lib/detect-tools.js";
9
10
 
10
11
  const program = new Command();
11
12