@jeffreyjyz/opencode-command-code 0.5.1 → 0.5.4
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/api.d.ts +0 -10
- package/dist/cli.d.ts +19 -2
- package/dist/constants/providers.d.ts +2 -0
- package/dist/constants/sidebar.d.ts +3 -2
- package/dist/index.js +51 -23
- package/dist/sidebar/data.d.ts +4 -3
- package/dist/sidebar/rows.d.ts +6 -1
- package/dist/sidebar/usageDb.d.ts +15 -0
- package/dist/sidebar/zen.d.ts +39 -1
- package/dist/tui.js +190 -70
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export declare function authHeaders(key: string): Record<string, string>;
|
|
2
|
-
export declare function getJson(path: string, key: string, timeoutMs?: number): Promise<unknown>;
|
|
3
1
|
export type Whoami = {
|
|
4
2
|
success: boolean;
|
|
5
3
|
user?: {
|
|
@@ -30,13 +28,6 @@ export type Credits = {
|
|
|
30
28
|
weekly?: Window;
|
|
31
29
|
};
|
|
32
30
|
};
|
|
33
|
-
export type Summary = {
|
|
34
|
-
totalCount?: number;
|
|
35
|
-
totalCost?: number;
|
|
36
|
-
successRate?: number;
|
|
37
|
-
totalTokensIn?: number;
|
|
38
|
-
totalTokensOut?: number;
|
|
39
|
-
};
|
|
40
31
|
export type ProviderModelsResp = {
|
|
41
32
|
data: Array<{
|
|
42
33
|
id: string;
|
|
@@ -48,5 +39,4 @@ export type ProviderModelsResp = {
|
|
|
48
39
|
export declare function whoami(key: string): Promise<Whoami>;
|
|
49
40
|
export declare function subscriptions(key: string): Promise<SubData>;
|
|
50
41
|
export declare function credits(key: string): Promise<Credits>;
|
|
51
|
-
export declare function usageSummary(key: string): Promise<Summary>;
|
|
52
42
|
export declare function providerModels(key: string): Promise<ProviderModelsResp>;
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
/** Fallback lookup paths: the opencode background service's PATH often lacks
|
|
2
3
|
* the homebrew prefix, so the bare name alone is not enough. `CMDUSE_BIN` wins
|
|
3
4
|
* when set — the dev-build hook the sibling cmduse consumers (mpc) honour too. */
|
|
@@ -6,6 +7,9 @@ export declare function cmduseCandidates(env?: Record<string, string | undefined
|
|
|
6
7
|
* wins when set (the `bun link` dev hook oc-cmd-compare installs),
|
|
7
8
|
* then the bun/homebrew fallbacks. */
|
|
8
9
|
export declare function mpcCandidates(env?: Record<string, string | undefined>): string[];
|
|
10
|
+
/** Same lookup for ocuse, the OpenCode Go/Zen reader shipped in the cmd-usage
|
|
11
|
+
* crate: `OCUSE_BIN` wins when set, then the homebrew fallbacks. */
|
|
12
|
+
export declare function ocuseCandidates(env?: Record<string, string | undefined>): string[];
|
|
9
13
|
/** Shell-words split: whitespace-separated, with single/double quoting and
|
|
10
14
|
* backslash escapes ("--tz +05:30", arg='a b'). Unterminated quotes close at
|
|
11
15
|
* end of input. */
|
|
@@ -24,14 +28,27 @@ export type RunOptions = {
|
|
|
24
28
|
env?: Record<string, string>;
|
|
25
29
|
/** Overridable for tests. */
|
|
26
30
|
candidates?: readonly string[];
|
|
31
|
+
/** Extra spawn options. Excludes `stdio`: the callbacks below assume stdout
|
|
32
|
+
* and stderr are piped (the default here), so callers may only add fields
|
|
33
|
+
* that leave that contract intact — the sidebar passes `detached` (see
|
|
34
|
+
* constants/cli.ts; its own stdio equals the default). */
|
|
35
|
+
spawnOptions?: Omit<NonNullable<Parameters<typeof spawn>[2]>, "stdio">;
|
|
36
|
+
/** Kill the child after this many ms. The sidebar bounds each poll; the
|
|
37
|
+
* tool spawns leave it unset. */
|
|
38
|
+
timeoutMs?: number;
|
|
27
39
|
};
|
|
28
40
|
/** Run cmduse and return its plain-text stdout. Tries PATH, then common
|
|
29
41
|
* homebrew locations; every candidate failing with ENOENT yields a single
|
|
30
42
|
* install-hint error. */
|
|
31
|
-
export declare function runCmduse(arg: string, opts?: RunOptions): Promise<string>;
|
|
43
|
+
export declare function runCmduse(arg: string | readonly string[], opts?: RunOptions): Promise<string>;
|
|
32
44
|
/** Run mpc and return its plain-text stdout (the per-model price
|
|
33
45
|
* comparison). Same contract as runCmduse: `MPC_BIN` wins, then the
|
|
34
46
|
* fallback paths; every candidate failing with ENOENT yields a
|
|
35
47
|
* single install-hint error, and any other failure (a non-zero exit
|
|
36
48
|
* included) surfaces immediately with mpc's last stderr lines. */
|
|
37
|
-
export declare function runMpc(arg: string, opts?: RunOptions): Promise<string>;
|
|
49
|
+
export declare function runMpc(arg: string | readonly string[], opts?: RunOptions): Promise<string>;
|
|
50
|
+
/** Run ocuse and return its plain-text stdout (OpenCode Go/Zen usage). Same
|
|
51
|
+
* contract as runCmduse; ocuse ships in the cmd-usage crate, so its install
|
|
52
|
+
* hint is the same brew formula. A string arg is shell-split, an array used
|
|
53
|
+
* as-is. */
|
|
54
|
+
export declare function runOcuse(arg: string | readonly string[], opts?: RunOptions): Promise<string>;
|
|
@@ -13,6 +13,8 @@ export declare const ZEN_PROVIDER_ID = "opencode";
|
|
|
13
13
|
export declare const ZEN_PROVIDER_PREFIX = "opencode-zen";
|
|
14
14
|
/** TUI plugin id (stable contract; test/tui.test.ts pins it). */
|
|
15
15
|
export declare const TUI_ID = "command-code.tui";
|
|
16
|
+
/** The v2 /cmd-usage slash command id (registered and bound in tui.tsx). */
|
|
17
|
+
export declare const CMD_USAGE_COMMAND_ID = "command-code.cmd-usage";
|
|
16
18
|
/** The cmd_usage tool description, shared by the v1 and v2 halves. */
|
|
17
19
|
export declare const TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg='plans' for the plan comparison table only, or extra cmduse flags (e.g. '--tz +05:30 daily').";
|
|
18
20
|
/** The mpc_prices tool description, shared by the v1 and v2 halves. */
|
|
@@ -11,8 +11,9 @@ export declare const ROW_WIDTH = 37;
|
|
|
11
11
|
export declare const TIER_DISPLAY: Readonly<Record<Category, string>>;
|
|
12
12
|
/**
|
|
13
13
|
* OpenCode Go plans the panel can render: the same token rates, different
|
|
14
|
-
* per-model monthly limits. Which one a user is on is not discoverable
|
|
15
|
-
*
|
|
14
|
+
* per-model monthly limits. Which one a user is on is not discoverable even with
|
|
15
|
+
* the usage endpoint in hand — it answers percentages, not a plan id, and the
|
|
16
|
+
* store records no subscription — so it is a setting.
|
|
16
17
|
*/
|
|
17
18
|
export declare const OC_PLANS: readonly ["go", "go-plus"];
|
|
18
19
|
export type OcPlan = (typeof OC_PLANS)[number];
|
package/dist/index.js
CHANGED
|
@@ -2414,7 +2414,7 @@ function superRefine(fn) {
|
|
|
2414
2414
|
// package.json
|
|
2415
2415
|
var package_default = {
|
|
2416
2416
|
name: "@jeffreyjyz/opencode-command-code",
|
|
2417
|
-
version: "0.5.
|
|
2417
|
+
version: "0.5.4",
|
|
2418
2418
|
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.",
|
|
2419
2419
|
type: "module",
|
|
2420
2420
|
main: "dist/index.js",
|
|
@@ -3230,6 +3230,11 @@ var MPC_BIN_CANDIDATES = [
|
|
|
3230
3230
|
join(homedir(), ".bun/bin/mpc"),
|
|
3231
3231
|
"/opt/homebrew/bin/mpc"
|
|
3232
3232
|
];
|
|
3233
|
+
var OCUSE_BIN_CANDIDATES = [
|
|
3234
|
+
"ocuse",
|
|
3235
|
+
"/opt/homebrew/bin/ocuse",
|
|
3236
|
+
"/usr/local/bin/ocuse"
|
|
3237
|
+
];
|
|
3233
3238
|
|
|
3234
3239
|
// src/constants/cli.ts
|
|
3235
3240
|
var DEFAULT_ARGS = ["-1", "--plain"];
|
|
@@ -3256,6 +3261,11 @@ function mpcCandidates(env = process.env) {
|
|
|
3256
3261
|
return candidates.filter((bin) => Boolean(bin));
|
|
3257
3262
|
}
|
|
3258
3263
|
var MPC_CANDIDATES = mpcCandidates();
|
|
3264
|
+
function ocuseCandidates(env = process.env) {
|
|
3265
|
+
const candidates = [env.OCUSE_BIN, ...OCUSE_BIN_CANDIDATES];
|
|
3266
|
+
return candidates.filter((bin) => Boolean(bin));
|
|
3267
|
+
}
|
|
3268
|
+
var OCUSE_CANDIDATES = ocuseCandidates();
|
|
3259
3269
|
function splitCliArgs(input) {
|
|
3260
3270
|
const out = [];
|
|
3261
3271
|
let cur = "";
|
|
@@ -3318,36 +3328,53 @@ function runOnce(bin, args, opts, name = "cmduse") {
|
|
|
3318
3328
|
return new Promise((resolve, reject) => {
|
|
3319
3329
|
const child = spawn(bin, args, {
|
|
3320
3330
|
env: { ...process.env, ...opts.env ?? {} },
|
|
3321
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3331
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3332
|
+
...opts.spawnOptions
|
|
3322
3333
|
});
|
|
3323
3334
|
const out = [];
|
|
3324
3335
|
const err = [];
|
|
3336
|
+
let settled = false;
|
|
3337
|
+
const timer = opts.timeoutMs === undefined ? undefined : setTimeout(() => {
|
|
3338
|
+
if (settled)
|
|
3339
|
+
return;
|
|
3340
|
+
settled = true;
|
|
3341
|
+
child.kill();
|
|
3342
|
+
reject(new Error(`${name} ${args.join(" ")}: timed out after ${opts.timeoutMs}ms`));
|
|
3343
|
+
}, opts.timeoutMs);
|
|
3325
3344
|
const onAbort = () => child.kill("SIGTERM");
|
|
3326
3345
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3346
|
+
const settle = (fn) => {
|
|
3347
|
+
if (settled)
|
|
3348
|
+
return;
|
|
3349
|
+
settled = true;
|
|
3350
|
+
if (timer)
|
|
3351
|
+
clearTimeout(timer);
|
|
3352
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
3353
|
+
fn();
|
|
3354
|
+
};
|
|
3327
3355
|
child.stdout.on("data", (d) => out.push(d));
|
|
3328
3356
|
child.stderr.on("data", (d) => err.push(d));
|
|
3329
|
-
child.on("error", (e) =>
|
|
3330
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
3331
|
-
reject(e);
|
|
3332
|
-
});
|
|
3357
|
+
child.on("error", (e) => settle(() => reject(e)));
|
|
3333
3358
|
child.on("close", (code, signal) => {
|
|
3334
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
3335
3359
|
const stdout = Buffer.concat(out).toString("utf8");
|
|
3336
3360
|
if (opts.signal?.aborted) {
|
|
3337
|
-
reject(new Error(`${name} invocation aborted`));
|
|
3361
|
+
settle(() => reject(new Error(`${name} invocation aborted`)));
|
|
3338
3362
|
} else if (code === 0) {
|
|
3339
|
-
resolve(stdout);
|
|
3363
|
+
settle(() => resolve(stdout));
|
|
3340
3364
|
} else {
|
|
3341
3365
|
const why = signal ? `killed by ${signal}` : `exit ${code}`;
|
|
3342
3366
|
const tail = stderrTail(Buffer.concat(err).toString("utf8"));
|
|
3343
|
-
reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
|
|
3344
|
-
${tail}` : ""}`));
|
|
3367
|
+
settle(() => reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
|
|
3368
|
+
${tail}` : ""}`)));
|
|
3345
3369
|
}
|
|
3346
3370
|
});
|
|
3347
3371
|
});
|
|
3348
3372
|
}
|
|
3373
|
+
function resolveArgs(arg, build) {
|
|
3374
|
+
return typeof arg === "string" ? build(arg) : [...arg];
|
|
3375
|
+
}
|
|
3349
3376
|
async function runCmduse(arg, opts = {}) {
|
|
3350
|
-
const args =
|
|
3377
|
+
const args = resolveArgs(arg, buildCmduseArgs);
|
|
3351
3378
|
const candidates = opts.candidates ?? CANDIDATES;
|
|
3352
3379
|
let last;
|
|
3353
3380
|
for (const bin of candidates) {
|
|
@@ -3362,7 +3389,7 @@ async function runCmduse(arg, opts = {}) {
|
|
|
3362
3389
|
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 });
|
|
3363
3390
|
}
|
|
3364
3391
|
async function runMpc(arg, opts = {}) {
|
|
3365
|
-
const args =
|
|
3392
|
+
const args = resolveArgs(arg, buildMpcArgs);
|
|
3366
3393
|
const candidates = opts.candidates ?? MPC_CANDIDATES;
|
|
3367
3394
|
let last;
|
|
3368
3395
|
for (const bin of candidates) {
|
|
@@ -5025,6 +5052,7 @@ var commandCodeV2 = {
|
|
|
5025
5052
|
|
|
5026
5053
|
// src/index.ts
|
|
5027
5054
|
var PROVIDER_NPM = `${package_default.name}@${package_default.version}`;
|
|
5055
|
+
var [ANTHROPIC, OPENAI] = LANES;
|
|
5028
5056
|
function tool(input) {
|
|
5029
5057
|
return input;
|
|
5030
5058
|
}
|
|
@@ -5071,7 +5099,7 @@ function toModelDefs(models, providerID, npm, interleaved = false) {
|
|
|
5071
5099
|
var CommandCodePlugin = async (_input) => {
|
|
5072
5100
|
return {
|
|
5073
5101
|
auth: {
|
|
5074
|
-
provider:
|
|
5102
|
+
provider: ANTHROPIC.id,
|
|
5075
5103
|
loader: async (getAuth) => {
|
|
5076
5104
|
const auth = await getAuth();
|
|
5077
5105
|
if (auth?.type !== "api") {
|
|
@@ -5101,7 +5129,7 @@ var CommandCodePlugin = async (_input) => {
|
|
|
5101
5129
|
return {
|
|
5102
5130
|
type: "success",
|
|
5103
5131
|
key,
|
|
5104
|
-
provider:
|
|
5132
|
+
provider: ANTHROPIC.id
|
|
5105
5133
|
};
|
|
5106
5134
|
} catch {
|
|
5107
5135
|
return { type: "failed" };
|
|
@@ -5125,19 +5153,19 @@ var CommandCodePlugin = async (_input) => {
|
|
|
5125
5153
|
console.warn("[command-code] model list unavailable:", e);
|
|
5126
5154
|
}
|
|
5127
5155
|
}
|
|
5128
|
-
const claudeDefs = split ? toModelDefs(split.claude,
|
|
5129
|
-
const openDefs = split ? toModelDefs(split.open,
|
|
5156
|
+
const claudeDefs = split ? toModelDefs(split.claude, ANTHROPIC.id, PROVIDER_NPM) : {};
|
|
5157
|
+
const openDefs = split ? toModelDefs(split.open, OPENAI.id, PROVIDER_NPM, {
|
|
5130
5158
|
field: "reasoning_content"
|
|
5131
5159
|
}) : {};
|
|
5132
|
-
const userAnthropic = existing(
|
|
5133
|
-
cfg.provider[
|
|
5160
|
+
const userAnthropic = existing(ANTHROPIC.id);
|
|
5161
|
+
cfg.provider[ANTHROPIC.id] = {
|
|
5134
5162
|
npm: userAnthropic.npm ?? PROVIDER_NPM,
|
|
5135
5163
|
name: userAnthropic.name ?? "Command Code (Anthropic)",
|
|
5136
5164
|
options: { baseURL: PROVIDER_BASE, ...userAnthropic.options },
|
|
5137
5165
|
models: { ...claudeDefs, ...userAnthropic.models }
|
|
5138
5166
|
};
|
|
5139
|
-
const userOpenai = existing(
|
|
5140
|
-
cfg.provider[
|
|
5167
|
+
const userOpenai = existing(OPENAI.id);
|
|
5168
|
+
cfg.provider[OPENAI.id] = {
|
|
5141
5169
|
npm: userOpenai.npm ?? PROVIDER_NPM,
|
|
5142
5170
|
name: userOpenai.name ?? "Command Code (OpenAI)",
|
|
5143
5171
|
options: {
|
|
@@ -5154,14 +5182,14 @@ var CommandCodePlugin = async (_input) => {
|
|
|
5154
5182
|
};
|
|
5155
5183
|
},
|
|
5156
5184
|
provider: {
|
|
5157
|
-
id:
|
|
5185
|
+
id: ANTHROPIC.id,
|
|
5158
5186
|
models: async (_provider, ctx) => {
|
|
5159
5187
|
const key = await resolveKey(async () => {
|
|
5160
5188
|
const a = ctx.auth;
|
|
5161
5189
|
return a?.type === "api" && a.key ? { key: a.key } : undefined;
|
|
5162
5190
|
});
|
|
5163
5191
|
const split = await loadModels(key);
|
|
5164
|
-
return toModelDefs(split.claude,
|
|
5192
|
+
return toModelDefs(split.claude, ANTHROPIC.id, PROVIDER_NPM);
|
|
5165
5193
|
}
|
|
5166
5194
|
},
|
|
5167
5195
|
tool: {
|
package/dist/sidebar/data.d.ts
CHANGED
|
@@ -33,8 +33,9 @@ export declare function loadMeta(plan?: OcPlan): Promise<Map<string, ModelMeta>>
|
|
|
33
33
|
* unreachable (see `requireSnapshot`). */
|
|
34
34
|
export declare function loadUsage(): Promise<Usage>;
|
|
35
35
|
/**
|
|
36
|
-
* One OpenCode Go / Zen snapshot from ocuse
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* One OpenCode Go / Zen snapshot from ocuse: opencode's own store for the
|
|
37
|
+
* per-model totals, plus OpenCode Go's usage endpoint for the account meters
|
|
38
|
+
* when the account has a Go key. Throws when ocuse is missing or fails, and the
|
|
39
|
+
* caller then omits the panel.
|
|
39
40
|
*/
|
|
40
41
|
export declare function loadZen(plan?: OcPlan): Promise<ZenUsage>;
|
package/dist/sidebar/rows.d.ts
CHANGED
|
@@ -140,7 +140,12 @@ export declare function until(epoch: number | undefined, now?: number): string;
|
|
|
140
140
|
* the OpenCode Go/Zen side adds the window's request count as `extra`, so
|
|
141
141
|
* `LIMIT EXCEEDED`, the tone, `cap in …`, `elapsed` and `resets` all have one
|
|
142
142
|
* implementation. */
|
|
143
|
-
export declare function windowRows(label: string, w: WindowUsage | undefined, now: number, durSecs: number, extra?: string
|
|
143
|
+
export declare function windowRows(label: string, w: WindowUsage | undefined, now: number, durSecs: number, extra?: string,
|
|
144
|
+
/** How the use line reads. The default is money (`$0.55/$14 (3.9%)`); the
|
|
145
|
+
* account's own meters publish shares rather than dollars and pass a percent
|
|
146
|
+
* formatter, so the tone, `LIMIT EXCEEDED`, `cap in …`, `elapsed` and
|
|
147
|
+
* `resets` stay this one implementation for both. */
|
|
148
|
+
use?: (used: number, cap: number, pct: number) => string): SidebarRow[];
|
|
144
149
|
/**
|
|
145
150
|
* The two store-backed rows for the active model — its requests under `Usage
|
|
146
151
|
* (this model)`, then this conversation's own totals under `Session`. They need
|
|
@@ -24,6 +24,21 @@ export declare function periodStart(usage: {
|
|
|
24
24
|
* while the session said 2.7K).
|
|
25
25
|
*/
|
|
26
26
|
export declare function loadModelUsage(modelID: string, sinceMs: number, path?: string, sessionID?: string): ModelUsage | null;
|
|
27
|
+
/**
|
|
28
|
+
* The oldest in-window assistant turn for one model, as epoch ms — the start of
|
|
29
|
+
* the rolling window the Go/Zen panel needs. A rolling 5-hour/weekly window has
|
|
30
|
+
* no reset recorded locally (the store carries none for these providers), but
|
|
31
|
+
* its clock does have a real start: the oldest request still inside it ages out
|
|
32
|
+
* at `start + window`, which is that window's honest reset. Null when the model has no turn in the
|
|
33
|
+
* window, or the store is missing/unreadable, so callers then omit the
|
|
34
|
+
* continuation exactly as before.
|
|
35
|
+
*
|
|
36
|
+
* Account-wide like ocuse's per-model window totals, so it is *not* scoped to a
|
|
37
|
+
* session: a rolling window counts the model's spend across every conversation.
|
|
38
|
+
* Matching is on the canonical key, the same normalisation `loadModelUsage` uses
|
|
39
|
+
* (the session id and the stored `modelID` differ by vendor prefix/punctuation).
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadModelWindowStart(modelID: string, sinceMs: number, path?: string): number | null;
|
|
27
42
|
/**
|
|
28
43
|
* Requests and cost for one session, so the panel can show what this
|
|
29
44
|
* conversation has spent rather than only the period-to-date figure. Null when
|
package/dist/sidebar/zen.d.ts
CHANGED
|
@@ -13,6 +13,25 @@ export interface ZenModel {
|
|
|
13
13
|
weekly: ZenTotals;
|
|
14
14
|
month: ZenTotals;
|
|
15
15
|
}
|
|
16
|
+
/** One of the account's own meters (`ocuse`'s `account` block). */
|
|
17
|
+
export interface ZenMeter {
|
|
18
|
+
/** The server's own word; anything but "ok" reads as over the limit. */
|
|
19
|
+
status: string;
|
|
20
|
+
/** Percent of the window's limit already used — the console's number. */
|
|
21
|
+
percent: number;
|
|
22
|
+
/** Reset instant, epoch ms. */
|
|
23
|
+
resetsAt?: number;
|
|
24
|
+
}
|
|
25
|
+
/** The account's three meters, under the endpoint's own names: `rolling` is the
|
|
26
|
+
* 5-hour window. Only present when ocuse reached OpenCode Go's usage endpoint,
|
|
27
|
+
* and then these — not the per-model figures — are what opencode.ai/console
|
|
28
|
+
* shows, because only the server knows its own window anchors and price
|
|
29
|
+
* basis. */
|
|
30
|
+
export interface ZenAccount {
|
|
31
|
+
rolling: ZenMeter;
|
|
32
|
+
weekly: ZenMeter;
|
|
33
|
+
monthly: ZenMeter;
|
|
34
|
+
}
|
|
16
35
|
export interface ZenUsage {
|
|
17
36
|
/**
|
|
18
37
|
* Billing period start, epoch ms: `periodStart` on current ocuse, or
|
|
@@ -29,14 +48,33 @@ export interface ZenUsage {
|
|
|
29
48
|
weekly: ZenTotals;
|
|
30
49
|
month: ZenTotals;
|
|
31
50
|
};
|
|
51
|
+
/** The account's own meters, when ocuse could ask for them. */
|
|
52
|
+
account?: ZenAccount;
|
|
32
53
|
/** Keyed by canonical model key, the same normalisation the CC side matches on. */
|
|
33
54
|
models: Map<string, ZenModel>;
|
|
34
55
|
}
|
|
35
56
|
/** Parse `ocuse -1 --json`. Anything malformed reads as "no usage". */
|
|
36
57
|
export declare function parseZenJson(text: string): ZenUsage;
|
|
58
|
+
/**
|
|
59
|
+
* The starts of the active model's rolling windows, epoch ms: the oldest request
|
|
60
|
+
* still inside each one, from opencode's own store. The Go/Zen panel turns each
|
|
61
|
+
* into a reset via `rollingReset`; absent when no such request exists.
|
|
62
|
+
*/
|
|
63
|
+
export interface ZenWindowStarts {
|
|
64
|
+
fiveHour?: number;
|
|
65
|
+
weekly?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A rolling window's reset, derived from the oldest request still inside it:
|
|
69
|
+
* that request leaves the window at `oldest + durSecs`, which is an honest reset
|
|
70
|
+
* epoch — unlike `now + durSecs`, a constant that would be noise in a usage
|
|
71
|
+
* panel. Undefined when there is no oldest request (an empty window), so the row
|
|
72
|
+
* keeps the same spend-only degrade it had before.
|
|
73
|
+
*/
|
|
74
|
+
export declare function rollingReset(startMs: number | undefined, durSecs: number): number | undefined;
|
|
37
75
|
/** The OpenCode Go / Zen panel: period windows, a rule, then the active model.
|
|
38
76
|
* Like `modelRows`, `fallbackName` (the session's own id) keeps the model block
|
|
39
77
|
* — and the local `Usage (this model)` / `Session` figures, which need no network
|
|
40
78
|
* — on screen when mpc's catalog is unreachable; only the catalog-derived rows
|
|
41
79
|
* then drop out. */
|
|
42
|
-
export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, modelUsage?: ModelUsage, session?: ModelUsage, fallbackName?: string): SidebarRow[];
|
|
80
|
+
export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, modelUsage?: ModelUsage, session?: ModelUsage, fallbackName?: string, windows?: ZenWindowStarts): SidebarRow[];
|
package/dist/tui.js
CHANGED
|
@@ -14,6 +14,7 @@ var OCUSE_BIN_CANDIDATES = ["ocuse", "/opt/homebrew/bin/ocuse", "/usr/local/bin/
|
|
|
14
14
|
|
|
15
15
|
// src/constants/cli.ts
|
|
16
16
|
var DEFAULT_ARGS = ["-1", "--plain"];
|
|
17
|
+
var MPC_DEFAULT_ARGS = ["--minimal"];
|
|
17
18
|
var SUBCOMMANDS = new Set(["plans", "models", "daily", "hourly", "model", "session", "statusline", "config"]);
|
|
18
19
|
var SPAWN_OPTIONS = {
|
|
19
20
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -31,6 +32,11 @@ function mpcCandidates(env = process.env) {
|
|
|
31
32
|
return candidates.filter((bin) => Boolean(bin));
|
|
32
33
|
}
|
|
33
34
|
var MPC_CANDIDATES = mpcCandidates();
|
|
35
|
+
function ocuseCandidates(env = process.env) {
|
|
36
|
+
const candidates = [env.OCUSE_BIN, ...OCUSE_BIN_CANDIDATES];
|
|
37
|
+
return candidates.filter((bin) => Boolean(bin));
|
|
38
|
+
}
|
|
39
|
+
var OCUSE_CANDIDATES = ocuseCandidates();
|
|
34
40
|
function splitCliArgs(input) {
|
|
35
41
|
const out = [];
|
|
36
42
|
let cur = "";
|
|
@@ -77,6 +83,12 @@ function buildCmduseArgs(arg) {
|
|
|
77
83
|
return tokens;
|
|
78
84
|
return [...DEFAULT_ARGS, ...tokens];
|
|
79
85
|
}
|
|
86
|
+
function buildMpcArgs(arg) {
|
|
87
|
+
const tokens = splitCliArgs(arg);
|
|
88
|
+
if (!tokens.length)
|
|
89
|
+
return [...MPC_DEFAULT_ARGS];
|
|
90
|
+
return tokens;
|
|
91
|
+
}
|
|
80
92
|
function stderrTail(text) {
|
|
81
93
|
const lines = text.trimEnd().split(`
|
|
82
94
|
`);
|
|
@@ -90,38 +102,55 @@ function runOnce(bin, args, opts, name = "cmduse") {
|
|
|
90
102
|
...process.env,
|
|
91
103
|
...opts.env ?? {}
|
|
92
104
|
},
|
|
93
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
105
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
+
...opts.spawnOptions
|
|
94
107
|
});
|
|
95
108
|
const out = [];
|
|
96
109
|
const err = [];
|
|
110
|
+
let settled = false;
|
|
111
|
+
const timer = opts.timeoutMs === undefined ? undefined : setTimeout(() => {
|
|
112
|
+
if (settled)
|
|
113
|
+
return;
|
|
114
|
+
settled = true;
|
|
115
|
+
child.kill();
|
|
116
|
+
reject(new Error(`${name} ${args.join(" ")}: timed out after ${opts.timeoutMs}ms`));
|
|
117
|
+
}, opts.timeoutMs);
|
|
97
118
|
const onAbort = () => child.kill("SIGTERM");
|
|
98
119
|
opts.signal?.addEventListener("abort", onAbort, {
|
|
99
120
|
once: true
|
|
100
121
|
});
|
|
122
|
+
const settle = (fn) => {
|
|
123
|
+
if (settled)
|
|
124
|
+
return;
|
|
125
|
+
settled = true;
|
|
126
|
+
if (timer)
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
129
|
+
fn();
|
|
130
|
+
};
|
|
101
131
|
child.stdout.on("data", (d) => out.push(d));
|
|
102
132
|
child.stderr.on("data", (d) => err.push(d));
|
|
103
|
-
child.on("error", (e) =>
|
|
104
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
105
|
-
reject(e);
|
|
106
|
-
});
|
|
133
|
+
child.on("error", (e) => settle(() => reject(e)));
|
|
107
134
|
child.on("close", (code, signal) => {
|
|
108
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
109
135
|
const stdout = Buffer.concat(out).toString("utf8");
|
|
110
136
|
if (opts.signal?.aborted) {
|
|
111
|
-
reject(new Error(`${name} invocation aborted`));
|
|
137
|
+
settle(() => reject(new Error(`${name} invocation aborted`)));
|
|
112
138
|
} else if (code === 0) {
|
|
113
|
-
resolve(stdout);
|
|
139
|
+
settle(() => resolve(stdout));
|
|
114
140
|
} else {
|
|
115
141
|
const why = signal ? `killed by ${signal}` : `exit ${code}`;
|
|
116
142
|
const tail = stderrTail(Buffer.concat(err).toString("utf8"));
|
|
117
|
-
reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
|
|
118
|
-
${tail}` : ""}`));
|
|
143
|
+
settle(() => reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
|
|
144
|
+
${tail}` : ""}`)));
|
|
119
145
|
}
|
|
120
146
|
});
|
|
121
147
|
});
|
|
122
148
|
}
|
|
149
|
+
function resolveArgs(arg, build) {
|
|
150
|
+
return typeof arg === "string" ? build(arg) : [...arg];
|
|
151
|
+
}
|
|
123
152
|
async function runCmduse(arg, opts = {}) {
|
|
124
|
-
const args =
|
|
153
|
+
const args = resolveArgs(arg, buildCmduseArgs);
|
|
125
154
|
const candidates = opts.candidates ?? CANDIDATES;
|
|
126
155
|
let last;
|
|
127
156
|
for (const bin of candidates) {
|
|
@@ -137,6 +166,40 @@ async function runCmduse(arg, opts = {}) {
|
|
|
137
166
|
cause: last
|
|
138
167
|
});
|
|
139
168
|
}
|
|
169
|
+
async function runMpc(arg, opts = {}) {
|
|
170
|
+
const args = resolveArgs(arg, buildMpcArgs);
|
|
171
|
+
const candidates = opts.candidates ?? MPC_CANDIDATES;
|
|
172
|
+
let last;
|
|
173
|
+
for (const bin of candidates) {
|
|
174
|
+
try {
|
|
175
|
+
return await runOnce(bin, args, opts, "mpc");
|
|
176
|
+
} catch (e) {
|
|
177
|
+
last = e;
|
|
178
|
+
if (e?.code !== "ENOENT")
|
|
179
|
+
throw e;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`mpc binary not found (install: bun add -g @jeffreyjyz/mpc)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, {
|
|
183
|
+
cause: last
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function runOcuse(arg, opts = {}) {
|
|
187
|
+
const args = resolveArgs(arg, splitCliArgs);
|
|
188
|
+
const candidates = opts.candidates ?? OCUSE_CANDIDATES;
|
|
189
|
+
let last;
|
|
190
|
+
for (const bin of candidates) {
|
|
191
|
+
try {
|
|
192
|
+
return await runOnce(bin, args, opts, "ocuse");
|
|
193
|
+
} catch (e) {
|
|
194
|
+
last = e;
|
|
195
|
+
if (e?.code !== "ENOENT")
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`ocuse binary not found (install: brew install JeffreyJYZ/tap/cmduse)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, {
|
|
200
|
+
cause: last
|
|
201
|
+
});
|
|
202
|
+
}
|
|
140
203
|
|
|
141
204
|
// src/constants/providers.ts
|
|
142
205
|
var PROVIDER_ID_PREFIX = "command-code";
|
|
@@ -144,6 +207,7 @@ var GO_PROVIDER_ID = "opencode-go";
|
|
|
144
207
|
var ZEN_PROVIDER_ID = "opencode";
|
|
145
208
|
var ZEN_PROVIDER_PREFIX = "opencode-zen";
|
|
146
209
|
var TUI_ID = "command-code.tui";
|
|
210
|
+
var CMD_USAGE_COMMAND_ID = "command-code.cmd-usage";
|
|
147
211
|
|
|
148
212
|
// src/sidebar/panel.tsx
|
|
149
213
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
@@ -621,7 +685,7 @@ function until(epoch, now = Date.now()) {
|
|
|
621
685
|
const days = Math.floor(hours / 24);
|
|
622
686
|
return hours % 24 ? `${days}d ${hours % 24}h` : `${days}d`;
|
|
623
687
|
}
|
|
624
|
-
function windowRows(label, w, now, durSecs, extra) {
|
|
688
|
+
function windowRows(label, w, now, durSecs, extra, use) {
|
|
625
689
|
if (!w || typeof w.cap !== "number" || w.cap <= 0)
|
|
626
690
|
return [];
|
|
627
691
|
const used = typeof w.used === "number" ? w.used : 0;
|
|
@@ -632,7 +696,7 @@ function windowRows(label, w, now, durSecs, extra) {
|
|
|
632
696
|
const reset = until(w.resetAt, now);
|
|
633
697
|
const lead = w.exceeded ? "LIMIT EXCEEDED" : eta === undefined ? elapsed === undefined ? "" : `${elapsed} elapsed` : `cap in ${until(now + eta * 1000, now)}`;
|
|
634
698
|
const detail = [lead, reset ? `resets ${reset}` : ""].filter(Boolean).join(" \xB7 ");
|
|
635
|
-
return [[label, `${money(used)}/${money(w.cap)} (${pct.toFixed(1)}%)`, w.exceeded ? "crit" : pctTone(pct)], ...detail ? [["", detail, "muted"]] : [], ...extra ? [["", extra, "muted"]] : []];
|
|
699
|
+
return [[label, use ? use(used, w.cap, pct) : `${money(used)}/${money(w.cap)} (${pct.toFixed(1)}%)`, w.exceeded ? "crit" : pctTone(pct)], ...detail ? [["", detail, "muted"]] : [], ...extra ? [["", extra, "muted"]] : []];
|
|
636
700
|
}
|
|
637
701
|
function localUsageRows(usage, session) {
|
|
638
702
|
const rows = [];
|
|
@@ -2027,7 +2091,6 @@ function minPlan(id) {
|
|
|
2027
2091
|
}
|
|
2028
2092
|
|
|
2029
2093
|
// src/sidebar/data.ts
|
|
2030
|
-
import { spawn as spawn2 } from "child_process";
|
|
2031
2094
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2032
2095
|
import { homedir as homedir3 } from "os";
|
|
2033
2096
|
import { dirname, join as join3 } from "path";
|
|
@@ -2042,6 +2105,14 @@ function totals(value) {
|
|
|
2042
2105
|
tokens: num("tokens")
|
|
2043
2106
|
};
|
|
2044
2107
|
}
|
|
2108
|
+
function meter(value) {
|
|
2109
|
+
const v = value ?? {};
|
|
2110
|
+
return {
|
|
2111
|
+
status: typeof v.status === "string" ? v.status : "ok",
|
|
2112
|
+
percent: typeof v.percent === "number" ? v.percent : 0,
|
|
2113
|
+
resetsAt: typeof v.resetsAt === "number" ? v.resetsAt : undefined
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2045
2116
|
function parseZenJson(text) {
|
|
2046
2117
|
let body;
|
|
2047
2118
|
try {
|
|
@@ -2067,6 +2138,11 @@ function parseZenJson(text) {
|
|
|
2067
2138
|
periodStart: typeof body.periodStartAt === "number" ? body.periodStartAt : typeof body.periodStart === "number" ? body.periodStart : undefined,
|
|
2068
2139
|
periodEndAt: typeof body.periodEndAt === "number" ? body.periodEndAt : undefined,
|
|
2069
2140
|
source: typeof body.source === "string" ? body.source : undefined,
|
|
2141
|
+
account: body.account ? {
|
|
2142
|
+
rolling: meter(body.account.rolling),
|
|
2143
|
+
weekly: meter(body.account.weekly),
|
|
2144
|
+
monthly: meter(body.account.monthly)
|
|
2145
|
+
} : undefined,
|
|
2070
2146
|
totals: {
|
|
2071
2147
|
fiveHour: totals(top.fiveHour),
|
|
2072
2148
|
weekly: totals(top.weekly),
|
|
@@ -2090,6 +2166,11 @@ function monthPeriod(usage) {
|
|
|
2090
2166
|
durSecs
|
|
2091
2167
|
};
|
|
2092
2168
|
}
|
|
2169
|
+
function rollingReset(startMs, durSecs) {
|
|
2170
|
+
if (typeof startMs !== "number" || !Number.isFinite(startMs))
|
|
2171
|
+
return;
|
|
2172
|
+
return startMs + durSecs * 1000;
|
|
2173
|
+
}
|
|
2093
2174
|
function zenWindow(label, spent, limit, share, now, durSecs, resetAt) {
|
|
2094
2175
|
const cap = typeof limit === "number" && limit > 0 ? limit * share : undefined;
|
|
2095
2176
|
if (cap === undefined) {
|
|
@@ -2102,19 +2183,33 @@ function zenWindow(label, spent, limit, share, now, durSecs, resetAt) {
|
|
|
2102
2183
|
exceeded: spent.cost >= cap
|
|
2103
2184
|
}, now, durSecs, spent.requests > 0 ? `${count(spent.requests)} req` : undefined);
|
|
2104
2185
|
}
|
|
2105
|
-
function
|
|
2186
|
+
function zenMeterRows(label, meter, now, durSecs) {
|
|
2187
|
+
return windowRows(label, {
|
|
2188
|
+
cap: 100,
|
|
2189
|
+
used: meter.percent,
|
|
2190
|
+
resetAt: meter.resetsAt,
|
|
2191
|
+
exceeded: meter.percent >= 100 || meter.status !== "ok"
|
|
2192
|
+
}, now, durSecs, undefined, (_used, _cap, pct) => `${pct.toFixed(1)}% of limit`);
|
|
2193
|
+
}
|
|
2194
|
+
function zenRows(usage, meta, modelUsage, session, fallbackName, windows) {
|
|
2106
2195
|
if (!usage)
|
|
2107
2196
|
return [];
|
|
2108
2197
|
const now = Date.now();
|
|
2109
2198
|
const oc = meta?.oc;
|
|
2110
2199
|
const rows = [["Plan", `${oc?.plan ?? "Go"} \xB7 limits per model`, "base"]];
|
|
2111
|
-
const mine = meta ? usage.models.get(modelKey(meta.name)) : undefined;
|
|
2200
|
+
const mine = usage.models.get(modelKey(fallbackName ?? "")) ?? (meta ? usage.models.get(modelKey(meta.name)) : undefined);
|
|
2112
2201
|
const limit = oc?.allowance ?? mine?.limit;
|
|
2113
2202
|
const cap = mine ? limit : undefined;
|
|
2114
|
-
rows.push(...zenWindow("5-hour", mine?.fiveHour ?? usage.totals.fiveHour, cap, GO_WINDOW_SHARE.fiveHour, now, FIVE_HOUR_SECS));
|
|
2115
|
-
rows.push(...zenWindow("Weekly", mine?.weekly ?? usage.totals.weekly, cap, GO_WINDOW_SHARE.weekly, now, WEEKLY_SECS));
|
|
2116
2203
|
const period = monthPeriod(usage);
|
|
2117
|
-
|
|
2204
|
+
if (usage.account) {
|
|
2205
|
+
rows.push(...zenMeterRows("5-hour", usage.account.rolling, now, FIVE_HOUR_SECS));
|
|
2206
|
+
rows.push(...zenMeterRows("Weekly", usage.account.weekly, now, WEEKLY_SECS));
|
|
2207
|
+
rows.push(...zenMeterRows("Monthly", usage.account.monthly, now, period?.durSecs ?? 0));
|
|
2208
|
+
} else {
|
|
2209
|
+
rows.push(...zenWindow("5-hour", mine?.fiveHour ?? usage.totals.fiveHour, cap, GO_WINDOW_SHARE.fiveHour, now, FIVE_HOUR_SECS, rollingReset(windows?.fiveHour, FIVE_HOUR_SECS)));
|
|
2210
|
+
rows.push(...zenWindow("Weekly", mine?.weekly ?? usage.totals.weekly, cap, GO_WINDOW_SHARE.weekly, now, WEEKLY_SECS, rollingReset(windows?.weekly, WEEKLY_SECS)));
|
|
2211
|
+
rows.push(...zenWindow("Monthly", mine?.month ?? usage.totals.month, cap, GO_WINDOW_SHARE.month, now, period?.durSecs ?? 0, period?.resetAt));
|
|
2212
|
+
}
|
|
2118
2213
|
rows.push(["Period", `${count(usage.totals.month.requests)} requests \xB7 ${money(usage.totals.month.cost)}`, "base"]);
|
|
2119
2214
|
rows.push(separator());
|
|
2120
2215
|
const name = meta?.name ?? fallbackName;
|
|
@@ -2122,11 +2217,11 @@ function zenRows(usage, meta, modelUsage, session, fallbackName) {
|
|
|
2122
2217
|
return rows;
|
|
2123
2218
|
rows.push(["Model", clip(name, valueWidth("Model")), "strong", true]);
|
|
2124
2219
|
rows.push(...localUsageRows(modelUsage, session));
|
|
2125
|
-
if (!meta)
|
|
2126
|
-
return rows;
|
|
2127
2220
|
if (typeof limit === "number") {
|
|
2128
2221
|
rows.push(["Allowance", allowanceLabel(limit, oc?.free), "base"]);
|
|
2129
2222
|
}
|
|
2223
|
+
if (!meta)
|
|
2224
|
+
return rows;
|
|
2130
2225
|
if (oc?.rates) {
|
|
2131
2226
|
const cache = [`cache read ${rate(oc.rates.cacheRead)}`];
|
|
2132
2227
|
if (typeof oc.rates.cacheWrite === "number" && oc.rates.cacheWrite > 0) {
|
|
@@ -2144,48 +2239,10 @@ function zenRows(usage, meta, modelUsage, session, fallbackName) {
|
|
|
2144
2239
|
}
|
|
2145
2240
|
|
|
2146
2241
|
// src/sidebar/data.ts
|
|
2147
|
-
var
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
return new Promise((resolve, reject) => {
|
|
2152
|
-
const child = spawn2(bin, args, SPAWN_OPTIONS);
|
|
2153
|
-
const out = [];
|
|
2154
|
-
const err = [];
|
|
2155
|
-
let settled = false;
|
|
2156
|
-
const timer = setTimeout(() => {
|
|
2157
|
-
if (settled)
|
|
2158
|
-
return;
|
|
2159
|
-
settled = true;
|
|
2160
|
-
child.kill();
|
|
2161
|
-
reject(new Error(`${bin} ${args.join(" ")}: timed out after ${CHILD_TIMEOUT_MS}ms`));
|
|
2162
|
-
}, CHILD_TIMEOUT_MS);
|
|
2163
|
-
const done = (fn) => {
|
|
2164
|
-
if (settled)
|
|
2165
|
-
return;
|
|
2166
|
-
settled = true;
|
|
2167
|
-
clearTimeout(timer);
|
|
2168
|
-
fn();
|
|
2169
|
-
};
|
|
2170
|
-
child.stdout?.on("data", (d) => out.push(d));
|
|
2171
|
-
child.stderr?.on("data", (d) => err.push(d));
|
|
2172
|
-
child.on("error", (e) => done(() => reject(e)));
|
|
2173
|
-
child.on("close", (code) => done(() => code === 0 ? resolve(Buffer.concat(out).toString("utf8")) : reject(new Error(`${bin} ${args.join(" ")}: exit ${code} ${Buffer.concat(err).toString("utf8").slice(-200)}`))));
|
|
2174
|
-
});
|
|
2175
|
-
}
|
|
2176
|
-
async function runFirst(candidates, args) {
|
|
2177
|
-
let last;
|
|
2178
|
-
for (const bin of candidates) {
|
|
2179
|
-
try {
|
|
2180
|
-
return await run(bin, args);
|
|
2181
|
-
} catch (e) {
|
|
2182
|
-
last = e;
|
|
2183
|
-
if (e?.code !== "ENOENT")
|
|
2184
|
-
throw e;
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
throw last instanceof Error ? last : new Error(`${candidates[0]} not found`);
|
|
2188
|
-
}
|
|
2242
|
+
var SIDEBAR_OPTS = {
|
|
2243
|
+
spawnOptions: SPAWN_OPTIONS,
|
|
2244
|
+
timeoutMs: CHILD_TIMEOUT_MS
|
|
2245
|
+
};
|
|
2189
2246
|
function parseUsageJson(text) {
|
|
2190
2247
|
const raw = JSON.parse(text);
|
|
2191
2248
|
const win = (v) => {
|
|
@@ -2271,7 +2328,7 @@ async function loadMeta(plan = OC_PLAN_DEFAULT) {
|
|
|
2271
2328
|
return new Map(cached.rows ?? []);
|
|
2272
2329
|
}
|
|
2273
2330
|
} catch {}
|
|
2274
|
-
const meta = parseMpcJson(await
|
|
2331
|
+
const meta = parseMpcJson(await runMpc(metaArgs(plan), SIDEBAR_OPTS));
|
|
2275
2332
|
try {
|
|
2276
2333
|
const path = cachePath(plan);
|
|
2277
2334
|
await mkdir(dirname(path), {
|
|
@@ -2285,10 +2342,10 @@ async function loadMeta(plan = OC_PLAN_DEFAULT) {
|
|
|
2285
2342
|
return meta;
|
|
2286
2343
|
}
|
|
2287
2344
|
async function loadUsage() {
|
|
2288
|
-
return requireSnapshot(parseUsageJson(await
|
|
2345
|
+
return requireSnapshot(parseUsageJson(await runCmduse(["-1", "--json", "--plain"], SIDEBAR_OPTS)));
|
|
2289
2346
|
}
|
|
2290
2347
|
async function loadZen(plan = OC_PLAN_DEFAULT) {
|
|
2291
|
-
return parseZenJson(await
|
|
2348
|
+
return parseZenJson(await runOcuse(zenArgs(plan), SIDEBAR_OPTS));
|
|
2292
2349
|
}
|
|
2293
2350
|
|
|
2294
2351
|
// src/sidebar/usageDb.ts
|
|
@@ -2375,6 +2432,53 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath(), sessionID) {
|
|
|
2375
2432
|
db.close();
|
|
2376
2433
|
}
|
|
2377
2434
|
}
|
|
2435
|
+
function loadModelWindowStart(modelID, sinceMs, path = usageDbPath()) {
|
|
2436
|
+
let db;
|
|
2437
|
+
try {
|
|
2438
|
+
db = new Database(path, {
|
|
2439
|
+
readonly: true
|
|
2440
|
+
});
|
|
2441
|
+
} catch {
|
|
2442
|
+
return null;
|
|
2443
|
+
}
|
|
2444
|
+
try {
|
|
2445
|
+
const key = modelKey(modelID);
|
|
2446
|
+
if (hasV2(db)) {
|
|
2447
|
+
const rows = db.query(`SELECT json_extract(data, '$.model.id') AS model,
|
|
2448
|
+
min(time_created) AS start
|
|
2449
|
+
FROM session_message
|
|
2450
|
+
WHERE type = 'assistant' AND time_created >= ?
|
|
2451
|
+
GROUP BY model`).all(sinceMs);
|
|
2452
|
+
for (const row of rows) {
|
|
2453
|
+
if (row.model && modelKey(row.model) === key)
|
|
2454
|
+
return row.start;
|
|
2455
|
+
}
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
const rows = db.query(`SELECT data, time_created FROM message
|
|
2459
|
+
WHERE data LIKE '%"tokens"%' AND time_created >= ?`).all(sinceMs);
|
|
2460
|
+
let oldest = null;
|
|
2461
|
+
for (const row of rows) {
|
|
2462
|
+
let data;
|
|
2463
|
+
try {
|
|
2464
|
+
data = JSON.parse(row.data);
|
|
2465
|
+
} catch {
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (data.role !== "assistant" || !data.tokens || !data.modelID)
|
|
2469
|
+
continue;
|
|
2470
|
+
if (modelKey(data.modelID) !== key)
|
|
2471
|
+
continue;
|
|
2472
|
+
if (oldest === null || row.time_created < oldest)
|
|
2473
|
+
oldest = row.time_created;
|
|
2474
|
+
}
|
|
2475
|
+
return oldest;
|
|
2476
|
+
} catch {
|
|
2477
|
+
return null;
|
|
2478
|
+
} finally {
|
|
2479
|
+
db.close();
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2378
2482
|
function loadSessionUsage(sessionID, path = usageDbPath()) {
|
|
2379
2483
|
let db;
|
|
2380
2484
|
try {
|
|
@@ -2485,6 +2589,7 @@ function useRows(activeModelId, providerID, sessionID) {
|
|
|
2485
2589
|
const [meta, setMeta] = createSignal(seededMeta());
|
|
2486
2590
|
const [modelUsage, setModelUsage] = createSignal(lastModelUsage);
|
|
2487
2591
|
const [sessionUsage, setSessionUsage] = createSignal();
|
|
2592
|
+
const [modelWindows, setModelWindows] = createSignal();
|
|
2488
2593
|
const [wasOurs, setWasOurs] = createSignal(false);
|
|
2489
2594
|
const refreshMeta = () => {
|
|
2490
2595
|
loadMeta(loadPrefs().ocPlan).then((next) => {
|
|
@@ -2505,6 +2610,15 @@ function useRows(activeModelId, providerID, sessionID) {
|
|
|
2505
2610
|
rememberModelUsage(id, fresh, session);
|
|
2506
2611
|
setModelUsage(lastModelUsage);
|
|
2507
2612
|
};
|
|
2613
|
+
const refreshModelWindows = (id) => {
|
|
2614
|
+
if (!id)
|
|
2615
|
+
return;
|
|
2616
|
+
const now = Date.now();
|
|
2617
|
+
setModelWindows({
|
|
2618
|
+
fiveHour: loadModelWindowStart(id, now - FIVE_HOUR_SECS * 1000) ?? undefined,
|
|
2619
|
+
weekly: loadModelWindowStart(id, now - WEEKLY_SECS * 1000) ?? undefined
|
|
2620
|
+
});
|
|
2621
|
+
};
|
|
2508
2622
|
let inFlight = false;
|
|
2509
2623
|
const refresh = async () => {
|
|
2510
2624
|
if (inFlight)
|
|
@@ -2524,6 +2638,7 @@ function useRows(activeModelId, providerID, sessionID) {
|
|
|
2524
2638
|
} else if (kind === "go" || kind === "zen") {
|
|
2525
2639
|
setZen(await loadZen(loadPrefs().ocPlan));
|
|
2526
2640
|
refreshModelUsage(activeModelId());
|
|
2641
|
+
refreshModelWindows(activeModelId());
|
|
2527
2642
|
}
|
|
2528
2643
|
} catch {} finally {
|
|
2529
2644
|
inFlight = false;
|
|
@@ -2541,7 +2656,12 @@ function useRows(activeModelId, providerID, sessionID) {
|
|
|
2541
2656
|
createEffect(() => {
|
|
2542
2657
|
const id = activeModelId();
|
|
2543
2658
|
const session = sessionID();
|
|
2544
|
-
|
|
2659
|
+
const kind = sessionKind(providerID());
|
|
2660
|
+
if (!shows(kind) || !id)
|
|
2661
|
+
return;
|
|
2662
|
+
if (kind === "go" || kind === "zen")
|
|
2663
|
+
refreshModelWindows(id);
|
|
2664
|
+
if (!session)
|
|
2545
2665
|
return;
|
|
2546
2666
|
refreshModelUsage(id);
|
|
2547
2667
|
});
|
|
@@ -2561,7 +2681,7 @@ function useRows(activeModelId, providerID, sessionID) {
|
|
|
2561
2681
|
const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
|
|
2562
2682
|
const fallbackName = id ? id.split("/").pop() ?? id : undefined;
|
|
2563
2683
|
if (kind === "go" || kind === "zen") {
|
|
2564
|
-
return zenRows(zen(), found, usageFor(id), sessionUsage(), fallbackName);
|
|
2684
|
+
return zenRows(zen(), found, usageFor(id), sessionUsage(), fallbackName, modelWindows());
|
|
2565
2685
|
}
|
|
2566
2686
|
const model = found ? {
|
|
2567
2687
|
...found,
|
|
@@ -2629,7 +2749,7 @@ var commandCodeTui = {
|
|
|
2629
2749
|
mode: "global",
|
|
2630
2750
|
priority: 10,
|
|
2631
2751
|
commands: [{
|
|
2632
|
-
id:
|
|
2752
|
+
id: CMD_USAGE_COMMAND_ID,
|
|
2633
2753
|
title: "Command Code usage",
|
|
2634
2754
|
group: "Command Code",
|
|
2635
2755
|
slash: {
|
|
@@ -2656,7 +2776,7 @@ var commandCodeTui = {
|
|
|
2656
2776
|
});
|
|
2657
2777
|
}
|
|
2658
2778
|
}],
|
|
2659
|
-
bindings: [
|
|
2779
|
+
bindings: [CMD_USAGE_COMMAND_ID]
|
|
2660
2780
|
}));
|
|
2661
2781
|
}
|
|
2662
2782
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffreyjyz/opencode-command-code",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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",
|