agent-relay 2.1.1 → 2.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.
Files changed (70) hide show
  1. package/dist/index.cjs +179 -8
  2. package/dist/src/cli/index.d.ts +11 -1
  3. package/dist/src/cli/index.d.ts.map +1 -1
  4. package/dist/src/cli/index.js +112 -110
  5. package/dist/src/cli/index.js.map +1 -1
  6. package/package.json +18 -18
  7. package/packages/api-types/package.json +1 -1
  8. package/packages/benchmark/package.json +4 -4
  9. package/packages/bridge/package.json +8 -8
  10. package/packages/cli-tester/package.json +1 -1
  11. package/packages/config/package.json +2 -2
  12. package/packages/continuity/package.json +2 -2
  13. package/packages/daemon/dist/connection.d.ts +5 -0
  14. package/packages/daemon/dist/connection.d.ts.map +1 -1
  15. package/packages/daemon/dist/connection.js +19 -1
  16. package/packages/daemon/dist/connection.js.map +1 -1
  17. package/packages/daemon/dist/server.js +2 -2
  18. package/packages/daemon/dist/server.js.map +1 -1
  19. package/packages/daemon/package.json +12 -12
  20. package/packages/daemon/src/connection.ts +22 -1
  21. package/packages/daemon/src/router.test.ts +32 -0
  22. package/packages/daemon/src/server.ts +2 -2
  23. package/packages/hooks/package.json +4 -4
  24. package/packages/mcp/package.json +3 -3
  25. package/packages/memory/package.json +2 -2
  26. package/packages/policy/package.json +2 -2
  27. package/packages/protocol/dist/types.d.ts +5 -0
  28. package/packages/protocol/dist/types.d.ts.map +1 -1
  29. package/packages/protocol/package.json +1 -1
  30. package/packages/protocol/src/types.ts +5 -0
  31. package/packages/resiliency/package.json +1 -1
  32. package/packages/sdk/dist/client.d.ts +6 -0
  33. package/packages/sdk/dist/client.d.ts.map +1 -1
  34. package/packages/sdk/dist/client.js +1 -0
  35. package/packages/sdk/dist/client.js.map +1 -1
  36. package/packages/sdk/package.json +2 -2
  37. package/packages/sdk/src/client.ts +7 -0
  38. package/packages/spawner/package.json +1 -1
  39. package/packages/state/package.json +1 -1
  40. package/packages/storage/dist/adapter.d.ts +2 -0
  41. package/packages/storage/dist/adapter.d.ts.map +1 -1
  42. package/packages/storage/dist/adapter.js +7 -1
  43. package/packages/storage/dist/adapter.js.map +1 -1
  44. package/packages/storage/dist/jsonl-adapter.d.ts +14 -0
  45. package/packages/storage/dist/jsonl-adapter.d.ts.map +1 -1
  46. package/packages/storage/dist/jsonl-adapter.js +75 -0
  47. package/packages/storage/dist/jsonl-adapter.js.map +1 -1
  48. package/packages/storage/package.json +2 -2
  49. package/packages/storage/src/adapter.ts +9 -1
  50. package/packages/storage/src/jsonl-adapter.test.ts +31 -0
  51. package/packages/storage/src/jsonl-adapter.ts +86 -0
  52. package/packages/telemetry/package.json +1 -1
  53. package/packages/trajectory/package.json +2 -2
  54. package/packages/user-directory/package.json +2 -2
  55. package/packages/utils/package.json +2 -2
  56. package/packages/wrapper/dist/base-wrapper.d.ts +5 -0
  57. package/packages/wrapper/dist/base-wrapper.d.ts.map +1 -1
  58. package/packages/wrapper/dist/base-wrapper.js +14 -1
  59. package/packages/wrapper/dist/base-wrapper.js.map +1 -1
  60. package/packages/wrapper/dist/shared.d.ts +36 -0
  61. package/packages/wrapper/dist/shared.d.ts.map +1 -1
  62. package/packages/wrapper/dist/shared.js +123 -2
  63. package/packages/wrapper/dist/shared.js.map +1 -1
  64. package/packages/wrapper/dist/tmux-wrapper.js +1 -1
  65. package/packages/wrapper/dist/tmux-wrapper.js.map +1 -1
  66. package/packages/wrapper/package.json +6 -6
  67. package/packages/wrapper/src/base-wrapper.ts +15 -0
  68. package/packages/wrapper/src/shared.test.ts +156 -11
  69. package/packages/wrapper/src/shared.ts +154 -2
  70. package/packages/wrapper/src/tmux-wrapper.ts +1 -1
