@adhdev/daemon-core 0.9.77-rc.2 → 0.9.77-rc.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.77-rc.2",
3
+ "version": "0.9.77-rc.21",
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",
@@ -195,6 +195,8 @@ export class ProviderCliAdapter implements CliAdapter {
195
195
 
196
196
  // ─── CLI Scripts (script-based parsing) ───
197
197
  private cliScripts: CliScripts;
198
+ /** Per-session opaque state object created by cliScripts.createState(), reset on stop. */
199
+ private scriptState: unknown = null;
198
200
  private runtimeSettings: Record<string, any> = {};
199
201
  /** Full accumulated rendered PTY transcript for parser/readback use */
200
202
  private accumulatedBuffer: string = '';
@@ -477,6 +479,9 @@ export class ProviderCliAdapter implements CliAdapter {
477
479
  this.cliScripts = scripts;
478
480
  this.parsedStatusCache = null;
479
481
  this.parseErrorMessage = null;
482
+ // Initialize per-session state: createState() is called once here and on script reload.
483
+ // The returned object lives until the PTY exits (scriptState = null on exit).
484
+ this.scriptState = typeof scripts.createState === 'function' ? scripts.createState() : null;
480
485
  const scriptNames = listCliScriptNames(scripts);
481
486
  LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
482
487
  }
@@ -610,6 +615,7 @@ export class ProviderCliAdapter implements CliAdapter {
610
615
  this.ready = false;
611
616
  this.startupParseGate = false;
612
617
  this.spawnAt = 0;
618
+ this.scriptState = null;
613
619
  this.onStatusChange?.();
614
620
  });
615
621
 
@@ -1470,7 +1476,7 @@ export class ProviderCliAdapter implements CliAdapter {
1470
1476
  scope: this.currentTurnScope,
1471
1477
  runtimeSettings: this.runtimeSettings,
1472
1478
  });
