@jeffreyjyz/opencode-command-code 0.3.11 → 0.3.13
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 +1 -1
- package/dist/sidebar/data.d.ts +12 -1
- package/dist/sidebar/panel.d.ts +1 -0
- package/dist/sidebar/rows.d.ts +22 -2
- package/dist/sidebar/usageDb.d.ts +14 -1
- package/dist/sidebar/useRows.d.ts +8 -6
- package/dist/sidebar/windows.d.ts +11 -0
- package/dist/sidebar/zen.d.ts +36 -0
- package/dist/tui.js +271 -43
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4063,7 +4063,7 @@ var commandCodeV2 = {
|
|
|
4063
4063
|
// package.json
|
|
4064
4064
|
var package_default = {
|
|
4065
4065
|
name: "@jeffreyjyz/opencode-command-code",
|
|
4066
|
-
version: "0.3.
|
|
4066
|
+
version: "0.3.13",
|
|
4067
4067
|
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.",
|
|
4068
4068
|
type: "module",
|
|
4069
4069
|
main: "dist/index.js",
|
package/dist/sidebar/data.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { type ModelMeta, type Usage } from "./rows";
|
|
3
|
+
import { type ZenUsage } from "./zen";
|
|
3
4
|
/** `detached` is load-bearing, not tidiness: it starts the child in its own
|
|
4
5
|
* session, so it has no controlling terminal. cmduse's snapshot() paints a
|
|
5
6
|
* "fetching usage…" spinner straight to /dev/tty — piping stdout/stderr does
|
|
@@ -8,9 +9,19 @@ import { type ModelMeta, type Usage } from "./rows";
|
|
|
8
9
|
export declare const SPAWN_OPTIONS: Parameters<typeof spawn>[2];
|
|
9
10
|
/** Parse `cmduse -1 --json` into the usage snapshot. */
|
|
10
11
|
export declare function parseUsageJson(text: string): Usage;
|
|
11
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Parse `mpc --json` into per-model meta: the CommandCode fields at the top
|
|
14
|
+
* level and the OpenCode Go / Zen side under `oc`, so one lookup serves either
|
|
15
|
+
* kind of session. Rows that carry only one side are kept.
|
|
16
|
+
*/
|
|
12
17
|
export declare function parseMpcJson(text: string): Map<string, ModelMeta>;
|
|
13
18
|
/** mpc's catalog, cached on disk: it scrapes live docs over the network. */
|
|
14
19
|
export declare function loadMeta(): Promise<Map<string, ModelMeta>>;
|
|
15
20
|
/** One usage snapshot from cmduse. Throws when cmduse is missing or fails. */
|
|
16
21
|
export declare function loadUsage(): Promise<Usage>;
|
|
22
|
+
/**
|
|
23
|
+
* One OpenCode Go / Zen snapshot from ocuse, which reads opencode's own store
|
|
24
|
+
* (those providers have no usage API). Throws when ocuse is missing or fails,
|
|
25
|
+
* and the caller then omits the panel.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadZen(): Promise<ZenUsage>;
|
package/dist/sidebar/panel.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export declare function legacyColors(theme: {
|
|
|
45
45
|
export declare function mono(colors: PanelColors): PanelColors;
|
|
46
46
|
/** Bold title, then one line per row. Renders nothing when there are no rows. */
|
|
47
47
|
export declare function Panel(props: {
|
|
48
|
+
title: () => string;
|
|
48
49
|
rows: () => SidebarRow[];
|
|
49
50
|
colors: () => PanelColors;
|
|
50
51
|
}): import("solid-js").JSX.Element;
|
package/dist/sidebar/rows.d.ts
CHANGED
|
@@ -51,11 +51,30 @@ export interface ModelMeta {
|
|
|
51
51
|
};
|
|
52
52
|
intelligence?: number;
|
|
53
53
|
tps?: number;
|
|
54
|
+
/**
|
|
55
|
+
* The OpenCode Go / Zen side of the same mpc row (0.4.0): the CommandCode
|
|
56
|
+
* fields above are the default, this is what a Go/Zen session reads.
|
|
57
|
+
*/
|
|
58
|
+
oc?: {
|
|
59
|
+
provider?: string;
|
|
60
|
+
plan?: string;
|
|
61
|
+
allowance?: number;
|
|
62
|
+
rates?: {
|
|
63
|
+
input: number;
|
|
64
|
+
output: number;
|
|
65
|
+
cacheRead: number;
|
|
66
|
+
cacheWrite?: number | null;
|
|
67
|
+
};
|
|
68
|
+
ability?: number;
|
|
69
|
+
tps?: number;
|
|
70
|
+
};
|
|
54
71
|
}
|
|
55
72
|
export interface WindowUsage {
|
|
56
73
|
cap?: number;
|
|
57
74
|
used?: number;
|
|
58
75
|
resetAt?: number;
|
|
76
|
+
/** The account API's own over-cap flag (cmduse passes it through). */
|
|
77
|
+
exceeded?: boolean;
|
|
59
78
|
}
|
|
60
79
|
/** One model's usage for the account's current billing period. */
|
|
61
80
|
export interface ModelUsage {
|
|
@@ -85,8 +104,9 @@ export declare function until(epoch: number | undefined, now?: number): string;
|
|
|
85
104
|
/** Plan + rolling windows + period totals, from a cmduse snapshot. */
|
|
86
105
|
export declare function usageRows(usage: Usage | undefined, now?: number): SidebarRow[];
|
|
87
106
|
/** Active model's allowance, rates and benchmarks, from mpc's catalog; its
|
|
88
|
-
* period usage (when the store has it) rides directly under the model name
|
|
89
|
-
|
|
107
|
+
* period usage (when the store has it) rides directly under the model name, and
|
|
108
|
+
* this session's own totals under that (0.4.0). */
|
|
109
|
+
export declare function modelRows(meta: ModelMeta | undefined, usage?: ModelUsage, session?: ModelUsage): SidebarRow[];
|
|
90
110
|
/**
|
|
91
111
|
* Canonical key for a model id, mirroring mpc's normalizeKey: lowercase, drop
|
|
92
112
|
* the vendor prefix and any parenthetical, strip punctuation. Used to match the
|
|
@@ -16,5 +16,18 @@ export declare function periodStart(usage: {
|
|
|
16
16
|
* missing or unreadable, so callers can fall back to omitting the row.
|
|
17
17
|
* Matching is on the canonical key: the session id and the stored modelID can
|
|
18
18
|
* differ by vendor prefix/punctuation.
|
|
19
|
+
*
|
|
20
|
+
* Given a `sessionID`, the scan is scoped to that conversation and `sinceMs` is
|
|
21
|
+
* ignored — the panel shows "this model in this session" beside the session's
|
|
22
|
+
* own total, and two figures measuring different windows than each other read
|
|
23
|
+
* as a bug (the billing period had just rolled over, so the model row said 129
|
|
24
|
+
* while the session said 2.7K).
|
|
19
25
|
*/
|
|
20
|
-
export declare function loadModelUsage(modelID: string, sinceMs: number, path?: string): ModelUsage | null;
|
|
26
|
+
export declare function loadModelUsage(modelID: string, sinceMs: number, path?: string, sessionID?: string): ModelUsage | null;
|
|
27
|
+
/**
|
|
28
|
+
* Requests and cost for one session, so the panel can show what this
|
|
29
|
+
* conversation has spent rather than only the period-to-date figure. Null when
|
|
30
|
+
* the store is missing or unreadable, and callers then omit the row. Rows are
|
|
31
|
+
* keyed by their primary key: message rows get rewritten in place.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadSessionUsage(sessionID: string, path?: string): ModelUsage | null;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { type ModelUsage, type SidebarRow, type Usage } from "./rows";
|
|
2
|
-
/** Which provider a session is on, as far as this panel is concerned. */
|
|
3
|
-
export type SessionKind = "ours" | "other" | "unknown";
|
|
4
2
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Which family a session's provider belongs to, as far as this panel is
|
|
4
|
+
* concerned. `unknown` is the frame during a session switch, before the host has
|
|
5
|
+
* resolved the new session's model; it must not be read as someone else's, or
|
|
6
|
+
* the panel blanks and refetches on every tab change.
|
|
8
7
|
*/
|
|
8
|
+
export type SessionKind = "ours" | "go" | "zen" | "other" | "unknown";
|
|
9
9
|
export declare function sessionKind(providerID: string | undefined): SessionKind;
|
|
10
|
+
/** The section's title, per family. */
|
|
11
|
+
export declare function panelTitle(kind: SessionKind): string;
|
|
10
12
|
/** Keep an account snapshot for the next panel that needs it. */
|
|
11
13
|
export declare function rememberSnapshot(snapshot: Usage): void;
|
|
12
14
|
/** Rows to paint before this panel's first fetch of the session completes. */
|
|
@@ -22,4 +24,4 @@ export declare function rememberModelUsage(id: string, usage: ModelUsage): void;
|
|
|
22
24
|
* beats blanking it, the same call `wasOurs` makes for the account block.
|
|
23
25
|
*/
|
|
24
26
|
export declare function seededModelUsage(id: string | undefined): ModelUsage | undefined;
|
|
25
|
-
export declare function useRows(activeModelId: () => string | undefined, providerID: () => string | undefined): import("solid-js").Accessor<SidebarRow[]>;
|
|
27
|
+
export declare function useRows(activeModelId: () => string | undefined, providerID: () => string | undefined, sessionID: () => string | undefined): import("solid-js").Accessor<SidebarRow[]>;
|
|
@@ -14,3 +14,14 @@ export declare function elapsedPct(resetAtMs: number | undefined, durSecs: numbe
|
|
|
14
14
|
* Exact zero stays "0%".
|
|
15
15
|
*/
|
|
16
16
|
export declare function elapsedLabel(resetAtMs: number | undefined, durSecs: number, nowSecs: number): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Seconds until the cap is reached at the window's current burn rate, or
|
|
19
|
+
* undefined when there is nothing honest to warn about: no reset, a window that
|
|
20
|
+
* has not started or is already rolling over, under a tenth elapsed (a flat-rate
|
|
21
|
+
* projection off a few seconds of data is noise), no spend yet, a cap already
|
|
22
|
+
* reached, or a cap that would land after the reset anyway.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors `cmduse_core::pace_eta`; the `paceEta` vectors in the shared
|
|
25
|
+
* `core/conformance.json` are asserted against this in test/conformance.test.ts.
|
|
26
|
+
*/
|
|
27
|
+
export declare function paceEtaSecs(resetAtMs: number | undefined, durSecs: number, used: number, cap: number, nowSecs: number): number | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ModelMeta, type ModelUsage, type SidebarRow } from "./rows";
|
|
2
|
+
/** Go's documented per-model window shares of the monthly allowance. */
|
|
3
|
+
export declare const GO_WINDOW_SHARE: {
|
|
4
|
+
readonly fiveHour: 0.2;
|
|
5
|
+
readonly weekly: 0.5;
|
|
6
|
+
readonly month: 1;
|
|
7
|
+
};
|
|
8
|
+
export interface ZenTotals {
|
|
9
|
+
requests: number;
|
|
10
|
+
cost: number;
|
|
11
|
+
tokens: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ZenModel {
|
|
14
|
+
id: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
/** Monthly allowance the docs give this model, when they give one. */
|
|
17
|
+
limit?: number;
|
|
18
|
+
fiveHour: ZenTotals;
|
|
19
|
+
weekly: ZenTotals;
|
|
20
|
+
month: ZenTotals;
|
|
21
|
+
}
|
|
22
|
+
export interface ZenUsage {
|
|
23
|
+
periodStart?: number;
|
|
24
|
+
source?: string;
|
|
25
|
+
totals: {
|
|
26
|
+
fiveHour: ZenTotals;
|
|
27
|
+
weekly: ZenTotals;
|
|
28
|
+
month: ZenTotals;
|
|
29
|
+
};
|
|
30
|
+
/** Keyed by canonical model key, the same normalisation the CC side matches on. */
|
|
31
|
+
models: Map<string, ZenModel>;
|
|
32
|
+
}
|
|
33
|
+
/** Parse `ocuse -1 --json`. Anything malformed reads as "no usage". */
|
|
34
|
+
export declare function parseZenJson(text: string): ZenUsage;
|
|
35
|
+
/** The OpenCode Go / Zen panel: period windows, a rule, then the active model. */
|
|
36
|
+
export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, session?: ModelUsage): SidebarRow[];
|
package/dist/tui.js
CHANGED
|
@@ -120,7 +120,6 @@ async function runCmduse(arg, opts = {}) {
|
|
|
120
120
|
|
|
121
121
|
// src/sidebar/panel.tsx
|
|
122
122
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
123
|
-
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
124
123
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
125
124
|
import { spread as _$spread } from "@opentui/solid";
|
|
126
125
|
import { mergeProps as _$mergeProps } from "@opentui/solid";
|
|
@@ -469,6 +468,26 @@ function elapsedLabel(resetAtMs, durSecs, nowSecs) {
|
|
|
469
468
|
return "<1%";
|
|
470
469
|
return `${pct}%`;
|
|
471
470
|
}
|
|
471
|
+
function paceEtaSecs(resetAtMs, durSecs, used, cap, nowSecs) {
|
|
472
|
+
if (typeof resetAtMs !== "number" || !Number.isFinite(resetAtMs))
|
|
473
|
+
return;
|
|
474
|
+
const reset = Math.floor(resetAtMs / 1000);
|
|
475
|
+
if (reset < durSecs)
|
|
476
|
+
return;
|
|
477
|
+
const start = reset - durSecs;
|
|
478
|
+
if (nowSecs <= start || nowSecs >= reset)
|
|
479
|
+
return;
|
|
480
|
+
const elapsed = nowSecs - start;
|
|
481
|
+
if (elapsed / durSecs < 0.1)
|
|
482
|
+
return;
|
|
483
|
+
const rate = used / elapsed;
|
|
484
|
+
if (rate <= 0 || used >= cap)
|
|
485
|
+
return;
|
|
486
|
+
const secsToCap = (cap - used) / rate;
|
|
487
|
+
if (secsToCap >= reset - nowSecs)
|
|
488
|
+
return;
|
|
489
|
+
return secsToCap;
|
|
490
|
+
}
|
|
472
491
|
|
|
473
492
|
// src/sidebar/rows.ts
|
|
474
493
|
function pctTone(pct) {
|
|
@@ -533,10 +552,13 @@ function windowRows(label, w, now, durSecs) {
|
|
|
533
552
|
return [];
|
|
534
553
|
const used = typeof w.used === "number" ? w.used : 0;
|
|
535
554
|
const pct = Math.round(used / w.cap * 100);
|
|
536
|
-
const
|
|
555
|
+
const nowSecs = Math.floor(now / 1000);
|
|
556
|
+
const elapsed = elapsedLabel(w.resetAt, durSecs, nowSecs);
|
|
557
|
+
const eta = paceEtaSecs(w.resetAt, durSecs, used, w.cap, nowSecs);
|
|
537
558
|
const reset = until(w.resetAt, now);
|
|
538
|
-
const
|
|
539
|
-
|
|
559
|
+
const lead = w.exceeded ? "LIMIT EXCEEDED" : eta === undefined ? elapsed === undefined ? "" : `${elapsed} elapsed` : `cap in ${until(now + eta * 1000, now)}`;
|
|
560
|
+
const detail = [lead, reset ? `resets ${reset}` : ""].filter(Boolean).join(" \xB7 ");
|
|
561
|
+
return [[label, `${money(used)}/${money(w.cap)} (${pct}%)`, w.exceeded ? "crit" : pctTone(pct)], ...detail ? [["", detail, "muted"]] : []];
|
|
540
562
|
}
|
|
541
563
|
function monthlyDetail(usage, now) {
|
|
542
564
|
const {
|
|
@@ -581,7 +603,7 @@ function usageRows(usage, now = Date.now()) {
|
|
|
581
603
|
}
|
|
582
604
|
return rows;
|
|
583
605
|
}
|
|
584
|
-
function modelRows(meta, usage) {
|
|
606
|
+
function modelRows(meta, usage, session) {
|
|
585
607
|
if (!meta)
|
|
586
608
|
return [];
|
|
587
609
|
const rows = [["Model", clip(meta.name, valueWidth("Model")), "strong", true]];
|
|
@@ -589,6 +611,10 @@ function modelRows(meta, usage) {
|
|
|
589
611
|
const spent = usage.cost > 0 ? ` \xB7 ${money(usage.cost)}` : "";
|
|
590
612
|
rows.push(["Usage (this model)", `${count(usage.requests)} req${spent}`, "base"]);
|
|
591
613
|
}
|
|
614
|
+
if (session) {
|
|
615
|
+
const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
|
|
616
|
+
rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
|
|
617
|
+
}
|
|
592
618
|
if (meta.tier)
|
|
593
619
|
rows.push(["Tier", TIER_DISPLAY[meta.tier], "base"]);
|
|
594
620
|
else if (meta.minPlan)
|
|
@@ -704,7 +730,7 @@ function Panel(props) {
|
|
|
704
730
|
var _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$9 = _$createElement("b");
|
|
705
731
|
_$insertNode(_el$7, _el$8);
|
|
706
732
|
_$insertNode(_el$8, _el$9);
|
|
707
|
-
_$
|
|
733
|
+
_$insert(_el$9, () => props.title());
|
|
708
734
|
_$insert(_el$7, _$createComponent(For, {
|
|
709
735
|
get each() {
|
|
710
736
|
return props.rows();
|
|
@@ -1834,8 +1860,107 @@ import { spawn as spawn2 } from "child_process";
|
|
|
1834
1860
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1835
1861
|
import { homedir as homedir2 } from "os";
|
|
1836
1862
|
import { dirname, join as join2 } from "path";
|
|
1863
|
+
|
|
1864
|
+
// src/sidebar/zen.ts
|
|
1865
|
+
var GO_WINDOW_SHARE = {
|
|
1866
|
+
fiveHour: 0.2,
|
|
1867
|
+
weekly: 0.5,
|
|
1868
|
+
month: 1
|
|
1869
|
+
};
|
|
1870
|
+
function totals(value) {
|
|
1871
|
+
const v = value ?? {};
|
|
1872
|
+
const num = (key) => typeof v[key] === "number" ? v[key] : 0;
|
|
1873
|
+
return {
|
|
1874
|
+
requests: num("requests"),
|
|
1875
|
+
cost: num("cost"),
|
|
1876
|
+
tokens: num("tokens")
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function parseZenJson(text) {
|
|
1880
|
+
let body;
|
|
1881
|
+
try {
|
|
1882
|
+
body = JSON.parse(text);
|
|
1883
|
+
} catch {
|
|
1884
|
+
body = {};
|
|
1885
|
+
}
|
|
1886
|
+
const models = new Map;
|
|
1887
|
+
for (const raw of body.models ?? []) {
|
|
1888
|
+
if (typeof raw.id !== "string")
|
|
1889
|
+
continue;
|
|
1890
|
+
models.set(modelKey(raw.id), {
|
|
1891
|
+
id: raw.id,
|
|
1892
|
+
provider: typeof raw.provider === "string" ? raw.provider : "",
|
|
1893
|
+
limit: typeof raw.limit === "number" ? raw.limit : undefined,
|
|
1894
|
+
fiveHour: totals(raw.fiveHour),
|
|
1895
|
+
weekly: totals(raw.weekly),
|
|
1896
|
+
month: totals(raw.month)
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
const top = body.totals ?? {};
|
|
1900
|
+
return {
|
|
1901
|
+
periodStart: typeof body.periodStart === "number" ? body.periodStart : undefined,
|
|
1902
|
+
source: typeof body.source === "string" ? body.source : undefined,
|
|
1903
|
+
totals: {
|
|
1904
|
+
fiveHour: totals(top.fiveHour),
|
|
1905
|
+
weekly: totals(top.weekly),
|
|
1906
|
+
month: totals(top.month)
|
|
1907
|
+
},
|
|
1908
|
+
models
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
function windowRows2(label, spent, limit, share) {
|
|
1912
|
+
if (typeof limit !== "number" || limit <= 0) {
|
|
1913
|
+
return [[label, `${money(spent.cost)} \xB7 ${count(spent.requests)} req`, "base"]];
|
|
1914
|
+
}
|
|
1915
|
+
const cap = limit * share;
|
|
1916
|
+
const pct = cap > 0 ? Math.round(spent.cost / cap * 100) : 0;
|
|
1917
|
+
const rows = [[label, `${money(spent.cost)}/${money(cap)} (${pct}%)`, pctTone(pct)]];
|
|
1918
|
+
if (spent.requests > 0)
|
|
1919
|
+
rows.push(["", `${count(spent.requests)} req`, "muted"]);
|
|
1920
|
+
return rows;
|
|
1921
|
+
}
|
|
1922
|
+
function zenRows(usage, meta, session) {
|
|
1923
|
+
if (!usage)
|
|
1924
|
+
return [];
|
|
1925
|
+
const oc = meta?.oc;
|
|
1926
|
+
const rows = [["Plan", `${oc?.plan ?? "Go"} \xB7 limits per model`, "base"]];
|
|
1927
|
+
const mine = meta ? usage.models.get(modelKey(meta.name)) : undefined;
|
|
1928
|
+
const limit = oc?.allowance ?? mine?.limit;
|
|
1929
|
+
rows.push(...windowRows2("5-hour", mine?.fiveHour ?? usage.totals.fiveHour, limit, GO_WINDOW_SHARE.fiveHour));
|
|
1930
|
+
rows.push(...windowRows2("Weekly", mine?.weekly ?? usage.totals.weekly, limit, GO_WINDOW_SHARE.weekly));
|
|
1931
|
+
rows.push(...windowRows2("Month", mine?.month ?? usage.totals.month, limit, GO_WINDOW_SHARE.month));
|
|
1932
|
+
rows.push(["Period", `${count(usage.totals.month.requests)} requests \xB7 ${money(usage.totals.month.cost)}`, "base"]);
|
|
1933
|
+
rows.push(separator());
|
|
1934
|
+
if (!meta)
|
|
1935
|
+
return rows;
|
|
1936
|
+
rows.push(["Model", clip(meta.name, valueWidth("Model")), "strong", true]);
|
|
1937
|
+
if (session) {
|
|
1938
|
+
const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
|
|
1939
|
+
rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
|
|
1940
|
+
}
|
|
1941
|
+
if (typeof limit === "number") {
|
|
1942
|
+
rows.push(["Allowance", `${money(limit)}/mo`, "base"]);
|
|
1943
|
+
}
|
|
1944
|
+
if (oc?.rates) {
|
|
1945
|
+
const cache = [`cache read ${rate(oc.rates.cacheRead)}`];
|
|
1946
|
+
if (typeof oc.rates.cacheWrite === "number" && oc.rates.cacheWrite > 0) {
|
|
1947
|
+
cache.push(`write ${rate(oc.rates.cacheWrite)}`);
|
|
1948
|
+
}
|
|
1949
|
+
rows.push(["Rates", `${rate(oc.rates.input)}/${rate(oc.rates.output)} in/out`, "base"]);
|
|
1950
|
+
rows.push(["", cache.join(" \xB7 "), "muted"]);
|
|
1951
|
+
}
|
|
1952
|
+
if (typeof oc?.ability === "number") {
|
|
1953
|
+
rows.push(["Intelligence", String(oc.ability), "base"]);
|
|
1954
|
+
}
|
|
1955
|
+
if (typeof oc?.tps === "number")
|
|
1956
|
+
rows.push(["Tok/s", String(oc.tps), "base"]);
|
|
1957
|
+
return rows;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/sidebar/data.ts
|
|
1837
1961
|
var CMDUSE = cmduseCandidates();
|
|
1838
1962
|
var MPC = ["mpc", join2(homedir2(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
|
|
1963
|
+
var OCUSE = [process.env.OCUSE_BIN, "ocuse", "/opt/homebrew/bin/ocuse", "/usr/local/bin/ocuse"].filter((bin) => Boolean(bin));
|
|
1839
1964
|
var CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
1840
1965
|
var SPAWN_OPTIONS = {
|
|
1841
1966
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1874,7 +1999,8 @@ function parseUsageJson(text) {
|
|
|
1874
1999
|
return {
|
|
1875
2000
|
cap: typeof w.cap === "number" ? w.cap : undefined,
|
|
1876
2001
|
used: typeof w.used === "number" ? w.used : undefined,
|
|
1877
|
-
resetAt: typeof w.resetAt === "number" ? w.resetAt : undefined
|
|
2002
|
+
resetAt: typeof w.resetAt === "number" ? w.resetAt : undefined,
|
|
2003
|
+
exceeded: typeof w.exceeded === "boolean" ? w.exceeded : undefined
|
|
1878
2004
|
};
|
|
1879
2005
|
};
|
|
1880
2006
|
const summary = raw.summary ?? {};
|
|
@@ -1895,16 +2021,25 @@ function parseMpcJson(text) {
|
|
|
1895
2021
|
const body = JSON.parse(text);
|
|
1896
2022
|
const meta = new Map;
|
|
1897
2023
|
for (const row of body.rows ?? []) {
|
|
1898
|
-
|
|
1899
|
-
if (!cc || !row.name)
|
|
2024
|
+
if (!row.name || !row.cc && !row.oc)
|
|
1900
2025
|
continue;
|
|
2026
|
+
const cc = row.cc;
|
|
2027
|
+
const oc = row.oc;
|
|
1901
2028
|
const entry = {
|
|
1902
2029
|
key: row.key ?? modelKey(row.name),
|
|
1903
2030
|
name: row.name,
|
|
1904
|
-
allowance: cc
|
|
1905
|
-
rates: cc
|
|
1906
|
-
intelligence: cc
|
|
1907
|
-
tps: cc
|
|
2031
|
+
allowance: cc?.allowance,
|
|
2032
|
+
rates: cc?.pricing,
|
|
2033
|
+
intelligence: cc?.ability ?? undefined,
|
|
2034
|
+
tps: cc?.tps ?? undefined,
|
|
2035
|
+
oc: oc ? {
|
|
2036
|
+
provider: oc.provider,
|
|
2037
|
+
plan: oc.plan,
|
|
2038
|
+
allowance: oc.allowance,
|
|
2039
|
+
rates: oc.pricing,
|
|
2040
|
+
ability: oc.ability ?? undefined,
|
|
2041
|
+
tps: oc.tps ?? undefined
|
|
2042
|
+
} : undefined
|
|
1908
2043
|
};
|
|
1909
2044
|
meta.set(entry.key, entry);
|
|
1910
2045
|
meta.set(modelKey(row.name), entry);
|
|
@@ -1938,6 +2073,9 @@ async function loadMeta() {
|
|
|
1938
2073
|
async function loadUsage() {
|
|
1939
2074
|
return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
|
|
1940
2075
|
}
|
|
2076
|
+
async function loadZen() {
|
|
2077
|
+
return parseZenJson(await runFirst(OCUSE, ["-1", "--json"]));
|
|
2078
|
+
}
|
|
1941
2079
|
|
|
1942
2080
|
// src/sidebar/usageDb.ts
|
|
1943
2081
|
import { Database } from "bun:sqlite";
|
|
@@ -1949,20 +2087,14 @@ function usageDbPath() {
|
|
|
1949
2087
|
const base = process.env.XDG_DATA_HOME ?? join3(homedir3(), ".local", "share");
|
|
1950
2088
|
return join3(base, "opencode", "opencode.db");
|
|
1951
2089
|
}
|
|
1952
|
-
function periodStart(usage, now = Date.now()) {
|
|
1953
|
-
if (typeof usage.periodStartAt === "number")
|
|
1954
|
-
return usage.periodStartAt;
|
|
1955
|
-
const end = usage.periodEnd ? Date.parse(`${usage.periodEnd}T23:59:59`) : Number.NaN;
|
|
1956
|
-
if (!Number.isFinite(end))
|
|
1957
|
-
return now - 30 * 24 * 60 * 60 * 1000;
|
|
1958
|
-
const start = new Date(end);
|
|
1959
|
-
start.setMonth(start.getMonth() - 1);
|
|
1960
|
-
return start.getTime();
|
|
1961
|
-
}
|
|
1962
2090
|
function usage(value) {
|
|
1963
2091
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1964
2092
|
}
|
|
1965
|
-
function
|
|
2093
|
+
function hasV2(db) {
|
|
2094
|
+
const row = db.query("SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = 'session_message'").get();
|
|
2095
|
+
return Boolean(row);
|
|
2096
|
+
}
|
|
2097
|
+
function loadModelUsage(modelID, sinceMs, path = usageDbPath(), sessionID) {
|
|
1966
2098
|
let db;
|
|
1967
2099
|
try {
|
|
1968
2100
|
db = new Database(path, {
|
|
@@ -1972,8 +2104,26 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
|
|
|
1972
2104
|
return null;
|
|
1973
2105
|
}
|
|
1974
2106
|
try {
|
|
2107
|
+
if (hasV2(db)) {
|
|
2108
|
+
const rows = db.query(`SELECT json_extract(data, '$.model.id') AS model, count(*) AS c
|
|
2109
|
+
FROM session_message
|
|
2110
|
+
WHERE type = 'assistant'
|
|
2111
|
+
${sessionID ? "AND session_id = ?" : "AND time_created >= ?"}
|
|
2112
|
+
GROUP BY model`).all(sessionID ?? sinceMs);
|
|
2113
|
+
const key = modelKey(modelID);
|
|
2114
|
+
let requests = 0;
|
|
2115
|
+
for (const row of rows) {
|
|
2116
|
+
if (row.model && modelKey(row.model) === key)
|
|
2117
|
+
requests += row.c;
|
|
2118
|
+
}
|
|
2119
|
+
return {
|
|
2120
|
+
requests,
|
|
2121
|
+
cost: 0
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
1975
2124
|
const rows = db.query(`SELECT data, time_created FROM message
|
|
1976
|
-
WHERE data LIKE '%"tokens"%'
|
|
2125
|
+
WHERE data LIKE '%"tokens"%'
|
|
2126
|
+
${sessionID ? "AND session_id = ?" : "AND time_created >= ?"}`).all(sessionID ?? sinceMs);
|
|
1977
2127
|
const key = modelKey(modelID);
|
|
1978
2128
|
const seen = new Set;
|
|
1979
2129
|
let requests = 0;
|
|
@@ -2006,13 +2156,77 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
|
|
|
2006
2156
|
db.close();
|
|
2007
2157
|
}
|
|
2008
2158
|
}
|
|
2159
|
+
function loadSessionUsage(sessionID, path = usageDbPath()) {
|
|
2160
|
+
let db;
|
|
2161
|
+
try {
|
|
2162
|
+
db = new Database(path, {
|
|
2163
|
+
readonly: true
|
|
2164
|
+
});
|
|
2165
|
+
} catch {
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
try {
|
|
2169
|
+
if (hasV2(db)) {
|
|
2170
|
+
const totals = db.query("SELECT cost FROM session_v2 WHERE id = ?").get(sessionID);
|
|
2171
|
+
const counted = db.query("SELECT count(*) AS c FROM session_message WHERE session_id = ? AND type = 'assistant'").get(sessionID);
|
|
2172
|
+
return {
|
|
2173
|
+
requests: counted.c,
|
|
2174
|
+
cost: typeof totals?.cost === "number" ? totals.cost : 0
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
const rows = db.query(`SELECT id, data FROM message
|
|
2178
|
+
WHERE session_id = ? AND data LIKE '%"tokens"%'`).all(sessionID);
|
|
2179
|
+
const seen = new Set;
|
|
2180
|
+
let requests = 0;
|
|
2181
|
+
let cost = 0;
|
|
2182
|
+
for (const row of rows) {
|
|
2183
|
+
if (seen.has(row.id))
|
|
2184
|
+
continue;
|
|
2185
|
+
seen.add(row.id);
|
|
2186
|
+
let data;
|
|
2187
|
+
try {
|
|
2188
|
+
data = JSON.parse(row.data);
|
|
2189
|
+
} catch {
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
if (data.role !== "assistant" || !data.tokens)
|
|
2193
|
+
continue;
|
|
2194
|
+
requests += 1;
|
|
2195
|
+
cost += usage(data.cost);
|
|
2196
|
+
}
|
|
2197
|
+
return {
|
|
2198
|
+
requests,
|
|
2199
|
+
cost
|
|
2200
|
+
};
|
|
2201
|
+
} catch {
|
|
2202
|
+
return null;
|
|
2203
|
+
} finally {
|
|
2204
|
+
db.close();
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2009
2207
|
|
|
2010
2208
|
// src/sidebar/useRows.ts
|
|
2011
|
-
var POLL_MS =
|
|
2209
|
+
var POLL_MS = 20000;
|
|
2012
2210
|
function sessionKind(providerID) {
|
|
2013
2211
|
if (!providerID)
|
|
2014
2212
|
return "unknown";
|
|
2015
|
-
|
|
2213
|
+
if (providerID.startsWith("command-code"))
|
|
2214
|
+
return "ours";
|
|
2215
|
+
if (providerID.startsWith("opencode-go"))
|
|
2216
|
+
return "go";
|
|
2217
|
+
if (providerID === "opencode" || providerID.startsWith("opencode-zen"))
|
|
2218
|
+
return "zen";
|
|
2219
|
+
return "other";
|
|
2220
|
+
}
|
|
2221
|
+
function shows(kind) {
|
|
2222
|
+
return kind === "ours" || kind === "go" || kind === "zen";
|
|
2223
|
+
}
|
|
2224
|
+
function panelTitle(kind) {
|
|
2225
|
+
if (kind === "ours")
|
|
2226
|
+
return "Command Code";
|
|
2227
|
+
if (kind === "zen")
|
|
2228
|
+
return "OpenCode Zen";
|
|
2229
|
+
return "OpenCode Go";
|
|
2016
2230
|
}
|
|
2017
2231
|
var lastSnapshot;
|
|
2018
2232
|
function rememberSnapshot(snapshot) {
|
|
@@ -2035,31 +2249,41 @@ function seededModelUsage(id) {
|
|
|
2035
2249
|
return lastModelUsage.usage;
|
|
2036
2250
|
return lastModelUsage.key === modelKey(id) ? lastModelUsage.usage : undefined;
|
|
2037
2251
|
}
|
|
2038
|
-
function useRows(activeModelId, providerID) {
|
|
2252
|
+
function useRows(activeModelId, providerID, sessionID) {
|
|
2039
2253
|
const [usage, setUsage] = createSignal(seededRows());
|
|
2254
|
+
const [zen, setZen] = createSignal();
|
|
2040
2255
|
const [meta, setMeta] = createSignal(new Map);
|
|
2041
2256
|
const [modelUsage, setModelUsage] = createSignal(lastModelUsage);
|
|
2257
|
+
const [sessionUsage, setSessionUsage] = createSignal();
|
|
2042
2258
|
const [wasOurs, setWasOurs] = createSignal(false);
|
|
2043
2259
|
loadMeta().then(setMeta).catch(() => {});
|
|
2044
|
-
const refreshModelUsage = (id
|
|
2260
|
+
const refreshModelUsage = (id) => {
|
|
2045
2261
|
if (!id)
|
|
2046
2262
|
return;
|
|
2047
|
-
const fresh = loadModelUsage(id,
|
|
2263
|
+
const fresh = loadModelUsage(id, 0, usageDbPath(), sessionID());
|
|
2048
2264
|
if (fresh)
|
|
2049
2265
|
rememberModelUsage(id, fresh);
|
|
2050
2266
|
setModelUsage(lastModelUsage);
|
|
2051
2267
|
};
|
|
2052
2268
|
const refresh = async () => {
|
|
2269
|
+
const kind = sessionKind(providerID());
|
|
2270
|
+
const session = sessionID();
|
|
2271
|
+
if (session)
|
|
2272
|
+
setSessionUsage(loadSessionUsage(session) ?? undefined);
|
|
2053
2273
|
try {
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2274
|
+
if (kind === "ours") {
|
|
2275
|
+
const snapshot = await loadUsage();
|
|
2276
|
+
rememberSnapshot(snapshot);
|
|
2277
|
+
setUsage(usageRows(snapshot));
|
|
2278
|
+
refreshModelUsage(activeModelId());
|
|
2279
|
+
} else if (kind === "go" || kind === "zen") {
|
|
2280
|
+
setZen(await loadZen());
|
|
2281
|
+
}
|
|
2058
2282
|
} catch {}
|
|
2059
2283
|
};
|
|
2060
2284
|
createEffect(() => {
|
|
2061
2285
|
const kind = sessionKind(providerID());
|
|
2062
|
-
if (kind
|
|
2286
|
+
if (!shows(kind))
|
|
2063
2287
|
return;
|
|
2064
2288
|
setWasOurs(true);
|
|
2065
2289
|
refresh();
|
|
@@ -2068,9 +2292,10 @@ function useRows(activeModelId, providerID) {
|
|
|
2068
2292
|
});
|
|
2069
2293
|
createEffect(() => {
|
|
2070
2294
|
const id = activeModelId();
|
|
2071
|
-
|
|
2295
|
+
const session = sessionID();
|
|
2296
|
+
if (sessionKind(providerID()) !== "ours" || !id || !session)
|
|
2072
2297
|
return;
|
|
2073
|
-
refreshModelUsage(id
|
|
2298
|
+
refreshModelUsage(id);
|
|
2074
2299
|
});
|
|
2075
2300
|
const usageFor = (id) => {
|
|
2076
2301
|
const seen = modelUsage();
|
|
@@ -2081,18 +2306,19 @@ function useRows(activeModelId, providerID) {
|
|
|
2081
2306
|
};
|
|
2082
2307
|
return createMemo(() => {
|
|
2083
2308
|
const kind = sessionKind(providerID());
|
|
2084
|
-
if (kind === "other")
|
|
2085
|
-
return [];
|
|
2086
|
-
if (kind === "unknown" && !wasOurs())
|
|
2309
|
+
if (!shows(kind) && (kind === "other" || !wasOurs()))
|
|
2087
2310
|
return [];
|
|
2088
2311
|
const id = activeModelId();
|
|
2089
2312
|
const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
|
|
2313
|
+
if (kind === "go" || kind === "zen") {
|
|
2314
|
+
return zenRows(zen(), found, sessionUsage());
|
|
2315
|
+
}
|
|
2090
2316
|
const model = found ? {
|
|
2091
2317
|
...found,
|
|
2092
2318
|
tier: found.tier ?? (id ? tierFor(id) : undefined),
|
|
2093
2319
|
minPlan: id ? minPlan(id) : undefined
|
|
2094
2320
|
} : undefined;
|
|
2095
|
-
return [...usage(), separator(), ...modelRows(model, usageFor(id))];
|
|
2321
|
+
return [...usage(), separator(), ...modelRows(model, usageFor(id), sessionUsage())];
|
|
2096
2322
|
});
|
|
2097
2323
|
}
|
|
2098
2324
|
|
|
@@ -2101,8 +2327,9 @@ var ID = "command-code.tui";
|
|
|
2101
2327
|
var themed = (colors) => loadPrefs().colors ? colors : mono(colors);
|
|
2102
2328
|
function PanelV1(props) {
|
|
2103
2329
|
const current = () => props.api.state.session.get(props.sessionID)?.model;
|
|
2104
|
-
const rows = useRows(() => current()?.id, () => current()?.providerID);
|
|
2330
|
+
const rows = useRows(() => current()?.id, () => current()?.providerID, () => props.sessionID);
|
|
2105
2331
|
return _$createComponent2(Panel, {
|
|
2332
|
+
title: () => panelTitle(sessionKind(current()?.providerID)),
|
|
2106
2333
|
rows,
|
|
2107
2334
|
colors: () => themed(legacyColors(props.api.theme.current))
|
|
2108
2335
|
});
|
|
@@ -2127,8 +2354,9 @@ function PanelV2(props) {
|
|
|
2127
2354
|
ctx
|
|
2128
2355
|
} = props;
|
|
2129
2356
|
const current = () => ctx.data.session.get(props.sessionID)?.model;
|
|
2130
|
-
const rows = useRows(() => current()?.id, () => current()?.providerID);
|
|
2357
|
+
const rows = useRows(() => current()?.id, () => current()?.providerID, () => props.sessionID);
|
|
2131
2358
|
return _$createComponent2(Panel, {
|
|
2359
|
+
title: () => panelTitle(sessionKind(current()?.providerID)),
|
|
2132
2360
|
rows,
|
|
2133
2361
|
colors: () => themed(hostColors(ctx.theme))
|
|
2134
2362
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffreyjyz/opencode-command-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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",
|