@adhdev/daemon-core 0.9.76-rc.2 → 0.9.76-rc.20
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.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -1
- package/dist/cli-adapters/provider-cli-runtime.d.ts +1 -0
- package/dist/commands/cli-manager.d.ts +6 -4
- package/dist/commands/mesh-coordinator.d.ts +2 -0
- package/dist/commands/router.d.ts +4 -0
- package/dist/config/mesh-config.d.ts +3 -0
- package/dist/git/git-worktree.d.ts +64 -0
- package/dist/git/index.d.ts +2 -0
- package/dist/index.js +870 -361
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +897 -393
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +3 -0
- package/dist/repo-mesh-types.d.ts +4 -0
- package/dist/shared-types.d.ts +18 -1
- package/package.json +3 -4
- package/src/cli-adapters/provider-cli-adapter.ts +6 -3
- package/src/cli-adapters/provider-cli-runtime.ts +3 -2
- package/src/commands/chat-commands.ts +49 -4
- package/src/commands/cli-manager.ts +13 -3
- package/src/commands/mesh-coordinator.ts +149 -6
- package/src/commands/router.ts +247 -30
- package/src/config/mesh-config.ts +6 -0
- package/src/git/git-worktree.ts +214 -0
- package/src/git/index.ts +14 -0
- package/src/mesh/coordinator-prompt.ts +25 -10
- package/src/providers/cli-provider-instance.d.ts +2 -0
- package/src/providers/cli-provider-instance.ts +6 -1
- package/src/repo-mesh-types.ts +4 -0
- package/src/shared-types.ts +20 -1
- package/src/status/builders.ts +17 -12
|
@@ -8,6 +8,7 @@ import { type ProviderModule } from './contracts.js';
|
|
|
8
8
|
import type { ProviderInstance, ProviderState, InstanceContext, HotChatSessionState, SessionModalState } from './provider-instance.js';
|
|
9
9
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
10
10
|
import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
11
|
+
import type { ChatMessage } from '../types.js';
|
|
11
12
|
type PersistableCliHistoryMessage = {
|
|
12
13
|
role: string;
|
|
13
14
|
content: string;
|
|
@@ -67,6 +68,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
67
68
|
constructor(provider: ProviderModule, workingDir: string, cliArgs?: string[], instanceId?: string, transportFactory?: PtyTransportFactory, options?: {
|
|
68
69
|
providerSessionId?: string;
|
|
69
70
|
launchMode?: 'new' | 'resume' | 'manual';
|
|
71
|
+
extraEnv?: Record<string, string>;
|
|
70
72
|
onProviderSessionResolved?: (info: {
|
|
71
73
|
instanceId: string;
|
|
72
74
|
providerType: string;
|
|
@@ -112,6 +114,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
112
114
|
private maybeAppendRuntimeRecoveryMessage;
|
|
113
115
|
private appendRuntimeSystemMessage;
|
|
114
116
|
private appendRuntimeMessage;
|
|
117
|
+
mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[];
|
|
115
118
|
private mergeConversationMessages;
|
|
116
119
|
private formatApprovalRequestMessage;
|
|
117
120
|
private promoteProviderSessionId;
|
|
@@ -149,6 +149,10 @@ export interface LocalMeshNodeEntry {
|
|
|
149
149
|
policy: RepoMeshNodePolicy;
|
|
150
150
|
/** For single-machine mesh: same daemon, different worktree */
|
|
151
151
|
isLocalWorktree?: boolean;
|
|
152
|
+
/** Branch this worktree tracks (set when created via clone_mesh_node) */
|
|
153
|
+
worktreeBranch?: string;
|
|
154
|
+
/** Node ID this worktree was cloned from */
|
|
155
|
+
clonedFromNodeId?: string;
|
|
152
156
|
}
|
|
153
157
|
export interface RepoMeshStatus {
|
|
154
158
|
meshId: string;
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -328,6 +328,15 @@ export interface CompactSessionEntry {
|
|
|
328
328
|
settings?: Record<string, any>;
|
|
329
329
|
}
|
|
330
330
|
export type VersionUpdateReason = 'force_update_below' | 'major_minor_mismatch' | 'patch_mismatch' | 'daemon_ahead';
|
|
331
|
+
export type ReleaseChannel = 'stable' | 'preview';
|
|
332
|
+
export type NpmUpdateTag = 'latest' | 'next';
|
|
333
|
+
export interface VersionUpdatePolicy {
|
|
334
|
+
channel: ReleaseChannel;
|
|
335
|
+
npmTag: NpmUpdateTag;
|
|
336
|
+
targetVersion: string;
|
|
337
|
+
minVersion?: string;
|
|
338
|
+
updateCommand: string;
|
|
339
|
+
}
|
|
331
340
|
/** Available provider information */
|
|
332
341
|
export interface AvailableProviderInfo {
|
|
333
342
|
type: string;
|
|
@@ -469,6 +478,10 @@ export interface CompactDaemonEntry {
|
|
|
469
478
|
versionMismatch?: boolean;
|
|
470
479
|
versionUpdateRequired?: boolean;
|
|
471
480
|
versionUpdateReason?: VersionUpdateReason;
|
|
481
|
+
releaseChannel?: ReleaseChannel;
|
|
482
|
+
updateChannel?: ReleaseChannel;
|
|
483
|
+
updatePolicy?: VersionUpdatePolicy;
|
|
484
|
+
updateCommand?: string;
|
|
472
485
|
terminalBackend?: TerminalBackendStatus;
|
|
473
486
|
detectedIdes?: DetectedIdeInfo[];
|
|
474
487
|
availableProviders?: AvailableProviderInfo[];
|
|
@@ -490,10 +503,14 @@ export interface CloudDaemonSummaryEntry {
|
|
|
490
503
|
versionMismatch?: boolean;
|
|
491
504
|
versionUpdateRequired?: boolean;
|
|
492
505
|
versionUpdateReason?: VersionUpdateReason;
|
|
506
|
+
releaseChannel?: ReleaseChannel;
|
|
507
|
+
updateChannel?: ReleaseChannel;
|
|
508
|
+
updatePolicy?: VersionUpdatePolicy;
|
|
509
|
+
updateCommand?: string;
|
|
493
510
|
terminalBackend?: TerminalBackendStatus;
|
|
494
511
|
}
|
|
495
512
|
/** Minimal daemon bootstrap payload used by dashboard WS to initiate P2P. */
|
|
496
|
-
export interface DashboardBootstrapDaemonEntry {
|
|
513
|
+
export interface DashboardBootstrapDaemonEntry extends Partial<CloudDaemonSummaryEntry> {
|
|
497
514
|
id: string;
|
|
498
515
|
p2p?: StatusReportPayload['p2p'];
|
|
499
516
|
timestamp?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.76-rc.
|
|
3
|
+
"version": "0.9.76-rc.20",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -52,16 +52,15 @@
|
|
|
52
52
|
"chalk": "^5.3.0",
|
|
53
53
|
"chokidar": "^5.0.0",
|
|
54
54
|
"conf": "^13.0.0",
|
|
55
|
+
"js-yaml": "^4.1.1",
|
|
55
56
|
"node-pty": "^1.2.0-beta.12",
|
|
56
57
|
"ws": "^8.19.0"
|
|
57
58
|
},
|
|
58
|
-
"bundleDependencies": [
|
|
59
|
-
"@adhdev/session-host-core"
|
|
60
|
-
],
|
|
61
59
|
"optionalDependencies": {
|
|
62
60
|
"@adhdev/ghostty-vt-node": "*"
|
|
63
61
|
},
|
|
64
62
|
"devDependencies": {
|
|
63
|
+
"@types/js-yaml": "^4.0.9",
|
|
65
64
|
"@types/node": "^22.0.0",
|
|
66
65
|
"@types/ws": "^8.18.1",
|
|
67
66
|
"tsup": "^8.2.0",
|
|
@@ -266,9 +266,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
266
266
|
const currentSnapshot = normalizeScreenSnapshot(screenText);
|
|
267
267
|
const lastSnapshot = this.lastScreenSnapshot;
|
|
268
268
|
if (!lastSnapshot || lastSnapshot === currentSnapshot) return screenText;
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
269
|
+
const activeScreenPattern = /\besc to (?:interrupt|stop)\b|Enter to interrupt, Ctrl\+C to cancel|Enter to confirm\s*[·•-]\s*Esc to cancel|\b(?:MCP servers?|tool calls?)\b[^\n\r]{0,160}\brequire approval\b/i;
|
|
270
|
+
const staleSnapshotLooksActive = activeScreenPattern.test(lastSnapshot);
|
|
271
|
+
const currentScreenLooksIdle = /(?:^|\n|\r)\s*[❯›>]\s*(?:Try\s+["“][^\n\r"”]+["”])?\s*(?:\n|\r|$)/.test(screenText)
|
|
272
|
+
&& !activeScreenPattern.test(screenText);
|
|
272
273
|
if (staleSnapshotLooksActive && currentScreenLooksIdle) return screenText;
|
|
273
274
|
if (currentSnapshot.length >= lastSnapshot.length) return screenText;
|
|
274
275
|
// Terminal screen reads can miss a just-rendered completed Hermes box while
|
|
@@ -421,6 +422,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
421
422
|
provider: CliProviderModule,
|
|
422
423
|
workingDir: string,
|
|
423
424
|
private extraArgs: string[] = [],
|
|
425
|
+
private extraEnv: Record<string, string> = {},
|
|
424
426
|
transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
|
|
425
427
|
) {
|
|
426
428
|
this.provider = provider;
|
|
@@ -522,6 +524,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
522
524
|
runtimeSettings: this.runtimeSettings,
|
|
523
525
|
workingDir: this.workingDir,
|
|
524
526
|
extraArgs: this.extraArgs,
|
|
527
|
+
extraEnv: this.extraEnv,
|
|
525
528
|
});
|
|
526
529
|
|
|
527
530
|
LOG.info('CLI', `[${this.cliType}] Spawning in ${this.workingDir}`);
|
|
@@ -27,8 +27,9 @@ export function resolveCliSpawnPlan(options: {
|
|
|
27
27
|
runtimeSettings: Record<string, any>;
|
|
28
28
|
workingDir: string;
|
|
29
29
|
extraArgs: string[];
|
|
30
|
+
extraEnv?: Record<string, string>;
|
|
30
31
|
}): CliSpawnPlan {
|
|
31
|
-
const { provider, runtimeSettings, workingDir, extraArgs } = options;
|
|
32
|
+
const { provider, runtimeSettings, workingDir, extraArgs, extraEnv } = options;
|
|
32
33
|
const { spawn: spawnConfig } = provider;
|
|
33
34
|
const configuredCommand = typeof runtimeSettings.executablePath === 'string' && runtimeSettings.executablePath.trim()
|
|
34
35
|
? runtimeSettings.executablePath.trim()
|
|
@@ -65,7 +66,7 @@ export function resolveCliSpawnPlan(options: {
|
|
|
65
66
|
shellArgs = allArgs;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const env = buildCliSpawnEnv(process.env, spawnConfig.env);
|
|
69
|
+
const env = buildCliSpawnEnv(process.env, { ...(spawnConfig.env || {}), ...(extraEnv || {}) });
|
|
69
70
|
// Some CLI agents, notably Hermes, route their tools through TERMINAL_CWD
|
|
70
71
|
// rather than process.cwd(). Keep the generic ADHDev launch workspace as
|
|
71
72
|
// the single source of truth so PTY cwd and tool cwd cannot diverge.
|
|
@@ -28,6 +28,10 @@ interface ApprovalSelectableInstance extends ProviderInstance {
|
|
|
28
28
|
recordApprovalSelection?(buttonText: string): void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
interface RuntimeChatMessageMerger extends ProviderInstance {
|
|
32
|
+
mergeRuntimeChatMessages?(messages: ChatMessage[]): ChatMessage[];
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
type LegacyStringScript = (params?: Record<string, unknown> | string) => string;
|
|
32
36
|
|
|
33
37
|
function getCurrentProviderType(h: CommandHelpers, fallback = ''): string {
|
|
@@ -250,6 +254,40 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
|
|
|
250
254
|
}
|
|
251
255
|
}
|
|
252
256
|
|
|
257
|
+
function isGeneratingLikeStatus(status: unknown): boolean {
|
|
258
|
+
return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
|
|
262
|
+
if (!isGeneratingLikeStatus(parsedStatus)) return false;
|
|
263
|
+
if (hasNonEmptyModalButtons(activeModal)) return false;
|
|
264
|
+
const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
265
|
+
if (adapterRawStatus !== 'idle') return false;
|
|
266
|
+
if (typeof adapter.isProcessing === 'function' && adapter.isProcessing()) return false;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): string {
|
|
271
|
+
if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
|
|
272
|
+
return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function finalizeStreamingMessagesWhenIdle(messages: ChatMessage[], status: string): ChatMessage[] {
|
|
276
|
+
if (status !== 'idle') return messages;
|
|
277
|
+
return messages.map((message) => {
|
|
278
|
+
const meta = message.meta && typeof message.meta === 'object'
|
|
279
|
+
? message.meta as Record<string, unknown>
|
|
280
|
+
: undefined;
|
|
281
|
+
const hasStreamingMeta = meta?.streaming === true;
|
|
282
|
+
if (message.bubbleState !== 'streaming' && !hasStreamingMeta) return message;
|
|
283
|
+
return {
|
|
284
|
+
...message,
|
|
285
|
+
...(message.bubbleState === 'streaming' ? { bubbleState: 'final' as const } : {}),
|
|
286
|
+
...(hasStreamingMeta ? { meta: { ...meta, streaming: false } } : {}),
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
253
291
|
function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
|
|
254
292
|
let validatedPayload: Record<string, any>;
|
|
255
293
|
const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
|
|
@@ -760,10 +798,17 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
760
798
|
? parsedRecord.coverage
|
|
761
799
|
: undefined;
|
|
762
800
|
const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
|
|
763
|
-
const returnedStatus = parsedRecord.status
|
|
764
|
-
|
|
801
|
+
const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus);
|
|
802
|
+
const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
|
|
803
|
+
const parsedMessages = finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus);
|
|
804
|
+
const returnedMessages = runtimeMessageMerger?.category === 'cli'
|
|
805
|
+
&& runtimeMessageMerger.type === adapter.cliType
|
|
806
|
+
&& typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
|
|
807
|
+
? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
|
|
808
|
+
: parsedMessages;
|
|
809
|
+
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}`);
|
|
765
810
|
return buildReadChatCommandResult({
|
|
766
|
-
messages:
|
|
811
|
+
messages: returnedMessages,
|
|
767
812
|
status: returnedStatus,
|
|
768
813
|
activeModal,
|
|
769
814
|
debugReadChat: {
|
|
@@ -774,7 +819,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
774
819
|
returnedStatus: String(returnedStatus || ''),
|
|
775
820
|
shouldPreferAdapterMessages: false,
|
|
776
821
|
parsedMsgCount: parsedRecord.messages.length,
|
|
777
|
-
returnedMsgCount:
|
|
822
|
+
returnedMsgCount: returnedMessages.length,
|
|
778
823
|
},
|
|
779
824
|
...(title ? { title } : {}),
|
|
780
825
|
...(providerSessionId ? { providerSessionId } : {}),
|
|
@@ -132,6 +132,12 @@ type CliAdapterWithExtraArgs = CliAdapter & {
|
|
|
132
132
|
extraArgs?: string[];
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
+
type CliStartOptions = {
|
|
136
|
+
resumeSessionId?: string;
|
|
137
|
+
settingsOverride?: Record<string, any>;
|
|
138
|
+
extraEnv?: Record<string, string>;
|
|
139
|
+
};
|
|
140
|
+
|
|
135
141
|
function isUuid(value: string): boolean {
|
|
136
142
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
137
143
|
}
|
|
@@ -365,6 +371,7 @@ export class DaemonCliManager {
|
|
|
365
371
|
runtimeId: string,
|
|
366
372
|
providerSessionId?: string,
|
|
367
373
|
attachExisting = false,
|
|
374
|
+
extraEnv?: Record<string, string>,
|
|
368
375
|
): CliAdapter {
|
|
369
376
|
// cliType normalize (Resolve alias)
|
|
370
377
|
const normalizedType = this.providerLoader.resolveAlias(cliType);
|
|
@@ -382,7 +389,7 @@ export class DaemonCliManager {
|
|
|
382
389
|
providerSessionId,
|
|
383
390
|
attachExisting,
|
|
384
391
|
);
|
|
385
|
-
return new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, transportFactory);
|
|
392
|
+
return new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, extraEnv || {}, transportFactory);
|
|
386
393
|
}
|
|
387
394
|
|
|
388
395
|
throw new Error(`No CLI provider found for '${cliType}'. Create a provider.js in providers/cli/${cliType}/`);
|
|
@@ -425,6 +432,7 @@ export class DaemonCliManager {
|
|
|
425
432
|
options?: {
|
|
426
433
|
providerSessionId?: string;
|
|
427
434
|
launchMode?: CliLaunchMode;
|
|
435
|
+
extraEnv?: Record<string, string>;
|
|
428
436
|
onProviderSessionResolved?: (info: {
|
|
429
437
|
instanceId: string;
|
|
430
438
|
providerType: string;
|
|
@@ -480,7 +488,7 @@ export class DaemonCliManager {
|
|
|
480
488
|
workingDir: string,
|
|
481
489
|
cliArgs?: string[],
|
|
482
490
|
initialModel?: string,
|
|
483
|
-
options?:
|
|
491
|
+
options?: CliStartOptions,
|
|
484
492
|
): Promise<{ runtimeSessionId: string; providerSessionId?: string }> {
|
|
485
493
|
const trimmed = (workingDir || '').trim();
|
|
486
494
|
if (!trimmed) throw new Error('working directory required');
|
|
@@ -629,6 +637,7 @@ export class DaemonCliManager {
|
|
|
629
637
|
{
|
|
630
638
|
providerSessionId: sessionBinding.providerSessionId,
|
|
631
639
|
launchMode: sessionBinding.launchMode,
|
|
640
|
+
extraEnv: options?.extraEnv,
|
|
632
641
|
onProviderSessionResolved: ({ providerSessionId, providerName, providerType, workspace }) => {
|
|
633
642
|
this.persistRecentActivity({
|
|
634
643
|
kind: 'cli',
|
|
@@ -651,6 +660,7 @@ export class DaemonCliManager {
|
|
|
651
660
|
key,
|
|
652
661
|
sessionBinding.providerSessionId,
|
|
653
662
|
false,
|
|
663
|
+
options?.extraEnv,
|
|
654
664
|
);
|
|
655
665
|
try {
|
|
656
666
|
await adapter.spawn();
|
|
@@ -904,7 +914,7 @@ export class DaemonCliManager {
|
|
|
904
914
|
dir,
|
|
905
915
|
args?.cliArgs,
|
|
906
916
|
args?.initialModel,
|
|
907
|
-
{ resumeSessionId: args?.resumeSessionId, settingsOverride: args?.settings },
|
|
917
|
+
{ resumeSessionId: args?.resumeSessionId, settingsOverride: args?.settings, extraEnv: args?.env },
|
|
908
918
|
);
|
|
909
919
|
|
|
910
920
|
return {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, readdirSync, realpathSync } from 'node:fs'
|
|
2
3
|
import { createRequire } from 'node:module'
|
|
3
|
-
import
|
|
4
|
+
import * as os from 'node:os'
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
4
6
|
import type { ProviderModule, MeshCoordinatorMcpConfigFormat } from '../providers/contracts.js'
|
|
5
7
|
|
|
6
8
|
export interface MeshCoordinatorMcpServerLaunch {
|
|
@@ -32,6 +34,7 @@ export type MeshCoordinatorSetup =
|
|
|
32
34
|
|
|
33
35
|
export interface ResolveMeshCoordinatorSetupOptions {
|
|
34
36
|
provider?: ProviderModule | null
|
|
37
|
+
cliType?: string
|
|
35
38
|
meshId: string
|
|
36
39
|
workspace: string
|
|
37
40
|
adhdevMcpCommand?: string
|
|
@@ -41,6 +44,58 @@ export interface ResolveMeshCoordinatorSetupOptions {
|
|
|
41
44
|
|
|
42
45
|
const DEFAULT_SERVER_NAME = 'adhdev-mesh'
|
|
43
46
|
const DEFAULT_ADHDEV_MCP_COMMAND = 'adhdev-mcp'
|
|
47
|
+
const HERMES_CLI_TYPE = 'hermes-cli'
|
|
48
|
+
const HERMES_MCP_CONFIG_PATH = '~/.hermes/config.yaml'
|
|
49
|
+
|
|
50
|
+
function isHermesProvider(provider: ProviderModule | null | undefined, cliType?: string): boolean {
|
|
51
|
+
const type = cliType?.trim() || provider?.type?.trim() || ''
|
|
52
|
+
return type === HERMES_CLI_TYPE
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveHermesMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupOptions): MeshCoordinatorSetup {
|
|
56
|
+
const mcpServer = resolveAdhdevMcpServerLaunch({
|
|
57
|
+
meshId: options.meshId,
|
|
58
|
+
nodeExecutable: options.nodeExecutable,
|
|
59
|
+
adhdevMcpEntryPath: options.adhdevMcpEntryPath,
|
|
60
|
+
})
|
|
61
|
+
if (!mcpServer) {
|
|
62
|
+
return {
|
|
63
|
+
kind: 'unsupported',
|
|
64
|
+
reason: 'Could not resolve the ADHDev MCP server entrypoint and a Node runtime with WebSocket support for daemon IPC mode',
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const configPath = resolveMcpConfigPath(HERMES_MCP_CONFIG_PATH, options.workspace)
|
|
68
|
+
if (!configPath.trim()) {
|
|
69
|
+
return createHermesManualMeshCoordinatorSetup(options.meshId, options.workspace)
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
kind: 'auto_import',
|
|
73
|
+
serverName: DEFAULT_SERVER_NAME,
|
|
74
|
+
configPath,
|
|
75
|
+
configFormat: 'hermes_config_yaml',
|
|
76
|
+
mcpServer,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createHermesManualMeshCoordinatorSetup(meshId: string, workspace: string): MeshCoordinatorSetup {
|
|
81
|
+
return {
|
|
82
|
+
kind: 'manual',
|
|
83
|
+
serverName: DEFAULT_SERVER_NAME,
|
|
84
|
+
configFormat: 'hermes_config_yaml',
|
|
85
|
+
configPathCommand: HERMES_MCP_CONFIG_PATH,
|
|
86
|
+
requiresRestart: true,
|
|
87
|
+
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.',
|
|
88
|
+
template: renderMeshCoordinatorTemplate(
|
|
89
|
+
'mcp_servers:\n {{serverName}}:\n command: {{adhdevMcpCommand}}\n args:\n - --repo-mesh\n - {{meshId}}\n enabled: true\n',
|
|
90
|
+
{
|
|
91
|
+
meshId,
|
|
92
|
+
workspace,
|
|
93
|
+
serverName: DEFAULT_SERVER_NAME,
|
|
94
|
+
adhdevMcpCommand: DEFAULT_ADHDEV_MCP_COMMAND,
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
44
99
|
|
|
45
100
|
export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupOptions): MeshCoordinatorSetup {
|
|
46
101
|
const { provider, meshId, workspace } = options
|
|
@@ -52,6 +107,10 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
|
|
|
52
107
|
}
|
|
53
108
|
}
|
|
54
109
|
|
|
110
|
+
if (isHermesProvider(provider, options.cliType)) {
|
|
111
|
+
return resolveHermesMeshCoordinatorSetup(options)
|
|
112
|
+
}
|
|
113
|
+
|
|
55
114
|
const mcpConfig = config.mcpConfig
|
|
56
115
|
if (!mcpConfig || mcpConfig.mode === 'none') {
|
|
57
116
|
return {
|
|
@@ -74,13 +133,13 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
|
|
|
74
133
|
if (!mcpServer) {
|
|
75
134
|
return {
|
|
76
135
|
kind: 'unsupported',
|
|
77
|
-
reason: 'Could not resolve the ADHDev MCP server entrypoint
|
|
136
|
+
reason: 'Could not resolve the ADHDev MCP server entrypoint and a Node runtime with WebSocket support for daemon IPC mode',
|
|
78
137
|
}
|
|
79
138
|
}
|
|
80
139
|
return {
|
|
81
140
|
kind: 'auto_import',
|
|
82
141
|
serverName,
|
|
83
|
-
configPath:
|
|
142
|
+
configPath: resolveMcpConfigPath(path, workspace),
|
|
84
143
|
configFormat: mcpConfig.format,
|
|
85
144
|
mcpServer,
|
|
86
145
|
}
|
|
@@ -118,6 +177,14 @@ function renderMeshCoordinatorTemplate(template: string, values: Record<string,
|
|
|
118
177
|
return template.replace(/\{\{\s*(meshId|workspace|serverName|adhdevMcpCommand)\s*\}\}/g, (_, key: string) => values[key] || '')
|
|
119
178
|
}
|
|
120
179
|
|
|
180
|
+
function resolveMcpConfigPath(configPath: string, workspace: string): string {
|
|
181
|
+
const trimmed = configPath.trim()
|
|
182
|
+
if (trimmed === '~') return os.homedir()
|
|
183
|
+
if (trimmed.startsWith('~/')) return join(os.homedir(), trimmed.slice(2))
|
|
184
|
+
if (isAbsolute(trimmed)) return trimmed
|
|
185
|
+
return join(workspace, trimmed)
|
|
186
|
+
}
|
|
187
|
+
|
|
121
188
|
function resolveAdhdevMcpServerLaunch(options: {
|
|
122
189
|
meshId: string
|
|
123
190
|
nodeExecutable?: string
|
|
@@ -125,9 +192,85 @@ function resolveAdhdevMcpServerLaunch(options: {
|
|
|
125
192
|
}): MeshCoordinatorMcpServerLaunch | null {
|
|
126
193
|
const entryPath = resolveAdhdevMcpEntryPath(options.adhdevMcpEntryPath)
|
|
127
194
|
if (!entryPath) return null
|
|
195
|
+
const nodeExecutable = resolveMcpNodeExecutable(options.nodeExecutable)
|
|
196
|
+
if (!nodeExecutable) return null
|
|
128
197
|
return {
|
|
129
|
-
command:
|
|
130
|
-
args: [entryPath, '--repo-mesh', options.meshId],
|
|
198
|
+
command: nodeExecutable,
|
|
199
|
+
args: [entryPath, '--mode', 'ipc', '--repo-mesh', options.meshId],
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveMcpNodeExecutable(explicitExecutable?: string): string | null {
|
|
204
|
+
const explicit = explicitExecutable?.trim()
|
|
205
|
+
if (explicit) return explicit
|
|
206
|
+
|
|
207
|
+
const candidates: string[] = []
|
|
208
|
+
const addCandidate = (candidate?: string | null) => {
|
|
209
|
+
const trimmed = candidate?.trim()
|
|
210
|
+
if (!trimmed) return
|
|
211
|
+
const normalized = normalizeExistingPath(trimmed) || trimmed
|
|
212
|
+
if (!candidates.includes(normalized)) candidates.push(normalized)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
addCandidate(process.env.ADHDEV_MCP_NODE_EXECUTABLE)
|
|
216
|
+
addCandidate(process.env.ADHDEV_NODE_EXECUTABLE)
|
|
217
|
+
addCandidate(process.env.npm_node_execpath)
|
|
218
|
+
addNodeCandidatesFromPath(process.env.PATH, addCandidate)
|
|
219
|
+
addNodeCandidatesFromNvm(os.homedir(), addCandidate)
|
|
220
|
+
addCandidate('/opt/homebrew/bin/node')
|
|
221
|
+
addCandidate('/usr/local/bin/node')
|
|
222
|
+
addCandidate('/usr/bin/node')
|
|
223
|
+
addCandidate(process.execPath)
|
|
224
|
+
|
|
225
|
+
for (const candidate of candidates) {
|
|
226
|
+
if (nodeRuntimeSupportsWebSocket(candidate)) return candidate
|
|
227
|
+
}
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function addNodeCandidatesFromPath(pathValue: string | undefined, addCandidate: (candidate?: string | null) => void) {
|
|
232
|
+
for (const entry of (pathValue || '').split(':')) {
|
|
233
|
+
const dir = entry.trim()
|
|
234
|
+
if (!dir) continue
|
|
235
|
+
addCandidate(join(dir, 'node'))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function addNodeCandidatesFromNvm(homeDir: string, addCandidate: (candidate?: string | null) => void) {
|
|
240
|
+
const versionsDir = join(homeDir, '.nvm', 'versions', 'node')
|
|
241
|
+
try {
|
|
242
|
+
const versionDirs = readdirSync(versionsDir, { withFileTypes: true })
|
|
243
|
+
.filter((entry) => entry.isDirectory())
|
|
244
|
+
.map((entry) => entry.name)
|
|
245
|
+
.sort(compareNodeVersionNamesDescending)
|
|
246
|
+
for (const versionDir of versionDirs) {
|
|
247
|
+
addCandidate(join(versionsDir, versionDir, 'bin', 'node'))
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// nvm is optional; PATH and process.execPath candidates still cover normal installs.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function compareNodeVersionNamesDescending(a: string, b: string): number {
|
|
255
|
+
const parse = (value: string) => value.replace(/^v/, '').split('.').map((part) => Number.parseInt(part, 10) || 0)
|
|
256
|
+
const left = parse(a)
|
|
257
|
+
const right = parse(b)
|
|
258
|
+
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
259
|
+
const diff = (right[i] || 0) - (left[i] || 0)
|
|
260
|
+
if (diff !== 0) return diff
|
|
261
|
+
}
|
|
262
|
+
return b.localeCompare(a)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function nodeRuntimeSupportsWebSocket(nodeExecutable: string): boolean {
|
|
266
|
+
try {
|
|
267
|
+
execFileSync(nodeExecutable, ['-e', "process.exit(typeof WebSocket === 'function' ? 0 : 42)"], {
|
|
268
|
+
stdio: 'ignore',
|
|
269
|
+
timeout: 3000,
|
|
270
|
+
})
|
|
271
|
+
return true
|
|
272
|
+
} catch {
|
|
273
|
+
return false
|
|
131
274
|
}
|
|
132
275
|
}
|
|
133
276
|
|