package/dist/index.cjs CHANGED
@@ -26022,7 +26022,7 @@ var jsonl_adapter_exports = {};
26022
26022
  __export(jsonl_adapter_exports, {
26023
26023
  JsonlStorageAdapter: () => JsonlStorageAdapter
26024
26024
  });
26025
- var import_node_fs20, import_node_path22, DEFAULT_RETENTION_MS2, DEFAULT_CLEANUP_INTERVAL_MS2, JsonlStorageAdapter;
26025
+ var import_node_fs20, import_node_path22, DEFAULT_RETENTION_MS2, DEFAULT_CLEANUP_INTERVAL_MS2, DEFAULT_WATCH_DEBOUNCE_MS, JsonlStorageAdapter;
26026
26026
  var init_jsonl_adapter = __esm({
26027
26027
  "packages/storage/dist/jsonl-adapter.js"() {
26028
26028
  "use strict";
@@ -26030,6 +26030,7 @@ var init_jsonl_adapter = __esm({
26030
26030
  import_node_path22 = __toESM(require("node:path"), 1);
26031
26031
  DEFAULT_RETENTION_MS2 = 7 * 24 * 60 * 60 * 1e3;
26032
26032
  DEFAULT_CLEANUP_INTERVAL_MS2 = 60 * 60 * 1e3;
26033
+ DEFAULT_WATCH_DEBOUNCE_MS = 100;
26033
26034
  JsonlStorageAdapter = class {
26034
26035
  baseDir;
26035
26036
  messageDir;
@@ -26045,6 +26046,12 @@ var init_jsonl_adapter = __esm({
26045
26046
  deletedMessages = /* @__PURE__ */ new Set();
26046
26047
  sessions = /* @__PURE__ */ new Map();
26047
26048
  resumeIndex = /* @__PURE__ */ new Map();
26049
+ watchForChanges;
26050
+ watchDebounceMs;
26051
+ messageWatcher;
26052
+ sessionWatcher;
26053
+ reloadDebounceTimer;
26054
+ sessionReloadDebounceTimer;
26048
26055
  constructor(options) {
26049
26056
  this.baseDir = options.baseDir;
26050
26057
  this.messageDir = import_node_path22.default.join(this.baseDir, "messages");
@@ -26052,6 +26059,8 @@ var init_jsonl_adapter = __esm({
26052
26059
  this.retentionMs = options.messageRetentionMs ?? DEFAULT_RETENTION_MS2;
26053
26060
  this.cleanupIntervalMs = options.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS2;
26054
26061
  this.fallbackReason = options.reason;
26062
+ this.watchForChanges = options.watchForChanges ?? false;
26063
+ this.watchDebounceMs = options.watchDebounceMs ?? DEFAULT_WATCH_DEBOUNCE_MS;
26055
26064
  }
26056
26065
  async init() {
26057
26066
  await import_node_fs20.default.promises.mkdir(this.messageDir, { recursive: true });
@@ -26062,12 +26071,16 @@ var init_jsonl_adapter = __esm({
26062
26071
  if (this.cleanupIntervalMs > 0) {
26063
26072
  this.startCleanupTimer();
26064
26073
  }
26074
+ if (this.watchForChanges) {
26075
+ this.startFileWatching();
26076
+ }
26065
26077
  }
26066
26078
  async close() {
26067
26079
  if (this.cleanupTimer) {
26068
26080
  clearInterval(this.cleanupTimer);
26069
26081
  this.cleanupTimer = void 0;
26070
26082
  }
26083
+ this.stopFileWatching();
26071
26084
  this.messages.clear();
26072
26085
  this.deletedMessages.clear();
26073
26086
  this.sessions.clear();
@@ -26323,6 +26336,64 @@ var init_jsonl_adapter = __esm({
26323
26336
  this.cleanupTimer.unref();
26324
26337
  }
26325
26338
  }
26339
+ startFileWatching() {
26340
+ try {
26341
+ this.messageWatcher = import_node_fs20.default.watch(this.messageDir, (eventType, filename) => {
26342
+ if (filename && filename.endsWith(".jsonl")) {
26343
+ this.debouncedReloadMessages();
26344
+ }
26345
+ });
26346
+ if (this.messageWatcher.unref) {
26347
+ this.messageWatcher.unref();
26348
+ }
26349
+ } catch {
26350
+ }
26351
+ try {
26352
+ this.sessionWatcher = import_node_fs20.default.watch(this.sessionFile, () => {
26353
+ this.debouncedReloadSessions();
26354
+ });
26355
+ if (this.sessionWatcher.unref) {
26356
+ this.sessionWatcher.unref();
26357
+ }
26358
+ } catch {
26359
+ }
26360
+ }
26361
+ stopFileWatching() {
26362
+ if (this.messageWatcher) {
26363
+ this.messageWatcher.close();
26364
+ this.messageWatcher = void 0;
26365
+ }
26366
+ if (this.sessionWatcher) {
26367
+ this.sessionWatcher.close();
26368
+ this.sessionWatcher = void 0;
26369
+ }
26370
+ if (this.reloadDebounceTimer) {
26371
+ clearTimeout(this.reloadDebounceTimer);
26372
+ this.reloadDebounceTimer = void 0;
26373
+ }
26374
+ if (this.sessionReloadDebounceTimer) {
26375
+ clearTimeout(this.sessionReloadDebounceTimer);
26376
+ this.sessionReloadDebounceTimer = void 0;
26377
+ }
26378
+ }
26379
+ debouncedReloadMessages() {
26380
+ if (this.reloadDebounceTimer) {
26381
+ clearTimeout(this.reloadDebounceTimer);
26382
+ }
26383
+ this.reloadDebounceTimer = setTimeout(() => {
26384
+ this.loadMessagesFromDisk().catch(() => {
26385
+ });
26386
+ }, this.watchDebounceMs);
26387
+ }
26388
+ debouncedReloadSessions() {
26389
+ if (this.sessionReloadDebounceTimer) {
26390
+ clearTimeout(this.sessionReloadDebounceTimer);
26391
+ }
26392
+ this.sessionReloadDebounceTimer = setTimeout(() => {
26393
+ this.loadSessionsFromDisk().catch(() => {
26394
+ });
26395
+ }, this.watchDebounceMs);
26396
+ }
26326
26397
  async loadMessagesFromDisk() {
26327
26398
  this.messages.clear();
26328
26399
  this.deletedMessages.clear();
@@ -35497,6 +35568,7 @@ __export(index_exports, {
35497
35568
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
35498
35569
  PROVIDER_AUTH_PATTERNS: () => PROVIDER_AUTH_PATTERNS,
35499
35570
  PostgresDLQAdapter: () => PostgresDLQAdapter,
35571
+ RESERVED_AGENT_NAMES: () => RESERVED_AGENT_NAMES,
35500
35572
  RelayClient: () => RelayClient,
35501
35573
  RelayEvent: () => RelayEvent,
35502
35574
  RelayPtyOrchestrator: () => RelayPtyOrchestrator,
@@ -37229,13 +37301,81 @@ function stripAnsi2(str) {
37229
37301
  function sleep(ms) {
37230
37302
  return new Promise((resolve5) => setTimeout(resolve5, ms));
37231
37303
  }
37304
+ var AUTO_SUGGEST_PATTERNS = {
37305
+ // Dim text styling - commonly used for ghost text
37306
+ dim: /\x1B\[2m/,
37307
+ // Bright black (dark gray) - common for suggestions
37308
+ brightBlack: /\x1B\[90m/,
37309
+ // 256-color grays (8 is dark gray, 240-250 are grays)
37310
+ gray256: /\x1B\[38;5;(?:8|24[0-9]|250)m/,
37311
+ // Cursor save (CSI s or ESC 7)
37312
+ cursorSave: /\x1B\[s|\x1B7/,
37313
+ // Cursor restore (CSI u or ESC 8)
37314
+ cursorRestore: /\x1B\[u|\x1B8/,
37315
+ // Italic text - sometimes used for suggestions
37316
+ italic: /\x1B\[3m/
37317
+ };
37318
+ function detectAutoSuggest(output) {
37319
+ const patterns = [];
37320
+ let confidence = 0;
37321
+ if (AUTO_SUGGEST_PATTERNS.dim.test(output)) {
37322
+ patterns.push("dim");
37323
+ confidence += 0.4;
37324
+ }
37325
+ if (AUTO_SUGGEST_PATTERNS.brightBlack.test(output)) {
37326
+ patterns.push("brightBlack");
37327
+ confidence += 0.4;
37328
+ }
37329
+ if (AUTO_SUGGEST_PATTERNS.gray256.test(output)) {
37330
+ patterns.push("gray256");
37331
+ confidence += 0.3;
37332
+ }
37333
+ if (AUTO_SUGGEST_PATTERNS.italic.test(output)) {
37334
+ patterns.push("italic");
37335
+ confidence += 0.2;
37336
+ }
37337
+ const hasCursorSave = AUTO_SUGGEST_PATTERNS.cursorSave.test(output);
37338
+ const hasCursorRestore = AUTO_SUGGEST_PATTERNS.cursorRestore.test(output);
37339
+ if (hasCursorSave && hasCursorRestore) {
37340
+ patterns.push("cursorSaveRestore");
37341
+ confidence += 0.5;
37342
+ } else if (hasCursorSave || hasCursorRestore) {
37343
+ patterns.push(hasCursorSave ? "cursorSave" : "cursorRestore");
37344
+ confidence += 0.2;
37345
+ }
37346
+ confidence = Math.min(confidence, 1);
37347
+ const stripped = stripAnsi2(output);
37348
+ if (patterns.length === 0) {
37349
+ return { isAutoSuggest: false, confidence: 0, patterns, strippedContent: stripped };
37350
+ }
37351
+ const lines = stripped.split("\n").filter((l) => l.trim().length > 0);
37352
+ if (lines.length > 2) {
37353
+ confidence *= 0.5;
37354
+ }
37355
+ const isAutoSuggest = confidence >= 0.4;
37356
+ return { isAutoSuggest, confidence, patterns, strippedContent: stripped };
37357
+ }
37358
+ function shouldIgnoreForIdleDetection(output) {
37359
+ if (!output || output.length === 0) {
37360
+ return true;
37361
+ }
37362
+ const result = detectAutoSuggest(output);
37363
+ if (result.isAutoSuggest) {
37364
+ return true;
37365
+ }
37366
+ const stripped = stripAnsi2(output).trim();
37367
+ if (stripped.length === 0) {
37368
+ return true;
37369
+ }
37370
+ return false;
37371
+ }
37232
37372
  function buildInjectionString(msg) {
37233
37373
  const sanitizedBody = stripAnsi2(msg.body || "").replace(/[\r\n]+/g, " ").trim();
37234
37374
  if (sanitizedBody.startsWith("Relay message from ")) {
37235
37375
  return sanitizedBody;
37236
37376
  }
37237
37377
  const shortId = msg.messageId.substring(0, 8);
37238
- const displayFrom = msg.from === "_DashboardUI" && typeof msg.data?.senderName === "string" ? msg.data.senderName : msg.from;
37378
+ const displayFrom = msg.from === "Dashboard" && typeof msg.data?.senderName === "string" ? msg.data.senderName : msg.from;
37239
37379
  const threadHint = msg.thread ? ` [thread:${msg.thread}]` : "";
37240
37380
  const importanceHint = msg.importance !== void 0 && msg.importance > 75 ? " [!!]" : msg.importance !== void 0 && msg.importance > 50 ? " [!]" : "";
37241
37381
  const channelHint = msg.originalTo === "*" ? " [#general] (reply to #general, not sender)" : msg.originalTo?.startsWith("#") ? ` [${msg.originalTo}] (reply to ${msg.originalTo}, not sender)` : "";
@@ -42483,8 +42623,17 @@ var BaseWrapper = class extends import_node_events2.EventEmitter {
42483
42623
  /**
42484
42624
  * Feed output to the idle and stuck detectors.
42485
42625
  * Call this whenever new output is received from the agent.
42626
+ *
42627
+ * Note: Auto-suggestions (ghost text) are filtered out to prevent
42628
+ * false idle resets. Claude Code and other CLIs show suggestions
42629
+ * in gray/dim text with cursor save/restore, which should not
42630
+ * be treated as "real" output for idle detection.
42486
42631
  */
42487
42632
  feedIdleDetectorOutput(output) {
42633
+ if (shouldIgnoreForIdleDetection(output)) {
42634
+ this.stuckDetector.onOutput(output);
42635
+ return;
42636
+ }
42488
42637
  this.idleDetector.onOutput(output);
42489
42638
  this.stuckDetector.onOutput(output);
42490
42639
  }
@@ -52803,6 +52952,16 @@ function generateEventSchemas() {
52803
52952
  }
52804
52953
 
52805
52954
  // packages/daemon/dist/connection.js
52955
+ var RESERVED_AGENT_NAMES = /* @__PURE__ */ new Set([
52956
+ "Dashboard",
52957
+ // Dashboard system client
52958
+ "cli",
52959
+ // CLI tool
52960
+ "system",
52961
+ // System messages
52962
+ "_router"
52963
+ // Internal router target
52964
+ ]);
52806
52965
  var DEFAULT_CONFIG10 = {
52807
52966
  ...DEFAULT_CONNECTION_CONFIG
52808
52967
  };
@@ -52951,7 +53110,12 @@ var Connection = class {
52951
53110
  this.sendError("BAD_REQUEST", "Unexpected HELLO", false);
52952
53111
  return;
52953
53112
  }
52954
- this._agentName = envelope.payload.agent;
53113
+ const agentName = envelope.payload.agent;
53114
+ if (RESERVED_AGENT_NAMES.has(agentName) && !envelope.payload._isSystemComponent) {
53115
+ this.sendError("BAD_REQUEST", `Agent name "${agentName}" is reserved for system use`, true);
53116
+ return;
53117
+ }
53118
+ this._agentName = agentName;
52955
53119
  this._entityType = envelope.payload.entityType;
52956
53120
  this._cli = envelope.payload.cli;
52957
53121
  this._program = envelope.payload.program;
@@ -69989,7 +70153,8 @@ async function createStorageAdapter(dbPath, config2) {
69989
70153
  const finalConfig = {
69990
70154
  type: config2?.type ?? envConfig.type ?? "jsonl",
69991
70155
  path: config2?.path ?? envConfig.path ?? dbPath,
69992
- url: config2?.url ?? envConfig.url
70156
+ url: config2?.url ?? envConfig.url,
70157
+ watchForChanges: config2?.watchForChanges
69993
70158
  };
69994
70159
  const storageType = finalConfig.type?.toLowerCase();
69995
70160
  switch (storageType) {
@@ -70027,7 +70192,8 @@ async function createStorageAdapter(dbPath, config2) {
70027
70192
  console.warn("[storage] \u26A0\uFE0F Falling back to JSONL storage (append-only files)");
70028
70193
  const adapter2 = new JsonlStorageAdapter2({
70029
70194
  baseDir,
70030
- reason: "upgrade to Node.js 22+ or run: npm rebuild better-sqlite3"
70195
+ reason: "upgrade to Node.js 22+ or run: npm rebuild better-sqlite3",
70196
+ watchForChanges: finalConfig.watchForChanges
70031
70197
  });
70032
70198
  await adapter2.init();
70033
70199
  return adapter2;
@@ -70048,7 +70214,10 @@ async function createStorageAdapter(dbPath, config2) {
70048
70214
  const { JsonlStorageAdapter: JsonlStorageAdapter2 } = await Promise.resolve().then(() => (init_jsonl_adapter(), jsonl_adapter_exports));
70049
70215
  const baseDir = import_node_path23.default.dirname(finalConfig.path);
70050
70216
  console.error("[storage] Using JSONL storage");
70051
- const adapter = new JsonlStorageAdapter2({ baseDir });
70217
+ const adapter = new JsonlStorageAdapter2({
70218
+ baseDir,
70219
+ watchForChanges: finalConfig.watchForChanges
70220
+ });
70052
70221
  await adapter.init();
70053
70222
  return adapter;
70054
70223
  }
@@ -70068,7 +70237,8 @@ async function createStorageAdapter(dbPath, config2) {
70068
70237
  console.warn("[storage] \u26A0\uFE0F Falling back to JSONL storage (append-only files)");
70069
70238
  const adapter2 = new JsonlStorageAdapter2({
70070
70239
  baseDir,
70071
- reason: "upgrade to Node.js 22+ or run: npm rebuild better-sqlite3"
70240
+ reason: "upgrade to Node.js 22+ or run: npm rebuild better-sqlite3",
70241
+ watchForChanges: finalConfig.watchForChanges
70072
70242
  });
70073
70243
  await adapter2.init();
70074
70244
  return adapter2;
@@ -72681,7 +72851,7 @@ var Daemon = class _Daemon {
72681
72851
  isInternalAgent(name) {
72682
72852
  if (name.startsWith("__"))
72683
72853
  return true;
72684
- return name === "Dashboard" || name === "_DashboardUI" || name === "cli";
72854
+ return name === "Dashboard" || name === "cli";
72685
72855
  }
72686
72856
  /**
72687
72857
  * Stop the daemon.
@@ -78770,6 +78940,7 @@ init_dist();
78770
78940
  PROTOCOL_VERSION,
78771
78941
  PROVIDER_AUTH_PATTERNS,
78772
78942
  PostgresDLQAdapter,
78943
+ RESERVED_AGENT_NAMES,
78773
78944
  RelayClient,
78774
78945
  RelayEvent,
78775
78946
  RelayPtyOrchestrator,
@@ -12,5 +12,15 @@
12
12
  * relay agents - List connected agents
13
13
  * relay who - Show currently active agents
14
14
  */
15
- export {};
15
+ /**
16
+ * Install agent-relay-snippet to markdown files using prpm.
17
+ * Installs to CLAUDE.md, GEMINI.md, and AGENTS.md.
18
+ * prpm handles idempotency - won't duplicate if already installed.
19
+ */
20
+ export declare function installRelaySnippets(options?: {
21
+ silent?: boolean;
22
+ }): Promise<{
23
+ success: boolean;
24
+ installed: string[];
25
+ }>;
16
26
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AAuGH;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAqC7H"}
@@ -29,59 +29,9 @@ import readline from 'node:readline';
29
29
  import { promisify } from 'node:util';
30
30
  import { exec, spawn as spawnProcess } from 'node:child_process';
31
31
  import { fileURLToPath } from 'node:url';
32
- const RELAY_DASHBOARD_REPO = 'https://github.com/AgentWorkforce/relay-dashboard';
33
- /**
34
- * Prompt user to choose how to handle missing dashboard package.
35
- * Returns: 'npx' | 'install' | 'skip'
36
- */
37
- async function promptDashboardInstall() {
38
- const rl = readline.createInterface({
39
- input: process.stdin,
40
- output: process.stdout,
41
- });
42
- console.log(`
43
- The web dashboard requires @agent-relay/dashboard-server package.
44
-
45
- How would you like to proceed?
46
- 1. Start with npx (recommended - auto-installs temporarily)
47
- 2. View installation instructions
48
- 3. Skip and continue without dashboard
49
- `);
50
- return new Promise((resolve) => {
51
- rl.question('Choose [1/2/3]: ', (answer) => {
52
- rl.close();
53
- const choice = answer.trim();
54
- if (choice === '1') {
55
- resolve('npx');
56
- }
57
- else if (choice === '2') {
58
- resolve('install');
59
- }
60
- else {
61
- resolve('skip');
62
- }
63
- });
64
- });
65
- }
66
- /**
67
- * Show instructions for installing the external dashboard package.
68
- */
69
- function showDashboardInstallInstructions() {
70
- console.log(`
71
- To install the dashboard, run:
72
-
73
- npm install @agent-relay/dashboard-server @agent-relay/dashboard
74
-
75
- Then restart with:
76
-
77
- agent-relay up --dashboard
78
-
79
- For more options, see: ${RELAY_DASHBOARD_REPO}
80
- `);
81
- }
82
32
  /**
83
33
  * Start dashboard via npx (downloads and runs if not installed).
84
- * Returns the spawned child process and port.
34
+ * Returns the spawned child process, port, and a promise that resolves when ready.
85
35
  */
86
36
  function startDashboardViaNpx(options) {
87
37
  console.log('Starting dashboard via npx (this may take a moment on first run)...');
@@ -100,10 +50,21 @@ function startDashboardViaNpx(options) {
100
50
  // Pass any additional env vars needed
101
51
  },
102
52
  });
53
+ // Promise that resolves when dashboard is ready (or after timeout)
54
+ let resolveReady;
55
+ const ready = new Promise((resolve) => {
56
+ resolveReady = resolve;
57
+ // Fallback timeout in case we miss the ready signal
58
+ setTimeout(resolve, 30000);
59
+ });
103
60
  // Forward dashboard output with prefix
104
61
  dashboardProcess.stdout?.on('data', (data) => {
105
62
  const lines = data.toString().split('\n').filter(Boolean);
106
63
  for (const line of lines) {
64
+ // Detect when dashboard is ready (listening message)
65
+ if (line.includes('Dashboard:') || line.includes('listening') || line.includes('ready')) {
66
+ resolveReady();
67
+ }
107
68
  // Don't duplicate the "Dashboard:" line
108
69
  if (!line.includes('Dashboard:')) {
109
70
  console.log(`[dashboard] ${line}`);
@@ -118,13 +79,56 @@ function startDashboardViaNpx(options) {
118
79
  });
119
80
  dashboardProcess.on('error', (err) => {
120
81
  console.error('Failed to start dashboard via npx:', err.message);
82
+ resolveReady(); // Resolve to not block forever
121
83
  });
122
84
  dashboardProcess.on('exit', (code) => {
85
+ resolveReady(); // Resolve on exit to not block forever
123
86
  if (code !== 0 && code !== null) {
124
87
  console.error(`Dashboard process exited with code ${code}`);
125
88
  }
126
89
  });
127
- return { process: dashboardProcess, port: options.port };
90
+ return { process: dashboardProcess, port: options.port, ready };
91
+ }
92
+ /**
93
+ * Install agent-relay-snippet to markdown files using prpm.
94
+ * Installs to CLAUDE.md, GEMINI.md, and AGENTS.md.
95
+ * prpm handles idempotency - won't duplicate if already installed.
96
+ */
97
+ export async function installRelaySnippets(options) {
98
+ const execAsync = promisify(exec);
99
+ const installed = [];
100
+ const targets = [
101
+ { location: 'CLAUDE.md', name: 'CLAUDE.md' },
102
+ { location: 'GEMINI.md', name: 'GEMINI.md' },
103
+ { location: undefined, name: 'AGENTS.md' }, // Default location
104
+ ];
105
+ for (const target of targets) {
106
+ try {
107
+ const args = ['npx', 'prpm', 'install', '@agent-relay/agent-relay-snippet'];
108
+ if (target.location) {
109
+ args.push('--location', target.location);
110
+ }
111
+ await execAsync(args.join(' '), { timeout: 60000 });
112
+ installed.push(target.name);
113
+ if (!options?.silent) {
114
+ console.log(` ✓ Installed to ${target.name}`);
115
+ }
116
+ }
117
+ catch (err) {
118
+ // prpm exits with error if already installed or other issues
119
+ // Check if it's an "already exists" situation by looking at stderr
120
+ if (err.stderr?.includes('already') || err.stdout?.includes('already')) {
121
+ if (!options?.silent) {
122
+ console.log(` ✓ ${target.name} (already installed)`);
123
+ }
124
+ installed.push(target.name);
125
+ }
126
+ else if (!options?.silent) {
127
+ console.error(` ⚠ Failed to install to ${target.name}: ${err.message}`);
128
+ }
129
+ }
130
+ }
131
+ return { success: installed.length > 0, installed };
128
132
  }
129
133
  dotenvConfig();
130
134
  const DEFAULT_DASHBOARD_PORT = process.env.AGENT_RELAY_DASHBOARD_PORT || '3888';
@@ -465,6 +469,36 @@ program
465
469
  const socketPath = paths.socketPath;
466
470
  const dbPath = paths.dbPath;
467
471
  const pidFilePath = pidFilePathForSocket(socketPath);
472
+ // Auto-install relay protocol snippets if not already present
473
+ // Check if the snippet marker exists in any of the target files
474
+ const snippetTargets = ['CLAUDE.md', 'GEMINI.md', 'AGENTS.md'];
475
+ const snippetMarker = '<!-- prpm:snippet:start @agent-relay/agent-relay-snippet';
476
+ const hasSnippetInstalled = snippetTargets.some(file => {
477
+ const filePath = path.join(paths.projectRoot, file);
478
+ if (!fs.existsSync(filePath))
479
+ return false;
480
+ try {
481
+ const content = fs.readFileSync(filePath, 'utf-8');
482
+ return content.includes(snippetMarker);
483
+ }
484
+ catch {
485
+ return false;
486
+ }
487
+ });
488
+ if (!hasSnippetInstalled) {
489
+ console.log('Installing relay protocol snippets...');
490
+ try {
491
+ const result = await installRelaySnippets({ silent: false });
492
+ if (result.success) {
493
+ console.log(`Installed snippets to: ${result.installed.join(', ')}`);
494
+ }
495
+ }
496
+ catch (err) {
497
+ // Non-fatal - continue even if snippet install fails
498
+ console.log(`Note: Could not auto-install snippets: ${err.message}`);
499
+ }
500
+ console.log('');
501
+ }
468
502
  // Set up log file to avoid console output polluting TUI terminals
469
503
  // Only set if not already configured via environment
470
504
  if (!process.env.AGENT_RELAY_LOG_FILE) {
@@ -578,60 +612,27 @@ program
578
612
  if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
579
613
  // Dashboard package not installed
580
614
  if (dashboardRequested) {
581
- // User explicitly asked for dashboard but it's not installed
582
- // Only prompt interactively if stdin is a TTY (not in Docker/CI)
583
- if (process.stdin.isTTY) {
584
- console.log('');
585
- const action = await promptDashboardInstall();
586
- if (action === 'npx') {
587
- // Start dashboard via npx
588
- const { process: dashboardProcess, port: npxPort } = startDashboardViaNpx({
589
- port,
590
- dataDir: paths.dataDir,
591
- teamDir: paths.teamDir,
592
- projectRoot: paths.projectRoot,
593
- });
594
- dashboardPort = npxPort;
595
- // Clean up dashboard process on exit
596
- const cleanupDashboard = () => {
597
- if (dashboardProcess && !dashboardProcess.killed) {
598
- dashboardProcess.kill('SIGTERM');
599
- }
600
- };
601
- process.on('SIGINT', cleanupDashboard);
602
- process.on('SIGTERM', cleanupDashboard);
603
- process.on('exit', cleanupDashboard);
604
- // Wait a moment for dashboard to start
605
- await new Promise(resolve => setTimeout(resolve, 2000));
606
- console.log(`Dashboard: http://localhost:${dashboardPort}`);
615
+ // User explicitly asked for dashboard but it's not installed - start via npx
616
+ console.log('Dashboard package not installed. Starting via npx...');
617
+ const { process: dashboardProcess, port: npxPort, ready } = startDashboardViaNpx({
618
+ port,
619
+ dataDir: paths.dataDir,
620
+ teamDir: paths.teamDir,
621
+ projectRoot: paths.projectRoot,
622
+ });
623
+ dashboardPort = npxPort;
624
+ // Clean up dashboard process on exit
625
+ const cleanupDashboard = () => {
626
+ if (dashboardProcess && !dashboardProcess.killed) {
627
+ dashboardProcess.kill('SIGTERM');
607
628
  }
608
- else if (action === 'install') {
609
- showDashboardInstallInstructions();
610
- }
611
- }
612
- else {
613
- // Non-interactive: try npx automatically
614
- console.log('Dashboard package not installed. Starting via npx...');
615
- const { process: dashboardProcess, port: npxPort } = startDashboardViaNpx({
616
- port,
617
- dataDir: paths.dataDir,
618
- teamDir: paths.teamDir,
619
- projectRoot: paths.projectRoot,
620
- });
621
- dashboardPort = npxPort;
622
- // Clean up dashboard process on exit
623
- const cleanupDashboard = () => {
624
- if (dashboardProcess && !dashboardProcess.killed) {
625
- dashboardProcess.kill('SIGTERM');
626
- }
627
- };
628
- process.on('SIGINT', cleanupDashboard);
629
- process.on('SIGTERM', cleanupDashboard);
630
- process.on('exit', cleanupDashboard);
631
- // Wait a moment for dashboard to start
632
- await new Promise(resolve => setTimeout(resolve, 3000));
633
- console.log(`Dashboard: http://localhost:${dashboardPort}`);
634
- }
629
+ };
630
+ process.on('SIGINT', cleanupDashboard);
631
+ process.on('SIGTERM', cleanupDashboard);
632
+ process.on('exit', cleanupDashboard);
633
+ // Wait for dashboard to be ready
634
+ await ready;
635
+ console.log(`Dashboard: http://localhost:${dashboardPort}`);
635
636
  }
636
637
  // Silent if user didn't explicitly request dashboard
637
638
  }
@@ -3155,10 +3156,11 @@ async function runInit(options) {
3155
3156
  console.log(' ○ Daemon is not running');
3156
3157
  }
3157
3158
  console.log('');
3158
- // Step 3: Install MCP for editors
3159
+ // Step 1: Install MCP for editors (only if RELAY_MCP_AUTO_INSTALL=1)
3159
3160
  let mcpInstalled = false;
3160
- if (!options.skipMcp) {
3161
- console.log(' ┌─ Step 1: MCP Server for AI Editors ─────────────────────┐');
3161
+ const mcpAutoInstallEnabled = process.env.RELAY_MCP_AUTO_INSTALL === '1';
3162
+ if (!options.skipMcp && mcpAutoInstallEnabled) {
3163
+ console.log(' ┌─ MCP Server for AI Editors ───────────────────────────────┐');
3162
3164
  console.log(' │ │');
3163
3165
  console.log(' │ MCP (Model Context Protocol) gives AI editors native │');
3164
3166
  console.log(' │ tools for agent communication: │');
@@ -3193,9 +3195,9 @@ async function runInit(options) {
3193
3195
  console.log('');
3194
3196
  }
3195
3197
  }
3196
- // Step 4: Start daemon
3198
+ // Start daemon
3197
3199
  if (!daemonRunning && !options.skipDaemon) {
3198
- console.log(' ┌─ Step 2: Start the Relay Daemon ─────────────────────────┐');
3200
+ console.log(' ┌─ Start the Relay Daemon ──────────────────────────────────┐');
3199
3201
  console.log(' │ │');
3200
3202
  console.log(' │ The daemon manages agent connections and message │');
3201
3203
  console.log(' │ routing. It runs in the background. │');