@adhdev/daemon-core 0.9.76-rc.3 → 0.9.76-rc.31
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 +17 -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-types.d.ts +1 -1
- package/dist/git/git-worktree.d.ts +64 -0
- package/dist/git/index.d.ts +2 -0
- package/dist/index.js +1058 -384
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1085 -416
- 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/providers/provider-instance-manager.d.ts +1 -0
- package/dist/providers/provider-instance.d.ts +2 -0
- package/dist/repo-mesh-types.d.ts +6 -0
- package/dist/shared-types.d.ts +22 -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 +50 -5
- package/src/commands/cli-manager.ts +78 -5
- package/src/commands/handler.ts +13 -4
- package/src/commands/mesh-coordinator.ts +149 -6
- package/src/commands/router.ts +319 -32
- package/src/config/mesh-config.ts +6 -0
- package/src/git/git-commands.ts +5 -1
- package/src/git/git-types.ts +1 -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/mesh/mesh-events.ts +40 -17
- package/src/providers/cli-provider-instance.d.ts +2 -0
- package/src/providers/cli-provider-instance.ts +55 -7
- package/src/providers/provider-instance-manager.ts +20 -1
- package/src/providers/provider-instance.ts +2 -0
- package/src/repo-mesh-types.ts +6 -0
- package/src/shared-types.ts +24 -1
- package/src/status/builders.ts +17 -12
- package/src/status/reporter.ts +6 -0
|
@@ -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
|
|
package/src/commands/router.ts
CHANGED
|
@@ -30,10 +30,11 @@ import { SessionRegistry } from '../sessions/registry.js';
|
|
|
30
30
|
import { LOG } from '../logging/logger.js';
|
|
31
31
|
import { logCommand } from '../logging/command-log.js';
|
|
32
32
|
import type { CommandLogEntry } from '../logging/command-log.js';
|
|
33
|
+
import * as yaml from 'js-yaml';
|
|
33
34
|
import { getRecentLogs, LOG_PATH } from '../logging/logger.js';
|
|
34
35
|
import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
|
|
35
36
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
36
|
-
import { resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
37
|
+
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
37
38
|
import { buildSessionEntries } from '../status/builders.js';
|
|
38
39
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
39
40
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
@@ -61,8 +62,78 @@ function resolveUpgradeChannel(args: any): ReleaseChannel {
|
|
|
61
62
|
|| normalizeReleaseChannel(loadConfig().updateChannel)
|
|
62
63
|
|| 'stable';
|
|
63
64
|
}
|
|
65
|
+
|
|
66
|
+
function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
67
|
+
const record = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
68
|
+
? policy as Record<string, unknown>
|
|
69
|
+
: {};
|
|
70
|
+
const raw = record.providerPriority;
|
|
71
|
+
if (!Array.isArray(raw)) return [];
|
|
72
|
+
const seen = new Set<string>();
|
|
73
|
+
return raw
|
|
74
|
+
.map(type => typeof type === 'string' ? type.trim() : '')
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.filter(type => {
|
|
77
|
+
if (seen.has(type)) return false;
|
|
78
|
+
seen.add(type);
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function resolveProviderTypeFromPriority(args: {
|
|
84
|
+
nodeId: string;
|
|
85
|
+
providerPriority: string[];
|
|
86
|
+
providerLoader: ProviderLoader;
|
|
87
|
+
onStatusChange?: () => void;
|
|
88
|
+
}): Promise<{ providerType?: string; error?: string }> {
|
|
89
|
+
if (!args.providerPriority.length) {
|
|
90
|
+
return { error: `Node '${args.nodeId}' has no providerPriority policy; pass cliType explicitly or configure node.policy.providerPriority` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const failed: string[] = [];
|
|
94
|
+
for (const requestedType of args.providerPriority) {
|
|
95
|
+
const normalizedType = args.providerLoader.resolveAlias(requestedType);
|
|
96
|
+
if (!args.providerLoader.isMachineProviderEnabled(normalizedType)) {
|
|
97
|
+
failed.push(`${requestedType}: disabled`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const detected = await detectCLI(normalizedType, args.providerLoader, { includeVersion: false });
|
|
101
|
+
args.providerLoader.setCliDetectionResults([{
|
|
102
|
+
id: normalizedType,
|
|
103
|
+
installed: !!detected,
|
|
104
|
+
path: detected?.path,
|
|
105
|
+
}], false);
|
|
106
|
+
args.onStatusChange?.();
|
|
107
|
+
if (detected) return { providerType: normalizedType };
|
|
108
|
+
failed.push(`${requestedType}: not detected`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
|
|
112
|
+
}
|
|
64
113
|
import * as fs from 'fs';
|
|
65
114
|
|
|
115
|
+
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
116
|
+
|
|
117
|
+
function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
|
|
118
|
+
return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
|
|
122
|
+
return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
|
|
126
|
+
if (!text.trim()) return {};
|
|
127
|
+
if (format === 'claude_mcp_json') return JSON.parse(text);
|
|
128
|
+
const parsed = loadYamlModule().load(text);
|
|
129
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
|
|
133
|
+
if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
|
|
134
|
+
return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
|
|
135
|
+
}
|
|
136
|
+
|
|
66
137
|
// ─── Types ───
|
|
67
138
|
|
|
68
139
|
export interface SessionHostControlPlane {
|
|
@@ -226,6 +297,43 @@ export class DaemonCommandRouter {
|
|
|
226
297
|
this.deps = deps;
|
|
227
298
|
}
|
|
228
299
|
|
|
300
|
+
private getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
301
|
+
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
302
|
+
this.inlineMeshCache.set(meshId, inlineMesh as any);
|
|
303
|
+
return inlineMesh as any;
|
|
304
|
+
}
|
|
305
|
+
return this.inlineMeshCache.get(meshId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async getMeshForCommand(meshId: string, inlineMesh?: unknown): Promise<{ mesh: any; inline: boolean } | null> {
|
|
309
|
+
try {
|
|
310
|
+
const { getMesh } = await import('../config/mesh-config.js');
|
|
311
|
+
const mesh = getMesh(meshId);
|
|
312
|
+
if (mesh) return { mesh, inline: false };
|
|
313
|
+
} catch { /* fall through to inline cache */ }
|
|
314
|
+
const cached = this.getCachedInlineMesh(meshId, inlineMesh);
|
|
315
|
+
return cached ? { mesh: cached, inline: true } : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
319
|
+
if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
|
|
320
|
+
const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
|
|
321
|
+
if (idx >= 0) mesh.nodes[idx] = node;
|
|
322
|
+
else mesh.nodes.push(node);
|
|
323
|
+
mesh.updatedAt = new Date().toISOString();
|
|
324
|
+
this.inlineMeshCache.set(meshId, mesh);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
328
|
+
if (!mesh || !Array.isArray(mesh.nodes)) return false;
|
|
329
|
+
const idx = mesh.nodes.findIndex((entry: any) => entry?.id === nodeId || entry?.nodeId === nodeId);
|
|
330
|
+
if (idx === -1) return false;
|
|
331
|
+
mesh.nodes.splice(idx, 1);
|
|
332
|
+
mesh.updatedAt = new Date().toISOString();
|
|
333
|
+
this.inlineMeshCache.set(meshId, mesh);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
229
337
|
private async traceSessionHostAction<T>(
|
|
230
338
|
action: string,
|
|
231
339
|
args: any,
|
|
@@ -1009,7 +1117,15 @@ export class DaemonCommandRouter {
|
|
|
1009
1117
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
1010
1118
|
try {
|
|
1011
1119
|
const { addNode } = await import('../config/mesh-config.js');
|
|
1012
|
-
const
|
|
1120
|
+
const providerPriority = Array.isArray(args?.providerPriority)
|
|
1121
|
+
? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
|
|
1122
|
+
: [];
|
|
1123
|
+
const readOnly = args?.readOnly === true;
|
|
1124
|
+
const policy = {
|
|
1125
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
1126
|
+
...(providerPriority.length ? { providerPriority } : {}),
|
|
1127
|
+
};
|
|
1128
|
+
const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
|
|
1013
1129
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
1014
1130
|
return { success: true, node };
|
|
1015
1131
|
} catch (e: any) {
|
|
@@ -1022,18 +1138,111 @@ export class DaemonCommandRouter {
|
|
|
1022
1138
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1023
1139
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
1024
1140
|
try {
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1141
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
1142
|
+
const mesh = meshRecord?.mesh;
|
|
1143
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
1144
|
+
|
|
1145
|
+
// If this is a worktree node, clean up the git worktree first
|
|
1146
|
+
if (node?.isLocalWorktree && node.workspace) {
|
|
1147
|
+
try {
|
|
1148
|
+
const sourceNode = node.clonedFromNodeId
|
|
1149
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
1150
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
1151
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
1152
|
+
if (repoRoot) {
|
|
1153
|
+
const { removeWorktree } = await import('../git/git-worktree.js');
|
|
1154
|
+
await removeWorktree(repoRoot, node.workspace);
|
|
1155
|
+
}
|
|
1156
|
+
} catch (e: any) {
|
|
1157
|
+
LOG.warn('MeshNode', `Worktree cleanup failed for ${nodeId}: ${e.message}`);
|
|
1158
|
+
// Continue with node removal even if worktree cleanup fails
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
let removed = false;
|
|
1163
|
+
if (meshRecord?.inline) {
|
|
1164
|
+
removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
|
|
1165
|
+
} else {
|
|
1166
|
+
const { removeNode } = await import('../config/mesh-config.js');
|
|
1167
|
+
removed = removeNode(meshId, nodeId);
|
|
1168
|
+
}
|
|
1027
1169
|
return { success: true, removed };
|
|
1028
1170
|
} catch (e: any) {
|
|
1029
1171
|
return { success: false, error: e.message };
|
|
1030
1172
|
}
|
|
1031
1173
|
}
|
|
1032
1174
|
|
|
1175
|
+
case 'clone_mesh_node': {
|
|
1176
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1177
|
+
const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
|
|
1178
|
+
const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
|
|
1179
|
+
const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
|
|
1180
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1181
|
+
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
1182
|
+
if (!branch) return { success: false, error: 'branch required' };
|
|
1183
|
+
|
|
1184
|
+
try {
|
|
1185
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
1186
|
+
const mesh = meshRecord?.mesh;
|
|
1187
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1188
|
+
|
|
1189
|
+
const sourceNode = mesh.nodes?.find((n: any) => n.id === sourceNodeId || n.nodeId === sourceNodeId);
|
|
1190
|
+
if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
|
|
1191
|
+
|
|
1192
|
+
const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
|
|
1193
|
+
const { createWorktree } = await import('../git/git-worktree.js');
|
|
1194
|
+
const result = await createWorktree({
|
|
1195
|
+
repoRoot,
|
|
1196
|
+
branch,
|
|
1197
|
+
baseBranch,
|
|
1198
|
+
meshName: mesh.name,
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
let node: any;
|
|
1202
|
+
if (meshRecord.inline) {
|
|
1203
|
+
const { randomUUID } = await import('crypto');
|
|
1204
|
+
node = {
|
|
1205
|
+
id: `node_${randomUUID().replace(/-/g, '')}`,
|
|
1206
|
+
workspace: result.worktreePath,
|
|
1207
|
+
repoRoot: result.worktreePath,
|
|
1208
|
+
daemonId: sourceNode.daemonId,
|
|
1209
|
+
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
1210
|
+
policy: { ...(sourceNode.policy || {}) },
|
|
1211
|
+
isLocalWorktree: true,
|
|
1212
|
+
worktreeBranch: result.branch,
|
|
1213
|
+
clonedFromNodeId: sourceNodeId,
|
|
1214
|
+
};
|
|
1215
|
+
this.updateInlineMeshNode(meshId, mesh, node);
|
|
1216
|
+
} else {
|
|
1217
|
+
const { addNode } = await import('../config/mesh-config.js');
|
|
1218
|
+
node = addNode(meshId, {
|
|
1219
|
+
workspace: result.worktreePath,
|
|
1220
|
+
repoRoot: result.worktreePath,
|
|
1221
|
+
daemonId: sourceNode.daemonId,
|
|
1222
|
+
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
1223
|
+
isLocalWorktree: true,
|
|
1224
|
+
worktreeBranch: result.branch,
|
|
1225
|
+
clonedFromNodeId: sourceNodeId,
|
|
1226
|
+
policy: { ...(sourceNode.policy || {}) },
|
|
1227
|
+
});
|
|
1228
|
+
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return {
|
|
1232
|
+
success: true,
|
|
1233
|
+
node,
|
|
1234
|
+
worktreePath: result.worktreePath,
|
|
1235
|
+
branch: result.branch,
|
|
1236
|
+
};
|
|
1237
|
+
} catch (e: any) {
|
|
1238
|
+
return { success: false, error: e.message };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1033
1242
|
// ─── Mesh Coordinator Launch ───
|
|
1034
1243
|
case 'launch_mesh_coordinator': {
|
|
1035
1244
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1036
|
-
|
|
1245
|
+
let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
|
|
1037
1246
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1038
1247
|
|
|
1039
1248
|
try {
|
|
@@ -1071,9 +1280,29 @@ export class DaemonCommandRouter {
|
|
|
1071
1280
|
}
|
|
1072
1281
|
const workspace = typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '';
|
|
1073
1282
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
1283
|
+
if (!cliType) {
|
|
1284
|
+
const resolved = await resolveProviderTypeFromPriority({
|
|
1285
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || 'coordinator'),
|
|
1286
|
+
providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
|
|
1287
|
+
providerLoader: this.deps.providerLoader,
|
|
1288
|
+
onStatusChange: this.deps.onStatusChange,
|
|
1289
|
+
});
|
|
1290
|
+
if (!resolved.providerType) {
|
|
1291
|
+
return {
|
|
1292
|
+
success: false,
|
|
1293
|
+
code: 'mesh_coordinator_provider_priority_unusable',
|
|
1294
|
+
error: resolved.error || 'No usable provider found from node providerPriority',
|
|
1295
|
+
meshId,
|
|
1296
|
+
cliType,
|
|
1297
|
+
workspace,
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
cliType = resolved.providerType;
|
|
1301
|
+
}
|
|
1074
1302
|
const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
|
|
1075
1303
|
const coordinatorSetup = resolveMeshCoordinatorSetup({
|
|
1076
1304
|
provider: providerMeta,
|
|
1305
|
+
cliType,
|
|
1077
1306
|
meshId,
|
|
1078
1307
|
workspace,
|
|
1079
1308
|
});
|
|
@@ -1101,7 +1330,8 @@ export class DaemonCommandRouter {
|
|
|
1101
1330
|
};
|
|
1102
1331
|
}
|
|
1103
1332
|
|
|
1104
|
-
|
|
1333
|
+
const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
|
|
1334
|
+
if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
|
|
1105
1335
|
return {
|
|
1106
1336
|
success: false,
|
|
1107
1337
|
code: 'mesh_coordinator_unsupported',
|
|
@@ -1112,20 +1342,42 @@ export class DaemonCommandRouter {
|
|
|
1112
1342
|
};
|
|
1113
1343
|
}
|
|
1114
1344
|
|
|
1115
|
-
//
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1345
|
+
// Build the coordinator prompt before mutating workspace config or launching.
|
|
1346
|
+
// Prompt generation failures are configuration/data-shape errors; fail closed so
|
|
1347
|
+
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
1348
|
+
let systemPrompt = '';
|
|
1349
|
+
try {
|
|
1350
|
+
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
1351
|
+
} catch (error: any) {
|
|
1352
|
+
const message = error?.message || String(error);
|
|
1353
|
+
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
1354
|
+
return {
|
|
1355
|
+
success: false,
|
|
1356
|
+
code: 'mesh_coordinator_prompt_failed',
|
|
1357
|
+
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
1358
|
+
meshId,
|
|
1359
|
+
cliType,
|
|
1360
|
+
workspace,
|
|
1361
|
+
};
|
|
1127
1362
|
}
|
|
1128
1363
|
|
|
1364
|
+
// 1. Write provider-declared MCP config for CLIs that auto-import it.
|
|
1365
|
+
const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
|
|
1366
|
+
const { dirname } = await import('path');
|
|
1367
|
+
const mcpConfigPath = coordinatorSetup.configPath;
|
|
1368
|
+
const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
|
|
1369
|
+
? createHermesManualMeshCoordinatorSetup(meshId, workspace)
|
|
1370
|
+
: null;
|
|
1371
|
+
const returnManualFallback = (message: string) => ({
|
|
1372
|
+
success: false,
|
|
1373
|
+
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
1374
|
+
error: message,
|
|
1375
|
+
meshId,
|
|
1376
|
+
cliType,
|
|
1377
|
+
workspace,
|
|
1378
|
+
meshCoordinatorSetup: hermesManualFallback,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1129
1381
|
// Merge ADHDev mesh server into existing config.
|
|
1130
1382
|
// Pass full mesh data as env var so the MCP server can bootstrap
|
|
1131
1383
|
// without depending on meshes.json or a running daemon.
|
|
@@ -1136,41 +1388,76 @@ export class DaemonCommandRouter {
|
|
|
1136
1388
|
if (args?.inlineMesh) {
|
|
1137
1389
|
mcpServerEntry.env = {
|
|
1138
1390
|
ADHDEV_INLINE_MESH: JSON.stringify(mesh),
|
|
1391
|
+
ADHDEV_MCP_TRANSPORT: 'ipc',
|
|
1139
1392
|
};
|
|
1140
1393
|
}
|
|
1394
|
+
|
|
1395
|
+
try {
|
|
1396
|
+
mkdirSync(dirname(mcpConfigPath), { recursive: true });
|
|
1397
|
+
} catch (error: any) {
|
|
1398
|
+
const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
|
|
1399
|
+
LOG.error('MeshCoordinator', message);
|
|
1400
|
+
if (hermesManualFallback) return returnManualFallback(message);
|
|
1401
|
+
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Backup existing MCP config if present.
|
|
1405
|
+
const hadExistingMcpConfig = existsSync(mcpConfigPath);
|
|
1406
|
+
let existingMcpConfig: Record<string, any> = {};
|
|
1407
|
+
if (hadExistingMcpConfig) {
|
|
1408
|
+
try {
|
|
1409
|
+
existingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
|
|
1410
|
+
copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
|
|
1411
|
+
} catch (error: any) {
|
|
1412
|
+
LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
|
|
1413
|
+
return {
|
|
1414
|
+
success: false,
|
|
1415
|
+
code: 'mesh_coordinator_config_parse_failed',
|
|
1416
|
+
error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const mcpServersKey = getMcpServersKey(configFormat);
|
|
1422
|
+
const existingServers = existingMcpConfig[mcpServersKey];
|
|
1141
1423
|
const mcpConfig = {
|
|
1142
1424
|
...existingMcpConfig,
|
|
1143
|
-
|
|
1144
|
-
...(
|
|
1425
|
+
[mcpServersKey]: {
|
|
1426
|
+
...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
|
|
1145
1427
|
[coordinatorSetup.serverName]: mcpServerEntry,
|
|
1146
1428
|
},
|
|
1147
1429
|
};
|
|
1148
|
-
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
1149
|
-
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
1150
|
-
|
|
1151
|
-
// 2. Build coordinator system prompt
|
|
1152
|
-
let systemPrompt = '';
|
|
1153
1430
|
try {
|
|
1154
|
-
|
|
1155
|
-
} catch {
|
|
1156
|
-
|
|
1431
|
+
writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
|
|
1432
|
+
} catch (error: any) {
|
|
1433
|
+
const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
|
|
1434
|
+
LOG.error('MeshCoordinator', message);
|
|
1435
|
+
if (hermesManualFallback) return returnManualFallback(message);
|
|
1436
|
+
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
1157
1437
|
}
|
|
1438
|
+
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
1158
1439
|
|
|
1159
1440
|
const cliArgs: string[] = [];
|
|
1441
|
+
const launchEnv: Record<string, string> = {};
|
|
1160
1442
|
if (systemPrompt) {
|
|
1161
|
-
|
|
1443
|
+
if (configFormat === 'hermes_config_yaml') {
|
|
1444
|
+
launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
|
|
1445
|
+
} else {
|
|
1446
|
+
cliArgs.push('--append-system-prompt', systemPrompt);
|
|
1447
|
+
}
|
|
1162
1448
|
}
|
|
1163
1449
|
if (cliType === 'claude-cli') {
|
|
1164
1450
|
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
1165
1451
|
}
|
|
1166
1452
|
|
|
1167
|
-
// 3. Launch CLI session via existing cliManager
|
|
1168
|
-
//
|
|
1169
|
-
// CLI
|
|
1453
|
+
// 3. Launch CLI session via existing cliManager.
|
|
1454
|
+
// Provider-specific prompt injection remains fail-closed: Claude gets
|
|
1455
|
+
// explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
|
|
1170
1456
|
const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
1171
1457
|
cliType,
|
|
1172
1458
|
dir: workspace,
|
|
1173
1459
|
cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
|
|
1460
|
+
env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
|
|
1174
1461
|
settings: {
|
|
1175
1462
|
meshCoordinatorFor: meshId
|
|
1176
1463
|
}
|