@adhdev/daemon-core 0.9.82-rc.17 → 0.9.82-rc.170

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 (221) hide show
  1. package/dist/boot/process-hardening.d.ts +50 -0
  2. package/dist/chat/source-machine.d.ts +166 -0
  3. package/dist/chat/source-resolver.d.ts +104 -0
  4. package/dist/chat/subscription-updates.d.ts +1 -0
  5. package/dist/cli-adapter-types.d.ts +22 -2
  6. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  7. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  8. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  11. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  12. package/dist/commands/handler.d.ts +110 -0
  13. package/dist/commands/mesh-coordinator.d.ts +72 -1
  14. package/dist/commands/router.d.ts +22 -0
  15. package/dist/config/chat-history.d.ts +7 -0
  16. package/dist/config/config.d.ts +5 -0
  17. package/dist/config/mesh-config.d.ts +71 -1
  18. package/dist/git/git-commands.d.ts +5 -1
  19. package/dist/index.d.ts +35 -6
  20. package/dist/index.js +18621 -3760
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +18525 -3732
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/installer.d.ts +1 -4
  25. package/dist/ipc/local-ipc-server.d.ts +91 -0
  26. package/dist/launch.d.ts +1 -1
  27. package/dist/logging/async-batch-writer.d.ts +10 -0
  28. package/dist/mesh/beads-db.d.ts +72 -0
  29. package/dist/mesh/contracts.d.ts +164 -0
  30. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  31. package/dist/mesh/coordinator-registry.d.ts +59 -0
  32. package/dist/mesh/mesh-active-work.d.ts +90 -0
  33. package/dist/mesh/mesh-events.d.ts +96 -5
  34. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  35. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  36. package/dist/mesh/mesh-ledger.d.ts +58 -1
  37. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  38. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  39. package/dist/mesh/preview-freshness.d.ts +18 -0
  40. package/dist/mesh/refine-config.d.ts +193 -0
  41. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  42. package/dist/providers/approval-utils.d.ts +13 -0
  43. package/dist/providers/cli-provider-instance.d.ts +9 -3
  44. package/dist/providers/contracts.d.ts +98 -5
  45. package/dist/providers/external-sources.d.ts +71 -0
  46. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  47. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  48. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  49. package/dist/providers/native-history/dispatcher.d.ts +24 -0
  50. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  51. package/dist/providers/native-history/index.d.ts +13 -0
  52. package/dist/providers/provider-instance.d.ts +3 -0
  53. package/dist/providers/provider-loader.d.ts +26 -4
  54. package/dist/providers/provider-trust.d.ts +31 -0
  55. package/dist/providers/read-chat-contract.d.ts +29 -0
  56. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  57. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  58. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  59. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  60. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  61. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  62. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  63. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  64. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  65. package/dist/providers/sdk/v1/index.d.ts +30 -0
  66. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  67. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  68. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  69. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  70. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  71. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  72. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  73. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  74. package/dist/providers/spec/adapter.d.ts +56 -0
  75. package/dist/providers/spec/cli-adapter.d.ts +95 -0
  76. package/dist/providers/spec/driver.d.ts +149 -0
  77. package/dist/providers/spec/evaluator.d.ts +47 -0
  78. package/dist/providers/spec/loader.d.ts +14 -0
  79. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  80. package/dist/providers/spec/route.d.ts +4 -0
  81. package/dist/providers/spec/schema.gen.d.ts +511 -0
  82. package/dist/providers/spec/types.d.ts +212 -0
  83. package/dist/providers/transcript-v2.d.ts +176 -0
  84. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  85. package/dist/repo-mesh-types.d.ts +100 -1
  86. package/dist/sessions/registry.d.ts +3 -0
  87. package/dist/shared-types.d.ts +42 -1
  88. package/dist/status/reporter.d.ts +2 -0
  89. package/dist/status/snapshot.d.ts +1 -0
  90. package/dist/types.d.ts +5 -0
  91. package/package.json +7 -2
  92. package/src/agent-stream/poller.ts +2 -3
  93. package/src/boot/daemon-lifecycle.ts +17 -10
  94. package/src/boot/process-hardening.ts +89 -0
  95. package/src/chat/source-machine.ts +534 -0
  96. package/src/chat/source-resolver.ts +0 -0
  97. package/src/chat/subscription-updates.ts +14 -1
  98. package/src/cli-adapter-types.d.ts +1 -0
  99. package/src/cli-adapter-types.ts +21 -2
  100. package/src/cli-adapters/cli-script-runner.ts +421 -0
  101. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  102. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  103. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  104. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  105. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  106. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  107. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  108. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  109. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  110. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  111. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  112. package/src/commands/chat-commands.ts +1597 -60
  113. package/src/commands/cli-manager.ts +214 -3
  114. package/src/commands/handler.ts +809 -2
  115. package/src/commands/mesh-coordinator.ts +231 -127
  116. package/src/commands/router.ts +3559 -502
  117. package/src/config/chat-history.ts +87 -24
  118. package/src/config/config.ts +12 -0
  119. package/src/config/mesh-config.ts +264 -2
  120. package/src/config/recent-activity.ts +8 -2
  121. package/src/daemon/dev-cli-debug.ts +10 -1
  122. package/src/detection/ide-detector.ts +26 -16
  123. package/src/git/git-commands.ts +17 -5
  124. package/src/git/git-worktree.ts +8 -1
  125. package/src/index.ts +113 -5
  126. package/src/installer.d.ts +1 -1
  127. package/src/installer.ts +8 -6
  128. package/src/ipc/local-ipc-server.ts +278 -0
  129. package/src/launch.d.ts +1 -1
  130. package/src/launch.ts +37 -28
  131. package/src/logging/async-batch-writer.ts +55 -0
  132. package/src/logging/logger.ts +2 -1
  133. package/src/mesh/beads-db.ts +479 -0
  134. package/src/mesh/contracts.ts +329 -0
  135. package/src/mesh/coordinator-prompt.ts +204 -30
  136. package/src/mesh/coordinator-registry.ts +121 -0
  137. package/src/mesh/mesh-active-work.ts +437 -0
  138. package/src/mesh/mesh-events.ts +879 -61
  139. package/src/mesh/mesh-fast-forward.ts +430 -0
  140. package/src/mesh/mesh-host-ownership.ts +73 -0
  141. package/src/mesh/mesh-ledger.ts +457 -104
  142. package/src/mesh/mesh-refine-status.ts +144 -0
  143. package/src/mesh/mesh-work-queue.ts +216 -158
  144. package/src/mesh/preview-freshness.ts +118 -0
  145. package/src/mesh/refine-config.ts +366 -0
  146. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  147. package/src/providers/approval-utils.d.ts +4 -0
  148. package/src/providers/approval-utils.ts +47 -5
  149. package/src/providers/chat-message-normalization.ts +4 -11
  150. package/src/providers/cli-provider-instance.ts +452 -54
  151. package/src/providers/contracts.ts +105 -5
  152. package/src/providers/external-sources.ts +218 -0
  153. package/src/providers/ide-provider-instance.ts +19 -5
  154. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  155. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  156. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  157. package/src/providers/native-history/dispatcher.ts +227 -0
  158. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  159. package/src/providers/native-history/index.ts +30 -0
  160. package/src/providers/provider-instance.ts +3 -0
  161. package/src/providers/provider-loader.ts +585 -50
  162. package/src/providers/provider-schema.ts +31 -13
  163. package/src/providers/provider-trust.ts +114 -0
  164. package/src/providers/read-chat-contract.ts +76 -16
  165. package/src/providers/sdk/README.md +49 -0
  166. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  167. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  168. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  169. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  170. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  171. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  172. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  173. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  174. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  175. package/src/providers/sdk/v1/index.ts +152 -0
  176. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  177. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  178. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  179. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +458 -0
  180. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  181. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  182. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  183. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  184. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  185. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  186. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  187. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  188. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  189. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  190. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  191. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  192. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  193. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  194. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  195. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  196. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  197. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  198. package/src/providers/sdk/v1/validators/index.ts +19 -0
  199. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  200. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  201. package/src/providers/spec/adapter.ts +168 -0
  202. package/src/providers/spec/cli-adapter.ts +430 -0
  203. package/src/providers/spec/driver.ts +502 -0
  204. package/src/providers/spec/evaluator.ts +298 -0
  205. package/src/providers/spec/loader.ts +130 -0
  206. package/src/providers/spec/native-history-executor.ts +623 -0
  207. package/src/providers/spec/route.ts +51 -0
  208. package/src/providers/spec/schema.gen.ts +511 -0
  209. package/src/providers/spec/schema.json +211 -0
  210. package/src/providers/spec/types.ts +231 -0
  211. package/src/providers/transcript-v2.ts +567 -0
  212. package/src/providers/types/interactive-prompt.ts +297 -0
  213. package/src/providers/version-archive.ts +38 -20
  214. package/src/repo-mesh-types.ts +110 -1
  215. package/src/sessions/registry.ts +3 -0
  216. package/src/shared-types.ts +45 -1
  217. package/src/status/builders.ts +24 -6
  218. package/src/status/reporter.ts +15 -0
  219. package/src/status/snapshot.ts +84 -25
  220. package/src/system/host-memory.ts +29 -12
  221. package/src/types.ts +5 -0
