@mearl/plugin-qoder 0.0.0-stage → 3.0.0-beta.6
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/LICENSE +201 -0
- package/README.md +22 -2
- package/dist/THIRD_PARTY_NOTICES.txt +222 -0
- package/dist/agent.d.ts +68 -0
- package/dist/agent.js +968 -0
- package/dist/auth.d.ts +12 -0
- package/dist/auth.js +89 -0
- package/dist/capability/bundle.json +27 -0
- package/dist/capability/preset.json +14 -0
- package/dist/capability/runtime.cjs +27696 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +12 -0
- package/dist/connector.d.ts +108 -0
- package/dist/connector.js +517 -0
- package/dist/endpoint.d.ts +9 -0
- package/dist/endpoint.js +38 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/pairing.d.ts +29 -0
- package/dist/pairing.js +53 -0
- package/dist/runtime.d.ts +14 -0
- package/dist/runtime.js +12 -0
- package/docs/cloud-pairing.md +50 -0
- package/package.json +45 -3
package/dist/agent.js
ADDED
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { Readable, Writable } from 'node:stream';
|
|
6
|
+
import { agentExecutableCandidatePaths, AgentExecutableResolver, executableFileCandidates, probeAgentExecutable, } from '@mearl/agent-runtime/execution';
|
|
7
|
+
import { agentSpawnCommand, createAgentChildEnv } from '@mearl/agent-runtime/execution';
|
|
8
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
10
|
+
const MAX_MODEL_CHARS = 256;
|
|
11
|
+
const QODER_NOT_FOUND_ERROR = 'Qoder CLI was not found. Install Qoder CLI or set MEARL_QODER_PATH.';
|
|
12
|
+
function errorMessage(error) {
|
|
13
|
+
return error instanceof Error ? error.message : String(error);
|
|
14
|
+
}
|
|
15
|
+
function contentText(content) {
|
|
16
|
+
return content.type === 'text' ? content.text : null;
|
|
17
|
+
}
|
|
18
|
+
function planText(entries) {
|
|
19
|
+
return entries
|
|
20
|
+
.map(entry => {
|
|
21
|
+
const marker = entry.status === 'completed' ? '✓' : entry.status === 'in_progress' ? '→' : '•';
|
|
22
|
+
return `${marker} ${entry.content}`;
|
|
23
|
+
})
|
|
24
|
+
.join('\n');
|
|
25
|
+
}
|
|
26
|
+
export function qoderModelInfo(configOptions) {
|
|
27
|
+
const modelOption = configOptions?.find(option => option.category === 'model' && option.type === 'select');
|
|
28
|
+
if (!modelOption || typeof modelOption.currentValue !== 'string')
|
|
29
|
+
return undefined;
|
|
30
|
+
const id = modelOption.currentValue.trim().slice(0, MAX_MODEL_CHARS);
|
|
31
|
+
if (!id)
|
|
32
|
+
return undefined;
|
|
33
|
+
const options = modelOption.options.flatMap(option => 'options' in option ? option.options : [option]);
|
|
34
|
+
const displayName = options
|
|
35
|
+
.find(option => option.value === modelOption.currentValue)
|
|
36
|
+
?.name.trim()
|
|
37
|
+
.slice(0, MAX_MODEL_CHARS);
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
...(displayName && displayName !== id ? { displayName } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function parseQoderSkillsList(output, cwd, home = os.homedir()) {
|
|
44
|
+
const skills = [];
|
|
45
|
+
let current;
|
|
46
|
+
const finish = () => {
|
|
47
|
+
if (!current)
|
|
48
|
+
return;
|
|
49
|
+
const skillPath = current.path || (current.builtIn ? 'bundled' : '');
|
|
50
|
+
const relativeToCwd = skillPath && path.isAbsolute(skillPath) ? path.relative(cwd, skillPath) : '';
|
|
51
|
+
const inProject = Boolean(relativeToCwd) &&
|
|
52
|
+
relativeToCwd !== '..' &&
|
|
53
|
+
!relativeToCwd.startsWith(`..${path.sep}`) &&
|
|
54
|
+
!path.isAbsolute(relativeToCwd);
|
|
55
|
+
skills.push({
|
|
56
|
+
name: current.name,
|
|
57
|
+
path: skillPath,
|
|
58
|
+
scope: current.builtIn ? 'built-in' : cwd !== home && inProject ? 'project' : 'global',
|
|
59
|
+
});
|
|
60
|
+
current = undefined;
|
|
61
|
+
};
|
|
62
|
+
for (const line of output.split(/\r?\n/)) {
|
|
63
|
+
const heading = /^(\S+)\s+\[(Enabled|Disabled)\](?:\s+\[Built-in\])?\s*$/.exec(line);
|
|
64
|
+
if (heading?.[1] && heading[2]) {
|
|
65
|
+
finish();
|
|
66
|
+
current = {
|
|
67
|
+
name: heading[1],
|
|
68
|
+
builtIn: line.includes('[Built-in]'),
|
|
69
|
+
};
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!current)
|
|
73
|
+
continue;
|
|
74
|
+
const skillPath = /^\s+Location:\s*(.*)$/.exec(line)?.[1];
|
|
75
|
+
if (skillPath !== undefined)
|
|
76
|
+
current.path = skillPath;
|
|
77
|
+
}
|
|
78
|
+
finish();
|
|
79
|
+
return skills;
|
|
80
|
+
}
|
|
81
|
+
function qoderManagedExecutableCandidates(home, readdirSync = fs.readdirSync) {
|
|
82
|
+
const directory = path.join(home, '.qoder', 'bin', 'qodercli');
|
|
83
|
+
try {
|
|
84
|
+
return readdirSync(directory, { withFileTypes: true })
|
|
85
|
+
.filter(entry => entry.isFile() && entry.name.startsWith('qodercli-'))
|
|
86
|
+
.sort((left, right) => right.name.localeCompare(left.name, undefined, { numeric: true }))
|
|
87
|
+
.map(entry => path.join(directory, entry.name));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function isPermissionResponse(value) {
|
|
94
|
+
if (!value || typeof value !== 'object')
|
|
95
|
+
return false;
|
|
96
|
+
const outcome = value.outcome;
|
|
97
|
+
if (!outcome || typeof outcome !== 'object')
|
|
98
|
+
return false;
|
|
99
|
+
if (outcome.outcome === 'cancelled')
|
|
100
|
+
return true;
|
|
101
|
+
return outcome.outcome === 'selected' && typeof outcome.optionId === 'string';
|
|
102
|
+
}
|
|
103
|
+
export function qoderInstallCommand(platform = process.platform) {
|
|
104
|
+
return platform === 'win32'
|
|
105
|
+
? 'npm install -g @qoder-ai/qodercli'
|
|
106
|
+
: 'curl -fsSL https://qoder.com/install | bash';
|
|
107
|
+
}
|
|
108
|
+
export function qoderExecutableCandidatePaths(options = {}) {
|
|
109
|
+
const env = options.env ?? process.env;
|
|
110
|
+
const home = (options.homedir ?? os.homedir)();
|
|
111
|
+
const platform = options.platform ?? process.platform;
|
|
112
|
+
const executableNames = platform === 'win32' ? ['qodercli.exe', 'qodercli.cmd'] : ['qodercli'];
|
|
113
|
+
return agentExecutableCandidatePaths({
|
|
114
|
+
executableNames,
|
|
115
|
+
configuredPath: env.MEARL_QODER_PATH,
|
|
116
|
+
env,
|
|
117
|
+
extraUserCandidates: [
|
|
118
|
+
...qoderManagedExecutableCandidates(home, options.readdirSync),
|
|
119
|
+
...executableNames.map(name => path.join(home, '.qoder', 'bin', name)),
|
|
120
|
+
],
|
|
121
|
+
home,
|
|
122
|
+
platform,
|
|
123
|
+
processExecPath: options.processExecPath,
|
|
124
|
+
readdirSync: options.readdirSync,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function sessionOptions(options) {
|
|
128
|
+
const cwd = path.resolve(options.cwd || os.homedir());
|
|
129
|
+
return {
|
|
130
|
+
...options,
|
|
131
|
+
cwd,
|
|
132
|
+
instructionsSent: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function parsedTimestamp(value) {
|
|
136
|
+
if (!value)
|
|
137
|
+
return undefined;
|
|
138
|
+
const timestamp = Date.parse(value);
|
|
139
|
+
return Number.isNaN(timestamp) ? undefined : timestamp;
|
|
140
|
+
}
|
|
141
|
+
function historyItemId(capture, kind, sourceId) {
|
|
142
|
+
if (sourceId)
|
|
143
|
+
return `qoder_history_${kind}_${sourceId}`;
|
|
144
|
+
const id = `qoder_history_${kind}_${capture.nextId}`;
|
|
145
|
+
capture.nextId += 1;
|
|
146
|
+
return id;
|
|
147
|
+
}
|
|
148
|
+
function historyMessageKey(capture, role, messageId) {
|
|
149
|
+
if (messageId) {
|
|
150
|
+
capture.lastAnonymousMessage = undefined;
|
|
151
|
+
return `${role}:${messageId}`;
|
|
152
|
+
}
|
|
153
|
+
if (capture.lastAnonymousMessage?.role === role)
|
|
154
|
+
return capture.lastAnonymousMessage.key;
|
|
155
|
+
const key = `${role}:anonymous:${capture.nextId}`;
|
|
156
|
+
capture.nextId += 1;
|
|
157
|
+
capture.lastAnonymousMessage = { role, key };
|
|
158
|
+
return key;
|
|
159
|
+
}
|
|
160
|
+
function captureHistoryMessage(capture, role, update) {
|
|
161
|
+
const text = contentText(update.content);
|
|
162
|
+
const imageCount = update.content.type === 'image' ? 1 : 0;
|
|
163
|
+
if (!text && imageCount === 0)
|
|
164
|
+
return;
|
|
165
|
+
const key = historyMessageKey(capture, role, update.messageId);
|
|
166
|
+
const existing = capture.messageEvents.get(key);
|
|
167
|
+
if (existing) {
|
|
168
|
+
if (role === 'user') {
|
|
169
|
+
const params = existing.params;
|
|
170
|
+
if (text)
|
|
171
|
+
params.text += text;
|
|
172
|
+
if (imageCount > 0)
|
|
173
|
+
params.imageCount = (params.imageCount || 0) + imageCount;
|
|
174
|
+
}
|
|
175
|
+
else if (role === 'agent') {
|
|
176
|
+
const params = existing.params;
|
|
177
|
+
if (text)
|
|
178
|
+
params.item.text += text;
|
|
179
|
+
}
|
|
180
|
+
else if (text) {
|
|
181
|
+
const params = existing.params;
|
|
182
|
+
params.item.summary[0] = `${params.item.summary[0] || ''}${text}`;
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const itemId = historyItemId(capture, role, update.messageId);
|
|
187
|
+
const event = role === 'user'
|
|
188
|
+
? {
|
|
189
|
+
providerSessionId: capture.providerSessionId,
|
|
190
|
+
kind: 'lifecycle',
|
|
191
|
+
method: 'chat/userMessage',
|
|
192
|
+
params: { text: text || '', ...(imageCount > 0 ? { imageCount } : {}) },
|
|
193
|
+
}
|
|
194
|
+
: {
|
|
195
|
+
providerSessionId: capture.providerSessionId,
|
|
196
|
+
kind: 'notification',
|
|
197
|
+
method: 'item/completed',
|
|
198
|
+
params: {
|
|
199
|
+
item: {
|
|
200
|
+
id: itemId,
|
|
201
|
+
type: role === 'agent' ? 'agentMessage' : 'reasoning',
|
|
202
|
+
...(role === 'agent' ? { text: text || '' } : { summary: [text || ''] }),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
capture.messageEvents.set(key, event);
|
|
207
|
+
capture.events.push(event);
|
|
208
|
+
}
|
|
209
|
+
function captureHistoryUpdate(capture, update) {
|
|
210
|
+
switch (update.sessionUpdate) {
|
|
211
|
+
case 'user_message_chunk':
|
|
212
|
+
captureHistoryMessage(capture, 'user', update);
|
|
213
|
+
return;
|
|
214
|
+
case 'agent_message_chunk':
|
|
215
|
+
captureHistoryMessage(capture, 'agent', update);
|
|
216
|
+
return;
|
|
217
|
+
case 'agent_thought_chunk':
|
|
218
|
+
captureHistoryMessage(capture, 'reasoning', update);
|
|
219
|
+
return;
|
|
220
|
+
case 'tool_call':
|
|
221
|
+
case 'tool_call_update': {
|
|
222
|
+
capture.lastAnonymousMessage = undefined;
|
|
223
|
+
const completed = update.status === 'completed' || update.status === 'failed';
|
|
224
|
+
const existing = capture.toolEvents.get(update.toolCallId);
|
|
225
|
+
const existingItem = existing
|
|
226
|
+
? existing.params.item
|
|
227
|
+
: undefined;
|
|
228
|
+
const item = {
|
|
229
|
+
id: historyItemId(capture, 'tool', update.toolCallId),
|
|
230
|
+
type: 'dynamicToolCall',
|
|
231
|
+
namespace: 'qoder',
|
|
232
|
+
tool: update.title || existingItem?.tool || update.toolCallId,
|
|
233
|
+
status: update.status || (completed ? 'completed' : 'inProgress'),
|
|
234
|
+
kind: update.kind || existingItem?.kind,
|
|
235
|
+
};
|
|
236
|
+
if (existing) {
|
|
237
|
+
existing.method = completed ? 'item/completed' : 'item/started';
|
|
238
|
+
existing.params = { item };
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const event = {
|
|
242
|
+
providerSessionId: capture.providerSessionId,
|
|
243
|
+
kind: 'notification',
|
|
244
|
+
method: completed ? 'item/completed' : 'item/started',
|
|
245
|
+
params: { item },
|
|
246
|
+
};
|
|
247
|
+
capture.toolEvents.set(update.toolCallId, event);
|
|
248
|
+
capture.events.push(event);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
case 'plan': {
|
|
253
|
+
capture.lastAnonymousMessage = undefined;
|
|
254
|
+
const item = {
|
|
255
|
+
id: historyItemId(capture, 'plan', 'current'),
|
|
256
|
+
type: 'plan',
|
|
257
|
+
text: planText(update.entries),
|
|
258
|
+
};
|
|
259
|
+
if (capture.planEvent) {
|
|
260
|
+
capture.planEvent.params = { item };
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
capture.planEvent = {
|
|
264
|
+
providerSessionId: capture.providerSessionId,
|
|
265
|
+
kind: 'notification',
|
|
266
|
+
method: 'item/completed',
|
|
267
|
+
params: { item },
|
|
268
|
+
};
|
|
269
|
+
capture.events.push(capture.planEvent);
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
default:
|
|
274
|
+
capture.lastAnonymousMessage = undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export class QoderAgentProvider {
|
|
278
|
+
log;
|
|
279
|
+
clientVersion;
|
|
280
|
+
id = 'qoder';
|
|
281
|
+
process = null;
|
|
282
|
+
sdk;
|
|
283
|
+
connection = null;
|
|
284
|
+
context = null;
|
|
285
|
+
initializeResponse = null;
|
|
286
|
+
startPromise = null;
|
|
287
|
+
lifecycle = 0;
|
|
288
|
+
listeners = new Set();
|
|
289
|
+
sessions = new Map();
|
|
290
|
+
pendingPermissions = new Map();
|
|
291
|
+
historyCaptures = new Map();
|
|
292
|
+
nextTurnId = 1;
|
|
293
|
+
executableResolver;
|
|
294
|
+
constructor(log, clientVersion) {
|
|
295
|
+
this.log = log;
|
|
296
|
+
this.clientVersion = clientVersion;
|
|
297
|
+
this.executableResolver = new AgentExecutableResolver({
|
|
298
|
+
candidates: () => this.resolveExecutableCandidates(),
|
|
299
|
+
probe: executable => this.probeExecutable(executable),
|
|
300
|
+
notFoundError: QODER_NOT_FOUND_ERROR,
|
|
301
|
+
unusableError: 'Qoder CLI candidates were found, but none is usable.',
|
|
302
|
+
onFailure: (executable, error) => {
|
|
303
|
+
this.log('[Qoder] Skipping unusable CLI:', executable, error);
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async getDescriptor() {
|
|
308
|
+
const { error } = await this.resolveExecutable();
|
|
309
|
+
return {
|
|
310
|
+
id: this.id,
|
|
311
|
+
name: 'Qoder',
|
|
312
|
+
available: error === null,
|
|
313
|
+
capabilities: {
|
|
314
|
+
streaming: true,
|
|
315
|
+
resume: true,
|
|
316
|
+
listSessions: 'runtime',
|
|
317
|
+
refreshHistory: false,
|
|
318
|
+
imageInput: 'runtime',
|
|
319
|
+
interrupt: true,
|
|
320
|
+
requests: true,
|
|
321
|
+
},
|
|
322
|
+
...(error ? { error } : {}),
|
|
323
|
+
setup: {
|
|
324
|
+
installCommand: qoderInstallCommand(),
|
|
325
|
+
loginCommand: 'qodercli',
|
|
326
|
+
docsUrl: 'https://docs.qoder.com/en/cli/quick-start',
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
async prepare() {
|
|
331
|
+
await this.ensureStarted();
|
|
332
|
+
}
|
|
333
|
+
onEvent(listener) {
|
|
334
|
+
this.listeners.add(listener);
|
|
335
|
+
return () => this.listeners.delete(listener);
|
|
336
|
+
}
|
|
337
|
+
async listSessions(options) {
|
|
338
|
+
await this.ensureStarted();
|
|
339
|
+
if (!this.initializeResponse?.agentCapabilities?.sessionCapabilities?.list) {
|
|
340
|
+
throw new Error('This Qoder CLI version cannot list existing ACP sessions');
|
|
341
|
+
}
|
|
342
|
+
const result = await this.withTimeout(this.getContext().request(this.sdk.methods.agent.session.list, {
|
|
343
|
+
cursor: options.cursor ?? null,
|
|
344
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
345
|
+
}), 'Qoder session list');
|
|
346
|
+
return {
|
|
347
|
+
sessions: result.sessions.map(session => {
|
|
348
|
+
const updatedAt = parsedTimestamp(session.updatedAt);
|
|
349
|
+
return {
|
|
350
|
+
providerSessionId: session.sessionId,
|
|
351
|
+
...(session.title ? { title: session.title } : {}),
|
|
352
|
+
...(session.cwd ? { cwd: session.cwd } : {}),
|
|
353
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
354
|
+
};
|
|
355
|
+
}),
|
|
356
|
+
...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async listSkills(cwd) {
|
|
360
|
+
const resolution = await this.resolveExecutable();
|
|
361
|
+
if (resolution.executable === null) {
|
|
362
|
+
throw new Error(resolution.error);
|
|
363
|
+
}
|
|
364
|
+
const { executable } = resolution;
|
|
365
|
+
const resolvedCwd = cwd || os.homedir();
|
|
366
|
+
const output = await this.runSkillsList(executable, resolvedCwd);
|
|
367
|
+
return parseQoderSkillsList(output, resolvedCwd);
|
|
368
|
+
}
|
|
369
|
+
async startSession(options) {
|
|
370
|
+
const session = sessionOptions(options);
|
|
371
|
+
await this.ensureStarted();
|
|
372
|
+
const result = await this.withTimeout(this.getContext().request(this.sdk.methods.agent.session.new, {
|
|
373
|
+
cwd: session.cwd,
|
|
374
|
+
mcpServers: [],
|
|
375
|
+
}), 'Qoder session creation');
|
|
376
|
+
if (!result.sessionId)
|
|
377
|
+
throw new Error('Qoder did not return a session id');
|
|
378
|
+
const model = qoderModelInfo(result.configOptions);
|
|
379
|
+
if (model)
|
|
380
|
+
session.model = model;
|
|
381
|
+
this.sessions.set(result.sessionId, session);
|
|
382
|
+
return result.sessionId;
|
|
383
|
+
}
|
|
384
|
+
async resumeSession(providerSessionId, options) {
|
|
385
|
+
const session = sessionOptions(options);
|
|
386
|
+
await this.ensureStarted();
|
|
387
|
+
const capabilities = this.initializeResponse?.agentCapabilities;
|
|
388
|
+
let configOptions;
|
|
389
|
+
const historyCapture = capabilities?.loadSession
|
|
390
|
+
? {
|
|
391
|
+
providerSessionId,
|
|
392
|
+
events: [],
|
|
393
|
+
messageEvents: new Map(),
|
|
394
|
+
toolEvents: new Map(),
|
|
395
|
+
nextId: 1,
|
|
396
|
+
}
|
|
397
|
+
: null;
|
|
398
|
+
if (historyCapture)
|
|
399
|
+
this.historyCaptures.set(providerSessionId, historyCapture);
|
|
400
|
+
try {
|
|
401
|
+
if (historyCapture) {
|
|
402
|
+
const result = await this.withTimeout(this.getContext().request(this.sdk.methods.agent.session.load, {
|
|
403
|
+
sessionId: providerSessionId,
|
|
404
|
+
cwd: session.cwd,
|
|
405
|
+
mcpServers: [],
|
|
406
|
+
}), 'Qoder session load');
|
|
407
|
+
configOptions = result.configOptions;
|
|
408
|
+
}
|
|
409
|
+
else if (capabilities?.sessionCapabilities?.resume) {
|
|
410
|
+
const result = await this.withTimeout(this.getContext().request(this.sdk.methods.agent.session.resume, {
|
|
411
|
+
sessionId: providerSessionId,
|
|
412
|
+
cwd: session.cwd,
|
|
413
|
+
mcpServers: [],
|
|
414
|
+
}), 'Qoder session resume');
|
|
415
|
+
configOptions = result.configOptions;
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
throw new Error('This Qoder CLI version cannot resume an existing ACP session');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
this.historyCaptures.delete(providerSessionId);
|
|
423
|
+
}
|
|
424
|
+
const model = qoderModelInfo(configOptions);
|
|
425
|
+
if (model)
|
|
426
|
+
session.model = model;
|
|
427
|
+
this.sessions.set(providerSessionId, session);
|
|
428
|
+
return {
|
|
429
|
+
providerSessionId,
|
|
430
|
+
...(historyCapture ? { history: historyCapture.events, historyComplete: true } : {}),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
getSessionModel(providerSessionId) {
|
|
434
|
+
const model = this.sessions.get(providerSessionId)?.model;
|
|
435
|
+
return model ? { ...model } : undefined;
|
|
436
|
+
}
|
|
437
|
+
async validateMessageInput(images) {
|
|
438
|
+
if (images.length === 0)
|
|
439
|
+
return;
|
|
440
|
+
await this.ensureStarted();
|
|
441
|
+
this.assertImageInputSupported();
|
|
442
|
+
}
|
|
443
|
+
async sendMessage(providerSessionId, message, images = []) {
|
|
444
|
+
await this.ensureStarted();
|
|
445
|
+
const session = this.sessions.get(providerSessionId);
|
|
446
|
+
if (!session)
|
|
447
|
+
throw new Error(`Unknown Qoder session: ${providerSessionId}`);
|
|
448
|
+
if (session.activeTurn)
|
|
449
|
+
throw new Error('The current Qoder turn has not finished');
|
|
450
|
+
if (images.length > 0)
|
|
451
|
+
this.assertImageInputSupported();
|
|
452
|
+
const turnId = `qoder_turn_${Date.now()}_${this.nextTurnId++}`;
|
|
453
|
+
const turn = {
|
|
454
|
+
id: turnId,
|
|
455
|
+
assistantItemId: `${turnId}_message`,
|
|
456
|
+
assistantText: '',
|
|
457
|
+
reasoningItemId: `${turnId}_reasoning`,
|
|
458
|
+
reasoningText: '',
|
|
459
|
+
reasoningStarted: false,
|
|
460
|
+
planItemId: `${turnId}_plan`,
|
|
461
|
+
toolTitles: new Map(),
|
|
462
|
+
};
|
|
463
|
+
session.activeTurn = turn;
|
|
464
|
+
const prompt = !session.instructionsSent && session.developerInstructions
|
|
465
|
+
? `${session.developerInstructions}\n\nUser request:\n${message}`
|
|
466
|
+
: message;
|
|
467
|
+
const promptContent = [
|
|
468
|
+
...(prompt ? [{ type: 'text', text: prompt }] : []),
|
|
469
|
+
...images.map(image => ({
|
|
470
|
+
type: 'image',
|
|
471
|
+
data: image.data,
|
|
472
|
+
mimeType: image.mimeType,
|
|
473
|
+
})),
|
|
474
|
+
];
|
|
475
|
+
session.instructionsSent = true;
|
|
476
|
+
this.emit({
|
|
477
|
+
providerSessionId,
|
|
478
|
+
kind: 'notification',
|
|
479
|
+
method: 'turn/started',
|
|
480
|
+
params: { turn: { id: turnId, status: 'inProgress' } },
|
|
481
|
+
});
|
|
482
|
+
void this.runPrompt(providerSessionId, session, turn, promptContent);
|
|
483
|
+
return { turnId };
|
|
484
|
+
}
|
|
485
|
+
async interrupt(providerSessionId, _turnId) {
|
|
486
|
+
await this.ensureStarted();
|
|
487
|
+
await this.getContext().notify(this.sdk.methods.agent.session.cancel, {
|
|
488
|
+
sessionId: providerSessionId,
|
|
489
|
+
});
|
|
490
|
+
this.cancelPermissions(providerSessionId);
|
|
491
|
+
}
|
|
492
|
+
async closeSession(providerSessionId) {
|
|
493
|
+
const session = this.sessions.get(providerSessionId);
|
|
494
|
+
if (session?.activeTurn && this.context) {
|
|
495
|
+
await this.context
|
|
496
|
+
.notify(this.sdk.methods.agent.session.cancel, { sessionId: providerSessionId })
|
|
497
|
+
.catch(() => { });
|
|
498
|
+
}
|
|
499
|
+
this.cancelPermissions(providerSessionId);
|
|
500
|
+
this.sessions.delete(providerSessionId);
|
|
501
|
+
}
|
|
502
|
+
respondToRequest(requestId, result) {
|
|
503
|
+
const pending = this.pendingPermissions.get(requestId);
|
|
504
|
+
if (!pending)
|
|
505
|
+
throw new Error(`Unknown Qoder permission request: ${String(requestId)}`);
|
|
506
|
+
if (!isPermissionResponse(result)) {
|
|
507
|
+
throw new Error('Invalid Qoder permission response');
|
|
508
|
+
}
|
|
509
|
+
this.pendingPermissions.delete(requestId);
|
|
510
|
+
pending.resolve(result);
|
|
511
|
+
this.emit({
|
|
512
|
+
providerSessionId: pending.providerSessionId,
|
|
513
|
+
kind: 'notification',
|
|
514
|
+
method: 'serverRequest/resolved',
|
|
515
|
+
params: { requestId },
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
dispose() {
|
|
519
|
+
this.lifecycle++;
|
|
520
|
+
for (const pending of this.pendingPermissions.values()) {
|
|
521
|
+
pending.resolve({ outcome: { outcome: 'cancelled' } });
|
|
522
|
+
}
|
|
523
|
+
this.pendingPermissions.clear();
|
|
524
|
+
this.historyCaptures.clear();
|
|
525
|
+
this.sessions.clear();
|
|
526
|
+
const connection = this.connection;
|
|
527
|
+
const child = this.process;
|
|
528
|
+
this.process = null;
|
|
529
|
+
this.connection = null;
|
|
530
|
+
this.context = null;
|
|
531
|
+
this.initializeResponse = null;
|
|
532
|
+
this.startPromise = null;
|
|
533
|
+
this.executableResolver.reset();
|
|
534
|
+
connection?.close(new Error('Qoder provider disposed'));
|
|
535
|
+
if (child && !child.killed)
|
|
536
|
+
child.kill('SIGTERM');
|
|
537
|
+
}
|
|
538
|
+
async ensureStarted() {
|
|
539
|
+
if (this.process && !this.process.killed && this.context)
|
|
540
|
+
return;
|
|
541
|
+
if (this.startPromise)
|
|
542
|
+
return this.startPromise;
|
|
543
|
+
const pending = this.start();
|
|
544
|
+
this.startPromise = pending;
|
|
545
|
+
try {
|
|
546
|
+
await pending;
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
if (this.startPromise === pending)
|
|
550
|
+
this.startPromise = null;
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async start() {
|
|
555
|
+
const lifecycle = this.lifecycle;
|
|
556
|
+
const resolution = await this.resolveExecutable();
|
|
557
|
+
if (lifecycle !== this.lifecycle)
|
|
558
|
+
throw new Error('Qoder provider disposed during startup');
|
|
559
|
+
if (resolution.executable === null) {
|
|
560
|
+
throw new Error(resolution.error);
|
|
561
|
+
}
|
|
562
|
+
const { executable } = resolution;
|
|
563
|
+
const sdk = await import('@agentclientprotocol/sdk');
|
|
564
|
+
if (lifecycle !== this.lifecycle)
|
|
565
|
+
throw new Error('Qoder provider disposed during startup');
|
|
566
|
+
this.sdk = sdk;
|
|
567
|
+
const launch = agentSpawnCommand(executable, ['--acp']);
|
|
568
|
+
const child = spawn(launch.command, launch.args, {
|
|
569
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
570
|
+
windowsHide: true,
|
|
571
|
+
env: createAgentChildEnv(),
|
|
572
|
+
});
|
|
573
|
+
this.process = child;
|
|
574
|
+
child.stderr.on('data', chunk => {
|
|
575
|
+
const message = chunk.toString('utf8').trim();
|
|
576
|
+
if (message)
|
|
577
|
+
this.log('[Qoder]', message);
|
|
578
|
+
});
|
|
579
|
+
child.once('error', error => this.handleExit(child, error));
|
|
580
|
+
child.once('exit', (code, signal) => {
|
|
581
|
+
this.handleExit(child, new Error(`Qoder ACP exited (code=${String(code)}, signal=${String(signal)})`));
|
|
582
|
+
});
|
|
583
|
+
const stream = sdk.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
584
|
+
const app = sdk
|
|
585
|
+
.client({ name: 'mearl' })
|
|
586
|
+
.onRequest(this.sdk.methods.client.session.requestPermission, context => this.handlePermissionRequest(context.requestId, context.params))
|
|
587
|
+
.onNotification(this.sdk.methods.client.session.update, context => {
|
|
588
|
+
this.handleSessionUpdate(context.params);
|
|
589
|
+
});
|
|
590
|
+
const connection = app.connect(stream);
|
|
591
|
+
this.connection = connection;
|
|
592
|
+
this.context = connection.agent;
|
|
593
|
+
try {
|
|
594
|
+
const response = await this.withTimeout(connection.agent.request(this.sdk.methods.agent.initialize, {
|
|
595
|
+
protocolVersion: sdk.PROTOCOL_VERSION,
|
|
596
|
+
clientCapabilities: {},
|
|
597
|
+
clientInfo: {
|
|
598
|
+
name: 'mearl',
|
|
599
|
+
title: 'Mearl',
|
|
600
|
+
version: this.clientVersion,
|
|
601
|
+
},
|
|
602
|
+
}), 'Qoder ACP initialization');
|
|
603
|
+
if (lifecycle !== this.lifecycle || this.process !== child)
|
|
604
|
+
throw new Error('Qoder provider disposed during startup');
|
|
605
|
+
this.initializeResponse = response;
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
if (this.process === child) {
|
|
609
|
+
this.process = null;
|
|
610
|
+
this.connection = null;
|
|
611
|
+
this.context = null;
|
|
612
|
+
this.executableResolver.reset();
|
|
613
|
+
}
|
|
614
|
+
connection.close(error);
|
|
615
|
+
if (!child.killed)
|
|
616
|
+
child.kill('SIGTERM');
|
|
617
|
+
throw new Error(`Qoder ACP failed to initialize. Start "qodercli", enter "/login", and try again: ${errorMessage(error)}`, { cause: error });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async runPrompt(providerSessionId, session, turn, prompt) {
|
|
621
|
+
try {
|
|
622
|
+
const response = await this.getContext().request(this.sdk.methods.agent.session.prompt, {
|
|
623
|
+
sessionId: providerSessionId,
|
|
624
|
+
prompt,
|
|
625
|
+
});
|
|
626
|
+
if (turn.reasoningStarted) {
|
|
627
|
+
this.emit({
|
|
628
|
+
providerSessionId,
|
|
629
|
+
kind: 'notification',
|
|
630
|
+
method: 'item/completed',
|
|
631
|
+
params: {
|
|
632
|
+
turnId: turn.id,
|
|
633
|
+
item: {
|
|
634
|
+
id: turn.reasoningItemId,
|
|
635
|
+
type: 'reasoning',
|
|
636
|
+
summary: turn.reasoningText ? [turn.reasoningText] : [],
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (turn.assistantText) {
|
|
642
|
+
this.emit({
|
|
643
|
+
providerSessionId,
|
|
644
|
+
kind: 'notification',
|
|
645
|
+
method: 'item/completed',
|
|
646
|
+
params: {
|
|
647
|
+
turnId: turn.id,
|
|
648
|
+
item: {
|
|
649
|
+
id: turn.assistantItemId,
|
|
650
|
+
type: 'agentMessage',
|
|
651
|
+
text: turn.assistantText,
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (response.usage) {
|
|
657
|
+
this.emit({
|
|
658
|
+
providerSessionId,
|
|
659
|
+
kind: 'notification',
|
|
660
|
+
method: 'thread/tokenUsage/updated',
|
|
661
|
+
params: { turnId: turn.id, usage: response.usage },
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
this.emit({
|
|
665
|
+
providerSessionId,
|
|
666
|
+
kind: 'notification',
|
|
667
|
+
method: 'turn/completed',
|
|
668
|
+
params: {
|
|
669
|
+
turn: {
|
|
670
|
+
id: turn.id,
|
|
671
|
+
status: response.stopReason === 'cancelled' ? 'cancelled' : 'completed',
|
|
672
|
+
stopReason: response.stopReason,
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
this.emit({
|
|
679
|
+
providerSessionId,
|
|
680
|
+
kind: 'error',
|
|
681
|
+
method: 'chat/error',
|
|
682
|
+
params: { turnId: turn.id, message: errorMessage(error) },
|
|
683
|
+
});
|
|
684
|
+
this.emit({
|
|
685
|
+
providerSessionId,
|
|
686
|
+
kind: 'notification',
|
|
687
|
+
method: 'turn/completed',
|
|
688
|
+
params: { turn: { id: turn.id, status: 'failed' } },
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
finally {
|
|
692
|
+
if (session.activeTurn === turn)
|
|
693
|
+
delete session.activeTurn;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
handleSessionUpdate(notification) {
|
|
697
|
+
const historyCapture = this.historyCaptures.get(notification.sessionId);
|
|
698
|
+
if (historyCapture) {
|
|
699
|
+
captureHistoryUpdate(historyCapture, notification.update);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const session = this.sessions.get(notification.sessionId);
|
|
703
|
+
const update = notification.update;
|
|
704
|
+
if (update.sessionUpdate === 'config_option_update') {
|
|
705
|
+
const model = qoderModelInfo(update.configOptions);
|
|
706
|
+
if (session) {
|
|
707
|
+
if (model)
|
|
708
|
+
session.model = model;
|
|
709
|
+
else
|
|
710
|
+
delete session.model;
|
|
711
|
+
}
|
|
712
|
+
this.emit({
|
|
713
|
+
providerSessionId: notification.sessionId,
|
|
714
|
+
kind: 'notification',
|
|
715
|
+
method: 'session/model/updated',
|
|
716
|
+
params: { model: model ?? null },
|
|
717
|
+
});
|
|
718
|
+
this.emit({
|
|
719
|
+
providerSessionId: notification.sessionId,
|
|
720
|
+
kind: 'notification',
|
|
721
|
+
method: 'qoder/sessionUpdate',
|
|
722
|
+
params: notification,
|
|
723
|
+
});
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const turn = session?.activeTurn;
|
|
727
|
+
if (!turn) {
|
|
728
|
+
this.emit({
|
|
729
|
+
providerSessionId: notification.sessionId,
|
|
730
|
+
kind: 'notification',
|
|
731
|
+
method: 'qoder/sessionUpdate',
|
|
732
|
+
params: notification,
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
switch (update.sessionUpdate) {
|
|
737
|
+
case 'agent_message_chunk': {
|
|
738
|
+
const delta = contentText(update.content);
|
|
739
|
+
if (!delta)
|
|
740
|
+
return;
|
|
741
|
+
turn.assistantText += delta;
|
|
742
|
+
this.emit({
|
|
743
|
+
providerSessionId: notification.sessionId,
|
|
744
|
+
kind: 'notification',
|
|
745
|
+
method: 'item/agentMessage/delta',
|
|
746
|
+
params: { turnId: turn.id, itemId: turn.assistantItemId, delta },
|
|
747
|
+
});
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
case 'agent_thought_chunk': {
|
|
751
|
+
const delta = contentText(update.content);
|
|
752
|
+
if (!delta)
|
|
753
|
+
return;
|
|
754
|
+
if (!turn.reasoningStarted) {
|
|
755
|
+
turn.reasoningStarted = true;
|
|
756
|
+
this.emit({
|
|
757
|
+
providerSessionId: notification.sessionId,
|
|
758
|
+
kind: 'notification',
|
|
759
|
+
method: 'item/started',
|
|
760
|
+
params: {
|
|
761
|
+
turnId: turn.id,
|
|
762
|
+
item: { id: turn.reasoningItemId, type: 'reasoning', summary: [] },
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
turn.reasoningText += delta;
|
|
767
|
+
this.emit({
|
|
768
|
+
providerSessionId: notification.sessionId,
|
|
769
|
+
kind: 'notification',
|
|
770
|
+
method: 'item/reasoning/summaryTextDelta',
|
|
771
|
+
params: { turnId: turn.id, itemId: turn.reasoningItemId, delta },
|
|
772
|
+
});
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
case 'tool_call':
|
|
776
|
+
turn.toolTitles.set(update.toolCallId, update.title);
|
|
777
|
+
this.emitToolUpdate(notification.sessionId, turn, update, update.title);
|
|
778
|
+
return;
|
|
779
|
+
case 'tool_call_update': {
|
|
780
|
+
const title = update.title || turn.toolTitles.get(update.toolCallId) || update.toolCallId;
|
|
781
|
+
turn.toolTitles.set(update.toolCallId, title);
|
|
782
|
+
this.emitToolUpdate(notification.sessionId, turn, update, title);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
case 'plan': {
|
|
786
|
+
const text = planText(update.entries);
|
|
787
|
+
this.emit({
|
|
788
|
+
providerSessionId: notification.sessionId,
|
|
789
|
+
kind: 'notification',
|
|
790
|
+
method: 'item/completed',
|
|
791
|
+
params: {
|
|
792
|
+
turnId: turn.id,
|
|
793
|
+
item: { id: turn.planItemId, type: 'plan', text },
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
case 'usage_update':
|
|
799
|
+
this.emit({
|
|
800
|
+
providerSessionId: notification.sessionId,
|
|
801
|
+
kind: 'notification',
|
|
802
|
+
method: 'thread/tokenUsage/updated',
|
|
803
|
+
params: { turnId: turn.id, usage: update },
|
|
804
|
+
});
|
|
805
|
+
return;
|
|
806
|
+
default:
|
|
807
|
+
this.emit({
|
|
808
|
+
providerSessionId: notification.sessionId,
|
|
809
|
+
kind: 'notification',
|
|
810
|
+
method: 'qoder/sessionUpdate',
|
|
811
|
+
params: { turnId: turn.id, update },
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
emitToolUpdate(providerSessionId, turn, update, title) {
|
|
816
|
+
const completed = update.status === 'completed' || update.status === 'failed';
|
|
817
|
+
this.emit({
|
|
818
|
+
providerSessionId,
|
|
819
|
+
kind: 'notification',
|
|
820
|
+
method: completed ? 'item/completed' : 'item/started',
|
|
821
|
+
params: {
|
|
822
|
+
turnId: turn.id,
|
|
823
|
+
item: {
|
|
824
|
+
id: update.toolCallId,
|
|
825
|
+
type: 'dynamicToolCall',
|
|
826
|
+
namespace: 'qoder',
|
|
827
|
+
tool: title,
|
|
828
|
+
status: update.status || (completed ? 'completed' : 'inProgress'),
|
|
829
|
+
kind: update.kind,
|
|
830
|
+
rawInput: update.rawInput,
|
|
831
|
+
rawOutput: update.rawOutput,
|
|
832
|
+
content: update.content,
|
|
833
|
+
locations: update.locations,
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
handlePermissionRequest(requestId, request) {
|
|
839
|
+
if (requestId === null) {
|
|
840
|
+
return Promise.resolve({ outcome: { outcome: 'cancelled' } });
|
|
841
|
+
}
|
|
842
|
+
return new Promise(resolve => {
|
|
843
|
+
this.pendingPermissions.set(requestId, {
|
|
844
|
+
providerSessionId: request.sessionId,
|
|
845
|
+
resolve,
|
|
846
|
+
});
|
|
847
|
+
this.emit({
|
|
848
|
+
providerSessionId: request.sessionId,
|
|
849
|
+
kind: 'request',
|
|
850
|
+
method: 'acp/requestPermission',
|
|
851
|
+
params: request,
|
|
852
|
+
requestId,
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
cancelPermissions(providerSessionId) {
|
|
857
|
+
for (const [requestId, pending] of this.pendingPermissions) {
|
|
858
|
+
if (pending.providerSessionId !== providerSessionId)
|
|
859
|
+
continue;
|
|
860
|
+
this.pendingPermissions.delete(requestId);
|
|
861
|
+
pending.resolve({ outcome: { outcome: 'cancelled' } });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
handleExit(child, error) {
|
|
865
|
+
if (this.process !== child)
|
|
866
|
+
return;
|
|
867
|
+
this.process = null;
|
|
868
|
+
this.connection?.close(error);
|
|
869
|
+
this.connection = null;
|
|
870
|
+
this.context = null;
|
|
871
|
+
this.initializeResponse = null;
|
|
872
|
+
this.startPromise = null;
|
|
873
|
+
this.executableResolver.reset();
|
|
874
|
+
for (const providerSessionId of this.sessions.keys()) {
|
|
875
|
+
this.cancelPermissions(providerSessionId);
|
|
876
|
+
this.emit({
|
|
877
|
+
providerSessionId,
|
|
878
|
+
kind: 'error',
|
|
879
|
+
method: 'provider/exited',
|
|
880
|
+
params: { message: error.message },
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
getContext() {
|
|
885
|
+
if (!this.context)
|
|
886
|
+
throw new Error('Qoder ACP is not running');
|
|
887
|
+
return this.context;
|
|
888
|
+
}
|
|
889
|
+
assertImageInputSupported() {
|
|
890
|
+
if (!this.initializeResponse?.agentCapabilities?.promptCapabilities?.image) {
|
|
891
|
+
throw new Error('This Qoder CLI version does not support image prompts');
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
withTimeout(promise, operation) {
|
|
895
|
+
return new Promise((resolve, reject) => {
|
|
896
|
+
const timer = setTimeout(() => reject(new Error(`${operation} timed out`)), REQUEST_TIMEOUT_MS);
|
|
897
|
+
promise.then(result => {
|
|
898
|
+
clearTimeout(timer);
|
|
899
|
+
resolve(result);
|
|
900
|
+
}, error => {
|
|
901
|
+
clearTimeout(timer);
|
|
902
|
+
reject(error);
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
probeExecutable(executable) {
|
|
907
|
+
return probeAgentExecutable(executable, {
|
|
908
|
+
args: ['--version'],
|
|
909
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
910
|
+
timeoutError: 'Timed out while checking Qoder CLI availability.',
|
|
911
|
+
startError: error => `Failed to start Qoder CLI: ${error.message}`,
|
|
912
|
+
exitError: code => `Qoder CLI check exited with code ${String(code)}`,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
runSkillsList(executable, cwd) {
|
|
916
|
+
return new Promise((resolve, reject) => {
|
|
917
|
+
const launch = agentSpawnCommand(executable, ['skill', 'list', '--all']);
|
|
918
|
+
const child = spawn(launch.command, launch.args, {
|
|
919
|
+
cwd,
|
|
920
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
921
|
+
windowsHide: true,
|
|
922
|
+
env: createAgentChildEnv(),
|
|
923
|
+
});
|
|
924
|
+
let stdout = '';
|
|
925
|
+
let stderr = '';
|
|
926
|
+
let settled = false;
|
|
927
|
+
const finish = (error) => {
|
|
928
|
+
if (settled)
|
|
929
|
+
return;
|
|
930
|
+
settled = true;
|
|
931
|
+
clearTimeout(timer);
|
|
932
|
+
if (error)
|
|
933
|
+
reject(error);
|
|
934
|
+
else
|
|
935
|
+
resolve(stdout);
|
|
936
|
+
};
|
|
937
|
+
const timer = setTimeout(() => {
|
|
938
|
+
child.kill('SIGTERM');
|
|
939
|
+
finish(new Error('Timed out while listing Qoder skills.'));
|
|
940
|
+
}, REQUEST_TIMEOUT_MS);
|
|
941
|
+
child.stdout.on('data', chunk => {
|
|
942
|
+
if (stdout.length < 1_000_000)
|
|
943
|
+
stdout += chunk.toString('utf8');
|
|
944
|
+
});
|
|
945
|
+
child.stderr.on('data', chunk => {
|
|
946
|
+
if (stderr.length < 10_000)
|
|
947
|
+
stderr += chunk.toString('utf8');
|
|
948
|
+
});
|
|
949
|
+
child.once('error', error => finish(error));
|
|
950
|
+
child.once('exit', code => {
|
|
951
|
+
if (code === 0)
|
|
952
|
+
finish();
|
|
953
|
+
else
|
|
954
|
+
finish(new Error(stderr.trim() || `Qoder skill list exited with code ${String(code)}`));
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
resolveExecutable() {
|
|
959
|
+
return this.executableResolver.resolve();
|
|
960
|
+
}
|
|
961
|
+
resolveExecutableCandidates() {
|
|
962
|
+
return executableFileCandidates(qoderExecutableCandidatePaths());
|
|
963
|
+
}
|
|
964
|
+
emit(event) {
|
|
965
|
+
for (const listener of this.listeners)
|
|
966
|
+
listener(event);
|
|
967
|
+
}
|
|
968
|
+
}
|