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,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
|
+
}
|