@@ -1345,7 +1345,15 @@ function rewriteCanonicalSavedHistory(agentType: string, historySessionId: strin
1345
1345
 
1346
1346
  export type ProviderNativeHistoryScripts = Record<string, ((input: any) => any) | undefined>;
1347
1347
 
1348
- type ProviderNativeHistoryReadResult = { records: HistoryMessage[]; sourcePath: string; sourceMtimeMs: number };
1348
+ type ProviderNativeHistoryReadResult = {
1349
+ records: HistoryMessage[];
1350
+ sourcePath: string;
1351
+ sourceMtimeMs: number;
1352
+ providerSessionId?: string;
1353
+ nativeHistoryCoverage?: string;
1354
+ partialReason?: string;
1355
+ unavailableReason?: string;
1356
+ };
1349
1357
 
1350
1358
  function getNativeHistoryScriptName(canonicalHistory: ProviderCanonicalHistoryConfig | undefined, key: 'readSession' | 'listSessions'): string {
1351
1359
  const configured = canonicalHistory?.scripts?.[key];
@@ -1367,19 +1375,42 @@ function normalizeProviderNativeHistoryRecords(agentType: string, historySession
1367
1375
  if (!Array.isArray(records)) return [];
1368
1376
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1369
1377
  return records
1370
- .map((record: any) => sanitizeHistoryMessage(agentType, {
1371
- ts: typeof record?.ts === 'string' ? record.ts : new Date(Number(record?.receivedAt) || Date.now()).toISOString(),
1372
- receivedAt: Number(record?.receivedAt) || Date.parse(record?.ts || '') || Date.now(),
1373
- role: record?.role,
1374
- content: String(record?.content || ''),
1375
- kind: record?.kind || (record?.role === 'system' ? 'session_start' : 'standard'),
1376
- senderName: record?.senderName,
1377
- agent: agentType,
1378
- instanceId: record?.instanceId,
1379
- historySessionId: normalizeSavedHistorySessionId(record?.historySessionId || normalizedSessionId),
1380
- sessionTitle: record?.sessionTitle,
1381
- workspace: record?.workspace,
1382
- } as HistoryMessage))
1378
+ .map((record: any) => {
1379
+ const base: HistoryMessage = {
1380
+ ts: typeof record?.ts === 'string' ? record.ts : new Date(Number(record?.receivedAt) || Date.now()).toISOString(),
1381
+ receivedAt: Number(record?.receivedAt) || Date.parse(record?.ts || '') || Date.now(),
1382
+ role: record?.role,
1383
+ content: String(record?.content || ''),
1384
+ kind: record?.kind || (record?.role === 'system' ? 'session_start' : 'standard'),
1385
+ senderName: record?.senderName,
1386
+ agent: agentType,
1387
+ instanceId: record?.instanceId,
1388
+ historySessionId: normalizeSavedHistorySessionId(record?.historySessionId || normalizedSessionId),
1389
+ sessionTitle: record?.sessionTitle,
1390
+ workspace: record?.workspace,
1391
+ } as HistoryMessage;
1392
+ // (A2.3 v2 identity passthrough) — if the producer (native_history.js)
1393
+ // emitted v2 stable identity, keep it across the sanitize layer so
1394
+ // downstream (chat-commands.ts normalizeNativeHistoryMessages) sees
1395
+ // the producer's contract output instead of recomputing from index
1396
+ // and content hash. v1 producers without these fields are unaffected.
1397
+ if (typeof record?.providerUnitKey === 'string' && record.providerUnitKey) {
1398
+ (base as any).providerUnitKey = record.providerUnitKey;
1399
+ }
1400
+ if (typeof record?.bubbleId === 'string' && record.bubbleId) {
1401
+ (base as any).bubbleId = record.bubbleId;
1402
+ }
1403
+ if (typeof record?.sequence === 'number' && Number.isFinite(record.sequence)) {
1404
+ (base as any).sequence = record.sequence;
1405
+ }
1406
+ if (typeof record?._turnKey === 'string' && record._turnKey) {
1407
+ (base as any)._turnKey = record._turnKey;
1408
+ }
1409
+ if (typeof record?.bubbleState === 'string' && record.bubbleState) {
1410
+ (base as any).bubbleState = record.bubbleState;
1411
+ }
1412
+ return sanitizeHistoryMessage(agentType, base);
1413
+ })
1383
1414
  .filter(Boolean) as HistoryMessage[];
1384
1415
  }
1385
1416
 
@@ -1387,27 +1418,38 @@ function callProviderNativeHistoryRead(
1387
1418
  agentType: string,
1388
1419
  canonicalHistory: ProviderCanonicalHistoryConfig | undefined,
1389
1420
  scripts: ProviderNativeHistoryScripts | undefined,
1390
- historySessionId: string,
1421
+ historySessionId: string | undefined,
1391
1422
  workspace?: string,
1423
+ excludeInProgressTurn?: boolean,
1424
+ sessionStartedAtMs?: number,
1425
+ envOverrides?: Record<string, string>,
1392
1426
  ): ProviderNativeHistoryReadResult | null {
1393
1427
  const fn = getProviderNativeHistoryScript(scripts, canonicalHistory, 'readSession');
1394
1428
  if (!fn) return null;
1429
+ const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
1395
1430
  const result = fn({
1396
1431
  agentType,
1397
- sessionId: historySessionId,
1398
- historySessionId,
1432
+ sessionId: normalizedSessionId,
1433
+ historySessionId: normalizedSessionId,
1399
1434
  workspace,
1400
1435
  format: canonicalHistory?.format,
1401
1436
  watchPath: canonicalHistory?.watchPath,
1402
- args: { sessionId: historySessionId, historySessionId, workspace },
1437
+ excludeInProgressTurn: excludeInProgressTurn === true,
1438
+ sessionStartedAtMs,
1439
+ envOverrides,
1440
+ args: { sessionId: normalizedSessionId, historySessionId: normalizedSessionId, workspace, excludeInProgressTurn: excludeInProgressTurn === true, sessionStartedAtMs, envOverrides },
1403
1441
  });
1404
1442
  if (!result || typeof result !== 'object') return null;
1405
- const records = normalizeProviderNativeHistoryRecords(agentType, historySessionId, (result as any).messages || (result as any).records);
1443
+ const records = normalizeProviderNativeHistoryRecords(agentType, normalizedSessionId, (result as any).messages || (result as any).records);
1406
1444
  if (records.length === 0) return null;
1407
1445
  return {
1408
1446
  records,
1409
1447
  sourcePath: typeof (result as any).sourcePath === 'string' ? (result as any).sourcePath : '',
1410
1448
  sourceMtimeMs: Number((result as any).sourceMtimeMs) || 0,
1449
+ providerSessionId: typeof (result as any).providerSessionId === 'string' ? (result as any).providerSessionId.trim() : undefined,
1450
+ nativeHistoryCoverage: typeof (result as any).nativeHistoryCoverage === 'string' ? (result as any).nativeHistoryCoverage.trim() : undefined,
1451
+ partialReason: typeof (result as any).partialReason === 'string' ? (result as any).partialReason.trim() : undefined,
1452
+ unavailableReason: typeof (result as any).unavailableReason === 'string' ? (result as any).unavailableReason.trim() : undefined,
1411
1453
  };
1412
1454
  }
1413
1455
 
@@ -1417,10 +1459,14 @@ function buildNativeHistoryReadResult(
1417
1459
  scripts: ProviderNativeHistoryScripts | undefined,
1418
1460
  historySessionId: string | undefined,
1419
1461
  workspace?: string,
1462
+ excludeInProgressTurn?: boolean,
1463
+ sessionStartedAtMs?: number,
1464
+ envOverrides?: Record<string, string>,
1420
1465
  ): ProviderNativeHistoryReadResult | null {
1421
1466
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
1422
- if (!canonicalHistory || !normalizedSessionId || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
1423
- return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace);
1467
+ const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
1468
+ if (!canonicalHistory || (!normalizedSessionId && !normalizedWorkspace) || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
1469
+ return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace, excludeInProgressTurn, sessionStartedAtMs, envOverrides);
1424
1470
  }
1425
1471
 
1426
1472
  function materializeNativeHistoryToMirror(
@@ -1476,16 +1522,33 @@ export function readProviderChatHistory(
1476
1522
  excludeRecentCount?: number;
1477
1523
  historyBehavior?: ProviderHistoryBehavior;
1478
1524
  scripts?: ProviderNativeHistoryScripts;
1525
+ excludeInProgressTurn?: boolean;
1526
+ sessionStartedAtMs?: number;
1527
+ envOverrides?: Record<string, string>;
1479
1528
  } = {},
1480
- ): { messages: HistoryMessage[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' | 'native-unavailable'; sourcePath?: string; sourceMtimeMs?: number } {
1481
- if (isNativeSourceCanonicalHistory(options.canonicalHistory) && options.historySessionId) {
1482
- const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace);
1529
+ ): {
1530
+ messages: HistoryMessage[];
1531
+ hasMore: boolean;
1532
+ source: 'provider-native' | 'adhdev-mirror' | 'native-unavailable';
1533
+ sourcePath?: string;
1534
+ sourceMtimeMs?: number;
1535
+ providerSessionId?: string;
1536
+ nativeHistoryCoverage?: string;
1537
+ partialReason?: string;
1538
+ unavailableReason?: string;
1539
+ } {
1540
+ if (isNativeSourceCanonicalHistory(options.canonicalHistory) && (options.historySessionId || options.workspace)) {
1541
+ const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace, options.excludeInProgressTurn, options.sessionStartedAtMs, options.envOverrides);
1483
1542
  if (!nativeResult) return { messages: [], hasMore: false, source: 'native-unavailable' };
1484
1543
  return {
1485
1544
  ...pageHistoryRecords(agentType, nativeResult.records, options.offset || 0, options.limit || 30, options.excludeRecentCount || 0, options.historyBehavior),
1486
1545
  source: 'provider-native',
1487
1546
  sourcePath: nativeResult.sourcePath,
1488
1547
  sourceMtimeMs: nativeResult.sourceMtimeMs,
1548
+ providerSessionId: nativeResult.providerSessionId,
1549
+ nativeHistoryCoverage: nativeResult.nativeHistoryCoverage,
1550
+ partialReason: nativeResult.partialReason,
1551
+ unavailableReason: nativeResult.unavailableReason,
1489
1552
  };
1490
1553
  }
1491
1554
  return {
@@ -272,6 +272,18 @@ export function getConfigDir(): string {
272
272
  return dir;
273
273
  }
274
274
 
275
+ /**
276
+ * Get the daemon runtime data directory (~/.adhdev/daemon/).
277
+ * Distinct from the user-config dir so runtime state can be cleared independently.
278
+ */
279
+ export function getDaemonDataDir(): string {
280
+ const dir = join(getConfigDir(), 'daemon');
281
+ if (!existsSync(dir)) {
282
+ mkdirSync(dir, { recursive: true });
283
+ }
284
+ return dir;
285
+ }
286
+
275
287
  /**
276
288
  * Get the config file path
277
289
  */
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync } from 'fs';
10
10
  import { join } from 'path';
11
- import { randomUUID } from 'crypto';
11
+ import { createHash, randomBytes, randomUUID } from 'crypto';
12
12
  import { getConfigDir } from './config.js';
13
13
  import type {
14
14
  LocalMeshConfig,
@@ -18,8 +18,11 @@ import type {
18
18
  RepoMeshNodePolicy,
19
19
  RepoMeshNodeCapabilities,
20
20
  RepoMeshCoordinatorConfig,
21
+ RepoMeshHostMetadata,
22
+ RepoMeshDaemonRole,
21
23
  } from '../repo-mesh-types.js';
22
24
  import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
25
+ import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
23
26
 
24
27
  // ─── Persistence ────────────────────────────────
25
28
 
@@ -84,6 +87,7 @@ function mergeMeshPolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMe
84
87
  }
85
88
  const maxParallelTasks = Number(policy.maxParallelTasks);
86
89
  policy.maxParallelTasks = Number.isFinite(maxParallelTasks) ? Math.max(1, Math.min(8, Math.floor(maxParallelTasks))) : 2;
90
+ policy.allowAutoPublishSubmoduleMainCommits = policy.allowAutoPublishSubmoduleMainCommits === true;
87
91
  if (!SESSION_CLEANUP_MODES.has(String(policy.sessionCleanupOnNodeRemove))) {
88
92
  policy.sessionCleanupOnNodeRemove = 'preserve';
89
93
  }
@@ -112,6 +116,7 @@ export interface CreateMeshOptions {
112
116
  defaultBranch?: string;
113
117
  policy?: Partial<RepoMeshPolicy>;
114
118
  coordinator?: RepoMeshCoordinatorConfig;
119
+ meshHost?: RepoMeshHostMetadata;
115
120
  }
116
121
 
117
122
  export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
@@ -133,6 +138,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
133
138
  defaultBranch: opts.defaultBranch,
134
139
  policy: mergeMeshPolicy(undefined, opts.policy),
135
140
  coordinator: opts.coordinator || {},
141
+ meshHost: opts.meshHost || createDefaultMeshHostMetadata(),
136
142
  nodes: [],
137
143
  createdAt: now,
138
144
  updatedAt: now,
@@ -148,6 +154,7 @@ export interface UpdateMeshOptions {
148
154
  defaultBranch?: string;
149
155
  policy?: Partial<RepoMeshPolicy>;
150
156
  coordinator?: RepoMeshCoordinatorConfig;
157
+ meshHost?: RepoMeshHostMetadata;
151
158
  }
152
159
 
153
160
  export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEntry | undefined {
@@ -159,6 +166,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
159
166
  if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
160
167
  if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
161
168
  if (opts.coordinator) mesh.coordinator = opts.coordinator;
169
+ if (opts.meshHost) mesh.meshHost = opts.meshHost;
162
170
  mesh.updatedAt = new Date().toISOString();
163
171
 
164
172
  saveMeshConfig(config);
@@ -174,6 +182,240 @@ export function deleteMesh(meshId: string): boolean {
174
182
  return true;
175
183
  }
176
184
 
185
+ function normalizeManualHostAddress(hostAddress: string): string {
186
+ const normalized = hostAddress.trim().replace(/\/+$/, '');
187
+ if (!normalized) throw new Error('hostAddress required');
188
+ let parsed: URL;
189
+ try {
190
+ parsed = new URL(normalized);
191
+ } catch {
192
+ throw new Error('hostAddress must be a valid http(s) or ws(s) URL');
193
+ }
194
+ if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
195
+ throw new Error('hostAddress must use http, https, ws, or wss');
196
+ }
197
+ return normalized;
198
+ }
199
+
200
+ export function tokenIdForManualPairing(token: string): string {
201
+ return `tok_${createHash('sha256').update(token).digest('hex').slice(0, 16)}`;
202
+ }
203
+
204
+ function normalizeTokenExpiry(value: unknown): string | undefined {
205
+ if (typeof value !== 'string' || !value.trim()) return undefined;
206
+ const date = new Date(value);
207
+ if (Number.isNaN(date.getTime())) throw new Error('expiresAt must be a valid ISO date');
208
+ return date.toISOString();
209
+ }
210
+
211
+ function assertPairingTokenValid(pairing: RepoMeshHostMetadata['pairing'], rawToken: string, nowIso: string): { ok: true; tokenId: string } | { ok: false; reason: string; expectedTokenId?: string; presentedTokenId?: string } {
212
+ const token = rawToken.trim();
213
+ if (!token) return { ok: false, reason: 'token required' };
214
+ const presentedTokenId = tokenIdForManualPairing(token);
215
+ const expectedTokenId = pairing?.tokenId;
216
+ if (!expectedTokenId || pairing?.status === 'not_configured' || pairing?.status === 'revoked') {
217
+ return { ok: false, reason: 'host pairing token is not configured', presentedTokenId };
218
+ }
219
+ if (pairing.expiresAt && new Date(pairing.expiresAt).getTime() <= new Date(nowIso).getTime()) {
220
+ return { ok: false, reason: 'host pairing token expired', expectedTokenId, presentedTokenId };
221
+ }
222
+ if (presentedTokenId !== expectedTokenId) {
223
+ return { ok: false, reason: 'invalid pairing token', expectedTokenId, presentedTokenId };
224
+ }
225
+ return { ok: true, tokenId: presentedTokenId };
226
+ }
227
+
228
+ export interface ConfigureMeshHostPairingOptions {
229
+ hostAddress: string;
230
+ token: string;
231
+ now?: string;
232
+ }
233
+
234
+ export function configureMeshHostPairing(
235
+ meshId: string,
236
+ opts: ConfigureMeshHostPairingOptions,
237
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; hostAddress: string } | undefined {
238
+ const hostAddress = normalizeManualHostAddress(opts.hostAddress);
239
+ const token = opts.token.trim();
240
+ if (!token) throw new Error('token required');
241
+
242
+ const config = loadMeshConfig();
243
+ const mesh = config.meshes.find(m => m.id === meshId);
244
+ if (!mesh) return undefined;
245
+
246
+ const now = opts.now || new Date().toISOString();
247
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
248
+ const meshHost: RepoMeshHostMetadata = {
249
+ ...previous,
250
+ role: 'member',
251
+ hostAddress,
252
+ pairing: {
253
+ status: 'pairing',
254
+ tokenId: tokenIdForManualPairing(token),
255
+ lastPairedAt: now,
256
+ },
257
+ };
258
+
259
+ mesh.meshHost = meshHost;
260
+ mesh.updatedAt = now;
261
+ saveMeshConfig(config);
262
+ return { mesh, meshHost, hostAddress };
263
+ }
264
+
265
+ export interface CreateMeshHostPairingTokenOptions {
266
+ token?: string;
267
+ expiresAt?: string;
268
+ now?: string;
269
+ }
270
+
271
+ export function createMeshHostPairingToken(
272
+ meshId: string,
273
+ opts: CreateMeshHostPairingTokenOptions = {},
274
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; token: string; tokenId: string; expiresAt?: string } | undefined {
275
+ const config = loadMeshConfig();
276
+ const mesh = config.meshes.find(m => m.id === meshId);
277
+ if (!mesh) return undefined;
278
+ const now = opts.now || new Date().toISOString();
279
+ const token = (opts.token || `mhj_${randomBytes(24).toString('base64url')}`).trim();
280
+ if (!token) throw new Error('token required');
281
+ const tokenId = tokenIdForManualPairing(token);
282
+ const expiresAt = normalizeTokenExpiry(opts.expiresAt);
283
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
284
+ if (previous.role === 'member') {
285
+ throw new Error('Mesh Host daemon required to create host pairing tokens; member daemons cannot mint host join tokens.');
286
+ }
287
+ const meshHost: RepoMeshHostMetadata = {
288
+ ...previous,
289
+ role: 'host',
290
+ pairing: {
291
+ status: 'pairing',
292
+ tokenId,
293
+ lastPairedAt: now,
294
+ ...(expiresAt ? { expiresAt } : {}),
295
+ },
296
+ };
297
+ mesh.meshHost = meshHost;
298
+ mesh.updatedAt = now;
299
+ saveMeshConfig(config);
300
+ return { mesh, meshHost, token, tokenId, ...(expiresAt ? { expiresAt } : {}) };
301
+ }
302
+
303
+ export interface MeshHostJoinMemberNodeInput {
304
+ id?: string;
305
+ workspace: string;
306
+ repoRoot?: string;
307
+ daemonId?: string;
308
+ machineId?: string;
309
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
310
+ policy?: RepoMeshNodePolicy;
311
+ role?: RepoMeshDaemonRole;
312
+ }
313
+
314
+ export interface ApplyMeshHostJoinOptions {
315
+ token: string;
316
+ memberNode: MeshHostJoinMemberNodeInput;
317
+ memberMeshId?: string;
318
+ now?: string;
319
+ }
320
+
321
+ export function applyMeshHostJoinRequest(
322
+ meshId: string,
323
+ opts: ApplyMeshHostJoinOptions,
324
+ ): { accepted: true; mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; node: LocalMeshNodeEntry; tokenId: string } | { accepted: false; mesh?: LocalMeshEntry; meshHost?: RepoMeshHostMetadata; tokenId?: string; reason: string } | undefined {
325
+ const config = loadMeshConfig();
326
+ const mesh = config.meshes.find(m => m.id === meshId);
327
+ if (!mesh) return undefined;
328
+ const now = opts.now || new Date().toISOString();
329
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
330
+ if (previous.role === 'member') {
331
+ return { accepted: false, mesh, meshHost: previous, reason: 'Mesh Host daemon required to accept join requests' };
332
+ }
333
+ const meshHost: RepoMeshHostMetadata = { ...previous, role: 'host' };
334
+ const validation = assertPairingTokenValid(meshHost.pairing, opts.token, now);
335
+ if (!validation.ok) {
336
+ mesh.meshHost = {
337
+ ...meshHost,
338
+ pairing: {
339
+ ...(meshHost.pairing || { status: 'not_configured' as const }),
340
+ status: 'rejected',
341
+ lastRejectedAt: now,
342
+ },
343
+ };
344
+ mesh.updatedAt = now;
345
+ saveMeshConfig(config);
346
+ return { accepted: false, mesh, meshHost: mesh.meshHost, tokenId: validation.presentedTokenId, reason: validation.reason };
347
+ }
348
+
349
+ const workspace = opts.memberNode.workspace.trim();
350
+ if (!workspace) throw new Error('memberNode.workspace required');
351
+ const memberId = opts.memberNode.id?.trim();
352
+ let node = mesh.nodes.find(n => (memberId && n.id === memberId) || n.workspace === workspace);
353
+ if (node) {
354
+ node.workspace = workspace;
355
+ node.repoRoot = opts.memberNode.repoRoot;
356
+ node.daemonId = opts.memberNode.daemonId;
357
+ node.machineId = opts.memberNode.machineId;
358
+ node.userOverrides = opts.memberNode.userOverrides || node.userOverrides || {};
359
+ node.policy = { ...(node.policy || {}), ...(opts.memberNode.policy || {}) };
360
+ node.role = 'member';
361
+ } else {
362
+ if (mesh.nodes.length >= 10) throw new Error('Maximum 10 nodes per mesh');
363
+ node = {
364
+ id: memberId || `node_${randomUUID().replace(/-/g, '')}`,
365
+ workspace,
366
+ repoRoot: opts.memberNode.repoRoot,
367
+ daemonId: opts.memberNode.daemonId,
368
+ machineId: opts.memberNode.machineId,
369
+ userOverrides: opts.memberNode.userOverrides || {},
370
+ policy: opts.memberNode.policy || {},
371
+ role: 'member',
372
+ };
373
+ mesh.nodes.push(node);
374
+ }
375
+ mesh.meshHost = {
376
+ ...meshHost,
377
+ pairing: {
378
+ ...(meshHost.pairing || {}),
379
+ status: 'paired',
380
+ tokenId: validation.tokenId,
381
+ joinedAt: now,
382
+ lastPairedAt: meshHost.pairing?.lastPairedAt || now,
383
+ ...(meshHost.pairing?.expiresAt ? { expiresAt: meshHost.pairing.expiresAt } : {}),
384
+ },
385
+ };
386
+ mesh.updatedAt = now;
387
+ saveMeshConfig(config);
388
+ return { accepted: true, mesh, meshHost: mesh.meshHost, node, tokenId: validation.tokenId };
389
+ }
390
+
391
+ export function markMeshHostPairingJoined(
392
+ meshId: string,
393
+ opts: { hostDaemonId?: string; hostNodeId?: string; joinedAt?: string; token?: string; tokenId?: string },
394
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata } | undefined {
395
+ const config = loadMeshConfig();
396
+ const mesh = config.meshes.find(m => m.id === meshId);
397
+ if (!mesh) return undefined;
398
+ const now = opts.joinedAt || new Date().toISOString();
399
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
400
+ const tokenId = opts.tokenId || (opts.token ? tokenIdForManualPairing(opts.token) : previous.pairing?.tokenId);
401
+ mesh.meshHost = {
402
+ ...previous,
403
+ role: 'member',
404
+ ...(opts.hostDaemonId ? { hostDaemonId: opts.hostDaemonId } : {}),
405
+ ...(opts.hostNodeId ? { hostNodeId: opts.hostNodeId } : {}),
406
+ pairing: {
407
+ ...(previous.pairing || {}),
408
+ status: 'paired',
409
+ ...(tokenId ? { tokenId } : {}),
410
+ joinedAt: now,
411
+ lastPairedAt: previous.pairing?.lastPairedAt || now,
412
+ },
413
+ };
414
+ mesh.updatedAt = now;
415
+ saveMeshConfig(config);
416
+ return { mesh, meshHost: mesh.meshHost };
417
+ }
418
+
177
419
  // ─── Node Operations ────────────────────────────
178
420
 
179
421
  export interface AddNodeOptions {
@@ -186,6 +428,8 @@ export interface AddNodeOptions {
186
428
  isLocalWorktree?: boolean;
187
429
  worktreeBranch?: string;
188
430
  clonedFromNodeId?: string;
431
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
432
+ role?: RepoMeshDaemonRole;
189
433
  }
190
434
 
191
435
  export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
@@ -213,6 +457,8 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
213
457
  isLocalWorktree: opts.isLocalWorktree,
214
458
  worktreeBranch: opts.worktreeBranch,
215
459
  clonedFromNodeId: opts.clonedFromNodeId,
460
+ worktreeBootstrap: opts.worktreeBootstrap,
461
+ role: opts.role,
216
462
  };
217
463
 
218
464
  mesh.nodes.push(node);
@@ -238,7 +484,14 @@ export function removeNode(meshId: string, nodeId: string): boolean {
238
484
  export function updateNode(
239
485
  meshId: string,
240
486
  nodeId: string,
241
- opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy },
487
+ opts: {
488
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
489
+ policy?: RepoMeshNodePolicy;
490
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
491
+ /** Per-node instruction surfaced in the coordinator prompt. Pass an
492
+ * empty string or undefined to clear it. */
493
+ systemPrompt?: string;
494
+ },
242
495
  ): LocalMeshNodeEntry | undefined {
243
496
  const config = loadMeshConfig();
244
497
  const mesh = config.meshes.find(m => m.id === meshId);
@@ -249,6 +502,15 @@ export function updateNode(
249
502
 
250
503
  if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
251
504
  if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
505
+ if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
506
+ if (Object.prototype.hasOwnProperty.call(opts, 'systemPrompt')) {
507
+ // Honor explicit clears: { systemPrompt: undefined } drops the field.
508
+ if (opts.systemPrompt && opts.systemPrompt.trim()) {
509
+ node.systemPrompt = opts.systemPrompt;
510
+ } else {
511
+ delete node.systemPrompt;
512
+ }
513
+ }
252
514
  mesh.updatedAt = new Date().toISOString();
253
515
  saveMeshConfig(config);
254
516
  return node;
@@ -92,12 +92,18 @@ export function buildSessionReadStateKey(sessionId: string, providerSessionId?:
92
92
 
93
93
  export function getSessionSeenAt(state: DaemonState, sessionId: string, providerSessionId?: string | null): number {
94
94
  const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
95
- return state.sessionReads?.[providerKey] || state.sessionReads?.[sessionId] || 0;
95
+ return Math.max(state.sessionReads?.[providerKey] || 0, state.sessionReads?.[sessionId] || 0);
96
96
  }
97
97
 
98
98
  export function getSessionSeenMarker(state: DaemonState, sessionId: string, providerSessionId?: string | null): string {
99
99
  const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
100
- return state.sessionReadMarkers?.[providerKey] || state.sessionReadMarkers?.[sessionId] || '';
100
+ const providerSeenAt = state.sessionReads?.[providerKey] || 0;
101
+ const sessionSeenAt = state.sessionReads?.[sessionId] || 0;
102
+ const providerMarker = state.sessionReadMarkers?.[providerKey] || '';
103
+ const sessionMarker = state.sessionReadMarkers?.[sessionId] || '';
104
+ if (sessionSeenAt > providerSeenAt && sessionMarker) return sessionMarker;
105
+ if (providerSeenAt > sessionSeenAt && providerMarker) return providerMarker;
106
+ return providerMarker || sessionMarker;
101
107
  }
102
108
 
103
109
  export function getSessionNotificationDismissal(state: DaemonState, sessionId: string, providerSessionId?: string | null): string {
@@ -802,7 +802,16 @@ export async function handleCliSend(ctx: DevServerContext, req: http.IncomingMes
802
802
  }
803
803
 
804
804
  try {
805
- ctx.instanceManager!.sendEvent(target.instanceId, 'send_message', { text });
805
+ if (target.category === 'cli') {
806
+ const bundle = getCliTargetBundle(ctx, type, instanceId);
807
+ if (!bundle) {
808
+ ctx.json(res, 404, { error: `No running CLI adapter found for: ${type || instanceId}` });
809
+ return;
810
+ }
811
+ await bundle.adapter.sendMessage(text);
812
+ } else {
813
+ ctx.instanceManager!.sendEvent(target.instanceId, 'send_message', { text });
814
+ }
806
815
  ctx.json(res, 200, { sent: true, type: target.type, instanceId: target.instanceId });
807
816
  } catch (e: any) {
808
817
  ctx.json(res, 500, { error: `Send failed: ${e.message}` });
@@ -7,8 +7,10 @@
7
7
  * Migrated from @adhdev/core — this is now the single source of truth.
8
8
  */
9
9
 
10
- import { execSync } from 'child_process';
11
- import { existsSync } from 'fs';
10
+ import { exec } from 'child_process';
11
+ import { promisify } from 'util';
12
+ const execAsync = promisify(exec);
13
+ import { existsSync, statSync } from 'fs';
12
14
  import { platform, homedir } from 'os';
13
15
  import * as path from 'path';
14
16
  import type { ProviderLoader } from '../providers/provider-loader.js';
@@ -73,25 +75,33 @@ function findCliCommand(command: string): string | null {
73
75
  const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
74
76
  return existsSync(resolved) ? resolved : null;
75
77
  }
76
- try {
77
- const result = execSync(
78
- platform() === 'win32' ? `where ${trimmed}` : `which ${trimmed}`,
79
- { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
80
- ).trim();
81
- return result.split('\n')[0] || null;
82
- } catch {
83
- return null;
78
+ const isWin = platform() === 'win32';
79
+ const paths = (process.env.PATH || '').split(isWin ? ';' : ':');
80
+ const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
81
+ for (const p of paths) {
82
+ if (!p) continue;
83
+ for (const ext of exes) {
84
+ const fullPath = path.join(p, trimmed + ext);
85
+ try {
86
+ if (existsSync(fullPath)) {
87
+ const stat = statSync(fullPath);
88
+ if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
89
+ return fullPath;
90
+ }
91
+ }
92
+ } catch { }
93
+ }
84
94
  }
95
+ return null;
85
96
  }
86
97
 
87
- function getIdeVersion(cliCommand: string): string | null {
98
+ async function getIdeVersion(cliCommand: string): Promise<string | null> {
88
99
  try {
89
- const result = execSync(`"${cliCommand}" --version`, {
100
+ const { stdout } = await execAsync(`"${cliCommand}" --version`, {
90
101
  encoding: 'utf-8',
91
102
  timeout: 10000,
92
- stdio: ['pipe', 'pipe', 'pipe'],
93
- }).trim();
94
- return result.split('\n')[0] || null;
103
+ });
104
+ return stdout.trim().split('\n')[0] || null;
95
105
  } catch {
96
106
  return null;
97
107
  }
@@ -152,7 +162,7 @@ export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEIn
152
162
  const installed = os === 'darwin'
153
163
  ? !!(resolvedCli || appPath)
154
164
  : !!resolvedCli;
155
- const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
165
+ const version = resolvedCli ? await getIdeVersion(resolvedCli) : null;
156
166
 
157
167
  results.push({
158
168
  id: def.id,