@jeffreyjyz/opencode-command-code 0.2.0 → 0.2.2

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.js CHANGED
@@ -548,6 +548,8 @@ async function loadModels(key) {
548
548
  import { Plugin } from "@opencode/plugin";
549
549
  var PLUGIN_ID = "command-code";
550
550
  var PROVIDER_BASE = "https://api.commandcode.ai/provider/v1";
551
+ var INTEGRATION_ID = "command-code";
552
+ var INTEGRATION_NAME = "Command Code";
551
553
  var LANES = [
552
554
  { id: "command-code-anthropic", name: "Command Code (Anthropic)", pkg: "@opencode/ai/providers/anthropic" },
553
555
  {
@@ -586,16 +588,52 @@ function staticSeedModels() {
586
588
  }
587
589
  return seed;
588
590
  }
591
+ async function credentialKey(ctx) {
592
+ try {
593
+ const connection = await ctx.integration.connection.active(INTEGRATION_ID);
594
+ if (connection) {
595
+ const credential = await ctx.integration.connection.resolve(connection);
596
+ if (credential?.type === "key")
597
+ return credential.key;
598
+ if (credential?.type === "oauth")
599
+ return credential.access;
600
+ }
601
+ } catch {}
602
+ try {
603
+ return await resolveKey();
604
+ } catch {
605
+ return;
606
+ }
607
+ }
589
608
  var commandCodeV2 = Plugin.define({
590
609
  id: PLUGIN_ID,
591
610
  async setup(ctx) {
592
- let key;
611
+ let localKey;
612
+ try {
613
+ localKey = await resolveKey();
614
+ } catch {}
615
+ let hasConnection = false;
593
616
  try {
594
- key = await resolveKey();
617
+ hasConnection = await ctx.integration.connection.active(INTEGRATION_ID) !== undefined;
595
618
  } catch {}
596
- if (!key) {
597
- console.warn("[command-code] no API key found \u2014 providers registered disabled. `cmd login` (~/.commandcode/auth.json), CMD_API_KEY, or providers.command-code-*.settings.apiKey in opencode.jsonc, then restart.");
619
+ if (!localKey && !hasConnection) {
620
+ console.warn(`[command-code] no API key found. Run /connect and choose "${INTEGRATION_NAME}", or \`cmd login\` (~/.commandcode/auth.json), or set CMD_API_KEY.`);
598
621
  }
622
+ await ctx.integration.transform((editor) => {
623
+ editor.update(INTEGRATION_ID, (integration) => {
624
+ if (integration.name === integration.id) {
625
+ integration.name = INTEGRATION_NAME;
626
+ }
627
+ });
628
+ editor.method.update({
629
+ integrationID: INTEGRATION_ID,
630
+ method: { type: "env", names: ["CMD_API_KEY"] }
631
+ });
632
+ editor.method.update({
633
+ integrationID: INTEGRATION_ID,
634
+ method: { type: "key", label: "Command Code API key" }
635
+ });
636
+ });
599
637
  const seed = staticSeedModels();
600
638
  await ctx.provider.transform((editor) => {
601
639
  for (const lane of LANES) {
@@ -604,32 +642,16 @@ var commandCodeV2 = Plugin.define({
604
642
  provider.name = lane.name;
605
643
  if (provider.package === "")
606
644
  provider.package = lane.pkg;
607
- provider.activation = key ? "enabled" : "disabled";
645
+ provider.activation = hasConnection || localKey ? "enabled" : "auto";
608
646
  provider.settings = { ...provider.settings, baseURL: PROVIDER_BASE };
609
- if (key && provider.settings.apiKey === undefined)
610
- provider.settings.apiKey = key;
647
+ if (!hasConnection && localKey && provider.settings.apiKey === undefined) {
648
+ provider.settings.apiKey = localKey;
649
+ }
650
+ provider.integrationID ??= INTEGRATION_ID;
611
651
  });
612
652
  editor.models.set(lane.id, seed[lane.id]);
613
653
  }
614
654
  });
615
- await ctx.command.transform((editor) => {
616
- editor.add({
617
- name: "cmd-usage",
618
- description: "Show Command Code plan, credits, and usage windows",
619
- execute: async ({ sessionID, prompt }) => {
620
- const arg = typeof prompt?.text === "string" ? prompt.text : "";
621
- let text;
622
- try {
623
- text = await runCmduse(arg, { env: key ? { CMD_API_KEY: key } : undefined });
624
- } catch (e) {
625
- text = `cmd-usage failed: ${e instanceof Error ? e.message : String(e)}`;
626
- }
627
- await ctx.session.synthetic({ sessionID, text: `**/cmd-usage**
628
-
629
- ${text}` });
630
- }
631
- });
632
- });
633
655
  await ctx.tool.transform((editor) => {
634
656
  editor.add({
635
657
  name: "cmd_usage",
@@ -646,16 +668,18 @@ ${text}` });
646
668
  },
647
669
  async execute(input) {
648
670
  const arg = typeof input?.arg === "string" ? input.arg : "";
671
+ const key = await credentialKey(ctx);
649
672
  return {
650
673
  content: await runCmduse(arg, { env: key ? { CMD_API_KEY: key } : undefined })
651
674
  };
652
675
  }
653
676
  });
654
677
  });
655
- if (!key)
656
- return;
657
678
  const controller = new AbortController;
658
679
  (async () => {
680
+ const key = await credentialKey(ctx);
681
+ if (!key)
682
+ return;
659
683
  try {
660
684
  const split = await loadModels(key);
661
685
  if (controller.signal.aborted)
package/dist/tui.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Plugin } from "@opencode/plugin/tui";
2
+ declare const _default: Plugin.Definition;
3
+ export default _default;
package/dist/tui.js ADDED
@@ -0,0 +1,165 @@
1
+ // @bun
2
+ // src/tui.ts
3
+ import { Plugin } from "@opencode/plugin/tui";
4
+
5
+ // src/cli.ts
6
+ import { spawn } from "child_process";
7
+ var CANDIDATES = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
8
+ var DEFAULT_ARGS = ["-1", "--plain"];
9
+ var SUBCOMMANDS = new Set([
10
+ "plans",
11
+ "models",
12
+ "daily",
13
+ "hourly",
14
+ "model",
15
+ "session",
16
+ "statusline",
17
+ "config"
18
+ ]);
19
+ function splitCliArgs(input) {
20
+ const out = [];
21
+ let cur = "";
22
+ let has = false;
23
+ let quote;
24
+ for (let i = 0;i < input.length; i++) {
25
+ const ch = input[i] ?? "";
26
+ if (quote) {
27
+ if (ch === quote) {
28
+ quote = undefined;
29
+ } else if (ch === "\\" && quote === '"' && i + 1 < input.length) {
30
+ cur += input[++i];
31
+ } else {
32
+ cur += ch;
33
+ }
34
+ continue;
35
+ }
36
+ if (ch === '"' || ch === "'") {
37
+ quote = ch;
38
+ has = true;
39
+ } else if (ch === "\\") {
40
+ if (i + 1 < input.length)
41
+ cur += input[++i];
42
+ has = true;
43
+ } else if (/\s/.test(ch)) {
44
+ if (has)
45
+ out.push(cur);
46
+ cur = "";
47
+ has = false;
48
+ } else {
49
+ cur += ch;
50
+ has = true;
51
+ }
52
+ }
53
+ if (has)
54
+ out.push(cur);
55
+ return out;
56
+ }
57
+ function buildCmduseArgs(arg) {
58
+ const tokens = splitCliArgs(arg);
59
+ if (!tokens.length)
60
+ return DEFAULT_ARGS;
61
+ if (tokens.some((t) => SUBCOMMANDS.has(t)))
62
+ return tokens;
63
+ return [...DEFAULT_ARGS, ...tokens];
64
+ }
65
+ function stderrTail(text) {
66
+ const lines = text.trimEnd().split(`
67
+ `);
68
+ return lines.slice(-3).join(`
69
+ `).slice(-400);
70
+ }
71
+ function runOnce(bin, args, opts) {
72
+ return new Promise((resolve, reject) => {
73
+ const child = spawn(bin, args, {
74
+ env: { ...process.env, ...opts.env ?? {} },
75
+ stdio: ["ignore", "pipe", "pipe"]
76
+ });
77
+ const out = [];
78
+ const err = [];
79
+ const onAbort = () => child.kill("SIGTERM");
80
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
81
+ child.stdout.on("data", (d) => out.push(d));
82
+ child.stderr.on("data", (d) => err.push(d));
83
+ child.on("error", (e) => {
84
+ opts.signal?.removeEventListener("abort", onAbort);
85
+ reject(e);
86
+ });
87
+ child.on("close", (code, signal) => {
88
+ opts.signal?.removeEventListener("abort", onAbort);
89
+ const stdout = Buffer.concat(out).toString("utf8");
90
+ if (opts.signal?.aborted) {
91
+ reject(new Error("cmduse invocation aborted"));
92
+ } else if (code === 0) {
93
+ resolve(stdout);
94
+ } else {
95
+ const why = signal ? `killed by ${signal}` : `exit ${code}`;
96
+ const tail = stderrTail(Buffer.concat(err).toString("utf8"));
97
+ reject(new Error(`cmduse ${args.join(" ")}: ${why}${tail ? `
98
+ ${tail}` : ""}`));
99
+ }
100
+ });
101
+ });
102
+ }
103
+ async function runCmduse(arg, opts = {}) {
104
+ const args = buildCmduseArgs(arg);
105
+ const candidates = opts.candidates ?? CANDIDATES;
106
+ let last;
107
+ for (const bin of candidates) {
108
+ try {
109
+ return await runOnce(bin, args, opts);
110
+ } catch (e) {
111
+ last = e;
112
+ if (e?.code !== "ENOENT")
113
+ throw e;
114
+ }
115
+ }
116
+ throw new Error(`cmduse binary not found (install: brew install JeffreyJYZ/tap/cmduse)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, { cause: last });
117
+ }
118
+
119
+ // src/tui.ts
120
+ var tui_default = Plugin.define({
121
+ id: "command-code.tui",
122
+ setup(context) {
123
+ context.ui.slot({
124
+ append: "app",
125
+ render: () => {
126
+ context.keymap.layer(() => ({
127
+ mode: "global",
128
+ priority: 10,
129
+ commands: [
130
+ {
131
+ id: "command-code.cmd-usage",
132
+ title: "Command Code usage",
133
+ group: "Command Code",
134
+ slash: { name: "cmd-usage", arguments: true },
135
+ enabled: () => true,
136
+ suggested: true,
137
+ run: async (input) => {
138
+ let text;
139
+ try {
140
+ text = await runCmduse(input ?? "");
141
+ } catch (e) {
142
+ context.ui.toast.show({
143
+ title: "cmd-usage failed",
144
+ message: e instanceof Error ? e.message : String(e),
145
+ variant: "error"
146
+ });
147
+ return;
148
+ }
149
+ await context.ui.dialog.alert({
150
+ title: "Command Code usage",
151
+ message: text.trimEnd()
152
+ });
153
+ }
154
+ }
155
+ ],
156
+ bindings: ["command-code.cmd-usage"]
157
+ }));
158
+ return null;
159
+ }
160
+ });
161
+ }
162
+ });
163
+ export {
164
+ tui_default as default
165
+ };
package/dist/v2.d.ts CHANGED
@@ -2,6 +2,9 @@ import { Plugin } from "@opencode/plugin";
2
2
  import { type CmdModel } from "./models";
3
3
  export declare const PLUGIN_ID = "command-code";
4
4
  export declare const PROVIDER_BASE = "https://api.commandcode.ai/provider/v1";
5
+ /** Shared integration backing both lanes' credentials (/connect + env). */
6
+ export declare const INTEGRATION_ID = "command-code";
7
+ export declare const INTEGRATION_NAME = "Command Code";
5
8
  type Lane = {
6
9
  id: "command-code-anthropic" | "command-code-openai";
7
10
  name: string;
@@ -20,4 +23,18 @@ export declare function toV2Model(m: CmdModel, lane: Lane): unknown;
20
23
  * Replaced by the gated live list once the background fetch lands; also the
21
24
  * fallback shown when the live fetch fails. */
22
25
  export declare function staticSeedModels(): Record<Lane["id"], unknown[]>;
26
+ /** The slice of the plugin context the credential resolver needs. */
27
+ type CredentialContext = {
28
+ integration: {
29
+ connection: {
30
+ active(integrationID: string): Promise<unknown>;
31
+ resolve(connection: unknown): Promise<unknown>;
32
+ };
33
+ };
34
+ };
35
+ /** Credential for spawning cmduse, host-first: a key stored by /connect lives
36
+ * only in opencode's credential store, so ask the connection before falling
37
+ * back to CMD_API_KEY / ~/.commandcode/auth.json. Resolved per call — a
38
+ * /connect mid-session must be picked up. */
39
+ export declare function credentialKey(ctx: CredentialContext): Promise<string | undefined>;
23
40
  export declare const commandCodeV2: Plugin.Plugin;
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Command Code provider + usage dashboard for opencode: two providers (Anthropic wire for Claude, OpenAI wire for everything else) against api.commandcode.ai, live model list, plan gating, /cmd-usage. Requires the cmduse CLI for usage rendering.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./tui": "./dist/tui.js"
11
+ },
8
12
  "scripts": {
9
- "build": "rm -rf dist && bun build src/index.ts --outdir dist --target bun --external @opencode-ai/plugin --external @opencode/plugin && tsc -p tsconfig.build.json",
13
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --target bun --external @opencode-ai/plugin --external @opencode/plugin --external @opencode/plugin/tui && bun build src/tui.ts --outdir dist --target bun --external @opencode-ai/plugin --external @opencode/plugin --external @opencode/plugin/tui && tsc -p tsconfig.build.json",
10
14
  "test": "bun test",
11
15
  "typecheck": "tsc --noEmit"
12
16
  },
@@ -16,9 +20,10 @@
16
20
  },
17
21
  "devDependencies": {
18
22
  "@types/bun": "latest",
23
+ "solid-js": "^1.9.0",
19
24
  "typescript": "^5"
20
25
  },
21
- "files": ["dist", "LICENSE"],
26
+ "files": ["dist", "tui.js", "LICENSE"],
22
27
  "keywords": ["opencode", "command-code", "plugin", "provider", "ai-sdk"],
23
28
  "license": "MIT",
24
29
  "publishConfig": {
package/tui.js ADDED
@@ -0,0 +1,5 @@
1
+ // Entry shim for local/directory plugin loading: opencode resolves the TUI
2
+ // half as a literal `tui.js` beside the package root (package `exports`
3
+ // are only consulted for npm-installed plugins). Keep this file in sync with
4
+ // the built ./tui output.
5
+ export { default } from "./dist/tui.js"