@adhdev/daemon-core 0.9.82-rc.14 → 0.9.82-rc.140

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 (115) hide show
  1. package/dist/chat/source-machine.d.ts +166 -0
  2. package/dist/chat/source-resolver.d.ts +104 -0
  3. package/dist/chat/subscription-updates.d.ts +1 -0
  4. package/dist/cli-adapter-types.d.ts +5 -1
  5. package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
  6. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  7. package/dist/cli-adapters/provider-cli-adapter.d.ts +87 -63
  8. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  9. package/dist/cli-adapters/provider-cli-shared.d.ts +21 -0
  10. package/dist/commands/router.d.ts +22 -0
  11. package/dist/config/chat-history.d.ts +5 -0
  12. package/dist/config/config.d.ts +5 -0
  13. package/dist/config/mesh-config.d.ts +68 -1
  14. package/dist/git/git-commands.d.ts +5 -1
  15. package/dist/index.d.ts +18 -6
  16. package/dist/index.js +10431 -2827
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +10376 -2811
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/installer.d.ts +1 -4
  21. package/dist/launch.d.ts +1 -1
  22. package/dist/logging/async-batch-writer.d.ts +10 -0
  23. package/dist/mesh/beads-db.d.ts +72 -0
  24. package/dist/mesh/contracts.d.ts +164 -0
  25. package/dist/mesh/coordinator-registry.d.ts +25 -0
  26. package/dist/mesh/mesh-active-work.d.ts +90 -0
  27. package/dist/mesh/mesh-events.d.ts +77 -5
  28. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  29. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  30. package/dist/mesh/mesh-ledger.d.ts +58 -1
  31. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  32. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  33. package/dist/mesh/preview-freshness.d.ts +18 -0
  34. package/dist/mesh/refine-config.d.ts +193 -0
  35. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  36. package/dist/providers/approval-utils.d.ts +9 -0
  37. package/dist/providers/chat-message-normalization.d.ts +1 -0
  38. package/dist/providers/cli-provider-instance.d.ts +6 -1
  39. package/dist/providers/contracts.d.ts +19 -0
  40. package/dist/providers/read-chat-contract.d.ts +29 -0
  41. package/dist/providers/transcript-v2.d.ts +176 -0
  42. package/dist/repo-mesh-types.d.ts +67 -0
  43. package/dist/shared-types.d.ts +12 -0
  44. package/dist/status/reporter.d.ts +2 -0
  45. package/dist/status/snapshot.d.ts +1 -0
  46. package/dist/types.d.ts +5 -0
  47. package/package.json +3 -1
  48. package/src/boot/daemon-lifecycle.ts +3 -0
  49. package/src/chat/source-machine.ts +534 -0
  50. package/src/chat/source-resolver.ts +0 -0
  51. package/src/chat/subscription-updates.ts +14 -1
  52. package/src/cli-adapter-types.d.ts +1 -0
  53. package/src/cli-adapter-types.ts +3 -1
  54. package/src/cli-adapters/cli-script-runner.ts +145 -0
  55. package/src/cli-adapters/cli-state-engine.ts +1083 -0
  56. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  57. package/src/cli-adapters/provider-cli-adapter.ts +630 -1137
  58. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  59. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  60. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  61. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  62. package/src/cli-adapters/provider-cli-shared.ts +51 -11
  63. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  64. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  65. package/src/commands/chat-commands.ts +1428 -50
  66. package/src/commands/cli-manager.ts +145 -3
  67. package/src/commands/handler.ts +8 -1
  68. package/src/commands/mesh-coordinator.ts +13 -143
  69. package/src/commands/router.ts +3271 -427
  70. package/src/config/chat-history.ts +79 -24
  71. package/src/config/config.ts +12 -0
  72. package/src/config/mesh-config.ts +249 -2
  73. package/src/config/recent-activity.ts +8 -2
  74. package/src/daemon/dev-cli-debug.ts +10 -1
  75. package/src/detection/ide-detector.ts +26 -16
  76. package/src/git/git-commands.ts +17 -5
  77. package/src/git/git-worktree.ts +8 -1
  78. package/src/index.ts +45 -5
  79. package/src/installer.d.ts +1 -1
  80. package/src/installer.ts +8 -6
  81. package/src/launch.d.ts +1 -1
  82. package/src/launch.ts +37 -28
  83. package/src/logging/async-batch-writer.ts +55 -0
  84. package/src/logging/logger.ts +2 -1
  85. package/src/mesh/beads-db.ts +479 -0
  86. package/src/mesh/contracts.ts +329 -0
  87. package/src/mesh/coordinator-prompt.ts +40 -22
  88. package/src/mesh/coordinator-registry.ts +75 -0
  89. package/src/mesh/mesh-active-work.ts +437 -0
  90. package/src/mesh/mesh-events.ts +799 -56
  91. package/src/mesh/mesh-fast-forward.ts +430 -0
  92. package/src/mesh/mesh-host-ownership.ts +73 -0
  93. package/src/mesh/mesh-ledger.ts +457 -104
  94. package/src/mesh/mesh-refine-status.ts +144 -0
  95. package/src/mesh/mesh-work-queue.ts +216 -158
  96. package/src/mesh/preview-freshness.ts +118 -0
  97. package/src/mesh/refine-config.ts +366 -0
  98. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  99. package/src/providers/approval-utils.ts +39 -5
  100. package/src/providers/chat-message-normalization.ts +7 -12
  101. package/src/providers/cli-provider-instance.ts +362 -41
  102. package/src/providers/contracts.ts +19 -0
  103. package/src/providers/ide-provider-instance.ts +17 -3
  104. package/src/providers/provider-loader.ts +31 -11
  105. package/src/providers/provider-schema.ts +12 -0
  106. package/src/providers/read-chat-contract.ts +76 -16
  107. package/src/providers/transcript-v2.ts +567 -0
  108. package/src/providers/version-archive.ts +38 -20
  109. package/src/repo-mesh-types.ts +77 -0
  110. package/src/shared-types.ts +9 -0
  111. package/src/status/builders.ts +23 -6
  112. package/src/status/reporter.ts +15 -0
  113. package/src/status/snapshot.ts +35 -11
  114. package/src/system/host-memory.ts +29 -12
  115. package/src/types.ts +5 -0
