opencommand-plugin 0.0.5 → 0.0.6

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 CHANGED
@@ -47,7 +47,7 @@ npm run build
47
47
  npm test -- --runInBand
48
48
  ```
49
49
 
50
- ## v0.0.5 OpenCode loading fix
50
+ ## v0.0.6 non-blocking OpenCode startup
51
51
 
52
- `opencommand-plugin@0.0.5` publishes an explicit ESM wrapper at `bin/opencode-plugin.js` and should be pinned in `opencode.jsonc` as `opencommand-plugin@0.0.5` (or newer). This avoids stale `@latest` cache entries and ensures OpenCode sees the plugin default export as a function.
52
+ `opencommand-plugin@0.0.6` keeps OpenCode startup non-blocking: provider registration uses cached/static models immediately, while proxy startup and CommandCode plan/model refresh run in the background. Pin `opencode.jsonc` to `opencommand-plugin@0.0.6` (or newer) instead of `@latest` to avoid stale plugin cache entries.
53
53
 
package/dist/index.d.ts CHANGED
@@ -46,6 +46,7 @@ export declare class ProxyManager {
46
46
  constructor(binaryPath?: string);
47
47
  findAvailablePort(): Promise<number>;
48
48
  start(startConfig: ProxyStartConfig): Promise<ProxyConfig>;
49
+ private isPortAvailable;
49
50
  stop(): Promise<void>;
50
51
  private waitForHealthz;
51
52
  getConfig(): ProxyConfig | null;
@@ -77,13 +78,19 @@ export declare class OpenCommandPlugin {
77
78
  private proxyManager;
78
79
  private secretStorage;
79
80
  private cookieExtractor;
81
+ private startPromise;
82
+ private preloadPromise;
80
83
  private readonly commandCodeTokenKey;
81
84
  private readonly legacyTokenKey;
82
85
  private readonly sessionCookieKey;
83
86
  constructor(proxyBinaryPath?: string, storageDir?: string);
84
87
  activate(): Promise<void>;
85
88
  ensureStarted(): Promise<ProxyConfig | undefined>;
89
+ preloadForOpenCode(): void;
90
+ private preload;
91
+ private startProxy;
86
92
  deactivate(): Promise<void>;
93
+ getCurrentProxyConfig(): ProxyConfig | undefined;
87
94
  private loadCommandCodeToken;
88
95
  private loadSessionCookie;
89
96
  private saveOpenCodeConfig;
@@ -99,6 +106,7 @@ export declare function commandCodeModelsForPlan(planID: string): CommandCodeMod
99
106
  export declare function fetchCommandCodePlanModels(commandCodeToken: string, apiBaseURL?: string): Promise<CommandCodeModelDefinition[] | undefined>;
100
107
  export declare function parseCommandCodePlanID(body: unknown): string | undefined;
101
108
  export declare function registerOpenCommandProvider(config: OpenCodeConfig, baseURL?: string, modelDefinitions?: CommandCodeModelDefinition[]): void;
109
+ export declare function readCachedOpenCommandModels(maxAgeMs?: number): CommandCodeModelDefinition[] | undefined;
102
110
  export declare const OpenCommandOpenCodePlugin: () => Promise<{
103
111
  provider: {
104
112
  opencommand: {
@@ -118,7 +126,7 @@ export declare const OpenCommandOpenCodePlugin: () => Promise<{
118
126
  loader: () => Promise<{
119
127
  apiKey: string;
120
128
  baseURL: string;
121
- } | null>;
129
+ }>;
122
130
  methods: never[];
123
131
  };
124
132
  config: (config: OpenCodeConfig) => Promise<void>;
package/dist/index.js CHANGED
@@ -10,6 +10,22 @@ const PROVIDER_NAME = "CommandCode";
10
10
  const PROVIDER_API_KEY_PLACEHOLDER = "opencommand";
11
11
  const DEFAULT_PROXY_BASE_URL = "http://localhost:3000/v1";
12
12
  const COMMAND_CODE_API_BASE_URL = "https://api.commandcode.ai";
13
+ const MODEL_CACHE_MAX_AGE_MS = 10 * 60 * 1000;
14
+ function isDebugEnabled() {
15
+ return ["1", "true", "yes", "on"].includes((process.env.OPENCOMMAND_DEBUG ?? "").trim().toLowerCase());
16
+ }
17
+ function debugLog(...args) {
18
+ if (isDebugEnabled())
19
+ console.log(...args);
20
+ }
21
+ function debugWarn(...args) {
22
+ if (isDebugEnabled())
23
+ console.warn(...args);
24
+ }
25
+ function debugError(...args) {
26
+ if (isDebugEnabled())
27
+ console.error(...args);
28
+ }
13
29
  export const COMMAND_CODE_GO_PLAN_MODELS = [
14
30
  {
15
31
  id: "deepseek/deepseek-v4-pro",
@@ -206,7 +222,10 @@ export class ProxyManager {
206
222
  });
207
223
  }
208
224
  async start(startConfig) {
209
- const port = await this.findAvailablePort();
225
+ const preferredPort = parseProxyPort(process.env.OPENCOMMAND_PROXY_PORT) ?? 3000;
226
+ const port = (await this.isPortAvailable(preferredPort))
227
+ ? preferredPort
228
+ : await this.findAvailablePort();
210
229
  this.config = {
211
230
  port,
212
231
  commandCodeToken: startConfig.commandCodeToken,
@@ -222,20 +241,32 @@ export class ProxyManager {
222
241
  if (startConfig.sessionCookie) {
223
242
  env.CC_SESSION_COOKIE = startConfig.sessionCookie;
224
243
  }
244
+ const debug = isDebugEnabled();
225
245
  this.proxyProcess = cp.spawn(this.proxyBinaryPath, [], {
226
246
  env,
227
- stdio: ["ignore", "pipe", "pipe"],
228
- });
229
- this.proxyProcess.stdout?.on("data", (data) => {
230
- console.log(`[Proxy stdout] ${data}`);
231
- });
232
- this.proxyProcess.stderr?.on("data", (data) => {
233
- console.error(`[Proxy stderr] ${data}`);
247
+ stdio: ["ignore", debug ? "pipe" : "ignore", debug ? "pipe" : "ignore"],
234
248
  });
249
+ if (debug) {
250
+ this.proxyProcess.stdout?.on("data", (data) => {
251
+ debugLog(`[Proxy stdout] ${data}`);
252
+ });
253
+ this.proxyProcess.stderr?.on("data", (data) => {
254
+ debugError(`[Proxy stderr] ${data}`);
255
+ });
256
+ }
235
257
  await this.waitForHealthz(port, 30000);
236
- console.log(`✓ Proxy started on port ${port}`);
258
+ debugLog(`✓ Proxy started on port ${port}`);
237
259
  return this.config;
238
260
  }
261
+ async isPortAvailable(port) {
262
+ return new Promise((resolve) => {
263
+ const server = net.createServer();
264
+ server.once("error", () => resolve(false));
265
+ server.listen(port, () => {
266
+ server.close(() => resolve(true));
267
+ });
268
+ });
269
+ }
239
270
  async stop() {
240
271
  const proc = this.proxyProcess;
241
272
  if (!proc)
@@ -251,7 +282,7 @@ export class ProxyManager {
251
282
  };
252
283
  proc.once("exit", finish);
253
284
  proc.once("error", (error) => {
254
- console.error("Proxy process error while stopping:", error);
285
+ debugError("Proxy process error while stopping:", error);
255
286
  finish();
256
287
  });
257
288
  try {
@@ -259,7 +290,7 @@ export class ProxyManager {
259
290
  proc.kill("SIGTERM");
260
291
  }
261
292
  catch (error) {
262
- console.error("Failed to send SIGTERM to proxy process:", error);
293
+ debugError("Failed to send SIGTERM to proxy process:", error);
263
294
  finish();
264
295
  return;
265
296
  }
@@ -269,7 +300,7 @@ export class ProxyManager {
269
300
  proc.kill("SIGKILL");
270
301
  }
271
302
  catch (error) {
272
- console.error("Failed to send SIGKILL to proxy process:", error);
303
+ debugError("Failed to send SIGKILL to proxy process:", error);
273
304
  }
274
305
  }
275
306
  finish();
@@ -290,7 +321,7 @@ export class ProxyManager {
290
321
  return;
291
322
  }
292
323
  catch (err) {
293
- console.debug(`Health check attempt failed for port ${port}:`, err);
324
+ debugLog(`Health check attempt failed for port ${port}:`, err);
294
325
  }
295
326
  await new Promise((r) => setTimeout(r, 500));
296
327
  }
@@ -317,7 +348,7 @@ export class SecretStorage {
317
348
  }
318
349
  catch (error) {
319
350
  if (error.code !== "ENOENT") {
320
- console.debug("Could not read OpenCommand secret store:", error);
351
+ debugLog("Could not read OpenCommand secret store:", error);
321
352
  }
322
353
  return {};
323
354
  }
@@ -459,7 +490,7 @@ export class BrowserCookieExtractor {
459
490
  });
460
491
  }
461
492
  catch (error) {
462
- console.debug(`Could not read ${profile.browser} cookies:`, error);
493
+ debugLog(`Could not read ${profile.browser} cookies:`, error);
463
494
  return [];
464
495
  }
465
496
  finally {
@@ -538,28 +569,61 @@ export class OpenCommandPlugin {
538
569
  this.cookieExtractor = new BrowserCookieExtractor();
539
570
  }
540
571
  async activate() {
541
- console.log("OpenCommand Plugin activating...");
572
+ debugLog("OpenCommand Plugin activating...");
542
573
  try {
543
574
  const config = await this.ensureStarted();
544
575
  if (config)
545
- console.log(`✓ OpenCommand proxy ready at ${config.port}`);
576
+ debugLog(`✓ OpenCommand proxy ready at ${config.port}`);
546
577
  }
547
578
  catch (error) {
548
- console.error("Failed to start proxy:", error);
579
+ debugError("Failed to start proxy:", error);
549
580
  }
550
581
  }
551
582
  async ensureStarted() {
583
+ const existingConfig = this.proxyManager.getConfig();
584
+ if (existingConfig && this.proxyManager.isRunning())
585
+ return existingConfig;
586
+ this.startPromise ?? (this.startPromise = this.startProxy());
587
+ try {
588
+ return await this.startPromise;
589
+ }
590
+ finally {
591
+ this.startPromise = undefined;
592
+ }
593
+ }
594
+ preloadForOpenCode() {
595
+ if (this.preloadPromise)
596
+ return;
597
+ this.preloadPromise = this.preload()
598
+ .catch((error) => {
599
+ debugLog("OpenCommand background preload failed:", error);
600
+ })
601
+ .finally(() => {
602
+ this.preloadPromise = undefined;
603
+ });
604
+ }
605
+ async preload() {
606
+ const proxyConfig = await this.ensureStarted();
607
+ if (!proxyConfig)
608
+ return;
609
+ const baseURL = `http://localhost:${proxyConfig.port}/v1`;
610
+ const models = (await fetchCommandCodePlanModels(proxyConfig.commandCodeToken)) ??
611
+ (await fetchOpenCommandModels(baseURL));
612
+ if (models)
613
+ writeCachedOpenCommandModels(models);
614
+ }
615
+ async startProxy() {
552
616
  const existingConfig = this.proxyManager.getConfig();
553
617
  if (existingConfig && this.proxyManager.isRunning())
554
618
  return existingConfig;
555
619
  const commandCodeToken = await this.loadCommandCodeToken();
556
620
  if (!commandCodeToken) {
557
- console.warn("No COMMAND_CODE_TOKEN found. Please configure your CommandCode API key.");
621
+ debugWarn("No COMMAND_CODE_TOKEN found. Please configure your CommandCode API key.");
558
622
  return undefined;
559
623
  }
560
624
  const sessionCookie = await this.loadSessionCookie();
561
625
  if (!sessionCookie) {
562
- console.warn("No CommandCode Studio cookie found. Inference works, but usage scraping may be unavailable.");
626
+ debugWarn("No CommandCode Studio cookie found. Inference works, but usage scraping may be unavailable.");
563
627
  }
564
628
  const config = await this.proxyManager.start({
565
629
  commandCodeToken,
@@ -569,9 +633,12 @@ export class OpenCommandPlugin {
569
633
  return config;
570
634
  }
571
635
  async deactivate() {
572
- console.log("OpenCommand Plugin deactivating...");
636
+ debugLog("OpenCommand Plugin deactivating...");
573
637
  await this.proxyManager.stop();
574
- console.log("✓ Proxy stopped");
638
+ debugLog("✓ Proxy stopped");
639
+ }
640
+ getCurrentProxyConfig() {
641
+ return this.proxyManager.getConfig() ?? undefined;
575
642
  }
576
643
  async loadCommandCodeToken() {
577
644
  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));
@@ -587,13 +654,13 @@ export class OpenCommandPlugin {
587
654
  const extracted = await this.cookieExtractor.extractCommandCodeCookie();
588
655
  if (extracted) {
589
656
  await this.secretStorage.set(this.sessionCookieKey, extracted);
590
- console.log("✓ CommandCode Studio cookie discovered from local browser profile");
657
+ debugLog("✓ CommandCode Studio cookie discovered from local browser profile");
591
658
  }
592
659
  return extracted;
593
660
  }
594
661
  saveOpenCodeConfig(config) {
595
662
  const proxyUrl = `http://localhost:${config.port}`;
596
- console.log("Proxy configuration saved:", {
663
+ debugLog("Proxy configuration saved:", {
597
664
  opencommand: { proxyUrl, apiBaseUrl: `${proxyUrl}/v1` },
598
665
  });
599
666
  this.persistProxyConfig(proxyUrl, config.port);
@@ -604,15 +671,15 @@ export class OpenCommandPlugin {
604
671
  try {
605
672
  fs.mkdirSync(configDir, { recursive: true });
606
673
  fs.writeFileSync(configPath, JSON.stringify({ url, port, updatedAt: new Date().toISOString() }, null, 2), { mode: 0o644 });
607
- console.log(`✓ Proxy config persisted to ${configPath}`);
674
+ debugLog(`✓ Proxy config persisted to ${configPath}`);
608
675
  }
609
676
  catch (err) {
610
- console.error("Failed to persist proxy config:", err);
677
+ debugError("Failed to persist proxy config:", err);
611
678
  }
612
679
  }
613
680
  async setCommandCodeToken(token) {
614
681
  await this.secretStorage.set(this.commandCodeTokenKey, token);
615
- console.log("✓ CommandCode API key saved securely");
682
+ debugLog("✓ CommandCode API key saved securely");
616
683
  await this.restartIfPossible();
617
684
  }
618
685
  async setSessionToken(token) {
@@ -620,7 +687,7 @@ export class OpenCommandPlugin {
620
687
  }
621
688
  async setSessionCookie(cookie) {
622
689
  await this.secretStorage.set(this.sessionCookieKey, cookie);
623
- console.log("✓ CommandCode Studio cookie saved securely");
690
+ debugLog("✓ CommandCode Studio cookie saved securely");
624
691
  await this.restartIfPossible();
625
692
  }
626
693
  async restartIfPossible() {
@@ -654,7 +721,7 @@ function resolveProxyBinaryPath() {
654
721
  process.env.OPENCOMMAND_PROXY_PATH,
655
722
  path.join(os.homedir(), ".opencommand", "proxy"),
656
723
  path.join(process.cwd(), "packages", "proxy", "proxy"),
657
- path.join(__dirname, "proxy"),
724
+ path.join(process.cwd(), "proxy"),
658
725
  ].filter((candidate) => Boolean(candidate));
659
726
  return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[candidates.length - 1];
660
727
  }
@@ -706,7 +773,7 @@ export async function fetchOpenCommandModels(baseURL) {
706
773
  return models.length > 0 ? models : undefined;
707
774
  }
708
775
  catch (error) {
709
- console.debug("Could not fetch OpenCommand models from local proxy:", error);
776
+ debugLog("Could not fetch OpenCommand models from local proxy:", error);
710
777
  return undefined;
711
778
  }
712
779
  }
@@ -736,7 +803,7 @@ export async function fetchCommandCodePlanModels(commandCodeToken, apiBaseURL =
736
803
  return planID ? commandCodeModelsForPlan(planID) : undefined;
737
804
  }
738
805
  catch (error) {
739
- console.debug("Could not detect CommandCode plan for model registration:", error);
806
+ debugLog("Could not detect CommandCode plan for model registration:", error);
740
807
  return undefined;
741
808
  }
742
809
  }
@@ -787,6 +854,39 @@ export function registerOpenCommandProvider(config, baseURL = DEFAULT_PROXY_BASE
787
854
  },
788
855
  };
789
856
  }
857
+ export function readCachedOpenCommandModels(maxAgeMs = MODEL_CACHE_MAX_AGE_MS) {
858
+ try {
859
+ const raw = fs.readFileSync(openCommandModelCachePath(), "utf8");
860
+ const cache = JSON.parse(raw);
861
+ if (!cache.updatedAt || !Array.isArray(cache.models))
862
+ return undefined;
863
+ const updatedAt = Date.parse(cache.updatedAt);
864
+ if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > maxAgeMs) {
865
+ return undefined;
866
+ }
867
+ return cache.models.length > 0 ? cache.models : undefined;
868
+ }
869
+ catch {
870
+ return undefined;
871
+ }
872
+ }
873
+ function writeCachedOpenCommandModels(models) {
874
+ try {
875
+ const cachePath = openCommandModelCachePath();
876
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
877
+ fs.writeFileSync(cachePath, JSON.stringify({ updatedAt: new Date().toISOString(), models }, null, 2), { mode: 0o644 });
878
+ }
879
+ catch (error) {
880
+ debugLog("Could not write OpenCommand model cache:", error);
881
+ }
882
+ }
883
+ function openCommandModelCachePath() {
884
+ return path.join(process.env.HOME || "/tmp", ".opencommand", "model-cache.json");
885
+ }
886
+ function parseProxyPort(value) {
887
+ const port = Number(value);
888
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : undefined;
889
+ }
790
890
  let runtimePlugin;
791
891
  function getRuntimePlugin() {
792
892
  runtimePlugin ?? (runtimePlugin = new OpenCommandPlugin());
@@ -807,27 +907,21 @@ export const OpenCommandOpenCodePlugin = async () => ({
807
907
  auth: {
808
908
  provider: PROVIDER_ID,
809
909
  loader: async () => {
810
- const proxyConfig = await getRuntimePlugin().ensureStarted();
811
- if (!proxyConfig)
812
- return null;
910
+ const runtime = getRuntimePlugin();
911
+ const proxyConfig = runtime.getCurrentProxyConfig();
912
+ runtime.preloadForOpenCode();
813
913
  return {
814
914
  apiKey: PROVIDER_API_KEY_PLACEHOLDER,
815
- baseURL: `http://localhost:${proxyConfig.port}/v1`,
915
+ baseURL: proxyConfig
916
+ ? `http://localhost:${proxyConfig.port}/v1`
917
+ : DEFAULT_PROXY_BASE_URL,
816
918
  };
817
919
  },
818
920
  methods: [],
819
921
  },
820
922
  config: async (config) => {
821
- let baseURL = DEFAULT_PROXY_BASE_URL;
822
- let models;
823
- const proxyConfig = await getRuntimePlugin().ensureStarted();
824
- if (proxyConfig) {
825
- baseURL = `http://localhost:${proxyConfig.port}/v1`;
826
- models =
827
- (await fetchCommandCodePlanModels(proxyConfig.commandCodeToken)) ??
828
- (await fetchOpenCommandModels(baseURL));
829
- }
830
- registerOpenCommandProvider(config, baseURL, models ?? COMMAND_CODE_GO_PLAN_MODELS);
923
+ registerOpenCommandProvider(config, DEFAULT_PROXY_BASE_URL, readCachedOpenCommandModels() ?? COMMAND_CODE_GO_PLAN_MODELS);
924
+ getRuntimePlugin().preloadForOpenCode();
831
925
  },
832
926
  });
833
927
  export default OpenCommandOpenCodePlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencommand-plugin",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "OpenCommand - CommandCode API Plugin for OpenCode",
5
5
  "main": "./bin/opencode-plugin.js",
6
6
  "module": "./bin/opencode-plugin.js",