@adhdev/daemon-core 0.9.82-rc.167 → 0.9.82-rc.169

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.
@@ -4,6 +4,10 @@ export declare function pickApprovalButton(buttons: string[] | null | undefined,
4
4
  index: number;
5
5
  label: string;
6
6
  };
7
+ export declare function pickAutoApprovalButton(buttons: string[] | null | undefined): {
8
+ index: number;
9
+ label: string;
10
+ };
7
11
  export declare function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string;
8
12
  /**
9
13
  * Returns true when the given text (e.g. last assistant message content, or
@@ -245,6 +245,10 @@ export declare const SCHEMA: {
245
245
  readonly minimum: 1;
246
246
  readonly default: 2;
247
247
  };
248
+ readonly continuation_lines: {
249
+ readonly type: "boolean";
250
+ readonly default: false;
251
+ };
248
252
  };
249
253
  };
250
254
  };
@@ -23,6 +23,7 @@ export interface ModalButtonsRule {
23
23
  flags?: string;
24
24
  key_for_index: string;
25
25
  min_count?: number;
26
+ continuation_lines?: boolean;
26
27
  }
27
28
  export interface SpecState {
28
29
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.167",
3
+ "version": "0.9.82-rc.169",
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",
@@ -19,7 +19,7 @@ import type { SessionRegistry } from '../sessions/registry.js';
19
19
  import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
20
20
  import { LOG } from '../logging/logger.js';
21
21
  import type { AgentStreamState } from './types.js';
22
- import { formatAutoApprovalMessage, pickApprovalButton } from '../providers/approval-utils.js';
22
+ import { formatAutoApprovalMessage, pickAutoApprovalButton } from '../providers/approval-utils.js';
23
23
  import type { ProviderModule } from '../providers/contracts.js';
24
24
  import { buildRuntimeSystemChatMessage } from '../providers/chat-message-normalization.js';
25
25
 
@@ -210,8 +210,7 @@ export class AgentStreamPoller {
210
210
  if (stream?.status === 'waiting_approval') {
211
211
  const autoApprove = providerLoader.getSettings(stream.agentType).autoApprove !== false;
212
212
  if (autoApprove && resolvedActiveSessionId) {
213
- const provider = providerLoader.getMeta(stream.agentType);
214
- const { label: buttonLabel } = pickApprovalButton(stream.activeModal?.buttons, provider);
213
+ const { label: buttonLabel } = pickAutoApprovalButton(stream.activeModal?.buttons);
215
214
  const approved = await agentStreamManager.resolveSessionAction(cdp, resolvedActiveSessionId, 'approve', buttonLabel);
216
215
  if (approved) {
217
216
  const effectId = [
@@ -0,0 +1,252 @@
1
+ import { randomUUID } from 'crypto';
2
+ import {
3
+ SessionHostClient,
4
+ type SessionHostEndpoint,
5
+ type SessionHostRequest,
6
+ type SessionHostResponse,
7
+ type SessionTerminalSnapshot,
8
+ type SessionTerminalState,
9
+ } from '@adhdev/session-host-core';
10
+
11
+ type LowercaseLetter =
12
+ | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
13
+ | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
14
+
15
+ type FunctionKey = 'f1' | 'f2' | 'f3' | 'f4' | 'f5' | 'f6' | 'f7' | 'f8' | 'f9' | 'f10' | 'f11' | 'f12';
16
+ type BaseNamedKey =
17
+ | 'enter' | 'escape' | 'tab' | 'backspace'
18
+ | 'up' | 'down' | 'left' | 'right'
19
+ | 'home' | 'end' | 'pageup' | 'pagedown' | 'space'
20
+ | FunctionKey;
21
+ type ShiftNamedKey = BaseNamedKey | LowercaseLetter | `ctrl+${LowercaseLetter}` | `alt+${LowercaseLetter}`;
22
+
23
+ export type NamedKey =
24
+ | BaseNamedKey
25
+ | `ctrl+${LowercaseLetter}`
26
+ | `alt+${LowercaseLetter}`
27
+ | `shift+${ShiftNamedKey}`;
28
+
29
+ const BASE_KEY_SEQUENCES: Record<BaseNamedKey, string> = {
30
+ enter: '\r',
31
+ escape: '\x1b',
32
+ tab: '\t',
33
+ backspace: '\x7f',
34
+ up: '\x1b[A',
35
+ down: '\x1b[B',
36
+ right: '\x1b[C',
37
+ left: '\x1b[D',
38
+ home: '\x1b[H',
39
+ end: '\x1b[F',
40
+ pageup: '\x1b[5~',
41
+ pagedown: '\x1b[6~',
42
+ space: ' ',
43
+ f1: '\x1bOP',
44
+ f2: '\x1bOQ',
45
+ f3: '\x1bOR',
46
+ f4: '\x1bOS',
47
+ f5: '\x1b[15~',
48
+ f6: '\x1b[17~',
49
+ f7: '\x1b[18~',
50
+ f8: '\x1b[19~',
51
+ f9: '\x1b[20~',
52
+ f10: '\x1b[21~',
53
+ f11: '\x1b[23~',
54
+ f12: '\x1b[24~',
55
+ };
56
+
57
+ const SHIFTED_CSI_KEYS: Partial<Record<BaseNamedKey, string>> = {
58
+ up: '\x1b[1;2A',
59
+ down: '\x1b[1;2B',
60
+ right: '\x1b[1;2C',
61
+ left: '\x1b[1;2D',
62
+ home: '\x1b[1;2H',
63
+ end: '\x1b[1;2F',
64
+ pageup: '\x1b[5;2~',
65
+ pagedown: '\x1b[6;2~',
66
+ f1: '\x1b[1;2P',
67
+ f2: '\x1b[1;2Q',
68
+ f3: '\x1b[1;2R',
69
+ f4: '\x1b[1;2S',
70
+ f5: '\x1b[15;2~',
71
+ f6: '\x1b[17;2~',
72
+ f7: '\x1b[18;2~',
73
+ f8: '\x1b[19;2~',
74
+ f9: '\x1b[20;2~',
75
+ f10: '\x1b[21;2~',
76
+ f11: '\x1b[23;2~',
77
+ f12: '\x1b[24;2~',
78
+ };
79
+
80
+ function isLowercaseLetter(value: string): value is LowercaseLetter {
81
+ return /^[a-z]$/.test(value);
82
+ }
83
+
84
+ function encodeControlLetter(letter: LowercaseLetter): string {
85
+ return String.fromCharCode(letter.charCodeAt(0) - 96);
86
+ }
87
+
88
+ function encodeShiftedKey(key: string): string {
89
+ if (isLowercaseLetter(key)) return key.toUpperCase();
90
+ if (key.startsWith('ctrl+') && isLowercaseLetter(key.slice(5))) {
91
+ return encodeControlLetter(key.slice(5) as LowercaseLetter);
92
+ }
93
+ if (key.startsWith('alt+') && isLowercaseLetter(key.slice(4))) {
94
+ return `\x1b${key.slice(4).toUpperCase()}`;
95
+ }
96
+ if (key === 'tab') return '\x1b[Z';
97
+ if (key in SHIFTED_CSI_KEYS) return SHIFTED_CSI_KEYS[key as BaseNamedKey]!;
98
+ if (key in BASE_KEY_SEQUENCES) return BASE_KEY_SEQUENCES[key as BaseNamedKey];
99
+ throw new Error(`Unsupported named key: shift+${key}`);
100
+ }
101
+
102
+ export function namedKeyToAnsi(key: NamedKey | string): string {
103
+ const normalized = String(key || '').trim().toLowerCase();
104
+ if (normalized in BASE_KEY_SEQUENCES) return BASE_KEY_SEQUENCES[normalized as BaseNamedKey];
105
+ if (normalized.startsWith('ctrl+') && isLowercaseLetter(normalized.slice(5))) {
106
+ return encodeControlLetter(normalized.slice(5) as LowercaseLetter);
107
+ }
108
+ if (normalized.startsWith('alt+') && isLowercaseLetter(normalized.slice(4))) {
109
+ return `\x1b${normalized.slice(4)}`;
110
+ }
111
+ if (normalized.startsWith('shift+')) return encodeShiftedKey(normalized.slice(6));
112
+ throw new Error(`Unsupported named key: ${key}`);
113
+ }
114
+
115
+ export function namedKeysToAnsi(keys: readonly (NamedKey | string)[]): string {
116
+ if (!Array.isArray(keys)) throw new Error('keys must be an array');
117
+ return keys.map(namedKeyToAnsi).join('');
118
+ }
119
+
120
+ export interface RawTerminalSessionHostClient {
121
+ connect(): Promise<void>;
122
+ request<T = unknown>(request: SessionHostRequest): Promise<SessionHostResponse<T>>;
123
+ close(): Promise<void>;
124
+ }
125
+
126
+ export interface RawTerminalAttachmentOptions {
127
+ endpoint?: SessionHostEndpoint;
128
+ sessionId: string;
129
+ mode?: 'read' | 'write';
130
+ clientId?: string;
131
+ client?: RawTerminalSessionHostClient;
132
+ }
133
+
134
+ export class RawTerminalAttachment {
135
+ private closed = false;
136
+
137
+ private constructor(
138
+ readonly sessionId: string,
139
+ private readonly clientId: string,
140
+ private readonly mode: 'read' | 'write',
141
+ private readonly client: RawTerminalSessionHostClient,
142
+ ) {}
143
+
144
+ static async attach(options: RawTerminalAttachmentOptions): Promise<RawTerminalAttachment> {
145
+ const sessionId = String(options.sessionId || '').trim();
146
+ if (!sessionId) throw new Error('sessionId is required');
147
+ const mode = options.mode || 'read';
148
+ const clientId = options.clientId || `raw-terminal-${process.pid}-${randomUUID().slice(0, 8)}`;
149
+ const client = options.client || new SessionHostClient({ endpoint: options.endpoint });
150
+ await client.connect();
151
+
152
+ const attachResponse = await client.request({
153
+ type: 'attach_session',
154
+ payload: {
155
+ sessionId,
156
+ clientId,
157
+ clientType: 'web',
158
+ readOnly: mode === 'read',
159
+ },
160
+ });
161
+ if (!attachResponse.success) {
162
+ await client.close().catch(() => {});
163
+ throw new Error(attachResponse.error || `Failed to attach terminal session ${sessionId}`);
164
+ }
165
+
166
+ if (mode === 'write') {
167
+ const ownerResponse = await client.request({
168
+ type: 'acquire_write',
169
+ payload: {
170
+ sessionId,
171
+ clientId,
172
+ ownerType: 'user',
173
+ force: true,
174
+ },
175
+ });
176
+ if (!ownerResponse.success) {
177
+ await client.request({
178
+ type: 'detach_session',
179
+ payload: { sessionId, clientId },
180
+ }).catch(() => ({ success: false }));
181
+ await client.close().catch(() => {});
182
+ throw new Error(ownerResponse.error || `Failed to acquire terminal session ${sessionId}`);
183
+ }
184
+ }
185
+
186
+ return new RawTerminalAttachment(sessionId, clientId, mode, client);
187
+ }
188
+
189
+ async readSnapshot(): Promise<SessionTerminalSnapshot> {
190
+ const response = await this.client.request<SessionTerminalSnapshot>({
191
+ type: 'get_terminal_snapshot',
192
+ payload: { sessionId: this.sessionId },
193
+ });
194
+ if (!response.success || !response.result) {
195
+ throw new Error(response.error || `Terminal screen unavailable for ${this.sessionId}`);
196
+ }
197
+ return response.result;
198
+ }
199
+
200
+ async readScreenText(): Promise<string> {
201
+ return (await this.readSnapshot()).text;
202
+ }
203
+
204
+ async readState(): Promise<SessionTerminalState> {
205
+ return (await this.readSnapshot()).state;
206
+ }
207
+
208
+ async writeInput(text: string): Promise<void> {
209
+ if (this.mode !== 'write') throw new Error('Raw terminal attachment is read-only');
210
+ const response = await this.client.request({
211
+ type: 'send_input',
212
+ payload: {
213
+ sessionId: this.sessionId,
214
+ clientId: this.clientId,
215
+ data: text,
216
+ },
217
+ });
218
+ if (!response.success) throw new Error(response.error || `Failed to write terminal input to ${this.sessionId}`);
219
+ }
220
+
221
+ async writeKeys(keys: readonly (NamedKey | string)[]): Promise<void> {
222
+ await this.writeInput(namedKeysToAnsi(keys));
223
+ }
224
+
225
+ async close(): Promise<void> {
226
+ if (this.closed) return;
227
+ this.closed = true;
228
+ if (this.mode === 'write') {
229
+ await this.client.request({
230
+ type: 'release_write',
231
+ payload: { sessionId: this.sessionId, clientId: this.clientId },
232
+ }).catch(() => ({ success: false }));
233
+ }
234
+ await this.client.request({
235
+ type: 'detach_session',
236
+ payload: { sessionId: this.sessionId, clientId: this.clientId },
237
+ }).catch(() => ({ success: false }));
238
+ await this.client.close().catch(() => {});
239
+ }
240
+ }
241
+
242
+ export async function withRawTerminalAttachment<T>(
243
+ options: RawTerminalAttachmentOptions,
244
+ operation: (attachment: RawTerminalAttachment) => Promise<T>,
245
+ ): Promise<T> {
246
+ const attachment = await RawTerminalAttachment.attach(options);
247
+ try {
248
+ return await operation(attachment);
249
+ } finally {
250
+ await attachment.close();
251
+ }
252
+ }
package/src/index.ts CHANGED
@@ -428,6 +428,13 @@ export type { CliAdapter } from './cli-adapter-types.js';
428
428
  export { NodePtyTransportFactory } from './cli-adapters/pty-transport.js';
429
429
  export type { PtyRuntimeTransport, PtyTransportFactory, PtySpawnOptions } from './cli-adapters/pty-transport.js';
430
430
  export { SessionHostPtyTransportFactory } from './cli-adapters/session-host-transport.js';
431
+ export {
432
+ RawTerminalAttachment,
433
+ namedKeyToAnsi,
434
+ namedKeysToAnsi,
435
+ withRawTerminalAttachment,
436
+ } from './cli-adapters/raw-terminal-io.js';
437
+ export type { NamedKey, RawTerminalAttachmentOptions, RawTerminalSessionHostClient } from './cli-adapters/raw-terminal-io.js';
431
438
  export type { HostedCliRuntimeDescriptor, CliTransportFactoryParams } from './commands/cli-manager.js';
432
439
  export {
433
440
  DEFAULT_SESSION_HOST_APP_NAME,
@@ -4,4 +4,8 @@ export declare function pickApprovalButton(buttons: string[] | null | undefined,
4
4
  index: number;
5
5
  label: string;
6
6
  };
7
+ export declare function pickAutoApprovalButton(buttons: string[] | null | undefined): {
8
+ index: number;
9
+ label: string;
10
+ };
7
11
  export declare function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string;
@@ -66,6 +66,14 @@ export function pickApprovalButton(
66
66
  return { index: -1, label: '' };
67
67
  }
68
68
 
69
+ export function pickAutoApprovalButton(
70
+ buttons: string[] | null | undefined,
71
+ ): { index: number; label: string } {
72
+ const labels = (buttons || []).map((button) => String(button || '').trim());
73
+ const index = labels.findIndex(Boolean);
74
+ return index >= 0 ? { index, label: labels[index] } : { index: -1, label: '' };
75
+ }
76
+
69
77
  export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string {
70
78
  const lines = [`Auto-approved${buttonLabel ? `: ${buttonLabel}` : ''}`];
71
79
  const cleanMessage = String(modalMessage || '').trim();
@@ -22,7 +22,7 @@ import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderN
22
22
  import { LOG } from '../logging/logger.js';
23
23
  import type { ChatMessage } from '../types.js';
24
24
  import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
25
- import { formatAutoApprovalMessage, pickApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
25
+ import { formatAutoApprovalMessage, pickApprovalButton, pickAutoApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
26
26
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
27
27
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
28
28
  import { normalizeProviderSessionId } from './provider-session-id.js';
@@ -1092,9 +1092,9 @@ export class CliProviderInstance implements ProviderInstance {
1092
1092
  if (!modal || buttons.length === 0) {
1093
1093
  return autoApproveActive;
1094
1094
  }
1095
- const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(buttons, this.provider);
1095
+ const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
1096
1096
  if (buttonIndex < 0) {
1097
- // No positive button matched — don't pick a random index, just
1097
+ // No concrete button matched — don't pick a random index, just
1098
1098
  // surface the modal so the user can decide.
1099
1099
  return autoApproveActive;
1100
1100
  }
@@ -20,7 +20,7 @@ import { LOG } from '../logging/logger.js';
20
20
  import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
21
21
  import { validateReadChatResultPayload } from './read-chat-contract.js';
22
22
  import type { ChatMessage } from '../types.js';
23
- import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
23
+ import { formatAutoApprovalMessage, pickAutoApprovalButton } from './approval-utils.js';
24
24
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
25
25
  import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages, extractFinalSummaryFromMessages } from './chat-message-normalization.js';
26
26
  import { getProviderSessionCapabilities, IDE_PROVIDER_SESSION_CAPABILITIES_BASE } from './open-panel-support.js';
@@ -708,7 +708,7 @@ export class IdeProviderInstance implements ProviderInstance {
708
708
 
709
709
  this.autoApproveBusy = true;
710
710
  try {
711
- const { label: targetButton } = pickApprovalButton(_chatData?.activeModal?.buttons, this.provider);
711
+ const { label: targetButton } = pickAutoApprovalButton(_chatData?.activeModal?.buttons);
712
712
 
713
713
  const script = scriptFn({ action: 'approve', button: targetButton, buttonText: targetButton });
714
714
  if (!script) return;
@@ -125,6 +125,11 @@ function compilePattern(ref: { pattern: string; flags?: string }): RegExp {
125
125
  return new RegExp(ref.pattern, flags.includes('g') ? flags : flags + 'g');
126
126
  }
127
127
 
128
+ function compileLinePattern(ref: { pattern: string; flags?: string }): RegExp {
129
+ const flags = (ref.flags ?? 'm').replace(/g/g, '');
130
+ return new RegExp(ref.pattern, flags);
131
+ }
132
+
128
133
  // ────────────────────────────────────────────────────────────────────────────
129
134
  // State matching
130
135
  // ────────────────────────────────────────────────────────────────────────────
@@ -162,16 +167,41 @@ function extractModal(
162
167
  ): ModalSnapshot | null {
163
168
  if (!state.modal_buttons) return null;
164
169
  const hay = sectionText(sections, state.modal_buttons.section, fullScreen);
165
- const re = compilePattern(state.modal_buttons);
166
170
  const buttons: { index: number; label: string; key: string }[] = [];
167
- let m: RegExpExecArray | null;
168
- while ((m = re.exec(hay)) !== null) {
169
- const idx = Number(m[1]);
170
- const label = String(m[2] ?? '').trim();
171
- if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
172
- if (buttons.some(b => b.index === idx)) continue;
173
- const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
174
- buttons.push({ index: idx, label, key });
171
+ if (state.modal_buttons.continuation_lines) {
172
+ const re = compileLinePattern(state.modal_buttons);
173
+ const lines = hay.split('\n');
174
+ for (let i = 0; i < lines.length; i += 1) {
175
+ const m = re.exec(lines[i]);
176
+ if (!m) continue;
177
+ const idx = Number(m[1]);
178
+ let label = String(m[2] ?? '').trim();
179
+ if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
180
+ let j = i + 1;
181
+ while (j < lines.length) {
182
+ const next = lines[j];
183
+ if (!next.trim()) break;
184
+ if (re.test(next)) break;
185
+ if (!/^\s+/.test(next)) break;
186
+ label += ' ' + next.trim();
187
+ j += 1;
188
+ }
189
+ if (buttons.some(b => b.index === idx)) continue;
190
+ const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
191
+ buttons.push({ index: idx, label, key });
192
+ i = j - 1;
193
+ }
194
+ } else {
195
+ const re = compilePattern(state.modal_buttons);
196
+ let m: RegExpExecArray | null;
197
+ while ((m = re.exec(hay)) !== null) {
198
+ const idx = Number(m[1]);
199
+ const label = String(m[2] ?? '').trim();
200
+ if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
201
+ if (buttons.some(b => b.index === idx)) continue;
202
+ const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
203
+ buttons.push({ index: idx, label, key });
204
+ }
175
205
  }
176
206
  buttons.sort((a, b) => a.index - b.index);
177
207
  const minCount = state.modal_buttons.min_count ?? 2;
@@ -269,6 +269,10 @@ export const SCHEMA = {
269
269
  "type": "integer",
270
270
  "minimum": 1,
271
271
  "default": 2
272
+ },
273
+ "continuation_lines": {
274
+ "type": "boolean",
275
+ "default": false
272
276
  }
273
277
  }
274
278
  }
@@ -122,7 +122,8 @@
122
122
  "pattern": { "type": "string", "minLength": 1 },
123
123
  "flags": { "type": "string" },
124
124
  "key_for_index": { "type": "string", "minLength": 1 },
125
- "min_count": { "type": "integer", "minimum": 1, "default": 2 }
125
+ "min_count": { "type": "integer", "minimum": 1, "default": 2 },
126
+ "continuation_lines": { "type": "boolean", "default": false }
126
127
  }
127
128
  }
128
129
  }
@@ -33,6 +33,7 @@ export interface ModalButtonsRule {
33
33
  flags?: string;
34
34
  key_for_index: string;
35
35
  min_count?: number;
36
+ continuation_lines?: boolean;
36
37
  }
37
38
 
38
39
  export interface SpecState {