@adhdev/daemon-core 0.9.82-rc.7 → 0.9.82-rc.71

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 (74) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +2 -0
  2. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  3. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  4. package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
  5. package/dist/commands/router.d.ts +24 -0
  6. package/dist/config/mesh-config.d.ts +66 -1
  7. package/dist/git/git-commands.d.ts +1 -0
  8. package/dist/git/git-status.d.ts +5 -0
  9. package/dist/git/git-types.d.ts +10 -0
  10. package/dist/index.d.ts +13 -6
  11. package/dist/index.js +4888 -1149
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +4852 -1135
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/installer.d.ts +1 -4
  16. package/dist/launch.d.ts +1 -1
  17. package/dist/logging/async-batch-writer.d.ts +10 -0
  18. package/dist/mesh/beads-db.d.ts +18 -0
  19. package/dist/mesh/mesh-active-work.d.ts +48 -0
  20. package/dist/mesh/mesh-events.d.ts +28 -5
  21. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  22. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  23. package/dist/mesh/mesh-ledger.d.ts +38 -1
  24. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  25. package/dist/mesh/refine-config.d.ts +119 -0
  26. package/dist/providers/chat-message-normalization.d.ts +1 -0
  27. package/dist/providers/cli-provider-instance.d.ts +1 -0
  28. package/dist/repo-mesh-types.d.ts +160 -0
  29. package/dist/status/reporter.d.ts +2 -0
  30. package/package.json +3 -1
  31. package/src/boot/daemon-lifecycle.ts +4 -0
  32. package/src/cli-adapters/provider-cli-adapter.ts +91 -3
  33. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  34. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  35. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  36. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  37. package/src/cli-adapters/provider-cli-shared.ts +20 -10
  38. package/src/commands/chat-commands.ts +242 -7
  39. package/src/commands/cli-manager.ts +19 -0
  40. package/src/commands/handler.ts +8 -1
  41. package/src/commands/mesh-coordinator.ts +13 -143
  42. package/src/commands/router.ts +2518 -408
  43. package/src/config/chat-history.ts +9 -7
  44. package/src/config/mesh-config.ts +244 -1
  45. package/src/daemon/dev-cli-debug.ts +10 -1
  46. package/src/detection/ide-detector.ts +26 -16
  47. package/src/git/git-commands.ts +3 -3
  48. package/src/git/git-status.ts +97 -6
  49. package/src/git/git-summary.ts +3 -0
  50. package/src/git/git-types.ts +11 -0
  51. package/src/index.ts +39 -5
  52. package/src/installer.d.ts +1 -1
  53. package/src/installer.ts +8 -6
  54. package/src/launch.d.ts +1 -1
  55. package/src/launch.ts +37 -28
  56. package/src/logging/async-batch-writer.ts +55 -0
  57. package/src/logging/logger.ts +2 -1
  58. package/src/mesh/beads-db.ts +176 -0
  59. package/src/mesh/coordinator-prompt.ts +5 -2
  60. package/src/mesh/mesh-active-work.ts +205 -0
  61. package/src/mesh/mesh-events.ts +291 -38
  62. package/src/mesh/mesh-fast-forward.ts +430 -0
  63. package/src/mesh/mesh-host-ownership.ts +73 -0
  64. package/src/mesh/mesh-ledger.ts +138 -1
  65. package/src/mesh/mesh-work-queue.ts +199 -137
  66. package/src/mesh/refine-config.ts +306 -0
  67. package/src/providers/chat-message-normalization.ts +3 -1
  68. package/src/providers/cli-provider-instance.ts +68 -1
  69. package/src/providers/ide-provider-instance.ts +17 -3
  70. package/src/providers/provider-loader.ts +10 -4
  71. package/src/providers/version-archive.ts +38 -20
  72. package/src/repo-mesh-types.ts +174 -0
  73. package/src/status/reporter.ts +15 -0
  74. package/src/system/host-memory.ts +29 -12
@@ -24,6 +24,7 @@ import { filterUserFacingChatMessages, normalizeChatMessages } from '../provider
24
24
  const RECENT_SEND_WINDOW_MS = 1200;
