@adhdev/daemon-core 0.9.82-rc.166 → 0.9.82-rc.168
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/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +234 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -16
- package/dist/index.mjs.map +1 -1
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +1 -0
- package/package.json +1 -1
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/index.ts +7 -0
- package/src/mesh/mesh-events.ts +19 -1
- package/src/providers/cli-provider-instance.ts +1 -1
- package/src/providers/provider-loader.ts +4 -4
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +7 -2
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +6 -0
|
@@ -31,6 +31,7 @@ export interface ModalTuiSpec {
|
|
|
31
31
|
questionVariants?: ModalQuestionVariant[];
|
|
32
32
|
buttonPattern: string;
|
|
33
33
|
buttonFlags?: string;
|
|
34
|
+
buttonLabelGroup?: number;
|
|
34
35
|
/**
|
|
35
36
|
* Optional fallback for terminals that render all options on a single line
|
|
36
37
|
* (e.g. Antigravity feedback survey: `[0] skip [1] yes [2] no [3] still using`).
|
package/package.json
CHANGED
|
@@ -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,
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -556,6 +556,24 @@ function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId:
|
|
|
556
556
|
return false;
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: string): boolean {
|
|
560
|
+
// Some dispatch paths can persist task_dispatched before the direct-dispatch DB row is
|
|
561
|
+
// available. Recover routing from ledger order so coordinator self-targets still emit
|
|
562
|
+
// task_completed and pendingCoordinatorEvents.
|
|
563
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
564
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
565
|
+
const entry = entries[i];
|
|
566
|
+
if (entry.sessionId !== sessionId) continue;
|
|
567
|
+
if (entry.kind === 'task_completed' || entry.kind === 'task_failed' || entry.kind === 'task_stalled') {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
if (entry.kind === 'task_dispatched' && entry.payload?.source === 'direct') {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
559
577
|
function buildLongGeneratingCompletionReconciliation(args: {
|
|
560
578
|
meshId: string;
|
|
561
579
|
nodeId?: string;
|
|
@@ -1627,7 +1645,7 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
|
|
|
1627
1645
|
if (coordinatorMeshId) {
|
|
1628
1646
|
try {
|
|
1629
1647
|
const activeDispatches = getActiveDirectDispatches(coordinatorMeshId);
|
|
1630
|
-
if (activeDispatches.some(d => d.sessionId === instanceId)) {
|
|
1648
|
+
if (activeDispatches.some(d => d.sessionId === instanceId) || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, instanceId)) {
|
|
1631
1649
|
meshIdFromDirectDispatch = coordinatorMeshId;
|
|
1632
1650
|
}
|
|
1633
1651
|
} catch { /* best-effort */ }
|
|
@@ -729,7 +729,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
729
729
|
}
|
|
730
730
|
|
|
731
731
|
getSessionModalState(sessionId?: string): SessionModalState {
|
|
732
|
-
const adapterStatus = this.adapter.getStatus({ allowParse:
|
|
732
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: true });
|
|
733
733
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
734
734
|
const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
|
|
735
735
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
@@ -30,6 +30,10 @@ import type {
|
|
|
30
30
|
ResolvedProvider,
|
|
31
31
|
} from './contracts.js';
|
|
32
32
|
import { validateProviderDefinition } from './provider-schema.js';
|
|
33
|
+
import {
|
|
34
|
+
loadProvidersActive,
|
|
35
|
+
resolveActiveSource,
|
|
36
|
+
} from './external-sources.js';
|
|
33
37
|
import type { ProviderSourceMode } from '../config/config.js';
|
|
34
38
|
import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
|
|
35
39
|
|
|
@@ -430,10 +434,6 @@ export class ProviderLoader {
|
|
|
430
434
|
}
|
|
431
435
|
} else {
|
|
432
436
|
// New layout: external/<source-name>/<category>/<type>/…
|
|
433
|
-
const {
|
|
434
|
-
loadProvidersActive,
|
|
435
|
-
resolveActiveSource,
|
|
436
|
-
} = require('./external-sources.js') as typeof import('./external-sources.js');
|
|
437
437
|
const activeFile = loadProvidersActive();
|
|
438
438
|
let totalLoaded = 0;
|
|
439
439
|
const ambiguousTypes: { type: string; chosen: string; candidates: string[] }[] = [];
|
|
@@ -44,6 +44,7 @@ export interface ModalTuiSpec {
|
|
|
44
44
|
questionVariants?: ModalQuestionVariant[];
|
|
45
45
|
buttonPattern: string;
|
|
46
46
|
buttonFlags?: string;
|
|
47
|
+
buttonLabelGroup?: number;
|
|
47
48
|
/**
|
|
48
49
|
* Optional fallback for terminals that render all options on a single line
|
|
49
50
|
* (e.g. Antigravity feedback survey: `[0] skip [1] yes [2] no [3] still using`).
|
|
@@ -136,12 +137,16 @@ function extractButtons(
|
|
|
136
137
|
): string[] {
|
|
137
138
|
const buttonRe = compile(spec.buttonPattern, spec.buttonFlags ?? 'm');
|
|
138
139
|
const out: string[] = [];
|
|
140
|
+
const labelGroup = Number.isInteger(spec.buttonLabelGroup) && (spec.buttonLabelGroup ?? 0) > 0
|
|
141
|
+
? spec.buttonLabelGroup!
|
|
142
|
+
: 1;
|
|
139
143
|
let i = windowStart;
|
|
140
144
|
while (i < windowEnd) {
|
|
141
145
|
const line = lines[i];
|
|
142
146
|
const m = buttonRe.exec(line);
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
const captured = m?.[labelGroup] ?? (labelGroup === 1 && m && m.length > 2 ? m[m.length - 1] : undefined);
|
|
148
|
+
if (m && captured) {
|
|
149
|
+
let label = captured.trim();
|
|
145
150
|
// Continuation lines: when enabled, append indented lines below until
|
|
146
151
|
// the next button or blank.
|
|
147
152
|
if (spec.continuationLines) {
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
"type": "string",
|
|
37
37
|
"description": "Regex source matching a single button line. Capture group 1 is the button label. Example: `^[\\s❯>]*\\d+\\.\\s+(.+)$`."
|
|
38
38
|
},
|
|
39
|
+
"buttonLabelGroup": {
|
|
40
|
+
"type": "integer",
|
|
41
|
+
"minimum": 1,
|
|
42
|
+
"default": 1,
|
|
43
|
+
"description": "Capture group number to use as the button label when `buttonPattern` contains non-label groups such as selected-row markers."
|
|
44
|
+
},
|
|
39
45
|
"buttonFlags": {
|
|
40
46
|
"type": "string",
|
|
41
47
|
"pattern": "^[gimsuy]*$",
|