@revealui/harnesses 0.1.6 → 0.1.8

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,2040 @@
1
+ import {
2
+ DaemonStore
3
+ } from "./chunk-DGQ5OB6L.js";
4
+ import {
5
+ WorkboardManager,
6
+ deriveSessionId,
7
+ detectSessionType
8
+ } from "./chunk-4F4ANKIZ.js";
9
+ import {
10
+ __require
11
+ } from "./chunk-DGUM43GV.js";
12
+
13
+ // src/index.ts
14
+ import { isFeatureEnabled } from "@revealui/core/features";
15
+ import { initializeLicense } from "@revealui/core/license";
16
+ import { logger } from "@revealui/core/observability/logger";
17
+
18
+ // src/adapters/claude-code-adapter.ts
19
+ import { execFile } from "child_process";
20
+ import { promisify } from "util";
21
+ var execFileAsync = promisify(execFile);
22
+ var ClaudeCodeAdapter = class {
23
+ id = "claude-code";
24
+ name = "Claude Code";
25
+ eventHandlers = /* @__PURE__ */ new Set();
26
+ workboardPath;
27
+ constructor(workboardPath) {
28
+ this.workboardPath = workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH;
29
+ }
30
+ getCapabilities() {
31
+ return {
32
+ generateCode: false,
33
+ // interactive only — no headless CLI interface
34
+ analyzeCode: false,
35
+ // interactive only — no headless CLI interface
36
+ applyEdit: false,
37
+ // interactive only — edits are applied inside Claude Code sessions
38
+ applyConfig: false,
39
+ // config managed interactively via ~/.claude/settings.json
40
+ readWorkboard: this.workboardPath !== void 0,
41
+ writeWorkboard: this.workboardPath !== void 0
42
+ };
43
+ }
44
+ async getInfo() {
45
+ let version;
46
+ try {
47
+ const { stdout } = await execFileAsync("claude", ["--version"], {
48
+ timeout: 5e3
49
+ });
50
+ version = stdout.trim().split("\n")[0];
51
+ } catch {
52
+ }
53
+ return { id: this.id, name: this.name, version, capabilities: this.getCapabilities() };
54
+ }
55
+ async isAvailable() {
56
+ try {
57
+ await execFileAsync("claude", ["--version"], { timeout: 3e3 });
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ notifyRegistered() {
64
+ this.emit({ type: "harness-connected", harnessId: this.id });
65
+ }
66
+ notifyUnregistering() {
67
+ this.emit({ type: "harness-disconnected", harnessId: this.id });
68
+ }
69
+ async execute(command) {
70
+ try {
71
+ return await this.executeInner(command);
72
+ } catch (err) {
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ this.emit({ type: "error", harnessId: this.id, message });
75
+ return { success: false, command: command.type, message };
76
+ }
77
+ }
78
+ async executeInner(command) {
79
+ switch (command.type) {
80
+ case "get-status": {
81
+ const available = await this.isAvailable();
82
+ return { success: true, command: command.type, data: { available } };
83
+ }
84
+ case "get-running-instances": {
85
+ return { success: true, command: command.type, data: [] };
86
+ }
87
+ case "generate-code":
88
+ case "analyze-code": {
89
+ return {
90
+ success: false,
91
+ command: command.type,
92
+ message: `${command.type} is not supported \u2014 Claude Code operates interactively`
93
+ };
94
+ }
95
+ case "apply-config": {
96
+ return {
97
+ success: false,
98
+ command: command.type,
99
+ message: "Config is managed interactively via ~/.claude/settings.json"
100
+ };
101
+ }
102
+ case "read-workboard": {
103
+ if (!this.workboardPath) {
104
+ return {
105
+ success: false,
106
+ command: command.type,
107
+ message: "REVEALUI_WORKBOARD_PATH is not set"
108
+ };
109
+ }
110
+ const manager = new WorkboardManager(this.workboardPath);
111
+ const state = await manager.readAsync();
112
+ return { success: true, command: command.type, data: state };
113
+ }
114
+ case "update-workboard": {
115
+ if (!this.workboardPath) {
116
+ return {
117
+ success: false,
118
+ command: command.type,
119
+ message: "REVEALUI_WORKBOARD_PATH is not set"
120
+ };
121
+ }
122
+ const manager = new WorkboardManager(this.workboardPath);
123
+ manager.updateAgent(command.sessionId, {
124
+ ...command.task !== void 0 && { task: command.task },
125
+ ...command.files !== void 0 && { files: command.files.join(", ") },
126
+ updated: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`
127
+ });
128
+ return { success: true, command: command.type };
129
+ }
130
+ default: {
131
+ return {
132
+ success: false,
133
+ command: command.type,
134
+ message: `Command not supported by ${this.name}`
135
+ };
136
+ }
137
+ }
138
+ }
139
+ onEvent(handler) {
140
+ this.eventHandlers.add(handler);
141
+ return () => this.eventHandlers.delete(handler);
142
+ }
143
+ async dispose() {
144
+ this.eventHandlers.clear();
145
+ }
146
+ emit(event) {
147
+ for (const handler of this.eventHandlers) {
148
+ try {
149
+ handler(event);
150
+ } catch {
151
+ }
152
+ }
153
+ }
154
+ };
155
+
156
+ // src/adapters/cursor-adapter.ts
157
+ import { execFile as execFile2 } from "child_process";
158
+ import { promisify as promisify2 } from "util";
159
+ var execFileAsync2 = promisify2(execFile2);
160
+ var CursorAdapter = class {
161
+ id = "cursor";
162
+ name = "Cursor";
163
+ eventHandlers = /* @__PURE__ */ new Set();
164
+ workboardPath;
165
+ constructor(workboardPath) {
166
+ this.workboardPath = workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH;
167
+ }
168
+ getCapabilities() {
169
+ return {
170
+ generateCode: false,
171
+ analyzeCode: false,
172
+ applyEdit: false,
173
+ applyConfig: false,
174
+ readWorkboard: this.workboardPath !== void 0,
175
+ writeWorkboard: this.workboardPath !== void 0
176
+ };
177
+ }
178
+ async getInfo() {
179
+ let version;
180
+ try {
181
+ const { stdout } = await execFileAsync2("cursor", ["--version"], {
182
+ timeout: 5e3
183
+ });
184
+ version = stdout.trim().split("\n")[0];
185
+ } catch {
186
+ }
187
+ return { id: this.id, name: this.name, version, capabilities: this.getCapabilities() };
188
+ }
189
+ async isAvailable() {
190
+ try {
191
+ await execFileAsync2("cursor", ["--version"], { timeout: 3e3 });
192
+ return true;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+ notifyRegistered() {
198
+ this.emit({ type: "harness-connected", harnessId: this.id });
199
+ }
200
+ notifyUnregistering() {
201
+ this.emit({ type: "harness-disconnected", harnessId: this.id });
202
+ }
203
+ async execute(command) {
204
+ try {
205
+ return await this.executeInner(command);
206
+ } catch (err) {
207
+ const message = err instanceof Error ? err.message : String(err);
208
+ this.emit({ type: "error", harnessId: this.id, message });
209
+ return { success: false, command: command.type, message };
210
+ }
211
+ }
212
+ async executeInner(command) {
213
+ switch (command.type) {
214
+ case "get-status": {
215
+ const available = await this.isAvailable();
216
+ return { success: true, command: command.type, data: { available } };
217
+ }
218
+ case "read-workboard": {
219
+ if (!this.workboardPath) {
220
+ return {
221
+ success: false,
222
+ command: command.type,
223
+ message: "REVEALUI_WORKBOARD_PATH is not set"
224
+ };
225
+ }
226
+ const manager = new WorkboardManager(this.workboardPath);
227
+ const state = await manager.readAsync();
228
+ return { success: true, command: command.type, data: state };
229
+ }
230
+ case "update-workboard": {
231
+ if (!this.workboardPath) {
232
+ return {
233
+ success: false,
234
+ command: command.type,
235
+ message: "REVEALUI_WORKBOARD_PATH is not set"
236
+ };
237
+ }
238
+ const manager = new WorkboardManager(this.workboardPath);
239
+ manager.updateAgent(command.sessionId, {
240
+ ...command.task !== void 0 && { task: command.task },
241
+ ...command.files !== void 0 && { files: command.files.join(", ") },
242
+ updated: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`
243
+ });
244
+ return { success: true, command: command.type };
245
+ }
246
+ default: {
247
+ return {
248
+ success: false,
249
+ command: command.type,
250
+ message: `Command not supported by ${this.name}`
251
+ };
252
+ }
253
+ }
254
+ }
255
+ onEvent(handler) {
256
+ this.eventHandlers.add(handler);
257
+ return () => this.eventHandlers.delete(handler);
258
+ }
259
+ async dispose() {
260
+ this.eventHandlers.clear();
261
+ }
262
+ emit(event) {
263
+ for (const handler of this.eventHandlers) {
264
+ try {
265
+ handler(event);
266
+ } catch {
267
+ }
268
+ }
269
+ }
270
+ };
271
+
272
+ // src/config/config-sync.ts
273
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
274
+ import { dirname } from "path";
275
+
276
+ // src/config/harness-config-paths.ts
277
+ import { homedir } from "os";
278
+ import { join } from "path";
279
+ var HOME = homedir();
280
+ var REVEALUI_ROOT = process.env.REVEALUI_ROOT ?? join(HOME, ".revealui");
281
+ var LOCAL_CONFIG_PATHS = {
282
+ "claude-code": join(HOME, ".claude", "settings.json"),
283
+ cursor: join(HOME, ".cursor", "settings.json"),
284
+ copilot: join(HOME, ".config", "github-copilot", "hosts.json")
285
+ };
286
+ var ROOT_CONFIG_FILES = {
287
+ "claude-code": "settings.json",
288
+ cursor: "settings.json",
289
+ copilot: "hosts.json"
290
+ };
291
+ function getLocalConfigPath(harnessId) {
292
+ return LOCAL_CONFIG_PATHS[harnessId];
293
+ }
294
+ function getRootConfigPath(harnessId, root = REVEALUI_ROOT) {
295
+ const file = ROOT_CONFIG_FILES[harnessId];
296
+ if (!file) return void 0;
297
+ return join(root, "harness-configs", harnessId, file);
298
+ }
299
+ function getConfigurableHarnesses() {
300
+ return Object.keys(LOCAL_CONFIG_PATHS);
301
+ }
302
+
303
+ // src/config/config-sync.ts
304
+ function syncConfig(harnessId, direction, root) {
305
+ const localPath = getLocalConfigPath(harnessId);
306
+ const rootPath = getRootConfigPath(harnessId, root);
307
+ if (!(localPath && rootPath)) {
308
+ return {
309
+ success: false,
310
+ harnessId,
311
+ direction,
312
+ message: `No config path known for harness: ${harnessId}`
313
+ };
314
+ }
315
+ try {
316
+ if (direction === "pull") {
317
+ if (!existsSync(rootPath)) {
318
+ return {
319
+ success: false,
320
+ harnessId,
321
+ direction,
322
+ message: `Root config not found: ${rootPath}`
323
+ };
324
+ }
325
+ mkdirSync(dirname(localPath), { recursive: true });
326
+ backupIfExists(localPath);
327
+ copyFileSync(rootPath, localPath);
328
+ return { success: true, harnessId, direction, message: `Pulled ${rootPath} \u2192 ${localPath}` };
329
+ } else {
330
+ if (!existsSync(localPath)) {
331
+ return {
332
+ success: false,
333
+ harnessId,
334
+ direction,
335
+ message: `Local config not found: ${localPath}`
336
+ };
337
+ }
338
+ mkdirSync(dirname(rootPath), { recursive: true });
339
+ backupIfExists(rootPath);
340
+ copyFileSync(localPath, rootPath);
341
+ return { success: true, harnessId, direction, message: `Pushed ${localPath} \u2192 ${rootPath}` };
342
+ }
343
+ } catch (err) {
344
+ return {
345
+ success: false,
346
+ harnessId,
347
+ direction,
348
+ message: err instanceof Error ? err.message : String(err)
349
+ };
350
+ }
351
+ }
352
+ function diffConfig(harnessId, root) {
353
+ const localPath = getLocalConfigPath(harnessId);
354
+ const rootPath = getRootConfigPath(harnessId, root);
355
+ const localExists = !!localPath && existsSync(localPath);
356
+ const ssdExists = !!rootPath && existsSync(rootPath);
357
+ if (!(localExists && ssdExists)) {
358
+ return { harnessId, localExists, ssdExists, identical: false };
359
+ }
360
+ try {
361
+ const localContent = readFileSync(localPath, "utf8");
362
+ const ssdContent = readFileSync(rootPath, "utf8");
363
+ return { harnessId, localExists, ssdExists, identical: localContent === ssdContent };
364
+ } catch {
365
+ return { harnessId, localExists, ssdExists, identical: false };
366
+ }
367
+ }
368
+ function syncAllConfigs(direction, root) {
369
+ return getConfigurableHarnesses().map((id) => syncConfig(id, direction, root));
370
+ }
371
+ function diffAllConfigs(root) {
372
+ return getConfigurableHarnesses().map((id) => diffConfig(id, root));
373
+ }
374
+ function validateConfigJson(harnessId) {
375
+ const localPath = getLocalConfigPath(harnessId);
376
+ if (!localPath) return `No config path known for harness: ${harnessId}`;
377
+ if (!existsSync(localPath)) return `Config file not found: ${localPath}`;
378
+ try {
379
+ const content = readFileSync(localPath, "utf8");
380
+ JSON.parse(content);
381
+ return null;
382
+ } catch (err) {
383
+ return err instanceof Error ? err.message : String(err);
384
+ }
385
+ }
386
+ function backupIfExists(filePath) {
387
+ try {
388
+ if (existsSync(filePath)) {
389
+ copyFileSync(filePath, `${filePath}.bak`);
390
+ }
391
+ } catch {
392
+ }
393
+ }
394
+
395
+ // src/coordinator.ts
396
+ import { mkdirSync as mkdirSync2 } from "fs";
397
+ import { join as join4 } from "path";
398
+
399
+ // src/adapters/revealui-agent-adapter.ts
400
+ var DEFAULT_CONFIG = {
401
+ projectRoot: process.cwd(),
402
+ maxIterations: 10,
403
+ timeoutMs: 12e4
404
+ };
405
+ var RevealUIAgentAdapter = class {
406
+ id = "revealui-agent";
407
+ name = "RevealUI Agent";
408
+ config;
409
+ eventHandlers = /* @__PURE__ */ new Set();
410
+ constructor(config) {
411
+ this.config = { ...DEFAULT_CONFIG, ...config };
412
+ }
413
+ getCapabilities() {
414
+ return {
415
+ generateCode: true,
416
+ analyzeCode: true,
417
+ applyEdit: true,
418
+ applyConfig: false,
419
+ readWorkboard: (this.config.workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH) !== void 0,
420
+ writeWorkboard: (this.config.workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH) !== void 0
421
+ };
422
+ }
423
+ async getInfo() {
424
+ return {
425
+ id: this.id,
426
+ name: this.name,
427
+ version: "0.1.0",
428
+ capabilities: this.getCapabilities()
429
+ };
430
+ }
431
+ /**
432
+ * The RevealUI agent is available if at least one LLM provider is reachable.
433
+ * Checks in order: BitNet (localhost), Ollama (localhost), Groq (API key).
434
+ */
435
+ async isAvailable() {
436
+ if (process.env.BITNET_BASE_URL) {
437
+ try {
438
+ const url = `${process.env.BITNET_BASE_URL}/v1/models`;
439
+ const res = await fetch(url, { signal: AbortSignal.timeout(2e3) });
440
+ if (res.ok) return true;
441
+ } catch {
442
+ }
443
+ }
444
+ const ollamaUrl = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
445
+ try {
446
+ const res = await fetch(`${ollamaUrl}/api/tags`, {
447
+ signal: AbortSignal.timeout(2e3)
448
+ });
449
+ if (res.ok) return true;
450
+ } catch {
451
+ }
452
+ if (process.env.GROQ_API_KEY) return true;
453
+ return false;
454
+ }
455
+ notifyRegistered() {
456
+ this.emit({ type: "harness-connected", harnessId: this.id });
457
+ }
458
+ notifyUnregistering() {
459
+ this.emit({ type: "harness-disconnected", harnessId: this.id });
460
+ }
461
+ async execute(command) {
462
+ try {
463
+ return await this.executeInner(command);
464
+ } catch (err) {
465
+ const message = err instanceof Error ? err.message : String(err);
466
+ this.emit({ type: "error", harnessId: this.id, message });
467
+ return { success: false, command: command.type, message };
468
+ }
469
+ }
470
+ async executeInner(command) {
471
+ switch (command.type) {
472
+ case "get-status": {
473
+ const available = await this.isAvailable();
474
+ return {
475
+ success: true,
476
+ command: command.type,
477
+ data: {
478
+ available,
479
+ provider: this.config.provider ?? "auto",
480
+ model: this.config.model ?? "auto",
481
+ projectRoot: this.config.projectRoot
482
+ }
483
+ };
484
+ }
485
+ case "headless-prompt": {
486
+ return this.runHeadlessPrompt(command.prompt, command.maxTurns, command.timeoutMs);
487
+ }
488
+ case "generate-code": {
489
+ return this.runHeadlessPrompt(
490
+ `Generate code: ${command.prompt}${command.language ? ` (language: ${command.language})` : ""}${command.context ? `
491
+
492
+ Context:
493
+ ${command.context}` : ""}`
494
+ );
495
+ }
496
+ case "analyze-code": {
497
+ const question = command.question ?? "Analyze this file and explain what it does.";
498
+ return this.runHeadlessPrompt(
499
+ `Read the file at ${command.filePath} and answer: ${question}`
500
+ );
501
+ }
502
+ case "apply-edit": {
503
+ return this.runHeadlessPrompt(
504
+ `Apply the following diff to ${command.filePath}:
505
+
506
+ ${command.diff}`
507
+ );
508
+ }
509
+ case "apply-config":
510
+ case "sync-config":
511
+ case "diff-config": {
512
+ return {
513
+ success: false,
514
+ command: command.type,
515
+ message: "Config sync is not applicable \u2014 RevealUI agent uses the content layer directly"
516
+ };
517
+ }
518
+ case "read-workboard":
519
+ case "update-workboard": {
520
+ return {
521
+ success: false,
522
+ command: command.type,
523
+ message: "Workboard support not yet wired \u2014 use WorkboardManager directly"
524
+ };
525
+ }
526
+ default: {
527
+ return {
528
+ success: false,
529
+ command: command.type,
530
+ message: `Command not supported by ${this.name}`
531
+ };
532
+ }
533
+ }
534
+ }
535
+ /**
536
+ * Run a headless prompt through the coding agent.
537
+ * Lazy-imports @revealui/ai to avoid hard dependency at module load time.
538
+ * Types are inferred from the dynamic imports — no compile-time @revealui/ai dependency.
539
+ */
540
+ async runHeadlessPrompt(prompt, maxTurns, timeoutMs) {
541
+ const aiRuntimePath = "@revealui/ai/orchestration/streaming-runtime";
542
+ const aiClientPath = "@revealui/ai/llm/client";
543
+ const aiToolsPath = "@revealui/ai/tools/coding";
544
+ let runtimeMod;
545
+ let clientMod;
546
+ let toolsMod;
547
+ try {
548
+ [runtimeMod, clientMod, toolsMod] = await Promise.all([
549
+ import(aiRuntimePath),
550
+ import(aiClientPath),
551
+ import(aiToolsPath)
552
+ ]);
553
+ } catch {
554
+ return {
555
+ success: false,
556
+ command: "headless-prompt",
557
+ message: "@revealui/ai is not installed. Install it to use the RevealUI agent: npm install @revealui/ai"
558
+ };
559
+ }
560
+ const StreamingAgentRuntime = runtimeMod.StreamingAgentRuntime;
561
+ const createCodingTools = toolsMod.createCodingTools;
562
+ const projectRoot = this.config.projectRoot ?? process.cwd();
563
+ const tools = createCodingTools({ projectRoot });
564
+ let llmClient;
565
+ if (this.config.provider) {
566
+ const LLMClient = clientMod.LLMClient;
567
+ llmClient = new LLMClient({
568
+ provider: this.config.provider,
569
+ model: this.config.model,
570
+ baseURL: process.env.BITNET_BASE_URL ?? process.env.OLLAMA_BASE_URL,
571
+ apiKey: process.env.GROQ_API_KEY ?? "not-needed"
572
+ });
573
+ } else {
574
+ const createLLMClientFromEnv = clientMod.createLLMClientFromEnv;
575
+ llmClient = createLLMClientFromEnv();
576
+ }
577
+ const agent = {
578
+ id: "revealui-coding-agent",
579
+ name: "RevealUI Coding Agent",
580
+ instructions: this.buildInstructions(),
581
+ tools,
582
+ config: {},
583
+ getContext: () => ({ projectRoot, workingDirectory: projectRoot })
584
+ };
585
+ const task = {
586
+ id: `task-${Date.now()}`,
587
+ type: "headless-prompt",
588
+ description: prompt
589
+ };
590
+ const runtime = new StreamingAgentRuntime({
591
+ maxIterations: maxTurns ?? this.config.maxIterations ?? DEFAULT_CONFIG.maxIterations,
592
+ timeout: timeoutMs ?? this.config.timeoutMs ?? DEFAULT_CONFIG.timeoutMs
593
+ });
594
+ const taskId = task.id;
595
+ this.emit({ type: "generation-started", taskId });
596
+ const outputParts = [];
597
+ try {
598
+ for await (const chunk of runtime.streamTask(agent, task, llmClient)) {
599
+ switch (chunk.type) {
600
+ case "text":
601
+ if (chunk.content) outputParts.push(chunk.content);
602
+ break;
603
+ case "tool_call_result":
604
+ if (chunk.toolResult?.content) {
605
+ outputParts.push(`[tool: ${chunk.toolCall?.name}] ${chunk.toolResult.content}`);
606
+ }
607
+ break;
608
+ case "error":
609
+ if (chunk.error) outputParts.push(`[error] ${chunk.error}`);
610
+ break;
611
+ case "done":
612
+ break;
613
+ }
614
+ }
615
+ } finally {
616
+ await runtime.cleanup();
617
+ }
618
+ const output = outputParts.join("\n");
619
+ this.emit({ type: "generation-completed", taskId, output });
620
+ return {
621
+ success: true,
622
+ command: "headless-prompt",
623
+ message: output,
624
+ data: { taskId, output }
625
+ };
626
+ }
627
+ /**
628
+ * Build system instructions from the content layer.
629
+ * Loads rules from .claude/rules/ as a baseline (they are the canonical content).
630
+ */
631
+ buildInstructions() {
632
+ const lines = [
633
+ "You are the RevealUI coding agent. You help with software development tasks in this project.",
634
+ "Use the available tools to read, write, edit, search, and execute commands.",
635
+ "Always read files before modifying them. Prefer surgical edits over full rewrites.",
636
+ "Follow project conventions discovered via the project_context tool.",
637
+ ""
638
+ ];
639
+ try {
640
+ const { readdirSync, readFileSync: readFileSync2 } = __require("fs");
641
+ const { join: join5 } = __require("path");
642
+ const projectRoot = this.config.projectRoot ?? process.cwd();
643
+ const rulesDir = join5(projectRoot, ".claude", "rules");
644
+ const ruleFiles = readdirSync(rulesDir);
645
+ for (const file of ruleFiles) {
646
+ if (file.endsWith(".md")) {
647
+ const content = readFileSync2(join5(rulesDir, file), "utf8");
648
+ lines.push(`## ${file.replace(".md", "")}`, content, "");
649
+ }
650
+ }
651
+ } catch {
652
+ }
653
+ return lines.join("\n");
654
+ }
655
+ onEvent(handler) {
656
+ this.eventHandlers.add(handler);
657
+ return () => this.eventHandlers.delete(handler);
658
+ }
659
+ async dispose() {
660
+ this.eventHandlers.clear();
661
+ }
662
+ emit(event) {
663
+ for (const handler of this.eventHandlers) {
664
+ try {
665
+ handler(event);
666
+ } catch {
667
+ }
668
+ }
669
+ }
670
+ };
671
+
672
+ // src/detection/auto-detector.ts
673
+ async function autoDetectHarnesses(registry) {
674
+ const candidates = [
675
+ new RevealUIAgentAdapter(),
676
+ new ClaudeCodeAdapter(),
677
+ new CursorAdapter()
678
+ // Copilot adapter excluded — stub only, no standalone CLI available
679
+ ];
680
+ const registered = [];
681
+ await Promise.all(
682
+ candidates.map(async (adapter) => {
683
+ try {
684
+ if (await adapter.isAvailable()) {
685
+ registry.register(adapter);
686
+ registered.push(adapter.id);
687
+ } else {
688
+ await adapter.dispose();
689
+ }
690
+ } catch {
691
+ await adapter.dispose();
692
+ }
693
+ })
694
+ );
695
+ return registered;
696
+ }
697
+
698
+ // src/registry/harness-registry.ts
699
+ var HarnessRegistry = class {
700
+ adapters = /* @__PURE__ */ new Map();
701
+ /** Register an adapter. Throws if an adapter with the same id already exists. */
702
+ register(adapter) {
703
+ if (this.adapters.has(adapter.id)) {
704
+ throw new Error(`Harness adapter already registered: ${adapter.id}`);
705
+ }
706
+ this.adapters.set(adapter.id, adapter);
707
+ adapter.notifyRegistered?.();
708
+ }
709
+ /** Unregister an adapter, disposing it in the process. */
710
+ async unregister(id) {
711
+ const adapter = this.adapters.get(id);
712
+ if (adapter) {
713
+ adapter.notifyUnregistering?.();
714
+ await adapter.dispose();
715
+ this.adapters.delete(id);
716
+ }
717
+ }
718
+ /** Retrieve an adapter by id. */
719
+ get(id) {
720
+ return this.adapters.get(id);
721
+ }
722
+ /** List all registered adapter ids. */
723
+ listAll() {
724
+ return Array.from(this.adapters.keys());
725
+ }
726
+ /** List ids of adapters that report isAvailable() === true. */
727
+ async listAvailable() {
728
+ const results = await Promise.all(
729
+ Array.from(this.adapters.entries()).map(async ([id, adapter]) => ({
730
+ id,
731
+ available: await adapter.isAvailable()
732
+ }))
733
+ );
734
+ return results.filter((r) => r.available).map((r) => r.id);
735
+ }
736
+ /** Dispose all adapters and clear the registry. */
737
+ async disposeAll() {
738
+ await Promise.all(Array.from(this.adapters.values()).map((a) => a.dispose()));
739
+ this.adapters.clear();
740
+ }
741
+ };
742
+
743
+ // src/server/http-gateway.ts
744
+ import { randomBytes } from "crypto";
745
+ import { createReadStream, existsSync as existsSync2, statSync } from "fs";
746
+ import { createServer } from "http";
747
+ import { extname, join as join2, normalize } from "path";
748
+ var MIME_TYPES = {
749
+ ".html": "text/html; charset=utf-8",
750
+ ".js": "application/javascript; charset=utf-8",
751
+ ".css": "text/css; charset=utf-8",
752
+ ".json": "application/json; charset=utf-8",
753
+ ".png": "image/png",
754
+ ".jpg": "image/jpeg",
755
+ ".jpeg": "image/jpeg",
756
+ ".svg": "image/svg+xml",
757
+ ".ico": "image/x-icon",
758
+ ".woff": "font/woff",
759
+ ".woff2": "font/woff2",
760
+ ".ttf": "font/ttf",
761
+ ".wasm": "application/wasm"
762
+ };
763
+ var HttpGateway = class {
764
+ server;
765
+ config;
766
+ /** 6-digit pairing code (regenerated on each start) */
767
+ pairingCode;
768
+ /** Active session tokens (bearer tokens granted after pairing) */
769
+ sessionTokens = /* @__PURE__ */ new Set();
770
+ /** Whether pairing has been completed at least once */
771
+ paired = false;
772
+ constructor(config) {
773
+ this.config = config;
774
+ this.pairingCode = generatePairingCode();
775
+ this.server = createServer((req, res) => this.handleRequest(req, res));
776
+ }
777
+ /** The current pairing code (display this in Studio/terminal) */
778
+ getPairingCode() {
779
+ return this.pairingCode;
780
+ }
781
+ /** Regenerate the pairing code (invalidates previous code) */
782
+ regeneratePairingCode() {
783
+ this.pairingCode = generatePairingCode();
784
+ return this.pairingCode;
785
+ }
786
+ async start() {
787
+ return new Promise((resolve, reject) => {
788
+ this.server.listen(this.config.port, this.config.host, () => resolve());
789
+ this.server.once("error", reject);
790
+ });
791
+ }
792
+ async stop() {
793
+ return new Promise((resolve) => this.server.close(() => resolve()));
794
+ }
795
+ handleRequest(req, res) {
796
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
797
+ const path = url.pathname;
798
+ res.setHeader("Access-Control-Allow-Origin", "*");
799
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
800
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
801
+ if (req.method === "OPTIONS") {
802
+ res.writeHead(204);
803
+ res.end();
804
+ return;
805
+ }
806
+ if (path === "/api/pair" && req.method === "POST") {
807
+ this.handlePair(req, res);
808
+ return;
809
+ }
810
+ if (path === "/rpc" || path.startsWith("/api/")) {
811
+ if (!this.checkAuth(req, res)) return;
812
+ if (path === "/rpc" && req.method === "POST") {
813
+ this.handleRpc(req, res);
814
+ return;
815
+ }
816
+ if (path === "/api/pair" && req.method === "GET") {
817
+ this.handlePairStatus(res);
818
+ return;
819
+ }
820
+ if (path === "/api/status") {
821
+ this.handleStatus(res);
822
+ return;
823
+ }
824
+ if (path.startsWith("/api/stream") && req.method === "GET") {
825
+ const sessionFilter = path.split("/")[3] ?? null;
826
+ this.handleStream(req, res, sessionFilter);
827
+ return;
828
+ }
829
+ jsonResponse(res, 404, { error: "Not found" });
830
+ return;
831
+ }
832
+ this.handleStatic(path, res);
833
+ }
834
+ /** Verify the Authorization: Bearer <token> header */
835
+ checkAuth(req, res) {
836
+ if (this.sessionTokens.size === 0 && !this.paired) {
837
+ return true;
838
+ }
839
+ const authHeader = req.headers.authorization ?? "";
840
+ if (!authHeader.startsWith("Bearer ")) {
841
+ jsonResponse(res, 401, { error: "Authorization required", paired: this.paired });
842
+ return false;
843
+ }
844
+ const token = authHeader.slice(7);
845
+ if (!this.sessionTokens.has(token)) {
846
+ jsonResponse(res, 403, { error: "Invalid token" });
847
+ return false;
848
+ }
849
+ return true;
850
+ }
851
+ /** POST /api/pair — submit pairing code, receive session token */
852
+ handlePair(req, res) {
853
+ let body = "";
854
+ req.on("data", (chunk) => {
855
+ body += chunk.toString();
856
+ if (body.length > 1024) {
857
+ res.writeHead(413);
858
+ res.end();
859
+ req.destroy();
860
+ }
861
+ });
862
+ req.on("end", () => {
863
+ try {
864
+ const { code } = JSON.parse(body);
865
+ if (code !== this.pairingCode) {
866
+ jsonResponse(res, 403, { error: "Invalid pairing code" });
867
+ return;
868
+ }
869
+ const token = randomBytes(32).toString("hex");
870
+ this.sessionTokens.add(token);
871
+ this.paired = true;
872
+ this.pairingCode = generatePairingCode();
873
+ jsonResponse(res, 200, { token, expires: null });
874
+ } catch {
875
+ jsonResponse(res, 400, { error: "Invalid JSON" });
876
+ }
877
+ });
878
+ }
879
+ /** GET /api/pair — check pairing status */
880
+ handlePairStatus(res) {
881
+ jsonResponse(res, 200, {
882
+ paired: this.paired,
883
+ activeSessions: this.sessionTokens.size
884
+ });
885
+ }
886
+ /** GET /api/status — daemon status summary */
887
+ handleStatus(res) {
888
+ jsonResponse(res, 200, {
889
+ daemon: "revdev-harness",
890
+ pid: process.pid,
891
+ uptime: process.uptime(),
892
+ paired: this.paired,
893
+ activeSessions: this.sessionTokens.size
894
+ });
895
+ }
896
+ /** POST /rpc — proxy JSON-RPC to the daemon's dispatch */
897
+ handleRpc(req, res) {
898
+ let body = "";
899
+ req.on("data", (chunk) => {
900
+ body += chunk.toString();
901
+ if (body.length > 1048576) {
902
+ res.writeHead(413);
903
+ res.end();
904
+ req.destroy();
905
+ }
906
+ });
907
+ req.on("end", () => {
908
+ this.config.rpcDispatch.dispatchHttp(body, (response) => {
909
+ jsonResponse(res, 200, response);
910
+ });
911
+ });
912
+ }
913
+ /** GET /api/stream[/:sessionId] — SSE for agent output and exit events */
914
+ handleStream(req, res, sessionFilter) {
915
+ const spawner = this.config.spawner;
916
+ if (!spawner) {
917
+ jsonResponse(res, 503, { error: "Spawner not available" });
918
+ return;
919
+ }
920
+ res.writeHead(200, {
921
+ "Content-Type": "text/event-stream",
922
+ "Cache-Control": "no-cache",
923
+ Connection: "keep-alive",
924
+ "Access-Control-Allow-Origin": "*"
925
+ });
926
+ res.write(": connected\n\n");
927
+ const onOutput = (evt) => {
928
+ if (sessionFilter && evt.sessionId !== sessionFilter) return;
929
+ res.write(`event: output
930
+ data: ${JSON.stringify(evt)}
931
+
932
+ `);
933
+ };
934
+ const onExit = (evt) => {
935
+ if (sessionFilter && evt.sessionId !== sessionFilter) return;
936
+ res.write(`event: exit
937
+ data: ${JSON.stringify(evt)}
938
+
939
+ `);
940
+ };
941
+ spawner.on("output", onOutput);
942
+ spawner.on("exit", onExit);
943
+ const keepalive = setInterval(() => {
944
+ res.write(": keepalive\n\n");
945
+ }, 3e4);
946
+ req.on("close", () => {
947
+ clearInterval(keepalive);
948
+ spawner.off("output", onOutput);
949
+ spawner.off("exit", onExit);
950
+ });
951
+ }
952
+ /** Serve static files from the Studio build directory */
953
+ handleStatic(urlPath, res) {
954
+ if (!this.config.staticDir) {
955
+ jsonResponse(res, 404, { error: "Static serving not configured" });
956
+ return;
957
+ }
958
+ const filePath = urlPath === "/" ? "/index.html" : urlPath;
959
+ const normalized = normalize(filePath);
960
+ if (normalized.includes("..")) {
961
+ res.writeHead(400);
962
+ res.end("Bad request");
963
+ return;
964
+ }
965
+ const fullPath = join2(this.config.staticDir, normalized);
966
+ const targetPath = existsSync2(fullPath) && statSync(fullPath).isFile() ? fullPath : join2(this.config.staticDir, "index.html");
967
+ if (!existsSync2(targetPath)) {
968
+ res.writeHead(404);
969
+ res.end("Not found");
970
+ return;
971
+ }
972
+ const ext = extname(targetPath);
973
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
974
+ res.writeHead(200, { "Content-Type": contentType });
975
+ createReadStream(targetPath).pipe(res);
976
+ }
977
+ };
978
+ function generatePairingCode() {
979
+ const bytes = randomBytes(4);
980
+ const num = bytes.readUInt32BE(0) % 1e6;
981
+ return num.toString().padStart(6, "0");
982
+ }
983
+ function jsonResponse(res, status, body) {
984
+ const json = JSON.stringify(body);
985
+ res.writeHead(status, {
986
+ "Content-Type": "application/json; charset=utf-8",
987
+ "Content-Length": Buffer.byteLength(json)
988
+ });
989
+ res.end(json);
990
+ }
991
+
992
+ // src/server/inference-service.ts
993
+ import { execFile as execFile3 } from "child_process";
994
+ import { promisify as promisify3 } from "util";
995
+ var execFileAsync3 = promisify3(execFile3);
996
+ var KNOWN_SNAPS = [
997
+ ["nemotron-3-nano", "General (reasoning + non-reasoning) \u2014 free tier default"],
998
+ ["gemma3", "General + vision \u2014 image understanding, multimodal"],
999
+ ["deepseek-r1", "Reasoning \u2014 complex analysis, chain-of-thought"],
1000
+ ["qwen-vl", "Vision-language \u2014 document parsing, visual Q&A"]
1001
+ ];
1002
+ var BITNET_MODEL_PATHS = [
1003
+ `${process.env.HOME ?? "/root"}/models/bitnet`,
1004
+ "/mnt/forge/models/bitnet"
1005
+ ];
1006
+ async function commandExists(cmd) {
1007
+ try {
1008
+ await execFileAsync3("which", [cmd]);
1009
+ return true;
1010
+ } catch {
1011
+ return false;
1012
+ }
1013
+ }
1014
+ async function run(cmd, args) {
1015
+ return execFileAsync3(cmd, args, { timeout: 3e4 });
1016
+ }
1017
+ var InferenceService = class {
1018
+ // ── Ollama ──────────────────────────────────────────────────────
1019
+ async ollamaStatus() {
1020
+ const installed = await commandExists("ollama");
1021
+ if (!installed) return { installed: false, running: false, version: null };
1022
+ let version = null;
1023
+ try {
1024
+ const { stdout } = await run("ollama", ["--version"]);
1025
+ version = stdout.trim() || null;
1026
+ } catch {
1027
+ }
1028
+ let running = false;
1029
+ try {
1030
+ await run("ollama", ["list"]);
1031
+ running = true;
1032
+ } catch {
1033
+ }
1034
+ return { installed, running, version };
1035
+ }
1036
+ async ollamaModels() {
1037
+ const { stdout } = await run("ollama", ["list"]);
1038
+ const models = [];
1039
+ const lines = stdout.split("\n");
1040
+ for (let i = 1; i < lines.length; i++) {
1041
+ const line = lines[i]?.trim();
1042
+ if (!line) continue;
1043
+ const parts = line.split(/\s+/);
1044
+ if (parts.length >= 3) {
1045
+ models.push({
1046
+ name: parts[0] ?? "",
1047
+ size: parts[2] ?? "",
1048
+ modified: parts.slice(3).join(" ")
1049
+ });
1050
+ }
1051
+ }
1052
+ return models;
1053
+ }
1054
+ async ollamaPull(modelName) {
1055
+ try {
1056
+ const { stdout, stderr } = await execFileAsync3("ollama", ["pull", modelName], {
1057
+ timeout: 6e5
1058
+ // 10 min for large models
1059
+ });
1060
+ return { success: true, message: stdout || stderr };
1061
+ } catch (err) {
1062
+ return { success: false, message: err instanceof Error ? err.message : String(err) };
1063
+ }
1064
+ }
1065
+ async ollamaDelete(modelName) {
1066
+ await run("ollama", ["rm", modelName]);
1067
+ }
1068
+ async ollamaStart() {
1069
+ const { spawn } = await import("child_process");
1070
+ const child = spawn("ollama", ["serve"], {
1071
+ stdio: "ignore",
1072
+ detached: true
1073
+ });
1074
+ child.unref();
1075
+ }
1076
+ async ollamaStop() {
1077
+ try {
1078
+ await run("pkill", ["-f", "ollama serve"]);
1079
+ } catch {
1080
+ }
1081
+ }
1082
+ // ── BitNet ──────────────────────────────────────────────────────
1083
+ async bitnetStatus() {
1084
+ const installed = await commandExists("bitnet");
1085
+ let modelPath = null;
1086
+ if (installed) {
1087
+ const { existsSync: existsSync4 } = await import("fs");
1088
+ for (const p of BITNET_MODEL_PATHS) {
1089
+ if (existsSync4(p)) {
1090
+ modelPath = p;
1091
+ break;
1092
+ }
1093
+ }
1094
+ }
1095
+ return { installed, modelPath };
1096
+ }
1097
+ // ── Inference Snaps ─────────────────────────────────────────────
1098
+ async snapList() {
1099
+ const results = [];
1100
+ for (const [name, description] of KNOWN_SNAPS) {
1101
+ let installed = false;
1102
+ try {
1103
+ await run("snap", ["list", name]);
1104
+ installed = true;
1105
+ } catch {
1106
+ }
1107
+ results.push({ name, description, installed });
1108
+ }
1109
+ return results;
1110
+ }
1111
+ async snapStatus(snapName) {
1112
+ let installed = false;
1113
+ let version = null;
1114
+ try {
1115
+ const { stdout } = await run("snap", ["list", snapName]);
1116
+ installed = true;
1117
+ const secondLine = stdout.split("\n")[1];
1118
+ if (secondLine) {
1119
+ version = secondLine.split(/\s+/)[1] ?? null;
1120
+ }
1121
+ } catch {
1122
+ return { installed: false, running: false, snapName, endpoint: null, version: null };
1123
+ }
1124
+ let running = false;
1125
+ try {
1126
+ await run(snapName, ["status"]);
1127
+ running = true;
1128
+ } catch {
1129
+ }
1130
+ const endpoint = running ? "http://localhost:9090/v1" : null;
1131
+ return { installed, running, snapName, endpoint, version };
1132
+ }
1133
+ async snapInstall(snapName) {
1134
+ const known = KNOWN_SNAPS.some(([name]) => name === snapName);
1135
+ if (!known) throw new Error(`Unknown inference snap: ${snapName}`);
1136
+ try {
1137
+ const { stdout, stderr } = await execFileAsync3("sudo", ["snap", "install", snapName], {
1138
+ timeout: 3e5
1139
+ // 5 min for large snaps
1140
+ });
1141
+ return { success: true, message: stdout || stderr };
1142
+ } catch (err) {
1143
+ return { success: false, message: err instanceof Error ? err.message : String(err) };
1144
+ }
1145
+ }
1146
+ async snapRemove(snapName) {
1147
+ await execFileAsync3("sudo", ["snap", "remove", snapName], { timeout: 6e4 });
1148
+ }
1149
+ };
1150
+
1151
+ // src/server/rpc-server.ts
1152
+ import { existsSync as existsSync3, unlinkSync } from "fs";
1153
+ import { createServer as createServer2 } from "net";
1154
+
1155
+ // src/detection/process-detector.ts
1156
+ import { execFile as execFile4 } from "child_process";
1157
+ import { readdir } from "fs/promises";
1158
+ import { join as join3 } from "path";
1159
+ import { promisify as promisify4 } from "util";
1160
+ var execFileAsync4 = promisify4(execFile4);
1161
+ async function findProcesses(pattern) {
1162
+ try {
1163
+ const { stdout } = await execFileAsync4("pgrep", ["-a", pattern], { timeout: 3e3 });
1164
+ return stdout.trim().split("\n").filter(Boolean).map((line) => {
1165
+ const spaceIdx = line.indexOf(" ");
1166
+ const pid = parseInt(line.slice(0, spaceIdx), 10);
1167
+ const command = line.slice(spaceIdx + 1);
1168
+ return { pid, command };
1169
+ });
1170
+ } catch {
1171
+ return [];
1172
+ }
1173
+ }
1174
+ var HARNESS_PROCESS_PATTERNS = {
1175
+ "claude-code": ["claude"],
1176
+ cursor: ["cursor", "Cursor"],
1177
+ copilot: ["copilot"]
1178
+ };
1179
+ async function findHarnessProcesses(harnessId) {
1180
+ const patterns = HARNESS_PROCESS_PATTERNS[harnessId];
1181
+ if (!patterns) return [];
1182
+ const results = await Promise.all(patterns.map((p) => findProcesses(p)));
1183
+ return results.flat().map((p) => ({ ...p, harnessId }));
1184
+ }
1185
+ async function findAllHarnessProcesses() {
1186
+ const results = await Promise.all(
1187
+ Object.keys(HARNESS_PROCESS_PATTERNS).map((id) => findHarnessProcesses(id))
1188
+ );
1189
+ return results.flat();
1190
+ }
1191
+ async function findClaudeCodeSockets() {
1192
+ const dirs = [
1193
+ "/tmp",
1194
+ process.env.XDG_RUNTIME_DIR,
1195
+ join3(process.env.HOME ?? "/tmp", ".claude")
1196
+ ].filter(Boolean);
1197
+ const sockets = [];
1198
+ for (const dir of dirs) {
1199
+ try {
1200
+ const entries = await readdir(dir);
1201
+ for (const entry of entries) {
1202
+ if (/^claude.*\.sock$/.test(entry)) {
1203
+ sockets.push(join3(dir, entry));
1204
+ }
1205
+ }
1206
+ } catch {
1207
+ }
1208
+ }
1209
+ return sockets;
1210
+ }
1211
+
1212
+ // src/server/rpc-server.ts
1213
+ var ERR_PARSE = -32700;
1214
+ var ERR_INVALID_PARAMS = -32602;
1215
+ var ERR_METHOD_NOT_FOUND = -32601;
1216
+ var ERR_INTERNAL = -32603;
1217
+ var RpcServer = class {
1218
+ constructor(registry, socketPath, store) {
1219
+ this.registry = registry;
1220
+ this.socketPath = socketPath;
1221
+ this.store = store;
1222
+ this.server.on("connection", (socket) => {
1223
+ let buffer = "";
1224
+ socket.on("data", (chunk) => {
1225
+ buffer += chunk.toString();
1226
+ const lines = buffer.split("\n");
1227
+ buffer = lines.pop() ?? "";
1228
+ for (const line of lines) {
1229
+ this.handleLine(line.trim(), (response) => {
1230
+ socket.write(`${JSON.stringify(response)}
1231
+ `);
1232
+ });
1233
+ }
1234
+ });
1235
+ });
1236
+ }
1237
+ server = createServer2();
1238
+ healthCheckFn = null;
1239
+ spawner = null;
1240
+ inference = null;
1241
+ handleLine(line, reply) {
1242
+ let req;
1243
+ try {
1244
+ req = JSON.parse(line);
1245
+ } catch {
1246
+ reply({ jsonrpc: "2.0", id: null, error: { code: ERR_PARSE, message: "Parse error" } });
1247
+ return;
1248
+ }
1249
+ this.dispatch(req).then(reply).catch((err) => {
1250
+ reply({
1251
+ jsonrpc: "2.0",
1252
+ id: req.id,
1253
+ error: { code: ERR_INTERNAL, message: err instanceof Error ? err.message : String(err) }
1254
+ });
1255
+ });
1256
+ }
1257
+ async dispatch(req) {
1258
+ const { id, method, params } = req;
1259
+ const p = params ?? {};
1260
+ switch (method) {
1261
+ case "harness.list": {
1262
+ const ids = await this.registry.listAvailable();
1263
+ const infos = await Promise.all(ids.map((id2) => this.registry.get(id2)?.getInfo()));
1264
+ return { jsonrpc: "2.0", id, result: infos };
1265
+ }
1266
+ case "harness.info": {
1267
+ const harnessId = p.harnessId;
1268
+ if (!harnessId) {
1269
+ return {
1270
+ jsonrpc: "2.0",
1271
+ id,
1272
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId required" }
1273
+ };
1274
+ }
1275
+ const adapter = this.registry.get(harnessId);
1276
+ if (!adapter) {
1277
+ return {
1278
+ jsonrpc: "2.0",
1279
+ id,
1280
+ error: { code: ERR_INVALID_PARAMS, message: `Harness not found: ${harnessId}` }
1281
+ };
1282
+ }
1283
+ return { jsonrpc: "2.0", id, result: await adapter.getInfo() };
1284
+ }
1285
+ case "harness.execute": {
1286
+ const harnessId = p.harnessId;
1287
+ const command = p.command;
1288
+ if (!(harnessId && command)) {
1289
+ return {
1290
+ jsonrpc: "2.0",
1291
+ id,
1292
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId and command required" }
1293
+ };
1294
+ }
1295
+ const adapter = this.registry.get(harnessId);
1296
+ if (!adapter) {
1297
+ return {
1298
+ jsonrpc: "2.0",
1299
+ id,
1300
+ error: { code: ERR_INVALID_PARAMS, message: `Harness not found: ${harnessId}` }
1301
+ };
1302
+ }
1303
+ const result = await adapter.execute(command);
1304
+ return { jsonrpc: "2.0", id, result };
1305
+ }
1306
+ case "harness.syncConfig": {
1307
+ const harnessId = p.harnessId;
1308
+ const direction = p.direction;
1309
+ if (!(harnessId && direction)) {
1310
+ return {
1311
+ jsonrpc: "2.0",
1312
+ id,
1313
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId and direction required" }
1314
+ };
1315
+ }
1316
+ return { jsonrpc: "2.0", id, result: syncConfig(harnessId, direction) };
1317
+ }
1318
+ case "harness.diffConfig": {
1319
+ const harnessId = p.harnessId;
1320
+ if (!harnessId) {
1321
+ return {
1322
+ jsonrpc: "2.0",
1323
+ id,
1324
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId required" }
1325
+ };
1326
+ }
1327
+ return { jsonrpc: "2.0", id, result: diffConfig(harnessId) };
1328
+ }
1329
+ case "harness.listRunning": {
1330
+ const harnessId = p.harnessId;
1331
+ if (!harnessId) {
1332
+ return {
1333
+ jsonrpc: "2.0",
1334
+ id,
1335
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId required" }
1336
+ };
1337
+ }
1338
+ const processes = await findHarnessProcesses(harnessId);
1339
+ return { jsonrpc: "2.0", id, result: processes };
1340
+ }
1341
+ case "ping": {
1342
+ return { jsonrpc: "2.0", id, result: { status: "ok", pid: process.pid } };
1343
+ }
1344
+ case "harness.health": {
1345
+ if (!this.healthCheckFn) {
1346
+ return {
1347
+ jsonrpc: "2.0",
1348
+ id,
1349
+ error: { code: ERR_INTERNAL, message: "Health check not configured" }
1350
+ };
1351
+ }
1352
+ const health = await this.healthCheckFn();
1353
+ return { jsonrpc: "2.0", id, result: health };
1354
+ }
1355
+ // -----------------------------------------------------------------------
1356
+ // Session management (PGlite-backed)
1357
+ // -----------------------------------------------------------------------
1358
+ case "session.register": {
1359
+ if (!this.store) return this.noStore(id);
1360
+ const agentId = p.agentId;
1361
+ const env = p.env;
1362
+ if (!agentId) return this.missingParam(id, "agentId");
1363
+ const session = await this.store.registerSession({
1364
+ id: agentId,
1365
+ env: env ?? agentId,
1366
+ task: p.task,
1367
+ pid: p.pid
1368
+ });
1369
+ return { jsonrpc: "2.0", id, result: { session } };
1370
+ }
1371
+ case "session.update": {
1372
+ if (!this.store) return this.noStore(id);
1373
+ const agentId = p.agentId;
1374
+ if (!agentId) return this.missingParam(id, "agentId");
1375
+ const session = await this.store.updateSession(agentId, {
1376
+ task: p.task,
1377
+ files: p.files
1378
+ });
1379
+ return { jsonrpc: "2.0", id, result: session };
1380
+ }
1381
+ case "session.end": {
1382
+ if (!this.store) return this.noStore(id);
1383
+ const agentId = p.agentId;
1384
+ if (!agentId) return this.missingParam(id, "agentId");
1385
+ await this.store.endSession(agentId, p.exitSummary);
1386
+ await this.store.releaseAllReservations(agentId);
1387
+ await this.store.logEvent({
1388
+ agentId,
1389
+ eventType: "session-end",
1390
+ payload: { exitSummary: p.exitSummary }
1391
+ });
1392
+ return { jsonrpc: "2.0", id, result: { ok: true } };
1393
+ }
1394
+ case "session.list": {
1395
+ if (!this.store) return this.noStore(id);
1396
+ const sessions = await this.store.getActiveSessions();
1397
+ return { jsonrpc: "2.0", id, result: sessions };
1398
+ }
1399
+ case "session.history": {
1400
+ if (!this.store) return this.noStore(id);
1401
+ const agentId = p.agentId;
1402
+ if (!agentId) return this.missingParam(id, "agentId");
1403
+ const limit = p.limit ?? 10;
1404
+ const history = await this.store.getSessionHistory(agentId, limit);
1405
+ return { jsonrpc: "2.0", id, result: history };
1406
+ }
1407
+ // -----------------------------------------------------------------------
1408
+ // Inter-agent messaging (PGlite-backed)
1409
+ // -----------------------------------------------------------------------
1410
+ case "mail.send": {
1411
+ if (!this.store) return this.noStore(id);
1412
+ const fromAgent = p.fromAgent;
1413
+ const toAgent = p.toAgent;
1414
+ const subject = p.subject;
1415
+ if (!(fromAgent && toAgent && subject)) {
1416
+ return this.missingParam(id, "fromAgent, toAgent, subject");
1417
+ }
1418
+ const msg = await this.store.sendMessage({
1419
+ fromAgent,
1420
+ toAgent,
1421
+ subject,
1422
+ body: p.body
1423
+ });
1424
+ return { jsonrpc: "2.0", id, result: msg };
1425
+ }
1426
+ case "mail.broadcast": {
1427
+ if (!this.store) return this.noStore(id);
1428
+ const fromAgent = p.fromAgent;
1429
+ const subject = p.subject;
1430
+ if (!(fromAgent && subject)) return this.missingParam(id, "fromAgent, subject");
1431
+ const sent = await this.store.broadcastMessage({
1432
+ fromAgent,
1433
+ subject,
1434
+ body: p.body
1435
+ });
1436
+ return { jsonrpc: "2.0", id, result: { sent } };
1437
+ }
1438
+ case "mail.inbox": {
1439
+ if (!this.store) return this.noStore(id);
1440
+ const agentId = p.agentId;
1441
+ if (!agentId) return this.missingParam(id, "agentId");
1442
+ const unreadOnly = p.unreadOnly ?? true;
1443
+ const messages = await this.store.getInbox(agentId, unreadOnly);
1444
+ return { jsonrpc: "2.0", id, result: messages };
1445
+ }
1446
+ case "mail.markRead": {
1447
+ if (!this.store) return this.noStore(id);
1448
+ const messageIds = p.messageIds;
1449
+ if (!messageIds) return this.missingParam(id, "messageIds");
1450
+ await this.store.markRead(messageIds);
1451
+ return { jsonrpc: "2.0", id, result: { ok: true } };
1452
+ }
1453
+ // -----------------------------------------------------------------------
1454
+ // File reservations (PGlite-backed)
1455
+ // -----------------------------------------------------------------------
1456
+ case "files.reserve": {
1457
+ if (!this.store) return this.noStore(id);
1458
+ const filePath = p.filePath;
1459
+ const agentId = p.agentId;
1460
+ if (!(filePath && agentId)) return this.missingParam(id, "filePath, agentId");
1461
+ const ttlSeconds = p.ttlSeconds ?? 3600;
1462
+ const reservation = await this.store.reserveFile({
1463
+ filePath,
1464
+ agentId,
1465
+ ttlSeconds,
1466
+ reason: p.reason
1467
+ });
1468
+ return { jsonrpc: "2.0", id, result: reservation };
1469
+ }
1470
+ case "files.check": {
1471
+ if (!this.store) return this.noStore(id);
1472
+ const filePath = p.filePath;
1473
+ if (!filePath) return this.missingParam(id, "filePath");
1474
+ const reservation = await this.store.checkReservation(filePath);
1475
+ return { jsonrpc: "2.0", id, result: reservation };
1476
+ }
1477
+ case "files.release": {
1478
+ if (!this.store) return this.noStore(id);
1479
+ const agentId = p.agentId;
1480
+ if (!agentId) return this.missingParam(id, "agentId");
1481
+ const released = await this.store.releaseAllReservations(agentId);
1482
+ return { jsonrpc: "2.0", id, result: { released } };
1483
+ }
1484
+ case "files.list": {
1485
+ if (!this.store) return this.noStore(id);
1486
+ const agentId = p.agentId;
1487
+ if (!agentId) return this.missingParam(id, "agentId");
1488
+ const reservations = await this.store.getReservations(agentId);
1489
+ return { jsonrpc: "2.0", id, result: reservations };
1490
+ }
1491
+ // -----------------------------------------------------------------------
1492
+ // Tasks (PGlite-backed)
1493
+ // -----------------------------------------------------------------------
1494
+ case "tasks.create": {
1495
+ if (!this.store) return this.noStore(id);
1496
+ const taskId = p.taskId;
1497
+ const description = p.description;
1498
+ if (!taskId) return this.missingParam(id, "taskId");
1499
+ const task = await this.store.createTask({ id: taskId, description: description ?? "" });
1500
+ return { jsonrpc: "2.0", id, result: task };
1501
+ }
1502
+ case "tasks.claim": {
1503
+ if (!this.store) return this.noStore(id);
1504
+ const taskId = p.taskId;
1505
+ const agentId = p.agentId;
1506
+ if (!(taskId && agentId)) return this.missingParam(id, "taskId, agentId");
1507
+ const claim = await this.store.claimTask(taskId, agentId);
1508
+ return { jsonrpc: "2.0", id, result: claim };
1509
+ }
1510
+ case "tasks.complete": {
1511
+ if (!this.store) return this.noStore(id);
1512
+ const taskId = p.taskId;
1513
+ const agentId = p.agentId;
1514
+ if (!(taskId && agentId)) return this.missingParam(id, "taskId, agentId");
1515
+ const completed = await this.store.completeTask(taskId, agentId);
1516
+ return { jsonrpc: "2.0", id, result: { ok: completed } };
1517
+ }
1518
+ case "tasks.release": {
1519
+ if (!this.store) return this.noStore(id);
1520
+ const taskId = p.taskId;
1521
+ const agentId = p.agentId;
1522
+ if (!(taskId && agentId)) return this.missingParam(id, "taskId, agentId");
1523
+ const released = await this.store.releaseTask(taskId, agentId);
1524
+ return { jsonrpc: "2.0", id, result: { ok: released } };
1525
+ }
1526
+ case "tasks.list": {
1527
+ if (!this.store) return this.noStore(id);
1528
+ const tasks = await this.store.listTasks({
1529
+ status: p.status,
1530
+ owner: p.owner
1531
+ });
1532
+ return { jsonrpc: "2.0", id, result: tasks };
1533
+ }
1534
+ // -----------------------------------------------------------------------
1535
+ // Events (PGlite-backed)
1536
+ // -----------------------------------------------------------------------
1537
+ case "events.log": {
1538
+ if (!this.store) return this.noStore(id);
1539
+ const agentId = p.agentId;
1540
+ const eventType = p.eventType;
1541
+ if (!(agentId && eventType)) return this.missingParam(id, "agentId, eventType");
1542
+ const event = await this.store.logEvent({
1543
+ agentId,
1544
+ eventType,
1545
+ payload: p.payload
1546
+ });
1547
+ return { jsonrpc: "2.0", id, result: event };
1548
+ }
1549
+ case "events.recent": {
1550
+ if (!this.store) return this.noStore(id);
1551
+ const limit = p.limit ?? 50;
1552
+ const events = await this.store.getRecentEvents(limit);
1553
+ return { jsonrpc: "2.0", id, result: events };
1554
+ }
1555
+ // -----------------------------------------------------------------------
1556
+ // Agent spawner (process management)
1557
+ // -----------------------------------------------------------------------
1558
+ case "agent.spawn": {
1559
+ if (!this.spawner) return this.noService(id, "spawner");
1560
+ const name = p.name;
1561
+ const backend = p.backend;
1562
+ const model = p.model;
1563
+ const prompt = p.prompt;
1564
+ if (!(name && backend && model && prompt)) {
1565
+ return this.missingParam(id, "name, backend, model, prompt");
1566
+ }
1567
+ const sessionId = this.spawner.spawn(
1568
+ name,
1569
+ backend,
1570
+ model,
1571
+ prompt
1572
+ );
1573
+ return { jsonrpc: "2.0", id, result: { sessionId } };
1574
+ }
1575
+ case "agent.stop": {
1576
+ if (!this.spawner) return this.noService(id, "spawner");
1577
+ const sessionId = p.sessionId;
1578
+ if (!sessionId) return this.missingParam(id, "sessionId");
1579
+ this.spawner.stop(sessionId);
1580
+ return { jsonrpc: "2.0", id, result: { ok: true } };
1581
+ }
1582
+ case "agent.list": {
1583
+ if (!this.spawner) return this.noService(id, "spawner");
1584
+ return { jsonrpc: "2.0", id, result: this.spawner.list() };
1585
+ }
1586
+ case "agent.remove": {
1587
+ if (!this.spawner) return this.noService(id, "spawner");
1588
+ const sessionId = p.sessionId;
1589
+ if (!sessionId) return this.missingParam(id, "sessionId");
1590
+ this.spawner.remove(sessionId);
1591
+ return { jsonrpc: "2.0", id, result: { ok: true } };
1592
+ }
1593
+ // -----------------------------------------------------------------------
1594
+ // Inference management (Ollama, BitNet, Snaps)
1595
+ // -----------------------------------------------------------------------
1596
+ case "inference.ollama.status": {
1597
+ if (!this.inference) return this.noService(id, "inference");
1598
+ return { jsonrpc: "2.0", id, result: await this.inference.ollamaStatus() };
1599
+ }
1600
+ case "inference.ollama.models": {
1601
+ if (!this.inference) return this.noService(id, "inference");
1602
+ return { jsonrpc: "2.0", id, result: await this.inference.ollamaModels() };
1603
+ }
1604
+ case "inference.ollama.pull": {
1605
+ if (!this.inference) return this.noService(id, "inference");
1606
+ const modelName = p.modelName;
1607
+ if (!modelName) return this.missingParam(id, "modelName");
1608
+ return { jsonrpc: "2.0", id, result: await this.inference.ollamaPull(modelName) };
1609
+ }
1610
+ case "inference.ollama.delete": {
1611
+ if (!this.inference) return this.noService(id, "inference");
1612
+ const modelName = p.modelName;
1613
+ if (!modelName) return this.missingParam(id, "modelName");
1614
+ await this.inference.ollamaDelete(modelName);
1615
+ return { jsonrpc: "2.0", id, result: { ok: true } };
1616
+ }
1617
+ case "inference.ollama.start": {
1618
+ if (!this.inference) return this.noService(id, "inference");
1619
+ await this.inference.ollamaStart();
1620
+ return { jsonrpc: "2.0", id, result: { ok: true } };
1621
+ }
1622
+ case "inference.ollama.stop": {
1623
+ if (!this.inference) return this.noService(id, "inference");
1624
+ await this.inference.ollamaStop();
1625
+ return { jsonrpc: "2.0", id, result: { ok: true } };
1626
+ }
1627
+ case "inference.bitnet.status": {
1628
+ if (!this.inference) return this.noService(id, "inference");
1629
+ return { jsonrpc: "2.0", id, result: await this.inference.bitnetStatus() };
1630
+ }
1631
+ case "inference.snap.list": {
1632
+ if (!this.inference) return this.noService(id, "inference");
1633
+ return { jsonrpc: "2.0", id, result: await this.inference.snapList() };
1634
+ }
1635
+ case "inference.snap.status": {
1636
+ if (!this.inference) return this.noService(id, "inference");
1637
+ const snapName = p.snapName;
1638
+ if (!snapName) return this.missingParam(id, "snapName");
1639
+ return { jsonrpc: "2.0", id, result: await this.inference.snapStatus(snapName) };
1640
+ }
1641
+ case "inference.snap.install": {
1642
+ if (!this.inference) return this.noService(id, "inference");
1643
+ const snapName = p.snapName;
1644
+ if (!snapName) return this.missingParam(id, "snapName");
1645
+ return { jsonrpc: "2.0", id, result: await this.inference.snapInstall(snapName) };
1646
+ }
1647
+ case "inference.snap.remove": {
1648
+ if (!this.inference) return this.noService(id, "inference");
1649
+ const snapName = p.snapName;
1650
+ if (!snapName) return this.missingParam(id, "snapName");
1651
+ await this.inference.snapRemove(snapName);
1652
+ return { jsonrpc: "2.0", id, result: { ok: true } };
1653
+ }
1654
+ default:
1655
+ return {
1656
+ jsonrpc: "2.0",
1657
+ id,
1658
+ error: { code: ERR_METHOD_NOT_FOUND, message: `Method not found: ${method}` }
1659
+ };
1660
+ }
1661
+ }
1662
+ /** Helper: store not configured error. */
1663
+ noStore(id) {
1664
+ return {
1665
+ jsonrpc: "2.0",
1666
+ id,
1667
+ error: { code: ERR_INTERNAL, message: "Daemon store not initialized" }
1668
+ };
1669
+ }
1670
+ /** Helper: missing parameter error. */
1671
+ missingParam(id, param) {
1672
+ return {
1673
+ jsonrpc: "2.0",
1674
+ id,
1675
+ error: { code: ERR_INVALID_PARAMS, message: `Missing required parameter: ${param}` }
1676
+ };
1677
+ }
1678
+ /** Helper: service not configured error. */
1679
+ noService(id, service) {
1680
+ return {
1681
+ jsonrpc: "2.0",
1682
+ id,
1683
+ error: { code: ERR_INTERNAL, message: `${service} service not initialized` }
1684
+ };
1685
+ }
1686
+ /**
1687
+ * Dispatch an HTTP request body (JSON-RPC) and call the reply callback.
1688
+ * Used by HttpGateway to proxy requests without going through a socket.
1689
+ */
1690
+ dispatchHttp(body, reply) {
1691
+ this.handleLine(body.trim(), reply);
1692
+ }
1693
+ /** Set the health check function (called by coordinator after construction). */
1694
+ setHealthCheck(fn) {
1695
+ this.healthCheckFn = fn;
1696
+ }
1697
+ /** Attach the spawner service (called by coordinator after construction). */
1698
+ setSpawner(spawner) {
1699
+ this.spawner = spawner;
1700
+ }
1701
+ /** Attach the inference service (called by coordinator after construction). */
1702
+ setInference(inference) {
1703
+ this.inference = inference;
1704
+ }
1705
+ /** Get the spawner service (used by HTTP gateway for SSE). */
1706
+ getSpawner() {
1707
+ return this.spawner;
1708
+ }
1709
+ start() {
1710
+ if (existsSync3(this.socketPath)) {
1711
+ unlinkSync(this.socketPath);
1712
+ }
1713
+ return new Promise((resolve, reject) => {
1714
+ this.server.listen(this.socketPath, () => resolve());
1715
+ this.server.once("error", reject);
1716
+ });
1717
+ }
1718
+ stop() {
1719
+ return new Promise((resolve) => this.server.close(() => resolve()));
1720
+ }
1721
+ };
1722
+
1723
+ // src/server/spawner-service.ts
1724
+ import { spawn as nodeSpawn } from "child_process";
1725
+ import { randomUUID } from "crypto";
1726
+ import { EventEmitter } from "events";
1727
+ var DEFAULT_CONFIG2 = {
1728
+ snapEndpoint: "http://localhost:9090",
1729
+ maxSessions: 8
1730
+ };
1731
+ var SpawnerService = class extends EventEmitter {
1732
+ sessions = /* @__PURE__ */ new Map();
1733
+ config;
1734
+ constructor(overrides) {
1735
+ super();
1736
+ this.config = { ...DEFAULT_CONFIG2, ...overrides };
1737
+ }
1738
+ /** Spawn a new agent process. Returns the session ID. */
1739
+ spawn(name, backend, model, prompt) {
1740
+ if (this.sessions.size >= this.config.maxSessions) {
1741
+ throw new Error(`Max sessions (${this.config.maxSessions}) reached`);
1742
+ }
1743
+ const sessionId = randomUUID();
1744
+ let child;
1745
+ switch (backend) {
1746
+ case "Snap": {
1747
+ const body = JSON.stringify({
1748
+ model,
1749
+ messages: [{ role: "user", content: prompt }],
1750
+ stream: false
1751
+ });
1752
+ child = nodeSpawn(
1753
+ "curl",
1754
+ [
1755
+ "-s",
1756
+ "-X",
1757
+ "POST",
1758
+ `${this.config.snapEndpoint}/v1/chat/completions`,
1759
+ "-H",
1760
+ "Content-Type: application/json",
1761
+ "-d",
1762
+ body
1763
+ ],
1764
+ { stdio: ["ignore", "pipe", "pipe"] }
1765
+ );
1766
+ break;
1767
+ }
1768
+ case "Ollama": {
1769
+ child = nodeSpawn("ollama", ["run", model, prompt], {
1770
+ stdio: ["ignore", "pipe", "pipe"]
1771
+ });
1772
+ break;
1773
+ }
1774
+ case "BitNet": {
1775
+ child = nodeSpawn("bitnet", ["run", "--model", model, "--prompt", prompt], {
1776
+ stdio: ["ignore", "pipe", "pipe"]
1777
+ });
1778
+ break;
1779
+ }
1780
+ }
1781
+ const proc = { name, model, backend, prompt, child, status: "running" };
1782
+ this.sessions.set(sessionId, proc);
1783
+ child.stdout?.on("data", (chunk) => {
1784
+ const lines = chunk.toString().split("\n");
1785
+ for (const line of lines) {
1786
+ if (line.length > 0) {
1787
+ this.emit("output", { sessionId, stream: "stdout", line });
1788
+ }
1789
+ }
1790
+ });
1791
+ child.stderr?.on("data", (chunk) => {
1792
+ const lines = chunk.toString().split("\n");
1793
+ for (const line of lines) {
1794
+ if (line.length > 0) {
1795
+ this.emit("output", { sessionId, stream: "stderr", line });
1796
+ }
1797
+ }
1798
+ });
1799
+ child.on("close", (code) => {
1800
+ proc.status = code === 0 ? "stopped" : "errored";
1801
+ this.emit("exit", { sessionId, code });
1802
+ });
1803
+ child.on("error", () => {
1804
+ proc.status = "errored";
1805
+ this.emit("exit", { sessionId, code: null });
1806
+ });
1807
+ return sessionId;
1808
+ }
1809
+ /** Stop a running agent by killing its process. */
1810
+ stop(sessionId) {
1811
+ const proc = this.sessions.get(sessionId);
1812
+ if (!proc) throw new Error(`No agent session: ${sessionId}`);
1813
+ if (proc.status !== "running") throw new Error(`Agent is not running (${proc.status})`);
1814
+ proc.child.kill("SIGTERM");
1815
+ proc.status = "stopped";
1816
+ }
1817
+ /** List all agent sessions. */
1818
+ list() {
1819
+ const result = [];
1820
+ for (const [id, proc] of this.sessions) {
1821
+ result.push({
1822
+ id,
1823
+ name: proc.name,
1824
+ model: proc.model,
1825
+ backend: proc.backend,
1826
+ prompt: proc.prompt,
1827
+ status: proc.status,
1828
+ pid: proc.child.pid ?? null
1829
+ });
1830
+ }
1831
+ return result;
1832
+ }
1833
+ /** Remove a stopped/errored session. */
1834
+ remove(sessionId) {
1835
+ const proc = this.sessions.get(sessionId);
1836
+ if (!proc) throw new Error(`No agent session: ${sessionId}`);
1837
+ if (proc.status === "running") throw new Error("Cannot remove a running agent \u2014 stop it first");
1838
+ this.sessions.delete(sessionId);
1839
+ }
1840
+ /** Kill all running agents (called on daemon shutdown). */
1841
+ stopAll() {
1842
+ for (const [, proc] of this.sessions) {
1843
+ if (proc.status === "running") {
1844
+ proc.child.kill("SIGTERM");
1845
+ proc.status = "stopped";
1846
+ }
1847
+ }
1848
+ }
1849
+ };
1850
+
1851
+ // src/coordinator.ts
1852
+ var HarnessCoordinator = class {
1853
+ constructor(options) {
1854
+ this.options = options;
1855
+ const workboardPath = join4(options.projectRoot, ".claude", "workboard.md");
1856
+ this.workboard = new WorkboardManager(workboardPath);
1857
+ }
1858
+ registry = new HarnessRegistry();
1859
+ rpcServer = null;
1860
+ httpGateway = null;
1861
+ store = null;
1862
+ spawner = null;
1863
+ inference = null;
1864
+ sessionId = null;
1865
+ workboard;
1866
+ async start() {
1867
+ await autoDetectHarnesses(this.registry);
1868
+ const type = detectSessionType();
1869
+ const state = this.workboard.read();
1870
+ const existingIds = state.agents.map((a) => a.id);
1871
+ this.sessionId = deriveSessionId(type, existingIds);
1872
+ const envLabels = {
1873
+ zed: "Zed/ACP",
1874
+ cursor: "Cursor",
1875
+ terminal: "WSL/bash"
1876
+ };
1877
+ this.workboard.registerAgent({
1878
+ id: this.sessionId,
1879
+ env: envLabels[type] ?? type,
1880
+ started: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`,
1881
+ task: this.options.task ?? "Harness coordination active",
1882
+ files: "",
1883
+ updated: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`
1884
+ });
1885
+ const dataDir = join4(process.env.HOME ?? "/tmp", ".local", "share", "revealui", "harness.db");
1886
+ mkdirSync2(dataDir, { recursive: true });
1887
+ this.store = new DaemonStore({ dataDir });
1888
+ await this.store.init();
1889
+ const socketPath = this.options.socketPath ?? join4(process.env.HOME ?? "/tmp", ".local", "share", "revealui", "harness.sock");
1890
+ this.rpcServer = new RpcServer(this.registry, socketPath, this.store);
1891
+ this.rpcServer.setHealthCheck(() => this.healthCheck());
1892
+ this.spawner = new SpawnerService();
1893
+ this.inference = new InferenceService();
1894
+ this.rpcServer.setSpawner(this.spawner);
1895
+ this.rpcServer.setInference(this.inference);
1896
+ await this.rpcServer.start();
1897
+ if (this.options.httpPort) {
1898
+ this.httpGateway = new HttpGateway({
1899
+ port: this.options.httpPort,
1900
+ host: this.options.httpHost ?? "0.0.0.0",
1901
+ staticDir: this.options.httpStaticDir,
1902
+ rpcDispatch: this.rpcServer,
1903
+ spawner: this.spawner
1904
+ });
1905
+ await this.httpGateway.start();
1906
+ }
1907
+ }
1908
+ async stop() {
1909
+ if (this.sessionId) {
1910
+ this.workboard.unregisterAgent(this.sessionId);
1911
+ this.workboard.addLogEntry(
1912
+ this.sessionId,
1913
+ `Session ended \u2014 ${this.options.task ?? "harness coordination"}`
1914
+ );
1915
+ }
1916
+ if (this.httpGateway) {
1917
+ await this.httpGateway.stop();
1918
+ this.httpGateway = null;
1919
+ }
1920
+ if (this.spawner) {
1921
+ await this.spawner.stopAll();
1922
+ this.spawner = null;
1923
+ }
1924
+ this.inference = null;
1925
+ if (this.rpcServer) {
1926
+ await this.rpcServer.stop();
1927
+ this.rpcServer = null;
1928
+ }
1929
+ if (this.store) {
1930
+ await this.store.close();
1931
+ this.store = null;
1932
+ }
1933
+ await this.registry.disposeAll();
1934
+ }
1935
+ /** The registry of detected harnesses. Available after start(). */
1936
+ getRegistry() {
1937
+ return this.registry;
1938
+ }
1939
+ /** The workboard manager. */
1940
+ getWorkboard() {
1941
+ return this.workboard;
1942
+ }
1943
+ /** The daemon persistent store (available after start()). */
1944
+ getStore() {
1945
+ return this.store;
1946
+ }
1947
+ /** Register a custom adapter (must be called before start()). */
1948
+ registerAdapter(adapter) {
1949
+ this.registry.register(adapter);
1950
+ }
1951
+ /** The HTTP gateway (available after start() if httpPort was set). */
1952
+ getHttpGateway() {
1953
+ return this.httpGateway;
1954
+ }
1955
+ /** Run a health check across all registered harnesses and the workboard. */
1956
+ async healthCheck() {
1957
+ const diagnostics = [];
1958
+ const StaleMs = 4 * 60 * 60 * 1e3;
1959
+ const allIds = this.registry.listAll();
1960
+ const registeredHarnesses = await Promise.all(
1961
+ allIds.map(async (harnessId) => {
1962
+ const adapter = this.registry.get(harnessId);
1963
+ let available = false;
1964
+ try {
1965
+ available = adapter ? await adapter.isAvailable() : false;
1966
+ } catch (err) {
1967
+ diagnostics.push(
1968
+ `${harnessId}: availability check failed \u2014 ${err instanceof Error ? err.message : String(err)}`
1969
+ );
1970
+ }
1971
+ if (!available) diagnostics.push(`${harnessId}: not available`);
1972
+ return { harnessId, available };
1973
+ })
1974
+ );
1975
+ let readable = false;
1976
+ let sessionCount = 0;
1977
+ const staleSessionIds = [];
1978
+ try {
1979
+ const state = this.workboard.read();
1980
+ readable = true;
1981
+ sessionCount = state.agents.length;
1982
+ const now = Date.now();
1983
+ for (const s of state.agents) {
1984
+ const ts = Date.parse(s.updated);
1985
+ if (!Number.isNaN(ts) && now - ts > StaleMs) {
1986
+ staleSessionIds.push(s.id);
1987
+ }
1988
+ }
1989
+ if (staleSessionIds.length > 0) {
1990
+ diagnostics.push(`Stale sessions: ${staleSessionIds.join(", ")}`);
1991
+ }
1992
+ } catch (err) {
1993
+ diagnostics.push(`Workboard unreadable: ${err instanceof Error ? err.message : String(err)}`);
1994
+ }
1995
+ const healthy = registeredHarnesses.some((h) => h.available) && readable && staleSessionIds.length === 0;
1996
+ return {
1997
+ healthy,
1998
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1999
+ registeredHarnesses,
2000
+ workboard: { readable, sessionCount, staleSessionIds },
2001
+ diagnostics
2002
+ };
2003
+ }
2004
+ };
2005
+
2006
+ // src/index.ts
2007
+ async function checkHarnessesLicense() {
2008
+ await initializeLicense();
2009
+ if (!isFeatureEnabled("ai")) {
2010
+ logger.warn(
2011
+ "[@revealui/harnesses] AI harness integration requires a Pro or Enterprise license. Visit https://revealui.com/pricing for details.",
2012
+ { feature: "ai" }
2013
+ );
2014
+ return false;
2015
+ }
2016
+ return true;
2017
+ }
2018
+
2019
+ export {
2020
+ ClaudeCodeAdapter,
2021
+ CursorAdapter,
2022
+ autoDetectHarnesses,
2023
+ HarnessRegistry,
2024
+ getLocalConfigPath,
2025
+ getRootConfigPath,
2026
+ getConfigurableHarnesses,
2027
+ syncConfig,
2028
+ diffConfig,
2029
+ syncAllConfigs,
2030
+ diffAllConfigs,
2031
+ validateConfigJson,
2032
+ findProcesses,
2033
+ findHarnessProcesses,
2034
+ findAllHarnessProcesses,
2035
+ findClaudeCodeSockets,
2036
+ RpcServer,
2037
+ HarnessCoordinator,
2038
+ checkHarnessesLicense
2039
+ };
2040
+ //# sourceMappingURL=chunk-6E2BKO6U.js.map