@@ -18,6 +18,7 @@ import { loadConfig } from '../config/config.js';
18
18
  import { loadState, saveState } from '../config/state-store.js';
19
19
  import { getWorkspaceState, resolveLaunchDirectory } from '../config/workspaces.js';
20
20
  import { appendRecentActivity } from '../config/recent-activity.js';
21
+ import { unregisterMeshCoordinator } from '../mesh/coordinator-registry.js';
21
22
  import { upsertSavedProviderSession } from '../config/saved-sessions.js';
22
23
  import { buildLegacyModelModeSummaryMetadata, normalizeProviderSummaryMetadata } from '../providers/summary-metadata.js';
23
24
  import { CliProviderInstance } from '../providers/cli-provider-instance.js';
@@ -80,6 +81,114 @@ export interface CliManagerDeps {
80
81
 
81
82
  type CommandResult = { success: boolean;[key: string]: unknown };
82
83
 
84
+ const BUSY_AGENT_STATUSES = new Set(['generating', 'running', 'streaming', 'starting', 'busy', 'waiting', 'waiting_approval', 'long_generating']);
85
+ const ZERO_MESSAGE_STARTING_SEND_WAIT_MS = 2_000;
86
+
87
+ function normalizeAgentStatus(value: unknown): string {
88
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
89
+ }
90
+
91
+ function hasNonEmptyModalButtons(activeModal: unknown): boolean {
92
+ const buttons = (activeModal as any)?.buttons;
93
+ return Array.isArray(buttons) && buttons.some((button) => String(button || '').trim().length > 0);
94
+ }
95
+
96
+ function hasAdapterPendingResponse(adapter: any): boolean {
97
+ if (adapter?.isWaitingForResponse === true) return true;
98
+ if (adapter?.currentTurnScope) return true;
99
+ try {
100
+ if (typeof adapter?.isProcessing === 'function' && adapter.isProcessing()) return true;
101
+ } catch { /* defensive: send guard should not fail on diagnostics */ }
102
+ try {
103
+ const partial = typeof adapter?.getPartialResponse === 'function' ? adapter.getPartialResponse() : '';
104
+ if (typeof partial === 'string' && partial.trim()) return true;
105
+ } catch { /* defensive: missing partial means no pending evidence */ }
106
+ return false;
107
+ }
108
+
109
+ function countMessages(value: unknown): number {
110
+ return Array.isArray(value) ? value.length : 0;
111
+ }
112
+
113
+ function hasFinalAssistantMessage(value: unknown): boolean {
114
+ const messages = Array.isArray(value) ? value : [];
115
+ const last = messages[messages.length - 1] as any;
116
+ if (!last || last.role !== 'assistant') return false;
117
+ if (last.bubbleState === 'streaming') return false;
118
+ if (last.meta?.streaming === true) return false;
119
+ return typeof last.content === 'string' && last.content.trim().length > 0;
120
+ }
121
+
122
+ function hasZeroMessageStartingLaunch(adapter: any): boolean {
123
+ const adapterStatus = adapter?.getStatus?.({ allowParse: false }) ?? adapter?.getStatus?.() ?? {};
124
+ const parsedStatus = typeof adapter?.getScriptParsedStatus === 'function'
125
+ ? adapter.getScriptParsedStatus()
126
+ : {};
127
+ const adapterRawStatus = normalizeAgentStatus(adapterStatus?.status);
128
+ const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
129
+ if (adapterRawStatus !== 'starting') return false;
130
+ if (parsedRawStatus && parsedRawStatus !== 'starting' && parsedRawStatus !== 'generating') return false;
131
+ if (hasNonEmptyModalButtons(adapterStatus?.activeModal ?? adapterStatus?.modal ?? parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
132
+ if (countMessages(adapterStatus?.messages) > 0 || countMessages(parsedStatus?.messages) > 0) return false;
133
+ return !hasAdapterPendingResponse(adapter);
134
+ }
135
+
136
+ function hasCompletedStartingLaunch(adapter: any): boolean {
137
+ const adapterStatus = adapter?.getStatus?.({ allowParse: false }) ?? adapter?.getStatus?.() ?? {};
138
+ const adapterRawStatus = normalizeAgentStatus(adapterStatus?.status);
139
+ if (adapterRawStatus !== 'starting') return false;
140
+ if (hasAdapterPendingResponse(adapter)) return false;
141
+
142
+ const parsedStatus = typeof adapter?.getScriptParsedStatus === 'function'
143
+ ? adapter.getScriptParsedStatus()
144
+ : {};
145
+ const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
146
+ if (parsedRawStatus !== 'idle') return false;
147
+ if (hasNonEmptyModalButtons(adapterStatus?.activeModal ?? adapterStatus?.modal ?? parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
148
+ return hasFinalAssistantMessage(parsedStatus?.messages);
149
+ }
150
+
151
+ function shouldSuppressStaleParsedBusyStatus(adapterStatus: string, parsedStatus: any, adapter: any): boolean {
152
+ const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
153
+ if (!BUSY_AGENT_STATUSES.has(parsedRawStatus)) return false;
154
+ if (adapterStatus !== 'idle') return false;
155
+ if (hasNonEmptyModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
156
+ return !hasAdapterPendingResponse(adapter);
157
+ }
158
+
159
+ function getEffectiveAgentSendStatus(adapter: any): string {
160
+ const adapterStatus = normalizeAgentStatus(adapter?.getStatus?.({ allowParse: false })?.status ?? adapter?.getStatus?.()?.status);
161
+ if (adapterStatus === 'starting' && hasCompletedStartingLaunch(adapter)) return 'idle';
162
+ if (adapterStatus && adapterStatus !== 'idle') return adapterStatus;
163
+ if (adapterStatus !== 'idle') return adapterStatus;
164
+
165
+ if (typeof adapter?.getScriptParsedStatus !== 'function') return adapterStatus;
166
+ try {
167
+ const parsedStatus = adapter.getScriptParsedStatus();
168
+ const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
169
+ if (BUSY_AGENT_STATUSES.has(parsedRawStatus) && !shouldSuppressStaleParsedBusyStatus(adapterStatus, parsedStatus, adapter)) {
170
+ return parsedRawStatus;
171
+ }
172
+ } catch {
173
+ return adapterStatus;
174
+ }
175
+ return adapterStatus;
176
+ }
177
+
178
+ async function waitForZeroMessageStartingLaunch(adapter: any): Promise<boolean> {
179
+ try {
180
+ if (!hasZeroMessageStartingLaunch(adapter)) return false;
181
+ } catch {
182
+ return false;
183
+ }
184
+ await new Promise(resolve => setTimeout(resolve, ZERO_MESSAGE_STARTING_SEND_WAIT_MS));
185
+ try {
186
+ return hasZeroMessageStartingLaunch(adapter);
187
+ } catch {
188
+ return false;
189
+ }
190
+ }
191
+
83
192
  export interface CliTransportFactoryParams {
84
193
  runtimeId: string;
85
194
  providerType: string;
@@ -255,10 +364,14 @@ function detectExplicitProviderSessionId(
255
364
 
256
365
  const subcommands = resume?.sessionIdFromSubcommand;
257
366
  if (Array.isArray(subcommands) && subcommands.length > 0) {
367
+ const hasResumeSubcommand = args.some((arg) => subcommands.includes(arg));
258
368
  const subcommandSessionId = readSubcommandSessionId(args, subcommands);
259
369
  if (subcommandSessionId) {
260
370
  return { providerSessionId: subcommandSessionId, launchMode: 'resume' };
261
371
  }
372
+ if (hasResumeSubcommand) {
373
+ return { launchMode: 'resume' };
374
+ }
262
375
  }
263
376
 
264
377
  return { launchMode: 'manual' };
@@ -292,6 +405,12 @@ export function resolveCliSessionBinding(
292
405
  launchMode: explicit.launchMode,
293
406
  };
294
407
  }
408
+ if (explicit.launchMode === 'resume') {
409
+ return {
410
+ cliArgs: baseArgs,
411
+ launchMode: 'resume',
412
+ };
413
+ }
295
414
  if (explicit.launchMode === 'manual' && hasArg(baseArgs || [], ['--session-id'])) {
296
415
  return {
297
416
  cliArgs: baseArgs,
@@ -435,7 +554,9 @@ export class DaemonCliManager {
435
554
  providerSessionId,
436
555
  attachExisting,
437
556
  );
438
- return new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, extraEnv || {}, transportFactory);
557
+ const adapter = new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, extraEnv || {}, transportFactory);
558
+ if (providerSessionId) adapter.updateRuntimeMeta({ providerSessionId });
559
+ return adapter;
439
560
  }
440
561
 
441
562
  throw new Error(`No CLI provider found for '${cliType}'. Create a provider.js in providers/cli/${cliType}/`);
@@ -457,6 +578,7 @@ export class DaemonCliManager {
457
578
  this.deps.removeAgentTracking(key);
458
579
  sessionRegistry?.unregisterByInstanceKey(key);
459
580
  instanceManager?.removeInstance(key);
581
+ unregisterMeshCoordinator(key);
460
582
  LOG.info('CLI', `🧹 Auto-cleaned ${status.status} CLI: ${cliType}`);
461
583
  this.deps.onStatusChange();
462
584
  }
@@ -783,6 +905,7 @@ export class DaemonCliManager {
783
905
  this.deps.removeAgentTracking(key);
784
906
  this.deps.getSessionRegistry?.()?.unregisterByInstanceKey(key);
785
907
  this.deps.getInstanceManager()?.removeInstance(key);
908
+ unregisterMeshCoordinator(key);
786
909
  LOG.info('CLI', `🛑 Agent stopped: ${adapter.cliType} in ${adapter.workingDir}`);
787
910
  this.deps.onStatusChange();
788
911
  } else {
@@ -792,6 +915,7 @@ export class DaemonCliManager {
792
915
  this.deps.getSessionRegistry?.()?.unregisterByInstanceKey(key);
793
916
  im.removeInstance(key);
794
917
  this.deps.removeAgentTracking(key);
918
+ unregisterMeshCoordinator(key);
795
919
  LOG.warn('CLI', `🧹 Force-removed orphan entry: ${key}`);
796
920
  this.deps.onStatusChange();
797
921
  }
@@ -1073,6 +1197,12 @@ export class DaemonCliManager {
1073
1197
  const { adapter, key } = found;
1074
1198
 
1075
1199
  if (action === 'send_chat') {
1200
+ let currentStatus = getEffectiveAgentSendStatus(adapter);
1201
+ if (currentStatus === 'starting' && await waitForZeroMessageStartingLaunch(adapter)) {
1202
+ currentStatus = 'idle';
1203
+ } else if (currentStatus === 'starting') {
1204
+ currentStatus = getEffectiveAgentSendStatus(adapter);
1205
+ }
1076
1206
  const input = normalizeInputEnvelope(args?.input ? { input: args.input } : args);
1077
1207
  const provider = this.providerLoader.resolve(agentType) || this.providerLoader.getMeta(agentType);
1078
1208
  if (provider?.category === 'acp') {
@@ -1082,8 +1212,20 @@ export class DaemonCliManager {
1082
1212
  }
1083
1213
  const message = input.textFallback;
1084
1214
  if (!message) throw new Error('message required for send_chat');
1085
- await adapter.sendMessage(message);
1086
- return { success: true, status: 'generating' };
1215
+ const forceSend = args?.force === true || args?.forceSend === true;
1216
+ if (forceSend && typeof (adapter as any).forceSendMessage === 'function') {
1217
+ await (adapter as any).forceSendMessage(message);
1218
+ } else if (forceSend) {
1219
+ await adapter.sendMessage(message, { force: true });
1220
+ } else {
1221
+ await adapter.sendMessage(message);
1222
+ }
1223
+ return {
1224
+ success: true,
1225
+ status: BUSY_AGENT_STATUSES.has(currentStatus) ? currentStatus : 'generating',
1226
+ ...(BUSY_AGENT_STATUSES.has(currentStatus) ? { queued: true, queuedReason: 'agent_runtime_busy' } : {}),
1227
+ ...(forceSend ? { forceSent: true, queued: false } : {}),
1228
+ };
1087
1229
  } else if (action === 'clear_history') {
1088
1230
  if (typeof adapter.clearHistory === 'function') adapter.clearHistory();
1089
1231
  return { success: true, cleared: true };
@@ -408,12 +408,19 @@ export class DaemonCommandHandler implements CommandHelpers {
408
408
  'invoke_provider_script',
409
409
  ]);
410
410
 
411
+ // read_chat and get_chat_debug_bundle can serve historical transcript data even
412
+ // when the live session record is gone (stopped/destroyed). Allow the fallback
413
+ // when the provider type is known and any session identity hint is present:
414
+ // an explicit providerSessionId/historySessionId, or the targetSessionId itself
415
+ // (which getHistorySessionId already uses as a fallback history key).
416
+ const isReadOrDebugCmd = cmd === 'read_chat' || cmd === 'get_chat_debug_bundle';
411
417
  const allowsInactiveReadChatFallback =
412
- cmd === 'read_chat'
418
+ isReadOrDebugCmd
413
419
  && !!this._currentRoute.providerType
414
420
  && (
415
421
  (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim().length > 0)
416
422
  || (typeof args?.historySessionId === 'string' && args.historySessionId.trim().length > 0)
423
+ || (typeof args?.targetSessionId === 'string' && args.targetSessionId.trim().length > 0)
417
424
  );
418
425
 
419
426
  if (this._currentRoute.sessionLookupFailed && sessionScopedCommands.has(cmd) && !allowsInactiveReadChatFallback) {
@@ -1,9 +1,6 @@
1
- import { execFileSync } from 'node:child_process'
2
1
  import { createHash } from 'node:crypto'
3
- import { existsSync, readdirSync, realpathSync } from 'node:fs'
4
- import { createRequire } from 'node:module'
5
2
  import * as os from 'node:os'
6
- import { dirname, isAbsolute, join, resolve } from 'node:path'
3
+ import { isAbsolute, join, resolve } from 'node:path'
7
4
  import type { ProviderModule, MeshCoordinatorMcpConfigFormat } from '../providers/contracts.js'
8
5
 
9
6
  export interface MeshCoordinatorMcpServerLaunch {
@@ -55,7 +52,7 @@ export interface ResolveMeshCoordinatorSetupOptions {
55
52
  }
56
53
 
57
54
  const DEFAULT_SERVER_NAME = 'adhdev-mesh'
58
- const DEFAULT_ADHDEV_MCP_COMMAND = 'adhdev-mcp'
55
+ const DEFAULT_ADHDEV_MCP_COMMAND = 'adhdev'
59
56
  const HERMES_CLI_TYPE = 'hermes-cli'
60
57
  const HERMES_MCP_CONFIG_PATH = '~/.hermes/config.yaml'
61
58
 
@@ -67,8 +64,7 @@ function isHermesProvider(provider: ProviderModule | null | undefined, cliType?:
67
64
  function resolveHermesMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupOptions): MeshCoordinatorSetup {
68
65
  const mcpServer = resolveAdhdevMcpServerLaunch({
69
66
  meshId: options.meshId,
70
- nodeExecutable: options.nodeExecutable,
71
- adhdevMcpEntryPath: options.adhdevMcpEntryPath,
67
+ adhdevMcpCommand: options.adhdevMcpCommand,
72
68
  adhdevMcpTransport: options.adhdevMcpTransport,
73
69
  adhdevMcpPort: options.adhdevMcpPort,
74
70
  })
@@ -100,7 +96,7 @@ export function createHermesManualMeshCoordinatorSetup(meshId: string, workspace
100
96
  requiresRestart: true,
101
97
  instructions: 'Hermes CLI does not auto-import repo-local .mcp.json. Add this MCP server to Hermes config under mcp_servers, then start a fresh Hermes session.',
102
98
  template: renderMeshCoordinatorTemplate(
103
- 'mcp_servers:\n {{serverName}}:\n command: {{adhdevMcpCommand}}\n args:\n - --repo-mesh\n - {{meshId}}\n enabled: true\n',
99
+ 'mcp_servers:\n {{serverName}}:\n command: {{adhdevMcpCommand}}\n args:\n - mcp\n - --mode\n - ipc\n - --repo-mesh\n - {{meshId}}\n enabled: true\n',
104
100
  {
105
101
  meshId,
106
102
  workspace,
@@ -141,8 +137,7 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
141
137
  }
142
138
  const mcpServer = resolveAdhdevMcpServerLaunch({
143
139
  meshId,
144
- nodeExecutable: options.nodeExecutable,
145
- adhdevMcpEntryPath: options.adhdevMcpEntryPath,
140
+ adhdevMcpCommand: options.adhdevMcpCommand,
146
141
  adhdevMcpTransport: options.adhdevMcpTransport,
147
142
  adhdevMcpPort: options.adhdevMcpPort,
148
143
  })
@@ -222,25 +217,25 @@ function resolveMcpConfigPath(configPath: string, workspace: string): string {
222
217
 
223
218
  function resolveAdhdevMcpServerLaunch(options: {
224
219
  meshId: string
225
- nodeExecutable?: string
226
- adhdevMcpEntryPath?: string
220
+ adhdevMcpCommand?: string
227
221
  adhdevMcpTransport?: 'local' | 'ipc'
228
222
  adhdevMcpPort?: number
229
223
  }): MeshCoordinatorMcpServerLaunch | null {
230
- const entryPath = resolveAdhdevMcpEntryPath(options.adhdevMcpEntryPath)
231
- if (!entryPath) return null
232
- const nodeExecutable = resolveMcpNodeExecutable(options.nodeExecutable)
233
- if (!nodeExecutable) return null
224
+ const command = resolveAdhdevCommand(options.adhdevMcpCommand)
234
225
  const transport = resolveMcpTransport(options.adhdevMcpTransport)
235
- const args = [entryPath, '--mode', transport, '--repo-mesh', options.meshId]
226
+ const args = ['mcp', '--mode', transport, '--repo-mesh', options.meshId]
236
227
  const port = resolveMcpPort(options.adhdevMcpPort)
237
228
  if (port !== undefined) args.push('--port', String(port))
238
229
  return {
239
- command: nodeExecutable,
230
+ command,
240
231
  args,
241
232
  }
242
233
  }
243
234
 
235
+ function resolveAdhdevCommand(explicitCommand?: string): string {
236
+ return explicitCommand?.trim() || process.env.ADHDEV_COORDINATOR_MCP_COMMAND?.trim() || DEFAULT_ADHDEV_MCP_COMMAND
237
+ }
238
+
244
239
  function resolveMcpTransport(explicitTransport?: 'local' | 'ipc'): 'local' | 'ipc' {
245
240
  if (explicitTransport === 'local' || explicitTransport === 'ipc') return explicitTransport
246
241
  const envTransport = process.env.ADHDEV_COORDINATOR_MCP_TRANSPORT?.trim()
@@ -254,128 +249,3 @@ function resolveMcpPort(explicitPort?: number): number | undefined {
254
249
  const parsed = Number(raw)
255
250
  return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
256
251
  }
257
-
258
- function resolveMcpNodeExecutable(explicitExecutable?: string): string | null {
259
- const explicit = explicitExecutable?.trim()
260
- if (explicit) return explicit
261
-
262
- const candidates: string[] = []
263
- const addCandidate = (candidate?: string | null) => {
264
- const trimmed = candidate?.trim()
265
- if (!trimmed) return
266
- const normalized = normalizeExistingPath(trimmed) || trimmed
267
- if (!candidates.includes(normalized)) candidates.push(normalized)
268
- }
269
-
270
- addCandidate(process.env.ADHDEV_MCP_NODE_EXECUTABLE)
271
- addCandidate(process.env.ADHDEV_NODE_EXECUTABLE)
272
- addCandidate(process.env.npm_node_execpath)
273
- addNodeCandidatesFromPath(process.env.PATH, addCandidate)
274
- addNodeCandidatesFromNvm(os.homedir(), addCandidate)
275
- addCandidate('/opt/homebrew/bin/node')
276
- addCandidate('/usr/local/bin/node')
277
- addCandidate('/usr/bin/node')
278
- addCandidate(process.execPath)
279
-
280
- for (const candidate of candidates) {
281
- if (nodeRuntimeSupportsWebSocket(candidate)) return candidate
282
- }
283
- return null
284
- }
285
-
286
- function addNodeCandidatesFromPath(pathValue: string | undefined, addCandidate: (candidate?: string | null) => void) {
287
- for (const entry of (pathValue || '').split(':')) {
288
- const dir = entry.trim()
289
- if (!dir) continue
290
- addCandidate(join(dir, 'node'))
291
- }
292
- }
293
-
294
- function addNodeCandidatesFromNvm(homeDir: string, addCandidate: (candidate?: string | null) => void) {
295
- const versionsDir = join(homeDir, '.nvm', 'versions', 'node')
296
- try {
297
- const versionDirs = readdirSync(versionsDir, { withFileTypes: true })
298
- .filter((entry) => entry.isDirectory())
299
- .map((entry) => entry.name)
300
- .sort(compareNodeVersionNamesDescending)
301
- for (const versionDir of versionDirs) {
302
- addCandidate(join(versionsDir, versionDir, 'bin', 'node'))
303
- }
304
- } catch {
305
- // nvm is optional; PATH and process.execPath candidates still cover normal installs.
306
- }
307
- }
308
-
309
- function compareNodeVersionNamesDescending(a: string, b: string): number {
310
- const parse = (value: string) => value.replace(/^v/, '').split('.').map((part) => Number.parseInt(part, 10) || 0)
311
- const left = parse(a)
312
- const right = parse(b)
313
- for (let i = 0; i < Math.max(left.length, right.length); i++) {
314
- const diff = (right[i] || 0) - (left[i] || 0)
315
- if (diff !== 0) return diff
316
- }
317
- return b.localeCompare(a)
318
- }
319
-
320
- function nodeRuntimeSupportsWebSocket(nodeExecutable: string): boolean {
321
- try {
322
- execFileSync(nodeExecutable, ['-e', "process.exit(typeof WebSocket === 'function' ? 0 : 42)"], {
323
- stdio: 'ignore',
324
- timeout: 3000,
325
- })
326
- return true
327
- } catch {
328
- return false
329
- }
330
- }
331
-
332
- function resolveAdhdevMcpEntryPath(explicitPath?: string): string | null {
333
- const explicit = explicitPath?.trim()
334
- if (explicit) return normalizeExistingPath(explicit) || explicit
335
-
336
- const envPath = process.env.ADHDEV_MCP_SERVER_PATH?.trim()
337
- if (envPath) return normalizeExistingPath(envPath) || envPath
338
-
339
- const candidates: string[] = []
340
- const addCandidate = (candidate: string) => {
341
- if (!candidates.includes(candidate)) candidates.push(candidate)
342
- }
343
- const addPackagedCandidates = (baseFile?: string) => {
344
- if (!baseFile) return
345
- const realBase = normalizeExistingPath(baseFile) || baseFile
346
- const dir = dirname(realBase)
347
- addCandidate(resolve(dir, '../vendor/mcp-server/index.js'))
348
- addCandidate(resolve(dir, '../../vendor/mcp-server/index.js'))
349
- addCandidate(resolve(dir, '../../../vendor/mcp-server/index.js'))
350
- // Source checkout/dev mode does not vendor the MCP server into daemon-standalone.
351
- // Resolve the sibling workspace build directly so Repo Mesh auto-import still
352
- // writes an absolute Node entrypoint instead of falling back to a PATH bin shim.
353
- addCandidate(resolve(dir, '../../mcp-server/dist/index.js'))
354
- addCandidate(resolve(dir, '../../../mcp-server/dist/index.js'))
355
- }
356
-
357
- addPackagedCandidates(process.argv[1])
358
-
359
- for (const candidate of candidates) {
360
- const normalized = normalizeExistingPath(candidate)
361
- if (normalized) return normalized
362
- }
363
-
364
- try {
365
- const requireBase = process.argv[1] ? (normalizeExistingPath(process.argv[1]) || process.argv[1]) : join(process.cwd(), 'adhdev-daemon.js')
366
- const req = createRequire(requireBase)
367
- const resolvedModule = req.resolve('@adhdev/mcp-server')
368
- return normalizeExistingPath(resolvedModule) || resolvedModule
369
- } catch {
370
- return null
371
- }
372
- }
373
-
374
- function normalizeExistingPath(filePath: string): string | null {
375
- try {
376
- if (!existsSync(filePath)) return null
377
- return realpathSync.native(filePath)
378
- } catch {
379
- return null
380
- }
381
- }