opencommand-plugin 0.0.1

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/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # OpenCommand Plugin - OpenCode Integration
2
+
3
+ Plugin for OpenCode that manages the CommandCode proxy lifecycle.
4
+
5
+ ## Features
6
+
7
+ - ✅ Automatic proxy startup on editor launch
8
+ - ✅ Dynamic port allocation (no conflicts)
9
+ - ✅ Secure session token storage via SecretStorage
10
+ - ✅ Health checks (proxy readiness verification)
11
+ - ✅ Graceful shutdown on editor close
12
+ - ✅ Configuration export to OpenCode
13
+
14
+ ## Architecture
15
+
16
+ ### ProxyManager
17
+ Handles proxy process lifecycle:
18
+ - Start/stop proxy on dynamic port
19
+ - Health check polling
20
+ - Process signal handling
21
+
22
+ ### SecretStorage
23
+ Secure credential management:
24
+ - Store CC_SESSION_COOKIE securely
25
+ - Retrieve only when needed
26
+ - Mock implementation (real version uses OpenCode API)
27
+
28
+ ### OpenCommandPlugin
29
+ Main plugin interface:
30
+ - activate() - Initialize on editor startup
31
+ - deactivate() - Clean up on shutdown
32
+ - setSessionToken() - Configure CommandCode credentials
33
+ - getProxyConfig() - Get current config
34
+
35
+ ## Building
36
+
37
+ ```bash
38
+ npm install
39
+ npm run build
40
+ ```
41
+
42
+ ## Testing
43
+
44
+ ```bash
45
+ npm test
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ 1. User installs plugin in OpenCode
51
+ 2. Plugin prompts for CC_SESSION_COOKIE
52
+ 3. On startup:
53
+ - Token retrieved from SecretStorage
54
+ - Proxy started on random available port
55
+ - Configuration saved to OpenCode
56
+ - Clients use http://localhost:PORT/v1 for API calls
57
+ 4. On shutdown:
58
+ - Proxy process terminated gracefully
59
+ - SIGTERM → wait 5s → SIGKILL
60
+
61
+ ## Configuration
62
+
63
+ The plugin exports this configuration to OpenCode:
64
+
65
+ ```json
66
+ {
67
+ "opencommand": {
68
+ "proxyUrl": "http://localhost:3001",
69
+ "apiBaseUrl": "http://localhost:3001/v1"
70
+ }
71
+ }
72
+ ```
73
+
74
+ OpenCode and other clients use this to route requests to the proxy.
75
+
76
+ ## Related Issues
77
+
78
+ - Fase 2 Implementation (#2)
79
+ - Depends on: Proxy Core (Fase 1)
80
+ - Prepares for: Cookie Scraper (Fase 3)
@@ -0,0 +1,86 @@
1
+ export interface ProxyConfig {
2
+ port: number;
3
+ sessionToken: string;
4
+ }
5
+ export declare class ProxyManager {
6
+ private proxyProcess;
7
+ private config;
8
+ private proxyBinaryPath;
9
+ constructor(binaryPath: string);
10
+ /**
11
+ * Find an available port (dynamic allocation)
12
+ */
13
+ findAvailablePort(): Promise<number>;
14
+ /**
15
+ * Start the proxy server
16
+ */
17
+ start(sessionToken: string): Promise<ProxyConfig>;
18
+ /**
19
+ * Stop the proxy server
20
+ */
21
+ stop(): Promise<void>;
22
+ /**
23
+ * Wait for proxy to be healthy
24
+ */
25
+ private waitForHealthz;
26
+ /**
27
+ * Get current proxy configuration
28
+ */
29
+ getConfig(): ProxyConfig | null;
30
+ /**
31
+ * Get proxy base URL
32
+ */
33
+ getBaseUrl(): string | null;
34
+ /**
35
+ * Check if proxy is running
36
+ */
37
+ isRunning(): boolean;
38
+ }
39
+ /**
40
+ * File-based persistent secret storage.
41
+ * Fix: Persists session token across plugin restarts (not instance-local Map).
42
+ */
43
+ export declare class SecretStorage {
44
+ private filePath;
45
+ constructor(storageDir?: string);
46
+ private readStore;
47
+ private writeStore;
48
+ set(key: string, value: string): Promise<void>;
49
+ get(key: string): Promise<string | undefined>;
50
+ delete(key: string): Promise<void>;
51
+ }
52
+ /**
53
+ * OpenCommand Plugin Main Class
54
+ */
55
+ export declare class OpenCommandPlugin {
56
+ private proxyManager;
57
+ private secretStorage;
58
+ private SESSION_TOKEN_KEY;
59
+ constructor(proxyBinaryPath: string, storageDir?: string);
60
+ /**
61
+ * Initialize plugin on editor startup
62
+ */
63
+ activate(): Promise<void>;
64
+ /**
65
+ * Clean up on editor shutdown
66
+ */
67
+ deactivate(): Promise<void>;
68
+ /**
69
+ * Save proxy config to OpenCode configuration.
70
+ * Fix: Persist to file so OpenCodeBar can discover the dynamic port.
71
+ */
72
+ private saveOpenCodeConfig;
73
+ /**
74
+ * Persist proxy URL and port to a config file (Issue #5).
75
+ * OpenCodeBar reads this to discover the dynamically allocated port.
76
+ */
77
+ private persistProxyConfig;
78
+ /**
79
+ * Set session token via command
80
+ */
81
+ setSessionToken(token: string): Promise<void>;
82
+ /**
83
+ * Get proxy configuration
84
+ */
85
+ getProxyConfig(): string;
86
+ }
package/dist/index.js ADDED
@@ -0,0 +1,326 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.OpenCommandPlugin = exports.SecretStorage = exports.ProxyManager = void 0;
37
+ const cp = __importStar(require("child_process"));
38
+ const net = __importStar(require("net"));
39
+ class ProxyManager {
40
+ constructor(binaryPath) {
41
+ this.proxyProcess = null;
42
+ this.config = null;
43
+ this.proxyBinaryPath = binaryPath;
44
+ }
45
+ /**
46
+ * Find an available port (dynamic allocation)
47
+ */
48
+ async findAvailablePort() {
49
+ return new Promise((resolve, reject) => {
50
+ const server = net.createServer();
51
+ server.listen(0, () => {
52
+ const address = server.address();
53
+ if (address && typeof address === "object" && address.port) {
54
+ server.close(() => resolve(address.port));
55
+ }
56
+ else {
57
+ reject(new Error("Failed to find available port"));
58
+ }
59
+ });
60
+ server.on("error", reject);
61
+ });
62
+ }
63
+ /**
64
+ * Start the proxy server
65
+ */
66
+ async start(sessionToken) {
67
+ // Find available port
68
+ const port = await this.findAvailablePort();
69
+ this.config = { port, sessionToken };
70
+ // Prepare environment
71
+ const env = {
72
+ ...process.env,
73
+ PORT: String(port),
74
+ CC_SESSION_COOKIE: sessionToken,
75
+ PRODUCTION: "true",
76
+ };
77
+ // Start proxy process
78
+ this.proxyProcess = cp.spawn(this.proxyBinaryPath, [], {
79
+ env,
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ });
82
+ // Log output
83
+ this.proxyProcess.stdout?.on("data", (data) => {
84
+ console.log(`[Proxy stdout] ${data}`);
85
+ });
86
+ this.proxyProcess.stderr?.on("data", (data) => {
87
+ console.error(`[Proxy stderr] ${data}`);
88
+ });
89
+ // Wait for proxy to be ready
90
+ await this.waitForHealthz(port, 30000);
91
+ console.log(`✓ Proxy started on port ${port}`);
92
+ return this.config;
93
+ }
94
+ /**
95
+ * Stop the proxy server
96
+ */
97
+ async stop() {
98
+ const proc = this.proxyProcess;
99
+ if (!proc) {
100
+ return;
101
+ }
102
+ return new Promise((resolve) => {
103
+ let resolved = false;
104
+ const finish = () => {
105
+ if (!resolved) {
106
+ resolved = true;
107
+ this.proxyProcess = null;
108
+ resolve();
109
+ }
110
+ };
111
+ proc.once("exit", finish);
112
+ proc.once("error", (error) => {
113
+ console.error("Proxy process error while stopping:", error);
114
+ finish();
115
+ });
116
+ try {
117
+ if (!proc.killed) {
118
+ proc.kill("SIGTERM");
119
+ }
120
+ }
121
+ catch (error) {
122
+ console.error("Failed to send SIGTERM to proxy process:", error);
123
+ finish();
124
+ return;
125
+ }
126
+ setTimeout(() => {
127
+ if (!resolved && !proc.killed) {
128
+ try {
129
+ proc.kill("SIGKILL");
130
+ }
131
+ catch (error) {
132
+ console.error("Failed to send SIGKILL to proxy process:", error);
133
+ }
134
+ }
135
+ finish();
136
+ }, 5000);
137
+ });
138
+ }
139
+ /**
140
+ * Wait for proxy to be healthy
141
+ */
142
+ async waitForHealthz(port, timeoutMs) {
143
+ const startTime = Date.now();
144
+ while (Date.now() - startTime < timeoutMs) {
145
+ try {
146
+ const controller = new AbortController();
147
+ const timeoutId = setTimeout(() => controller.abort(), 2000);
148
+ const response = await fetch(`http://localhost:${port}/healthz`, {
149
+ signal: controller.signal,
150
+ });
151
+ clearTimeout(timeoutId);
152
+ if (response.ok) {
153
+ return;
154
+ }
155
+ }
156
+ catch (err) {
157
+ console.debug(`Health check attempt failed for port ${port}:`, err);
158
+ }
159
+ await new Promise((r) => setTimeout(r, 500));
160
+ }
161
+ throw new Error(`Proxy failed to become healthy after ${timeoutMs}ms on port ${port}`);
162
+ }
163
+ /**
164
+ * Get current proxy configuration
165
+ */
166
+ getConfig() {
167
+ return this.config;
168
+ }
169
+ /**
170
+ * Get proxy base URL
171
+ */
172
+ getBaseUrl() {
173
+ return this.config ? `http://localhost:${this.config.port}` : null;
174
+ }
175
+ /**
176
+ * Check if proxy is running
177
+ */
178
+ isRunning() {
179
+ return this.proxyProcess !== null && !this.proxyProcess.killed;
180
+ }
181
+ }
182
+ exports.ProxyManager = ProxyManager;
183
+ /**
184
+ * File-based persistent secret storage.
185
+ * Fix: Persists session token across plugin restarts (not instance-local Map).
186
+ */
187
+ class SecretStorage {
188
+ constructor(storageDir = `${process.env.HOME || "/tmp"}/.opencommand`) {
189
+ this.filePath = `${storageDir}/opencommand-secrets.json`;
190
+ }
191
+ async readStore() {
192
+ const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
193
+ try {
194
+ const data = await fs.readFile(this.filePath, "utf-8");
195
+ return JSON.parse(data);
196
+ }
197
+ catch (error) {
198
+ if (error.code !== "ENOENT") {
199
+ console.debug("Could not read OpenCommand secret store:", error);
200
+ }
201
+ return {};
202
+ }
203
+ }
204
+ async writeStore(store) {
205
+ const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
206
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
207
+ const dir = path.dirname(this.filePath);
208
+ await fs.mkdir(dir, { recursive: true });
209
+ await fs.writeFile(this.filePath, JSON.stringify(store, null, 2), {
210
+ mode: 0o600,
211
+ });
212
+ }
213
+ async set(key, value) {
214
+ const store = await this.readStore();
215
+ store[key] = value;
216
+ await this.writeStore(store);
217
+ }
218
+ async get(key) {
219
+ const store = await this.readStore();
220
+ return store[key];
221
+ }
222
+ async delete(key) {
223
+ const store = await this.readStore();
224
+ delete store[key];
225
+ await this.writeStore(store);
226
+ }
227
+ }
228
+ exports.SecretStorage = SecretStorage;
229
+ /**
230
+ * OpenCommand Plugin Main Class
231
+ */
232
+ class OpenCommandPlugin {
233
+ constructor(proxyBinaryPath, storageDir) {
234
+ this.SESSION_TOKEN_KEY = "opencommand.cc_session_token";
235
+ this.proxyManager = new ProxyManager(proxyBinaryPath);
236
+ const dir = storageDir || `${process.env.HOME || "/tmp"}/.opencommand`;
237
+ this.secretStorage = new SecretStorage(dir);
238
+ }
239
+ /**
240
+ * Initialize plugin on editor startup
241
+ */
242
+ async activate() {
243
+ console.log("OpenCommand Plugin activating...");
244
+ // Try to retrieve stored session token
245
+ const storedToken = await this.secretStorage.get(this.SESSION_TOKEN_KEY);
246
+ if (!storedToken) {
247
+ console.warn("No CC_SESSION_COOKIE found. Please configure token.");
248
+ return;
249
+ }
250
+ // Start proxy
251
+ try {
252
+ const config = await this.proxyManager.start(storedToken);
253
+ console.log(`✓ OpenCommand proxy ready at ${config.port}`);
254
+ // Store config for use by OpenCode
255
+ this.saveOpenCodeConfig(config);
256
+ }
257
+ catch (error) {
258
+ console.error("Failed to start proxy:", error);
259
+ }
260
+ }
261
+ /**
262
+ * Clean up on editor shutdown
263
+ */
264
+ async deactivate() {
265
+ console.log("OpenCommand Plugin deactivating...");
266
+ await this.proxyManager.stop();
267
+ console.log("✓ Proxy stopped");
268
+ }
269
+ /**
270
+ * Save proxy config to OpenCode configuration.
271
+ * Fix: Persist to file so OpenCodeBar can discover the dynamic port.
272
+ */
273
+ saveOpenCodeConfig(config) {
274
+ const proxyUrl = `http://localhost:${config.port}`;
275
+ const openCodeConfig = {
276
+ opencommand: {
277
+ proxyUrl,
278
+ apiBaseUrl: `${proxyUrl}/v1`,
279
+ },
280
+ };
281
+ // Log for debugging
282
+ console.log("Proxy configuration saved:", openCodeConfig);
283
+ // Persist to config file (readable by OpenCodeBar)
284
+ this.persistProxyConfig(proxyUrl, config.port);
285
+ }
286
+ /**
287
+ * Persist proxy URL and port to a config file (Issue #5).
288
+ * OpenCodeBar reads this to discover the dynamically allocated port.
289
+ */
290
+ persistProxyConfig(url, port) {
291
+ const fs = require("fs");
292
+ const path = require("path");
293
+ const configDir = `${process.env.HOME || "/tmp"}/.opencommand`;
294
+ const configPath = path.join(configDir, "proxy-config.json");
295
+ try {
296
+ fs.mkdirSync(configDir, { recursive: true });
297
+ fs.writeFileSync(configPath, JSON.stringify({ url, port, updatedAt: new Date().toISOString() }, null, 2), { mode: 0o644 });
298
+ console.log(`✓ Proxy config persisted to ${configPath}`);
299
+ }
300
+ catch (err) {
301
+ console.error("Failed to persist proxy config:", err);
302
+ }
303
+ }
304
+ /**
305
+ * Set session token via command
306
+ */
307
+ async setSessionToken(token) {
308
+ await this.secretStorage.set(this.SESSION_TOKEN_KEY, token);
309
+ console.log("✓ Session token saved securely");
310
+ // Restart proxy with new token
311
+ await this.proxyManager.stop();
312
+ const config = await this.proxyManager.start(token);
313
+ this.saveOpenCodeConfig(config);
314
+ }
315
+ /**
316
+ * Get proxy configuration
317
+ */
318
+ getProxyConfig() {
319
+ const config = this.proxyManager.getConfig();
320
+ if (!config) {
321
+ return "Proxy not running";
322
+ }
323
+ return JSON.stringify(config, null, 2);
324
+ }
325
+ }
326
+ exports.OpenCommandPlugin = OpenCommandPlugin;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "opencommand-plugin",
3
+ "version": "0.0.1",
4
+ "description": "OpenCommand - CommandCode API Plugin for OpenCode",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "test": "jest",
9
+ "dev": "tsc --watch",
10
+ "clean": "rm -rf dist",
11
+ "prepack": "npm run build"
12
+ },
13
+ "keywords": [
14
+ "opencode",
15
+ "commandcode",
16
+ "api",
17
+ "proxy"
18
+ ],
19
+ "author": "OpenCommand",
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "@types/node": "^20.0.0",
23
+ "@types/jest": "^29.0.0",
24
+ "typescript": "^5.0.0",
25
+ "jest": "^29.0.0",
26
+ "ts-jest": "^29.0.0"
27
+ },
28
+ "dependencies": {
29
+ "node-fetch": "^3.0.0"
30
+ },
31
+ "files": [
32
+ "dist/**",
33
+ "README.md",
34
+ "package.json"
35
+ ]
36
+ }