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/dist/lib/observe.js
CHANGED
|
@@ -1,112 +1,42 @@
|
|
|
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
|
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 {
|
|
11
|
-
// ── Agent
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
535
|
-
|
|
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(
|
|
541
|
-
result.skipped.push(
|
|
520
|
+
if (isHookInstalled(agentId, configDir, hookFormat)) {
|
|
521
|
+
result.skipped.push(agentName);
|
|
542
522
|
continue;
|
|
543
523
|
}
|
|
544
524
|
try {
|
|
545
|
-
switch (
|
|
525
|
+
switch (hookFormat) {
|
|
546
526
|
case "claude":
|
|
547
|
-
installClaudeCodeHook(
|
|
527
|
+
installClaudeCodeHook(configDir, scriptPath);
|
|
548
528
|
break;
|
|
549
529
|
case "cursor":
|
|
550
|
-
installCursorHook(
|
|
530
|
+
installCursorHook(configDir, scriptPath);
|
|
551
531
|
break;
|
|
552
532
|
case "opencode":
|
|
553
|
-
installOpenCodeHook(
|
|
533
|
+
installOpenCodeHook(configDir, scriptPath);
|
|
554
534
|
break;
|
|
555
535
|
case "codex":
|
|
556
|
-
installCodexHook(
|
|
536
|
+
installCodexHook(configDir, scriptPath);
|
|
557
537
|
break;
|
|
558
538
|
case "gemini":
|
|
559
|
-
installGeminiHook(
|
|
539
|
+
installGeminiHook(configDir, scriptPath);
|
|
560
540
|
break;
|
|
561
541
|
}
|
|
562
|
-
result.installed.push(
|
|
542
|
+
result.installed.push(agentName);
|
|
563
543
|
}
|
|
564
544
|
catch (err) {
|
|
565
545
|
result.errors.push({
|
|
566
|
-
agent:
|
|
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
|
|
580
|
-
|
|
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 (
|
|
567
|
+
switch (hookFormat) {
|
|
586
568
|
case "claude":
|
|
587
569
|
case "cursor": {
|
|
588
|
-
const hookFile = join(
|
|
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(
|
|
579
|
+
result.removed.push(agentName);
|
|
598
580
|
}
|
|
599
581
|
else {
|
|
600
|
-
result.notFound.push(
|
|
582
|
+
result.notFound.push(agentName);
|
|
601
583
|
}
|
|
602
584
|
break;
|
|
603
585
|
}
|
|
604
586
|
case "opencode": {
|
|
605
|
-
const pluginFile = join(
|
|
587
|
+
const pluginFile = join(configDir, "plugins", "jobarbiter-observer.js");
|
|
606
588
|
if (existsSync(pluginFile)) {
|
|
607
589
|
unlinkSync(pluginFile);
|
|
608
|
-
result.removed.push(
|
|
590
|
+
result.removed.push(agentName);
|
|
609
591
|
}
|
|
610
592
|
else {
|
|
611
|
-
result.notFound.push(
|
|
593
|
+
result.notFound.push(agentName);
|
|
612
594
|
}
|
|
613
595
|
break;
|
|
614
596
|
}
|
|
615
597
|
case "codex": {
|
|
616
|
-
const configFile = join(
|
|
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(
|
|
606
|
+
result.removed.push(agentName);
|
|
625
607
|
}
|
|
626
608
|
else {
|
|
627
|
-
result.notFound.push(
|
|
609
|
+
result.notFound.push(agentName);
|
|
628
610
|
}
|
|
629
611
|
break;
|
|
630
612
|
}
|
|
631
613
|
case "gemini": {
|
|
632
|
-
const settingsFile = join(
|
|
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(
|
|
623
|
+
result.removed.push(agentName);
|
|
642
624
|
}
|
|
643
625
|
else {
|
|
644
|
-
result.notFound.push(
|
|
626
|
+
result.notFound.push(agentName);
|
|
645
627
|
}
|
|
646
628
|
break;
|
|
647
629
|
}
|
|
648
630
|
}
|
|
649
631
|
}
|
|
650
632
|
catch {
|
|
651
|
-
result.notFound.push(
|
|
633
|
+
result.notFound.push(agentName);
|
|
652
634
|
}
|
|
653
635
|
}
|
|
654
636
|
return result;
|