@adhdev/daemon-core 0.9.76-rc.11 → 0.9.76-rc.12

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.
@@ -67,6 +67,7 @@ export declare class CliProviderInstance implements ProviderInstance {
67
67
  constructor(provider: ProviderModule, workingDir: string, cliArgs?: string[], instanceId?: string, transportFactory?: PtyTransportFactory, options?: {
68
68
  providerSessionId?: string;
69
69
  launchMode?: 'new' | 'resume' | 'manual';
70
+ extraEnv?: Record<string, string>;
70
71
  onProviderSessionResolved?: (info: {
71
72
  instanceId: string;
72
73
  providerType: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.76-rc.11",
3
+ "version": "0.9.76-rc.12",
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,6 +52,7 @@
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
  },
@@ -62,6 +63,7 @@
62
63
  "@adhdev/ghostty-vt-node": "*"
63
64
  },
64
65
  "devDependencies": {
66
+ "@types/js-yaml": "^4.0.9",
65
67
  "@types/node": "^22.0.0",
66
68
  "@types/ws": "^8.18.1",
67
69
  "tsup": "^8.2.0",
@@ -422,6 +422,7 @@ export class ProviderCliAdapter implements CliAdapter {
422
422
  provider: CliProviderModule,
423
423
  workingDir: string,
424
424
  private extraArgs: string[] = [],
425
+ private extraEnv: Record<string, string> = {},
425
426
  transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
426
427
  ) {
427
428
  this.provider = provider;
@@ -523,6 +524,7 @@ export class ProviderCliAdapter implements CliAdapter {
523
524
  runtimeSettings: this.runtimeSettings,
524
525
  workingDir: this.workingDir,
525
526
  extraArgs: this.extraArgs,
527
+ extraEnv: this.extraEnv,
526
528
  });
527
529
 
528
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.
@@ -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?: { resumeSessionId?: string, settingsOverride?: Record<string, any> },
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,7 @@
1
1
  import { existsSync, realpathSync } from 'node:fs'
2
2
  import { createRequire } from 'node:module'
3
- import { dirname, join, resolve } from 'node:path'
3
+ import * as os from 'node:os'
4
+ import { dirname, isAbsolute, join, resolve } from 'node:path'
4
5
  import type { ProviderModule, MeshCoordinatorMcpConfigFormat } from '../providers/contracts.js'
5
6
 
6
7
  export interface MeshCoordinatorMcpServerLaunch {
@@ -80,7 +81,7 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
80
81
  return {
81
82
  kind: 'auto_import',
82
83
  serverName,
83
- configPath: join(workspace, path),
84
+ configPath: resolveMcpConfigPath(path, workspace),
84
85
  configFormat: mcpConfig.format,
85
86
  mcpServer,
86
87
  }
@@ -118,6 +119,14 @@ function renderMeshCoordinatorTemplate(template: string, values: Record<string,
118
119
  return template.replace(/\{\{\s*(meshId|workspace|serverName|adhdevMcpCommand)\s*\}\}/g, (_, key: string) => values[key] || '')
119
120
  }
120
121
 
122
+ function resolveMcpConfigPath(configPath: string, workspace: string): string {
123
+ const trimmed = configPath.trim()
124
+ if (trimmed === '~') return os.homedir()
125
+ if (trimmed.startsWith('~/')) return join(os.homedir(), trimmed.slice(2))
126
+ if (isAbsolute(trimmed)) return trimmed
127
+ return join(workspace, trimmed)
128
+ }
129
+
121
130
  function resolveAdhdevMcpServerLaunch(options: {
122
131
  meshId: string
123
132
  nodeExecutable?: string
@@ -30,6 +30,7 @@ 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';
@@ -63,6 +64,28 @@ function resolveUpgradeChannel(args: any): ReleaseChannel {
63
64
  }
64
65
  import * as fs from 'fs';
65
66
 
67
+ type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
68
+
69
+ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
70
+ return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
71
+ }
72
+
73
+ function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
74
+ return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
75
+ }
76
+
77
+ function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
78
+ if (!text.trim()) return {};
79
+ if (format === 'claude_mcp_json') return JSON.parse(text);
80
+ const parsed = loadYamlModule().load(text);
81
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
82
+ }
83
+
84
+ function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
85
+ if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
86
+ return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
87
+ }
88
+
66
89
  // ─── Types ───
67
90
 
68
91
  export interface SessionHostControlPlane {
@@ -1231,7 +1254,8 @@ export class DaemonCommandRouter {
1231
1254
  };
1232
1255
  }
1233
1256
 
1234
- if (coordinatorSetup.configFormat !== 'claude_mcp_json') {
1257
+ const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
1258
+ if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
1235
1259
  return {
1236
1260
  success: false,
1237
1261
  code: 'mesh_coordinator_unsupported',
@@ -1261,18 +1285,27 @@ export class DaemonCommandRouter {
1261
1285
  };
1262
1286
  }
1263
1287
 
1264
- // 1. Write provider-declared MCP config to workspace for CLIs that auto-import it.
1265
- const { existsSync, readFileSync, writeFileSync, copyFileSync } = await import('fs');
1288
+ // 1. Write provider-declared MCP config for CLIs that auto-import it.
1289
+ const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
1290
+ const { dirname } = await import('path');
1266
1291
  const mcpConfigPath = coordinatorSetup.configPath;
1292
+ mkdirSync(dirname(mcpConfigPath), { recursive: true });
1267
1293
 
1268
1294
  // Backup existing MCP config if present.
1269
1295
  const hadExistingMcpConfig = existsSync(mcpConfigPath);
1270
- let existingMcpConfig: any = {};
1296
+ let existingMcpConfig: Record<string, any> = {};
1271
1297
  if (hadExistingMcpConfig) {
1272
1298
  try {
1273
- existingMcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));
1299
+ existingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
1274
1300
  copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1275
- } catch { /* empty */ }
1301
+ } catch (error: any) {
1302
+ LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
1303
+ return {
1304
+ success: false,
1305
+ code: 'mesh_coordinator_config_parse_failed',
1306
+ error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
1307
+ };
1308
+ }
1276
1309
  }
1277
1310
 
1278
1311
  // Merge ADHDev mesh server into existing config.
@@ -1288,31 +1321,39 @@ export class DaemonCommandRouter {
1288
1321
  ADHDEV_MCP_TRANSPORT: 'ipc',
1289
1322
  };
1290
1323
  }
