@arandevcode/opencode-quota-extended 0.2.2 → 0.2.3
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/README-es.md +4 -4
- package/README.md +4 -4
- package/dist/lib/balance.d.ts +14 -0
- package/dist/lib/balance.d.ts.map +1 -0
- package/dist/lib/balance.js +23 -0
- package/dist/lib/balance.js.map +1 -0
- package/dist/lib/pace.d.ts +28 -0
- package/dist/lib/pace.d.ts.map +1 -0
- package/dist/lib/pace.js +46 -0
- package/dist/lib/pace.js.map +1 -0
- package/dist/lib/refresh-scheduler.d.ts +20 -0
- package/dist/lib/refresh-scheduler.d.ts.map +1 -0
- package/dist/lib/refresh-scheduler.js +57 -0
- package/dist/lib/refresh-scheduler.js.map +1 -0
- package/dist/lib/types.d.ts +15 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/providers/opencode-go.d.ts +5 -2
- package/dist/providers/opencode-go.d.ts.map +1 -1
- package/dist/providers/opencode-go.js +156 -42
- package/dist/providers/opencode-go.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.tsx +58 -83
- package/dist/ui/SidebarPanel.d.ts.map +1 -1
- package/dist/ui/SidebarPanel.tsx +11 -3
- package/dist/ui/cards.d.ts.map +1 -1
- package/dist/ui/cards.tsx +13 -1
- package/package.json +7 -3
- package/scripts/clean.mjs +8 -0
- package/scripts/postbuild.mjs +56 -0
- package/scripts/setup.mjs +45 -0
- package/dist/lib/sanitize.d.ts +0 -10
- package/dist/lib/sanitize.d.ts.map +0 -1
- package/dist/lib/sanitize.js +0 -14
- package/dist/lib/sanitize.js.map +0 -1
- package/dist/lib/throttle.d.ts +0 -18
- package/dist/lib/throttle.d.ts.map +0 -1
- package/dist/lib/throttle.js +0 -26
- package/dist/lib/throttle.js.map +0 -1
- package/dist/lib/toast-format.d.ts +0 -24
- package/dist/lib/toast-format.d.ts.map +0 -1
- package/dist/lib/toast-format.js +0 -79
- package/dist/lib/toast-format.js.map +0 -1
package/dist/tui.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { createSignal, ErrorBoundary
|
|
3
|
-
import type { TuiPlugin,
|
|
2
|
+
import { createSignal, ErrorBoundary } from "solid-js";
|
|
3
|
+
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui";
|
|
4
4
|
import { loadCardsConfig } from "./config/loader.js";
|
|
5
|
+
import { createRefreshScheduler } from "./lib/refresh-scheduler.js";
|
|
5
6
|
import { collectSnapshot } from "./providers/registry.js";
|
|
6
7
|
import { SidebarPanel } from "./ui/SidebarPanel.js";
|
|
7
8
|
|
|
8
9
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
9
|
-
const
|
|
10
|
-
const MOUNT_RECOVERY_DELAYS_MS = [500, 1500, 4000] as const;
|
|
10
|
+
const MOUNT_RECOVERY_DELAY_MS = 1_500;
|
|
11
11
|
|
|
12
12
|
type CardsSnapshot = Awaited<ReturnType<typeof collectSnapshot>>;
|
|
13
13
|
|
|
@@ -20,95 +20,70 @@ export const quotaCardsTuiPlugin: TuiPlugin = async (api, _options, _meta) => {
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const existing = sessionResources.get(sessionID);
|
|
30
|
-
if (existing) return existing;
|
|
31
|
-
|
|
32
|
-
const [snapshot, setSnapshot] = createSignal<CardsSnapshot>({
|
|
33
|
-
generatedAt: new Date(0).toISOString(),
|
|
34
|
-
providers: [],
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
let inFlight = false;
|
|
38
|
-
let queued = false;
|
|
39
|
-
let version = 0;
|
|
40
|
-
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
41
|
-
|
|
42
|
-
const reload = () => {
|
|
43
|
-
if (inFlight) {
|
|
44
|
-
queued = true;
|
|
45
|
-
version += 1;
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
inFlight = true;
|
|
49
|
-
const v = ++version;
|
|
50
|
-
collectSnapshot(config).then((next) => {
|
|
51
|
-
if (v === version) setSnapshot(next);
|
|
52
|
-
}).catch((err) => {
|
|
53
|
-
const msg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err);
|
|
54
|
-
try { process.stderr.write(`[quota-cards] snapshot refresh failed: ${msg}\n`); } catch {}
|
|
55
|
-
}).finally(() => {
|
|
56
|
-
inFlight = false;
|
|
57
|
-
if (queued) { queued = false; reload(); }
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const queueRefresh = (delay: number) => {
|
|
62
|
-
const t = setTimeout(() => { timers.delete(t); reload(); }, delay);
|
|
63
|
-
timers.add(t);
|
|
64
|
-
};
|
|
65
|
-
const scheduleRefresh = () => {
|
|
66
|
-
for (const d of EVENT_REFRESH_DELAYS_MS) queueRefresh(d);
|
|
67
|
-
};
|
|
68
|
-
const scheduleMountRecovery = () => {
|
|
69
|
-
for (const d of MOUNT_RECOVERY_DELAYS_MS) queueRefresh(d);
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const interval = setInterval(reload, config.refreshIntervalMs ?? REFRESH_INTERVAL_MS);
|
|
73
|
-
const unsubs = [
|
|
74
|
-
api.event.on("session.updated", (e) => { try { if (e?.properties?.info?.id === sessionID) scheduleRefresh(); } catch {} }),
|
|
75
|
-
api.event.on("message.updated", (e) => { try { if (e?.properties?.info?.sessionID === sessionID) scheduleRefresh(); } catch {} }),
|
|
76
|
-
api.event.on("message.removed", (e) => { try { if (e?.properties?.sessionID === sessionID) scheduleRefresh(); } catch {} }),
|
|
77
|
-
api.event.on("tui.session.select", (e) => { try { if (e?.properties?.sessionID === sessionID) scheduleRefresh(); } catch {} }),
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
reload();
|
|
81
|
-
scheduleMountRecovery();
|
|
82
|
-
|
|
83
|
-
const dispose = () => {
|
|
84
|
-
clearInterval(interval);
|
|
85
|
-
for (const t of timers) clearTimeout(t);
|
|
86
|
-
timers.clear();
|
|
87
|
-
for (const u of unsubs) u();
|
|
88
|
-
sessionResources.delete(sessionID);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const resource = { snapshot, dispose };
|
|
92
|
-
sessionResources.set(sessionID, resource);
|
|
93
|
-
return resource;
|
|
94
|
-
}
|
|
23
|
+
// Quota is account-wide, not per session: one shared snapshot and one
|
|
24
|
+
// poller serve every mounted sidebar instead of one per session.
|
|
25
|
+
const [snapshot, setSnapshot] = createSignal<CardsSnapshot>({
|
|
26
|
+
generatedAt: new Date(0).toISOString(),
|
|
27
|
+
providers: [],
|
|
28
|
+
});
|
|
95
29
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
30
|
+
let inFlight = false;
|
|
31
|
+
let queued = false;
|
|
32
|
+
let version = 0;
|
|
33
|
+
let disposed = false;
|
|
34
|
+
|
|
35
|
+
const reload = () => {
|
|
36
|
+
if (disposed) return;
|
|
37
|
+
if (inFlight) {
|
|
38
|
+
queued = true;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
inFlight = true;
|
|
42
|
+
const v = ++version;
|
|
43
|
+
collectSnapshot(config).then((next) => {
|
|
44
|
+
if (v === version && !disposed) setSnapshot(next);
|
|
45
|
+
}).catch((err) => {
|
|
46
|
+
const msg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err);
|
|
47
|
+
try { process.stderr.write(`[quota-cards] snapshot refresh failed: ${msg}\n`); } catch {}
|
|
48
|
+
}).finally(() => {
|
|
49
|
+
inFlight = false;
|
|
50
|
+
if (queued) { queued = false; reload(); }
|
|
100
51
|
});
|
|
52
|
+
};
|
|
101
53
|
|
|
102
|
-
|
|
54
|
+
const scheduler = createRefreshScheduler({
|
|
55
|
+
refresh: reload,
|
|
56
|
+
intervalMs: config.refreshIntervalMs ?? REFRESH_INTERVAL_MS,
|
|
57
|
+
});
|
|
58
|
+
const unsubs = [
|
|
59
|
+
api.event.on("session.updated", () => scheduler.schedule()),
|
|
60
|
+
api.event.on("message.updated", () => scheduler.schedule()),
|
|
61
|
+
api.event.on("message.removed", () => scheduler.schedule()),
|
|
62
|
+
api.event.on("tui.session.select", () => scheduler.schedule()),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const dispose = () => {
|
|
66
|
+
disposed = true;
|
|
67
|
+
scheduler.dispose();
|
|
68
|
+
for (const u of unsubs) u();
|
|
69
|
+
};
|
|
70
|
+
api.lifecycle.onDispose(dispose);
|
|
71
|
+
|
|
72
|
+
scheduler.start();
|
|
73
|
+
reload();
|
|
74
|
+
scheduler.schedule(MOUNT_RECOVERY_DELAY_MS, true);
|
|
75
|
+
|
|
76
|
+
function SidebarContentView() {
|
|
77
|
+
return <SidebarPanel api={api} config={config} snapshot={snapshot} />;
|
|
103
78
|
}
|
|
104
79
|
|
|
105
80
|
api.slots.register({
|
|
106
81
|
order: config.cardsSidebar.order,
|
|
107
82
|
slots: {
|
|
108
|
-
sidebar_content(_ctx: unknown,
|
|
83
|
+
sidebar_content(_ctx: unknown, _props: { session_id: string }) {
|
|
109
84
|
return (
|
|
110
85
|
<ErrorBoundary fallback={<text fg={api.theme.current.error}>Quota error</text>}>
|
|
111
|
-
<SidebarContentView
|
|
86
|
+
<SidebarContentView />
|
|
112
87
|
</ErrorBoundary>
|
|
113
88
|
);
|
|
114
89
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SidebarPanel.d.ts","sourceRoot":"","sources":["../../src/ui/SidebarPanel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"SidebarPanel.d.ts","sourceRoot":"","sources":["../../src/ui/SidebarPanel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKrD,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,GAAG,EAAE,YAAY,CAAC;IAClB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,aAAa,CAAC;CAC/B,GAAG,GAAG,CAAC,OAAO,CAuCd"}
|
package/dist/ui/SidebarPanel.tsx
CHANGED
|
@@ -6,6 +6,8 @@ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
|
6
6
|
import type { CardsPluginConfig } from "../config/defaults.js";
|
|
7
7
|
import type { CardsSnapshot } from "../lib/types.js";
|
|
8
8
|
import { ProviderCards } from "./cards.js";
|
|
9
|
+
import { quotaPace } from "../lib/pace.js";
|
|
10
|
+
import { balanceUsd } from "../lib/balance.js";
|
|
9
11
|
|
|
10
12
|
export function SidebarPanel(props: {
|
|
11
13
|
api: TuiPluginApi;
|
|
@@ -21,13 +23,19 @@ export function SidebarPanel(props: {
|
|
|
21
23
|
|
|
22
24
|
const estimatedHeight = () => {
|
|
23
25
|
let total = 0
|
|
26
|
+
let blocks = 0
|
|
24
27
|
for (const p of props.snapshot().providers) {
|
|
25
28
|
if (p.status === "skipped") continue
|
|
29
|
+
blocks += 1
|
|
26
30
|
if (p.status === "no-auth" || p.status === "error") { total += 1; continue }
|
|
27
|
-
const
|
|
28
|
-
|
|
31
|
+
const windows = p.windows ?? []
|
|
32
|
+
if (windows.length === 0) { total += 1; continue }
|
|
33
|
+
// Each window is label + bar + left/used, plus a pace line when it applies.
|
|
34
|
+
const rows = windows.reduce((sum, w) => sum + 3 + (quotaPace(w) ? 1 : 0), 0)
|
|
35
|
+
const credit = balanceUsd(p.balance) === null ? 0 : 1 // a credit line under the title
|
|
36
|
+
total += 1 + credit + rows + (windows.length - 1) // title + credit + windows + gaps
|
|
29
37
|
}
|
|
30
|
-
return Math.min(total, 50)
|
|
38
|
+
return Math.min(total + Math.max(0, blocks - 1), 50) // + scrollbox gaps between providers
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
return (
|
package/dist/ui/cards.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../../src/ui/cards.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../../src/ui/cards.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,iBAAiB,CAAC;AAMlE,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,GAAG,EAAE;QAAE,KAAK,EAAE;YAAE,OAAO,EAAE,eAAe,CAAA;SAAE,CAAA;KAAE,CAAC;IAC7C,QAAQ,EAAE,aAAa,CAAC;IACxB,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG,GAAG,CAAC,OAAO,CAiDd"}
|
package/dist/ui/cards.tsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { For } from "solid-js";
|
|
2
|
+
import { For, createMemo } from "solid-js";
|
|
3
3
|
import { TextAttributes } from "@opentui/core";
|
|
4
4
|
import type { JSX } from "@opentui/solid";
|
|
5
5
|
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui";
|
|
6
6
|
import type { ProviderQuota, QuotaWindow } from "../lib/types.js";
|
|
7
7
|
import { trafficLight, trafficLightColorKey } from "./progress-bar.js";
|
|
8
8
|
import { formatResetCombined } from "../lib/reset-format.js";
|
|
9
|
+
import { formatPace, quotaPace } from "../lib/pace.js";
|
|
10
|
+
import { formatBalance } from "../lib/balance.js";
|
|
9
11
|
|
|
10
12
|
export function ProviderCards(props: {
|
|
11
13
|
api: { theme: { current: TuiThemeCurrent } };
|
|
@@ -15,6 +17,7 @@ export function ProviderCards(props: {
|
|
|
15
17
|
const theme = () => props.api.theme.current;
|
|
16
18
|
const muted = () => theme().textMuted;
|
|
17
19
|
const errorColor = () => theme().error;
|
|
20
|
+
const credit = () => formatBalance(props.provider.balance);
|
|
18
21
|
|
|
19
22
|
if (props.provider.status === "skipped") return null;
|
|
20
23
|
|
|
@@ -51,6 +54,7 @@ export function ProviderCards(props: {
|
|
|
51
54
|
<text fg={theme().text} attributes={TextAttributes.BOLD}>
|
|
52
55
|
{props.provider.label}
|
|
53
56
|
</text>
|
|
57
|
+
{credit() ? <text fg={theme().text}>{credit()}</text> : null}
|
|
54
58
|
<box flexDirection="column" gap={1} width="100%">
|
|
55
59
|
<For each={props.provider.windows}>
|
|
56
60
|
{(window) => <WindowRow api={props.api} window={window} />}
|
|
@@ -74,6 +78,13 @@ function WindowRow(props: {
|
|
|
74
78
|
};
|
|
75
79
|
const label = () => windowDisplayLabel(props.window);
|
|
76
80
|
const reset = () => formatReset(props.window.resetTimeIso);
|
|
81
|
+
const pace = createMemo(() => quotaPace(props.window));
|
|
82
|
+
const paceText = () => formatPace(props.window);
|
|
83
|
+
const paceColor = () => {
|
|
84
|
+
const reading = pace();
|
|
85
|
+
if (!reading || reading.pace === "on-pace") return muted();
|
|
86
|
+
return reading.pace === "buffer" ? theme().success : theme().error;
|
|
87
|
+
};
|
|
77
88
|
|
|
78
89
|
return (
|
|
79
90
|
<box flexDirection="column" gap={0} width="100%">
|
|
@@ -86,6 +97,7 @@ function WindowRow(props: {
|
|
|
86
97
|
<text fg={barColor()}>{percent()}% left</text>
|
|
87
98
|
<text fg={muted()}>{100 - percent()}% used</text>
|
|
88
99
|
</box>
|
|
100
|
+
{paceText() ? <text fg={paceColor()}>{paceText()}</text> : null}
|
|
89
101
|
</box>
|
|
90
102
|
);
|
|
91
103
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arandevcode/opencode-quota-extended",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "OpenCode plugin: quota usage cards with theme-colored gauge progress bars (5h / weekly / monthly) for OpenAI, GitHub Copilot and OpenCode Go in the TUI sidebar.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,13 +30,17 @@
|
|
|
30
30
|
"dist",
|
|
31
31
|
"bin",
|
|
32
32
|
"images",
|
|
33
|
+
"scripts/clean.mjs",
|
|
34
|
+
"scripts/postbuild.mjs",
|
|
35
|
+
"scripts/setup.mjs",
|
|
33
36
|
"README.md",
|
|
34
37
|
"README-es.md",
|
|
35
38
|
"LICENSE"
|
|
36
39
|
],
|
|
37
40
|
"scripts": {
|
|
38
|
-
"
|
|
39
|
-
"
|
|
41
|
+
"clean": "node scripts/clean.mjs",
|
|
42
|
+
"build": "npm run clean && tsc && node scripts/postbuild.mjs",
|
|
43
|
+
"prepublish": "npm run build",
|
|
40
44
|
"build:watch": "tsc --watch",
|
|
41
45
|
"typecheck": "tsc --noEmit",
|
|
42
46
|
"quota": "node bin/quota.mjs",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
7
|
+
|
|
8
|
+
await rm(join(ROOT, "dist"), { recursive: true, force: true });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { copyFile, readdir, readFile, rm, mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
7
|
+
const SRC = join(ROOT, "src");
|
|
8
|
+
const DIST = join(ROOT, "dist");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Strategy: TSC handles the .ts → .js transformation and emits .d.ts files
|
|
12
|
+
* for IDE consumers. JSX/TSX files are NOT compiled — @opentui/solid's JSX
|
|
13
|
+
* runtime is shipped as TS-only and must be transformed by OpenCode's host
|
|
14
|
+
* runtime (Bun). We copy the .tsx sources verbatim so OpenCode's plugin
|
|
15
|
+
* loader can compile them on import.
|
|
16
|
+
*
|
|
17
|
+
* We also strip the .jsx artifacts tsc leaves behind when jsx=preserve is on,
|
|
18
|
+
* plus the auto-generated index.html "directory listing" files the OpenCode
|
|
19
|
+
* TUI static server writes into dist — they leak the local build path.
|
|
20
|
+
*/
|
|
21
|
+
async function walk(dir) {
|
|
22
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
23
|
+
for (const e of entries) {
|
|
24
|
+
const p = join(dir, e.name);
|
|
25
|
+
if (e.isDirectory()) {
|
|
26
|
+
await walk(p);
|
|
27
|
+
} else if (e.name.endsWith(".jsx")) {
|
|
28
|
+
await rm(p);
|
|
29
|
+
} else if (e.name.endsWith(".jsx.map")) {
|
|
30
|
+
await rm(p);
|
|
31
|
+
} else if (e.name === "index.html" && (await readFile(p, "utf8")).includes("Directory listing")) {
|
|
32
|
+
await rm(p);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function copyTsxRecursive(srcDir, distDir, rel = "") {
|
|
38
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
const sp = join(srcDir, e.name);
|
|
41
|
+
const dp = join(distDir, e.name);
|
|
42
|
+
if (e.isDirectory()) {
|
|
43
|
+
await mkdir(dp, { recursive: true });
|
|
44
|
+
await copyTsxRecursive(sp, dp, rel + e.name + "/");
|
|
45
|
+
} else if (e.name.endsWith(".tsx")) {
|
|
46
|
+
await rm(dp.replace(/\.tsx$/, ".js"), { force: true });
|
|
47
|
+
await rm(dp.replace(/\.tsx$/, ".js.map"), { force: true });
|
|
48
|
+
await copyFile(sp, dp);
|
|
49
|
+
console.log(`[postbuild] copied ${rel}${e.name} (uncompiled, runtime will transform)`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await walk(DIST);
|
|
55
|
+
await copyTsxRecursive(SRC, DIST);
|
|
56
|
+
console.log("[postbuild] done");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
9
|
+
|
|
10
|
+
const outDir = join(homedir(), ".config", "opencode", "opencode-quota");
|
|
11
|
+
const outFile = join(outDir, "opencode-go.json");
|
|
12
|
+
|
|
13
|
+
console.log("");
|
|
14
|
+
console.log("== OpenCode Go — setup ==");
|
|
15
|
+
console.log("");
|
|
16
|
+
console.log("Necesitas 2 datos:");
|
|
17
|
+
console.log(" 1. Workspace ID → lo ves en la URL del dashboard");
|
|
18
|
+
console.log(" 2. Session Cookie → la cookie '__Host-console_session' de opencode.ai");
|
|
19
|
+
|
|
20
|
+
console.log("");
|
|
21
|
+
console.log("Cómo obtenerlos:");
|
|
22
|
+
console.log(" A. Abre https://opencode.ai y loguéate en Chrome");
|
|
23
|
+
console.log(" B. Abre DevTools (F12) → Application → Cookies → https://opencode.ai");
|
|
24
|
+
console.log(" C. Busca la cookie '__Host-console_session' — copia su valor (empieza con st_...)");
|
|
25
|
+
console.log(" (es HttpOnly, así que document.cookie no la ve; usa DevTools)");
|
|
26
|
+
console.log(" D. El Workspace ID está en la URL: https://opencode.ai/workspace/wrk_XXX/go");
|
|
27
|
+
console.log("");
|
|
28
|
+
|
|
29
|
+
const authCookie = (await ask("Pega la Session Cookie (st_...): ")).trim();
|
|
30
|
+
const workspaceId = (await ask("Pega el Workspace ID (wrk_...): ")).trim();
|
|
31
|
+
rl.close();
|
|
32
|
+
|
|
33
|
+
if (!authCookie || !workspaceId) {
|
|
34
|
+
console.error("Ambos campos son obligatorios. Abortando.");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await mkdir(outDir, { recursive: true });
|
|
39
|
+
await writeFile(
|
|
40
|
+
outFile,
|
|
41
|
+
JSON.stringify({ workspaceId, authCookie }, null, 2) + "\n",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
console.log(`\nConfiguración guardada en: ${outFile}`);
|
|
45
|
+
console.log("Reinicia OpenCode y el panel 'Quota cards' aparecerá.");
|
package/dist/lib/sanitize.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Display sanitization for user-visible toast output.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors the upstream `@slkiser/opencode-quota` helper of the same name:
|
|
5
|
-
* strips ANSI escape sequences and control characters (except newline/tab)
|
|
6
|
-
* so that provider error text cannot inject terminal control codes into
|
|
7
|
-
* toasts or transcript output.
|
|
8
|
-
*/
|
|
9
|
-
export declare function sanitizeDisplayText(text: string): string;
|
|
10
|
-
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/lib/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
package/dist/lib/sanitize.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Display sanitization for user-visible toast output.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors the upstream `@slkiser/opencode-quota` helper of the same name:
|
|
5
|
-
* strips ANSI escape sequences and control characters (except newline/tab)
|
|
6
|
-
* so that provider error text cannot inject terminal control codes into
|
|
7
|
-
* toasts or transcript output.
|
|
8
|
-
*/
|
|
9
|
-
// eslint-disable-next-line no-control-regex
|
|
10
|
-
const DISPLAY_CONTROL_RE = /\x1B\[[0-9;]*[A-Za-z]|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
11
|
-
export function sanitizeDisplayText(text) {
|
|
12
|
-
return text.replace(DISPLAY_CONTROL_RE, "");
|
|
13
|
-
}
|
|
14
|
-
//# sourceMappingURL=sanitize.js.map
|
package/dist/lib/sanitize.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../src/lib/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,4CAA4C;AAC5C,MAAM,kBAAkB,GAAG,yDAAyD,CAAC;AAErF,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;AAC9C,CAAC"}
|
package/dist/lib/throttle.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple time-since-last-fire throttle gate.
|
|
3
|
-
*
|
|
4
|
-
* Used by the toast feature so a flurry of `session.idle` /
|
|
5
|
-
* `session.compacted` events does not spam the user. No caching, no
|
|
6
|
-
* deduplication — callers ask "may I fire?" and, when they actually fire,
|
|
7
|
-
* call `record()`.
|
|
8
|
-
*/
|
|
9
|
-
export interface Throttle {
|
|
10
|
-
/** Returns true if the configured min interval has elapsed since the last `record()`. */
|
|
11
|
-
shouldFire(nowMs?: number): boolean;
|
|
12
|
-
/** Mark a fire as having occurred at the given time (defaults to now). */
|
|
13
|
-
record(nowMs?: number): void;
|
|
14
|
-
/** Reset the gate so the next `shouldFire()` returns true. Useful for tests. */
|
|
15
|
-
reset(): void;
|
|
16
|
-
}
|
|
17
|
-
export declare function createThrottle(minIntervalMs: number): Throttle;
|
|
18
|
-
//# sourceMappingURL=throttle.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"throttle.d.ts","sourceRoot":"","sources":["../../src/lib/throttle.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,QAAQ;IACvB,yFAAyF;IACzF,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,0EAA0E;IAC1E,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,gFAAgF;IAChF,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,QAAQ,CAe9D"}
|
package/dist/lib/throttle.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple time-since-last-fire throttle gate.
|
|
3
|
-
*
|
|
4
|
-
* Used by the toast feature so a flurry of `session.idle` /
|
|
5
|
-
* `session.compacted` events does not spam the user. No caching, no
|
|
6
|
-
* deduplication — callers ask "may I fire?" and, when they actually fire,
|
|
7
|
-
* call `record()`.
|
|
8
|
-
*/
|
|
9
|
-
export function createThrottle(minIntervalMs) {
|
|
10
|
-
let lastFireMs = Number.NEGATIVE_INFINITY;
|
|
11
|
-
return {
|
|
12
|
-
shouldFire(nowMs) {
|
|
13
|
-
if (minIntervalMs <= 0)
|
|
14
|
-
return true;
|
|
15
|
-
const now = nowMs ?? Date.now();
|
|
16
|
-
return now - lastFireMs >= minIntervalMs;
|
|
17
|
-
},
|
|
18
|
-
record(nowMs) {
|
|
19
|
-
lastFireMs = nowMs ?? Date.now();
|
|
20
|
-
},
|
|
21
|
-
reset() {
|
|
22
|
-
lastFireMs = Number.NEGATIVE_INFINITY;
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
//# sourceMappingURL=throttle.js.map
|
package/dist/lib/throttle.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"throttle.js","sourceRoot":"","sources":["../../src/lib/throttle.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,MAAM,UAAU,cAAc,CAAC,aAAqB;IAClD,IAAI,UAAU,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAC1C,OAAO;QACL,UAAU,CAAC,KAAc;YACvB,IAAI,aAAa,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;YACpC,MAAM,GAAG,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YAChC,OAAO,GAAG,GAAG,UAAU,IAAI,aAAa,CAAC;QAC3C,CAAC;QACD,MAAM,CAAC,KAAc;YACnB,UAAU,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QACnC,CAAC;QACD,KAAK;YACH,UAAU,GAAG,MAAM,CAAC,iBAAiB,CAAC;QACxC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Toast message formatter.
|
|
3
|
-
*
|
|
4
|
-
* Renders a `CardsSnapshot` (the same data structure the TUI sidebar
|
|
5
|
-
* consumes) into a compact multi-line string suitable for OpenCode's
|
|
6
|
-
* `tui.showToast`. The shape is loosely modelled on upstream's
|
|
7
|
-
* `formatQuotaRowsGrouped` — provider header followed by one line per
|
|
8
|
-
* usage window — but kept narrower since our snapshot has fewer fields.
|
|
9
|
-
*
|
|
10
|
-
* Returns `null` when no provider has any window worth showing.
|
|
11
|
-
*/
|
|
12
|
-
import type { CardsSnapshot } from "./types.js";
|
|
13
|
-
export interface FormatToastMessageOptions {
|
|
14
|
-
/** Override the current time for deterministic tests. */
|
|
15
|
-
now?: Date;
|
|
16
|
-
/** Maximum chars per line (lines longer than this are clipped). Default 60. */
|
|
17
|
-
maxLineWidth?: number;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Render a CardsSnapshot to a multi-line toast string, or `null` if there
|
|
21
|
-
* is nothing meaningful to display.
|
|
22
|
-
*/
|
|
23
|
-
export declare function formatToastMessage(snapshot: CardsSnapshot, opts?: FormatToastMessageOptions): string | null;
|
|
24
|
-
//# sourceMappingURL=toast-format.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"toast-format.d.ts","sourceRoot":"","sources":["../../src/lib/toast-format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAC;AAG/D,MAAM,WAAW,yBAAyB;IACxC,yDAAyD;IACzD,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAiED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,aAAa,EACvB,IAAI,GAAE,yBAA8B,GACnC,MAAM,GAAG,IAAI,CAyBf"}
|
package/dist/lib/toast-format.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { formatResetRelative, clampPercent } from "./reset-format.js";
|
|
2
|
-
function padRight(text, width) {
|
|
3
|
-
if (text.length >= width)
|
|
4
|
-
return text;
|
|
5
|
-
return text + " ".repeat(width - text.length);
|
|
6
|
-
}
|
|
7
|
-
function padLeft(text, width) {
|
|
8
|
-
if (text.length >= width)
|
|
9
|
-
return text;
|
|
10
|
-
return " ".repeat(width - text.length) + text;
|
|
11
|
-
}
|
|
12
|
-
function formatProviderHeader(provider) {
|
|
13
|
-
return provider.label;
|
|
14
|
-
}
|
|
15
|
-
function formatWindowLine(provider, window, now) {
|
|
16
|
-
const label = padRight(window.label, 7);
|
|
17
|
-
const countdown = formatResetRelative(window.resetTimeIso, now);
|
|
18
|
-
const reset = padRight(countdown, 11);
|
|
19
|
-
const percent = `${clampPercent(window.percentRemaining)}%`;
|
|
20
|
-
const percentCell = padLeft(percent, 4);
|
|
21
|
-
// Copilot exposes remaining/total counts — surface them after the percent.
|
|
22
|
-
let suffix = "";
|
|
23
|
-
if (provider.id === "github-copilot" &&
|
|
24
|
-
typeof window.remaining === "number" &&
|
|
25
|
-
typeof window.total === "number") {
|
|
26
|
-
suffix = ` (${window.remaining}/${window.total})`;
|
|
27
|
-
}
|
|
28
|
-
return ` ${label} ${reset} ${percentCell}${suffix}`;
|
|
29
|
-
}
|
|
30
|
-
function formatProviderBlock(provider, now) {
|
|
31
|
-
if (provider.status !== "ok" || provider.windows.length === 0)
|
|
32
|
-
return [];
|
|
33
|
-
const lines = [{ text: formatProviderHeader(provider) }];
|
|
34
|
-
for (const window of provider.windows) {
|
|
35
|
-
lines.push({ text: formatWindowLine(provider, window, now) });
|
|
36
|
-
}
|
|
37
|
-
return lines;
|
|
38
|
-
}
|
|
39
|
-
function formatProviderError(provider) {
|
|
40
|
-
if (provider.status === "ok")
|
|
41
|
-
return null;
|
|
42
|
-
// Skip purely "skipped" providers (no auth configured) — they would spam
|
|
43
|
-
// the toast every time it fires. Surface only real errors.
|
|
44
|
-
if (provider.status === "skipped" || provider.status === "no-auth") {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
const reason = provider.error ?? provider.status;
|
|
48
|
-
return { text: `${provider.label}: ${reason}` };
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Render a CardsSnapshot to a multi-line toast string, or `null` if there
|
|
52
|
-
* is nothing meaningful to display.
|
|
53
|
-
*/
|
|
54
|
-
export function formatToastMessage(snapshot, opts = {}) {
|
|
55
|
-
const now = opts.now ?? new Date();
|
|
56
|
-
const maxWidth = opts.maxLineWidth ?? 60;
|
|
57
|
-
const lines = [];
|
|
58
|
-
for (const provider of snapshot.providers) {
|
|
59
|
-
const block = formatProviderBlock(provider, now);
|
|
60
|
-
if (block.length > 0) {
|
|
61
|
-
lines.push(...block);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
const errors = [];
|
|
65
|
-
for (const provider of snapshot.providers) {
|
|
66
|
-
const err = formatProviderError(provider);
|
|
67
|
-
if (err)
|
|
68
|
-
errors.push(err);
|
|
69
|
-
}
|
|
70
|
-
if (errors.length > 0) {
|
|
71
|
-
if (lines.length > 0)
|
|
72
|
-
lines.push({ text: "" });
|
|
73
|
-
lines.push(...errors);
|
|
74
|
-
}
|
|
75
|
-
if (lines.length === 0)
|
|
76
|
-
return null;
|
|
77
|
-
return lines.map((l) => l.text.slice(0, maxWidth)).join("\n");
|
|
78
|
-
}
|
|
79
|
-
//# sourceMappingURL=toast-format.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"toast-format.js","sourceRoot":"","sources":["../../src/lib/toast-format.ts"],"names":[],"mappings":"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"}
|