25
25
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
26
26
  const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
27
+ const CODEX_NATIVE_HISTORY_FRESH_MS = 5 * 60_000;
27
28
  const recentSendByTarget = new Map<string, number>();
28
29
 
29
30
  interface ApprovalSelectableInstance extends ProviderInstance {
@@ -221,6 +222,114 @@ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[]
221
222
  return normalizeChatMessages(messages);
222
223
  }
223
224
 
225
+ function getMessageNewestReceivedAt(messages: Array<{ receivedAt?: unknown; timestamp?: unknown }>): number {
226
+ let newest = 0;
227
+ for (const message of messages) {
228
+ const receivedAt = Number(message?.receivedAt ?? message?.timestamp ?? 0);
229
+ if (Number.isFinite(receivedAt) && receivedAt > newest) newest = receivedAt;
230
+ }
231
+ return newest;
232
+ }
233
+
234
+ function buildCliMessageSourceProvenance(args: {
235
+ selected: 'native-history' | 'pty-parser';
236
+ provider: string;
237
+ nativeHandle?: string;
238
+ fallbackReason?: string;
239
+ nativeSource?: string;
240
+ sourcePath?: string;
241
+ sourceMtimeMs?: number;
242
+ nativeMessages?: ChatMessage[];
243
+ ptyMessages?: ChatMessage[];
244
+ returnedMessages?: ChatMessage[];
245
+ safeMapping?: boolean;
246
+ freshEnough?: boolean;
247
+ ptyStatusApprovalOnly?: boolean;
248
+ }): Record<string, unknown> {
249
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
250
+ const sourceMtimeAgeMs = sourceMtimeMs > 0 ? Math.max(0, Date.now() - sourceMtimeMs) : undefined;
251
+ const nativeMessages = args.nativeMessages || [];
252
+ const ptyMessages = args.ptyMessages || [];
253
+ const returnedMessages = args.returnedMessages || [];
254
+ return {
255
+ selected: args.selected,
256
+ provider: args.provider,
257
+ ...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
258
+ ...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
259
+ ...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
260
+ ...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
261
+ ptyStatusApprovalOnly: args.ptyStatusApprovalOnly === true,
262
+ staleness: {
263
+ sourceMtimeMs: sourceMtimeMs || undefined,
264
+ sourceMtimeAgeMs,
265
+ nativeNewestMessageAt: getMessageNewestReceivedAt(nativeMessages),
266
+ ptyNewestMessageAt: getMessageNewestReceivedAt(ptyMessages),
267
+ freshEnough: args.freshEnough === true,
268
+ },
269
+ coverage: {
270
+ nativeMessageCount: nativeMessages.length,
271
+ ptyMessageCount: ptyMessages.length,
272
+ returnedMessageCount: returnedMessages.length,
273
+ safeMapping: args.safeMapping === true,
274
+ },
275
+ };
276
+ }
277
+
278
+ function buildNativeHistoryFallbackReason(args: {
279
+ providerType: string;
280
+ nativeSource?: string;
281
+ nativeMessageCount: number;
282
+ safeMapping: boolean;
283
+ freshEnough: boolean;
284
+ }): string {
285
+ if (args.providerType !== 'codex-cli') return 'provider_not_codex_cli';
286
+ if (args.nativeSource === 'native-unavailable') return 'native_history_unavailable';
287
+ if (args.nativeSource && args.nativeSource !== 'provider-native') return `native_history_source_${args.nativeSource}`;
288
+ if (args.nativeMessageCount <= 0) return 'native_history_empty';
289
+ if (!args.safeMapping) return 'native_history_not_safely_mapped';
290
+ if (!args.freshEnough) return 'native_history_stale';
291
+ return 'native_history_not_selected';
292
+ }
293
+
294
+ function isCodexCliProvider(providerType: string): boolean {
295
+ return providerType === 'codex-cli';
296
+ }
297
+
298
+ function hasSafeNativeHistoryMapping(args: {
299
+ historySessionId?: string;
300
+ providerSessionId?: string;
301
+ workspace?: string;
302
+ nativeMessages: ChatMessage[];
303
+ }): boolean {
304
+ const explicitSessionId = String(args.historySessionId || args.providerSessionId || '').trim();
305
+ if (explicitSessionId) {
306
+ const messageSessionIds = args.nativeMessages
307
+ .map((message: any) => typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '')
308
+ .filter(Boolean);
309
+ if (messageSessionIds.length === 0) return true;
310
+ return messageSessionIds.some((id) => id === explicitSessionId);
311
+ }
312
+ const workspace = String(args.workspace || '').trim();
313
+ if (!workspace) return false;
314
+ return args.nativeMessages.some((message: any) => String(message?.workspace || '').trim() === workspace);
315
+ }
316
+
317
+ function isNativeHistoryFreshEnough(args: {
318
+ sourceMtimeMs?: number;
319
+ nativeMessages: ChatMessage[];
320
+ ptyMessages: ChatMessage[];
321
+ }): boolean {
322
+ const nativeNewest = getMessageNewestReceivedAt(args.nativeMessages);
323
+ const ptyNewest = getMessageNewestReceivedAt(args.ptyMessages);
324
+ if (nativeNewest > 0 && nativeNewest >= ptyNewest) return true;
325
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
326
+ if (sourceMtimeMs > 0 && Date.now() - sourceMtimeMs <= CODEX_NATIVE_HISTORY_FRESH_MS) return true;
327
+ return ptyNewest === 0 && nativeNewest > 0;
328
+ }
329
+
330
+ function shouldPreserveReadChatPayloadField(key: string): boolean {
331
+ return key === 'messageSource' || key === 'transcriptProvenance';
332
+ }
224
333
 
