@revealui/harnesses 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1158 @@
1
+ import {
2
+ WorkboardManager,
3
+ deriveSessionId,
4
+ detectSessionType
5
+ } from "./chunk-FJGN6DTH.js";
6
+ import {
7
+ __require
8
+ } from "./chunk-DGUM43GV.js";
9
+
10
+ // src/index.ts
11
+ import { isFeatureEnabled } from "@revealui/core/features";
12
+ import { initializeLicense } from "@revealui/core/license";
13
+ import { logger } from "@revealui/core/observability/logger";
14
+
15
+ // src/adapters/claude-code-adapter.ts
16
+ import { execFile } from "child_process";
17
+ import { promisify } from "util";
18
+ var execFileAsync = promisify(execFile);
19
+ var ClaudeCodeAdapter = class {
20
+ id = "claude-code";
21
+ name = "Claude Code";
22
+ eventHandlers = /* @__PURE__ */ new Set();
23
+ workboardPath;
24
+ constructor(workboardPath) {
25
+ this.workboardPath = workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH;
26
+ }
27
+ getCapabilities() {
28
+ return {
29
+ generateCode: false,
30
+ // interactive only — no headless CLI interface
31
+ analyzeCode: false,
32
+ // interactive only — no headless CLI interface
33
+ applyEdit: false,
34
+ // interactive only — edits are applied inside Claude Code sessions
35
+ applyConfig: false,
36
+ // config managed interactively via ~/.claude/settings.json
37
+ readWorkboard: this.workboardPath !== void 0,
38
+ writeWorkboard: this.workboardPath !== void 0
39
+ };
40
+ }
41
+ async getInfo() {
42
+ let version;
43
+ try {
44
+ const { stdout } = await execFileAsync("claude", ["--version"], {
45
+ timeout: 5e3
46
+ });
47
+ version = stdout.trim().split("\n")[0];
48
+ } catch {
49
+ }
50
+ return { id: this.id, name: this.name, version, capabilities: this.getCapabilities() };
51
+ }
52
+ async isAvailable() {
53
+ try {
54
+ await execFileAsync("claude", ["--version"], { timeout: 3e3 });
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+ notifyRegistered() {
61
+ this.emit({ type: "harness-connected", harnessId: this.id });
62
+ }
63
+ notifyUnregistering() {
64
+ this.emit({ type: "harness-disconnected", harnessId: this.id });
65
+ }
66
+ async execute(command) {
67
+ try {
68
+ return await this.executeInner(command);
69
+ } catch (err) {
70
+ const message = err instanceof Error ? err.message : String(err);
71
+ this.emit({ type: "error", harnessId: this.id, message });
72
+ return { success: false, command: command.type, message };
73
+ }
74
+ }
75
+ async executeInner(command) {
76
+ switch (command.type) {
77
+ case "get-status": {
78
+ const available = await this.isAvailable();
79
+ return { success: true, command: command.type, data: { available } };
80
+ }
81
+ case "get-running-instances": {
82
+ return { success: true, command: command.type, data: [] };
83
+ }
84
+ case "generate-code":
85
+ case "analyze-code": {
86
+ return {
87
+ success: false,
88
+ command: command.type,
89
+ message: `${command.type} is not supported \u2014 Claude Code operates interactively`
90
+ };
91
+ }
92
+ case "apply-config": {
93
+ return {
94
+ success: false,
95
+ command: command.type,
96
+ message: "Config is managed interactively via ~/.claude/settings.json"
97
+ };
98
+ }
99
+ case "read-workboard": {
100
+ if (!this.workboardPath) {
101
+ return {
102
+ success: false,
103
+ command: command.type,
104
+ message: "REVEALUI_WORKBOARD_PATH is not set"
105
+ };
106
+ }
107
+ const manager = new WorkboardManager(this.workboardPath);
108
+ const state = await manager.readAsync();
109
+ return { success: true, command: command.type, data: state };
110
+ }
111
+ case "update-workboard": {
112
+ if (!this.workboardPath) {
113
+ return {
114
+ success: false,
115
+ command: command.type,
116
+ message: "REVEALUI_WORKBOARD_PATH is not set"
117
+ };
118
+ }
119
+ const manager = new WorkboardManager(this.workboardPath);
120
+ manager.updateSession(command.sessionId, {
121
+ ...command.task !== void 0 && { task: command.task },
122
+ ...command.files !== void 0 && { files: command.files.join(", ") },
123
+ updated: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`
124
+ });
125
+ return { success: true, command: command.type };
126
+ }
127
+ default: {
128
+ return {
129
+ success: false,
130
+ command: command.type,
131
+ message: `Command not supported by ${this.name}`
132
+ };
133
+ }
134
+ }
135
+ }
136
+ onEvent(handler) {
137
+ this.eventHandlers.add(handler);
138
+ return () => this.eventHandlers.delete(handler);
139
+ }
140
+ async dispose() {
141
+ this.eventHandlers.clear();
142
+ }
143
+ emit(event) {
144
+ for (const handler of this.eventHandlers) {
145
+ try {
146
+ handler(event);
147
+ } catch {
148
+ }
149
+ }
150
+ }
151
+ };
152
+
153
+ // src/adapters/cursor-adapter.ts
154
+ import { execFile as execFile2 } from "child_process";
155
+ import { promisify as promisify2 } from "util";
156
+ var execFileAsync2 = promisify2(execFile2);
157
+ var CursorAdapter = class {
158
+ id = "cursor";
159
+ name = "Cursor";
160
+ eventHandlers = /* @__PURE__ */ new Set();
161
+ workboardPath;
162
+ constructor(workboardPath) {
163
+ this.workboardPath = workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH;
164
+ }
165
+ getCapabilities() {
166
+ return {
167
+ generateCode: false,
168
+ analyzeCode: false,
169
+ applyEdit: false,
170
+ applyConfig: false,
171
+ readWorkboard: this.workboardPath !== void 0,
172
+ writeWorkboard: this.workboardPath !== void 0
173
+ };
174
+ }
175
+ async getInfo() {
176
+ let version;
177
+ try {
178
+ const { stdout } = await execFileAsync2("cursor", ["--version"], {
179
+ timeout: 5e3
180
+ });
181
+ version = stdout.trim().split("\n")[0];
182
+ } catch {
183
+ }
184
+ return { id: this.id, name: this.name, version, capabilities: this.getCapabilities() };
185
+ }
186
+ async isAvailable() {
187
+ try {
188
+ await execFileAsync2("cursor", ["--version"], { timeout: 3e3 });
189
+ return true;
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+ notifyRegistered() {
195
+ this.emit({ type: "harness-connected", harnessId: this.id });
196
+ }
197
+ notifyUnregistering() {
198
+ this.emit({ type: "harness-disconnected", harnessId: this.id });
199
+ }
200
+ async execute(command) {
201
+ try {
202
+ return await this.executeInner(command);
203
+ } catch (err) {
204
+ const message = err instanceof Error ? err.message : String(err);
205
+ this.emit({ type: "error", harnessId: this.id, message });
206
+ return { success: false, command: command.type, message };
207
+ }
208
+ }
209
+ async executeInner(command) {
210
+ switch (command.type) {
211
+ case "get-status": {
212
+ const available = await this.isAvailable();
213
+ return { success: true, command: command.type, data: { available } };
214
+ }
215
+ case "read-workboard": {
216
+ if (!this.workboardPath) {
217
+ return {
218
+ success: false,
219
+ command: command.type,
220
+ message: "REVEALUI_WORKBOARD_PATH is not set"
221
+ };
222
+ }
223
+ const manager = new WorkboardManager(this.workboardPath);
224
+ const state = await manager.readAsync();
225
+ return { success: true, command: command.type, data: state };
226
+ }
227
+ case "update-workboard": {
228
+ if (!this.workboardPath) {
229
+ return {
230
+ success: false,
231
+ command: command.type,
232
+ message: "REVEALUI_WORKBOARD_PATH is not set"
233
+ };
234
+ }
235
+ const manager = new WorkboardManager(this.workboardPath);
236
+ manager.updateSession(command.sessionId, {
237
+ ...command.task !== void 0 && { task: command.task },
238
+ ...command.files !== void 0 && { files: command.files.join(", ") },
239
+ updated: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`
240
+ });
241
+ return { success: true, command: command.type };
242
+ }
243
+ default: {
244
+ return {
245
+ success: false,
246
+ command: command.type,
247
+ message: `Command not supported by ${this.name}`
248
+ };
249
+ }
250
+ }
251
+ }
252
+ onEvent(handler) {
253
+ this.eventHandlers.add(handler);
254
+ return () => this.eventHandlers.delete(handler);
255
+ }
256
+ async dispose() {
257
+ this.eventHandlers.clear();
258
+ }
259
+ emit(event) {
260
+ for (const handler of this.eventHandlers) {
261
+ try {
262
+ handler(event);
263
+ } catch {
264
+ }
265
+ }
266
+ }
267
+ };
268
+
269
+ // src/config/config-sync.ts
270
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
271
+ import { dirname } from "path";
272
+
273
+ // src/config/harness-config-paths.ts
274
+ import { homedir } from "os";
275
+ import { join } from "path";
276
+ var HOME = homedir();
277
+ var REVEALUI_ROOT = process.env.REVEALUI_ROOT ?? join(HOME, ".revealui");
278
+ var LOCAL_CONFIG_PATHS = {
279
+ "claude-code": join(HOME, ".claude", "settings.json"),
280
+ cursor: join(HOME, ".cursor", "settings.json"),
281
+ copilot: join(HOME, ".config", "github-copilot", "hosts.json")
282
+ };
283
+ var ROOT_CONFIG_FILES = {
284
+ "claude-code": "settings.json",
285
+ cursor: "settings.json",
286
+ copilot: "hosts.json"
287
+ };
288
+ function getLocalConfigPath(harnessId) {
289
+ return LOCAL_CONFIG_PATHS[harnessId];
290
+ }
291
+ function getRootConfigPath(harnessId, root = REVEALUI_ROOT) {
292
+ const file = ROOT_CONFIG_FILES[harnessId];
293
+ if (!file) return void 0;
294
+ return join(root, "harness-configs", harnessId, file);
295
+ }
296
+ function getConfigurableHarnesses() {
297
+ return Object.keys(LOCAL_CONFIG_PATHS);
298
+ }
299
+
300
+ // src/config/config-sync.ts
301
+ function syncConfig(harnessId, direction, root) {
302
+ const localPath = getLocalConfigPath(harnessId);
303
+ const rootPath = getRootConfigPath(harnessId, root);
304
+ if (!(localPath && rootPath)) {
305
+ return {
306
+ success: false,
307
+ harnessId,
308
+ direction,
309
+ message: `No config path known for harness: ${harnessId}`
310
+ };
311
+ }
312
+ try {
313
+ if (direction === "pull") {
314
+ if (!existsSync(rootPath)) {
315
+ return {
316
+ success: false,
317
+ harnessId,
318
+ direction,
319
+ message: `Root config not found: ${rootPath}`
320
+ };
321
+ }
322
+ mkdirSync(dirname(localPath), { recursive: true });
323
+ backupIfExists(localPath);
324
+ copyFileSync(rootPath, localPath);
325
+ return { success: true, harnessId, direction, message: `Pulled ${rootPath} \u2192 ${localPath}` };
326
+ } else {
327
+ if (!existsSync(localPath)) {
328
+ return {
329
+ success: false,
330
+ harnessId,
331
+ direction,
332
+ message: `Local config not found: ${localPath}`
333
+ };
334
+ }
335
+ mkdirSync(dirname(rootPath), { recursive: true });
336
+ backupIfExists(rootPath);
337
+ copyFileSync(localPath, rootPath);
338
+ return { success: true, harnessId, direction, message: `Pushed ${localPath} \u2192 ${rootPath}` };
339
+ }
340
+ } catch (err) {
341
+ return {
342
+ success: false,
343
+ harnessId,
344
+ direction,
345
+ message: err instanceof Error ? err.message : String(err)
346
+ };
347
+ }
348
+ }
349
+ function diffConfig(harnessId, root) {
350
+ const localPath = getLocalConfigPath(harnessId);
351
+ const rootPath = getRootConfigPath(harnessId, root);
352
+ const localExists = !!localPath && existsSync(localPath);
353
+ const ssdExists = !!rootPath && existsSync(rootPath);
354
+ if (!(localExists && ssdExists)) {
355
+ return { harnessId, localExists, ssdExists, identical: false };
356
+ }
357
+ try {
358
+ const localContent = readFileSync(localPath, "utf8");
359
+ const ssdContent = readFileSync(rootPath, "utf8");
360
+ return { harnessId, localExists, ssdExists, identical: localContent === ssdContent };
361
+ } catch {
362
+ return { harnessId, localExists, ssdExists, identical: false };
363
+ }
364
+ }
365
+ function syncAllConfigs(direction, root) {
366
+ return getConfigurableHarnesses().map((id) => syncConfig(id, direction, root));
367
+ }
368
+ function diffAllConfigs(root) {
369
+ return getConfigurableHarnesses().map((id) => diffConfig(id, root));
370
+ }
371
+ function validateConfigJson(harnessId) {
372
+ const localPath = getLocalConfigPath(harnessId);
373
+ if (!localPath) return `No config path known for harness: ${harnessId}`;
374
+ if (!existsSync(localPath)) return `Config file not found: ${localPath}`;
375
+ try {
376
+ const content = readFileSync(localPath, "utf8");
377
+ JSON.parse(content);
378
+ return null;
379
+ } catch (err) {
380
+ return err instanceof Error ? err.message : String(err);
381
+ }
382
+ }
383
+ function backupIfExists(filePath) {
384
+ try {
385
+ if (existsSync(filePath)) {
386
+ copyFileSync(filePath, `${filePath}.bak`);
387
+ }
388
+ } catch {
389
+ }
390
+ }
391
+
392
+ // src/coordinator.ts
393
+ import { join as join3 } from "path";
394
+
395
+ // src/adapters/copilot-adapter.ts
396
+ var CopilotAdapter = class {
397
+ id = "copilot";
398
+ name = "GitHub Copilot";
399
+ eventHandlers = /* @__PURE__ */ new Set();
400
+ getCapabilities() {
401
+ return {
402
+ generateCode: false,
403
+ // stub — no CLI available
404
+ analyzeCode: false,
405
+ applyEdit: false,
406
+ applyConfig: false,
407
+ readWorkboard: false,
408
+ writeWorkboard: false
409
+ };
410
+ }
411
+ async getInfo() {
412
+ return { id: this.id, name: this.name, capabilities: this.getCapabilities() };
413
+ }
414
+ async isAvailable() {
415
+ return false;
416
+ }
417
+ notifyRegistered() {
418
+ this.emit({ type: "harness-connected", harnessId: this.id });
419
+ }
420
+ notifyUnregistering() {
421
+ this.emit({ type: "harness-disconnected", harnessId: this.id });
422
+ }
423
+ async execute(command) {
424
+ return {
425
+ success: false,
426
+ command: command.type,
427
+ message: "Copilot adapter is a stub \u2014 no standalone CLI available"
428
+ };
429
+ }
430
+ onEvent(handler) {
431
+ this.eventHandlers.add(handler);
432
+ return () => this.eventHandlers.delete(handler);
433
+ }
434
+ async dispose() {
435
+ this.eventHandlers.clear();
436
+ }
437
+ emit(event) {
438
+ for (const handler of this.eventHandlers) {
439
+ try {
440
+ handler(event);
441
+ } catch {
442
+ }
443
+ }
444
+ }
445
+ };
446
+
447
+ // src/adapters/revealui-agent-adapter.ts
448
+ var DEFAULT_CONFIG = {
449
+ projectRoot: process.cwd(),
450
+ maxIterations: 10,
451
+ timeoutMs: 12e4
452
+ };
453
+ var RevealUIAgentAdapter = class {
454
+ id = "revealui-agent";
455
+ name = "RevealUI Agent";
456
+ config;
457
+ eventHandlers = /* @__PURE__ */ new Set();
458
+ constructor(config) {
459
+ this.config = { ...DEFAULT_CONFIG, ...config };
460
+ }
461
+ getCapabilities() {
462
+ return {
463
+ generateCode: true,
464
+ analyzeCode: true,
465
+ applyEdit: true,
466
+ applyConfig: false,
467
+ readWorkboard: (this.config.workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH) !== void 0,
468
+ writeWorkboard: (this.config.workboardPath ?? process.env.REVEALUI_WORKBOARD_PATH) !== void 0
469
+ };
470
+ }
471
+ async getInfo() {
472
+ return {
473
+ id: this.id,
474
+ name: this.name,
475
+ version: "0.1.0",
476
+ capabilities: this.getCapabilities()
477
+ };
478
+ }
479
+ /**
480
+ * The RevealUI agent is available if at least one LLM provider is reachable.
481
+ * Checks in order: BitNet (localhost), Ollama (localhost), Groq (API key).
482
+ */
483
+ async isAvailable() {
484
+ if (process.env.BITNET_BASE_URL) {
485
+ try {
486
+ const url = `${process.env.BITNET_BASE_URL}/v1/models`;
487
+ const res = await fetch(url, { signal: AbortSignal.timeout(2e3) });
488
+ if (res.ok) return true;
489
+ } catch {
490
+ }
491
+ }
492
+ const ollamaUrl = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
493
+ try {
494
+ const res = await fetch(`${ollamaUrl}/api/tags`, {
495
+ signal: AbortSignal.timeout(2e3)
496
+ });
497
+ if (res.ok) return true;
498
+ } catch {
499
+ }
500
+ if (process.env.GROQ_API_KEY) return true;
501
+ return false;
502
+ }
503
+ notifyRegistered() {
504
+ this.emit({ type: "harness-connected", harnessId: this.id });
505
+ }
506
+ notifyUnregistering() {
507
+ this.emit({ type: "harness-disconnected", harnessId: this.id });
508
+ }
509
+ async execute(command) {
510
+ try {
511
+ return await this.executeInner(command);
512
+ } catch (err) {
513
+ const message = err instanceof Error ? err.message : String(err);
514
+ this.emit({ type: "error", harnessId: this.id, message });
515
+ return { success: false, command: command.type, message };
516
+ }
517
+ }
518
+ async executeInner(command) {
519
+ switch (command.type) {
520
+ case "get-status": {
521
+ const available = await this.isAvailable();
522
+ return {
523
+ success: true,
524
+ command: command.type,
525
+ data: {
526
+ available,
527
+ provider: this.config.provider ?? "auto",
528
+ model: this.config.model ?? "auto",
529
+ projectRoot: this.config.projectRoot
530
+ }
531
+ };
532
+ }
533
+ case "headless-prompt": {
534
+ return this.runHeadlessPrompt(command.prompt, command.maxTurns, command.timeoutMs);
535
+ }
536
+ case "generate-code": {
537
+ return this.runHeadlessPrompt(
538
+ `Generate code: ${command.prompt}${command.language ? ` (language: ${command.language})` : ""}${command.context ? `
539
+
540
+ Context:
541
+ ${command.context}` : ""}`
542
+ );
543
+ }
544
+ case "analyze-code": {
545
+ const question = command.question ?? "Analyze this file and explain what it does.";
546
+ return this.runHeadlessPrompt(
547
+ `Read the file at ${command.filePath} and answer: ${question}`
548
+ );
549
+ }
550
+ case "apply-edit": {
551
+ return this.runHeadlessPrompt(
552
+ `Apply the following diff to ${command.filePath}:
553
+
554
+ ${command.diff}`
555
+ );
556
+ }
557
+ case "apply-config":
558
+ case "sync-config":
559
+ case "diff-config": {
560
+ return {
561
+ success: false,
562
+ command: command.type,
563
+ message: "Config sync is not applicable \u2014 RevealUI agent uses the content layer directly"
564
+ };
565
+ }
566
+ case "read-workboard":
567
+ case "update-workboard": {
568
+ return {
569
+ success: false,
570
+ command: command.type,
571
+ message: "Workboard support not yet wired \u2014 use WorkboardManager directly"
572
+ };
573
+ }
574
+ default: {
575
+ return {
576
+ success: false,
577
+ command: command.type,
578
+ message: `Command not supported by ${this.name}`
579
+ };
580
+ }
581
+ }
582
+ }
583
+ /**
584
+ * Run a headless prompt through the coding agent.
585
+ * Lazy-imports @revealui/ai to avoid hard dependency at module load time.
586
+ * Types are inferred from the dynamic imports — no compile-time @revealui/ai dependency.
587
+ */
588
+ async runHeadlessPrompt(prompt, maxTurns, timeoutMs) {
589
+ const aiRuntimePath = "@revealui/ai/orchestration/streaming-runtime";
590
+ const aiClientPath = "@revealui/ai/llm/client";
591
+ const aiToolsPath = "@revealui/ai/tools/coding";
592
+ let runtimeMod;
593
+ let clientMod;
594
+ let toolsMod;
595
+ try {
596
+ [runtimeMod, clientMod, toolsMod] = await Promise.all([
597
+ import(aiRuntimePath),
598
+ import(aiClientPath),
599
+ import(aiToolsPath)
600
+ ]);
601
+ } catch {
602
+ return {
603
+ success: false,
604
+ command: "headless-prompt",
605
+ message: "@revealui/ai is not installed. Install it to use the RevealUI agent: npm install @revealui/ai"
606
+ };
607
+ }
608
+ const StreamingAgentRuntime = runtimeMod.StreamingAgentRuntime;
609
+ const createCodingTools = toolsMod.createCodingTools;
610
+ const projectRoot = this.config.projectRoot ?? process.cwd();
611
+ const tools = createCodingTools({ projectRoot });
612
+ let llmClient;
613
+ if (this.config.provider) {
614
+ const LLMClient = clientMod.LLMClient;
615
+ llmClient = new LLMClient({
616
+ provider: this.config.provider,
617
+ model: this.config.model,
618
+ baseURL: process.env.BITNET_BASE_URL ?? process.env.OLLAMA_BASE_URL,
619
+ apiKey: process.env.GROQ_API_KEY ?? "not-needed"
620
+ });
621
+ } else {
622
+ const createLLMClientFromEnv = clientMod.createLLMClientFromEnv;
623
+ llmClient = createLLMClientFromEnv();
624
+ }
625
+ const agent = {
626
+ id: "revealui-coding-agent",
627
+ name: "RevealUI Coding Agent",
628
+ instructions: this.buildInstructions(),
629
+ tools,
630
+ config: {},
631
+ getContext: () => ({ projectRoot, workingDirectory: projectRoot })
632
+ };
633
+ const task = {
634
+ id: `task-${Date.now()}`,
635
+ type: "headless-prompt",
636
+ description: prompt
637
+ };
638
+ const runtime = new StreamingAgentRuntime({
639
+ maxIterations: maxTurns ?? this.config.maxIterations ?? DEFAULT_CONFIG.maxIterations,
640
+ timeout: timeoutMs ?? this.config.timeoutMs ?? DEFAULT_CONFIG.timeoutMs
641
+ });
642
+ const taskId = task.id;
643
+ this.emit({ type: "generation-started", taskId });
644
+ const outputParts = [];
645
+ try {
646
+ for await (const chunk of runtime.streamTask(agent, task, llmClient)) {
647
+ switch (chunk.type) {
648
+ case "text":
649
+ if (chunk.content) outputParts.push(chunk.content);
650
+ break;
651
+ case "tool_call_result":
652
+ if (chunk.toolResult?.content) {
653
+ outputParts.push(`[tool: ${chunk.toolCall?.name}] ${chunk.toolResult.content}`);
654
+ }
655
+ break;
656
+ case "error":
657
+ if (chunk.error) outputParts.push(`[error] ${chunk.error}`);
658
+ break;
659
+ case "done":
660
+ break;
661
+ }
662
+ }
663
+ } finally {
664
+ await runtime.cleanup();
665
+ }
666
+ const output = outputParts.join("\n");
667
+ this.emit({ type: "generation-completed", taskId, output });
668
+ return {
669
+ success: true,
670
+ command: "headless-prompt",
671
+ message: output,
672
+ data: { taskId, output }
673
+ };
674
+ }
675
+ /**
676
+ * Build system instructions from the content layer.
677
+ * Loads rules from .claude/rules/ as a baseline (they are the canonical content).
678
+ */
679
+ buildInstructions() {
680
+ const lines = [
681
+ "You are the RevealUI coding agent. You help with software development tasks in this project.",
682
+ "Use the available tools to read, write, edit, search, and execute commands.",
683
+ "Always read files before modifying them. Prefer surgical edits over full rewrites.",
684
+ "Follow project conventions discovered via the project_context tool.",
685
+ ""
686
+ ];
687
+ try {
688
+ const { readdirSync, readFileSync: readFileSync2 } = __require("fs");
689
+ const { join: join4 } = __require("path");
690
+ const projectRoot = this.config.projectRoot ?? process.cwd();
691
+ const rulesDir = join4(projectRoot, ".claude", "rules");
692
+ const ruleFiles = readdirSync(rulesDir);
693
+ for (const file of ruleFiles) {
694
+ if (file.endsWith(".md")) {
695
+ const content = readFileSync2(join4(rulesDir, file), "utf8");
696
+ lines.push(`## ${file.replace(".md", "")}`, content, "");
697
+ }
698
+ }
699
+ } catch {
700
+ }
701
+ return lines.join("\n");
702
+ }
703
+ onEvent(handler) {
704
+ this.eventHandlers.add(handler);
705
+ return () => this.eventHandlers.delete(handler);
706
+ }
707
+ async dispose() {
708
+ this.eventHandlers.clear();
709
+ }
710
+ emit(event) {
711
+ for (const handler of this.eventHandlers) {
712
+ try {
713
+ handler(event);
714
+ } catch {
715
+ }
716
+ }
717
+ }
718
+ };
719
+
720
+ // src/detection/auto-detector.ts
721
+ async function autoDetectHarnesses(registry) {
722
+ const candidates = [
723
+ new RevealUIAgentAdapter(),
724
+ new ClaudeCodeAdapter(),
725
+ new CursorAdapter(),
726
+ new CopilotAdapter()
727
+ ];
728
+ const registered = [];
729
+ await Promise.all(
730
+ candidates.map(async (adapter) => {
731
+ try {
732
+ if (await adapter.isAvailable()) {
733
+ registry.register(adapter);
734
+ registered.push(adapter.id);
735
+ } else {
736
+ await adapter.dispose();
737
+ }
738
+ } catch {
739
+ await adapter.dispose();
740
+ }
741
+ })
742
+ );
743
+ return registered;
744
+ }
745
+
746
+ // src/registry/harness-registry.ts
747
+ var HarnessRegistry = class {
748
+ adapters = /* @__PURE__ */ new Map();
749
+ /** Register an adapter. Throws if an adapter with the same id already exists. */
750
+ register(adapter) {
751
+ if (this.adapters.has(adapter.id)) {
752
+ throw new Error(`Harness adapter already registered: ${adapter.id}`);
753
+ }
754
+ this.adapters.set(adapter.id, adapter);
755
+ adapter.notifyRegistered?.();
756
+ }
757
+ /** Unregister an adapter, disposing it in the process. */
758
+ async unregister(id) {
759
+ const adapter = this.adapters.get(id);
760
+ if (adapter) {
761
+ adapter.notifyUnregistering?.();
762
+ await adapter.dispose();
763
+ this.adapters.delete(id);
764
+ }
765
+ }
766
+ /** Retrieve an adapter by id. */
767
+ get(id) {
768
+ return this.adapters.get(id);
769
+ }
770
+ /** List all registered adapter ids. */
771
+ listAll() {
772
+ return Array.from(this.adapters.keys());
773
+ }
774
+ /** List ids of adapters that report isAvailable() === true. */
775
+ async listAvailable() {
776
+ const results = await Promise.all(
777
+ Array.from(this.adapters.entries()).map(async ([id, adapter]) => ({
778
+ id,
779
+ available: await adapter.isAvailable()
780
+ }))
781
+ );
782
+ return results.filter((r) => r.available).map((r) => r.id);
783
+ }
784
+ /** Dispose all adapters and clear the registry. */
785
+ async disposeAll() {
786
+ await Promise.all(Array.from(this.adapters.values()).map((a) => a.dispose()));
787
+ this.adapters.clear();
788
+ }
789
+ };
790
+
791
+ // src/server/rpc-server.ts
792
+ import { createServer } from "net";
793
+
794
+ // src/detection/process-detector.ts
795
+ import { execFile as execFile3 } from "child_process";
796
+ import { readdir } from "fs/promises";
797
+ import { join as join2 } from "path";
798
+ import { promisify as promisify3 } from "util";
799
+ var execFileAsync3 = promisify3(execFile3);
800
+ async function findProcesses(pattern) {
801
+ try {
802
+ const { stdout } = await execFileAsync3("pgrep", ["-a", pattern], { timeout: 3e3 });
803
+ return stdout.trim().split("\n").filter(Boolean).map((line) => {
804
+ const spaceIdx = line.indexOf(" ");
805
+ const pid = parseInt(line.slice(0, spaceIdx), 10);
806
+ const command = line.slice(spaceIdx + 1);
807
+ return { pid, command };
808
+ });
809
+ } catch {
810
+ return [];
811
+ }
812
+ }
813
+ var HARNESS_PROCESS_PATTERNS = {
814
+ "claude-code": ["claude"],
815
+ cursor: ["cursor", "Cursor"],
816
+ copilot: ["copilot"]
817
+ };
818
+ async function findHarnessProcesses(harnessId) {
819
+ const patterns = HARNESS_PROCESS_PATTERNS[harnessId];
820
+ if (!patterns) return [];
821
+ const results = await Promise.all(patterns.map((p) => findProcesses(p)));
822
+ return results.flat().map((p) => ({ ...p, harnessId }));
823
+ }
824
+ async function findAllHarnessProcesses() {
825
+ const results = await Promise.all(
826
+ Object.keys(HARNESS_PROCESS_PATTERNS).map((id) => findHarnessProcesses(id))
827
+ );
828
+ return results.flat();
829
+ }
830
+ async function findClaudeCodeSockets() {
831
+ const dirs = [
832
+ "/tmp",
833
+ process.env.XDG_RUNTIME_DIR,
834
+ join2(process.env.HOME ?? "/tmp", ".claude")
835
+ ].filter(Boolean);
836
+ const sockets = [];
837
+ for (const dir of dirs) {
838
+ try {
839
+ const entries = await readdir(dir);
840
+ for (const entry of entries) {
841
+ if (/^claude.*\.sock$/.test(entry)) {
842
+ sockets.push(join2(dir, entry));
843
+ }
844
+ }
845
+ } catch {
846
+ }
847
+ }
848
+ return sockets;
849
+ }
850
+
851
+ // src/server/rpc-server.ts
852
+ var ERR_PARSE = -32700;
853
+ var ERR_INVALID_PARAMS = -32602;
854
+ var ERR_METHOD_NOT_FOUND = -32601;
855
+ var ERR_INTERNAL = -32603;
856
+ var RpcServer = class {
857
+ constructor(registry, socketPath) {
858
+ this.registry = registry;
859
+ this.socketPath = socketPath;
860
+ this.server.on("connection", (socket) => {
861
+ let buffer = "";
862
+ socket.on("data", (chunk) => {
863
+ buffer += chunk.toString();
864
+ const lines = buffer.split("\n");
865
+ buffer = lines.pop() ?? "";
866
+ for (const line of lines) {
867
+ this.handleLine(line.trim(), (response) => {
868
+ socket.write(`${JSON.stringify(response)}
869
+ `);
870
+ });
871
+ }
872
+ });
873
+ });
874
+ }
875
+ server = createServer();
876
+ healthCheckFn = null;
877
+ handleLine(line, reply) {
878
+ let req;
879
+ try {
880
+ req = JSON.parse(line);
881
+ } catch {
882
+ reply({ jsonrpc: "2.0", id: null, error: { code: ERR_PARSE, message: "Parse error" } });
883
+ return;
884
+ }
885
+ this.dispatch(req).then(reply).catch((err) => {
886
+ reply({
887
+ jsonrpc: "2.0",
888
+ id: req.id,
889
+ error: { code: ERR_INTERNAL, message: err instanceof Error ? err.message : String(err) }
890
+ });
891
+ });
892
+ }
893
+ async dispatch(req) {
894
+ const { id, method, params } = req;
895
+ const p = params ?? {};
896
+ switch (method) {
897
+ case "harness.list": {
898
+ const ids = await this.registry.listAvailable();
899
+ const infos = await Promise.all(ids.map((id2) => this.registry.get(id2)?.getInfo()));
900
+ return { jsonrpc: "2.0", id, result: infos };
901
+ }
902
+ case "harness.info": {
903
+ const harnessId = p.harnessId;
904
+ if (!harnessId) {
905
+ return {
906
+ jsonrpc: "2.0",
907
+ id,
908
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId required" }
909
+ };
910
+ }
911
+ const adapter = this.registry.get(harnessId);
912
+ if (!adapter) {
913
+ return {
914
+ jsonrpc: "2.0",
915
+ id,
916
+ error: { code: ERR_INVALID_PARAMS, message: `Harness not found: ${harnessId}` }
917
+ };
918
+ }
919
+ return { jsonrpc: "2.0", id, result: await adapter.getInfo() };
920
+ }
921
+ case "harness.execute": {
922
+ const harnessId = p.harnessId;
923
+ const command = p.command;
924
+ if (!(harnessId && command)) {
925
+ return {
926
+ jsonrpc: "2.0",
927
+ id,
928
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId and command required" }
929
+ };
930
+ }
931
+ const adapter = this.registry.get(harnessId);
932
+ if (!adapter) {
933
+ return {
934
+ jsonrpc: "2.0",
935
+ id,
936
+ error: { code: ERR_INVALID_PARAMS, message: `Harness not found: ${harnessId}` }
937
+ };
938
+ }
939
+ const result = await adapter.execute(command);
940
+ return { jsonrpc: "2.0", id, result };
941
+ }
942
+ case "harness.syncConfig": {
943
+ const harnessId = p.harnessId;
944
+ const direction = p.direction;
945
+ if (!(harnessId && direction)) {
946
+ return {
947
+ jsonrpc: "2.0",
948
+ id,
949
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId and direction required" }
950
+ };
951
+ }
952
+ return { jsonrpc: "2.0", id, result: syncConfig(harnessId, direction) };
953
+ }
954
+ case "harness.diffConfig": {
955
+ const harnessId = p.harnessId;
956
+ if (!harnessId) {
957
+ return {
958
+ jsonrpc: "2.0",
959
+ id,
960
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId required" }
961
+ };
962
+ }
963
+ return { jsonrpc: "2.0", id, result: diffConfig(harnessId) };
964
+ }
965
+ case "harness.listRunning": {
966
+ const harnessId = p.harnessId;
967
+ if (!harnessId) {
968
+ return {
969
+ jsonrpc: "2.0",
970
+ id,
971
+ error: { code: ERR_INVALID_PARAMS, message: "harnessId required" }
972
+ };
973
+ }
974
+ const processes = await findHarnessProcesses(harnessId);
975
+ return { jsonrpc: "2.0", id, result: processes };
976
+ }
977
+ case "harness.health": {
978
+ if (!this.healthCheckFn) {
979
+ return {
980
+ jsonrpc: "2.0",
981
+ id,
982
+ error: { code: ERR_INTERNAL, message: "Health check not configured" }
983
+ };
984
+ }
985
+ const health = await this.healthCheckFn();
986
+ return { jsonrpc: "2.0", id, result: health };
987
+ }
988
+ default:
989
+ return {
990
+ jsonrpc: "2.0",
991
+ id,
992
+ error: { code: ERR_METHOD_NOT_FOUND, message: `Method not found: ${method}` }
993
+ };
994
+ }
995
+ }
996
+ /** Set the health check function (called by coordinator after construction). */
997
+ setHealthCheck(fn) {
998
+ this.healthCheckFn = fn;
999
+ }
1000
+ start() {
1001
+ return new Promise((resolve, reject) => {
1002
+ this.server.listen(this.socketPath, () => resolve());
1003
+ this.server.once("error", reject);
1004
+ });
1005
+ }
1006
+ stop() {
1007
+ return new Promise((resolve) => this.server.close(() => resolve()));
1008
+ }
1009
+ };
1010
+
1011
+ // src/coordinator.ts
1012
+ var HarnessCoordinator = class {
1013
+ constructor(options) {
1014
+ this.options = options;
1015
+ const workboardPath = join3(options.projectRoot, ".claude", "workboard.md");
1016
+ this.workboard = new WorkboardManager(workboardPath);
1017
+ }
1018
+ registry = new HarnessRegistry();
1019
+ rpcServer = null;
1020
+ sessionId = null;
1021
+ workboard;
1022
+ async start() {
1023
+ await autoDetectHarnesses(this.registry);
1024
+ const type = detectSessionType();
1025
+ const state = this.workboard.read();
1026
+ const existingIds = state.sessions.map((s) => s.id);
1027
+ this.sessionId = deriveSessionId(type, existingIds);
1028
+ const envLabels = {
1029
+ zed: "Zed/ACP",
1030
+ cursor: "Cursor",
1031
+ terminal: "WSL/bash"
1032
+ };
1033
+ this.workboard.registerSession({
1034
+ id: this.sessionId,
1035
+ env: envLabels[type] ?? type,
1036
+ started: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`,
1037
+ task: this.options.task ?? "Harness coordination active",
1038
+ files: "",
1039
+ updated: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`
1040
+ });
1041
+ const socketPath = this.options.socketPath ?? join3(process.env.HOME ?? "/tmp", ".local", "share", "revealui", "harness.sock");
1042
+ this.rpcServer = new RpcServer(this.registry, socketPath);
1043
+ this.rpcServer.setHealthCheck(() => this.healthCheck());
1044
+ await this.rpcServer.start();
1045
+ }
1046
+ async stop() {
1047
+ if (this.sessionId) {
1048
+ this.workboard.unregisterSession(this.sessionId);
1049
+ this.workboard.addRecentEntry({
1050
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " "),
1051
+ sessionId: this.sessionId,
1052
+ description: `Session ended \u2014 ${this.options.task ?? "harness coordination"}`
1053
+ });
1054
+ }
1055
+ if (this.rpcServer) {
1056
+ await this.rpcServer.stop();
1057
+ this.rpcServer = null;
1058
+ }
1059
+ await this.registry.disposeAll();
1060
+ }
1061
+ /** The registry of detected harnesses. Available after start(). */
1062
+ getRegistry() {
1063
+ return this.registry;
1064
+ }
1065
+ /** The workboard manager. */
1066
+ getWorkboard() {
1067
+ return this.workboard;
1068
+ }
1069
+ /** Register a custom adapter (must be called before start()). */
1070
+ registerAdapter(adapter) {
1071
+ this.registry.register(adapter);
1072
+ }
1073
+ /** Run a health check across all registered harnesses and the workboard. */
1074
+ async healthCheck() {
1075
+ const diagnostics = [];
1076
+ const StaleMs = 4 * 60 * 60 * 1e3;
1077
+ const allIds = this.registry.listAll();
1078
+ const registeredHarnesses = await Promise.all(
1079
+ allIds.map(async (harnessId) => {
1080
+ const adapter = this.registry.get(harnessId);
1081
+ let available = false;
1082
+ try {
1083
+ available = adapter ? await adapter.isAvailable() : false;
1084
+ } catch (err) {
1085
+ diagnostics.push(
1086
+ `${harnessId}: availability check failed \u2014 ${err instanceof Error ? err.message : String(err)}`
1087
+ );
1088
+ }
1089
+ if (!available) diagnostics.push(`${harnessId}: not available`);
1090
+ return { harnessId, available };
1091
+ })
1092
+ );
1093
+ let readable = false;
1094
+ let sessionCount = 0;
1095
+ const staleSessionIds = [];
1096
+ try {
1097
+ const state = this.workboard.read();
1098
+ readable = true;
1099
+ sessionCount = state.sessions.length;
1100
+ const now = Date.now();
1101
+ for (const s of state.sessions) {
1102
+ const ts = Date.parse(s.updated);
1103
+ if (!Number.isNaN(ts) && now - ts > StaleMs) {
1104
+ staleSessionIds.push(s.id);
1105
+ }
1106
+ }
1107
+ if (staleSessionIds.length > 0) {
1108
+ diagnostics.push(`Stale sessions: ${staleSessionIds.join(", ")}`);
1109
+ }
1110
+ } catch (err) {
1111
+ diagnostics.push(`Workboard unreadable: ${err instanceof Error ? err.message : String(err)}`);
1112
+ }
1113
+ const healthy = registeredHarnesses.some((h) => h.available) && readable && staleSessionIds.length === 0;
1114
+ return {
1115
+ healthy,
1116
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1117
+ registeredHarnesses,
1118
+ workboard: { readable, sessionCount, staleSessionIds },
1119
+ diagnostics
1120
+ };
1121
+ }
1122
+ };
1123
+
1124
+ // src/index.ts
1125
+ async function checkHarnessesLicense() {
1126
+ await initializeLicense();
1127
+ if (!isFeatureEnabled("ai")) {
1128
+ logger.warn(
1129
+ "[@revealui/harnesses] AI harness integration requires a Pro or Enterprise license. Visit https://revealui.com/pricing for details.",
1130
+ { feature: "ai" }
1131
+ );
1132
+ return false;
1133
+ }
1134
+ return true;
1135
+ }
1136
+
1137
+ export {
1138
+ ClaudeCodeAdapter,
1139
+ CursorAdapter,
1140
+ autoDetectHarnesses,
1141
+ HarnessRegistry,
1142
+ getLocalConfigPath,
1143
+ getRootConfigPath,
1144
+ getConfigurableHarnesses,
1145
+ syncConfig,
1146
+ diffConfig,
1147
+ syncAllConfigs,
1148
+ diffAllConfigs,
1149
+ validateConfigJson,
1150
+ findProcesses,
1151
+ findHarnessProcesses,
1152
+ findAllHarnessProcesses,
1153
+ findClaudeCodeSockets,
1154
+ RpcServer,
1155
+ HarnessCoordinator,
1156
+ checkHarnessesLicense
1157
+ };
1158
+ //# sourceMappingURL=chunk-HH2PJYQN.js.map