opencommand-plugin 0.0.1 → 0.0.3
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/dist/index.d.ts +75 -53
- package/dist/index.js +458 -103
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,45 +1,56 @@
|
|
|
1
1
|
export interface ProxyConfig {
|
|
2
2
|
port: number;
|
|
3
|
-
|
|
3
|
+
commandCodeToken: string;
|
|
4
|
+
sessionCookie?: string;
|
|
4
5
|
}
|
|
6
|
+
export interface ProxyStartConfig {
|
|
7
|
+
commandCodeToken: string;
|
|
8
|
+
sessionCookie?: string;
|
|
9
|
+
}
|
|
10
|
+
interface CommandCodeModelDefinition {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
reasoning?: boolean;
|
|
15
|
+
context: number;
|
|
16
|
+
output: number;
|
|
17
|
+
cost: {
|
|
18
|
+
input: number;
|
|
19
|
+
output: number;
|
|
20
|
+
cache_read?: number;
|
|
21
|
+
cache_write?: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
interface OpenCodeProviderConfig {
|
|
25
|
+
npm: string;
|
|
26
|
+
name: string;
|
|
27
|
+
options: {
|
|
28
|
+
apiKey: string;
|
|
29
|
+
baseURL: string;
|
|
30
|
+
};
|
|
31
|
+
models: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
interface OpenCodeConfig {
|
|
34
|
+
provider?: Record<string, OpenCodeProviderConfig>;
|
|
35
|
+
}
|
|
36
|
+
interface CookiePair {
|
|
37
|
+
name: string;
|
|
38
|
+
value: string;
|
|
39
|
+
}
|
|
40
|
+
export declare const COMMAND_CODE_GO_PLAN_MODELS: CommandCodeModelDefinition[];
|
|
5
41
|
export declare class ProxyManager {
|
|
6
42
|
private proxyProcess;
|
|
7
43
|
private config;
|
|
8
44
|
private proxyBinaryPath;
|
|
9
|
-
constructor(binaryPath
|
|
10
|
-
/**
|
|
11
|
-
* Find an available port (dynamic allocation)
|
|
12
|
-
*/
|
|
45
|
+
constructor(binaryPath?: string);
|
|
13
46
|
findAvailablePort(): Promise<number>;
|
|
14
|
-
|
|
15
|
-
* Start the proxy server
|
|
16
|
-
*/
|
|
17
|
-
start(sessionToken: string): Promise<ProxyConfig>;
|
|
18
|
-
/**
|
|
19
|
-
* Stop the proxy server
|
|
20
|
-
*/
|
|
47
|
+
start(startConfig: ProxyStartConfig): Promise<ProxyConfig>;
|
|
21
48
|
stop(): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* Wait for proxy to be healthy
|
|
24
|
-
*/
|
|
25
49
|
private waitForHealthz;
|
|
26
|
-
/**
|
|
27
|
-
* Get current proxy configuration
|
|
28
|
-
*/
|
|
29
50
|
getConfig(): ProxyConfig | null;
|
|
30
|
-
/**
|
|
31
|
-
* Get proxy base URL
|
|
32
|
-
*/
|
|
33
51
|
getBaseUrl(): string | null;
|
|
34
|
-
/**
|
|
35
|
-
* Check if proxy is running
|
|
36
|
-
*/
|
|
37
52
|
isRunning(): boolean;
|
|
38
53
|
}
|
|
39
|
-
/**
|
|
40
|
-
* File-based persistent secret storage.
|
|
41
|
-
* Fix: Persists session token across plugin restarts (not instance-local Map).
|
|
42
|
-
*/
|
|
43
54
|
export declare class SecretStorage {
|
|
44
55
|
private filePath;
|
|
45
56
|
constructor(storageDir?: string);
|
|
@@ -49,38 +60,49 @@ export declare class SecretStorage {
|
|
|
49
60
|
get(key: string): Promise<string | undefined>;
|
|
50
61
|
delete(key: string): Promise<void>;
|
|
51
62
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
export declare class BrowserCookieExtractor {
|
|
64
|
+
private readonly homeDir;
|
|
65
|
+
constructor(homeDir?: string);
|
|
66
|
+
extractCommandCodeCookie(): Promise<string | undefined>;
|
|
67
|
+
static buildCookieHeader(pairs: CookiePair[]): string | undefined;
|
|
68
|
+
private findBrowserProfiles;
|
|
69
|
+
private profileDirectories;
|
|
70
|
+
private readCommandCodeCookies;
|
|
71
|
+
private cookiePairFromRow;
|
|
72
|
+
private decryptChromiumCookie;
|
|
73
|
+
private safeStoragePassword;
|
|
74
|
+
}
|
|
55
75
|
export declare class OpenCommandPlugin {
|
|
56
76
|
private proxyManager;
|
|
57
77
|
private secretStorage;
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
private cookieExtractor;
|
|
79
|
+
private readonly commandCodeTokenKey;
|
|
80
|
+
private readonly legacyTokenKey;
|
|
81
|
+
private readonly sessionCookieKey;
|
|
82
|
+
constructor(proxyBinaryPath?: string, storageDir?: string);
|
|
63
83
|
activate(): Promise<void>;
|
|
64
|
-
|
|
65
|
-
* Clean up on editor shutdown
|
|
66
|
-
*/
|
|
84
|
+
ensureStarted(): Promise<ProxyConfig | undefined>;
|
|
67
85
|
deactivate(): Promise<void>;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* Fix: Persist to file so OpenCodeBar can discover the dynamic port.
|
|
71
|
-
*/
|
|
86
|
+
private loadCommandCodeToken;
|
|
87
|
+
private loadSessionCookie;
|
|
72
88
|
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
89
|
private persistProxyConfig;
|
|
78
|
-
|
|
79
|
-
* Set session token via command
|
|
80
|
-
*/
|
|
90
|
+
setCommandCodeToken(token: string): Promise<void>;
|
|
81
91
|
setSessionToken(token: string): Promise<void>;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*/
|
|
92
|
+
setSessionCookie(cookie: string): Promise<void>;
|
|
93
|
+
private restartIfPossible;
|
|
85
94
|
getProxyConfig(): string;
|
|
86
95
|
}
|
|
96
|
+
export declare function registerOpenCommandProvider(config: OpenCodeConfig, baseURL?: string): void;
|
|
97
|
+
export declare const OpenCommandOpenCodePlugin: () => Promise<{
|
|
98
|
+
auth: {
|
|
99
|
+
provider: string;
|
|
100
|
+
loader: () => Promise<{
|
|
101
|
+
apiKey: string;
|
|
102
|
+
baseURL: string;
|
|
103
|
+
} | null>;
|
|
104
|
+
methods: never[];
|
|
105
|
+
};
|
|
106
|
+
config: (config: OpenCodeConfig) => Promise<void>;
|
|
107
|
+
}>;
|
|
108
|
+
export default OpenCommandOpenCodePlugin;
|
package/dist/index.js
CHANGED
|
@@ -33,18 +33,120 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.OpenCommandPlugin = exports.SecretStorage = exports.ProxyManager = void 0;
|
|
36
|
+
exports.OpenCommandOpenCodePlugin = exports.OpenCommandPlugin = exports.BrowserCookieExtractor = exports.SecretStorage = exports.ProxyManager = exports.COMMAND_CODE_GO_PLAN_MODELS = void 0;
|
|
37
|
+
exports.registerOpenCommandProvider = registerOpenCommandProvider;
|
|
37
38
|
const cp = __importStar(require("child_process"));
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const fsp = __importStar(require("fs/promises"));
|
|
38
42
|
const net = __importStar(require("net"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const PROVIDER_ID = "opencommand";
|
|
46
|
+
const PROVIDER_NAME = "CommandCode";
|
|
47
|
+
const PROVIDER_API_KEY_PLACEHOLDER = "opencommand";
|
|
48
|
+
const DEFAULT_PROXY_BASE_URL = "http://localhost:3000/v1";
|
|
49
|
+
exports.COMMAND_CODE_GO_PLAN_MODELS = [
|
|
50
|
+
{
|
|
51
|
+
id: "deepseek/deepseek-v4-pro",
|
|
52
|
+
name: "DeepSeek V4 Pro",
|
|
53
|
+
description: "Hybrid-attention long-context reasoning",
|
|
54
|
+
reasoning: true,
|
|
55
|
+
context: 1000000,
|
|
56
|
+
output: 8192,
|
|
57
|
+
cost: { input: 1.74, output: 3.48, cache_read: 0.0145 },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "deepseek/deepseek-v4-flash",
|
|
61
|
+
name: "DeepSeek V4 Flash",
|
|
62
|
+
description: "Fast hybrid-attention reasoning",
|
|
63
|
+
reasoning: true,
|
|
64
|
+
context: 1000000,
|
|
65
|
+
output: 8192,
|
|
66
|
+
cost: { input: 0.14, output: 0.28, cache_read: 0.01 },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "moonshotai/Kimi-K2.6",
|
|
70
|
+
name: "Kimi K2.6",
|
|
71
|
+
description: "Long-horizon coding with vision",
|
|
72
|
+
context: 256000,
|
|
73
|
+
output: 8192,
|
|
74
|
+
cost: { input: 0.95, output: 4.0, cache_read: 0.16 },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "moonshotai/Kimi-K2.5",
|
|
78
|
+
name: "Kimi K2.5",
|
|
79
|
+
description: "Multimodal frontend coding",
|
|
80
|
+
context: 256000,
|
|
81
|
+
output: 8192,
|
|
82
|
+
cost: { input: 0.6, output: 3.0, cache_read: 0.1 },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "zai-org/GLM-5.1",
|
|
86
|
+
name: "GLM-5.1",
|
|
87
|
+
description: "Long-horizon autonomous coding agent",
|
|
88
|
+
context: 200000,
|
|
89
|
+
output: 8192,
|
|
90
|
+
cost: { input: 1.4, output: 4.4, cache_read: 0.26 },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "zai-org/GLM-5",
|
|
94
|
+
name: "GLM-5",
|
|
95
|
+
description: "Multi-mode thinking and long-range planning",
|
|
96
|
+
context: 200000,
|
|
97
|
+
output: 8192,
|
|
98
|
+
cost: { input: 1.0, output: 3.2, cache_read: 0.2 },
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "MiniMaxAI/MiniMax-M2.7",
|
|
102
|
+
name: "MiniMax M2.7",
|
|
103
|
+
description: "End-to-end software engineering agent",
|
|
104
|
+
context: 200000,
|
|
105
|
+
output: 8192,
|
|
106
|
+
cost: { input: 0.3, output: 1.2, cache_read: 0.06 },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "MiniMaxAI/MiniMax-M2.5",
|
|
110
|
+
name: "MiniMax M2.5",
|
|
111
|
+
description: "Cross-platform full-stack agentic development",
|
|
112
|
+
context: 200000,
|
|
113
|
+
output: 8192,
|
|
114
|
+
cost: { input: 0.27, output: 0.95, cache_read: 0.03 },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "Qwen/Qwen3.6-Max-Preview",
|
|
118
|
+
name: "Qwen 3.6 Max Preview",
|
|
119
|
+
description: "Vibe coding and efficient agent execution",
|
|
120
|
+
reasoning: true,
|
|
121
|
+
context: 200000,
|
|
122
|
+
output: 8192,
|
|
123
|
+
cost: { input: 1.3, output: 7.8, cache_read: 0.26, cache_write: 1.63 },
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "Qwen/Qwen3.6-Plus",
|
|
127
|
+
name: "Qwen 3.6 Plus",
|
|
128
|
+
description: "Agentic coding and reasoning",
|
|
129
|
+
reasoning: true,
|
|
130
|
+
context: 200000,
|
|
131
|
+
output: 8192,
|
|
132
|
+
cost: { input: 0.5, output: 3.0, cache_read: 0.1 },
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "stepfun/Step-3.5-Flash",
|
|
136
|
+
name: "Step 3.5 Flash",
|
|
137
|
+
description: "Fast sparse-MoE agentic reasoning",
|
|
138
|
+
reasoning: true,
|
|
139
|
+
context: 1000000,
|
|
140
|
+
output: 8192,
|
|
141
|
+
cost: { input: 0.1, output: 0.3, cache_read: 0.02 },
|
|
142
|
+
},
|
|
143
|
+
];
|
|
39
144
|
class ProxyManager {
|
|
40
|
-
constructor(binaryPath) {
|
|
145
|
+
constructor(binaryPath = resolveProxyBinaryPath()) {
|
|
41
146
|
this.proxyProcess = null;
|
|
42
147
|
this.config = null;
|
|
43
148
|
this.proxyBinaryPath = binaryPath;
|
|
44
149
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Find an available port (dynamic allocation)
|
|
47
|
-
*/
|
|
48
150
|
async findAvailablePort() {
|
|
49
151
|
return new Promise((resolve, reject) => {
|
|
50
152
|
const server = net.createServer();
|
|
@@ -52,53 +154,48 @@ class ProxyManager {
|
|
|
52
154
|
const address = server.address();
|
|
53
155
|
if (address && typeof address === "object" && address.port) {
|
|
54
156
|
server.close(() => resolve(address.port));
|
|
157
|
+
return;
|
|
55
158
|
}
|
|
56
|
-
|
|
57
|
-
reject(new Error("Failed to find available port"));
|
|
58
|
-
}
|
|
159
|
+
reject(new Error("Failed to find available port"));
|
|
59
160
|
});
|
|
60
161
|
server.on("error", reject);
|
|
61
162
|
});
|
|
62
163
|
}
|
|
63
|
-
|
|
64
|
-
* Start the proxy server
|
|
65
|
-
*/
|
|
66
|
-
async start(sessionToken) {
|
|
67
|
-
// Find available port
|
|
164
|
+
async start(startConfig) {
|
|
68
165
|
const port = await this.findAvailablePort();
|
|
69
|
-
this.config = {
|
|
70
|
-
|
|
166
|
+
this.config = {
|
|
167
|
+
port,
|
|
168
|
+
commandCodeToken: startConfig.commandCodeToken,
|
|
169
|
+
sessionCookie: startConfig.sessionCookie,
|
|
170
|
+
};
|
|
71
171
|
const env = {
|
|
72
172
|
...process.env,
|
|
73
173
|
PORT: String(port),
|
|
74
|
-
|
|
174
|
+
COMMAND_CODE_TOKEN: startConfig.commandCodeToken,
|
|
175
|
+
COMMANDCODE_API_KEY: startConfig.commandCodeToken,
|
|
75
176
|
PRODUCTION: "true",
|
|
76
177
|
};
|
|
77
|
-
|
|
178
|
+
if (startConfig.sessionCookie) {
|
|
179
|
+
env.CC_SESSION_COOKIE = startConfig.sessionCookie;
|
|
180
|
+
}
|
|
78
181
|
this.proxyProcess = cp.spawn(this.proxyBinaryPath, [], {
|
|
79
182
|
env,
|
|
80
183
|
stdio: ["ignore", "pipe", "pipe"],
|
|
81
184
|
});
|
|
82
|
-
// Log output
|
|
83
185
|
this.proxyProcess.stdout?.on("data", (data) => {
|
|
84
186
|
console.log(`[Proxy stdout] ${data}`);
|
|
85
187
|
});
|
|
86
188
|
this.proxyProcess.stderr?.on("data", (data) => {
|
|
87
189
|
console.error(`[Proxy stderr] ${data}`);
|
|
88
190
|
});
|
|
89
|
-
// Wait for proxy to be ready
|
|
90
191
|
await this.waitForHealthz(port, 30000);
|
|
91
192
|
console.log(`✓ Proxy started on port ${port}`);
|
|
92
193
|
return this.config;
|
|
93
194
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Stop the proxy server
|
|
96
|
-
*/
|
|
97
195
|
async stop() {
|
|
98
196
|
const proc = this.proxyProcess;
|
|
99
|
-
if (!proc)
|
|
197
|
+
if (!proc)
|
|
100
198
|
return;
|
|
101
|
-
}
|
|
102
199
|
return new Promise((resolve) => {
|
|
103
200
|
let resolved = false;
|
|
104
201
|
const finish = () => {
|
|
@@ -114,9 +211,8 @@ class ProxyManager {
|
|
|
114
211
|
finish();
|
|
115
212
|
});
|
|
116
213
|
try {
|
|
117
|
-
if (!proc.killed)
|
|
214
|
+
if (!proc.killed)
|
|
118
215
|
proc.kill("SIGTERM");
|
|
119
|
-
}
|
|
120
216
|
}
|
|
121
217
|
catch (error) {
|
|
122
218
|
console.error("Failed to send SIGTERM to proxy process:", error);
|
|
@@ -136,9 +232,6 @@ class ProxyManager {
|
|
|
136
232
|
}, 5000);
|
|
137
233
|
});
|
|
138
234
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Wait for proxy to be healthy
|
|
141
|
-
*/
|
|
142
235
|
async waitForHealthz(port, timeoutMs) {
|
|
143
236
|
const startTime = Date.now();
|
|
144
237
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -149,9 +242,8 @@ class ProxyManager {
|
|
|
149
242
|
signal: controller.signal,
|
|
150
243
|
});
|
|
151
244
|
clearTimeout(timeoutId);
|
|
152
|
-
if (response.ok)
|
|
245
|
+
if (response.ok)
|
|
153
246
|
return;
|
|
154
|
-
}
|
|
155
247
|
}
|
|
156
248
|
catch (err) {
|
|
157
249
|
console.debug(`Health check attempt failed for port ${port}:`, err);
|
|
@@ -160,38 +252,24 @@ class ProxyManager {
|
|
|
160
252
|
}
|
|
161
253
|
throw new Error(`Proxy failed to become healthy after ${timeoutMs}ms on port ${port}`);
|
|
162
254
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Get current proxy configuration
|
|
165
|
-
*/
|
|
166
255
|
getConfig() {
|
|
167
256
|
return this.config;
|
|
168
257
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Get proxy base URL
|
|
171
|
-
*/
|
|
172
258
|
getBaseUrl() {
|
|
173
259
|
return this.config ? `http://localhost:${this.config.port}` : null;
|
|
174
260
|
}
|
|
175
|
-
/**
|
|
176
|
-
* Check if proxy is running
|
|
177
|
-
*/
|
|
178
261
|
isRunning() {
|
|
179
262
|
return this.proxyProcess !== null && !this.proxyProcess.killed;
|
|
180
263
|
}
|
|
181
264
|
}
|
|
182
265
|
exports.ProxyManager = ProxyManager;
|
|
183
|
-
/**
|
|
184
|
-
* File-based persistent secret storage.
|
|
185
|
-
* Fix: Persists session token across plugin restarts (not instance-local Map).
|
|
186
|
-
*/
|
|
187
266
|
class SecretStorage {
|
|
188
267
|
constructor(storageDir = `${process.env.HOME || "/tmp"}/.opencommand`) {
|
|
189
268
|
this.filePath = `${storageDir}/opencommand-secrets.json`;
|
|
190
269
|
}
|
|
191
270
|
async readStore() {
|
|
192
|
-
const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
|
|
193
271
|
try {
|
|
194
|
-
const data = await
|
|
272
|
+
const data = await fsp.readFile(this.filePath, "utf-8");
|
|
195
273
|
return JSON.parse(data);
|
|
196
274
|
}
|
|
197
275
|
catch (error) {
|
|
@@ -202,11 +280,9 @@ class SecretStorage {
|
|
|
202
280
|
}
|
|
203
281
|
}
|
|
204
282
|
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
283
|
const dir = path.dirname(this.filePath);
|
|
208
|
-
await
|
|
209
|
-
await
|
|
284
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
285
|
+
await fsp.writeFile(this.filePath, JSON.stringify(store, null, 2), {
|
|
210
286
|
mode: 0o600,
|
|
211
287
|
});
|
|
212
288
|
}
|
|
@@ -226,70 +302,261 @@ class SecretStorage {
|
|
|
226
302
|
}
|
|
227
303
|
}
|
|
228
304
|
exports.SecretStorage = SecretStorage;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
305
|
+
class BrowserCookieExtractor {
|
|
306
|
+
constructor(homeDir = os.homedir()) {
|
|
307
|
+
this.homeDir = homeDir;
|
|
308
|
+
}
|
|
309
|
+
async extractCommandCodeCookie() {
|
|
310
|
+
const rows = this.findBrowserProfiles().flatMap((profile) => this.readCommandCodeCookies(profile));
|
|
311
|
+
const pairs = rows
|
|
312
|
+
.map((row) => this.cookiePairFromRow(row))
|
|
313
|
+
.filter((pair) => pair !== undefined);
|
|
314
|
+
return BrowserCookieExtractor.buildCookieHeader(pairs);
|
|
315
|
+
}
|
|
316
|
+
static buildCookieHeader(pairs) {
|
|
317
|
+
const unique = new Map();
|
|
318
|
+
for (const pair of pairs) {
|
|
319
|
+
if (pair.name && pair.value)
|
|
320
|
+
unique.set(pair.name, pair.value);
|
|
321
|
+
}
|
|
322
|
+
if (unique.size === 0)
|
|
323
|
+
return undefined;
|
|
324
|
+
return [...unique.entries()]
|
|
325
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
326
|
+
.join("; ");
|
|
327
|
+
}
|
|
328
|
+
findBrowserProfiles() {
|
|
329
|
+
const candidates = [
|
|
330
|
+
{
|
|
331
|
+
browser: "Chrome",
|
|
332
|
+
base: "Library/Application Support/Google/Chrome",
|
|
333
|
+
services: ["Chrome Safe Storage", "Google Chrome Safe Storage"],
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
browser: "Comet",
|
|
337
|
+
base: "Library/Application Support/Comet",
|
|
338
|
+
services: ["Comet Safe Storage", "Chrome Safe Storage"],
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
browser: "Brave",
|
|
342
|
+
base: "Library/Application Support/BraveSoftware/Brave-Browser",
|
|
343
|
+
services: ["Brave Safe Storage", "Chrome Safe Storage"],
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
browser: "Edge",
|
|
347
|
+
base: "Library/Application Support/Microsoft Edge",
|
|
348
|
+
services: ["Microsoft Edge Safe Storage", "Chrome Safe Storage"],
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
const profiles = [];
|
|
352
|
+
for (const candidate of candidates) {
|
|
353
|
+
const basePath = path.join(this.homeDir, candidate.base);
|
|
354
|
+
for (const profileDir of this.profileDirectories(basePath)) {
|
|
355
|
+
for (const cookieDatabase of [
|
|
356
|
+
path.join(profileDir, "Network", "Cookies"),
|
|
357
|
+
path.join(profileDir, "Cookies"),
|
|
358
|
+
]) {
|
|
359
|
+
if (fs.existsSync(cookieDatabase)) {
|
|
360
|
+
profiles.push({
|
|
361
|
+
browser: candidate.browser,
|
|
362
|
+
safeStorageServices: candidate.services,
|
|
363
|
+
cookieDatabase,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const firefoxBase = path.join(this.homeDir, "Library/Application Support/Firefox/Profiles");
|
|
370
|
+
for (const profileDir of this.profileDirectories(firefoxBase)) {
|
|
371
|
+
const cookieDatabase = path.join(profileDir, "cookies.sqlite");
|
|
372
|
+
if (fs.existsSync(cookieDatabase)) {
|
|
373
|
+
profiles.push({
|
|
374
|
+
browser: "Firefox",
|
|
375
|
+
safeStorageServices: [],
|
|
376
|
+
cookieDatabase,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return profiles;
|
|
381
|
+
}
|
|
382
|
+
profileDirectories(basePath) {
|
|
383
|
+
if (!fs.existsSync(basePath))
|
|
384
|
+
return [];
|
|
385
|
+
const directCookie = path.join(basePath, "cookies.sqlite");
|
|
386
|
+
if (fs.existsSync(directCookie))
|
|
387
|
+
return [basePath];
|
|
388
|
+
return fs
|
|
389
|
+
.readdirSync(basePath, { withFileTypes: true })
|
|
390
|
+
.filter((entry) => entry.isDirectory())
|
|
391
|
+
.map((entry) => path.join(basePath, entry.name));
|
|
392
|
+
}
|
|
393
|
+
readCommandCodeCookies(profile) {
|
|
394
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencommand-cookies-"));
|
|
395
|
+
const tmpDb = path.join(tmpDir, "cookies.sqlite");
|
|
396
|
+
try {
|
|
397
|
+
fs.copyFileSync(profile.cookieDatabase, tmpDb);
|
|
398
|
+
const query = profile.browser === "Firefox" ? firefoxCookieQuery : chromiumCookieQuery;
|
|
399
|
+
const output = cp.execFileSync("/usr/bin/sqlite3", [tmpDb, query], {
|
|
400
|
+
encoding: "utf8",
|
|
401
|
+
timeout: 5000,
|
|
402
|
+
});
|
|
403
|
+
return output
|
|
404
|
+
.split("\n")
|
|
405
|
+
.filter(Boolean)
|
|
406
|
+
.map((line) => {
|
|
407
|
+
const [host = "", name = "", value = "", encryptedHex = ""] = line.split("\t");
|
|
408
|
+
return {
|
|
409
|
+
host,
|
|
410
|
+
name,
|
|
411
|
+
value,
|
|
412
|
+
encryptedHex,
|
|
413
|
+
browser: profile.browser,
|
|
414
|
+
safeStorageServices: profile.safeStorageServices,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
console.debug(`Could not read ${profile.browser} cookies:`, error);
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
cookiePairFromRow(row) {
|
|
427
|
+
if (!row.host.includes("commandcode.ai") || !row.name)
|
|
428
|
+
return undefined;
|
|
429
|
+
const value = row.value || this.decryptChromiumCookie(row);
|
|
430
|
+
if (!value)
|
|
431
|
+
return undefined;
|
|
432
|
+
return { name: row.name, value };
|
|
433
|
+
}
|
|
434
|
+
decryptChromiumCookie(row) {
|
|
435
|
+
if (!row.encryptedHex || row.browser === "Firefox")
|
|
436
|
+
return undefined;
|
|
437
|
+
const encrypted = Buffer.from(row.encryptedHex, "hex");
|
|
438
|
+
if (encrypted.length <= 3)
|
|
439
|
+
return undefined;
|
|
440
|
+
for (const service of row.safeStorageServices) {
|
|
441
|
+
const password = this.safeStoragePassword(service);
|
|
442
|
+
if (!password)
|
|
443
|
+
continue;
|
|
444
|
+
const decrypted = decryptMacChromiumCookie(encrypted, password);
|
|
445
|
+
if (decrypted)
|
|
446
|
+
return decrypted;
|
|
447
|
+
}
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
safeStoragePassword(service) {
|
|
451
|
+
try {
|
|
452
|
+
return cp
|
|
453
|
+
.execFileSync("/usr/bin/security", [
|
|
454
|
+
"find-generic-password",
|
|
455
|
+
"-w",
|
|
456
|
+
"-s",
|
|
457
|
+
service,
|
|
458
|
+
], { encoding: "utf8", timeout: 5000 })
|
|
459
|
+
.trim();
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
exports.BrowserCookieExtractor = BrowserCookieExtractor;
|
|
467
|
+
function decryptMacChromiumCookie(encryptedValue, safeStoragePassword) {
|
|
468
|
+
try {
|
|
469
|
+
const payload = encryptedValue.subarray(0, 3).toString() === "v10"
|
|
470
|
+
? encryptedValue.subarray(3)
|
|
471
|
+
: encryptedValue;
|
|
472
|
+
const key = crypto.pbkdf2Sync(safeStoragePassword, "saltysalt", 1003, 16, "sha1");
|
|
473
|
+
const decipher = crypto.createDecipheriv("aes-128-cbc", key, Buffer.alloc(16, " "));
|
|
474
|
+
return Buffer.concat([decipher.update(payload), decipher.final()]).toString("utf8");
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const chromiumCookieQuery = [
|
|
481
|
+
".mode tabs",
|
|
482
|
+
"SELECT host_key, name, value, hex(encrypted_value) FROM cookies WHERE host_key LIKE '%commandcode.ai%';",
|
|
483
|
+
].join("\n");
|
|
484
|
+
const firefoxCookieQuery = [
|
|
485
|
+
".mode tabs",
|
|
486
|
+
"SELECT host, name, value, '' FROM moz_cookies WHERE host LIKE '%commandcode.ai%';",
|
|
487
|
+
].join("\n");
|
|
232
488
|
class OpenCommandPlugin {
|
|
233
|
-
constructor(proxyBinaryPath, storageDir) {
|
|
234
|
-
this.
|
|
489
|
+
constructor(proxyBinaryPath = resolveProxyBinaryPath(), storageDir) {
|
|
490
|
+
this.commandCodeTokenKey = "opencommand.command_code_token";
|
|
491
|
+
this.legacyTokenKey = "opencommand.cc_session_token";
|
|
492
|
+
this.sessionCookieKey = "opencommand.cc_session_cookie";
|
|
235
493
|
this.proxyManager = new ProxyManager(proxyBinaryPath);
|
|
236
494
|
const dir = storageDir || `${process.env.HOME || "/tmp"}/.opencommand`;
|
|
237
495
|
this.secretStorage = new SecretStorage(dir);
|
|
496
|
+
this.cookieExtractor = new BrowserCookieExtractor();
|
|
238
497
|
}
|
|
239
|
-
/**
|
|
240
|
-
* Initialize plugin on editor startup
|
|
241
|
-
*/
|
|
242
498
|
async activate() {
|
|
243
499
|
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
500
|
try {
|
|
252
|
-
const config = await this.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.saveOpenCodeConfig(config);
|
|
501
|
+
const config = await this.ensureStarted();
|
|
502
|
+
if (config)
|
|
503
|
+
console.log(`✓ OpenCommand proxy ready at ${config.port}`);
|
|
256
504
|
}
|
|
257
505
|
catch (error) {
|
|
258
506
|
console.error("Failed to start proxy:", error);
|
|
259
507
|
}
|
|
260
508
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
509
|
+
async ensureStarted() {
|
|
510
|
+
const existingConfig = this.proxyManager.getConfig();
|
|
511
|
+
if (existingConfig && this.proxyManager.isRunning())
|
|
512
|
+
return existingConfig;
|
|
513
|
+
const commandCodeToken = await this.loadCommandCodeToken();
|
|
514
|
+
if (!commandCodeToken) {
|
|
515
|
+
console.warn("No COMMAND_CODE_TOKEN found. Please configure your CommandCode API key.");
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
const sessionCookie = await this.loadSessionCookie();
|
|
519
|
+
if (!sessionCookie) {
|
|
520
|
+
console.warn("No CommandCode Studio cookie found. Inference works, but usage scraping may be unavailable.");
|
|
521
|
+
}
|
|
522
|
+
const config = await this.proxyManager.start({
|
|
523
|
+
commandCodeToken,
|
|
524
|
+
sessionCookie,
|
|
525
|
+
});
|
|
526
|
+
this.saveOpenCodeConfig(config);
|
|
527
|
+
return config;
|
|
528
|
+
}
|
|
264
529
|
async deactivate() {
|
|
265
530
|
console.log("OpenCommand Plugin deactivating...");
|
|
266
531
|
await this.proxyManager.stop();
|
|
267
532
|
console.log("✓ Proxy stopped");
|
|
268
533
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
534
|
+
async loadCommandCodeToken() {
|
|
535
|
+
const token = firstDefined(process.env.COMMAND_CODE_TOKEN, process.env.COMMANDCODE_API_KEY, await this.secretStorage.get(this.commandCodeTokenKey), await this.secretStorage.get(this.legacyTokenKey));
|
|
536
|
+
if (token && token === (await this.secretStorage.get(this.legacyTokenKey))) {
|
|
537
|
+
await this.secretStorage.set(this.commandCodeTokenKey, token);
|
|
538
|
+
}
|
|
539
|
+
return token;
|
|
540
|
+
}
|
|
541
|
+
async loadSessionCookie() {
|
|
542
|
+
const configured = firstDefined(process.env.CC_SESSION_COOKIE, await this.secretStorage.get(this.sessionCookieKey));
|
|
543
|
+
if (configured)
|
|
544
|
+
return configured;
|
|
545
|
+
const extracted = await this.cookieExtractor.extractCommandCodeCookie();
|
|
546
|
+
if (extracted) {
|
|
547
|
+
await this.secretStorage.set(this.sessionCookieKey, extracted);
|
|
548
|
+
console.log("✓ CommandCode Studio cookie discovered from local browser profile");
|
|
549
|
+
}
|
|
550
|
+
return extracted;
|
|
551
|
+
}
|
|
273
552
|
saveOpenCodeConfig(config) {
|
|
274
553
|
const proxyUrl = `http://localhost:${config.port}`;
|
|
275
|
-
|
|
276
|
-
opencommand: {
|
|
277
|
-
|
|
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)
|
|
554
|
+
console.log("Proxy configuration saved:", {
|
|
555
|
+
opencommand: { proxyUrl, apiBaseUrl: `${proxyUrl}/v1` },
|
|
556
|
+
});
|
|
284
557
|
this.persistProxyConfig(proxyUrl, config.port);
|
|
285
558
|
}
|
|
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
559
|
persistProxyConfig(url, port) {
|
|
291
|
-
const fs = require("fs");
|
|
292
|
-
const path = require("path");
|
|
293
560
|
const configDir = `${process.env.HOME || "/tmp"}/.opencommand`;
|
|
294
561
|
const configPath = path.join(configDir, "proxy-config.json");
|
|
295
562
|
try {
|
|
@@ -301,26 +568,114 @@ class OpenCommandPlugin {
|
|
|
301
568
|
console.error("Failed to persist proxy config:", err);
|
|
302
569
|
}
|
|
303
570
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
571
|
+
async setCommandCodeToken(token) {
|
|
572
|
+
await this.secretStorage.set(this.commandCodeTokenKey, token);
|
|
573
|
+
console.log("✓ CommandCode API key saved securely");
|
|
574
|
+
await this.restartIfPossible();
|
|
575
|
+
}
|
|
307
576
|
async setSessionToken(token) {
|
|
308
|
-
await this.
|
|
309
|
-
|
|
310
|
-
|
|
577
|
+
await this.setCommandCodeToken(token);
|
|
578
|
+
}
|
|
579
|
+
async setSessionCookie(cookie) {
|
|
580
|
+
await this.secretStorage.set(this.sessionCookieKey, cookie);
|
|
581
|
+
console.log("✓ CommandCode Studio cookie saved securely");
|
|
582
|
+
await this.restartIfPossible();
|
|
583
|
+
}
|
|
584
|
+
async restartIfPossible() {
|
|
585
|
+
const commandCodeToken = await this.loadCommandCodeToken();
|
|
586
|
+
if (!commandCodeToken)
|
|
587
|
+
return;
|
|
311
588
|
await this.proxyManager.stop();
|
|
312
|
-
const config = await this.proxyManager.start(
|
|
589
|
+
const config = await this.proxyManager.start({
|
|
590
|
+
commandCodeToken,
|
|
591
|
+
sessionCookie: await this.loadSessionCookie(),
|
|
592
|
+
});
|
|
313
593
|
this.saveOpenCodeConfig(config);
|
|
314
594
|
}
|
|
315
|
-
/**
|
|
316
|
-
* Get proxy configuration
|
|
317
|
-
*/
|
|
318
595
|
getProxyConfig() {
|
|
319
596
|
const config = this.proxyManager.getConfig();
|
|
320
|
-
if (!config)
|
|
597
|
+
if (!config)
|
|
321
598
|
return "Proxy not running";
|
|
322
|
-
}
|
|
323
599
|
return JSON.stringify(config, null, 2);
|
|
324
600
|
}
|
|
325
601
|
}
|
|
326
602
|
exports.OpenCommandPlugin = OpenCommandPlugin;
|
|
603
|
+
function firstDefined(...values) {
|
|
604
|
+
for (const value of values) {
|
|
605
|
+
const trimmed = value?.trim();
|
|
606
|
+
if (trimmed)
|
|
607
|
+
return trimmed;
|
|
608
|
+
}
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
function resolveProxyBinaryPath() {
|
|
612
|
+
const candidates = [
|
|
613
|
+
process.env.OPENCOMMAND_PROXY_PATH,
|
|
614
|
+
path.join(os.homedir(), ".opencommand", "proxy"),
|
|
615
|
+
path.join(process.cwd(), "packages", "proxy", "proxy"),
|
|
616
|
+
path.join(__dirname, "proxy"),
|
|
617
|
+
].filter((candidate) => Boolean(candidate));
|
|
618
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[candidates.length - 1];
|
|
619
|
+
}
|
|
620
|
+
function openCodeModelConfig(model) {
|
|
621
|
+
return {
|
|
622
|
+
id: model.id,
|
|
623
|
+
name: model.name,
|
|
624
|
+
description: model.description,
|
|
625
|
+
reasoning: model.reasoning ?? false,
|
|
626
|
+
tool_call: true,
|
|
627
|
+
attachment: false,
|
|
628
|
+
cost: model.cost,
|
|
629
|
+
limit: {
|
|
630
|
+
context: model.context,
|
|
631
|
+
output: model.output,
|
|
632
|
+
},
|
|
633
|
+
modalities: {
|
|
634
|
+
input: ["text"],
|
|
635
|
+
output: ["text"],
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function registerOpenCommandProvider(config, baseURL = DEFAULT_PROXY_BASE_URL) {
|
|
640
|
+
const models = {};
|
|
641
|
+
for (const model of exports.COMMAND_CODE_GO_PLAN_MODELS) {
|
|
642
|
+
models[model.id] = openCodeModelConfig(model);
|
|
643
|
+
}
|
|
644
|
+
config.provider = {
|
|
645
|
+
...(config.provider ?? {}),
|
|
646
|
+
[PROVIDER_ID]: {
|
|
647
|
+
npm: "@ai-sdk/openai-compatible",
|
|
648
|
+
name: PROVIDER_NAME,
|
|
649
|
+
options: {
|
|
650
|
+
apiKey: PROVIDER_API_KEY_PLACEHOLDER,
|
|
651
|
+
baseURL,
|
|
652
|
+
},
|
|
653
|
+
models,
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
let runtimePlugin;
|
|
658
|
+
function getRuntimePlugin() {
|
|
659
|
+
runtimePlugin ?? (runtimePlugin = new OpenCommandPlugin());
|
|
660
|
+
return runtimePlugin;
|
|
661
|
+
}
|
|
662
|
+
const OpenCommandOpenCodePlugin = async () => ({
|
|
663
|
+
auth: {
|
|
664
|
+
provider: PROVIDER_ID,
|
|
665
|
+
loader: async () => {
|
|
666
|
+
const proxyConfig = await getRuntimePlugin().ensureStarted();
|
|
667
|
+
if (!proxyConfig)
|
|
668
|
+
return null;
|
|
669
|
+
return {
|
|
670
|
+
apiKey: PROVIDER_API_KEY_PLACEHOLDER,
|
|
671
|
+
baseURL: `http://localhost:${proxyConfig.port}/v1`,
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
methods: [],
|
|
675
|
+
},
|
|
676
|
+
config: async (config) => {
|
|
677
|
+
registerOpenCommandProvider(config);
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
exports.OpenCommandOpenCodePlugin = OpenCommandOpenCodePlugin;
|
|
681
|
+
exports.default = exports.OpenCommandOpenCodePlugin;
|