225
334
  function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
226
335
  const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
@@ -356,6 +465,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
356
465
  return {
357
466
  success: true,
358
467
  ...validatedPayload,
468
+ ...Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key))),
359
469
  messages: sync.messages,
360
470
  totalMessages: sync.totalMessages,
361
471
  ...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
@@ -584,6 +694,8 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
584
694
  adapterStatus: debugReadChat.adapterStatus,
585
695
  parsedStatus: debugReadChat.parsedStatus,
586
696
  returnedStatus: debugReadChat.returnedStatus,
697
+ selectedMessageSource: debugReadChat.selectedMessageSource,
698
+ messageSource: debugReadChat.messageSource,
587
699
  parsedMsgCount: debugReadChat.parsedMsgCount,
588
700
  returnedMsgCount: debugReadChat.returnedMsgCount,
589
701
  shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
@@ -646,6 +758,8 @@ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Pr
646
758
  providerSessionId: readResult.providerSessionId,
647
759
  transcriptAuthority: readResult.transcriptAuthority,
648
760
  coverage: readResult.coverage,
761
+ messageSource: readResult.messageSource,
762
+ transcriptProvenance: readResult.transcriptProvenance,
649
763
  activeModal: readResult.activeModal,
650
764
  messagesTail: Array.isArray(readResult.messages) ? readResult.messages.slice(-20) : [],
651
765
  debugReadChat: readResult.debugReadChat,
@@ -882,25 +996,146 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
882
996
  && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
883
997
  ? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
884
998
  : parsedMessages;
