@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 +51 -27
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +165 -0
- package/dist/v2.d.ts +17 -0
- package/package.json +8 -3
- package/tui.js +5 -0
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
|
|
611
|
+
let localKey;
|
|
612
|
+
try {
|
|
613
|
+
localKey = await resolveKey();
|
|
614
|
+
} catch {}
|
|
615
|
+
let hasConnection = false;
|
|
593
616
|
try {
|
|
594
|
-
|
|
617
|
+
hasConnection = await ctx.integration.connection.active(INTEGRATION_ID) !== undefined;
|
|
595
618
|
} catch {}
|
|
596
|
-
if (!
|
|
597
|
-
console.warn(
|
|
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 =
|
|
645
|
+
provider.activation = hasConnection || localKey ? "enabled" : "auto";
|
|
608
646
|
provider.settings = { ...provider.settings, baseURL: PROVIDER_BASE };
|
|
609
|
-
if (
|
|
610
|
-
provider.settings.apiKey =
|
|
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
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.
|
|
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"
|