@adhdev/daemon-core 0.8.93 → 0.8.94

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.
@@ -33,6 +33,17 @@ export declare class ProviderLoader {
33
33
  setVersionArchive(archive: VersionArchive): void;
34
34
  private static readonly GITHUB_TARBALL_URL;
35
35
  private static readonly META_FILE;
36
+ private static readonly REPO_PROVIDER_DIRNAME;
37
+ private static readonly SIBLING_MARKER_FILE;
38
+ private static readonly SIBLING_ENV_VAR;
39
+ private probeStarts;
40
+ private siblingLogged;
41
+ private userDirSource;
42
+ /** Process-level dedup for stderr sibling-adoption notices (shared across all ProviderLoader instances). */
43
+ private static siblingStderrLogged;
44
+ private static looksLikeProviderRoot;
45
+ private static hasProviderRootMarker;
46
+ private detectDefaultUserDir;
36
47
  constructor(options?: {
37
48
  userDir?: string;
38
49
  logFn?: (msg: string) => void;
@@ -40,6 +51,12 @@ export declare class ProviderLoader {
40
51
  sourceMode?: ProviderSourceMode;
41
52
  /** Deprecated alias for sourceMode='no-upstream' */
42
53
  disableUpstream?: boolean;
54
+ /**
55
+ * Directories from which to walk up looking for a sibling `adhdev-providers`
56
+ * checkout. Defaults to [process.cwd(), __dirname]. Used by tests for hermetic
57
+ * probing; production code should leave this unset.
58
+ */
59
+ probeStarts?: string[];
43
60
  });
44
61
  private log;
45
62
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.93",
3
+ "version": "0.8.94",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.93",
3
+ "version": "0.8.94",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1492,7 +1492,7 @@ export class ProviderCliAdapter implements CliAdapter {
1492
1492
 
1493
1493
  private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
1494
1494
  if (this.parseErrorMessage) return 'error';
1495
- if (startupModal) return 'waiting_approval';
1495
+ if (startupModal || this.activeModal) return 'waiting_approval';
1496
1496
  if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus === 'idle') return 'generating';
1497
1497
  return this.currentStatus;
1498
1498
  }
@@ -1502,12 +1502,28 @@ export class ProviderCliAdapter implements CliAdapter {
1502
1502
  getStatus(): CliSessionStatus {
1503
1503
  const screenText = this.terminalScreen.getText() || '';
1504
1504
  const startupModal = this.startupParseGate ? this.getStartupConfirmationModal(screenText) : null;
1505
- const effectiveStatus = this.projectEffectiveStatus(startupModal);
1505
+ let effectiveStatus = this.projectEffectiveStatus(startupModal);
1506
+ let effectiveModal = startupModal || this.activeModal;
1507
+ if (!startupModal && !effectiveModal && typeof this.cliScripts?.parseOutput === 'function') {
1508
+ try {
1509
+ const parsed = this.getScriptParsedStatus();
1510
+ const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
1511
+ && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
1512
+ ? parsed.activeModal
1513
+ : null;
1514
+ if (parsed?.status === 'waiting_approval' && parsedModal) {
1515
+ effectiveStatus = 'waiting_approval';
1516
+ effectiveModal = parsedModal;
1517
+ }
1518
+ } catch {
1519
+ // Ignore parse errors here; getScriptParsedStatus surfaces them on richer callers.
1520
+ }
1521
+ }
1506
1522
  return {
1507
1523
  status: effectiveStatus,
1508
1524
  messages: [...this.committedMessages],
1509
1525
  workingDir: this.workingDir,
1510
- activeModal: startupModal || this.activeModal,
1526
+ activeModal: effectiveModal,
1511
1527
  errorMessage: this.parseErrorMessage || undefined,
1512
1528
  errorReason: this.parseErrorMessage ? 'parse_error' : undefined,
1513
1529
  };
@@ -1565,6 +1581,18 @@ export class ProviderCliAdapter implements CliAdapter {
1565
1581
  this.currentTurnScope,
1566
1582
  screenText,
1567
1583
  );
1584
+ const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
1585
+ && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
1586
+ ? parsed.activeModal
1587
+ : null;
1588
+ if (parsedModal && parsed?.status === 'waiting_approval') {
1589
+ this.activeModal = parsedModal;
1590
+ this.isWaitingForResponse = true;
1591
+ if (this.currentStatus !== 'waiting_approval') {
1592
+ this.setStatus('waiting_approval', 'parsed_waiting_approval');
1593
+ this.onStatusChange?.();
1594
+ }
1595
+ }
1568
1596
  if (this.maybeCommitVisibleIdleTranscript(parsed)) {
1569
1597
  return this.getScriptParsedStatus();
1570
1598
  }
@@ -2219,7 +2247,26 @@ export class ProviderCliAdapter implements CliAdapter {
2219
2247
 
2220
2248
  resolveModal(buttonIndex: number): void {
2221
2249
  const screenText = this.terminalScreen.getText() || '';
2222
- const modal = this.activeModal || this.getStartupConfirmationModal(screenText);
2250
+ let modal = this.activeModal || this.getStartupConfirmationModal(screenText);
2251
+ if (!modal && typeof this.cliScripts?.parseOutput === 'function') {
2252
+ try {
2253
+ const parsed = this.getScriptParsedStatus();
2254
+ const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
2255
+ && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
2256
+ ? parsed.activeModal
2257
+ : null;
2258
+ if (parsed?.status === 'waiting_approval' && parsedModal) {
2259
+ modal = parsedModal;
2260
+ this.activeModal = parsedModal;
2261
+ if (this.currentStatus !== 'waiting_approval') {
2262
+ this.setStatus('waiting_approval', 'resolve_modal_parse');
2263
+ this.onStatusChange?.();
2264
+ }
2265
+ }
2266
+ } catch {
2267
+ // Ignore parse failures here; resolveModal falls back to current state.
2268
+ }
2269
+ }
2223
2270
  if (!this.ptyProcess || ((this.currentStatus !== 'waiting_approval') && !modal)) return;
2224
2271
  this.clearIdleFinishCandidate('resolve_modal');
2225
2272
  this.recordTrace('resolve_modal', {
@@ -42,7 +42,9 @@ function getTargetInstance(h: CommandHelpers, args: any): ApprovalSelectableInst
42
42
  const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
43
43
  const sessionId = targetSessionId || h.currentSession?.sessionId || '';
44
44
  if (!sessionId) return null;
45
- return (h.ctx.instanceManager?.getInstance(sessionId) as ApprovalSelectableInstance | undefined) || null;
45
+ const session = h.ctx.sessionRegistry?.get(sessionId);
46
+ const instanceKey = session?.adapterKey || session?.instanceKey || sessionId;
47
+ return (h.ctx.instanceManager?.getInstance(instanceKey) as ApprovalSelectableInstance | undefined) || null;
46
48
  }
47
49
 
48
50
  function getTargetTransport(h: CommandHelpers, provider?: ProviderModule): SessionTransport | null {
@@ -1238,10 +1240,25 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
1238
1240
  }
1239
1241
 
1240
1242
  const status = adapter.getStatus();
1241
- if (status?.status !== 'waiting_approval') {
1243
+ const targetInstance = getTargetInstance(h, args);
1244
+ const targetState = targetInstance?.getState?.() as { activeChat?: { status?: string; activeModal?: { message?: string; buttons?: string[] } | null } } | undefined;
1245
+ const surfacedModal = targetState?.activeChat?.activeModal && Array.isArray(targetState.activeChat.activeModal.buttons)
1246
+ && targetState.activeChat.activeModal.buttons.some((candidate) => typeof candidate === 'string' && candidate.trim())
1247
+ ? targetState.activeChat.activeModal
1248
+ : null;
1249
+ const statusModal = status?.activeModal && Array.isArray(status.activeModal.buttons)
1250
+ && status.activeModal.buttons.some((candidate) => typeof candidate === 'string' && candidate.trim())
1251
+ ? status.activeModal
1252
+ : null;
1253
+ const effectiveModal = statusModal || surfacedModal;
1254
+ const effectiveStatus = status?.status === 'waiting_approval' || targetState?.activeChat?.status === 'waiting_approval'
1255
+ ? 'waiting_approval'
1256
+ : status?.status;
1257
+ LOG.info('Command', `[resolveAction] CLI PTY gate target=${String(args?.targetSessionId || '')} rawStatus=${String(status?.status || '')} effectiveStatus=${String(effectiveStatus || '')} statusModal=${statusModal ? 'yes' : 'no'} surfacedModal=${surfacedModal ? 'yes' : 'no'} instance=${targetInstance ? 'yes' : 'no'}`);
1258
+ if (effectiveStatus !== 'waiting_approval' && !effectiveModal) {
1242
1259
  return { success: false, error: 'Not in approval state' };
1243
1260
  }
1244
- const buttons: string[] = status.activeModal?.buttons || ['Allow once', 'Always allow', 'Deny'];
1261
+ const buttons: string[] = effectiveModal?.buttons || ['Allow once', 'Always allow', 'Deny'];
1245
1262
  // Resolve button index: explicit buttonIndex arg → button text match → action fallback
1246
1263
  let buttonIndex = typeof args?.buttonIndex === 'number' ? args.buttonIndex : -1;
1247
1264
  if (buttonIndex < 0) {
@@ -1,10 +1,20 @@
1
1
  import type { ProviderSourceMode } from './config.js'
2
2
 
3
+ /**
4
+ * How the effective `userDir` was resolved:
5
+ * - `explicit` — the user set `config.providerDir` or passed `userDir`
6
+ * - `sibling-env` — auto-adopted a sibling `adhdev-providers/` via ADHDEV_USE_SIBLING_PROVIDERS=1
7
+ * - `sibling-marker` — auto-adopted a sibling `adhdev-providers/` via a `.adhdev-provider-root` marker file
8
+ * - `home-default` — fell back to `~/.adhdev/providers`
9
+ */
10
+ export type ProviderUserDirSource = 'explicit' | 'sibling-env' | 'sibling-marker' | 'home-default'
11
+
3
12
  export interface ProviderSourceConfigSnapshot {
4
13
  sourceMode: ProviderSourceMode
5
14
  disableUpstream: boolean
6
15
  explicitProviderDir: string | null
7
16
  userDir: string
17
+ userDirSource: ProviderUserDirSource
8
18
  upstreamDir: string
9
19
  providerRoots: string[]
10
20
  }
@@ -31,7 +31,7 @@ import type {
31
31
  } from './contracts.js';
32
32
  import { validateProviderDefinition } from './provider-schema.js';
33
33
  import type { ProviderSourceMode } from '../config/config.js';
34
- import type { ProviderSourceConfigSnapshot } from '../config/provider-source-config.js';
34
+ import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
35
35
 
36
36
  interface ProviderAvailabilityState {
37
37
  installed: boolean;
@@ -59,6 +59,75 @@ export class ProviderLoader {
59
59
 
60
60
  private static readonly GITHUB_TARBALL_URL = 'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
61
61
  private static readonly META_FILE = '.meta.json';
62
+ private static readonly REPO_PROVIDER_DIRNAME = 'adhdev-providers';
63
+ private static readonly SIBLING_MARKER_FILE = '.adhdev-provider-root';
64
+ private static readonly SIBLING_ENV_VAR = 'ADHDEV_USE_SIBLING_PROVIDERS';
65
+
66
+ private probeStarts: string[] = [];
67
+ private siblingLogged = false;
68
+ private userDirSource: ProviderUserDirSource = 'home-default';
69
+
70
+ /** Process-level dedup for stderr sibling-adoption notices (shared across all ProviderLoader instances). */
71
+ private static siblingStderrLogged: Set<string> = new Set();
72
+
73
+ private static looksLikeProviderRoot(candidate: string): boolean {
74
+ try {
75
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isDirectory()) return false;
76
+ return ['ide', 'extension', 'cli', 'acp'].some((category) =>
77
+ fs.existsSync(path.join(candidate, category))
78
+ );
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ private static hasProviderRootMarker(candidate: string): boolean {
85
+ try {
86
+ return fs.existsSync(path.join(candidate, ProviderLoader.SIBLING_MARKER_FILE));
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ private detectDefaultUserDir(): { path: string; source: 'sibling-env' | 'sibling-marker' | 'home-default' } {
93
+ const fallback = path.join(os.homedir(), '.adhdev', 'providers');
94
+ const envOptIn = process.env[ProviderLoader.SIBLING_ENV_VAR] === '1';
95
+ const visited = new Set<string>();
96
+
97
+ for (const start of this.probeStarts) {
98
+ let current = path.resolve(start);
99
+ while (!visited.has(current)) {
100
+ visited.add(current);
101
+ const siblingCandidate = path.join(path.dirname(current), ProviderLoader.REPO_PROVIDER_DIRNAME);
102
+ if (ProviderLoader.looksLikeProviderRoot(siblingCandidate)) {
103
+ const hasMarker = ProviderLoader.hasProviderRootMarker(siblingCandidate);
104
+ if (envOptIn || hasMarker) {
105
+ const source: 'sibling-env' | 'sibling-marker' = hasMarker ? 'sibling-marker' : 'sibling-env';
106
+ if (!this.siblingLogged) {
107
+ this.log(`Using sibling provider checkout (${source}): ${siblingCandidate}`);
108
+ this.siblingLogged = true;
109
+ }
110
+ // Force-surface adoption to stderr once per sibling path per process, so CLI
111
+ // entry points that suppress logFn still leave a visible trail.
112
+ if (!ProviderLoader.siblingStderrLogged.has(siblingCandidate)) {
113
+ ProviderLoader.siblingStderrLogged.add(siblingCandidate);
114
+ try {
115
+ process.stderr.write(
116
+ `[adhdev] Using sibling adhdev-providers checkout (${source}): ${siblingCandidate}\n`,
117
+ );
118
+ } catch { /* ignore */ }
119
+ }
120
+ return { path: siblingCandidate, source };
121
+ }
122
+ }
123
+ const parent = path.dirname(current);
124
+ if (parent === current) break;
125
+ current = parent;
126
+ }
127
+ }
128
+
129
+ return { path: fallback, source: 'home-default' };
130
+ }
62
131
 
63
132
  constructor(options?: {
64
133
  userDir?: string;
@@ -67,12 +136,21 @@ export class ProviderLoader {
67
136
  sourceMode?: ProviderSourceMode;
68
137
  /** Deprecated alias for sourceMode='no-upstream' */
69
138
  disableUpstream?: boolean;
139
+ /**
140
+ * Directories from which to walk up looking for a sibling `adhdev-providers`
141
+ * checkout. Defaults to [process.cwd(), __dirname]. Used by tests for hermetic
142
+ * probing; production code should leave this unset.
143
+ */
144
+ probeStarts?: string[];
70
145
  }) {
71
146
  this.logFn = options?.logFn || LOG.forComponent('Provider').asLogFn();
147
+ this.probeStarts = options?.probeStarts ?? [process.cwd(), __dirname];
72
148
 
73
149
  // Default directory for auto-downloads
74
150
  this.defaultProvidersDir = path.join(os.homedir(), '.adhdev', 'providers');
75
- this.userDir = this.defaultProvidersDir;
151
+ const detected = this.detectDefaultUserDir();
152
+ this.userDir = detected.path;
153
+ this.userDirSource = detected.source;
76
154
  this.upstreamDir = path.join(this.defaultProvidersDir, '.upstream');
77
155
  this.disableUpstream = false;
78
156
 
@@ -117,6 +195,7 @@ export class ProviderLoader {
117
195
  disableUpstream: this.disableUpstream,
118
196
  explicitProviderDir: this.explicitProviderDir,
119
197
  userDir: this.userDir,
198
+ userDirSource: this.userDirSource,
120
199
  upstreamDir: this.upstreamDir,
121
200
  providerRoots: this.getProviderRoots(),
122
201
  };
@@ -138,7 +217,14 @@ export class ProviderLoader {
138
217
  }
139
218
 
140
219
  this.sourceMode = nextSourceMode;
141
- this.userDir = this.explicitProviderDir || this.defaultProvidersDir;
220
+ if (this.explicitProviderDir) {
221
+ this.userDir = this.explicitProviderDir;
222
+ this.userDirSource = 'explicit';
223
+ } else {
224
+ const detected = this.detectDefaultUserDir();
225
+ this.userDir = detected.path;
226
+ this.userDirSource = detected.source;
227
+ }
142
228
  this.upstreamDir = path.join(this.defaultProvidersDir, '.upstream');
143
229
  this.disableUpstream = this.sourceMode === 'no-upstream';
144
230