1324
+ const mcpServersKey = getMcpServersKey(configFormat);
1325
+ const existingServers = existingMcpConfig[mcpServersKey];
1291
1326
  const mcpConfig = {
1292
1327
  ...existingMcpConfig,
1293
- mcpServers: {
1294
- ...(existingMcpConfig.mcpServers || {}),
1328
+ [mcpServersKey]: {
1329
+ ...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
1295
1330
  [coordinatorSetup.serverName]: mcpServerEntry,
1296
1331
  },
1297
1332
  };
1298
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
1333
+ writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
1299
1334
  LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
1300
1335
 
1301
1336
  const cliArgs: string[] = [];
1337
+ const launchEnv: Record<string, string> = {};
1302
1338
  if (systemPrompt) {
1303
- cliArgs.push('--append-system-prompt', systemPrompt);
1339
+ if (configFormat === 'hermes_config_yaml') {
1340
+ launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
1341
+ } else {
1342
+ cliArgs.push('--append-system-prompt', systemPrompt);
1343
+ }
1304
1344
  }
1305
1345
  if (cliType === 'claude-cli') {
1306
1346
  cliArgs.push('--mcp-config', coordinatorSetup.configPath);
1307
1347
  }
1308
1348
 
1309
- // 3. Launch CLI session via existing cliManager
1310
- // Pass coordinator system prompt via --append-system-prompt so the
1311
- // CLI inherits its default behavior AND knows it is a mesh coordinator.
1349
+ // 3. Launch CLI session via existing cliManager.
1350
+ // Provider-specific prompt injection remains fail-closed: Claude gets
1351
+ // explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
1312
1352
  const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
1313
1353
  cliType,
1314
1354
  dir: workspace,
1315
1355
  cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
1356
+ env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
1316
1357
  settings: {
1317
1358
  meshCoordinatorFor: meshId
1318
1359
  }
@@ -214,6 +214,7 @@ export class CliProviderInstance implements ProviderInstance {
214
214
  options?: {
215
215
  providerSessionId?: string;
216
216
  launchMode?: 'new' | 'resume' | 'manual';
217
+ extraEnv?: Record<string, string>;
217
218
  onProviderSessionResolved?: (info: {
218
219
  instanceId: string;
219
220
  providerType: string;
@@ -230,7 +231,7 @@ export class CliProviderInstance implements ProviderInstance {
230
231
  this.providerSessionId = options?.providerSessionId;
231
232
  this.launchMode = options?.launchMode || 'new';
232
233
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
233
- this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, transportFactory);
234
+ this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
234
235
  this.monitor = new StatusMonitor();
235
236
  this.historyWriter = new ChatHistoryWriter();
236
237
  }