@adhdev/daemon-core 0.6.74 → 0.6.76

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.6.74",
3
+ "version": "0.6.76",
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",
@@ -146,6 +146,25 @@ function stripAnsi(str: string): string {
146
146
  .replace(/ +/g, ' ');
147
147
  }
148
148
 
149
+ function stripTerminalNoise(str: string): string {
150
+ return String(str || '')
151
+ // Remove remaining C0/C1 control chars except newlines/tabs.
152
+ // eslint-disable-next-line no-control-regex
153
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
154
+ // Drop common terminal negotiation/report fragments that can remain after ANSI stripping.
155
+ .replace(/(^|[\s([])(?:\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
156
+ .replace(/(^|[\s([])(?:\[\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
157
+ .replace(/(^|[\s([])(?:\d{1,4};\?)(?=$|[\s)\]])/g, '$1')
158
+ .replace(/\r+/g, '\n')
159
+ .replace(/[ \t]+\n/g, '\n')
160
+ .replace(/\n{3,}/g, '\n\n')
161
+ .replace(/ {2,}/g, ' ');
162
+ }
163
+
164
+ function sanitizeTerminalText(str: string): string {
165
+ return stripTerminalNoise(stripAnsi(str));
166
+ }
167
+
149
168
  function findBinary(name: string): string {
150
169
  const isWin = os.platform() === 'win32';
151
170
  try {
@@ -370,6 +389,9 @@ export class ProviderCliAdapter implements CliAdapter {
370
389
  // Approval state machine
371
390
  private approvalTransitionBuffer: string = '';
372
391
  private approvalExitTimeout: NodeJS.Timeout | null = null;
392
+ private pendingScriptStatus: 'generating' | 'waiting_approval' | null = null;
393
+ private pendingScriptStatusSince = 0;
394
+ private pendingScriptStatusTimer: NodeJS.Timeout | null = null;
373
395
 
374
396
  // Output settle debounce — fires after PTY output goes quiet
375
397
  private settleTimer: NodeJS.Timeout | null = null;
@@ -454,6 +476,7 @@ export class ProviderCliAdapter implements CliAdapter {
454
476
  private readonly sendDelayMs: number;
455
477
  private readonly sendKey: string;
456
478
  private readonly submitStrategy: 'wait_for_echo' | 'immediate';
479
+ private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 1000;
457
480
 
458
481
  constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
459
482
  this.provider = provider;
@@ -645,7 +668,7 @@ export class ProviderCliAdapter implements CliAdapter {
645
668
  private handleOutput(rawData: string): void {
646
669
  this.terminalScreen.write(rawData);
647
670
  this.terminalHistory = mergeTerminalHistory(this.terminalHistory, this.terminalScreen.getText());
648
- const cleanData = stripAnsi(rawData);
671
+ const cleanData = sanitizeTerminalText(rawData);
649
672
 
650
673
  if (this.isWaitingForResponse && cleanData) {
651
674
  this.responseBuffer = (this.responseBuffer + cleanData).slice(-8000);
@@ -764,26 +787,45 @@ export class ProviderCliAdapter implements CliAdapter {
764
787
 
765
788
  const prevStatus = this.currentStatus;
766
789
 
767
- if (scriptStatus === 'waiting_approval') {
768
- // Auto-accept startup safety dialogs (e.g., "Claude Code'll be able to read, edit, and execute")
769
- const modalMessage = modal?.message || '';
770
- const screenText = this.terminalScreen.getText() || this.accumulatedBuffer;
771
- const autoAcceptPatterns = [
772
- /be able to read, edit, and execute/i,
773
- /Security guide/i,
774
- /Enter to confirm/i,
775
- /Quick safety check/i,
776
- /Do you trust the files/i,
777
- /Is this a project/i,
778
- ];
779
- if (autoAcceptPatterns.some(p => p.test(modalMessage) || p.test(screenText))) {
780
- LOG.info('CLI', `[${this.cliType}] Auto-accepting startup dialog: ${modalMessage.slice(0, 80)}`);
781
- setTimeout(() => this.ptyProcess?.write('\r'), 200);
782
- this.lastApprovalResolvedAt = Date.now();
783
- this.activeModal = null;
790
+ const clearPendingScriptStatus = () => {
791
+ this.pendingScriptStatus = null;
792
+ this.pendingScriptStatusSince = 0;
793
+ if (this.pendingScriptStatusTimer) {
794
+ clearTimeout(this.pendingScriptStatusTimer);
795
+ this.pendingScriptStatusTimer = null;
796
+ }
797
+ };
798
+ const armPendingScriptStatus = (delayMs: number) => {
799
+ if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
800
+ this.pendingScriptStatusTimer = setTimeout(() => {
801
+ this.pendingScriptStatusTimer = null;
802
+ this.settledBuffer = this.recentOutputBuffer;
803
+ this.evaluateSettled();
804
+ }, delayMs);
805
+ };
806
+ const shouldDebouncePromotion = (status: string) =>
807
+ prevStatus === 'idle'
808
+ && !this.isWaitingForResponse
809
+ && !this.currentTurnScope
810
+ && (status === 'generating' || status === 'waiting_approval');
811
+
812
+ if (shouldDebouncePromotion(scriptStatus)) {
813
+ if (this.pendingScriptStatus !== scriptStatus) {
814
+ this.pendingScriptStatus = scriptStatus as 'generating' | 'waiting_approval';
815
+ this.pendingScriptStatusSince = now;
816
+ armPendingScriptStatus(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
817
+ return;
818
+ }
819
+ const elapsed = now - this.pendingScriptStatusSince;
820
+ if (elapsed < ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS) {
821
+ armPendingScriptStatus(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS - elapsed);
784
822
  return;
785
823
  }
824
+ } else {
825
+ clearPendingScriptStatus();
826
+ }
786
827
 
828
+ if (scriptStatus === 'waiting_approval') {
787
829
  const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
788
830
  if (!inCooldown) {
789
831
  this.isWaitingForResponse = true;
@@ -800,6 +842,16 @@ export class ProviderCliAdapter implements CliAdapter {
800
842
  }
801
843
 
802
844
  if (scriptStatus === 'generating') {
845
+ const screenText = this.terminalScreen.getText() || this.accumulatedBuffer;
846
+ const noActiveTurn = !this.currentTurnScope;
847
+ const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(screenText)
848
+ || (/accept edits on/i.test(screenText)
849
+ && (/Update available!/i.test(screenText)
850
+ || /\/effort/i.test(screenText)
851
+ || /^.*➜\s+\S+/m.test(screenText)));
852
+ if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome) {
853
+ return;
854
+ }
803
855
  if (prevStatus === 'waiting_approval') {
804
856
  // Transitioned out of approval → generating
805
857
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
@@ -1242,7 +1294,7 @@ export class ProviderCliAdapter implements CliAdapter {
1242
1294
  committedMessages: this.committedMessages.slice(-20),
1243
1295
  structuredMessages: this.structuredMessages.slice(-20),
1244
1296
  messageCount: this.committedMessages.length,
1245
- screenText: this.terminalScreen.getText().slice(-4000),
1297
+ screenText: sanitizeTerminalText(this.terminalScreen.getText()).slice(-4000),
1246
1298
  terminalHistory: this.terminalHistory.slice(-8000),
1247
1299
  currentTurnScope: this.currentTurnScope,
1248
1300
  startupBuffer: this.startupBuffer.slice(-4000),
@@ -1251,6 +1303,7 @@ export class ProviderCliAdapter implements CliAdapter {
1251
1303
  accumulatedBufferLength: this.accumulatedBuffer.length,
1252
1304
  accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
1253
1305
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
1306
+ sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
1254
1307
  responseBuffer: this.responseBuffer.slice(-1000),
1255
1308
  isWaitingForResponse: this.isWaitingForResponse,
1256
1309
  activeModal: this.activeModal,
@@ -168,8 +168,24 @@ export class DaemonCommandHandler implements CommandHelpers {
168
168
  getCliAdapter(type?: string): any | null {
169
169
  const target = type || this._currentIdeType;
170
170
  if (!target || !this._ctx.adapters) return null;
171
+ // Normalize composite transport IDs:
172
+ // standalone_xxx:cli:<uuid> -> <uuid>
173
+ // daemon:acp:<uuid> -> <uuid>
174
+ let normalizedTarget = target;
175
+ const colonIdx = normalizedTarget.lastIndexOf(':');
176
+ if (colonIdx >= 0) normalizedTarget = normalizedTarget.substring(colonIdx + 1);
177
+
178
+ const direct = this._ctx.adapters.get(normalizedTarget);
179
+ if (direct) return direct;
180
+
171
181
  for (const [key, adapter] of this._ctx.adapters.entries()) {
172
- if ((adapter as any).cliType === target || key.startsWith(target)) {
182
+ if (
183
+ (adapter as any).cliType === target
184
+ || (adapter as any).cliType === normalizedTarget
185
+ || key === normalizedTarget
186
+ || key.startsWith(target)
187
+ || key.startsWith(normalizedTarget)
188
+ ) {
173
189
  return adapter;
174
190
  }
175
191
  }
@@ -1895,22 +1895,25 @@ export class DevServer {
1895
1895
  return path.join(scriptsDir, versions[0]);
1896
1896
  }
1897
1897
 
1898
- private resolveAutoImplWritableProviderDir(category: ProviderCategory, type: string, requestedDir?: string): string | null {
1898
+ private resolveAutoImplWritableProviderDir(
1899
+ category: ProviderCategory,
1900
+ type: string,
1901
+ requestedDir?: string,
1902
+ ): { dir: string | null; reason?: string } {
1899
1903
  const canonicalUserDir = path.resolve(this.providerLoader.getUserProviderDir(category, type));
1900
1904
  const desiredDir = requestedDir ? path.resolve(requestedDir) : canonicalUserDir;
1901
-
1902
- if (desiredDir !== canonicalUserDir) {
1903
- return null;
1905
+ const upstreamRoot = path.resolve(this.providerLoader.getUpstreamDir());
1906
+ if (desiredDir === upstreamRoot || desiredDir.startsWith(`${upstreamRoot}${path.sep}`)) {
1907
+ return { dir: null, reason: `Refusing to write into upstream provider directory: ${desiredDir}` };
1904
1908
  }
1905
1909
 
1906
- const userRoot = path.resolve(this.providerLoader.getUserDir());
1907
- if (desiredDir !== userRoot && !desiredDir.startsWith(`${userRoot}${path.sep}`)) {
1908
- return null;
1910
+ if (path.basename(desiredDir) !== type) {
1911
+ return { dir: null, reason: `Requested writable provider directory must end with '${type}': ${desiredDir}` };
1909
1912
  }
1910
1913
 
1911
1914
  const sourceDir = this.findProviderDir(type);
1912
1915
  if (!sourceDir) {
1913
- return null;
1916
+ return { dir: null, reason: `Provider source directory not found for '${type}'` };
1914
1917
  }
1915
1918
 
1916
1919
  if (!fs.existsSync(desiredDir)) {
@@ -1921,7 +1924,7 @@ export class DevServer {
1921
1924
 
1922
1925
  const providerJson = path.join(desiredDir, 'provider.json');
1923
1926
  if (!fs.existsSync(providerJson)) {
1924
- return null;
1927
+ return { dir: null, reason: `provider.json not found in writable provider directory: ${desiredDir}` };
1925
1928
  }
1926
1929
 
1927
1930
  try {
@@ -1930,11 +1933,14 @@ export class DevServer {
1930
1933
  providerData.disableUpstream = true;
1931
1934
  fs.writeFileSync(providerJson, JSON.stringify(providerData, null, 2));
1932
1935
  }
1933
- } catch {
1934
- return null;
1936
+ } catch (error) {
1937
+ return {
1938
+ dir: null,
1939
+ reason: `Failed to update provider.json in writable provider directory: ${(error as Error).message}`,
1940
+ };
1935
1941
  }
1936
1942
 
1937
- return desiredDir;
1943
+ return { dir: desiredDir };
1938
1944
  }
1939
1945
 
1940
1946
  private loadAutoImplReferenceScripts(referenceType: string | null): Record<string, string> {
@@ -1975,13 +1981,14 @@ export class DevServer {
1975
1981
  const provider = this.providerLoader.resolve(type);
1976
1982
  if (!provider) { this.json(res, 404, { error: `Provider not found: ${type}` }); return; }
1977
1983
 
1978
- const providerDir = this.resolveAutoImplWritableProviderDir(provider.category, type, requestedProviderDir);
1979
- if (!providerDir) {
1984
+ const writableProvider = this.resolveAutoImplWritableProviderDir(provider.category, type, requestedProviderDir);
1985
+ if (!writableProvider.dir) {
1980
1986
  this.json(res, 409, {
1981
- error: `Auto-implement only writes to the canonical user provider directory for '${type}'.`,
1987
+ error: writableProvider.reason || `Auto-implement only writes to the canonical user provider directory for '${type}'.`,
1982
1988
  });
1983
1989
  return;
1984
1990
  }
1991
+ const providerDir = writableProvider.dir;
1985
1992
 
1986
1993
  try {
1987
1994
  // 1. Collect DOM context
@@ -2749,6 +2756,9 @@ export class DevServer {
2749
2756
  lines.push('8. Keep exports compatible with the existing `scripts.js` router (`module.exports = function ...`).');
2750
2757
  lines.push('9. Do not rewrite unrelated provider config. Only touch the scripts needed for this task unless a tiny supporting change is required.');
2751
2758
  lines.push('10. When the verification API returns `instanceId`, keep using that exact instance for follow-up `send`, `resolve`, `raw`, and `stop` calls. Do not assume type-only routing is safe if multiple sessions exist.');
2759
+ lines.push('11. Do NOT repeatedly dump the same target files. Read the target scripts once, reproduce the bug, then move directly to patching.');
2760
+ lines.push('12. If the user instructions include concrete screen text, raw PTY snippets, or a specific repro, treat that as the primary acceptance criteria.');
2761
+ lines.push('13. After the first successful live repro, stop broad diagnosis. Edit the scripts, reload, and verify. Do not burn tokens on repeated re-inspection without code changes.');
2752
2762
  lines.push('');
2753
2763
 
2754
2764
  lines.push('## Task');
@@ -2792,6 +2802,9 @@ export class DevServer {
2792
2802
  lines.push('');
2793
2803
  lines.push('Use `resolve` when the parsed modal buttons are correct. Use `raw` when the CLI expects a literal keystroke like `1`, `y`, or Enter. Repeat until idle.');
2794
2804
  lines.push('');
2805
+ lines.push('### Patch Discipline');
2806
+ lines.push('Once the repro is confirmed, immediately edit the target files. Avoid loops where you keep re-reading long files or re-running the same debug commands without changing code.');
2807
+ lines.push('');
2795
2808
  lines.push('### 5. Verify the side effects outside the CLI');
2796
2809
  lines.push('```bash');
2797
2810
  lines.push('test -f tmp/adhdev_provider_fix_test.py');
@@ -16,12 +16,18 @@ export interface CLIInfo {
16
16
  displayName: string;
17
17
  icon: string;
18
18
  command: string;
19
+ versionCommand?: string;
19
20
  installed: boolean;
20
21
  version?: string;
21
22
  path?: string;
22
23
  category?: string;
23
24
  }
24
25
 
26
+ function parseVersion(raw: string): string {
27
+ const match = raw.match(/v?(\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9.]+)?)/);
28
+ return match ? match[1] : raw.split('\n')[0].slice(0, 100);
29
+ }
30
+
25
31
  /** Run a shell command with timeout, returning stdout or null on failure */
26
32
  function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
27
33
  return new Promise((resolve) => {
@@ -62,11 +68,18 @@ export async function detectCLIs(providerLoader?: ProviderLoader): Promise<CLIIn
62
68
  // Get version (parallel with other checks)
63
69
  let version: string | undefined;
64
70
  try {
65
- const versionResult = await execAsync(`${cli.command} --version 2>/dev/null`, 3000);
66
- if (versionResult) {
67
- // Extract version number (e.g. "gemini v1.2.3" → "1.2.3")
68
- const match = versionResult.match(/(\d+\.\d+[\.\d]*)/);
69
- version = match ? match[1] : versionResult.split('\n')[0].slice(0, 30);
71
+ const versionCommands = [
72
+ cli.versionCommand,
73
+ `${cli.command} --version 2>/dev/null`,
74
+ `${cli.command} -V 2>/dev/null`,
75
+ `${cli.command} -v 2>/dev/null`,
76
+ ].filter((v): v is string => !!v);
77
+ for (const versionCommand of versionCommands) {
78
+ const versionResult = await execAsync(versionCommand, 3000);
79
+ if (versionResult) {
80
+ version = parseVersion(versionResult);
81
+ break;
82
+ }
70
83
  }
71
84
  } catch { }
72
85
 
@@ -85,25 +85,6 @@ export class CliProviderInstance implements ProviderInstance {
85
85
 
86
86
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
87
87
 
88
- // Preserve rich message objects, just truncate extremely long text strings
89
- const recentMessages = adapterStatus.messages.slice(-50).map((m: any) => {
90
- const content = typeof m.content === 'string' && m.content.length > 8000
91
- ? m.content.slice(0, 8000) + '\n... (truncated)'
92
- : m.content;
93
- return { ...m, content };
94
- });
95
-
96
- // generating during partial response add
97
- // Save history
98
- if (recentMessages.length > 0) {
99
- const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
100
- this.historyWriter.appendNewMessages(
101
- this.type,
102
- recentMessages,
103
- `${this.provider.name} · ${dirName}`,
104
- this.instanceId,
105
- );
106
- }
107
88
  if (adapterStatus.terminalHistory?.trim()) {
108
89
  this.historyWriter.appendTerminalHistory(
109
90
  this.type,
@@ -118,12 +99,12 @@ export class CliProviderInstance implements ProviderInstance {
118
99
  name: this.provider.name,
119
100
  category: 'cli',
120
101
  status: adapterStatus.status,
121
- mode: (this.settings.mode as 'terminal' | 'chat') || 'terminal',
102
+ mode: 'terminal',
122
103
  activeChat: {
123
104
  id: `${this.type}_${this.workingDir}`,
124
105
  title: `${this.provider.name} · ${dirName}`,
125
106
  status: adapterStatus.status,
126
- messages: recentMessages,
107
+ messages: [],
127
108
  activeModal: adapterStatus.activeModal,
128
109
  terminalHistory: adapterStatus.terminalHistory,
129
110
  inputContent: '',
@@ -138,11 +119,15 @@ export class CliProviderInstance implements ProviderInstance {
138
119
 
139
120
  onEvent(event: string, data?: any): void {
140
121
  if (event === 'send_message' && data?.text) {
141
- this.adapter.sendMessage(data.text);
122
+ void this.adapter.sendMessage(data.text).catch((e: any) => {
123
+ LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
124
+ });
142
125
  } else if (event === 'server_connected' && data?.serverConn) {
143
126
  this.adapter.setServerConn(data.serverConn);
144
127
  } else if (event === 'resolve_action' && data) {
145
- this.adapter.resolveAction(data);
128
+ void this.adapter.resolveAction(data).catch((e: any) => {
129
+ LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
130
+ });
146
131
  }
147
132
  }
148
133
 
@@ -237,16 +237,23 @@ export class ProviderLoader {
237
237
  * Build CLI/ACP detection list (replaces cli-detector)
238
238
  * Dynamically generated from provider.js spawn.command.
239
239
  */
240
- getCliDetectionList(): { id: string; displayName: string; icon: string; command: string; category: string }[] {
241
- const result: { id: string; displayName: string; icon: string; command: string; category: string }[] = [];
240
+ getCliDetectionList(): { id: string; displayName: string; icon: string; command: string; category: string; versionCommand?: string }[] {
241
+ const result: { id: string; displayName: string; icon: string; command: string; category: string; versionCommand?: string }[] = [];
242
242
  for (const p of this.providers.values()) {
243
243
  if ((p.category === 'cli' || p.category === 'acp') && p.spawn?.command) {
244
+ const verCmdConfig = (p as any).versionCommand;
245
+ const versionCommand = typeof verCmdConfig === 'object' && verCmdConfig !== null
246
+ ? verCmdConfig[process.platform]
247
+ : verCmdConfig;
244
248
  result.push({
245
249
  id: p.type,
246
250
  displayName: p.displayName || p.name,
247
251
  icon: p.icon || '🔧',
248
252
  command: p.spawn.command,
249
253
  category: p.category,
254
+ ...(typeof versionCommand === 'string' && versionCommand.trim()
255
+ ? { versionCommand: versionCommand.trim() }
256
+ : {}),
250
257
  });
251
258
  }
252
259
  }
@@ -75,7 +75,7 @@ export interface ManagedCliEntry {
75
75
  cliType: string;
76
76
  cliName: string;
77
77
  status: string;
78
- mode: 'terminal' | 'chat';
78
+ mode: 'terminal';
79
79
  workspace: string;
80
80
  activeChat: _ActiveChatData | null;
81
81
  }
@@ -156,7 +156,7 @@ export function buildManagedClis(
156
156
  cliType: s.type,
157
157
  cliName: s.name,
158
158
  status: s.status,
159
- mode: s.mode as 'terminal' | 'chat',
159
+ mode: 'terminal' as const,
160
160
  workspace: s.workspace || '',
161
161
  activeChat: s.activeChat,
162
162
  }));