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.
Files changed (71) hide show
  1. package/bin/wraptc +4 -4
  2. package/package.json +2 -2
  3. package/src/cli/__tests__/cli.test.ts +337 -0
  4. package/src/cli/index.ts +149 -0
  5. package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
  6. package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
  7. package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
  8. package/src/core/__tests__/integration/integration.test.ts +241 -0
  9. package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
  10. package/src/core/__tests__/test-utils.ts +136 -0
  11. package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
  12. package/src/core/__tests__/unit/basic-test.test.ts +44 -0
  13. package/src/core/__tests__/unit/basic.test.ts +12 -0
  14. package/src/core/__tests__/unit/config.test.ts +244 -0
  15. package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
  16. package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
  17. package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
  18. package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
  19. package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
  20. package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
  21. package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
  22. package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
  23. package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
  24. package/src/core/__tests__/unit/router.test.ts +967 -0
  25. package/src/core/__tests__/unit/state.test.ts +1079 -0
  26. package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
  27. package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
  28. package/src/core/adapters/builtin/codex.ts +35 -0
  29. package/src/core/adapters/builtin/gemini.ts +34 -0
  30. package/src/core/adapters/builtin/index.ts +31 -0
  31. package/src/core/adapters/builtin/mock-coder.ts +148 -0
  32. package/src/core/adapters/builtin/qwen.ts +34 -0
  33. package/src/core/adapters/define.ts +48 -0
  34. package/src/core/adapters/index.ts +43 -0
  35. package/src/core/adapters/loader.ts +143 -0
  36. package/src/core/adapters/provider-bridge.ts +190 -0
  37. package/src/core/adapters/runner.ts +437 -0
  38. package/src/core/adapters/types.ts +172 -0
  39. package/src/core/config.ts +290 -0
  40. package/src/core/define-provider.ts +212 -0
  41. package/src/core/error-patterns.ts +147 -0
  42. package/src/core/index.ts +130 -0
  43. package/src/core/memory-monitor.ts +171 -0
  44. package/src/core/plugin/builtin.ts +87 -0
  45. package/src/core/plugin/index.ts +34 -0
  46. package/src/core/plugin/registry.ts +350 -0
  47. package/src/core/plugin/types.ts +209 -0
  48. package/src/core/provider-factory.ts +397 -0
  49. package/src/core/provider-loader.ts +171 -0
  50. package/src/core/providers/codex.ts +56 -0
  51. package/src/core/providers/configurable.ts +637 -0
  52. package/src/core/providers/custom.ts +261 -0
  53. package/src/core/providers/gemini.ts +41 -0
  54. package/src/core/providers/index.ts +383 -0
  55. package/src/core/providers/opencode.ts +168 -0
  56. package/src/core/providers/qwen-code.ts +41 -0
  57. package/src/core/router.ts +370 -0
  58. package/src/core/state.ts +258 -0
  59. package/src/core/types.ts +206 -0
  60. package/src/core/unified/capabilities.ts +184 -0
  61. package/src/core/unified/errors.ts +141 -0
  62. package/src/core/unified/index.ts +29 -0
  63. package/src/core/unified/output.ts +189 -0
  64. package/src/core/wrap-terminalcoder.ts +245 -0
  65. package/src/mcp/__tests__/server.test.ts +295 -0
  66. package/src/mcp/server.ts +284 -0
  67. package/src/test-fixtures/mock-coder.sh +194 -0
  68. package/dist/cli/index.js +0 -16501
  69. package/dist/core/index.js +0 -7531
  70. package/dist/mcp/server.js +0 -14568
  71. 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
+ }