1473
- const session = this.cliScripts.parseSession({ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
1479
+ const session = this.cliScripts.parseSession(this.scriptState, { ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
1474
1480
  this.parseErrorMessage = null;
1475
1481
  return session && typeof session === 'object' ? session : null;
1476
1482
  } catch (e: any) {
@@ -1485,7 +1491,7 @@ export class ProviderCliAdapter implements CliAdapter {
1485
1491
  if (!this.cliScripts?.detectStatus) return null;
1486
1492
  try {
1487
1493
  const screenText = this.terminalScreen.getText();
1488
- const status = this.cliScripts.detectStatus({
1494
+ const status = this.cliScripts.detectStatus(this.scriptState, {
1489
1495
  tail: text.slice(-500),
1490
1496
  screenText,
1491
1497
  rawBuffer: this.accumulatedRawBuffer,
@@ -1505,7 +1511,7 @@ export class ProviderCliAdapter implements CliAdapter {
1505
1511
  try {
1506
1512
  const screenText = this.terminalScreen.getText();
1507
1513
  const buffer = screenText || this.accumulatedBuffer;
1508
- return this.cliScripts.parseApproval({
1514
+ return this.cliScripts.parseApproval(this.scriptState, {
1509
1515
  buffer,
1510
1516
  screenText,
1511
1517
  rawBuffer: this.accumulatedRawBuffer,
@@ -1640,7 +1646,7 @@ export class ProviderCliAdapter implements CliAdapter {
1640
1646
  scope: this.currentTurnScope,
1641
1647
  runtimeSettings: this.runtimeSettings,
1642
1648
  });
1643
- return await Promise.resolve(fn({
1649
+ return await Promise.resolve(fn(this.scriptState, {
1644
1650
  ...input,
1645
1651
  args: args && typeof args === 'object' ? { ...args } : {},
1646
1652
  }));
@@ -48,11 +48,21 @@ export interface ParsedSession {
48
48
  }
49
49
 
50
50
  export interface CliScripts {
51
- parseSession?: (input: CliScriptInput & { tail?: string; tailScreen?: CliScreenSnapshot }) => ParsedSession | null;
52
- detectStatus?: (input: CliStatusInput) => string | null;
53
- parseApproval?: (input: CliApprovalInput) => { message: string; buttons: string[] } | null;
51
+ /**
52
+ * Optional state factory. Called once per CLI session start (or script reload).
53
+ * The returned object is passed as the first argument to detectStatus, parseApproval,
54
+ * and parseSession on every invocation, allowing scripts to maintain per-session state
55
+ * (e.g. last-seen status, approval fingerprints, stability counters).
56
+ *
57
+ * Scripts that don't define createState() receive null as the state argument,
58
+ * making this change fully backward compatible.
59
+ */
60
+ createState?: () => unknown;
61
+ parseSession?: (state: unknown, input: CliScriptInput & { tail?: string; tailScreen?: CliScreenSnapshot }) => ParsedSession | null;
62
+ detectStatus?: (state: unknown, input: CliStatusInput) => string | null;
63
+ parseApproval?: (state: unknown, input: CliApprovalInput) => { message: string; buttons: string[] } | null;
54
64
  resolveAction?: (data: any) => string;
55
- [name: string]: ((input: any) => any) | undefined;
65
+ [name: string]: ((state: unknown, input: any) => any) | ((data: any) => any) | (() => unknown) | undefined;
56
66
  }
57
67
 
58
68
  export interface CliScreenLine {
@@ -177,10 +177,6 @@ export function buildCoordinatorDelegatedCliLaunchOptions(
177
177
  const cliArgs = Array.isArray(input.cliArgs) ? [...input.cliArgs] : [];
178
178
  const env: Record<string, string> = { ...(input.env || {}), ...COORDINATOR_DELEGATED_ENV_UNSETS };
179
179
 
180
- if (cliType === 'hermes-cli' && !hasCliArg(cliArgs, '--ignore-user-config')) {
181
- cliArgs.unshift('--ignore-user-config');
182
- }
183
-
184
180
  if (cliType === 'claude-cli' && !hasCliArg(cliArgs, '--mcp-config')) {
185
181
  cliArgs.unshift('--mcp-config', ensureEmptyDelegatedMcpConfig(input.workspace));
186
182
  }
@@ -28,6 +28,15 @@ export type MeshCoordinatorSetup =
28
28
  instructions: string
29
29
  template: string
30
30
  }
31
+ | {
32
+ /** Provider registers MCP via its own CLI command (e.g. `codex mcp add` / `gemini mcp add`). */
33
+ kind: 'cli_command'
34
+ serverName: string
35
+ /** The rendered shell command to execute before launching the coordinator session. */
36
+ command: string
37
+ requiresRestart: boolean
38
+ instructions: string
39
+ }
31
40
  | {
32
41
  kind: 'unsupported'
33
42
  reason: string
@@ -152,6 +161,24 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
152
161
  if (!instructions || !template?.trim()) {
153
162
  return { kind: 'unsupported', reason: 'Provider manual MCP setup is missing instructions or template' }
154
163
  }
164
+ const renderedTemplate = renderMeshCoordinatorTemplate(template, {
165
+ meshId,
166
+ workspace,
167
+ serverName,
168
+ adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
169
+ })
170
+ // Detect if the template is a runnable CLI command (single line, no YAML/JSON structure).
171
+ // If so, use cli_command kind so the daemon can execute it automatically.
172
+ const isCliCommand = !renderedTemplate.trim().includes('\n') && !renderedTemplate.trim().startsWith('{')
173
+ if (isCliCommand) {
174
+ return {
175
+ kind: 'cli_command',
176
+ serverName,
177
+ command: renderedTemplate.trim(),
178
+ requiresRestart: mcpConfig.requiresRestart === true,
179
+ instructions: instructions,
180
+ }
181
+ }
155
182
  return {
156
183
  kind: 'manual',
157
184
  serverName,
@@ -159,12 +186,7 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
159
186
  configPathCommand: mcpConfig.configPathCommand,
160
187
  requiresRestart: mcpConfig.requiresRestart === true,
161
188
  instructions,
162
- template: renderMeshCoordinatorTemplate(template, {
163
- meshId,
164
- workspace,
165
- serverName,
166
- adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
167
- }),
189
+ template: renderedTemplate,
168
190
  }
169
191
  }
170
192
 
@@ -1711,6 +1711,105 @@ export class DaemonCommandRouter {
1711
1711
  };
1712
1712
  }
1713
1713
 
1714
+ // ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
1715
+ if (coordinatorSetup.kind === 'cli_command') {
1716
+ // Build coordinator prompt first — fail closed on errors.
1717
+ let cliCmdSystemPrompt = '';
1718
+ try {
1719
+ cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
1720
+ } catch (error: any) {
1721
+ const message = error?.message || String(error);
1722
+ LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
1723
+ return {
1724
+ success: false,
1725
+ code: 'mesh_coordinator_prompt_failed',
1726
+ error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
1727
+ meshId, cliType, workspace,
1728
+ };
1729
+ }
1730
+
1731
+ // Run the provider's MCP registration command.
1732
+ try {
1733
+ const { execFileSync: execCmdSync } = await import('node:child_process');
1734
+ const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
1735
+ const [regCmd, ...regArgs] = cmdParts;
1736
+ LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
1737
+ execCmdSync(regCmd, regArgs, { stdio: 'pipe', timeout: 15_000 });
1738
+ } catch (error: any) {
1739
+ // Non-fatal — server may already be registered (providers return exit 1 on duplicate).
1740
+ LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
1741
+ }
1742
+
1743
+ // Inject system prompt using provider-native methods.
1744
+ // Codex: -c 'instructions="..."' CLI config override
1745
+ // Gemini: write GEMINI.md to workspace (auto-loaded as context)
1746
+ const cliCmdArgs: string[] = [];
1747
+ const cliCmdEnv: Record<string, string> = {};
1748
+ if (cliCmdSystemPrompt) {
1749
+ if (cliType === 'codex-cli') {
1750
+ // Codex reads `developer_instructions` from config.toml as system instructions.
1751
+ // The -c flag overrides a config key for this session only.
1752
+ cliCmdArgs.push('-c', `developer_instructions=${JSON.stringify(cliCmdSystemPrompt)}`);
1753
+ } else if (cliType === 'gemini-cli') {
1754
+ // Gemini CLI auto-loads GEMINI.md from CWD as project context.
1755
+ // Write a temporary GEMINI.md to the workspace before launch.
1756
+ try {
1757
+ const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
1758
+ const geminiMdPath = `${workspace}/GEMINI.md`;
1759
+ const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
1760
+ const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
1761
+ const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
1762
+ if (efs(geminiMdPath)) {
1763
+ const existing = rfs(geminiMdPath, 'utf-8');
1764
+ // Replace existing block or append
1765
+ const replaced = existing.replace(
1766
+ new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
1767
+ block,
1768
+ );
1769
+ wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
1770
+ } else {
1771
+ wfs(geminiMdPath, block);
1772
+ }
1773
+ LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
1774
+ } catch (e: any) {
1775
+ LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
1776
+ }
1777
+ }
1778
+ }
1779
+
1780
+ const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
1781
+ cliType,
1782
+ dir: workspace,
1783
+ cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
1784
+ env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
1785
+ settings: { meshCoordinatorFor: meshId },
1786
+ });
1787
+
1788
+ if (!cliCmdLaunch?.success) {
1789
+ return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
1790
+ }
1791
+
1792
+ LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
1793
+ try {
1794
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
1795
+ appendLedgerEntry(meshId, {
1796
+ kind: 'coordinator_started',
1797
+ sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
1798
+ providerType: cliType,
1799
+ payload: { workspace },
1800
+ });
1801
+ } catch { /* best-effort */ }
1802
+
1803
+ return {
1804
+ success: true,
1805
+ meshId,
1806
+ cliType,
1807
+ workspace,
1808
+ sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
1809
+ mcpRegistered: true,
1810
+ };
1811
+ }
1812
+
1714
1813
  const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
1715
1814
  if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
1716
1815
  return {
@@ -113,11 +113,18 @@ export async function handleOpenPanel(h: CommandHelpers, args: any): Promise<Com
113
113
  export async function handlePtyInput(h: CommandHelpers, args: any): Promise<CommandResult> {
114
114
  const { cliType, data, targetSessionId } = args || {};
115
115
  if (!data) return { success: false, error: 'data required' };
116
+
117
+ // Filter out VT100/VT420 Device Attributes responses (e.g. \x1b[?1;2c or \x1b[>0;276;0c)
118
+ // These are echoed by xterm.js in the dashboard in response to \x1b[c queries
119
+ // and pollute the CLI input buffer.
120
+ const cleanData = typeof data === 'string' ? data.replace(/\x1b\[[?>][0-9;]*c/g, '') : data;
121
+ if (!cleanData) return { success: true };
122
+
116
123
  const adapter = h.getCliAdapter(targetSessionId || cliType);
117
124
  if (!adapter || typeof adapter.writeRaw !== 'function') {
118
125
  return { success: false, error: `CLI adapter not found: ${targetSessionId || cliType || 'unknown'}` };
119
126
  }
120
- await adapter.writeRaw(data);
127
+ await adapter.writeRaw(cleanData);
121
128
  return { success: true };
122
129
  }
123
130
 
@@ -37,6 +37,7 @@ const MESH_COORDINATOR_EVENTS = new Set([
37
37
  'agent:generating_completed',
38
38
  'agent:waiting_approval',
39
39
  'agent:stopped',
40
+ 'agent:ready',
40
41
  'monitor:long_generating',
41
42
  ]);
42
43
 
@@ -68,7 +69,9 @@ export function tryAssignQueueTask(
68
69
  providerType: string
69
70
  ): boolean {
70
71
  const task = claimNextTask(meshId, nodeId, sessionId);
71
- if (!task) return false;
72
+ if (!task) {
73
+ return false;
74
+ }
72
75
 
73
76
  LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
74
77
 
@@ -76,7 +79,7 @@ export function tryAssignQueueTask(
76
79
  targetSessionId: sessionId,
77
80
  cliType: providerType,
78
81
  action: 'send_chat',
79
- input: task.message,
82
+ message: task.message,
80
83
  }).catch((e: any) => {
81
84
  LOG.error('MeshQueue', `Failed to dispatch task to node ${nodeId}: ${e?.message}`);
82
85
  });
@@ -164,6 +167,7 @@ function buildMeshSystemMessage(args: {
164
167
  function injectMeshSystemMessage(components: DaemonComponents, args: {
165
168
  meshId: string;
166
169
  sourceInstanceId?: string;
170
+ nodeId?: string;
167
171
  nodeLabel: string;
168
172
  event: string;
169
173
  metadataEvent: Record<string, unknown>;
@@ -171,7 +175,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
171
175
  // ── Task Queue & Ledger ──
172
176
  if (args.event === 'agent:generating_completed') {
173
177
  const sessionId = readNonEmptyString(args.metadataEvent.targetSessionId);
174
- const nodeId = readNonEmptyString(args.metadataEvent.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
178
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
175
179
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
176
180
 
177
181
  if (sessionId) {
@@ -183,6 +187,16 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
183
187
  }, 500);
184
188
  }
185
189
  }
190
+ } else if (args.event === 'agent:ready') {
191
+ const sessionId = readNonEmptyString(args.metadataEvent.targetSessionId);
192
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
193
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
194
+
195
+ if (sessionId && nodeId && providerType) {
196
+ setTimeout(() => {
197
+ tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
198
+ }, 500);
199
+ }
186
200
  } else if (args.event === 'agent:stopped') {
187
201
  const sessionId = readNonEmptyString(args.metadataEvent.targetSessionId);
188
202
  if (sessionId) {
@@ -195,7 +209,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
195
209
  try {
196
210
  appendLedgerEntry(args.meshId, {
197
211
  kind: ledgerKind,
198
- nodeId: readNonEmptyString(args.metadataEvent.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
212
+ nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
199
213
  sessionId: readNonEmptyString(args.metadataEvent.targetSessionId) || undefined,
200
214
  providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
201
215
  payload: {
@@ -219,7 +233,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
219
233
 
220
234
  recoveryContext = getSessionRecoveryContext(args.meshId, {
221
235
  sessionId: readNonEmptyString(args.metadataEvent.targetSessionId) || undefined,
222
- nodeId: readNonEmptyString(args.metadataEvent.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
236
+ nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
223
237
  maxRetries,
224
238
  });
225
239
  recoveryContext.failedProviderType = readNonEmptyString(args.metadataEvent.providerType) || null;
@@ -327,6 +341,7 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
327
341
  const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
328
342
  return injectMeshSystemMessage(components, {
329
343
  meshId,
344
+ nodeId,
330
345
  nodeLabel,
331
346
  event: eventName,
332
347
  metadataEvent: {
@@ -374,6 +389,7 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
374
389
  // Determine node label. Inline/cloud meshes may be unavailable here, so preserve runtime node id.
375
390
  const targetNode = mesh?.nodes?.find((n: any) => n.workspace === workspace);
376
391
  const runtimeNodeId = readNonEmptyString(settings.meshNodeId);
392
+ const resolvedNodeId = targetNode?.id || runtimeNodeId;
377
393
  const nodeLabel = targetNode
378
394
  ? `Node '${targetNode.id}'`
379
395
  : runtimeNodeId
@@ -383,6 +399,7 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
383
399
  injectMeshSystemMessage(components, {
384
400
  meshId,
385
401
  sourceInstanceId: instanceId,
402
+ nodeId: resolvedNodeId,
386
403
  nodeLabel,
387
404
  event: event.event,
388
405
  metadataEvent: event,
@@ -833,6 +833,8 @@ export class CliProviderInstance implements ProviderInstance {
833
833
  this.completedDebounceTimer = null;
834
834
  }, 3000);
835
835
  }
836
+ } else if (newStatus === 'idle' && this.lastStatus === 'starting') {
837
+ this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
836
838
  } else if (newStatus === 'stopped') {
837
839
  // Cancel any pending debounce
838
840
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }