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,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;