wraptc 1.0.2 → 1.0.4
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/bin/wraptc +4 -4
- package/package.json +2 -2
- package/src/cli/__tests__/cli.test.ts +337 -0
- package/src/cli/index.ts +149 -0
- package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
- package/src/core/__tests__/integration/integration.test.ts +241 -0
- package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
- package/src/core/__tests__/test-utils.ts +136 -0
- package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
- package/src/core/__tests__/unit/basic-test.test.ts +44 -0
- package/src/core/__tests__/unit/basic.test.ts +12 -0
- package/src/core/__tests__/unit/config.test.ts +244 -0
- package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
- package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
- package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
- package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
- package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
- package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
- package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
- package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
- package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
- package/src/core/__tests__/unit/router.test.ts +967 -0
- package/src/core/__tests__/unit/state.test.ts +1079 -0
- package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
- package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
- package/src/core/adapters/builtin/codex.ts +35 -0
- package/src/core/adapters/builtin/gemini.ts +34 -0
- package/src/core/adapters/builtin/index.ts +31 -0
- package/src/core/adapters/builtin/mock-coder.ts +148 -0
- package/src/core/adapters/builtin/qwen.ts +34 -0
- package/src/core/adapters/define.ts +48 -0
- package/src/core/adapters/index.ts +43 -0
- package/src/core/adapters/loader.ts +143 -0
- package/src/core/adapters/provider-bridge.ts +190 -0
- package/src/core/adapters/runner.ts +437 -0
- package/src/core/adapters/types.ts +172 -0
- package/src/core/config.ts +290 -0
- package/src/core/define-provider.ts +212 -0
- package/src/core/error-patterns.ts +147 -0
- package/src/core/index.ts +130 -0
- package/src/core/memory-monitor.ts +171 -0
- package/src/core/plugin/builtin.ts +87 -0
- package/src/core/plugin/index.ts +34 -0
- package/src/core/plugin/registry.ts +350 -0
- package/src/core/plugin/types.ts +209 -0
- package/src/core/provider-factory.ts +397 -0
- package/src/core/provider-loader.ts +171 -0
- package/src/core/providers/codex.ts +56 -0
- package/src/core/providers/configurable.ts +637 -0
- package/src/core/providers/custom.ts +261 -0
- package/src/core/providers/gemini.ts +41 -0
- package/src/core/providers/index.ts +383 -0
- package/src/core/providers/opencode.ts +168 -0
- package/src/core/providers/qwen-code.ts +41 -0
- package/src/core/router.ts +370 -0
- package/src/core/state.ts +258 -0
- package/src/core/types.ts +206 -0
- package/src/core/unified/capabilities.ts +184 -0
- package/src/core/unified/errors.ts +141 -0
- package/src/core/unified/index.ts +29 -0
- package/src/core/unified/output.ts +189 -0
- package/src/core/wrap-terminalcoder.ts +245 -0
- package/src/mcp/__tests__/server.test.ts +295 -0
- package/src/mcp/server.ts +284 -0
- package/src/test-fixtures/mock-coder.sh +194 -0
- package/dist/cli/index.js +0 -16501
- package/dist/core/index.js +0 -7531
- package/dist/mcp/server.js +0 -14568
- package/dist/wraptc-1.0.2.tgz +0 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdapterRunner - Core execution engine for adapters
|
|
3
|
+
*
|
|
4
|
+
* Handles process execution, input/output handling, streaming, and error classification.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_ERROR_PATTERNS } from "../define-provider";
|
|
8
|
+
import type { ProviderErrorKind, TokenUsage } from "../types";
|
|
9
|
+
import type {
|
|
10
|
+
AdapterDefinition,
|
|
11
|
+
AdapterInvokeOptions,
|
|
12
|
+
AdapterResult,
|
|
13
|
+
InputMethod,
|
|
14
|
+
OutputFormat,
|
|
15
|
+
StreamChunk,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* AdapterRunner executes adapter definitions against CLI tools
|
|
20
|
+
*/
|
|
21
|
+
export class AdapterRunner {
|
|
22
|
+
private def: AdapterDefinition;
|
|
23
|
+
|
|
24
|
+
constructor(definition: AdapterDefinition) {
|
|
25
|
+
this.def = definition;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run the adapter and return the result
|
|
30
|
+
*/
|
|
31
|
+
async run(prompt: string, opts: AdapterInvokeOptions = {}): Promise<AdapterResult> {
|
|
32
|
+
const args = this.buildArgs(prompt, opts);
|
|
33
|
+
const stdin = this.getStdin(prompt);
|
|
34
|
+
const timeout = opts.timeoutMs ?? this.def.timeoutMs ?? 60000;
|
|
35
|
+
|
|
36
|
+
// Set up timeout handling
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Build command array
|
|
42
|
+
const command = [this.def.binary];
|
|
43
|
+
if (this.def.subcommand) {
|
|
44
|
+
command.push(this.def.subcommand);
|
|
45
|
+
}
|
|
46
|
+
command.push(...args);
|
|
47
|
+
|
|
48
|
+
// Spawn the process
|
|
49
|
+
const proc = Bun.spawn(command, {
|
|
50
|
+
cwd: opts.cwd,
|
|
51
|
+
env: { ...process.env, ...this.def.env, ...opts.env },
|
|
52
|
+
stdin: stdin !== undefined ? "pipe" : "ignore",
|
|
53
|
+
stdout: "pipe",
|
|
54
|
+
stderr: "pipe",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Write stdin if provided
|
|
58
|
+
if (stdin !== undefined && proc.stdin) {
|
|
59
|
+
proc.stdin.write(stdin);
|
|
60
|
+
proc.stdin.end();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle abort signals
|
|
64
|
+
const combinedSignal =
|
|
65
|
+
opts.signal && controller.signal
|
|
66
|
+
? AbortSignal.any([opts.signal, controller.signal])
|
|
67
|
+
: (opts.signal ?? controller.signal);
|
|
68
|
+
|
|
69
|
+
combinedSignal.addEventListener("abort", () => proc.kill());
|
|
70
|
+
|
|
71
|
+
// Read stdout and stderr
|
|
72
|
+
const stdout = await new Response(proc.stdout).text();
|
|
73
|
+
const stderr = await new Response(proc.stderr).text();
|
|
74
|
+
const exitCode = await proc.exited;
|
|
75
|
+
|
|
76
|
+
// Check for errors
|
|
77
|
+
const allowed = this.def.allowedExitCodes ?? [0];
|
|
78
|
+
if (!allowed.includes(exitCode ?? -1)) {
|
|
79
|
+
const kind = this.classifyError(stderr, stdout, exitCode);
|
|
80
|
+
const error = new AdapterError(
|
|
81
|
+
`${this.def.displayName ?? this.def.id} failed with exit code ${exitCode}: ${stderr || stdout}`,
|
|
82
|
+
kind,
|
|
83
|
+
this.def.id,
|
|
84
|
+
{ stderr, stdout, exitCode },
|
|
85
|
+
);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return this.parseOutput(stdout);
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Run the adapter with streaming output
|
|
97
|
+
*/
|
|
98
|
+
async *runStream(prompt: string, opts: AdapterInvokeOptions = {}): AsyncGenerator<StreamChunk> {
|
|
99
|
+
const requestId = crypto.randomUUID();
|
|
100
|
+
yield { type: "start", requestId };
|
|
101
|
+
|
|
102
|
+
const args = this.buildArgs(prompt, opts);
|
|
103
|
+
const stdin = this.getStdin(prompt);
|
|
104
|
+
const timeout = opts.timeoutMs ?? this.def.timeoutMs ?? 60000;
|
|
105
|
+
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Build command array
|
|
111
|
+
const command = [this.def.binary];
|
|
112
|
+
if (this.def.subcommand) {
|
|
113
|
+
command.push(this.def.subcommand);
|
|
114
|
+
}
|
|
115
|
+
command.push(...args);
|
|
116
|
+
|
|
117
|
+
const proc = Bun.spawn(command, {
|
|
118
|
+
cwd: opts.cwd,
|
|
119
|
+
env: { ...process.env, ...this.def.env, ...opts.env },
|
|
120
|
+
stdin: stdin !== undefined ? "pipe" : "ignore",
|
|
121
|
+
stdout: "pipe",
|
|
122
|
+
stderr: "pipe",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (stdin !== undefined && proc.stdin) {
|
|
126
|
+
proc.stdin.write(stdin);
|
|
127
|
+
proc.stdin.end();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const combinedSignal =
|
|
131
|
+
opts.signal && controller.signal
|
|
132
|
+
? AbortSignal.any([opts.signal, controller.signal])
|
|
133
|
+
: (opts.signal ?? controller.signal);
|
|
134
|
+
|
|
135
|
+
combinedSignal.addEventListener("abort", () => proc.kill());
|
|
136
|
+
|
|
137
|
+
// Stream stdout
|
|
138
|
+
const fullTextChunks: string[] = [];
|
|
139
|
+
const streamingMode = this.def.streaming ?? "none";
|
|
140
|
+
|
|
141
|
+
if (proc.stdout) {
|
|
142
|
+
const reader = proc.stdout.getReader();
|
|
143
|
+
const decoder = new TextDecoder();
|
|
144
|
+
let buffer = "";
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
while (true) {
|
|
148
|
+
const { done, value } = await reader.read();
|
|
149
|
+
if (done) break;
|
|
150
|
+
|
|
151
|
+
const text = decoder.decode(value, { stream: true });
|
|
152
|
+
fullTextChunks.push(text);
|
|
153
|
+
|
|
154
|
+
if (streamingMode === "none") {
|
|
155
|
+
// No streaming - just accumulate
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (streamingMode === "line" || streamingMode === "jsonl") {
|
|
160
|
+
// Line-based streaming
|
|
161
|
+
buffer += text;
|
|
162
|
+
const lines = buffer.split("\n");
|
|
163
|
+
buffer = lines.pop() ?? ""; // Keep incomplete line in buffer
|
|
164
|
+
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
if (!line.trim()) continue;
|
|
167
|
+
const chunk = this.parseStreamLine(line);
|
|
168
|
+
if (chunk) yield chunk;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle remaining buffer
|
|
174
|
+
if (buffer.trim() && streamingMode !== "none") {
|
|
175
|
+
const chunk = this.parseStreamLine(buffer);
|
|
176
|
+
if (chunk) yield chunk;
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
reader.releaseLock();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Collect stderr
|
|
184
|
+
const stderr = await new Response(proc.stderr).text();
|
|
185
|
+
const exitCode = await proc.exited;
|
|
186
|
+
|
|
187
|
+
// Check for errors
|
|
188
|
+
const allowed = this.def.allowedExitCodes ?? [0];
|
|
189
|
+
if (!allowed.includes(exitCode ?? -1)) {
|
|
190
|
+
const kind = this.classifyError(stderr, fullTextChunks.join(""), exitCode);
|
|
191
|
+
yield {
|
|
192
|
+
type: "error",
|
|
193
|
+
message: stderr || `Exit code ${exitCode}`,
|
|
194
|
+
kind,
|
|
195
|
+
};
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Emit complete event
|
|
200
|
+
const fullText = fullTextChunks.join("");
|
|
201
|
+
const result = this.parseOutput(fullText);
|
|
202
|
+
yield { type: "complete", text: result.text, usage: result.usage };
|
|
203
|
+
} finally {
|
|
204
|
+
clearTimeout(timeoutId);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Build command line arguments
|
|
210
|
+
*/
|
|
211
|
+
private buildArgs(prompt: string, opts: AdapterInvokeOptions): string[] {
|
|
212
|
+
// Use custom hook if provided
|
|
213
|
+
if (this.def.buildArgs) {
|
|
214
|
+
return this.def.buildArgs(prompt, opts);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const args: string[] = [];
|
|
218
|
+
|
|
219
|
+
// Add static args
|
|
220
|
+
if (this.def.args) {
|
|
221
|
+
args.push(...this.def.args);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Add prompt based on input method
|
|
225
|
+
const input = this.def.input ?? "stdin";
|
|
226
|
+
|
|
227
|
+
if (input === "positional") {
|
|
228
|
+
args.push(prompt);
|
|
229
|
+
} else if (typeof input === "object" && input.flag) {
|
|
230
|
+
args.push(input.flag, prompt);
|
|
231
|
+
}
|
|
232
|
+
// stdin: handled separately via getStdin
|
|
233
|
+
|
|
234
|
+
return args;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get stdin input for the process
|
|
239
|
+
*/
|
|
240
|
+
private getStdin(prompt: string): string | undefined {
|
|
241
|
+
// Use custom hook if provided
|
|
242
|
+
if (this.def.getStdin) {
|
|
243
|
+
return this.def.getStdin(prompt);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const input = this.def.input ?? "stdin";
|
|
247
|
+
return input === "stdin" ? prompt : undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Parse output from the process
|
|
252
|
+
*/
|
|
253
|
+
private parseOutput(stdout: string): AdapterResult {
|
|
254
|
+
// Use custom hook if provided
|
|
255
|
+
if (this.def.parseOutput) {
|
|
256
|
+
return this.def.parseOutput(stdout);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const output = this.def.output ?? "text";
|
|
260
|
+
|
|
261
|
+
if (output === "text") {
|
|
262
|
+
return { text: stdout.trim() };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (output === "json") {
|
|
266
|
+
return this.parseJsonOutput(stdout);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// jsonPath extraction
|
|
270
|
+
if (typeof output === "object" && output.jsonPath) {
|
|
271
|
+
try {
|
|
272
|
+
const parsed = JSON.parse(stdout);
|
|
273
|
+
const text = this.getJsonPath(parsed, output.jsonPath);
|
|
274
|
+
return { text: String(text ?? stdout.trim()) };
|
|
275
|
+
} catch {
|
|
276
|
+
return { text: stdout.trim() };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { text: stdout.trim() };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Parse JSON output with common field extraction
|
|
285
|
+
*/
|
|
286
|
+
private parseJsonOutput(stdout: string): AdapterResult {
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(stdout);
|
|
289
|
+
|
|
290
|
+
// Try common text fields
|
|
291
|
+
const textFields = ["text", "response", "output", "content", "result", "message"];
|
|
292
|
+
let text = stdout.trim();
|
|
293
|
+
|
|
294
|
+
for (const field of textFields) {
|
|
295
|
+
if (typeof parsed[field] === "string") {
|
|
296
|
+
text = parsed[field];
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Extract usage if present
|
|
302
|
+
const usage = parsed.usage as TokenUsage | undefined;
|
|
303
|
+
|
|
304
|
+
return { text, usage };
|
|
305
|
+
} catch {
|
|
306
|
+
return { text: stdout.trim() };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Parse a streaming line
|
|
312
|
+
*/
|
|
313
|
+
private parseStreamLine(line: string): StreamChunk | null {
|
|
314
|
+
// Use custom hook if provided
|
|
315
|
+
if (this.def.parseStreamChunk) {
|
|
316
|
+
return this.def.parseStreamChunk(line);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const streamingMode = this.def.streaming ?? "line";
|
|
320
|
+
|
|
321
|
+
if (streamingMode === "jsonl") {
|
|
322
|
+
try {
|
|
323
|
+
const parsed = JSON.parse(line);
|
|
324
|
+
return { type: "json", data: parsed };
|
|
325
|
+
} catch {
|
|
326
|
+
return { type: "text", content: line };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { type: "text", content: line };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Classify an error based on stderr/stdout content
|
|
335
|
+
*/
|
|
336
|
+
private classifyError(
|
|
337
|
+
stderr: string,
|
|
338
|
+
stdout: string,
|
|
339
|
+
exitCode: number | null,
|
|
340
|
+
): ProviderErrorKind {
|
|
341
|
+
// Use custom hook if provided
|
|
342
|
+
if (this.def.classifyError) {
|
|
343
|
+
return this.def.classifyError(stderr, stdout, exitCode);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const combined = (stderr + stdout).toLowerCase();
|
|
347
|
+
|
|
348
|
+
// Check adapter-specific patterns first
|
|
349
|
+
if (this.def.errorPatterns) {
|
|
350
|
+
for (const [kind, patterns] of Object.entries(this.def.errorPatterns)) {
|
|
351
|
+
if (patterns?.some((p) => combined.includes(p.toLowerCase()))) {
|
|
352
|
+
return kind as ProviderErrorKind;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Fall back to default patterns
|
|
358
|
+
for (const [kind, patterns] of Object.entries(DEFAULT_ERROR_PATTERNS)) {
|
|
359
|
+
if (patterns.some((p) => combined.includes(p.toLowerCase()))) {
|
|
360
|
+
return kind as ProviderErrorKind;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return "TRANSIENT";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get a nested value from an object by path
|
|
369
|
+
*/
|
|
370
|
+
private getJsonPath(obj: unknown, path: string): unknown {
|
|
371
|
+
return path.split(".").reduce((o, k) => {
|
|
372
|
+
if (o === null || o === undefined) return undefined;
|
|
373
|
+
return (o as Record<string, unknown>)[k];
|
|
374
|
+
}, obj);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Get adapter info
|
|
379
|
+
*/
|
|
380
|
+
getInfo() {
|
|
381
|
+
return {
|
|
382
|
+
id: this.def.id,
|
|
383
|
+
displayName: this.def.displayName ?? this.def.id,
|
|
384
|
+
description: this.def.description,
|
|
385
|
+
version: this.def.version,
|
|
386
|
+
binary: this.def.binary,
|
|
387
|
+
capabilities: this.def.capabilities ?? ["generate"],
|
|
388
|
+
supportsStreaming: (this.def.streaming ?? "none") !== "none",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Custom error class for adapter failures
|
|
395
|
+
*/
|
|
396
|
+
export class AdapterError extends Error {
|
|
397
|
+
readonly kind: ProviderErrorKind;
|
|
398
|
+
readonly adapterId: string;
|
|
399
|
+
readonly context: {
|
|
400
|
+
stderr?: string;
|
|
401
|
+
stdout?: string;
|
|
402
|
+
exitCode?: number | null;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
constructor(
|
|
406
|
+
message: string,
|
|
407
|
+
kind: ProviderErrorKind,
|
|
408
|
+
adapterId: string,
|
|
409
|
+
context: { stderr?: string; stdout?: string; exitCode?: number | null },
|
|
410
|
+
) {
|
|
411
|
+
super(message);
|
|
412
|
+
this.name = "AdapterError";
|
|
413
|
+
this.kind = kind;
|
|
414
|
+
this.adapterId = adapterId;
|
|
415
|
+
this.context = context;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
get isRetryable(): boolean {
|
|
419
|
+
return ["TRANSIENT", "RATE_LIMIT", "TIMEOUT"].includes(this.kind);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
get isUserError(): boolean {
|
|
423
|
+
return ["BAD_REQUEST", "UNAUTHORIZED", "CONTEXT_LENGTH", "CONTENT_FILTER"].includes(this.kind);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Test helper interface that exposes private methods for testing.
|
|
429
|
+
* Only use this in test files.
|
|
430
|
+
*/
|
|
431
|
+
export interface AdapterRunnerTestHelper extends AdapterRunner {
|
|
432
|
+
buildArgs(prompt: string, opts: AdapterInvokeOptions): string[];
|
|
433
|
+
getStdin(prompt: string): string | undefined;
|
|
434
|
+
parseOutput(stdout: string): AdapterResult;
|
|
435
|
+
parseJsonOutput(stdout: string): AdapterResult;
|
|
436
|
+
parseStreamLine(line: string): StreamChunk | null;
|
|
437
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter Types - Lightweight configuration for terminal coding tools
|
|
3
|
+
*
|
|
4
|
+
* Designed to be simpler than defineProvider while maintaining full functionality.
|
|
5
|
+
* Most adapters should be < 20 lines of TypeScript.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ProviderErrorKind, TokenUsage } from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How to pass the prompt to the CLI tool
|
|
12
|
+
*/
|
|
13
|
+
export type InputMethod =
|
|
14
|
+
| "stdin" // Pipe prompt to stdin (default)
|
|
15
|
+
| "positional" // Pass as positional argument
|
|
16
|
+
| { flag: string }; // Pass with flag: -p "<prompt>"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How to parse CLI output
|
|
20
|
+
*/
|
|
21
|
+
export type OutputFormat =
|
|
22
|
+
| "text" // Raw stdout as response (default)
|
|
23
|
+
| "json" // Parse JSON, extract text from common fields
|
|
24
|
+
| { jsonPath: string }; // Extract from specific JSON path
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Streaming mode for real-time output
|
|
28
|
+
*/
|
|
29
|
+
export type StreamingMode =
|
|
30
|
+
| "none" // No streaming (default)
|
|
31
|
+
| "line" // Emit each line as it arrives
|
|
32
|
+
| "jsonl"; // Parse JSON lines
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Invoke options passed to adapter execution
|
|
36
|
+
*/
|
|
37
|
+
export interface AdapterInvokeOptions {
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
cwd?: string;
|
|
40
|
+
env?: Record<string, string>;
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Streaming chunk types
|
|
46
|
+
*/
|
|
47
|
+
export type StreamChunk =
|
|
48
|
+
| { type: "start"; requestId: string }
|
|
49
|
+
| { type: "text"; content: string }
|
|
50
|
+
| { type: "json"; data: unknown }
|
|
51
|
+
| { type: "complete"; text: string; usage?: TokenUsage }
|
|
52
|
+
| { type: "error"; message: string; kind: ProviderErrorKind };
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Error pattern configuration
|
|
56
|
+
* Maps error kind to array of case-insensitive patterns to match
|
|
57
|
+
*/
|
|
58
|
+
export type ErrorPatterns = Partial<Record<ProviderErrorKind, string[]>>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Core adapter configuration
|
|
62
|
+
*
|
|
63
|
+
* Only `id` and `binary` are required - everything else has sensible defaults.
|
|
64
|
+
*/
|
|
65
|
+
export interface AdapterConfig {
|
|
66
|
+
// Required fields
|
|
67
|
+
/** Unique identifier for this adapter */
|
|
68
|
+
id: string;
|
|
69
|
+
/** CLI binary name or path */
|
|
70
|
+
binary: string;
|
|
71
|
+
|
|
72
|
+
// Display
|
|
73
|
+
/** Human-readable name (defaults to id) */
|
|
74
|
+
displayName?: string;
|
|
75
|
+
/** Description of this adapter */
|
|
76
|
+
description?: string;
|
|
77
|
+
/** Version string */
|
|
78
|
+
version?: string;
|
|
79
|
+
|
|
80
|
+
// Input handling
|
|
81
|
+
/** How to pass the prompt (default: 'stdin') */
|
|
82
|
+
input?: InputMethod;
|
|
83
|
+
|
|
84
|
+
// Arguments
|
|
85
|
+
/** Static args to always include */
|
|
86
|
+
args?: string[];
|
|
87
|
+
/** Subcommand to invoke (e.g., 'exec', 'run') */
|
|
88
|
+
subcommand?: string;
|
|
89
|
+
/** Environment variables to set */
|
|
90
|
+
env?: Record<string, string>;
|
|
91
|
+
|
|
92
|
+
// Output handling
|
|
93
|
+
/** How to parse output (default: 'text') */
|
|
94
|
+
output?: OutputFormat;
|
|
95
|
+
/** Streaming mode (default: 'none') */
|
|
96
|
+
streaming?: StreamingMode;
|
|
97
|
+
|
|
98
|
+
// Error handling
|
|
99
|
+
/** Patterns to match for error classification */
|
|
100
|
+
errorPatterns?: ErrorPatterns;
|
|
101
|
+
/** Exit codes to treat as success (default: [0]) */
|
|
102
|
+
allowedExitCodes?: number[];
|
|
103
|
+
|
|
104
|
+
// Capabilities
|
|
105
|
+
/** Coding modes this adapter supports */
|
|
106
|
+
capabilities?: string[];
|
|
107
|
+
|
|
108
|
+
// Timeouts
|
|
109
|
+
/** Timeout in milliseconds (default: 60000) */
|
|
110
|
+
timeoutMs?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Optional hooks for custom behavior
|
|
115
|
+
* Use these when declarative config isn't sufficient
|
|
116
|
+
*/
|
|
117
|
+
export interface AdapterHooks {
|
|
118
|
+
/**
|
|
119
|
+
* Custom argument building logic
|
|
120
|
+
* @returns Array of command line arguments
|
|
121
|
+
*/
|
|
122
|
+
buildArgs?: (prompt: string, options: AdapterInvokeOptions) => string[];
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Custom stdin input generation
|
|
126
|
+
* @returns String to pipe to stdin, or undefined for no stdin
|
|
127
|
+
*/
|
|
128
|
+
getStdin?: (prompt: string) => string | undefined;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Custom output parsing for non-streaming responses
|
|
132
|
+
*/
|
|
133
|
+
parseOutput?: (stdout: string) => { text: string; usage?: TokenUsage };
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Custom streaming chunk parsing
|
|
137
|
+
* @returns Parsed chunk, or null to skip the line
|
|
138
|
+
*/
|
|
139
|
+
parseStreamChunk?: (line: string) => StreamChunk | null;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Custom error classification
|
|
143
|
+
*/
|
|
144
|
+
classifyError?: (stderr: string, stdout: string, exitCode: number | null) => ProviderErrorKind;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Full adapter definition = config + optional hooks
|
|
149
|
+
*/
|
|
150
|
+
export type AdapterDefinition = AdapterConfig & Partial<AdapterHooks>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Result from running an adapter
|
|
154
|
+
*/
|
|
155
|
+
export interface AdapterResult {
|
|
156
|
+
text: string;
|
|
157
|
+
usage?: TokenUsage;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Metadata about an adapter
|
|
162
|
+
*/
|
|
163
|
+
export interface AdapterInfo {
|
|
164
|
+
id: string;
|
|
165
|
+
displayName: string;
|
|
166
|
+
description?: string;
|
|
167
|
+
version?: string;
|
|
168
|
+
binary: string;
|
|
169
|
+
capabilities: string[];
|
|
170
|
+
supportsStreaming: boolean;
|
|
171
|
+
isAvailable?: boolean;
|
|
172
|
+
}
|