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,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProviderFactory - Lazy loading and caching for providers
|
|
3
|
+
*
|
|
4
|
+
* Performance optimizations:
|
|
5
|
+
* - Lazy instantiation: Providers are only created when first used
|
|
6
|
+
* - Binary validation caching: `which` lookups are cached
|
|
7
|
+
* - Provider instance caching: Reuse provider instances
|
|
8
|
+
*
|
|
9
|
+
* New: Supports user-defined providers from .ts config files
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ProviderDefinition } from "./define-provider";
|
|
13
|
+
import { loadProviderConfigs } from "./provider-loader";
|
|
14
|
+
import { createConfigurableProvider } from "./providers/configurable";
|
|
15
|
+
import type { Provider } from "./providers/index";
|
|
16
|
+
import type { Config, ProviderConfig } from "./types";
|
|
17
|
+
|
|
18
|
+
// Provider creator function type (factory function that creates providers)
|
|
19
|
+
type ProviderCreator = (id: string, config: ProviderConfig) => Provider;
|
|
20
|
+
|
|
21
|
+
// Cache for binary path lookups
|
|
22
|
+
const binaryPathCache = new Map<string, string | null>();
|
|
23
|
+
|
|
24
|
+
// Cache for binary existence checks
|
|
25
|
+
const binaryExistsCache = new Map<string, boolean>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a binary exists in PATH (cached)
|
|
29
|
+
*/
|
|
30
|
+
export async function binaryExists(binary: string): Promise<boolean> {
|
|
31
|
+
if (binaryExistsCache.has(binary)) {
|
|
32
|
+
return binaryExistsCache.get(binary)!;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const proc = Bun.spawn(["which", binary], {
|
|
37
|
+
stdout: "pipe",
|
|
38
|
+
stderr: "ignore",
|
|
39
|
+
});
|
|
40
|
+
await proc.exited;
|
|
41
|
+
const exists = proc.exitCode === 0;
|
|
42
|
+
binaryExistsCache.set(binary, exists);
|
|
43
|
+
return exists;
|
|
44
|
+
} catch {
|
|
45
|
+
binaryExistsCache.set(binary, false);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the full path to a binary (cached)
|
|
52
|
+
*/
|
|
53
|
+
export async function getBinaryPath(binary: string): Promise<string | null> {
|
|
54
|
+
if (binaryPathCache.has(binary)) {
|
|
55
|
+
return binaryPathCache.get(binary)!;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const proc = Bun.spawn(["which", binary], {
|
|
60
|
+
stdout: "pipe",
|
|
61
|
+
stderr: "ignore",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const output = await new Response(proc.stdout).text();
|
|
65
|
+
await proc.exited;
|
|
66
|
+
|
|
67
|
+
if (proc.exitCode === 0) {
|
|
68
|
+
const path = output.trim();
|
|
69
|
+
binaryPathCache.set(binary, path);
|
|
70
|
+
return path;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
binaryPathCache.set(binary, null);
|
|
74
|
+
return null;
|
|
75
|
+
} catch {
|
|
76
|
+
binaryPathCache.set(binary, null);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear the binary caches (useful for testing or when PATH changes)
|
|
83
|
+
*/
|
|
84
|
+
export function clearBinaryCache(): void {
|
|
85
|
+
binaryPathCache.clear();
|
|
86
|
+
binaryExistsCache.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* ProviderFactory with lazy loading and caching
|
|
91
|
+
*
|
|
92
|
+
* Supports three provider sources:
|
|
93
|
+
* 1. Built-in providers (registered with registerProvider)
|
|
94
|
+
* 2. Config-defined providers (from config.providers)
|
|
95
|
+
* 3. User-defined providers (from ~/.config/wraptc/providers/*.ts)
|
|
96
|
+
*/
|
|
97
|
+
export class ProviderFactory {
|
|
98
|
+
private config: Config;
|
|
99
|
+
private providerCache: Map<string, Provider> = new Map();
|
|
100
|
+
private providerCreators: Map<string, ProviderCreator> = new Map();
|
|
101
|
+
private loadingPromises: Map<string, Promise<Provider>> = new Map();
|
|
102
|
+
|
|
103
|
+
// User-defined providers loaded from .ts config files
|
|
104
|
+
private userProviders: Map<string, ProviderDefinition> = new Map();
|
|
105
|
+
private userProvidersLoaded = false;
|
|
106
|
+
|
|
107
|
+
constructor(config: Config) {
|
|
108
|
+
this.config = config;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Load user-defined providers from .ts config files
|
|
113
|
+
* Call this during initialization
|
|
114
|
+
*/
|
|
115
|
+
async loadUserProviders(): Promise<void> {
|
|
116
|
+
if (this.userProvidersLoaded) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
this.userProviders = await loadProviderConfigs();
|
|
122
|
+
this.userProvidersLoaded = true;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.warn("Failed to load user providers:", err);
|
|
125
|
+
this.userProvidersLoaded = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get all user-defined provider IDs
|
|
131
|
+
*/
|
|
132
|
+
getUserProviderIds(): string[] {
|
|
133
|
+
return Array.from(this.userProviders.keys());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Register a provider creator for a specific provider ID
|
|
138
|
+
*/
|
|
139
|
+
registerProvider(id: string, creator: ProviderCreator): void {
|
|
140
|
+
this.providerCreators.set(id, creator);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Register the default CustomProvider creator for fallback
|
|
145
|
+
*/
|
|
146
|
+
registerCustomProvider(creator: ProviderCreator): void {
|
|
147
|
+
this.providerCreators.set("__custom__", creator);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get a provider instance (lazy loaded and cached)
|
|
152
|
+
*/
|
|
153
|
+
async getProvider(id: string): Promise<Provider | null> {
|
|
154
|
+
// Check cache first
|
|
155
|
+
if (this.providerCache.has(id)) {
|
|
156
|
+
return this.providerCache.get(id)!;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check if already loading (prevent duplicate instantiation)
|
|
160
|
+
if (this.loadingPromises.has(id)) {
|
|
161
|
+
return this.loadingPromises.get(id)!;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Start loading
|
|
165
|
+
const loadPromise = this.loadProvider(id);
|
|
166
|
+
this.loadingPromises.set(id, loadPromise);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const provider = await loadPromise;
|
|
170
|
+
return provider;
|
|
171
|
+
} finally {
|
|
172
|
+
this.loadingPromises.delete(id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async loadProvider(id: string): Promise<Provider | null> {
|
|
177
|
+
// Ensure user providers are loaded
|
|
178
|
+
await this.loadUserProviders();
|
|
179
|
+
|
|
180
|
+
// Priority 1: Check for registered built-in provider creator
|
|
181
|
+
if (this.providerCreators.has(id)) {
|
|
182
|
+
const providerConfig = this.config.providers[id];
|
|
183
|
+
if (providerConfig) {
|
|
184
|
+
const exists = await binaryExists(providerConfig.binary);
|
|
185
|
+
if (!exists) {
|
|
186
|
+
console.warn(`Provider ${id}: binary '${providerConfig.binary}' not found in PATH`);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const creator = this.providerCreators.get(id)!;
|
|
190
|
+
const provider = creator(id, providerConfig);
|
|
191
|
+
this.providerCache.set(id, provider);
|
|
192
|
+
return provider;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Priority 2: Check for user-defined provider from .ts config file
|
|
197
|
+
const userDef = this.userProviders.get(id);
|
|
198
|
+
if (userDef) {
|
|
199
|
+
const exists = await binaryExists(userDef.binary);
|
|
200
|
+
if (!exists) {
|
|
201
|
+
console.warn(`Provider ${id}: binary '${userDef.binary}' not found in PATH`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const provider = createConfigurableProvider(userDef);
|
|
205
|
+
this.providerCache.set(id, provider);
|
|
206
|
+
return provider;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Priority 3: Check for config-defined provider with custom fallback
|
|
210
|
+
const providerConfig = this.config.providers[id];
|
|
211
|
+
if (providerConfig) {
|
|
212
|
+
const exists = await binaryExists(providerConfig.binary);
|
|
213
|
+
if (!exists) {
|
|
214
|
+
console.warn(`Provider ${id}: binary '${providerConfig.binary}' not found in PATH`);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Use custom fallback creator if registered
|
|
219
|
+
const creator = this.providerCreators.get("__custom__");
|
|
220
|
+
if (creator) {
|
|
221
|
+
const provider = creator(id, providerConfig);
|
|
222
|
+
this.providerCache.set(id, provider);
|
|
223
|
+
return provider;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.warn(`Provider ${id}: no creator or definition found`);
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Preload providers in parallel (optional optimization for startup)
|
|
233
|
+
*/
|
|
234
|
+
async preloadProviders(ids: string[]): Promise<void> {
|
|
235
|
+
await Promise.all(ids.map((id) => this.getProvider(id)));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get all cached providers
|
|
240
|
+
*/
|
|
241
|
+
getCachedProviders(): Map<string, Provider> {
|
|
242
|
+
return new Map(this.providerCache);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if a provider is available (binary exists)
|
|
247
|
+
*/
|
|
248
|
+
async isProviderAvailable(id: string): Promise<boolean> {
|
|
249
|
+
const providerConfig = this.config.providers[id];
|
|
250
|
+
if (!providerConfig) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return binaryExists(providerConfig.binary);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get available provider IDs (those with valid binaries)
|
|
258
|
+
* Includes both config-defined and user-defined providers
|
|
259
|
+
*/
|
|
260
|
+
async getAvailableProviderIds(): Promise<string[]> {
|
|
261
|
+
// Ensure user providers are loaded
|
|
262
|
+
await this.loadUserProviders();
|
|
263
|
+
|
|
264
|
+
// Combine config providers and user providers
|
|
265
|
+
const configIds = Object.keys(this.config.providers);
|
|
266
|
+
const userIds = Array.from(this.userProviders.keys());
|
|
267
|
+
const allIds = [...new Set([...configIds, ...userIds])];
|
|
268
|
+
|
|
269
|
+
const availability = await Promise.all(
|
|
270
|
+
allIds.map(async (id) => {
|
|
271
|
+
// Check user providers first
|
|
272
|
+
const userDef = this.userProviders.get(id);
|
|
273
|
+
if (userDef) {
|
|
274
|
+
return binaryExists(userDef.binary);
|
|
275
|
+
}
|
|
276
|
+
// Then config providers
|
|
277
|
+
const providerConfig = this.config.providers[id];
|
|
278
|
+
if (providerConfig) {
|
|
279
|
+
return binaryExists(providerConfig.binary);
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return allIds.filter((_, i) => availability[i]);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get all registered provider IDs (including user-defined)
|
|
290
|
+
*/
|
|
291
|
+
async getAllProviderIds(): Promise<string[]> {
|
|
292
|
+
await this.loadUserProviders();
|
|
293
|
+
const configIds = Object.keys(this.config.providers);
|
|
294
|
+
const userIds = Array.from(this.userProviders.keys());
|
|
295
|
+
return [...new Set([...configIds, ...userIds])];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Request deduplication for identical concurrent requests
|
|
301
|
+
*/
|
|
302
|
+
export class RequestDeduplicator {
|
|
303
|
+
private pendingRequests: Map<string, Promise<any>> = new Map();
|
|
304
|
+
private readonly ttlMs: number;
|
|
305
|
+
|
|
306
|
+
constructor(ttlMs = 100) {
|
|
307
|
+
this.ttlMs = ttlMs;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Generate a cache key for a request
|
|
312
|
+
*/
|
|
313
|
+
private getKey(providerId: string, prompt: string, mode?: string): string {
|
|
314
|
+
return `${providerId}:${mode || "default"}:${prompt.substring(0, 100)}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Deduplicate a request - if an identical request is in-flight, return its promise
|
|
319
|
+
*/
|
|
320
|
+
async dedupe<T>(
|
|
321
|
+
providerId: string,
|
|
322
|
+
prompt: string,
|
|
323
|
+
mode: string | undefined,
|
|
324
|
+
executor: () => Promise<T>,
|
|
325
|
+
): Promise<T> {
|
|
326
|
+
const key = this.getKey(providerId, prompt, mode);
|
|
327
|
+
|
|
328
|
+
// Check if identical request is already in-flight
|
|
329
|
+
if (this.pendingRequests.has(key)) {
|
|
330
|
+
return this.pendingRequests.get(key)! as Promise<T>;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Execute and cache the promise
|
|
334
|
+
const promise = executor().finally(() => {
|
|
335
|
+
// Remove from cache after TTL
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
this.pendingRequests.delete(key);
|
|
338
|
+
}, this.ttlMs);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
this.pendingRequests.set(key, promise);
|
|
342
|
+
return promise;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Clear all pending requests
|
|
347
|
+
*/
|
|
348
|
+
clear(): void {
|
|
349
|
+
this.pendingRequests.clear();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get count of pending requests
|
|
354
|
+
*/
|
|
355
|
+
get pendingCount(): number {
|
|
356
|
+
return this.pendingRequests.size;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Streaming buffer pool for memory efficiency
|
|
362
|
+
*/
|
|
363
|
+
export class BufferPool {
|
|
364
|
+
private pool: string[][] = [];
|
|
365
|
+
private readonly maxPoolSize: number;
|
|
366
|
+
|
|
367
|
+
constructor(maxPoolSize = 10) {
|
|
368
|
+
this.maxPoolSize = maxPoolSize;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get a buffer from the pool or create a new one
|
|
373
|
+
*/
|
|
374
|
+
acquire(): string[] {
|
|
375
|
+
return this.pool.pop() || [];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Return a buffer to the pool
|
|
380
|
+
*/
|
|
381
|
+
release(buffer: string[]): void {
|
|
382
|
+
if (this.pool.length < this.maxPoolSize) {
|
|
383
|
+
buffer.length = 0; // Clear the array
|
|
384
|
+
this.pool.push(buffer);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get current pool size
|
|
390
|
+
*/
|
|
391
|
+
get size(): number {
|
|
392
|
+
return this.pool.length;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Global buffer pool instance
|
|
397
|
+
export const globalBufferPool = new BufferPool();
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Loader - Dynamically loads provider configs from .ts files
|
|
3
|
+
*
|
|
4
|
+
* Scans these directories for provider definitions:
|
|
5
|
+
* 1. ~/.config/wraptc/providers/*.ts (user providers)
|
|
6
|
+
* 2. ./.wtc/providers/*.ts (project-local providers)
|
|
7
|
+
*
|
|
8
|
+
* Bun imports TypeScript directly - no build step required!
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { type ProviderDefinition, ProviderDefinitionSchema } from "./define-provider";
|
|
14
|
+
|
|
15
|
+
// Directories to scan for provider configs (in order of precedence)
|
|
16
|
+
const PROVIDER_DIRS = [
|
|
17
|
+
// User-level providers
|
|
18
|
+
join(process.env.HOME || "~", ".config", "wrap-terminalcoder", "providers"),
|
|
19
|
+
// Project-local providers (higher precedence)
|
|
20
|
+
"./.wtc/providers",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load a single provider definition from a file
|
|
25
|
+
*/
|
|
26
|
+
async function loadProviderFile(filePath: string): Promise<ProviderDefinition | null> {
|
|
27
|
+
try {
|
|
28
|
+
// Bun imports TypeScript directly - no transpilation needed!
|
|
29
|
+
const module = await import(filePath);
|
|
30
|
+
const rawConfig = module.default;
|
|
31
|
+
|
|
32
|
+
if (!rawConfig) {
|
|
33
|
+
console.warn(`No default export in ${filePath}`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate base config with Zod
|
|
38
|
+
const validated = ProviderDefinitionSchema.parse(rawConfig);
|
|
39
|
+
|
|
40
|
+
// Preserve function overrides that Zod strips
|
|
41
|
+
const fullConfig: ProviderDefinition = {
|
|
42
|
+
...validated,
|
|
43
|
+
buildArgs: rawConfig.buildArgs,
|
|
44
|
+
parseOutput: rawConfig.parseOutput,
|
|
45
|
+
classifyError: rawConfig.classifyError,
|
|
46
|
+
getStdinInput: rawConfig.getStdinInput,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return fullConfig;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn(`Failed to load provider from ${filePath}:`, err);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load all provider definitions from configured directories
|
|
58
|
+
*
|
|
59
|
+
* @returns Map of provider ID to ProviderDefinition
|
|
60
|
+
*/
|
|
61
|
+
export async function loadProviderConfigs(): Promise<Map<string, ProviderDefinition>> {
|
|
62
|
+
const providers = new Map<string, ProviderDefinition>();
|
|
63
|
+
|
|
64
|
+
for (const dir of PROVIDER_DIRS) {
|
|
65
|
+
// Resolve relative paths
|
|
66
|
+
const resolvedDir = dir.startsWith(".") ? join(process.cwd(), dir) : dir;
|
|
67
|
+
|
|
68
|
+
if (!existsSync(resolvedDir)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const glob = new Bun.Glob("*.ts");
|
|
74
|
+
|
|
75
|
+
for await (const file of glob.scan(resolvedDir)) {
|
|
76
|
+
// Skip test files and type definition files
|
|
77
|
+
if (file.endsWith(".test.ts") || file.endsWith(".d.ts")) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const filePath = join(resolvedDir, file);
|
|
82
|
+
const config = await loadProviderFile(filePath);
|
|
83
|
+
|
|
84
|
+
if (config) {
|
|
85
|
+
// Later directories (project-local) override earlier ones (user)
|
|
86
|
+
providers.set(config.id, config);
|
|
87
|
+
console.log(`Loaded provider: ${config.id} from ${filePath}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.warn(`Failed to scan directory ${resolvedDir}:`, err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return providers;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Watch provider directories for changes and reload
|
|
100
|
+
* (for development/hot-reload scenarios)
|
|
101
|
+
*/
|
|
102
|
+
export async function watchProviderConfigs(
|
|
103
|
+
onChange: (providers: Map<string, ProviderDefinition>) => void,
|
|
104
|
+
): Promise<() => void> {
|
|
105
|
+
const watchers: Array<{ close: () => void }> = [];
|
|
106
|
+
|
|
107
|
+
for (const dir of PROVIDER_DIRS) {
|
|
108
|
+
const resolvedDir = dir.startsWith(".") ? join(process.cwd(), dir) : dir;
|
|
109
|
+
|
|
110
|
+
if (!existsSync(resolvedDir)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const watcher = Bun.file(resolvedDir).watch();
|
|
116
|
+
|
|
117
|
+
// Debounce reloads
|
|
118
|
+
let reloadTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
119
|
+
|
|
120
|
+
const handleChange = async () => {
|
|
121
|
+
if (reloadTimeout) {
|
|
122
|
+
clearTimeout(reloadTimeout);
|
|
123
|
+
}
|
|
124
|
+
reloadTimeout = setTimeout(async () => {
|
|
125
|
+
const providers = await loadProviderConfigs();
|
|
126
|
+
onChange(providers);
|
|
127
|
+
}, 100);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Note: Bun.file().watch() is for files, not directories
|
|
131
|
+
// For directory watching, we'd need a different approach
|
|
132
|
+
// This is a simplified version
|
|
133
|
+
watchers.push({
|
|
134
|
+
close: () => {
|
|
135
|
+
if (reloadTimeout) {
|
|
136
|
+
clearTimeout(reloadTimeout);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
// Directory watching not critical
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Return cleanup function
|
|
146
|
+
return () => {
|
|
147
|
+
for (const watcher of watchers) {
|
|
148
|
+
watcher.close();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the paths where providers can be placed
|
|
155
|
+
*/
|
|
156
|
+
export function getProviderDirectories(): string[] {
|
|
157
|
+
return PROVIDER_DIRS.map((dir) => (dir.startsWith(".") ? join(process.cwd(), dir) : dir));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create the user provider directory if it doesn't exist
|
|
162
|
+
*/
|
|
163
|
+
export async function ensureUserProviderDir(): Promise<string> {
|
|
164
|
+
const userDir = join(process.env.HOME || "~", ".config", "wrap-terminalcoder", "providers");
|
|
165
|
+
|
|
166
|
+
if (!existsSync(userDir)) {
|
|
167
|
+
await Bun.$`mkdir -p ${userDir}`.quiet();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return userDir;
|
|
171
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexProvider - Simplified provider using ProcessProvider defaults
|
|
3
|
+
*
|
|
4
|
+
* Codex CLI uses:
|
|
5
|
+
* - exec subcommand: codex exec <prompt>
|
|
6
|
+
* - Text output (no JSON mode)
|
|
7
|
+
*/
|
|
8
|
+
import type {
|
|
9
|
+
CodingRequest,
|
|
10
|
+
ProviderConfig,
|
|
11
|
+
ProviderErrorContext,
|
|
12
|
+
ProviderErrorKind,
|
|
13
|
+
ProviderInvokeOptions,
|
|
14
|
+
} from "../types";
|
|
15
|
+
import { ProcessProvider } from "./index";
|
|
16
|
+
|
|
17
|
+
export class CodexProvider extends ProcessProvider {
|
|
18
|
+
constructor(config: ProviderConfig) {
|
|
19
|
+
super("codex", "Codex CLI", {
|
|
20
|
+
...config,
|
|
21
|
+
binary: config.binary || "codex",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Codex uses positional prompt (not stdin)
|
|
27
|
+
*/
|
|
28
|
+
protected getStdinInput(): string | undefined {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build args: codex exec [...args] <prompt>
|
|
34
|
+
*/
|
|
35
|
+
protected buildArgs(req: CodingRequest, _opts: ProviderInvokeOptions): string[] {
|
|
36
|
+
const args: string[] = ["exec"];
|
|
37
|
+
args.push(...this.config.args);
|
|
38
|
+
args.push(req.prompt);
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Codex has specific error patterns for plan limits
|
|
44
|
+
*/
|
|
45
|
+
classifyError(error: ProviderErrorContext): ProviderErrorKind {
|
|
46
|
+
const combined = ((error.stderr || "") + (error.stdout || "")).toLowerCase();
|
|
47
|
+
|
|
48
|
+
// Codex-specific: plan limits
|
|
49
|
+
if (combined.includes("plan limit") || combined.includes("subscription required")) {
|
|
50
|
+
return "OUT_OF_CREDITS";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Use base implementation for common patterns
|
|
54
|
+
return super.classifyError(error);
|
|
55
|
+
}
|
|
56
|
+
}
|