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.
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/detect-tools.d.ts +46 -0
- package/dist/lib/detect-tools.js +473 -0
- package/dist/lib/observe.d.ts +6 -2
- package/dist/lib/observe.js +111 -129
- package/dist/lib/onboard.js +211 -127
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/lib/config.ts +2 -0
- package/src/lib/detect-tools.ts +526 -0
- package/src/lib/observe.ts +116 -131
- package/src/lib/onboard.ts +229 -143
package/src/lib/observe.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JobArbiter Observer — Hook installer for coding agent CLIs
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
9
|
+
import { join } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import {
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
595
|
-
|
|
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(
|
|
602
|
-
result.skipped.push(
|
|
583
|
+
if (isHookInstalled(agentId, configDir, hookFormat)) {
|
|
584
|
+
result.skipped.push(agentName);
|
|
603
585
|
continue;
|
|
604
586
|
}
|
|
605
587
|
|
|
606
588
|
try {
|
|
607
|
-
switch (
|
|
589
|
+
switch (hookFormat) {
|
|
608
590
|
case "claude":
|
|
609
|
-
installClaudeCodeHook(
|
|
591
|
+
installClaudeCodeHook(configDir, scriptPath);
|
|
610
592
|
break;
|
|
611
593
|
case "cursor":
|
|
612
|
-
installCursorHook(
|
|
594
|
+
installCursorHook(configDir, scriptPath);
|
|
613
595
|
break;
|
|
614
596
|
case "opencode":
|
|
615
|
-
installOpenCodeHook(
|
|
597
|
+
installOpenCodeHook(configDir, scriptPath);
|
|
616
598
|
break;
|
|
617
599
|
case "codex":
|
|
618
|
-
installCodexHook(
|
|
600
|
+
installCodexHook(configDir, scriptPath);
|
|
619
601
|
break;
|
|
620
602
|
case "gemini":
|
|
621
|
-
installGeminiHook(
|
|
603
|
+
installGeminiHook(configDir, scriptPath);
|
|
622
604
|
break;
|
|
623
605
|
}
|
|
624
|
-
result.installed.push(
|
|
606
|
+
result.installed.push(agentName);
|
|
625
607
|
} catch (err) {
|
|
626
608
|
result.errors.push({
|
|
627
|
-
agent:
|
|
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
|
|
644
|
-
|
|
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 (
|
|
635
|
+
switch (hookFormat) {
|
|
651
636
|
case "claude":
|
|
652
637
|
case "cursor": {
|
|
653
|
-
const hookFile = join(
|
|
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(
|
|
649
|
+
result.removed.push(agentName);
|
|
665
650
|
} else {
|
|
666
|
-
result.notFound.push(
|
|
651
|
+
result.notFound.push(agentName);
|
|
667
652
|
}
|
|
668
653
|
break;
|
|
669
654
|
}
|
|
670
655
|
case "opencode": {
|
|
671
|
-
const pluginFile = join(
|
|
656
|
+
const pluginFile = join(configDir, "plugins", "jobarbiter-observer.js");
|
|
672
657
|
if (existsSync(pluginFile)) {
|
|
673
|
-
|
|
674
|
-
result.removed.push(
|
|
658
|
+
unlinkSync(pluginFile);
|
|
659
|
+
result.removed.push(agentName);
|
|
675
660
|
} else {
|
|
676
|
-
result.notFound.push(
|
|
661
|
+
result.notFound.push(agentName);
|
|
677
662
|
}
|
|
678
663
|
break;
|
|
679
664
|
}
|
|
680
665
|
case "codex": {
|
|
681
|
-
const configFile = join(
|
|
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(
|
|
674
|
+
result.removed.push(agentName);
|
|
690
675
|
} else {
|
|
691
|
-
result.notFound.push(
|
|
676
|
+
result.notFound.push(agentName);
|
|
692
677
|
}
|
|
693
678
|
break;
|
|
694
679
|
}
|
|
695
680
|
case "gemini": {
|
|
696
|
-
const settingsFile = join(
|
|
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(
|
|
692
|
+
result.removed.push(agentName);
|
|
708
693
|
} else {
|
|
709
|
-
result.notFound.push(
|
|
694
|
+
result.notFound.push(agentName);
|
|
710
695
|
}
|
|
711
696
|
break;
|
|
712
697
|
}
|
|
713
698
|
}
|
|
714
699
|
} catch {
|
|
715
|
-
result.notFound.push(
|
|
700
|
+
result.notFound.push(agentName);
|
|
716
701
|
}
|
|
717
702
|
}
|
|
718
703
|
|