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,258 @@
1
+ import { rename } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import type { FullState, ProviderErrorKind, ProviderState } from "./types";
4
+
5
+ export interface StateManagerOptions {
6
+ statePath?: string;
7
+ }
8
+
9
+ export class StateManager {
10
+ private statePath: string;
11
+ private state: FullState;
12
+ private isDirty = false;
13
+ private saveTimer?: Timer;
14
+ private initialized = false;
15
+
16
+ constructor(options: StateManagerOptions = {}) {
17
+ this.statePath = options.statePath || this.getDefaultStatePath();
18
+ this.state = this.createInitialState();
19
+ }
20
+
21
+ private getDefaultStatePath(): string {
22
+ return join(process.env.HOME || "~", ".config", "wrap-terminalcoder", "state.json");
23
+ }
24
+
25
+ private createInitialState(): FullState {
26
+ return {
27
+ version: "1.0.0",
28
+ providers: {},
29
+ };
30
+ }
31
+
32
+ async initialize(): Promise<void> {
33
+ if (this.initialized) {
34
+ return;
35
+ }
36
+
37
+ try {
38
+ // Use Bun.file() for faster file reading
39
+ const file = Bun.file(this.statePath);
40
+ const exists = await file.exists();
41
+
42
+ if (exists) {
43
+ const data = await file.text();
44
+ this.state = JSON.parse(data);
45
+
46
+ // Reset daily counters if needed
47
+ await this.resetDailyCountersIfNeeded();
48
+ }
49
+ } catch (error) {
50
+ // If parsing fails or other error, start with initial state
51
+ console.error("Failed to load state, starting fresh:", error);
52
+ }
53
+
54
+ this.initialized = true;
55
+ }
56
+
57
+ private async resetDailyCountersIfNeeded(): Promise<void> {
58
+ const now = new Date();
59
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
60
+
61
+ for (const [_providerId, state] of Object.entries(this.state.providers)) {
62
+ const lastReset = state.lastReset ? new Date(state.lastReset) : null;
63
+
64
+ if (!lastReset || lastReset < today) {
65
+ state.requestsToday = 0;
66
+ state.lastReset = now.toISOString();
67
+ state.outOfCreditsUntil = undefined;
68
+ // Reset daily tokens saved counter
69
+ state.tokensSavedToday = 0;
70
+ this.isDirty = true;
71
+ }
72
+ }
73
+
74
+ if (this.isDirty) {
75
+ await this.save();
76
+ }
77
+ }
78
+
79
+ async getProviderState(providerId: string): Promise<ProviderState> {
80
+ await this.initialize();
81
+
82
+ if (!this.state.providers[providerId]) {
83
+ this.state.providers[providerId] = {
84
+ lastUsedAt: undefined,
85
+ requestsToday: 0,
86
+ lastReset: undefined,
87
+ outOfCreditsUntil: undefined,
88
+ lastErrors: [],
89
+ consecutiveErrors: 0,
90
+ tokensSavedToday: 0,
91
+ totalTokensSaved: 0,
92
+ };
93
+ this.isDirty = true;
94
+ }
95
+
96
+ // Ensure new fields exist on existing states (migration)
97
+ const state = this.state.providers[providerId];
98
+ if (state.consecutiveErrors === undefined) {
99
+ state.consecutiveErrors = 0;
100
+ this.isDirty = true;
101
+ }
102
+ if (state.tokensSavedToday === undefined) {
103
+ state.tokensSavedToday = 0;
104
+ this.isDirty = true;
105
+ }
106
+ if (state.totalTokensSaved === undefined) {
107
+ state.totalTokensSaved = 0;
108
+ this.isDirty = true;
109
+ }
110
+
111
+ return state;
112
+ }
113
+
114
+ async recordSuccess(providerId: string, tokensSaved = 0): Promise<void> {
115
+ const state = await this.getProviderState(providerId);
116
+ const now = new Date();
117
+
118
+ state.lastUsedAt = now.toISOString();
119
+ state.requestsToday++;
120
+ state.lastErrors = []; // Clear errors on success
121
+ state.consecutiveErrors = 0; // Reset error counter on success
122
+ state.tokensSavedToday += tokensSaved;
123
+ state.totalTokensSaved += tokensSaved;
124
+ this.isDirty = true;
125
+
126
+ await this.scheduleSave();
127
+ }
128
+
129
+ async recordError(providerId: string, kind: ProviderErrorKind, message: string): Promise<void> {
130
+ const state = await this.getProviderState(providerId);
131
+
132
+ state.lastErrors.push(`[${new Date().toISOString()}] ${kind}: ${message}`);
133
+ state.consecutiveErrors++;
134
+
135
+ // Keep only last 10 errors
136
+ if (state.lastErrors.length > 10) {
137
+ state.lastErrors = state.lastErrors.slice(-10);
138
+ }
139
+
140
+ this.isDirty = true;
141
+
142
+ await this.scheduleSave();
143
+ }
144
+
145
+ /**
146
+ * Check if provider should be blacklisted based on error threshold
147
+ */
148
+ async shouldBlacklist(providerId: string, errorThreshold: number): Promise<boolean> {
149
+ const state = await this.getProviderState(providerId);
150
+ return state.consecutiveErrors >= errorThreshold;
151
+ }
152
+
153
+ /**
154
+ * Blacklist a provider for a specified duration
155
+ */
156
+ async blacklistProvider(providerId: string, durationHours: number): Promise<void> {
157
+ const until = new Date();
158
+ until.setHours(until.getHours() + durationHours);
159
+ await this.markOutOfCredits(providerId, until);
160
+ }
161
+
162
+ /**
163
+ * Get total tokens saved across all providers
164
+ */
165
+ async getTotalTokensSaved(): Promise<number> {
166
+ await this.initialize();
167
+ return Object.values(this.state.providers).reduce(
168
+ (sum, state) => sum + (state.totalTokensSaved || 0),
169
+ 0,
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Get tokens saved today across all providers
175
+ */
176
+ async getTokensSavedToday(): Promise<number> {
177
+ await this.initialize();
178
+ return Object.values(this.state.providers).reduce(
179
+ (sum, state) => sum + (state.tokensSavedToday || 0),
180
+ 0,
181
+ );
182
+ }
183
+
184
+ async markOutOfCredits(providerId: string, until: Date): Promise<void> {
185
+ const state = await this.getProviderState(providerId);
186
+
187
+ state.outOfCreditsUntil = until.toISOString();
188
+ this.isDirty = true;
189
+
190
+ await this.scheduleSave();
191
+ }
192
+
193
+ async resetProvider(providerId: string): Promise<void> {
194
+ const state = await this.getProviderState(providerId);
195
+
196
+ state.requestsToday = 0;
197
+ state.outOfCreditsUntil = undefined;
198
+ state.lastErrors = [];
199
+ state.consecutiveErrors = 0;
200
+ state.tokensSavedToday = 0;
201
+ // Note: totalTokensSaved is preserved across resets
202
+ this.isDirty = true;
203
+
204
+ await this.scheduleSave();
205
+ }
206
+
207
+ async resetAll(): Promise<void> {
208
+ this.state = this.createInitialState();
209
+ this.isDirty = true;
210
+
211
+ await this.save();
212
+ }
213
+
214
+ private async scheduleSave(): Promise<void> {
215
+ if (this.saveTimer) {
216
+ clearTimeout(this.saveTimer);
217
+ }
218
+
219
+ // Debounce saves to avoid too many writes
220
+ this.saveTimer = setTimeout(() => {
221
+ this.save().catch(console.error);
222
+ }, 1000);
223
+ }
224
+
225
+ async save(): Promise<void> {
226
+ if (!this.isDirty) {
227
+ return;
228
+ }
229
+
230
+ try {
231
+ // Ensure directory exists using Bun shell
232
+ const dir = dirname(this.statePath);
233
+ await Bun.$`mkdir -p ${dir}`.quiet();
234
+
235
+ // Write atomically: write to temp file, then rename
236
+ const tempPath = `${this.statePath}.tmp`;
237
+
238
+ // Use Bun.write() for faster file writing
239
+ await Bun.write(tempPath, JSON.stringify(this.state, null, 2));
240
+
241
+ // Atomic rename (still using node:fs rename for reliability)
242
+ await rename(tempPath, this.statePath);
243
+ } catch (error) {
244
+ // Log the error but don't throw to avoid breaking tests
245
+ console.error("Failed to save state:", error);
246
+ }
247
+
248
+ this.isDirty = false;
249
+ }
250
+
251
+ getState(): FullState {
252
+ return { ...this.state };
253
+ }
254
+
255
+ getStatePath(): string {
256
+ return this.statePath;
257
+ }
258
+ }
@@ -0,0 +1,206 @@
1
+ // Types for wrap-terminalcoder
2
+ import { z } from "zod";
3
+
4
+ // Standard coding modes
5
+ export const CodingMode = {
6
+ GENERATE: "generate",
7
+ EDIT: "edit",
8
+ EXPLAIN: "explain",
9
+ TEST: "test",
10
+ REVIEW: "review",
11
+ REFACTOR: "refactor",
12
+ } as const;
13
+
14
+ export type CodingModeType = (typeof CodingMode)[keyof typeof CodingMode];
15
+
16
+ // Provider configuration schema
17
+ export const ProviderConfigSchema = z.object({
18
+ binary: z.string(),
19
+ args: z.array(z.string()).default([]),
20
+ jsonMode: z.enum(["none", "flag"]).default("none"),
21
+ jsonFlag: z.string().optional(),
22
+ streamingMode: z.enum(["none", "line", "jsonl"]).default("none"),
23
+ capabilities: z.array(z.string()).default([]),
24
+ argsTemplate: z.array(z.string()).optional(),
25
+ // New: default model for this provider
26
+ defaultModel: z.string().optional(),
27
+ });
28
+
29
+ // Provider configuration type
30
+ export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
31
+
32
+ // Provider invoke options
33
+ export interface ProviderInvokeOptions {
34
+ signal?: AbortSignal;
35
+ cwd?: string;
36
+ // Environment variables to pass to the process (merged with provider defaults)
37
+ env?: Record<string, string>;
38
+ // Timeout in milliseconds (overrides provider default)
39
+ timeoutMs?: number;
40
+ }
41
+
42
+ // Main configuration schema
43
+ export const ConfigSchema = z.object({
44
+ routing: z.object({
45
+ defaultOrder: z.array(z.string()),
46
+ perModeOverride: z.record(z.array(z.string())).optional(),
47
+ }),
48
+ providers: z.record(ProviderConfigSchema),
49
+ credits: z.object({
50
+ providers: z.record(
51
+ z.union([
52
+ z.object({
53
+ dailyRequestLimit: z.number(),
54
+ resetHourUtc: z.number(),
55
+ }),
56
+ z.object({
57
+ plan: z.string(),
58
+ }),
59
+ ]),
60
+ ),
61
+ }),
62
+ });
63
+
64
+ // Main configuration type
65
+ export type Config = z.infer<typeof ConfigSchema>;
66
+
67
+ // File context for providing code context
68
+ export const FileContextSchema = z.object({
69
+ path: z.string(),
70
+ content: z.string().optional(),
71
+ language: z.string().optional(),
72
+ });
73
+
74
+ export type FileContext = z.infer<typeof FileContextSchema>;
75
+
76
+ // Coding request schema
77
+ export const CodingRequestSchema = z.object({
78
+ // Required: the main prompt/instruction
79
+ prompt: z.string(),
80
+ // Optional: coding mode (generate, edit, explain, test, review, refactor)
81
+ mode: z.string().optional(),
82
+ // Optional: specific provider to use (bypasses routing)
83
+ provider: z.string().optional(),
84
+ // Optional: specific model to use (provider-specific)
85
+ model: z.string().optional(),
86
+ // Optional: enable streaming response
87
+ stream: z.boolean().optional(),
88
+ // Optional: file context (renamed from fileContext for consistency)
89
+ files: z.array(z.union([z.string(), FileContextSchema])).optional(),
90
+ // Optional: system prompt / instructions
91
+ systemPrompt: z.string().optional(),
92
+ // Optional: maximum output tokens
93
+ maxTokens: z.number().optional(),
94
+ // Optional: temperature (0-2, default varies by provider)
95
+ temperature: z.number().min(0).max(2).optional(),
96
+ // Optional: target language for code generation
97
+ language: z.string().optional(),
98
+ // Deprecated: use 'files' instead
99
+ fileContext: z.array(z.string()).optional(),
100
+ });
101
+
102
+ // Coding request type
103
+ export interface CodingRequest {
104
+ prompt: string;
105
+ mode?: CodingModeType | string;
106
+ provider?: string;
107
+ model?: string;
108
+ stream?: boolean;
109
+ files?: (string | FileContext)[];
110
+ systemPrompt?: string;
111
+ maxTokens?: number;
112
+ temperature?: number;
113
+ language?: string;
114
+ /** @deprecated Use 'files' instead */
115
+ fileContext?: string[];
116
+ }
117
+
118
+ // Token usage information
119
+ export interface TokenUsage {
120
+ inputTokens?: number;
121
+ outputTokens?: number;
122
+ totalTokens?: number;
123
+ }
124
+
125
+ // Response metadata
126
+ export interface ResponseMeta {
127
+ elapsedMs?: number;
128
+ model?: string;
129
+ finishReason?: "stop" | "length" | "content_filter" | "error";
130
+ }
131
+
132
+ // Coding response type
133
+ export interface CodingResponse {
134
+ provider: string;
135
+ model?: string;
136
+ text: string;
137
+ usage?: TokenUsage;
138
+ meta?: ResponseMeta;
139
+ }
140
+
141
+ // Streaming event types - unified naming
142
+ export type CodingEvent =
143
+ // Stream started
144
+ | { type: "start"; provider: string; model?: string; requestId: string }
145
+ // Text content delta (incremental text)
146
+ | { type: "text_delta"; text: string }
147
+ // Structured chunk (for JSONL mode - parsed JSON object)
148
+ | { type: "chunk"; data: unknown }
149
+ // Stream completed successfully
150
+ | {
151
+ type: "complete";
152
+ provider: string;
153
+ model?: string;
154
+ text: string;
155
+ usage?: TokenUsage;
156
+ finishReason?: "stop" | "length" | "content_filter";
157
+ }
158
+ // Error occurred
159
+ | { type: "error"; provider: string; code: ProviderErrorKind; message: string };
160
+
161
+ // Error context type
162
+ export interface ProviderErrorContext {
163
+ stderr?: string;
164
+ stdout?: string;
165
+ exitCode?: number | null;
166
+ httpStatus?: number;
167
+ }
168
+
169
+ // Error kind enum - expanded
170
+ export type ProviderErrorKind =
171
+ | "OUT_OF_CREDITS" // Provider credits/quota exhausted
172
+ | "RATE_LIMIT" // Rate limit hit, retry after cooldown
173
+ | "BAD_REQUEST" // Invalid request (malformed, missing params)
174
+ | "UNAUTHORIZED" // Authentication failed (bad API key)
175
+ | "FORBIDDEN" // Permission denied
176
+ | "NOT_FOUND" // Resource not found (model, endpoint)
177
+ | "TIMEOUT" // Request timed out
178
+ | "CONTEXT_LENGTH" // Input too long for model
179
+ | "CONTENT_FILTER" // Content blocked by safety filter
180
+ | "INTERNAL" // Provider internal error
181
+ | "TRANSIENT" // Temporary error, safe to retry
182
+ | "UNKNOWN"; // Unknown error type
183
+
184
+ // Provider info type
185
+ export interface ProviderInfo {
186
+ id: string;
187
+ displayName: string;
188
+ supportsStreaming: boolean;
189
+ prefersJson: boolean;
190
+ capabilities?: string[];
191
+ }
192
+
193
+ // Provider state type
194
+ export interface ProviderState {
195
+ lastUsedAt?: string;
196
+ requestsToday: number;
197
+ lastReset?: string;
198
+ outOfCreditsUntil?: string;
199
+ lastErrors: string[];
200
+ }
201
+
202
+ // Full state type
203
+ export interface FullState {
204
+ version: string;
205
+ providers: Record<string, ProviderState>;
206
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Capability System - Typed capabilities for terminal coders
3
+ *
4
+ * Provides a structured way to declare and query provider capabilities.
5
+ */
6
+
7
+ /**
8
+ * Standard capability types
9
+ */
10
+ export const Capability = {
11
+ // Coding modes
12
+ GENERATE: "generate",
13
+ EDIT: "edit",
14
+ EXPLAIN: "explain",
15
+ TEST: "test",
16
+ REVIEW: "review",
17
+ REFACTOR: "refactor",
18
+
19
+ // Input handling
20
+ FILE_CONTEXT: "file_context",
21
+ MULTI_FILE: "multi_file",
22
+ SYSTEM_PROMPT: "system_prompt",
23
+
24
+ // Output features
25
+ STREAMING: "streaming",
26
+ JSON_OUTPUT: "json_output",
27
+ USAGE_TRACKING: "usage_tracking",
28
+
29
+ // Advanced features
30
+ MODEL_SELECTION: "model_selection",
31
+ TEMPERATURE_CONTROL: "temperature_control",
32
+ MAX_TOKENS: "max_tokens",
33
+ } as const;
34
+
35
+ export type CapabilityType = (typeof Capability)[keyof typeof Capability];
36
+
37
+ /**
38
+ * Capabilities interface - what a provider can do
39
+ */
40
+ export interface ICapabilities {
41
+ /** Supported coding modes */
42
+ readonly supportedModes: ReadonlySet<string>;
43
+
44
+ // Input capabilities
45
+ readonly supportsFileContext: boolean;
46
+ readonly supportsMultiFile: boolean;
47
+ readonly supportsSystemPrompt: boolean;
48
+
49
+ // Output capabilities
50
+ readonly supportsStreaming: boolean;
51
+ readonly supportsJsonOutput: boolean;
52
+ readonly supportsUsageTracking: boolean;
53
+
54
+ // Request parameter capabilities
55
+ readonly supportsModelSelection: boolean;
56
+ readonly supportsTemperature: boolean;
57
+ readonly supportsMaxTokens: boolean;
58
+
59
+ // Feature discovery
60
+ hasCapability(capability: CapabilityType): boolean;
61
+ getCapabilities(): CapabilityType[];
62
+ }
63
+
64
+ /**
65
+ * Implementation of ICapabilities
66
+ */
67
+ export class ProviderCapabilities implements ICapabilities {
68
+ private readonly capabilities: Set<CapabilityType>;
69
+
70
+ constructor(capabilityList: CapabilityType[]) {
71
+ this.capabilities = new Set(capabilityList);
72
+ }
73
+
74
+ get supportedModes(): ReadonlySet<string> {
75
+ const modes = new Set<string>();
76
+ const modeCapabilities = [
77
+ Capability.GENERATE,
78
+ Capability.EDIT,
79
+ Capability.EXPLAIN,
80
+ Capability.TEST,
81
+ Capability.REVIEW,
82
+ Capability.REFACTOR,
83
+ ];
84
+
85
+ for (const cap of modeCapabilities) {
86
+ if (this.capabilities.has(cap)) {
87
+ modes.add(cap);
88
+ }
89
+ }
90
+
91
+ return modes;
92
+ }
93
+
94
+ get supportsFileContext(): boolean {
95
+ return this.capabilities.has(Capability.FILE_CONTEXT);
96
+ }
97
+
98
+ get supportsMultiFile(): boolean {
99
+ return this.capabilities.has(Capability.MULTI_FILE);
100
+ }
101
+
102
+ get supportsSystemPrompt(): boolean {
103
+ return this.capabilities.has(Capability.SYSTEM_PROMPT);
104
+ }
105
+
106
+ get supportsStreaming(): boolean {
107
+ return this.capabilities.has(Capability.STREAMING);
108
+ }
109
+
110
+ get supportsJsonOutput(): boolean {
111
+ return this.capabilities.has(Capability.JSON_OUTPUT);
112
+ }
113
+
114
+ get supportsUsageTracking(): boolean {
115
+ return this.capabilities.has(Capability.USAGE_TRACKING);
116
+ }
117
+
118
+ get supportsModelSelection(): boolean {
119
+ return this.capabilities.has(Capability.MODEL_SELECTION);
120
+ }
121
+
122
+ get supportsTemperature(): boolean {
123
+ return this.capabilities.has(Capability.TEMPERATURE_CONTROL);
124
+ }
125
+
126
+ get supportsMaxTokens(): boolean {
127
+ return this.capabilities.has(Capability.MAX_TOKENS);
128
+ }
129
+
130
+ hasCapability(capability: CapabilityType): boolean {
131
+ return this.capabilities.has(capability);
132
+ }
133
+
134
+ getCapabilities(): CapabilityType[] {
135
+ return Array.from(this.capabilities);
136
+ }
137
+
138
+ /**
139
+ * Factory method from string array (for backwards compatibility)
140
+ */
141
+ static fromStringArray(strings: string[]): ProviderCapabilities {
142
+ const capabilities: CapabilityType[] = [];
143
+
144
+ for (const str of strings) {
145
+ // Map legacy string capabilities to typed capabilities
146
+ const mapping: Record<string, CapabilityType> = {
147
+ generate: Capability.GENERATE,
148
+ edit: Capability.EDIT,
149
+ explain: Capability.EXPLAIN,
150
+ test: Capability.TEST,
151
+ review: Capability.REVIEW,
152
+ refactor: Capability.REFACTOR,
153
+ streaming: Capability.STREAMING,
154
+ json_output: Capability.JSON_OUTPUT,
155
+ };
156
+
157
+ const cap = mapping[str.toLowerCase()];
158
+ if (cap) capabilities.push(cap);
159
+ }
160
+
161
+ return new ProviderCapabilities(capabilities);
162
+ }
163
+
164
+ /**
165
+ * Create capabilities from provider info
166
+ */
167
+ static fromProviderInfo(info: {
168
+ capabilities?: string[];
169
+ supportsStreaming?: boolean;
170
+ prefersJson?: boolean;
171
+ }): ProviderCapabilities {
172
+ const caps = ProviderCapabilities.fromStringArray(info.capabilities ?? []);
173
+ const allCaps = caps.getCapabilities();
174
+
175
+ if (info.supportsStreaming) {
176
+ allCaps.push(Capability.STREAMING);
177
+ }
178
+ if (info.prefersJson) {
179
+ allCaps.push(Capability.JSON_OUTPUT);
180
+ }
181
+
182
+ return new ProviderCapabilities(allCaps);
183
+ }
184
+ }