@jeffreyjyz/opencode-command-code 0.2.7 → 0.2.9
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/sidebar/data.d.ts +7 -0
- package/dist/sidebar/rows.d.ts +8 -2
- package/dist/sidebar/usageDb.d.ts +16 -0
- package/dist/tui.js +92 -12
- package/package.json +1 -1
package/dist/sidebar/data.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { type ModelMeta, type Usage } from "./rows";
|
|
3
|
+
/** `detached` is load-bearing, not tidiness: it starts the child in its own
|
|
4
|
+
* session, so it has no controlling terminal. cmduse's snapshot() paints a
|
|
5
|
+
* "fetching usage…" spinner straight to /dev/tty — piping stdout/stderr does
|
|
6
|
+
* not stop it — which would corrupt the opencode TUI this process inherits its
|
|
7
|
+
* tty from. Detaching makes that open fail; stdout/stderr stay piped. */
|
|
8
|
+
export declare const SPAWN_OPTIONS: Parameters<typeof spawn>[2];
|
|
2
9
|
/** Parse `cmduse -1 --json` into the usage snapshot. */
|
|
3
10
|
export declare function parseUsageJson(text: string): Usage;
|
|
4
11
|
/** Parse `mpc --json` into per-model meta, CommandCode side only. */
|
package/dist/sidebar/rows.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ export interface WindowUsage {
|
|
|
19
19
|
used?: number;
|
|
20
20
|
resetAt?: number;
|
|
21
21
|
}
|
|
22
|
+
/** One model's usage for the account's current billing period. */
|
|
23
|
+
export interface ModelUsage {
|
|
24
|
+
requests: number;
|
|
25
|
+
cost: number;
|
|
26
|
+
}
|
|
22
27
|
export interface Usage {
|
|
23
28
|
plan?: string;
|
|
24
29
|
monthlyCap?: number;
|
|
@@ -38,8 +43,9 @@ export declare function count(value: number): string;
|
|
|
38
43
|
export declare function until(epoch: number | undefined, now?: number): string;
|
|
39
44
|
/** Plan + rolling windows + period totals, from a cmduse snapshot. */
|
|
40
45
|
export declare function usageRows(usage: Usage | undefined, now?: number): SidebarRow[];
|
|
41
|
-
/** Active model's allowance, rates and benchmarks, from mpc's catalog
|
|
42
|
-
|
|
46
|
+
/** Active model's allowance, rates and benchmarks, from mpc's catalog; its
|
|
47
|
+
* period usage (when the store has it) rides directly under the model name. */
|
|
48
|
+
export declare function modelRows(meta: ModelMeta | undefined, usage?: ModelUsage): SidebarRow[];
|
|
43
49
|
/**
|
|
44
50
|
* Canonical key for a model id, mirroring mpc's normalizeKey: lowercase, drop
|
|
45
51
|
* the vendor prefix and any parenthetical, strip punctuation. Used to match the
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ModelUsage } from "./rows";
|
|
2
|
+
/** opencode's message store: `OPENCODE_DB`, else the XDG data dir. */
|
|
3
|
+
export declare function usageDbPath(): string;
|
|
4
|
+
/**
|
|
5
|
+
* Start of the billing period a cmduse snapshot reports, as epoch ms. cmduse
|
|
6
|
+
* publishes only `periodEnd` (YYYY-MM-DD), so the start is that date one
|
|
7
|
+
* calendar month back. Falls back to the last 30 days when it is missing.
|
|
8
|
+
*/
|
|
9
|
+
export declare function periodStart(periodEnd: string | undefined, now?: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Requests and cost for one model since `sinceMs`. Null when the store is
|
|
12
|
+
* missing or unreadable, so callers can fall back to omitting the row.
|
|
13
|
+
* Matching is on the canonical key: the session id and the stored modelID can
|
|
14
|
+
* differ by vendor prefix/punctuation.
|
|
15
|
+
*/
|
|
16
|
+
export declare function loadModelUsage(modelID: string, sinceMs: number, path?: string): ModelUsage | null;
|
package/dist/tui.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
|
9
9
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
10
10
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
11
11
|
import { Plugin } from "@opencode/plugin/tui";
|
|
12
|
-
import { For, Show, createMemo, createSignal, onCleanup } from "solid-js";
|
|
12
|
+
import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
|
|
13
13
|
|
|
14
14
|
// src/cli.ts
|
|
15
15
|
import { spawn } from "child_process";
|
|
@@ -462,10 +462,14 @@ function usageRows(usage, now = Date.now()) {
|
|
|
462
462
|
}
|
|
463
463
|
return rows;
|
|
464
464
|
}
|
|
465
|
-
function modelRows(meta) {
|
|
465
|
+
function modelRows(meta, usage) {
|
|
466
466
|
if (!meta)
|
|
467
467
|
return [];
|
|
468
468
|
const rows = [["Model", meta.name]];
|
|
469
|
+
if (usage) {
|
|
470
|
+
const spent = usage.cost > 0 ? ` \xB7 ${money(usage.cost)}` : "";
|
|
471
|
+
rows.push(["Usage", `${count(usage.requests)} req${spent}`]);
|
|
472
|
+
}
|
|
469
473
|
if (meta.tier)
|
|
470
474
|
rows.push(["Tier", TIER_DISPLAY[meta.tier]]);
|
|
471
475
|
if (typeof meta.allowance === "number")
|
|
@@ -493,15 +497,17 @@ function tierFor(modelId) {
|
|
|
493
497
|
var CMDUSE = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
|
|
494
498
|
var MPC = ["mpc", join(homedir(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
|
|
495
499
|
var CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
500
|
+
var SPAWN_OPTIONS = {
|
|
501
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
502
|
+
detached: true
|
|
503
|
+
};
|
|
496
504
|
function run(bin, args) {
|
|
497
505
|
return new Promise((resolve, reject) => {
|
|
498
|
-
const child = spawn2(bin, args,
|
|
499
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
500
|
-
});
|
|
506
|
+
const child = spawn2(bin, args, SPAWN_OPTIONS);
|
|
501
507
|
const out = [];
|
|
502
508
|
const err = [];
|
|
503
|
-
child.stdout
|
|
504
|
-
child.stderr
|
|
509
|
+
child.stdout?.on("data", (d) => out.push(d));
|
|
510
|
+
child.stderr?.on("data", (d) => err.push(d));
|
|
505
511
|
child.on("error", reject);
|
|
506
512
|
child.on("close", (code) => code === 0 ? resolve(Buffer.concat(out).toString("utf8")) : reject(new Error(`${bin} ${args.join(" ")}: exit ${code} ${Buffer.concat(err).toString("utf8").slice(-200)}`)));
|
|
507
513
|
});
|
|
@@ -591,6 +597,72 @@ async function loadUsage() {
|
|
|
591
597
|
return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
|
|
592
598
|
}
|
|
593
599
|
|
|
600
|
+
// src/sidebar/usageDb.ts
|
|
601
|
+
import { Database } from "bun:sqlite";
|
|
602
|
+
import { homedir as homedir2 } from "os";
|
|
603
|
+
import { join as join2 } from "path";
|
|
604
|
+
function usageDbPath() {
|
|
605
|
+
if (process.env.OPENCODE_DB)
|
|
606
|
+
return process.env.OPENCODE_DB;
|
|
607
|
+
const base = process.env.XDG_DATA_HOME ?? join2(homedir2(), ".local", "share");
|
|
608
|
+
return join2(base, "opencode", "opencode.db");
|
|
609
|
+
}
|
|
610
|
+
function periodStart(periodEnd, now = Date.now()) {
|
|
611
|
+
const end = periodEnd ? Date.parse(`${periodEnd}T23:59:59`) : Number.NaN;
|
|
612
|
+
if (!Number.isFinite(end))
|
|
613
|
+
return now - 30 * 24 * 60 * 60 * 1000;
|
|
614
|
+
const start = new Date(end);
|
|
615
|
+
start.setMonth(start.getMonth() - 1);
|
|
616
|
+
return start.getTime();
|
|
617
|
+
}
|
|
618
|
+
function usage(value) {
|
|
619
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
620
|
+
}
|
|
621
|
+
function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
|
|
622
|
+
let db;
|
|
623
|
+
try {
|
|
624
|
+
db = new Database(path, {
|
|
625
|
+
readonly: true
|
|
626
|
+
});
|
|
627
|
+
} catch {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const rows = db.query(`SELECT data, time_created FROM message
|
|
632
|
+
WHERE data LIKE '%"tokens"%' AND time_created >= ?`).all(sinceMs);
|
|
633
|
+
const key = modelKey(modelID);
|
|
634
|
+
const seen = new Set;
|
|
635
|
+
let requests = 0;
|
|
636
|
+
let cost = 0;
|
|
637
|
+
for (const row of rows) {
|
|
638
|
+
let data;
|
|
639
|
+
try {
|
|
640
|
+
data = JSON.parse(row.data);
|
|
641
|
+
} catch {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (data.role !== "assistant" || !data.modelID || !data.tokens)
|
|
645
|
+
continue;
|
|
646
|
+
if (modelKey(data.modelID) !== key)
|
|
647
|
+
continue;
|
|
648
|
+
const id = `${data.modelID}@${row.time_created}`;
|
|
649
|
+
if (seen.has(id))
|
|
650
|
+
continue;
|
|
651
|
+
seen.add(id);
|
|
652
|
+
requests += 1;
|
|
653
|
+
cost += usage(data.cost);
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
requests,
|
|
657
|
+
cost
|
|
658
|
+
};
|
|
659
|
+
} catch {
|
|
660
|
+
return null;
|
|
661
|
+
} finally {
|
|
662
|
+
db.close();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
594
666
|
// src/tui.tsx
|
|
595
667
|
var ID = "command-code.tui";
|
|
596
668
|
var POLL_MS = 30000;
|
|
@@ -626,15 +698,23 @@ function Panel(props) {
|
|
|
626
698
|
function useRows(activeModelId, active) {
|
|
627
699
|
const [usage, setUsage] = createSignal([]);
|
|
628
700
|
const [meta, setMeta] = createSignal(new Map);
|
|
701
|
+
const [modelUsage, setModelUsage] = createSignal();
|
|
629
702
|
loadMeta().then(setMeta).catch(() => {});
|
|
630
703
|
const refresh = async () => {
|
|
631
704
|
try {
|
|
632
|
-
|
|
705
|
+
const snapshot = await loadUsage();
|
|
706
|
+
setUsage(usageRows(snapshot));
|
|
707
|
+
const id = activeModelId();
|
|
708
|
+
setModelUsage(id ? loadModelUsage(id, periodStart(snapshot.periodEnd)) ?? undefined : undefined);
|
|
633
709
|
} catch {}
|
|
634
710
|
};
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
711
|
+
createEffect(() => {
|
|
712
|
+
if (!active())
|
|
713
|
+
return;
|
|
714
|
+
refresh();
|
|
715
|
+
const timer = setInterval(() => void refresh(), POLL_MS);
|
|
716
|
+
onCleanup(() => clearInterval(timer));
|
|
717
|
+
});
|
|
638
718
|
return createMemo(() => {
|
|
639
719
|
if (!active())
|
|
640
720
|
return [];
|
|
@@ -644,7 +724,7 @@ function useRows(activeModelId, active) {
|
|
|
644
724
|
...found,
|
|
645
725
|
tier: found.tier ?? (id ? tierFor(id) : undefined)
|
|
646
726
|
} : undefined;
|
|
647
|
-
return [...usage(), ...modelRows(model)];
|
|
727
|
+
return [...usage(), ...modelRows(model, modelUsage())];
|
|
648
728
|
});
|
|
649
729
|
}
|
|
650
730
|
var isOurs = (providerID) => Boolean(providerID?.startsWith("command-code"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffreyjyz/opencode-command-code",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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",
|