@adhdev/daemon-core 0.9.76-rc.6 → 0.9.76-rc.61
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 +11 -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.d.ts +3 -3
- package/dist/index.js +1690 -447
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1703 -478
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +1 -0
- package/dist/mesh/mesh-events.d.ts +9 -0
- package/dist/providers/chat-message-normalization.d.ts +40 -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 +27 -0
- package/dist/session-host/runtime-support.d.ts +2 -1
- package/dist/shared-types.d.ts +4 -0
- package/dist/types.d.ts +9 -0
- package/package.json +4 -5
- package/src/chat/subscription-updates.ts +3 -1
- package/src/cli-adapters/provider-cli-adapter.ts +28 -7
- package/src/cli-adapters/provider-cli-runtime.ts +3 -2
- package/src/commands/chat-commands.ts +126 -11
- package/src/commands/cli-manager.ts +78 -5
- package/src/commands/handler.ts +13 -4
- package/src/commands/mesh-coordinator.ts +148 -5
- package/src/commands/router.d.ts +1 -0
- package/src/commands/router.ts +553 -34
- package/src/config/mesh-config.ts +23 -2
- 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/index.ts +16 -1
- package/src/mesh/coordinator-prompt.ts +29 -14
- package/src/mesh/mesh-events.ts +109 -43
- package/src/providers/chat-message-normalization.ts +241 -0
- package/src/providers/cli-provider-instance.d.ts +2 -0
- package/src/providers/cli-provider-instance.ts +93 -8
- package/src/providers/provider-instance-manager.ts +20 -1
- package/src/providers/provider-instance.ts +2 -0
- package/src/providers/read-chat-contract.ts +8 -0
- package/src/repo-mesh-types.ts +30 -0
- package/src/session-host/runtime-support.ts +55 -7
- package/src/shared-types.ts +4 -0
- package/src/status/builders.ts +17 -12
- package/src/status/reporter.ts +6 -0
- package/src/types.ts +9 -0
package/src/commands/router.ts
CHANGED
|
@@ -30,14 +30,17 @@ 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';
|
|
39
|
+
import { handleMeshForwardEvent } from '../mesh/mesh-events.js';
|
|
38
40
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
39
41
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
40
42
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
43
|
+
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
41
44
|
|
|
42
45
|
type ReleaseChannel = 'stable' | 'preview';
|
|
43
46
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
@@ -61,14 +64,85 @@ function resolveUpgradeChannel(args: any): ReleaseChannel {
|
|
|
61
64
|
|| normalizeReleaseChannel(loadConfig().updateChannel)
|
|
62
65
|
|| 'stable';
|
|
63
66
|
}
|
|
67
|
+
|
|
68
|
+
function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
69
|
+
const record = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
70
|
+
? policy as Record<string, unknown>
|
|
71
|
+
: {};
|
|
72
|
+
const raw = record.providerPriority;
|
|
73
|
+
if (!Array.isArray(raw)) return [];
|
|
74
|
+
const seen = new Set<string>();
|
|
75
|
+
return raw
|
|
76
|
+
.map(type => typeof type === 'string' ? type.trim() : '')
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.filter(type => {
|
|
79
|
+
if (seen.has(type)) return false;
|
|
80
|
+
seen.add(type);
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function resolveProviderTypeFromPriority(args: {
|
|
86
|
+
nodeId: string;
|
|
87
|
+
providerPriority: string[];
|
|
88
|
+
providerLoader: ProviderLoader;
|
|
89
|
+
onStatusChange?: () => void;
|
|
90
|
+
}): Promise<{ providerType?: string; error?: string }> {
|
|
91
|
+
if (!args.providerPriority.length) {
|
|
92
|
+
return { error: `Node '${args.nodeId}' has no providerPriority policy; pass cliType explicitly or configure node.policy.providerPriority` };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const failed: string[] = [];
|
|
96
|
+
for (const requestedType of args.providerPriority) {
|
|
97
|
+
const normalizedType = args.providerLoader.resolveAlias(requestedType);
|
|
98
|
+
if (!args.providerLoader.isMachineProviderEnabled(normalizedType)) {
|
|
99
|
+
failed.push(`${requestedType}: disabled`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const detected = await detectCLI(normalizedType, args.providerLoader, { includeVersion: false });
|
|
103
|
+
args.providerLoader.setCliDetectionResults([{
|
|
104
|
+
id: normalizedType,
|
|
105
|
+
installed: !!detected,
|
|
106
|
+
path: detected?.path,
|
|
107
|
+
}], false);
|
|
108
|
+
args.onStatusChange?.();
|
|
109
|
+
if (detected) return { providerType: normalizedType };
|
|
110
|
+
failed.push(`${requestedType}: not detected`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
|
|
114
|
+
}
|
|
64
115
|
import * as fs from 'fs';
|
|
65
116
|
|
|
117
|
+
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
118
|
+
|
|
119
|
+
function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
|
|
120
|
+
return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
|
|
124
|
+
return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
|
|
128
|
+
if (!text.trim()) return {};
|
|
129
|
+
if (format === 'claude_mcp_json') return JSON.parse(text);
|
|
130
|
+
const parsed = loadYamlModule().load(text);
|
|
131
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
|
|
135
|
+
if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
|
|
136
|
+
return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
|
|
137
|
+
}
|
|
138
|
+
|
|
66
139
|
// ─── Types ───
|
|
67
140
|
|
|
68
141
|
export interface SessionHostControlPlane {
|
|
69
142
|
getDiagnostics(payload?: { includeSessions?: boolean; limit?: number }): Promise<any>;
|
|
70
143
|
listSessions(): Promise<any[]>;
|
|
71
144
|
stopSession(sessionId: string): Promise<any>;
|
|
145
|
+
deleteSession(sessionId: string, opts?: { force?: boolean }): Promise<any>;
|
|
72
146
|
resumeSession(sessionId: string): Promise<any>;
|
|
73
147
|
restartSession(sessionId: string): Promise<any>;
|
|
74
148
|
sendSignal(sessionId: string, signal: string): Promise<any>;
|
|
@@ -226,6 +300,184 @@ export class DaemonCommandRouter {
|
|
|
226
300
|
this.deps = deps;
|
|
227
301
|
}
|
|
228
302
|
|
|
303
|
+
private getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
304
|
+
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
305
|
+
this.inlineMeshCache.set(meshId, inlineMesh as any);
|
|
306
|
+
return inlineMesh as any;
|
|
307
|
+
}
|
|
308
|
+
return this.inlineMeshCache.get(meshId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async getMeshForCommand(meshId: string, inlineMesh?: unknown): Promise<{ mesh: any; inline: boolean } | null> {
|
|
312
|
+
try {
|
|
313
|
+
const { getMesh } = await import('../config/mesh-config.js');
|
|
314
|
+
const mesh = getMesh(meshId);
|
|
315
|
+
if (mesh) return { mesh, inline: false };
|
|
316
|
+
} catch { /* fall through to inline cache */ }
|
|
317
|
+
const cached = this.getCachedInlineMesh(meshId, inlineMesh);
|
|
318
|
+
return cached ? { mesh: cached, inline: true } : null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
322
|
+
if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
|
|
323
|
+
const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
|
|
324
|
+
if (idx >= 0) mesh.nodes[idx] = node;
|
|
325
|
+
else mesh.nodes.push(node);
|
|
326
|
+
mesh.updatedAt = new Date().toISOString();
|
|
327
|
+
this.inlineMeshCache.set(meshId, mesh);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
331
|
+
if (!mesh || !Array.isArray(mesh.nodes)) return false;
|
|
332
|
+
const idx = mesh.nodes.findIndex((entry: any) => entry?.id === nodeId || entry?.nodeId === nodeId);
|
|
333
|
+
if (idx === -1) return false;
|
|
334
|
+
mesh.nodes.splice(idx, 1);
|
|
335
|
+
mesh.updatedAt = new Date().toISOString();
|
|
336
|
+
this.inlineMeshCache.set(meshId, mesh);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private normalizeMeshSessionCleanupMode(value: unknown): RepoMeshSessionCleanupMode {
|
|
341
|
+
return value === 'stop'
|
|
342
|
+
|| value === 'delete_stopped'
|
|
343
|
+
|| value === 'stop_and_delete'
|
|
344
|
+
|| value === 'preserve'
|
|
345
|
+
? value
|
|
346
|
+
: 'preserve';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private sessionMatchesMeshNode(record: any, node: any, nodeId: string, sessionIds?: Set<string>): boolean {
|
|
350
|
+
const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : '';
|
|
351
|
+
if (!sessionId) return false;
|
|
352
|
+
if (sessionIds?.size) return sessionIds.has(sessionId);
|
|
353
|
+
const workspace = typeof node?.workspace === 'string' ? node.workspace : '';
|
|
354
|
+
if (workspace && record?.workspace === workspace) return true;
|
|
355
|
+
if (record?.meta?.meshNodeId === nodeId) return true;
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private isCompletedHostedSession(record: any): boolean {
|
|
360
|
+
return record?.lifecycle === 'stopped' || record?.lifecycle === 'failed' || record?.lifecycle === 'interrupted';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private async cleanupMeshSessions(args: {
|
|
364
|
+
meshId: string;
|
|
365
|
+
nodeId: string;
|
|
366
|
+
node: any;
|
|
367
|
+
mode: RepoMeshSessionCleanupMode;
|
|
368
|
+
sessionIds?: string[];
|
|
369
|
+
dryRun?: boolean;
|
|
370
|
+
}): Promise<{ success: boolean; [key: string]: unknown }> {
|
|
371
|
+
if (args.mode === 'preserve') {
|
|
372
|
+
return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
|
|
373
|
+
}
|
|
374
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
375
|
+
|
|
376
|
+
const requestedSessionIds = Array.isArray(args.sessionIds)
|
|
377
|
+
? new Set(args.sessionIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean))
|
|
378
|
+
: undefined;
|
|
379
|
+
const sessions = await this.deps.sessionHostControl.listSessions();
|
|
380
|
+
const matched = sessions.filter(record => this.sessionMatchesMeshNode(record, args.node, args.nodeId, requestedSessionIds));
|
|
381
|
+
const hasExplicitSessionIds = !!requestedSessionIds?.size;
|
|
382
|
+
const stoppedSessionIds: string[] = [];
|
|
383
|
+
const deletedSessionIds: string[] = [];
|
|
384
|
+
const skippedSessionIds: string[] = [];
|
|
385
|
+
const skippedLiveSessionIds: string[] = [];
|
|
386
|
+
const deleteUnsupportedSessionIds: string[] = [];
|
|
387
|
+
const recordsRemainSessionIds: string[] = [];
|
|
388
|
+
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
389
|
+
const matchedBySurfaceKind = {
|
|
390
|
+
live_runtime: 0,
|
|
391
|
+
recovery_snapshot: 0,
|
|
392
|
+
inactive_record: 0,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
for (const record of matched) {
|
|
396
|
+
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
397
|
+
matchedBySurfaceKind[surfaceKind] += 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const record of matched) {
|
|
401
|
+
const sessionId = String(record.sessionId);
|
|
402
|
+
const completed = this.isCompletedHostedSession(record);
|
|
403
|
+
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
404
|
+
const liveRuntime = surfaceKind === 'live_runtime';
|
|
405
|
+
if (!hasExplicitSessionIds && liveRuntime) {
|
|
406
|
+
skippedSessionIds.push(sessionId);
|
|
407
|
+
skippedLiveSessionIds.push(sessionId);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
if (args.mode === 'stop') {
|
|
412
|
+
if (!completed) {
|
|
413
|
+
if (!args.dryRun) await this.deps.sessionHostControl.stopSession(sessionId);
|
|
414
|
+
stoppedSessionIds.push(sessionId);
|
|
415
|
+
} else {
|
|
416
|
+
skippedSessionIds.push(sessionId);
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (args.mode === 'delete_stopped') {
|
|
422
|
+
if (completed) {
|
|
423
|
+
if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: false });
|
|
424
|
+
deletedSessionIds.push(sessionId);
|
|
425
|
+
} else {
|
|
426
|
+
skippedSessionIds.push(sessionId);
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (args.mode === 'stop_and_delete') {
|
|
432
|
+
if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: true });
|
|
433
|
+
deletedSessionIds.push(sessionId);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
} catch (e: any) {
|
|
437
|
+
const message = e?.message || String(e);
|
|
438
|
+
if (message.includes('Unsupported session host request: delete_session')
|
|
439
|
+
&& (args.mode === 'delete_stopped' || args.mode === 'stop_and_delete')) {
|
|
440
|
+
deleteUnsupportedSessionIds.push(sessionId);
|
|
441
|
+
recordsRemainSessionIds.push(sessionId);
|
|
442
|
+
if (args.mode === 'stop_and_delete' && !completed) {
|
|
443
|
+
try {
|
|
444
|
+
await this.deps.sessionHostControl.stopSession(sessionId);
|
|
445
|
+
stoppedSessionIds.push(sessionId);
|
|
446
|
+
} catch (stopError: any) {
|
|
447
|
+
errors.push({ sessionId, error: stopError?.message || String(stopError) });
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
skippedSessionIds.push(sessionId);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
errors.push({ sessionId, error: message });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const deleteUnsupported = deleteUnsupportedSessionIds.length > 0;
|
|
459
|
+
return {
|
|
460
|
+
success: errors.length === 0,
|
|
461
|
+
mode: args.mode,
|
|
462
|
+
dryRun: args.dryRun === true,
|
|
463
|
+
matchedCount: matched.length,
|
|
464
|
+
matchedBySurfaceKind,
|
|
465
|
+
stoppedSessionIds,
|
|
466
|
+
deletedSessionIds,
|
|
467
|
+
skippedSessionIds,
|
|
468
|
+
skippedLiveSessionIds,
|
|
469
|
+
...(deleteUnsupported ? {
|
|
470
|
+
deleteUnsupported: true,
|
|
471
|
+
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
472
|
+
? 'stopped_only_records_remain'
|
|
473
|
+
: 'delete_unsupported_records_remain',
|
|
474
|
+
deleteUnsupportedSessionIds,
|
|
475
|
+
recordsRemainSessionIds,
|
|
476
|
+
} : {}),
|
|
477
|
+
...(errors.length ? { errors } : {}),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
229
481
|
private async traceSessionHostAction<T>(
|
|
230
482
|
action: string,
|
|
231
483
|
args: any,
|
|
@@ -363,6 +615,10 @@ export class DaemonCommandRouter {
|
|
|
363
615
|
private async executeDaemonCommand(cmd: string, args: any): Promise<CommandRouterResult | null> {
|
|
364
616
|
switch (cmd) {
|
|
365
617
|
// ─── CLI / ACP commands ───
|
|
618
|
+
case 'mesh_forward_event': {
|
|
619
|
+
return handleMeshForwardEvent({ instanceManager: this.deps.instanceManager } as any, args as Record<string, unknown>);
|
|
620
|
+
}
|
|
621
|
+
|
|
366
622
|
case 'launch_cli':
|
|
367
623
|
case 'stop_cli':
|
|
368
624
|
case 'set_cli_view_mode':
|
|
@@ -983,7 +1239,27 @@ export class DaemonCommandRouter {
|
|
|
983
1239
|
if (!name) return { success: false, error: 'name required' };
|
|
984
1240
|
try {
|
|
985
1241
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
986
|
-
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch });
|
|
1242
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy });
|
|
1243
|
+
return { success: true, mesh };
|
|
1244
|
+
} catch (e: any) {
|
|
1245
|
+
return { success: false, error: e.message };
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
case 'update_mesh': {
|
|
1250
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1251
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1252
|
+
try {
|
|
1253
|
+
const { updateMesh } = await import('../config/mesh-config.js');
|
|
1254
|
+
const patch: Record<string, unknown> = {};
|
|
1255
|
+
if (typeof args?.name === 'string') patch.name = args.name;
|
|
1256
|
+
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
1257
|
+
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
1258
|
+
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
1259
|
+
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
1260
|
+
const mesh = updateMesh(meshId, patch as any);
|
|
1261
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1262
|
+
this.inlineMeshCache.set(meshId, mesh);
|
|
987
1263
|
return { success: true, mesh };
|
|
988
1264
|
} catch (e: any) {
|
|
989
1265
|
return { success: false, error: e.message };
|
|
@@ -1009,7 +1285,15 @@ export class DaemonCommandRouter {
|
|
|
1009
1285
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
1010
1286
|
try {
|
|
1011
1287
|
const { addNode } = await import('../config/mesh-config.js');
|
|
1012
|
-
const
|
|
1288
|
+
const providerPriority = Array.isArray(args?.providerPriority)
|
|
1289
|
+
? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
|
|
1290
|
+
: [];
|
|
1291
|
+
const readOnly = args?.readOnly === true;
|
|
1292
|
+
const policy = {
|
|
1293
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
1294
|
+
...(providerPriority.length ? { providerPriority } : {}),
|
|
1295
|
+
};
|
|
1296
|
+
const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
|
|
1013
1297
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
1014
1298
|
return { success: true, node };
|
|
1015
1299
|
} catch (e: any) {
|
|
@@ -1017,14 +1301,172 @@ export class DaemonCommandRouter {
|
|
|
1017
1301
|
}
|
|
1018
1302
|
}
|
|
1019
1303
|
|
|
1304
|
+
case 'update_mesh_node': {
|
|
1305
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1306
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1307
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
1308
|
+
try {
|
|
1309
|
+
const { updateNode } = await import('../config/mesh-config.js');
|
|
1310
|
+
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
1311
|
+
? { ...(args.policy as Record<string, unknown>) }
|
|
1312
|
+
: {};
|
|
1313
|
+
if (Array.isArray(args?.providerPriority)) {
|
|
1314
|
+
const providerPriority = args.providerPriority
|
|
1315
|
+
.map((type: any) => typeof type === 'string' ? type.trim() : '')
|
|
1316
|
+
.filter(Boolean);
|
|
1317
|
+
delete (policy as any).provider_priority;
|
|
1318
|
+
if (providerPriority.length) {
|
|
1319
|
+
(policy as any).providerPriority = providerPriority;
|
|
1320
|
+
} else {
|
|
1321
|
+
delete (policy as any).providerPriority;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const node = updateNode(meshId, nodeId, { policy: policy as any });
|
|
1325
|
+
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
1326
|
+
return { success: true, node };
|
|
1327
|
+
} catch (e: any) {
|
|
1328
|
+
return { success: false, error: e.message };
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
case 'cleanup_mesh_sessions': {
|
|
1333
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1334
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1335
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
1336
|
+
try {
|
|
1337
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
1338
|
+
const mesh = meshRecord?.mesh;
|
|
1339
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1340
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
1341
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
1342
|
+
const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
|
|
1343
|
+
const sessionIds = Array.isArray(args?.sessionIds)
|
|
1344
|
+
? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
|
|
1345
|
+
: undefined;
|
|
1346
|
+
const result = await this.cleanupMeshSessions({
|
|
1347
|
+
meshId,
|
|
1348
|
+
nodeId,
|
|
1349
|
+
node,
|
|
1350
|
+
mode,
|
|
1351
|
+
sessionIds,
|
|
1352
|
+
dryRun: args?.dryRun === true,
|
|
1353
|
+
});
|
|
1354
|
+
return result;
|
|
1355
|
+
} catch (e: any) {
|
|
1356
|
+
return { success: false, error: e.message };
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1020
1360
|
case 'remove_mesh_node': {
|
|
1021
1361
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1022
1362
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1023
1363
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
1024
1364
|
try {
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1365
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
1366
|
+
const mesh = meshRecord?.mesh;
|
|
1367
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
1368
|
+
|
|
1369
|
+
const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
|
|
1370
|
+
args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
|
|
1371
|
+
);
|
|
1372
|
+
let sessionCleanup: Record<string, unknown> | undefined;
|
|
1373
|
+
if (node && sessionCleanupMode !== 'preserve') {
|
|
1374
|
+
sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode });
|
|
1375
|
+
if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// If this is a worktree node, clean up the git worktree first
|
|
1379
|
+
if (node?.isLocalWorktree && node.workspace) {
|
|
1380
|
+
try {
|
|
1381
|
+
const sourceNode = node.clonedFromNodeId
|
|
1382
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
1383
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
1384
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
1385
|
+
if (repoRoot) {
|
|
1386
|
+
const { removeWorktree } = await import('../git/git-worktree.js');
|
|
1387
|
+
await removeWorktree(repoRoot, node.workspace);
|
|
1388
|
+
}
|
|
1389
|
+
} catch (e: any) {
|
|
1390
|
+
LOG.warn('MeshNode', `Worktree cleanup failed for ${nodeId}: ${e.message}`);
|
|
1391
|
+
// Continue with node removal even if worktree cleanup fails
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
let removed = false;
|
|
1396
|
+
if (meshRecord?.inline) {
|
|
1397
|
+
removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
|
|
1398
|
+
} else {
|
|
1399
|
+
const { removeNode } = await import('../config/mesh-config.js');
|
|
1400
|
+
removed = removeNode(meshId, nodeId);
|
|
1401
|
+
}
|
|
1402
|
+
return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}) };
|
|
1403
|
+
} catch (e: any) {
|
|
1404
|
+
return { success: false, error: e.message };
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
case 'clone_mesh_node': {
|
|
1409
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1410
|
+
const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
|
|
1411
|
+
const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
|
|
1412
|
+
const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
|
|
1413
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1414
|
+
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
1415
|
+
if (!branch) return { success: false, error: 'branch required' };
|
|
1416
|
+
|
|
1417
|
+
try {
|
|
1418
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
1419
|
+
const mesh = meshRecord?.mesh;
|
|
1420
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1421
|
+
|
|
1422
|
+
const sourceNode = mesh.nodes?.find((n: any) => n.id === sourceNodeId || n.nodeId === sourceNodeId);
|
|
1423
|
+
if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
|
|
1424
|
+
|
|
1425
|
+
const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
|
|
1426
|
+
const { createWorktree } = await import('../git/git-worktree.js');
|
|
1427
|
+
const result = await createWorktree({
|
|
1428
|
+
repoRoot,
|
|
1429
|
+
branch,
|
|
1430
|
+
baseBranch,
|
|
1431
|
+
meshName: mesh.name,
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
let node: any;
|
|
1435
|
+
if (meshRecord.inline) {
|
|
1436
|
+
const { randomUUID } = await import('crypto');
|
|
1437
|
+
node = {
|
|
1438
|
+
id: `node_${randomUUID().replace(/-/g, '')}`,
|
|
1439
|
+
workspace: result.worktreePath,
|
|
1440
|
+
repoRoot: result.worktreePath,
|
|
1441
|
+
daemonId: sourceNode.daemonId,
|
|
1442
|
+
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
1443
|
+
policy: { ...(sourceNode.policy || {}) },
|
|
1444
|
+
isLocalWorktree: true,
|
|
1445
|
+
worktreeBranch: result.branch,
|
|
1446
|
+
clonedFromNodeId: sourceNodeId,
|
|
1447
|
+
};
|
|
1448
|
+
this.updateInlineMeshNode(meshId, mesh, node);
|
|
1449
|
+
} else {
|
|
1450
|
+
const { addNode } = await import('../config/mesh-config.js');
|
|
1451
|
+
node = addNode(meshId, {
|
|
1452
|
+
workspace: result.worktreePath,
|
|
1453
|
+
repoRoot: result.worktreePath,
|
|
1454
|
+
daemonId: sourceNode.daemonId,
|
|
1455
|
+
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
1456
|
+
isLocalWorktree: true,
|
|
1457
|
+
worktreeBranch: result.branch,
|
|
1458
|
+
clonedFromNodeId: sourceNodeId,
|
|
1459
|
+
policy: { ...(sourceNode.policy || {}) },
|
|
1460
|
+
});
|
|
1461
|
+
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return {
|
|
1465
|
+
success: true,
|
|
1466
|
+
node,
|
|
1467
|
+
worktreePath: result.worktreePath,
|
|
1468
|
+
branch: result.branch,
|
|
1469
|
+
};
|
|
1028
1470
|
} catch (e: any) {
|
|
1029
1471
|
return { success: false, error: e.message };
|
|
1030
1472
|
}
|
|
@@ -1033,7 +1475,7 @@ export class DaemonCommandRouter {
|
|
|
1033
1475
|
// ─── Mesh Coordinator Launch ───
|
|
1034
1476
|
case 'launch_mesh_coordinator': {
|
|
1035
1477
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1036
|
-
|
|
1478
|
+
let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
|
|
1037
1479
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1038
1480
|
|
|
1039
1481
|
try {
|
|
@@ -1071,9 +1513,29 @@ export class DaemonCommandRouter {
|
|
|
1071
1513
|
}
|
|
1072
1514
|
const workspace = typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '';
|
|
1073
1515
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
1516
|
+
if (!cliType) {
|
|
1517
|
+
const resolved = await resolveProviderTypeFromPriority({
|
|
1518
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || 'coordinator'),
|
|
1519
|
+
providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
|
|
1520
|
+
providerLoader: this.deps.providerLoader,
|
|
1521
|
+
onStatusChange: this.deps.onStatusChange,
|
|
1522
|
+
});
|
|
1523
|
+
if (!resolved.providerType) {
|
|
1524
|
+
return {
|
|
1525
|
+
success: false,
|
|
1526
|
+
code: 'mesh_coordinator_provider_priority_unusable',
|
|
1527
|
+
error: resolved.error || 'No usable provider found from node providerPriority',
|
|
1528
|
+
meshId,
|
|
1529
|
+
cliType,
|
|
1530
|
+
workspace,
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
cliType = resolved.providerType;
|
|
1534
|
+
}
|
|
1074
1535
|
const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
|
|
1075
1536
|
const coordinatorSetup = resolveMeshCoordinatorSetup({
|
|
1076
1537
|
provider: providerMeta,
|
|
1538
|
+
cliType,
|
|
1077
1539
|
meshId,
|
|
1078
1540
|
workspace,
|
|
1079
1541
|
});
|
|
@@ -1101,7 +1563,8 @@ export class DaemonCommandRouter {
|
|
|
1101
1563
|
};
|
|
1102
1564
|
}
|
|
1103
1565
|
|
|
1104
|
-
|
|
1566
|
+
const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
|
|
1567
|
+
if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
|
|
1105
1568
|
return {
|
|
1106
1569
|
success: false,
|
|
1107
1570
|
code: 'mesh_coordinator_unsupported',
|
|
@@ -1112,20 +1575,42 @@ export class DaemonCommandRouter {
|
|
|
1112
1575
|
};
|
|
1113
1576
|
}
|
|
1114
1577
|
|
|
1115
|
-
//
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1578
|
+
// Build the coordinator prompt before mutating workspace config or launching.
|
|
1579
|
+
// Prompt generation failures are configuration/data-shape errors; fail closed so
|
|
1580
|
+
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
1581
|
+
let systemPrompt = '';
|
|
1582
|
+
try {
|
|
1583
|
+
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
1584
|
+
} catch (error: any) {
|
|
1585
|
+
const message = error?.message || String(error);
|
|
1586
|
+
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
1587
|
+
return {
|
|
1588
|
+
success: false,
|
|
1589
|
+
code: 'mesh_coordinator_prompt_failed',
|
|
1590
|
+
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
1591
|
+
meshId,
|
|
1592
|
+
cliType,
|
|
1593
|
+
workspace,
|
|
1594
|
+
};
|
|
1127
1595
|
}
|
|
1128
1596
|
|
|
1597
|
+
// 1. Write provider-declared MCP config for CLIs that auto-import it.
|
|
1598
|
+
const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
|
|
1599
|
+
const { dirname } = await import('path');
|
|
1600
|
+
const mcpConfigPath = coordinatorSetup.configPath;
|
|
1601
|
+
const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
|
|
1602
|
+
? createHermesManualMeshCoordinatorSetup(meshId, workspace)
|
|
1603
|
+
: null;
|
|
1604
|
+
const returnManualFallback = (message: string) => ({
|
|
1605
|
+
success: false,
|
|
1606
|
+
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
1607
|
+
error: message,
|
|
1608
|
+
meshId,
|
|
1609
|
+
cliType,
|
|
1610
|
+
workspace,
|
|
1611
|
+
meshCoordinatorSetup: hermesManualFallback,
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1129
1614
|
// Merge ADHDev mesh server into existing config.
|
|
1130
1615
|
// Pass full mesh data as env var so the MCP server can bootstrap
|
|
1131
1616
|
// without depending on meshes.json or a running daemon.
|
|
@@ -1139,39 +1624,73 @@ export class DaemonCommandRouter {
|
|
|
1139
1624
|
ADHDEV_MCP_TRANSPORT: 'ipc',
|
|
1140
1625
|
};
|
|
1141
1626
|
}
|
|
1627
|
+
|
|
1628
|
+
try {
|
|
1629
|
+
mkdirSync(dirname(mcpConfigPath), { recursive: true });
|
|
1630
|
+
} catch (error: any) {
|
|
1631
|
+
const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
|
|
1632
|
+
LOG.error('MeshCoordinator', message);
|
|
1633
|
+
if (hermesManualFallback) return returnManualFallback(message);
|
|
1634
|
+
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// Backup existing MCP config if present.
|
|
1638
|
+
const hadExistingMcpConfig = existsSync(mcpConfigPath);
|
|
1639
|
+
let existingMcpConfig: Record<string, any> = {};
|
|
1640
|
+
if (hadExistingMcpConfig) {
|
|
1641
|
+
try {
|
|
1642
|
+
existingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
|
|
1643
|
+
copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
|
|
1644
|
+
} catch (error: any) {
|
|
1645
|
+
LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
|
|
1646
|
+
return {
|
|
1647
|
+
success: false,
|
|
1648
|
+
code: 'mesh_coordinator_config_parse_failed',
|
|
1649
|
+
error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const mcpServersKey = getMcpServersKey(configFormat);
|
|
1655
|
+
const existingServers = existingMcpConfig[mcpServersKey];
|
|
1142
1656
|
const mcpConfig = {
|
|
1143
1657
|
...existingMcpConfig,
|
|
1144
|
-
|
|
1145
|
-
...(
|
|
1658
|
+
[mcpServersKey]: {
|
|
1659
|
+
...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
|
|
1146
1660
|
[coordinatorSetup.serverName]: mcpServerEntry,
|
|
1147
1661
|
},
|
|
1148
1662
|
};
|
|
1149
|
-
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
1150
|
-
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
1151
|
-
|
|
1152
|
-
// 2. Build coordinator system prompt
|
|
1153
|
-
let systemPrompt = '';
|
|
1154
1663
|
try {
|
|
1155
|
-
|
|
1156
|
-
} catch {
|
|
1157
|
-
|
|
1664
|
+
writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
|
|
1665
|
+
} catch (error: any) {
|
|
1666
|
+
const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
|
|
1667
|
+
LOG.error('MeshCoordinator', message);
|
|
1668
|
+
if (hermesManualFallback) return returnManualFallback(message);
|
|
1669
|
+
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
1158
1670
|
}
|
|
1671
|
+
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
1159
1672
|
|
|
1160
1673
|
const cliArgs: string[] = [];
|
|
1674
|
+
const launchEnv: Record<string, string> = {};
|
|
1161
1675
|
if (systemPrompt) {
|
|
1162
|
-
|
|
1676
|
+
if (configFormat === 'hermes_config_yaml') {
|
|
1677
|
+
launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
|
|
1678
|
+
} else {
|
|
1679
|
+
cliArgs.push('--append-system-prompt', systemPrompt);
|
|
1680
|
+
}
|
|
1163
1681
|
}
|
|
1164
1682
|
if (cliType === 'claude-cli') {
|
|
1165
1683
|
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
1166
1684
|
}
|
|
1167
1685
|
|
|
1168
|
-
// 3. Launch CLI session via existing cliManager
|
|
1169
|
-
//
|
|
1170
|
-
// CLI
|
|
1686
|
+
// 3. Launch CLI session via existing cliManager.
|
|
1687
|
+
// Provider-specific prompt injection remains fail-closed: Claude gets
|
|
1688
|
+
// explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
|
|
1171
1689
|
const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
1172
1690
|
cliType,
|
|
1173
1691
|
dir: workspace,
|
|
1174
1692
|
cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
|
|
1693
|
+
env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
|
|
1175
1694
|
settings: {
|
|
1176
1695
|
meshCoordinatorFor: meshId
|
|
1177
1696
|
}
|