999
+ const providerType = provider?.type || adapter.cliType;
1000
+ let selectedMessages = returnedMessages;
1001
+ let selectedTitle = title;
1002
+ let selectedProviderSessionId = providerSessionId;
1003
+ let selectedTranscriptAuthority = transcriptAuthority;
1004
+ let selectedCoverage = coverage;
1005
+ let messageSource = buildCliMessageSourceProvenance({
1006
+ selected: 'pty-parser',
1007
+ provider: adapter.cliType,
1008
+ fallbackReason: isCodexCliProvider(providerType) ? 'native_history_not_checked' : 'provider_not_codex_cli',
1009
+ ptyMessages: returnedMessages,
1010
+ returnedMessages,
1011
+ ptyStatusApprovalOnly: false,
1012
+ });
1013
+
1014
+ if (isCodexCliProvider(providerType)) {
1015
+ const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
1016
+ const workspace = typeof args?.workspace === 'string'
1017
+ ? args.workspace
1018
+ : typeof (h.currentSession as any)?.workspace === 'string'
1019
+ ? (h.currentSession as any).workspace
1020
+ : typeof adapter.workingDir === 'string'
1021
+ ? adapter.workingDir
1022
+ : undefined;
1023
+ const nativeHistoryLimit = Math.max(
1024
+ normalizeReadChatTailLimit(args) || 0,
1025
+ returnedMessages.length,
1026
+ 200,
1027
+ );
1028
+ let nativeHistory: ReturnType<typeof readProviderChatHistory> | null = null;
1029
+ try {
1030
+ nativeHistory = readProviderChatHistory(agentStr, {
1031
+ canonicalHistory: provider?.canonicalHistory,
1032
+ historySessionId,
1033
+ workspace,
1034
+ offset: 0,
1035
+ limit: nativeHistoryLimit,
1036
+ excludeRecentCount: 0,
1037
+ historyBehavior: provider?.historyBehavior,
1038
+ scripts: provider?.scripts as any,
1039
+ });
1040
+ } catch (error: any) {
1041
+ const fallbackReason = `native_history_error:${error?.message || String(error)}`;
1042
+ messageSource = buildCliMessageSourceProvenance({
1043
+ selected: 'pty-parser',
1044
+ provider: adapter.cliType,
1045
+ fallbackReason,
1046
+ ptyMessages: returnedMessages,
1047
+ returnedMessages,
1048
+ ptyStatusApprovalOnly: false,
1049
+ });
1050
+ nativeHistory = null;
1051
+ }
1052
+
1053
+ if (nativeHistory) {
1054
+ const nativeMessages = Array.isArray((nativeHistory as any).messages)
1055
+ ? normalizeChatMessages((nativeHistory as any).messages as ChatMessage[])
1056
+ : [];
1057
+ const historyProviderSessionId = typeof (nativeHistory as any)?.providerSessionId === 'string'
1058
+ ? (nativeHistory as any).providerSessionId
1059
+ : historySessionId;
1060
+ const safeMapping = hasSafeNativeHistoryMapping({
1061
+ historySessionId,
1062
+ providerSessionId,
1063
+ workspace,
1064
+ nativeMessages,
1065
+ });
1066
+ const freshEnough = isNativeHistoryFreshEnough({
1067
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1068
+ nativeMessages,
1069
+ ptyMessages: returnedMessages,
1070
+ });
1071
+ if ((nativeHistory as any).source === 'provider-native' && nativeMessages.length > 0 && safeMapping && freshEnough) {
1072
+ selectedMessages = finalizeStreamingMessagesWhenIdle(nativeMessages, returnedStatus);
1073
+ selectedProviderSessionId = historyProviderSessionId || providerSessionId;
1074
+ selectedTranscriptAuthority = 'provider';
1075
+ selectedCoverage = (nativeHistory as any).hasMore ? 'tail' : 'full';
1076
+ messageSource = buildCliMessageSourceProvenance({
1077
+ selected: 'native-history',
1078
+ provider: adapter.cliType,
1079
+ nativeHandle: selectedProviderSessionId || historySessionId,
1080
+ nativeSource: (nativeHistory as any).source,
1081
+ sourcePath: (nativeHistory as any).sourcePath,
1082
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1083
+ nativeMessages,
1084
+ ptyMessages: returnedMessages,
1085
+ returnedMessages: selectedMessages,
1086
+ safeMapping,
1087
+ freshEnough,
1088
+ ptyStatusApprovalOnly: true,
1089
+ });
1090
+ } else {
1091
+ const fallbackReason = buildNativeHistoryFallbackReason({
1092
+ providerType,
1093
+ nativeSource: (nativeHistory as any).source,
1094
+ nativeMessageCount: nativeMessages.length,
1095
+ safeMapping,
1096
+ freshEnough,
1097
+ });
1098
+ messageSource = buildCliMessageSourceProvenance({
1099
+ selected: 'pty-parser',
1100
+ provider: adapter.cliType,
1101
+ nativeHandle: historyProviderSessionId || historySessionId,
1102
+ fallbackReason,
1103
+ nativeSource: (nativeHistory as any).source,
1104
+ sourcePath: (nativeHistory as any).sourcePath,
1105
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1106
+ nativeMessages,
1107
+ ptyMessages: returnedMessages,
1108
+ returnedMessages,
1109
+ safeMapping,
1110
+ freshEnough,
1111
+ ptyStatusApprovalOnly: false,
1112
+ });
1113
+ }
1114
+ }
1115
+ }
885
1116
  LOG.debug('Command', `[read_chat] cli-like parsed provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord.status || '')} parsedMsgCount=${parsedRecord.messages.length} returnedMsgCount=${returnedMessages.length}`);
