jobarbiter 0.3.1 → 0.3.3

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