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,290 @@
|
|
|
1
|
+
import { rename } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import type { Config } from "./types";
|
|
5
|
+
import { ConfigSchema } from "./types";
|
|
6
|
+
|
|
7
|
+
export interface ConfigLoaderOptions {
|
|
8
|
+
configPath?: string;
|
|
9
|
+
projectConfigPath?: string;
|
|
10
|
+
/** Enable debug logging (default: false) */
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ConfigLoader {
|
|
15
|
+
private options: ConfigLoaderOptions;
|
|
16
|
+
|
|
17
|
+
constructor(options: ConfigLoaderOptions = {}) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async loadConfig(): Promise<Config> {
|
|
22
|
+
// Load configs from different sources in order of precedence
|
|
23
|
+
const configs: Config[] = [];
|
|
24
|
+
|
|
25
|
+
// 1. Built-in defaults
|
|
26
|
+
configs.push(this.getDefaultConfig());
|
|
27
|
+
|
|
28
|
+
// 2. System config (optional)
|
|
29
|
+
const systemConfig = await this.loadSystemConfig();
|
|
30
|
+
if (systemConfig) {
|
|
31
|
+
configs.push(systemConfig);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 3. User config
|
|
35
|
+
const userConfig = await this.loadUserConfig();
|
|
36
|
+
if (userConfig) {
|
|
37
|
+
configs.push(userConfig);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 4. Project config
|
|
41
|
+
const projectConfig = await this.loadProjectConfig();
|
|
42
|
+
if (projectConfig) {
|
|
43
|
+
configs.push(projectConfig);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 5. Env variables (WTC_*)
|
|
47
|
+
const envConfig = this.loadEnvConfig();
|
|
48
|
+
if (envConfig) {
|
|
49
|
+
configs.push(envConfig);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Merge all configs
|
|
53
|
+
const merged = this.mergeConfigs(configs);
|
|
54
|
+
|
|
55
|
+
// Validate with Zod
|
|
56
|
+
const result = ConfigSchema.safeParse(merged);
|
|
57
|
+
if (!result.success) {
|
|
58
|
+
throw new Error(`Invalid configuration: ${result.error.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result.data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private getDefaultConfig(): Config {
|
|
65
|
+
return {
|
|
66
|
+
routing: {
|
|
67
|
+
defaultOrder: ["gemini", "opencode", "qwen-code", "codex"],
|
|
68
|
+
perModeOverride: {
|
|
69
|
+
test: ["codex", "gemini"],
|
|
70
|
+
explain: ["gemini", "qwen-code"],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
providers: {
|
|
74
|
+
gemini: {
|
|
75
|
+
binary: "gemini",
|
|
76
|
+
args: [],
|
|
77
|
+
jsonMode: "flag",
|
|
78
|
+
jsonFlag: "--output-format",
|
|
79
|
+
streamingMode: "jsonl",
|
|
80
|
+
capabilities: ["generate", "edit", "explain", "test"],
|
|
81
|
+
},
|
|
82
|
+
opencode: {
|
|
83
|
+
binary: "opencode",
|
|
84
|
+
args: [],
|
|
85
|
+
jsonMode: "flag",
|
|
86
|
+
jsonFlag: "-f",
|
|
87
|
+
streamingMode: "none",
|
|
88
|
+
capabilities: ["generate", "edit", "explain", "test", "refactor"],
|
|
89
|
+
},
|
|
90
|
+
"qwen-code": {
|
|
91
|
+
binary: "qwen",
|
|
92
|
+
args: [],
|
|
93
|
+
jsonMode: "flag",
|
|
94
|
+
jsonFlag: "--json",
|
|
95
|
+
streamingMode: "jsonl",
|
|
96
|
+
capabilities: ["generate", "edit", "explain", "test"],
|
|
97
|
+
},
|
|
98
|
+
codex: {
|
|
99
|
+
binary: "codex",
|
|
100
|
+
args: [],
|
|
101
|
+
jsonMode: "none",
|
|
102
|
+
streamingMode: "line",
|
|
103
|
+
capabilities: ["generate", "edit", "test"],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
credits: {
|
|
107
|
+
providers: {
|
|
108
|
+
gemini: {
|
|
109
|
+
dailyRequestLimit: 1000,
|
|
110
|
+
resetHourUtc: 0,
|
|
111
|
+
},
|
|
112
|
+
opencode: {
|
|
113
|
+
dailyRequestLimit: 1000,
|
|
114
|
+
resetHourUtc: 0,
|
|
115
|
+
},
|
|
116
|
+
"qwen-code": {
|
|
117
|
+
dailyRequestLimit: 2000,
|
|
118
|
+
resetHourUtc: 0,
|
|
119
|
+
},
|
|
120
|
+
codex: {
|
|
121
|
+
plan: "chatgpt_plus",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async loadSystemConfig(): Promise<Config | null> {
|
|
129
|
+
const systemPath = "/etc/wrap-terminalcoder/config.json";
|
|
130
|
+
return this.loadConfigFile(systemPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async loadUserConfig(): Promise<Config | null> {
|
|
134
|
+
const userPath = join(process.env.HOME || "~", ".config", "wrap-terminalcoder", "config.json");
|
|
135
|
+
return this.loadConfigFile(userPath);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async loadProjectConfig(): Promise<Config | null> {
|
|
139
|
+
const projectPath = this.options.projectConfigPath || ".config/wraptc/config.json";
|
|
140
|
+
return this.loadConfigFile(projectPath);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async loadConfigFile(filePath: string): Promise<Config | null> {
|
|
144
|
+
// Use Bun.file() for async file existence check and reading
|
|
145
|
+
const file = Bun.file(filePath);
|
|
146
|
+
const exists = await file.exists();
|
|
147
|
+
|
|
148
|
+
if (!exists) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// File exists - now errors are meaningful and should be reported
|
|
153
|
+
try {
|
|
154
|
+
const content = await file.text();
|
|
155
|
+
const parsed = JSON.parse(content);
|
|
156
|
+
|
|
157
|
+
if (this.options.debug) {
|
|
158
|
+
console.debug(`[ConfigLoader] Loaded config from ${filePath}`);
|
|
159
|
+
}
|
|
160
|
+
return parsed;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// File exists but couldn't be read or parsed - this is a real error
|
|
163
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
164
|
+
if (errorMessage.includes("JSON")) {
|
|
165
|
+
throw new Error(`Invalid JSON in config file ${filePath}: ${errorMessage}`);
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Failed to read config file ${filePath}: ${errorMessage}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private loadEnvConfig(): Config | null {
|
|
172
|
+
const envVars = Object.entries(process.env).filter(([key]) => key.startsWith("WTC_"));
|
|
173
|
+
|
|
174
|
+
if (envVars.length === 0) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const config: any = {};
|
|
179
|
+
|
|
180
|
+
for (const [key, value] of envVars) {
|
|
181
|
+
// Convert WTC_ROUTING__DEFAULT_ORDER='["qwen-code","gemini"]' to config.routing.defaultOrder
|
|
182
|
+
const path = key
|
|
183
|
+
.replace(/^WTC_/, "")
|
|
184
|
+
.toLowerCase()
|
|
185
|
+
.split("__")
|
|
186
|
+
.map((segment) => this.camelCase(segment));
|
|
187
|
+
|
|
188
|
+
this.setNestedProperty(config, path, this.parseEnvValue(value));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return config;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private camelCase(str: string): string {
|
|
195
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private parseEnvValue(value: string | undefined): any {
|
|
199
|
+
if (!value) return value;
|
|
200
|
+
// Try to parse as JSON first
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(value);
|
|
203
|
+
} catch {
|
|
204
|
+
// If not JSON, return as string
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private setNestedProperty(obj: any, path: string[], value: any): void {
|
|
210
|
+
if (path.length === 0) {
|
|
211
|
+
// For empty path, set the value directly on the object
|
|
212
|
+
obj[""] = value;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let current = obj;
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
219
|
+
const key = path[i];
|
|
220
|
+
if (!(key in current)) {
|
|
221
|
+
current[key] = {};
|
|
222
|
+
}
|
|
223
|
+
current = current[key];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
current[path[path.length - 1]] = value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private mergeConfigs(configs: Config[]): Config {
|
|
230
|
+
// Deep merge all configs, with later configs overriding earlier ones
|
|
231
|
+
const merged = configs.reduce((acc, config) => {
|
|
232
|
+
return this.deepMerge(acc, config);
|
|
233
|
+
}, {} as Config);
|
|
234
|
+
|
|
235
|
+
return merged;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private deepMerge(target: any, source: any): any {
|
|
239
|
+
const result = { ...target };
|
|
240
|
+
|
|
241
|
+
for (const key in source) {
|
|
242
|
+
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
243
|
+
result[key] = this.deepMerge(result[key] || {}, source[key]);
|
|
244
|
+
} else {
|
|
245
|
+
result[key] = source[key];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async saveConfig(config: Config, path?: string): Promise<void> {
|
|
253
|
+
const configPath = path || this.options.configPath || this.getDefaultUserConfigPath();
|
|
254
|
+
|
|
255
|
+
// Ensure directory exists using Bun shell
|
|
256
|
+
const dir = dirname(configPath);
|
|
257
|
+
await Bun.$`mkdir -p ${dir}`.quiet();
|
|
258
|
+
|
|
259
|
+
// Write atomically: write to temp file, then rename
|
|
260
|
+
const tempPath = `${configPath}.tmp`;
|
|
261
|
+
|
|
262
|
+
// Use Bun.write() for faster file writing
|
|
263
|
+
await Bun.write(tempPath, JSON.stringify(config, null, 2));
|
|
264
|
+
|
|
265
|
+
// Atomic rename
|
|
266
|
+
await rename(tempPath, configPath);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private getDefaultUserConfigPath(): string {
|
|
270
|
+
return join(process.env.HOME || "~", ".config", "wrap-terminalcoder", "config.json");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function addCLIConfigOverrides(command: Command): void {
|
|
275
|
+
command
|
|
276
|
+
.option("--provider <provider>", "Override provider")
|
|
277
|
+
.option("--routing-default-order <order>", "Override routing default order (JSON array)")
|
|
278
|
+
.option("--config <path>", "Config file path")
|
|
279
|
+
.hook("preAction", async (thisCommand) => {
|
|
280
|
+
const opts = thisCommand.opts();
|
|
281
|
+
|
|
282
|
+
// Convert CLI options to env variables temporarily
|
|
283
|
+
if (opts.provider) {
|
|
284
|
+
process.env.WTC_REQUEST__PROVIDER = opts.provider;
|
|
285
|
+
}
|
|
286
|
+
if (opts.routingDefaultOrder) {
|
|
287
|
+
process.env.WTC_ROUTING__DEFAULT_ORDER = opts.routingDefaultOrder;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineProvider - Helper for creating CLI provider configurations
|
|
3
|
+
*
|
|
4
|
+
* This enables users to add new CLI tools by creating a simple .ts file:
|
|
5
|
+
*
|
|
6
|
+
* ```typescript
|
|
7
|
+
* // ~/.config/wraptc/providers/my-cli.ts
|
|
8
|
+
* import { defineProvider } from "wrap-terminalcoder";
|
|
9
|
+
*
|
|
10
|
+
* export default defineProvider({
|
|
11
|
+
* id: "my-cli",
|
|
12
|
+
* binary: "my-cli",
|
|
13
|
+
* input: { method: "positional" },
|
|
14
|
+
* output: { format: "json", textField: "response" },
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import type { CodingRequest, ProviderErrorContext, ProviderErrorKind, TokenUsage } from "./types";
|
|
21
|
+
|
|
22
|
+
// Input configuration - how to pass the prompt to the CLI
|
|
23
|
+
export const InputConfigSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
// stdin: pipe prompt to stdin
|
|
26
|
+
// positional: pass prompt as positional argument
|
|
27
|
+
// flag: pass prompt with a flag (e.g., --prompt "...")
|
|
28
|
+
method: z.enum(["stdin", "positional", "flag"]).default("stdin"),
|
|
29
|
+
// Flag name if method is "flag" (e.g., "--prompt")
|
|
30
|
+
flag: z.string().optional(),
|
|
31
|
+
// Position if method is "positional" (first or last argument)
|
|
32
|
+
position: z.enum(["first", "last"]).default("last"),
|
|
33
|
+
})
|
|
34
|
+
.default({ method: "stdin" });
|
|
35
|
+
|
|
36
|
+
// Output configuration - how to parse CLI output
|
|
37
|
+
export const OutputConfigSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
// text: raw stdout as response
|
|
40
|
+
// json: parse JSON, extract textField
|
|
41
|
+
// jsonl: parse JSON lines
|
|
42
|
+
format: z.enum(["text", "json", "jsonl"]).default("text"),
|
|
43
|
+
// Field containing the text response (for json format)
|
|
44
|
+
textField: z.string().optional(),
|
|
45
|
+
// Field containing usage info (for json format)
|
|
46
|
+
usageField: z.string().optional(),
|
|
47
|
+
})
|
|
48
|
+
.default({ format: "text" });
|
|
49
|
+
|
|
50
|
+
// Streaming configuration
|
|
51
|
+
export const StreamingConfigSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
// none: no streaming support
|
|
54
|
+
// line: emit each line as text_delta
|
|
55
|
+
// jsonl: parse JSON lines and emit as chunks
|
|
56
|
+
mode: z.enum(["none", "line", "jsonl"]).default("none"),
|
|
57
|
+
})
|
|
58
|
+
.default({ mode: "none" });
|
|
59
|
+
|
|
60
|
+
// Args configuration - how to build command line arguments
|
|
61
|
+
export const ArgsConfigSchema = z
|
|
62
|
+
.object({
|
|
63
|
+
// Base arguments always included
|
|
64
|
+
base: z.array(z.string()).default([]),
|
|
65
|
+
// Flag to enable JSON output (e.g., "-o json" or "--format json")
|
|
66
|
+
jsonFlag: z.string().optional(),
|
|
67
|
+
// Flag to enable streaming
|
|
68
|
+
streamFlag: z.string().optional(),
|
|
69
|
+
// Flag for model selection (e.g., "--model")
|
|
70
|
+
modelFlag: z.string().optional(),
|
|
71
|
+
// Flag for file context (e.g., "-f" - will be repeated for each file)
|
|
72
|
+
fileFlag: z.string().optional(),
|
|
73
|
+
// Flag for max tokens (e.g., "--max-tokens")
|
|
74
|
+
maxTokensFlag: z.string().optional(),
|
|
75
|
+
// Flag for temperature (e.g., "--temperature" or "-t")
|
|
76
|
+
temperatureFlag: z.string().optional(),
|
|
77
|
+
// Flag for language (e.g., "--language" or "-l")
|
|
78
|
+
languageFlag: z.string().optional(),
|
|
79
|
+
// Flag for system prompt (e.g., "--system" or "-S")
|
|
80
|
+
systemPromptFlag: z.string().optional(),
|
|
81
|
+
// How to handle system prompt
|
|
82
|
+
systemPromptMethod: z.enum(["flag", "combined"]).optional(),
|
|
83
|
+
})
|
|
84
|
+
.default({ base: [] });
|
|
85
|
+
|
|
86
|
+
// Error patterns - strings to match in stderr/stdout for error classification
|
|
87
|
+
export const ErrorPatternsSchema = z.record(z.array(z.string())).optional();
|
|
88
|
+
|
|
89
|
+
// Retry configuration schema
|
|
90
|
+
export const RetryConfigSchema = z
|
|
91
|
+
.object({
|
|
92
|
+
// Maximum number of retry attempts (1 = no retries)
|
|
93
|
+
maxAttempts: z.number().default(1),
|
|
94
|
+
// Initial delay between retries in milliseconds
|
|
95
|
+
delayMs: z.number().default(1000),
|
|
96
|
+
// Multiplier for exponential backoff
|
|
97
|
+
backoffMultiplier: z.number().default(2),
|
|
98
|
+
// Error kinds that should trigger a retry
|
|
99
|
+
retryOn: z.array(z.string()).default(["TRANSIENT"]),
|
|
100
|
+
})
|
|
101
|
+
.optional();
|
|
102
|
+
|
|
103
|
+
// Main provider definition schema
|
|
104
|
+
export const ProviderDefinitionSchema = z.object({
|
|
105
|
+
// Unique identifier for the provider
|
|
106
|
+
id: z.string(),
|
|
107
|
+
// Display name (defaults to id)
|
|
108
|
+
displayName: z.string().optional(),
|
|
109
|
+
// Path to the CLI binary
|
|
110
|
+
binary: z.string(),
|
|
111
|
+
// Subcommand to invoke (e.g., "exec", "run", "chat")
|
|
112
|
+
subcommand: z.string().optional(),
|
|
113
|
+
|
|
114
|
+
// Environment variables to set for the process
|
|
115
|
+
env: z.record(z.string()).optional(),
|
|
116
|
+
// Default working directory for the process
|
|
117
|
+
defaultCwd: z.string().optional(),
|
|
118
|
+
|
|
119
|
+
// Input handling configuration
|
|
120
|
+
input: InputConfigSchema,
|
|
121
|
+
// Output parsing configuration
|
|
122
|
+
output: OutputConfigSchema,
|
|
123
|
+
// Streaming configuration
|
|
124
|
+
streaming: StreamingConfigSchema,
|
|
125
|
+
// Arguments configuration
|
|
126
|
+
args: ArgsConfigSchema,
|
|
127
|
+
|
|
128
|
+
// Error patterns for classification
|
|
129
|
+
errors: ErrorPatternsSchema,
|
|
130
|
+
// Exit codes to treat as success (default: [0])
|
|
131
|
+
allowedExitCodes: z.array(z.number()).default([0]),
|
|
132
|
+
|
|
133
|
+
// Timeout in milliseconds (default: 60000 = 1 minute)
|
|
134
|
+
timeoutMs: z.number().optional(),
|
|
135
|
+
// Retry configuration for transient errors
|
|
136
|
+
retry: RetryConfigSchema,
|
|
137
|
+
|
|
138
|
+
// Capabilities this provider supports
|
|
139
|
+
capabilities: z.array(z.string()).default(["generate"]),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Inferred type from schema
|
|
143
|
+
export type ProviderDefinitionBase = z.infer<typeof ProviderDefinitionSchema>;
|
|
144
|
+
|
|
145
|
+
// Full provider definition including optional function overrides
|
|
146
|
+
export interface ProviderDefinition extends ProviderDefinitionBase {
|
|
147
|
+
/**
|
|
148
|
+
* Custom argument building logic.
|
|
149
|
+
* Called instead of default arg building when provided.
|
|
150
|
+
*/
|
|
151
|
+
buildArgs?: (req: CodingRequest) => string[];
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Custom output parsing logic.
|
|
155
|
+
* Called instead of default parsing when provided.
|
|
156
|
+
*/
|
|
157
|
+
parseOutput?: (stdout: string) => { text: string; usage?: TokenUsage };
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Custom error classification logic.
|
|
161
|
+
* Called instead of default pattern matching when provided.
|
|
162
|
+
*/
|
|
163
|
+
classifyError?: (ctx: ProviderErrorContext) => ProviderErrorKind;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Custom input handling.
|
|
167
|
+
* Returns the string to pipe to stdin, or undefined to use args only.
|
|
168
|
+
*/
|
|
169
|
+
getStdinInput?: (req: CodingRequest) => string | undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Input type for defineProvider (allows partial nested objects)
|
|
173
|
+
export type ProviderDefinitionInput = Omit<
|
|
174
|
+
ProviderDefinition,
|
|
175
|
+
"input" | "output" | "streaming" | "args" | "capabilities"
|
|
176
|
+
> & {
|
|
177
|
+
input?: Partial<z.infer<typeof InputConfigSchema>>;
|
|
178
|
+
output?: Partial<z.infer<typeof OutputConfigSchema>>;
|
|
179
|
+
streaming?: Partial<z.infer<typeof StreamingConfigSchema>>;
|
|
180
|
+
args?: Partial<z.infer<typeof ArgsConfigSchema>>;
|
|
181
|
+
capabilities?: string[];
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Define a CLI provider with full TypeScript support.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* export default defineProvider({
|
|
190
|
+
* id: "my-cli",
|
|
191
|
+
* binary: "my-cli",
|
|
192
|
+
* input: { method: "positional" },
|
|
193
|
+
* output: { format: "json", textField: "response" },
|
|
194
|
+
* });
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function defineProvider(config: ProviderDefinitionInput): ProviderDefinition {
|
|
198
|
+
// Validate base config with Zod
|
|
199
|
+
const validated = ProviderDefinitionSchema.parse(config);
|
|
200
|
+
|
|
201
|
+
// Merge with function overrides
|
|
202
|
+
return {
|
|
203
|
+
...validated,
|
|
204
|
+
buildArgs: config.buildArgs,
|
|
205
|
+
parseOutput: config.parseOutput,
|
|
206
|
+
classifyError: config.classifyError,
|
|
207
|
+
getStdinInput: config.getStdinInput,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Re-export from centralized error patterns module
|
|
212
|
+
export { DEFAULT_ERROR_PATTERNS, classifyErrorDefault } from "./error-patterns";
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Error Pattern Registry
|
|
3
|
+
*
|
|
4
|
+
* This module provides a unified set of error patterns for classifying
|
|
5
|
+
* errors across all providers. Providers can extend these patterns
|
|
6
|
+
* with provider-specific patterns.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ProviderErrorKind } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default error patterns used by all providers
|
|
13
|
+
* Patterns are matched case-insensitively against stderr + stdout
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_ERROR_PATTERNS: Record<ProviderErrorKind, string[]> = {
|
|
16
|
+
OUT_OF_CREDITS: [
|
|
17
|
+
"quota exceeded",
|
|
18
|
+
"out of quota",
|
|
19
|
+
"out of credits",
|
|
20
|
+
"credits exhausted",
|
|
21
|
+
"insufficient quota",
|
|
22
|
+
"no credits remaining",
|
|
23
|
+
"credit limit",
|
|
24
|
+
"you've exceeded your quota",
|
|
25
|
+
],
|
|
26
|
+
RATE_LIMIT: ["rate limit", "rate_limit", "too many requests", "429", "throttle", "slow down"],
|
|
27
|
+
BAD_REQUEST: [
|
|
28
|
+
"bad request",
|
|
29
|
+
"invalid request",
|
|
30
|
+
"malformed",
|
|
31
|
+
"400",
|
|
32
|
+
"validation error",
|
|
33
|
+
"invalid parameter",
|
|
34
|
+
],
|
|
35
|
+
UNAUTHORIZED: [
|
|
36
|
+
"unauthorized",
|
|
37
|
+
"authentication failed",
|
|
38
|
+
"invalid api key",
|
|
39
|
+
"api key invalid",
|
|
40
|
+
"api key missing",
|
|
41
|
+
"401",
|
|
42
|
+
],
|
|
43
|
+
FORBIDDEN: ["forbidden", "permission denied", "access denied", "403"],
|
|
44
|
+
NOT_FOUND: ["not found", "404", "no such file", "command not found", "model not found"],
|
|
45
|
+
TIMEOUT: ["timeout", "timed out", "deadline exceeded", "request timeout"],
|
|
46
|
+
CONTEXT_LENGTH: [
|
|
47
|
+
"context length",
|
|
48
|
+
"too long",
|
|
49
|
+
"max tokens",
|
|
50
|
+
"token limit",
|
|
51
|
+
"context too large",
|
|
52
|
+
"input too long",
|
|
53
|
+
],
|
|
54
|
+
CONTENT_FILTER: ["content filter", "blocked", "safety", "moderation", "policy violation"],
|
|
55
|
+
INTERNAL: ["internal error", "internal server error", "server error", "500", "502", "503"],
|
|
56
|
+
TRANSIENT: [
|
|
57
|
+
"temporarily unavailable",
|
|
58
|
+
"service unavailable",
|
|
59
|
+
"connection refused",
|
|
60
|
+
"econnrefused",
|
|
61
|
+
"econnreset",
|
|
62
|
+
"network error",
|
|
63
|
+
"connection reset",
|
|
64
|
+
"socket hang up",
|
|
65
|
+
],
|
|
66
|
+
UNKNOWN: [],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Classify an error based on the combined output using default patterns
|
|
71
|
+
*/
|
|
72
|
+
export function classifyErrorDefault(
|
|
73
|
+
stderr = "",
|
|
74
|
+
stdout = "",
|
|
75
|
+
exitCode?: number | null,
|
|
76
|
+
httpStatus?: number,
|
|
77
|
+
): ProviderErrorKind {
|
|
78
|
+
// HTTP status code based classification takes priority
|
|
79
|
+
if (httpStatus) {
|
|
80
|
+
if (httpStatus === 401) return "UNAUTHORIZED";
|
|
81
|
+
if (httpStatus === 403) return "FORBIDDEN";
|
|
82
|
+
if (httpStatus === 404) return "NOT_FOUND";
|
|
83
|
+
if (httpStatus === 429) return "RATE_LIMIT";
|
|
84
|
+
if (httpStatus >= 500) return "INTERNAL";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const combined = (stderr + stdout).toLowerCase();
|
|
88
|
+
|
|
89
|
+
for (const [kind, patterns] of Object.entries(DEFAULT_ERROR_PATTERNS)) {
|
|
90
|
+
if (patterns.some((p) => combined.includes(p.toLowerCase()))) {
|
|
91
|
+
return kind as ProviderErrorKind;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check exit codes for common error types
|
|
96
|
+
if (exitCode === 127) {
|
|
97
|
+
return "NOT_FOUND"; // Command not found
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return "TRANSIENT";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Merge provider-specific patterns with default patterns
|
|
105
|
+
*/
|
|
106
|
+
export function mergeErrorPatterns(
|
|
107
|
+
providerPatterns: Partial<Record<ProviderErrorKind, string[]>>,
|
|
108
|
+
): Record<ProviderErrorKind, string[]> {
|
|
109
|
+
const merged = { ...DEFAULT_ERROR_PATTERNS };
|
|
110
|
+
|
|
111
|
+
for (const [kind, patterns] of Object.entries(providerPatterns)) {
|
|
112
|
+
if (patterns) {
|
|
113
|
+
merged[kind as ProviderErrorKind] = [
|
|
114
|
+
...patterns,
|
|
115
|
+
...DEFAULT_ERROR_PATTERNS[kind as ProviderErrorKind],
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return merged;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a classifier function with merged patterns
|
|
125
|
+
*/
|
|
126
|
+
export function createErrorClassifier(
|
|
127
|
+
providerPatterns: Partial<Record<ProviderErrorKind, string[]>> = {},
|
|
128
|
+
): (stderr?: string, stdout?: string, exitCode?: number | null) => ProviderErrorKind {
|
|
129
|
+
const patterns = mergeErrorPatterns(providerPatterns);
|
|
130
|
+
|
|
131
|
+
return (stderr = "", stdout = "", exitCode?: number | null): ProviderErrorKind => {
|
|
132
|
+
const combined = (stderr + stdout).toLowerCase();
|
|
133
|
+
|
|
134
|
+
for (const [kind, kindPatterns] of Object.entries(patterns)) {
|
|
135
|
+
if (kindPatterns.some((p) => combined.includes(p.toLowerCase()))) {
|
|
136
|
+
return kind as ProviderErrorKind;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check exit codes for common error types
|
|
141
|
+
if (exitCode === 127) {
|
|
142
|
+
return "NOT_FOUND";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return "TRANSIENT";
|
|
146
|
+
};
|
|
147
|
+
}
|