886
1117
  return buildReadChatCommandResult({
887
- messages: returnedMessages,
1118
+ messages: selectedMessages,
888
1119
  status: returnedStatus,
889
1120
  activeModal,
1121
+ messageSource,
1122
+ transcriptProvenance: messageSource,
890
1123
  debugReadChat: {
891
1124
  provider: adapter.cliType,
892
1125
  targetSessionId: String(args?.targetSessionId || ''),
893
1126
  adapterStatus: String(adapterStatus.status || ''),
894
1127
  parsedStatus: String(parsedRecord.status || ''),
895
1128
  returnedStatus: String(returnedStatus || ''),
896
- shouldPreferAdapterMessages: false,
1129
+ selectedMessageSource: (messageSource as any).selected,
1130
+ messageSource,
1131
+ shouldPreferAdapterMessages: (messageSource as any).selected !== 'native-history',
897
1132
  parsedMsgCount: parsedRecord.messages.length,
898
- returnedMsgCount: returnedMessages.length,
1133
+ returnedMsgCount: selectedMessages.length,
899
1134
  },
900
- ...(title ? { title } : {}),
901
- ...(providerSessionId ? { providerSessionId } : {}),
902
- ...(transcriptAuthority ? { transcriptAuthority } : {}),
903
- ...(coverage ? { coverage } : {}),
1135
+ ...(selectedTitle ? { title: selectedTitle } : {}),
1136
+ ...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
1137
+ ...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
1138
+ ...(selectedCoverage ? { coverage: selectedCoverage } : {}),
904
1139
  }, args);
905
1140
  }
906
1141
  const historyLimit = normalizeReadChatTailLimit(args);
@@ -80,6 +80,12 @@ export interface CliManagerDeps {
80
80
 
81
81
  type CommandResult = { success: boolean;[key: string]: unknown };
82
82
 
83
+ const BUSY_AGENT_STATUSES = new Set(['generating', 'running', 'streaming', 'starting', 'busy', 'waiting', 'waiting_approval', 'long_generating']);
84
+
85
+ function normalizeAgentStatus(value: unknown): string {
86
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
87
+ }
88
+
83
89
  export interface CliTransportFactoryParams {
84
90
  runtimeId: string;
85
91
  providerType: string;
@@ -1073,6 +1079,19 @@ export class DaemonCliManager {
1073
1079
  const { adapter, key } = found;
1074
1080
 
1075
1081
  if (action === 'send_chat') {
1082
+ const currentStatus = normalizeAgentStatus(adapter.getStatus?.()?.status);
1083
+ if (BUSY_AGENT_STATUSES.has(currentStatus)) {
1084
+ return {
1085
+ success: false,
1086
+ code: 'agent_runtime_busy',
1087
+ reason: 'agent_runtime_busy',
1088
+ retryable: true,
1089
+ retryRecommended: true,
1090
+ status: currentStatus,
1091
+ targetSessionId: args?.targetSessionId,
1092
+ error: `CLI agent '${agentType}' is currently ${currentStatus}; retry after the current turn finishes.`,
1093
+ };
1094
+ }
1076
1095
  const input = normalizeInputEnvelope(args?.input ? { input: args.input } : args);
1077
1096
  const provider = this.providerLoader.resolve(agentType) || this.providerLoader.getMeta(agentType);
1078
1097
  if (provider?.category === 'acp') {
@@ -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
- }