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.
- 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 +108 -74
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/lib/detect-tools.ts +526 -0
- package/src/lib/observe.ts +116 -131
- package/src/lib/onboard.ts +124 -88
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
|
|
package/src/lib/onboard.ts
CHANGED
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
import * as readline from "node:readline";
|
|
12
12
|
import { loadConfig, saveConfig, getConfigPath, type Config } from "./config.js";
|
|
13
13
|
import { apiUnauthenticated, api, ApiError } from "./api.js";
|
|
14
|
-
import {
|
|
14
|
+
import { installObservers } from "./observe.js";
|
|
15
|
+
import {
|
|
16
|
+
detectAllTools,
|
|
17
|
+
getInstalledTools,
|
|
18
|
+
getToolsNeedingObserver,
|
|
19
|
+
formatToolDisplay,
|
|
20
|
+
type DetectedTool,
|
|
21
|
+
type ToolCategory,
|
|
22
|
+
} from "./detect-tools.js";
|
|
15
23
|
|
|
16
24
|
// ── ANSI Colors ────────────────────────────────────────────────────────
|
|
17
25
|
|
|
@@ -216,7 +224,9 @@ async function handleEmailVerification(
|
|
|
216
224
|
baseUrl: string,
|
|
217
225
|
userType: "worker" | "employer"
|
|
218
226
|
): Promise<{ email: string; apiKey: string; userId: string }> {
|
|
219
|
-
|
|
227
|
+
// Workers: 1) Account, 2) Tool Detection, 3) Domains, 4) GitHub, 5) LinkedIn, 6) Done
|
|
228
|
+
// Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done
|
|
229
|
+
const totalSteps = 6;
|
|
220
230
|
|
|
221
231
|
console.log(`\n${sym.email} ${c.bold(`Step 1/${totalSteps} — Create Your Account`)}\n`);
|
|
222
232
|
|
|
@@ -303,16 +313,13 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void>
|
|
|
303
313
|
userType: "worker",
|
|
304
314
|
};
|
|
305
315
|
|
|
306
|
-
// Step 2:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
console.log(`What AI tools do you use? ${c.dim("(comma-separated)")}`);
|
|
310
|
-
console.log(c.dim("Examples: Claude Code, Cursor, OpenClaw, ChatGPT, Copilot, Midjourney\n"));
|
|
311
|
-
const toolsInput = await prompt.question(`${sym.arrow} `);
|
|
312
|
-
const tools = toolsInput.split(",").map(s => s.trim()).filter(Boolean);
|
|
313
|
-
state.tools = tools;
|
|
316
|
+
// Step 2: Auto-detect AI Tools
|
|
317
|
+
const detectedToolsResult = await runToolDetectionStep(prompt, config);
|
|
318
|
+
state.tools = detectedToolsResult.tools;
|
|
314
319
|
|
|
315
|
-
|
|
320
|
+
// Step 3: Domains
|
|
321
|
+
console.log(`${sym.target} ${c.bold("Step 3/6 — Your Domains")}\n`);
|
|
322
|
+
console.log(`What domains do you work in? ${c.dim("(comma-separated)")}`);
|
|
316
323
|
console.log(c.dim("Examples: full-stack dev, data engineering, trading, content creation\n"));
|
|
317
324
|
const domainsInput = await prompt.question(`${sym.arrow} `);
|
|
318
325
|
const domains = domainsInput.split(",").map(s => s.trim()).filter(Boolean);
|
|
@@ -325,7 +332,7 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void>
|
|
|
325
332
|
await api(config, "POST", "/v1/profile", {
|
|
326
333
|
domains,
|
|
327
334
|
tools: {
|
|
328
|
-
primary: tools,
|
|
335
|
+
primary: state.tools,
|
|
329
336
|
},
|
|
330
337
|
});
|
|
331
338
|
console.log(`${sym.check} Profile saved\n`);
|
|
@@ -333,9 +340,6 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void>
|
|
|
333
340
|
console.log(`${sym.warning} ${c.warning("Could not save profile details — you can update later with 'jobarbiter profile create'")}\n`);
|
|
334
341
|
}
|
|
335
342
|
|
|
336
|
-
// Step 3: Install Coding Agent Observers
|
|
337
|
-
await runObserverStep(prompt, state, 3, 6);
|
|
338
|
-
|
|
339
343
|
// Step 4: Connect GitHub (optional)
|
|
340
344
|
console.log(`${sym.link} ${c.bold("Step 4/6 — Connect GitHub")} ${c.dim("(optional)")}\n`);
|
|
341
345
|
console.log(`Connecting your GitHub lets us analyze your AI-assisted work patterns.`);
|
|
@@ -384,101 +388,133 @@ async function runWorkerFlow(prompt: Prompt, state: OnboardState): Promise<void>
|
|
|
384
388
|
showWorkerCompletion(state);
|
|
385
389
|
}
|
|
386
390
|
|
|
387
|
-
// ──
|
|
391
|
+
// ── Tool Detection Step ────────────────────────────────────────────────
|
|
388
392
|
|
|
389
|
-
async function
|
|
393
|
+
async function runToolDetectionStep(
|
|
390
394
|
prompt: Prompt,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
395
|
+
config: Config,
|
|
396
|
+
): Promise<{ tools: string[] }> {
|
|
397
|
+
console.log(`🔍 ${c.bold("Step 2/6 — Detecting AI Tools")}\n`);
|
|
398
|
+
console.log(c.dim(" Scanning your machine...\n"));
|
|
399
|
+
|
|
400
|
+
const allTools = detectAllTools();
|
|
401
|
+
const installed = allTools.filter((t) => t.installed);
|
|
402
|
+
const notInstalled = allTools.filter((t) => !t.installed && t.category === "coding-agent");
|
|
403
|
+
|
|
404
|
+
// Group by category
|
|
405
|
+
const codingAgents = installed.filter((t) => t.category === "coding-agent");
|
|
406
|
+
const chatTools = installed.filter((t) => t.category === "chat");
|
|
407
|
+
const orchestration = installed.filter((t) => t.category === "orchestration");
|
|
408
|
+
const apiProviders = installed.filter((t) => t.category === "api-provider");
|
|
409
|
+
|
|
410
|
+
// Display found tools
|
|
411
|
+
if (installed.length === 0) {
|
|
412
|
+
console.log(` ${c.dim("No AI tools detected on this system.")}\n`);
|
|
413
|
+
console.log(c.dim(" You can add tools later with 'jobarbiter observe install'.\n"));
|
|
414
|
+
return { tools: [] };
|
|
407
415
|
}
|
|
408
416
|
|
|
409
|
-
console.log(` Found
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
417
|
+
console.log(` ${c.bold("Found:")}`);
|
|
418
|
+
|
|
419
|
+
// Show coding agents with observer status
|
|
420
|
+
for (const tool of codingAgents) {
|
|
421
|
+
const display = formatToolDisplay(tool);
|
|
422
|
+
if (tool.observerAvailable) {
|
|
423
|
+
if (tool.observerActive) {
|
|
424
|
+
console.log(` ${sym.check} ${display} ${c.dim("(observer active)")}`);
|
|
425
|
+
} else {
|
|
426
|
+
console.log(` ${sym.check} ${display} ${c.success("(observer available)")}`);
|
|
427
|
+
}
|
|
413
428
|
} else {
|
|
414
|
-
console.log(` ${c.
|
|
429
|
+
console.log(` ${sym.check} ${display} ${c.dim("(detected)")}`);
|
|
415
430
|
}
|
|
416
431
|
}
|
|
417
|
-
|
|
418
|
-
|
|
432
|
+
|
|
433
|
+
// Show other tools
|
|
434
|
+
for (const tool of chatTools) {
|
|
435
|
+
console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
|
|
436
|
+
}
|
|
437
|
+
for (const tool of orchestration) {
|
|
438
|
+
console.log(` ${sym.check} ${formatToolDisplay(tool)} ${c.dim("(detected)")}`);
|
|
439
|
+
}
|
|
440
|
+
for (const tool of apiProviders) {
|
|
441
|
+
console.log(` ${sym.check} ${tool.name} ${c.dim("configured")}`);
|
|
419
442
|
}
|
|
420
443
|
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
444
|
+
// Show not-detected coding agents
|
|
445
|
+
if (notInstalled.length > 0) {
|
|
446
|
+
console.log(`\n ${c.dim("Not detected (install to track):")}`);
|
|
447
|
+
for (const tool of notInstalled.slice(0, 5)) {
|
|
448
|
+
console.log(` ${c.dim("⬚")} ${tool.name}`);
|
|
449
|
+
}
|
|
450
|
+
if (notInstalled.length > 5) {
|
|
451
|
+
console.log(` ${c.dim(`... and ${notInstalled.length - 5} more`)}`);
|
|
452
|
+
}
|
|
427
453
|
}
|
|
428
454
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
|
|
432
|
-
console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
|
|
433
|
-
console.log(c.dim(` Review anytime: jobarbiter observe status\n`));
|
|
455
|
+
// Collect tool names for profile
|
|
456
|
+
const toolNames = installed.map((t) => t.name);
|
|
434
457
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
);
|
|
458
|
+
// Observer installation for coding agents
|
|
459
|
+
const needsObserver = codingAgents.filter((t) => t.observerAvailable && !t.observerActive);
|
|
438
460
|
|
|
439
|
-
|
|
461
|
+
if (needsObserver.length > 0) {
|
|
462
|
+
console.log(`\n ${c.bold("Observers")}`);
|
|
463
|
+
console.log(` JobArbiter observes your coding sessions to build your`);
|
|
464
|
+
console.log(` proficiency profile. ${c.bold("No code or prompts leave your machine")} —`);
|
|
465
|
+
console.log(` only aggregate scores (tool usage, session counts, token volume).\n`);
|
|
466
|
+
console.log(c.dim(` Data stored locally: ~/.config/jobarbiter/observer/observations.json`));
|
|
467
|
+
console.log(c.dim(` Review anytime: jobarbiter observe status\n`));
|
|
440
468
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
console.log(`\n Select which agents to observe:\n`);
|
|
446
|
-
const selections: Record<string, boolean> = {};
|
|
447
|
-
|
|
448
|
-
for (const agent of needsInstall) {
|
|
449
|
-
selections[agent.id] = await prompt.confirm(` ${agent.name}?`, true);
|
|
450
|
-
}
|
|
469
|
+
const observerNames = needsObserver.map((t) => t.name).join(", ");
|
|
470
|
+
const installAll = await prompt.confirm(
|
|
471
|
+
` Install observers for detected tools? (${observerNames})`,
|
|
472
|
+
);
|
|
451
473
|
|
|
452
|
-
|
|
453
|
-
.
|
|
454
|
-
.
|
|
474
|
+
if (installAll) {
|
|
475
|
+
const toInstall = needsObserver.map((t) => t.id);
|
|
476
|
+
console.log(c.dim("\n Installing observers..."));
|
|
477
|
+
const result = installObservers(toInstall);
|
|
455
478
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
479
|
+
for (const name of result.installed) {
|
|
480
|
+
console.log(` ${sym.check} ${name}`);
|
|
481
|
+
}
|
|
482
|
+
for (const name of result.skipped) {
|
|
483
|
+
console.log(` ${c.dim("—")} ${name} ${c.dim("(already installed)")}`);
|
|
484
|
+
}
|
|
485
|
+
for (const { agent, error: errMsg } of result.errors) {
|
|
486
|
+
console.log(` ${sym.cross} ${agent}: ${c.error(errMsg)}`);
|
|
487
|
+
}
|
|
465
488
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
489
|
+
if (result.installed.length > 0) {
|
|
490
|
+
console.log(`\n ${sym.check} ${c.success(`${result.installed.length} observer${result.installed.length > 1 ? "s" : ""} installed!`)}`);
|
|
491
|
+
console.log(c.dim(` Your proficiency profile will start building automatically.\n`));
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
console.log(c.dim("\n Skipped — you can install observers later with 'jobarbiter observe install'.\n"));
|
|
495
|
+
}
|
|
496
|
+
} else if (codingAgents.length > 0) {
|
|
497
|
+
const hasActiveObservers = codingAgents.some((t) => t.observerActive);
|
|
498
|
+
if (hasActiveObservers) {
|
|
499
|
+
console.log(`\n ${c.dim("All detected agents already have observers installed.")}\n`);
|
|
500
|
+
} else {
|
|
501
|
+
console.log();
|
|
502
|
+
}
|
|
474
503
|
}
|
|
475
504
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
505
|
+
// "Did we miss anything?" prompt
|
|
506
|
+
console.log(` ${c.dim("Did we miss anything?")}`);
|
|
507
|
+
const additionalTools = await prompt.question(` Other AI tools you use ${c.dim("(comma-separated, or press Enter)")}: `);
|
|
508
|
+
|
|
509
|
+
if (additionalTools.trim()) {
|
|
510
|
+
const additional = additionalTools.split(",").map((s) => s.trim()).filter(Boolean);
|
|
511
|
+
toolNames.push(...additional);
|
|
512
|
+
console.log(` ${sym.check} Added: ${additional.join(", ")}\n`);
|
|
479
513
|
} else {
|
|
480
514
|
console.log();
|
|
481
515
|
}
|
|
516
|
+
|
|
517
|
+
return { tools: toolNames };
|
|
482
518
|
}
|
|
483
519
|
|
|
484
520
|
function showWorkerCompletion(state: OnboardState): void {
|