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,350 @@
1
+ /**
2
+ * PluginRegistry - Central registry for terminal coder plugins
3
+ *
4
+ * Singleton pattern with lifecycle management and provider creation.
5
+ */
6
+
7
+ import { binaryExists } from "../provider-factory";
8
+ import type { Provider } from "../providers/index";
9
+ import type {
10
+ CreateProviderOptions,
11
+ IPluginRegistry,
12
+ ManagedProvider,
13
+ PluginContext,
14
+ PluginDefinition,
15
+ PluginInfo,
16
+ PluginRegistrationOptions,
17
+ PluginRegistrationResult,
18
+ } from "./types";
19
+
20
+ /**
21
+ * PluginRegistry - Manages plugin registration and provider creation
22
+ */
23
+ export class PluginRegistry implements IPluginRegistry {
24
+ private static instance: PluginRegistry | null = null;
25
+
26
+ /** Registered plugins by type */
27
+ private plugins = new Map<string, PluginDefinition>();
28
+
29
+ /** Track built-in plugins */
30
+ private builtInTypes = new Set<string>();
31
+
32
+ /** Track initialized providers for lifecycle management */
33
+ private initializedProviders = new Map<string, ManagedProvider>();
34
+
35
+ /** Default context */
36
+ private defaultContext: PluginContext;
37
+
38
+ private constructor() {
39
+ this.defaultContext = {
40
+ registry: this,
41
+ cwd: process.cwd(),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Get the singleton instance
47
+ */
48
+ static getInstance(): PluginRegistry {
49
+ if (!PluginRegistry.instance) {
50
+ PluginRegistry.instance = new PluginRegistry();
51
+ }
52
+ return PluginRegistry.instance;
53
+ }
54
+
55
+ /**
56
+ * Reset the singleton instance (for testing)
57
+ */
58
+ static resetInstance(): void {
59
+ if (PluginRegistry.instance) {
60
+ // Clean up initialized providers
61
+ PluginRegistry.instance.shutdownAll().catch(console.warn);
62
+ }
63
+ PluginRegistry.instance = null;
64
+ }
65
+
66
+ /**
67
+ * Register a plugin (async, with lifecycle hooks)
68
+ */
69
+ async register(
70
+ plugin: PluginDefinition,
71
+ options: PluginRegistrationOptions = {},
72
+ ): Promise<PluginRegistrationResult> {
73
+ const { overwrite = false, builtIn = false } = options;
74
+
75
+ // Check for existing registration
76
+ if (this.plugins.has(plugin.type)) {
77
+ if (!overwrite) {
78
+ return {
79
+ success: false,
80
+ type: plugin.type,
81
+ message: `Plugin type '${plugin.type}' is already registered.`,
82
+ };
83
+ }
84
+
85
+ // Call onUnregister for existing plugin
86
+ const existing = this.plugins.get(plugin.type);
87
+ if (existing?.onUnregister) {
88
+ try {
89
+ await existing.onUnregister(this.defaultContext);
90
+ } catch (error) {
91
+ console.warn(`Error during onUnregister for plugin '${plugin.type}':`, error);
92
+ }
93
+ }
94
+ }
95
+
96
+ // Register the plugin
97
+ this.plugins.set(plugin.type, plugin);
98
+
99
+ // Mark as built-in if specified
100
+ if (builtIn) {
101
+ this.builtInTypes.add(plugin.type);
102
+ }
103
+
104
+ // Call onRegister hook
105
+ if (plugin.onRegister) {
106
+ try {
107
+ await plugin.onRegister(this.defaultContext);
108
+ } catch (error) {
109
+ // Rollback on failure
110
+ this.plugins.delete(plugin.type);
111
+ this.builtInTypes.delete(plugin.type);
112
+ return {
113
+ success: false,
114
+ type: plugin.type,
115
+ message: `Plugin onRegister failed: ${
116
+ error instanceof Error ? error.message : String(error)
117
+ }`,
118
+ };
119
+ }
120
+ }
121
+
122
+ return {
123
+ success: true,
124
+ type: plugin.type,
125
+ overwritten: this.plugins.has(plugin.type) && overwrite,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Register a plugin synchronously (no lifecycle hooks called)
131
+ */
132
+ registerSync(
133
+ plugin: PluginDefinition,
134
+ options: PluginRegistrationOptions = {},
135
+ ): PluginRegistrationResult {
136
+ const { overwrite = false, builtIn = false } = options;
137
+
138
+ if (this.plugins.has(plugin.type) && !overwrite) {
139
+ return {
140
+ success: false,
141
+ type: plugin.type,
142
+ message: `Plugin type '${plugin.type}' is already registered.`,
143
+ };
144
+ }
145
+
146
+ this.plugins.set(plugin.type, plugin);
147
+ if (builtIn) {
148
+ this.builtInTypes.add(plugin.type);
149
+ }
150
+
151
+ return { success: true, type: plugin.type };
152
+ }
153
+
154
+ /**
155
+ * Unregister a plugin
156
+ */
157
+ async unregister(type: string): Promise<boolean> {
158
+ const plugin = this.plugins.get(type);
159
+ if (!plugin) return false;
160
+
161
+ // Shutdown any initialized provider
162
+ const provider = this.initializedProviders.get(type);
163
+ if (provider?.shutdown) {
164
+ await provider.shutdown();
165
+ this.initializedProviders.delete(type);
166
+ }
167
+
168
+ // Call onUnregister hook
169
+ if (plugin.onUnregister) {
170
+ await plugin.onUnregister(this.defaultContext);
171
+ }
172
+
173
+ this.plugins.delete(type);
174
+ this.builtInTypes.delete(type);
175
+ return true;
176
+ }
177
+
178
+ /**
179
+ * Check if a plugin type is registered
180
+ */
181
+ has(type: string): boolean {
182
+ return this.plugins.has(type);
183
+ }
184
+
185
+ /**
186
+ * Get a plugin definition
187
+ */
188
+ get(type: string): PluginDefinition | undefined {
189
+ return this.plugins.get(type);
190
+ }
191
+
192
+ /**
193
+ * Get all registered plugin types
194
+ */
195
+ getTypes(): string[] {
196
+ return Array.from(this.plugins.keys());
197
+ }
198
+
199
+ /**
200
+ * List all plugins with metadata
201
+ */
202
+ listPlugins(): PluginInfo[] {
203
+ const infos: PluginInfo[] = [];
204
+
205
+ for (const plugin of this.plugins.values()) {
206
+ infos.push({
207
+ type: plugin.type,
208
+ displayName: plugin.displayName,
209
+ description: plugin.description,
210
+ docsUrl: plugin.docsUrl,
211
+ version: plugin.version,
212
+ capabilities: plugin.capabilities,
213
+ binary: plugin.binary,
214
+ hasLifecycle: plugin.hasLifecycle,
215
+ isBuiltIn: this.builtInTypes.has(plugin.type),
216
+ });
217
+ }
218
+
219
+ return infos;
220
+ }
221
+
222
+ /**
223
+ * Get plugin info by type
224
+ */
225
+ getPluginInfo(type: string): PluginInfo | undefined {
226
+ const plugin = this.plugins.get(type);
227
+ if (!plugin) return undefined;
228
+
229
+ return {
230
+ type: plugin.type,
231
+ displayName: plugin.displayName,
232
+ description: plugin.description,
233
+ docsUrl: plugin.docsUrl,
234
+ version: plugin.version,
235
+ capabilities: plugin.capabilities,
236
+ binary: plugin.binary,
237
+ hasLifecycle: plugin.hasLifecycle,
238
+ isBuiltIn: this.builtInTypes.has(type),
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Create a provider from a registered plugin
244
+ */
245
+ createProvider(
246
+ type: string,
247
+ config: unknown,
248
+ options: CreateProviderOptions = {},
249
+ ): ManagedProvider {
250
+ const plugin = this.plugins.get(type);
251
+ if (!plugin) {
252
+ throw new Error(
253
+ `No plugin registered for type '${type}'. ` +
254
+ `Available types: ${this.getTypes().join(", ") || "none"}`,
255
+ );
256
+ }
257
+
258
+ // Use factory function
259
+ const context = options.context ?? this.defaultContext;
260
+ return plugin.factory(config, context) as ManagedProvider;
261
+ }
262
+
263
+ /**
264
+ * Create and initialize a provider with lifecycle management
265
+ */
266
+ async createAndInitProvider(
267
+ type: string,
268
+ config: unknown,
269
+ options: CreateProviderOptions = {},
270
+ ): Promise<ManagedProvider> {
271
+ const provider = this.createProvider(type, config, options);
272
+
273
+ // Initialize if lifecycle provider
274
+ if (provider.init) {
275
+ await provider.init();
276
+ this.initializedProviders.set(type, provider);
277
+ }
278
+
279
+ return provider;
280
+ }
281
+
282
+ /**
283
+ * Check if a plugin's binary is available
284
+ */
285
+ async isAvailable(type: string): Promise<boolean> {
286
+ const plugin = this.plugins.get(type);
287
+ if (!plugin) return false;
288
+ if (!plugin.binary) return true;
289
+ return binaryExists(plugin.binary);
290
+ }
291
+
292
+ /**
293
+ * Get all available plugin types (with valid binaries)
294
+ */
295
+ async getAvailableTypes(): Promise<string[]> {
296
+ const types = this.getTypes();
297
+ const availability = await Promise.all(types.map((type) => this.isAvailable(type)));
298
+ return types.filter((_, i) => availability[i]);
299
+ }
300
+
301
+ /**
302
+ * Shutdown all initialized providers
303
+ */
304
+ async shutdownAll(): Promise<void> {
305
+ for (const provider of this.initializedProviders.values()) {
306
+ if (provider.shutdown) {
307
+ try {
308
+ await provider.shutdown();
309
+ } catch (error) {
310
+ console.warn("Error during provider shutdown:", error);
311
+ }
312
+ }
313
+ }
314
+ this.initializedProviders.clear();
315
+ }
316
+
317
+ /**
318
+ * Clear all plugins
319
+ */
320
+ async clear(): Promise<void> {
321
+ // Shutdown initialized providers
322
+ await this.shutdownAll();
323
+
324
+ // Call onUnregister for all plugins
325
+ for (const plugin of this.plugins.values()) {
326
+ if (plugin.onUnregister) {
327
+ try {
328
+ await plugin.onUnregister(this.defaultContext);
329
+ } catch (error) {
330
+ console.warn(`Error during onUnregister for plugin '${plugin.type}':`, error);
331
+ }
332
+ }
333
+ }
334
+
335
+ this.plugins.clear();
336
+ this.builtInTypes.clear();
337
+ }
338
+
339
+ /**
340
+ * Get the number of registered plugins
341
+ */
342
+ get size(): number {
343
+ return this.plugins.size;
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Get the global plugin registry instance
349
+ */
350
+ export const getPluginRegistry = (): PluginRegistry => PluginRegistry.getInstance();
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Plugin System Types
3
+ *
4
+ * Provides a formal registry for terminal coder plugins with lifecycle hooks
5
+ * and configuration validation.
6
+ */
7
+
8
+ import type { Provider } from "../providers/index";
9
+
10
+ /**
11
+ * Plugin lifecycle interface
12
+ * Providers can optionally implement these for managed lifecycle
13
+ */
14
+ export interface IPluginLifecycle {
15
+ /**
16
+ * Initialize the plugin (async setup, warmup, etc.)
17
+ * Called once when the plugin is first used
18
+ */
19
+ init?(): Promise<void>;
20
+
21
+ /**
22
+ * Perform health check
23
+ * @returns true if healthy
24
+ */
25
+ healthcheck?(): Promise<boolean>;
26
+
27
+ /**
28
+ * Clean shutdown
29
+ * Called when the system is shutting down
30
+ */
31
+ shutdown?(): Promise<void>;
32
+ }
33
+
34
+ /**
35
+ * Factory function type for creating provider instances
36
+ */
37
+ export type ProviderFactory<TConfig = unknown> = (
38
+ config: TConfig,
39
+ context?: PluginContext,
40
+ ) => Provider;
41
+
42
+ /**
43
+ * Context passed to plugin factory and lifecycle methods
44
+ */
45
+ export interface PluginContext {
46
+ /** Plugin registry for accessing other plugins */
47
+ registry: IPluginRegistry;
48
+ /** Configuration path (if loaded from file) */
49
+ configPath?: string;
50
+ /** Working directory */
51
+ cwd?: string;
52
+ }
53
+
54
+ /**
55
+ * Plugin definition interface
56
+ *
57
+ * This is the main interface for defining a terminal coder plugin.
58
+ * For simple tools, use the adapter system instead.
59
+ */
60
+ export interface PluginDefinition<TConfig = unknown> {
61
+ /**
62
+ * Unique type identifier for this plugin
63
+ * Used as discriminator in config and registry lookup
64
+ */
65
+ readonly type: string;
66
+
67
+ /**
68
+ * Human-readable name for display
69
+ */
70
+ readonly displayName: string;
71
+
72
+ /**
73
+ * Brief description of the plugin
74
+ */
75
+ readonly description?: string;
76
+
77
+ /**
78
+ * URL to documentation
79
+ */
80
+ readonly docsUrl?: string;
81
+
82
+ /**
83
+ * Version of the plugin (semver)
84
+ */
85
+ readonly version?: string;
86
+
87
+ /**
88
+ * Factory function to create provider instances
89
+ */
90
+ readonly factory: ProviderFactory<TConfig>;
91
+
92
+ /**
93
+ * Whether this provider implements IPluginLifecycle
94
+ * If true, init/shutdown will be called during lifecycle management
95
+ */
96
+ readonly hasLifecycle: boolean;
97
+
98
+ /**
99
+ * Capabilities this plugin supports
100
+ * e.g., ["generate", "edit", "explain", "test", "review"]
101
+ */
102
+ readonly capabilities?: string[];
103
+
104
+ /**
105
+ * Binary required by this plugin
106
+ * Used for availability checking
107
+ */
108
+ readonly binary?: string;
109
+
110
+ /**
111
+ * Hook called when plugin is registered
112
+ * Use for one-time setup or validation
113
+ */
114
+ onRegister?(context: PluginContext): void | Promise<void>;
115
+
116
+ /**
117
+ * Hook called when plugin is unregistered
118
+ * Use for cleanup
119
+ */
120
+ onUnregister?(context: PluginContext): void | Promise<void>;
121
+ }
122
+
123
+ /**
124
+ * Registration options
125
+ */
126
+ export interface PluginRegistrationOptions {
127
+ /** Whether to overwrite if plugin type already exists */
128
+ overwrite?: boolean;
129
+ /** Mark as built-in plugin */
130
+ builtIn?: boolean;
131
+ }
132
+
133
+ /**
134
+ * Registration result
135
+ */
136
+ export interface PluginRegistrationResult {
137
+ success: boolean;
138
+ type: string;
139
+ message?: string;
140
+ overwritten?: boolean;
141
+ }
142
+
143
+ /**
144
+ * Plugin metadata for queries
145
+ */
146
+ export interface PluginInfo {
147
+ type: string;
148
+ displayName: string;
149
+ description?: string;
150
+ docsUrl?: string;
151
+ version?: string;
152
+ capabilities?: string[];
153
+ binary?: string;
154
+ hasLifecycle: boolean;
155
+ isBuiltIn: boolean;
156
+ isAvailable?: boolean;
157
+ }
158
+
159
+ /**
160
+ * Options for creating a provider
161
+ */
162
+ export interface CreateProviderOptions {
163
+ /** Plugin context */
164
+ context?: PluginContext;
165
+ /** Skip config validation */
166
+ skipValidation?: boolean;
167
+ }
168
+
169
+ /**
170
+ * Extended provider type with optional lifecycle
171
+ */
172
+ export type ManagedProvider = Provider & Partial<IPluginLifecycle>;
173
+
174
+ /**
175
+ * Type guard for lifecycle providers
176
+ */
177
+ export function isLifecycleProvider(provider: Provider): provider is Provider & IPluginLifecycle {
178
+ return (
179
+ provider != null &&
180
+ typeof provider === "object" &&
181
+ "init" in provider &&
182
+ typeof (provider as IPluginLifecycle).init === "function"
183
+ );
184
+ }
185
+
186
+ /**
187
+ * Helper type to extract config type from plugin
188
+ */
189
+ export type PluginConfigType<T> = T extends PluginDefinition<infer C> ? C : never;
190
+
191
+ /**
192
+ * Plugin registry interface (for dependency injection)
193
+ */
194
+ export interface IPluginRegistry {
195
+ register(
196
+ plugin: PluginDefinition,
197
+ options?: PluginRegistrationOptions,
198
+ ): Promise<PluginRegistrationResult>;
199
+ registerSync(
200
+ plugin: PluginDefinition,
201
+ options?: PluginRegistrationOptions,
202
+ ): PluginRegistrationResult;
203
+ unregister(type: string): Promise<boolean>;
204
+ has(type: string): boolean;
205
+ get(type: string): PluginDefinition | undefined;
206
+ getTypes(): string[];
207
+ listPlugins(): PluginInfo[];
208
+ createProvider(type: string, config: unknown, options?: CreateProviderOptions): ManagedProvider;
209
+ }