jobarbiter 0.3.0 → 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.
@@ -0,0 +1,672 @@
1
+ /**
2
+ * JobArbiter Observer — Hook installer for coding agent CLIs
3
+ *
4
+ * Installs observation hooks that extract proficiency signals from
5
+ * session transcripts. Uses detect-tools.ts for agent detection.
6
+ */
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
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
+ */
30
+ export function detectAgents() {
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
+ }));
40
+ }
41
+ // ── Observer Data Directory ────────────────────────────────────────────
42
+ const OBSERVER_DIR = join(homedir(), ".config", "jobarbiter", "observer");
43
+ const OBSERVATIONS_FILE = join(OBSERVER_DIR, "observations.json");
44
+ const HOOKS_DIR = join(OBSERVER_DIR, "hooks");
45
+ function ensureObserverDirs() {
46
+ mkdirSync(OBSERVER_DIR, { recursive: true });
47
+ mkdirSync(HOOKS_DIR, { recursive: true });
48
+ // Initialize observations file if missing
49
+ if (!existsSync(OBSERVATIONS_FILE)) {
50
+ writeFileSync(OBSERVATIONS_FILE, JSON.stringify({
51
+ version: 1,
52
+ installedAt: new Date().toISOString(),
53
+ agents: {},
54
+ sessions: [],
55
+ accumulated: {
56
+ totalSessions: 0,
57
+ totalTokens: 0,
58
+ toolCounts: {},
59
+ domainSignals: [],
60
+ lastSubmitted: null,
61
+ },
62
+ }, null, 2) + "\n");
63
+ }
64
+ }
65
+ // ── Core Observer Script ───────────────────────────────────────────────
66
+ /**
67
+ * The universal observer script. Runs as a hook in any coding agent.
68
+ * Reads session transcript data from stdin (JSON), extracts proficiency
69
+ * signals, and appends them to the local observations file.
70
+ *
71
+ * This is written as a standalone shell script so it has zero dependencies
72
+ * and works regardless of the user's Node.js setup.
73
+ */
74
+ function getObserverScript() {
75
+ return `#!/usr/bin/env node
76
+ /**
77
+ * JobArbiter Observer Hook
78
+ * Extracts proficiency signals from coding agent sessions.
79
+ *
80
+ * Reads JSON from stdin, writes observations to:
81
+ * ~/.config/jobarbiter/observer/observations.json
82
+ *
83
+ * Signals extracted:
84
+ * - Tool names and frequencies (tool fluency)
85
+ * - Session duration and message counts (output velocity)
86
+ * - File types worked on (domain application)
87
+ * - Token usage when available (token throughput)
88
+ */
89
+
90
+ const fs = require("fs");
91
+ const path = require("path");
92
+ const os = require("os");
93
+
94
+ const OBSERVATIONS_FILE = path.join(
95
+ os.homedir(), ".config", "jobarbiter", "observer", "observations.json"
96
+ );
97
+
98
+ // Read stdin
99
+ let input = "";
100
+ process.stdin.setEncoding("utf-8");
101
+ process.stdin.on("data", (chunk) => { input += chunk; });
102
+ process.stdin.on("end", () => {
103
+ try {
104
+ const data = JSON.parse(input);
105
+ const observation = extractSignals(data);
106
+ if (observation) appendObservation(observation);
107
+ } catch (err) {
108
+ // Silent failure — never block the coding agent
109
+ fs.appendFileSync(
110
+ path.join(os.homedir(), ".config", "jobarbiter", "observer", "errors.log"),
111
+ \`[\${new Date().toISOString()}] \${err.message}\\n\`
112
+ );
113
+ }
114
+ process.exit(0);
115
+ });
116
+
117
+ function extractSignals(data) {
118
+ const observation = {
119
+ timestamp: new Date().toISOString(),
120
+ agent: process.env.JOBARBITER_AGENT || detectAgent(),
121
+ sessionId: data.sessionId || data.session_id || data.thread_id || data.id || null,
122
+ signals: {
123
+ toolsUsed: [],
124
+ fileExtensions: [],
125
+ messageCount: 0,
126
+ thinkingBlocks: 0,
127
+ tokenUsage: null,
128
+ duration: null,
129
+ },
130
+ };
131
+
132
+ // Claude Code / Cursor: stop hook provides session info
133
+ if (data.transcript || data.messages) {
134
+ const messages = data.transcript || data.messages || [];
135
+ observation.signals.messageCount = messages.length;
136
+
137
+ for (const msg of messages) {
138
+ // Extract tool calls
139
+ if (msg.tool_name || msg.toolName) {
140
+ observation.signals.toolsUsed.push(msg.tool_name || msg.toolName);
141
+ }
142
+ if (msg.tool_calls) {
143
+ for (const tc of msg.tool_calls) {
144
+ observation.signals.toolsUsed.push(tc.name || tc.function?.name);
145
+ }
146
+ }
147
+
148
+ // Extract file extensions from tool args
149
+ const args = msg.tool_input || msg.args || msg.tool_calls?.[0]?.input || {};
150
+ const filePath = args.file_path || args.filePath || args.path || args.filename || "";
151
+ if (filePath) {
152
+ const ext = path.extname(filePath).toLowerCase();
153
+ if (ext) observation.signals.fileExtensions.push(ext);
154
+ }
155
+
156
+ // Count thinking blocks
157
+ if (msg.type === "thinking" || msg.role === "thinking") {
158
+ observation.signals.thinkingBlocks++;
159
+ }
160
+
161
+ // Token usage
162
+ if (msg.usage) {
163
+ if (!observation.signals.tokenUsage) {
164
+ observation.signals.tokenUsage = { input: 0, output: 0, total: 0 };
165
+ }
166
+ observation.signals.tokenUsage.input += msg.usage.input_tokens || msg.usage.input || 0;
167
+ observation.signals.tokenUsage.output += msg.usage.output_tokens || msg.usage.output || 0;
168
+ observation.signals.tokenUsage.total += msg.usage.total_tokens || msg.usage.totalTokens || 0;
169
+ }
170
+ }
171
+ }
172
+
173
+ // Codex: notification format
174
+ if (data.type === "agent-turn-complete") {
175
+ observation.signals.messageCount = (data["input-messages"] || []).length;
176
+ }
177
+
178
+ // Gemini: AfterAgent / SessionEnd
179
+ if (data.toolResults || data.toolCalls) {
180
+ const tools = data.toolCalls || data.toolResults || [];
181
+ for (const t of tools) {
182
+ observation.signals.toolsUsed.push(t.name || t.toolName);
183
+ }
184
+ }
185
+
186
+ // Deduplicate
187
+ observation.signals.toolsUsed = [...new Set(observation.signals.toolsUsed.filter(Boolean))];
188
+ observation.signals.fileExtensions = [...new Set(observation.signals.fileExtensions.filter(Boolean))];
189
+
190
+ // Skip empty observations
191
+ if (
192
+ observation.signals.toolsUsed.length === 0 &&
193
+ observation.signals.messageCount === 0 &&
194
+ !observation.signals.tokenUsage
195
+ ) {
196
+ return null;
197
+ }
198
+
199
+ return observation;
200
+ }
201
+
202
+ function detectAgent() {
203
+ // Detect from environment or parent process
204
+ if (process.env.CLAUDE_CODE) return "claude-code";
205
+ if (process.env.CURSOR_SESSION) return "cursor";
206
+ if (process.env.CODEX_HOME) return "codex";
207
+ if (process.env.GEMINI_PROJECT_DIR) return "gemini";
208
+ return "unknown";
209
+ }
210
+
211
+ function appendObservation(obs) {
212
+ try {
213
+ const raw = fs.readFileSync(OBSERVATIONS_FILE, "utf-8");
214
+ const data = JSON.parse(raw);
215
+ data.sessions.push(obs);
216
+ data.accumulated.totalSessions++;
217
+
218
+ // Update tool counts
219
+ for (const tool of obs.signals.toolsUsed) {
220
+ data.accumulated.toolCounts[tool] = (data.accumulated.toolCounts[tool] || 0) + 1;
221
+ }
222
+
223
+ // Update token totals
224
+ if (obs.signals.tokenUsage) {
225
+ data.accumulated.totalTokens += obs.signals.tokenUsage.total || 0;
226
+ }
227
+
228
+ // Keep only last 500 detailed sessions (rolling window)
229
+ if (data.sessions.length > 500) {
230
+ data.sessions = data.sessions.slice(-500);
231
+ }
232
+
233
+ fs.writeFileSync(OBSERVATIONS_FILE, JSON.stringify(data, null, 2) + "\\n");
234
+ } catch (err) {
235
+ // If file is corrupted, start fresh
236
+ fs.writeFileSync(OBSERVATIONS_FILE, JSON.stringify({
237
+ version: 1,
238
+ sessions: [obs],
239
+ accumulated: {
240
+ totalSessions: 1,
241
+ totalTokens: obs.signals.tokenUsage?.total || 0,
242
+ toolCounts: Object.fromEntries(obs.signals.toolsUsed.map(t => [t, 1])),
243
+ domainSignals: [],
244
+ lastSubmitted: null,
245
+ },
246
+ }, null, 2) + "\\n");
247
+ }
248
+ }
249
+ `;
250
+ }
251
+ // ── Hook Installers ────────────────────────────────────────────────────
252
+ function writeObserverScript() {
253
+ ensureObserverDirs();
254
+ const scriptPath = join(HOOKS_DIR, "observer.js");
255
+ writeFileSync(scriptPath, getObserverScript());
256
+ chmodSync(scriptPath, 0o755);
257
+ return scriptPath;
258
+ }
259
+ /**
260
+ * Install hook for Claude Code (~/.claude/hooks.json)
261
+ * Uses the Stop event to observe after each session turn.
262
+ */
263
+ function installClaudeCodeHook(configDir, scriptPath) {
264
+ const hookFile = join(configDir, "hooks.json");
265
+ let config = {};
266
+ if (existsSync(hookFile)) {
267
+ try {
268
+ config = JSON.parse(readFileSync(hookFile, "utf-8"));
269
+ }
270
+ catch {
271
+ config = {};
272
+ }
273
+ }
274
+ // Ensure hooks object exists
275
+ if (!config.hooks)
276
+ config.hooks = {};
277
+ const hooks = config.hooks;
278
+ // Add to Stop event (don't duplicate)
279
+ if (!hooks.Stop)
280
+ hooks.Stop = [];
281
+ const stopHooks = hooks.Stop;
282
+ if (!stopHooks.some((h) => h.command?.includes("jobarbiter"))) {
283
+ stopHooks.push({
284
+ command: `node ${scriptPath}`,
285
+ timeout: 10,
286
+ });
287
+ }
288
+ mkdirSync(configDir, { recursive: true });
289
+ writeFileSync(hookFile, JSON.stringify(config, null, 2) + "\n");
290
+ }
291
+ /**
292
+ * Install hook for Cursor (~/.cursor/hooks.json)
293
+ * Same JSON format as Claude Code — uses stop event.
294
+ */
295
+ function installCursorHook(configDir, scriptPath) {
296
+ const hookFile = join(configDir, "hooks.json");
297
+ let config = {};
298
+ if (existsSync(hookFile)) {
299
+ try {
300
+ config = JSON.parse(readFileSync(hookFile, "utf-8"));
301
+ }
302
+ catch {
303
+ config = {};
304
+ }
305
+ }
306
+ if (!config.version)
307
+ config.version = 1;
308
+ if (!config.hooks)
309
+ config.hooks = {};
310
+ const hooks = config.hooks;
311
+ // Use stop event
312
+ if (!hooks.stop)
313
+ hooks.stop = [];
314
+ const stopHooks = hooks.stop;
315
+ if (!stopHooks.some((h) => h.command?.includes("jobarbiter"))) {
316
+ stopHooks.push({
317
+ command: `node ${scriptPath}`,
318
+ timeout: 10,
319
+ });
320
+ }
321
+ mkdirSync(configDir, { recursive: true });
322
+ writeFileSync(hookFile, JSON.stringify(config, null, 2) + "\n");
323
+ }
324
+ /**
325
+ * Install plugin for OpenCode (~/.config/opencode/plugins/)
326
+ * OpenCode uses JS/TS plugin modules, not JSON config.
327
+ */
328
+ function installOpenCodeHook(configDir, scriptPath) {
329
+ const pluginDir = join(configDir, "plugins");
330
+ mkdirSync(pluginDir, { recursive: true });
331
+ const pluginCode = `// JobArbiter Observer Plugin for OpenCode
332
+ // Observes session activity and extracts proficiency signals.
333
+
334
+ const { execSync } = require("child_process");
335
+ const { readFileSync } = require("fs");
336
+
337
+ exports.JobArbiterObserver = async ({ project, client, $, directory }) => {
338
+ return {
339
+ event: async ({ event }) => {
340
+ if (event.type === "session.idle" || event.type === "session.updated") {
341
+ try {
342
+ const sessionData = JSON.stringify({
343
+ sessionId: event.sessionId || "unknown",
344
+ agent: "opencode",
345
+ messages: event.messages || [],
346
+ });
347
+ execSync(\`echo '\${sessionData.replace(/'/g, "'\\"'\\"\\'")}' | node ${scriptPath}\`, {
348
+ stdio: "ignore",
349
+ timeout: 10000,
350
+ env: { ...process.env, JOBARBITER_AGENT: "opencode" },
351
+ });
352
+ } catch {
353
+ // Silent failure
354
+ }
355
+ }
356
+ },
357
+ "tool.execute.after": async (input, output) => {
358
+ // Track individual tool executions for richer signal
359
+ try {
360
+ const toolData = JSON.stringify({
361
+ agent: "opencode",
362
+ messages: [{
363
+ tool_name: input.tool,
364
+ args: output.args || {},
365
+ }],
366
+ });
367
+ execSync(\`echo '\${toolData.replace(/'/g, "'\\"'\\"\\'")}' | node ${scriptPath}\`, {
368
+ stdio: "ignore",
369
+ timeout: 5000,
370
+ env: { ...process.env, JOBARBITER_AGENT: "opencode" },
371
+ });
372
+ } catch {
373
+ // Silent failure
374
+ }
375
+ },
376
+ };
377
+ };
378
+ `;
379
+ writeFileSync(join(pluginDir, "jobarbiter-observer.js"), pluginCode);
380
+ }
381
+ /**
382
+ * Install notification handler for Codex CLI (~/.codex/config.toml)
383
+ * Codex uses a `notify` config key that runs an external program.
384
+ */
385
+ function installCodexHook(configDir, scriptPath) {
386
+ const configFile = join(configDir, "config.toml");
387
+ // Create a wrapper script that converts Codex's CLI arg format to stdin
388
+ const wrapperPath = join(HOOKS_DIR, "codex-wrapper.sh");
389
+ writeFileSync(wrapperPath, `#!/bin/bash
390
+ # JobArbiter Observer wrapper for Codex CLI
391
+ # Codex passes JSON as $1, our observer reads stdin
392
+ echo "$1" | JOBARBITER_AGENT=codex node ${scriptPath}
393
+ `);
394
+ chmodSync(wrapperPath, 0o755);
395
+ mkdirSync(configDir, { recursive: true });
396
+ let config = "";
397
+ if (existsSync(configFile)) {
398
+ config = readFileSync(configFile, "utf-8");
399
+ }
400
+ // Add notify line if not present
401
+ if (!config.includes("jobarbiter")) {
402
+ // Check if there's already a notify line
403
+ if (config.includes("notify")) {
404
+ // Don't overwrite existing notify — append comment
405
+ config += `\n# JobArbiter observer: add this to your notify script:\n# ${wrapperPath} "$1"\n`;
406
+ }
407
+ else {
408
+ config += `\n# JobArbiter observer\nnotify = "${wrapperPath}"\n`;
409
+ }
410
+ writeFileSync(configFile, config);
411
+ }
412
+ }
413
+ /**
414
+ * Install hook for Gemini CLI (~/.gemini/settings.json)
415
+ * Uses SessionEnd event.
416
+ */
417
+ function installGeminiHook(configDir, scriptPath) {
418
+ const settingsFile = join(configDir, "settings.json");
419
+ let settings = {};
420
+ if (existsSync(settingsFile)) {
421
+ try {
422
+ settings = JSON.parse(readFileSync(settingsFile, "utf-8"));
423
+ }
424
+ catch {
425
+ settings = {};
426
+ }
427
+ }
428
+ if (!settings.hooks)
429
+ settings.hooks = {};
430
+ const hooks = settings.hooks;
431
+ // Add SessionEnd hook
432
+ if (!hooks.SessionEnd)
433
+ hooks.SessionEnd = [];
434
+ const sessionEndHooks = hooks.SessionEnd;
435
+ if (!sessionEndHooks.some((h) => h.hooks?.some((hh) => hh.command?.includes("jobarbiter")))) {
436
+ sessionEndHooks.push({
437
+ matcher: "*",
438
+ hooks: [
439
+ {
440
+ name: "jobarbiter-observer",
441
+ type: "command",
442
+ command: `node ${scriptPath}`,
443
+ timeout: 10000,
444
+ },
445
+ ],
446
+ });
447
+ }
448
+ mkdirSync(configDir, { recursive: true });
449
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
450
+ }
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
+ }
500
+ /**
501
+ * Install observer hooks for the specified agents.
502
+ * Returns a summary of what was installed.
503
+ */
504
+ export function installObservers(agentIds) {
505
+ const scriptPath = writeObserverScript();
506
+ const result = {
507
+ installed: [],
508
+ skipped: [],
509
+ errors: [],
510
+ };
511
+ for (const agentId of agentIds) {
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) {
516
+ result.errors.push({ agent: agentId, error: "Unknown agent" });
517
+ continue;
518
+ }
519
+ // Check if already installed
520
+ if (isHookInstalled(agentId, configDir, hookFormat)) {
521
+ result.skipped.push(agentName);
522
+ continue;
523
+ }
524
+ try {
525
+ switch (hookFormat) {
526
+ case "claude":
527
+ installClaudeCodeHook(configDir, scriptPath);
528
+ break;
529
+ case "cursor":
530
+ installCursorHook(configDir, scriptPath);
531
+ break;
532
+ case "opencode":
533
+ installOpenCodeHook(configDir, scriptPath);
534
+ break;
535
+ case "codex":
536
+ installCodexHook(configDir, scriptPath);
537
+ break;
538
+ case "gemini":
539
+ installGeminiHook(configDir, scriptPath);
540
+ break;
541
+ }
542
+ result.installed.push(agentName);
543
+ }
544
+ catch (err) {
545
+ result.errors.push({
546
+ agent: agentName,
547
+ error: err instanceof Error ? err.message : String(err),
548
+ });
549
+ }
550
+ }
551
+ return result;
552
+ }
553
+ /**
554
+ * Remove observer hooks for the specified agents.
555
+ */
556
+ export function removeObservers(agentIds) {
557
+ const result = { removed: [], notFound: [] };
558
+ for (const agentId of agentIds) {
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) {
563
+ result.notFound.push(agentId);
564
+ continue;
565
+ }
566
+ try {
567
+ switch (hookFormat) {
568
+ case "claude":
569
+ case "cursor": {
570
+ const hookFile = join(configDir, "hooks.json");
571
+ if (existsSync(hookFile)) {
572
+ const config = JSON.parse(readFileSync(hookFile, "utf-8"));
573
+ for (const [key, hooks] of Object.entries(config.hooks || {})) {
574
+ if (Array.isArray(hooks)) {
575
+ config.hooks[key] = hooks.filter((h) => !JSON.stringify(h).includes("jobarbiter"));
576
+ }
577
+ }
578
+ writeFileSync(hookFile, JSON.stringify(config, null, 2) + "\n");
579
+ result.removed.push(agentName);
580
+ }
581
+ else {
582
+ result.notFound.push(agentName);
583
+ }
584
+ break;
585
+ }
586
+ case "opencode": {
587
+ const pluginFile = join(configDir, "plugins", "jobarbiter-observer.js");
588
+ if (existsSync(pluginFile)) {
589
+ unlinkSync(pluginFile);
590
+ result.removed.push(agentName);
591
+ }
592
+ else {
593
+ result.notFound.push(agentName);
594
+ }
595
+ break;
596
+ }
597
+ case "codex": {
598
+ const configFile = join(configDir, "config.toml");
599
+ if (existsSync(configFile)) {
600
+ let content = readFileSync(configFile, "utf-8");
601
+ content = content
602
+ .split("\n")
603
+ .filter((line) => !line.includes("jobarbiter"))
604
+ .join("\n");
605
+ writeFileSync(configFile, content);
606
+ result.removed.push(agentName);
607
+ }
608
+ else {
609
+ result.notFound.push(agentName);
610
+ }
611
+ break;
612
+ }
613
+ case "gemini": {
614
+ const settingsFile = join(configDir, "settings.json");
615
+ if (existsSync(settingsFile)) {
616
+ const settings = JSON.parse(readFileSync(settingsFile, "utf-8"));
617
+ for (const [key, hookGroups] of Object.entries(settings.hooks || {})) {
618
+ if (Array.isArray(hookGroups)) {
619
+ settings.hooks[key] = hookGroups.filter((g) => !JSON.stringify(g).includes("jobarbiter"));
620
+ }
621
+ }
622
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
623
+ result.removed.push(agentName);
624
+ }
625
+ else {
626
+ result.notFound.push(agentName);
627
+ }
628
+ break;
629
+ }
630
+ }
631
+ }
632
+ catch {
633
+ result.notFound.push(agentName);
634
+ }
635
+ }
636
+ return result;
637
+ }
638
+ /**
639
+ * Get observation status — what's been accumulated locally.
640
+ */
641
+ export function getObservationStatus() {
642
+ ensureObserverDirs();
643
+ try {
644
+ const raw = readFileSync(OBSERVATIONS_FILE, "utf-8");
645
+ const data = JSON.parse(raw);
646
+ const topTools = Object.entries(data.accumulated?.toolCounts || {})
647
+ .map(([tool, count]) => ({ tool, count: count }))
648
+ .sort((a, b) => b.count - a.count)
649
+ .slice(0, 10);
650
+ const agents = [
651
+ ...new Set((data.sessions || []).map((s) => s.agent).filter(Boolean)),
652
+ ];
653
+ return {
654
+ hasData: (data.accumulated?.totalSessions || 0) > 0,
655
+ totalSessions: data.accumulated?.totalSessions || 0,
656
+ totalTokens: data.accumulated?.totalTokens || 0,
657
+ topTools,
658
+ agents,
659
+ lastSubmitted: data.accumulated?.lastSubmitted || null,
660
+ };
661
+ }
662
+ catch {
663
+ return {
664
+ hasData: false,
665
+ totalSessions: 0,
666
+ totalTokens: 0,
667
+ topTools: [],
668
+ agents: [],
669
+ lastSubmitted: null,
670
+ };
671
+ }
672
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Interactive Onboard Wizard
3
+ *
4
+ * A polished, step-by-step wizard that guides users through:
5
+ * - Account creation (email + verification)
6
+ * - User type selection (worker vs employer)
7
+ * - Profile setup (track, tools, domains)
8
+ * - Optional integrations (GitHub)
9
+ */
10
+ export declare function runOnboardWizard(opts: {
11
+ force?: boolean;
12
+ baseUrl?: string;
13
+ }): Promise<void>;