opencode-usage-coach 0.1.1 → 0.1.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/agents/usage-coach-harness.md +3 -1
- package/dist/index.js +92 -16
- package/dist/tui.js +179 -85
- package/package.json +13 -4
|
@@ -32,8 +32,10 @@ Default to the loop only when it genuinely adds value. Do not over-engineer smal
|
|
|
32
32
|
## Harness loop (only for substantive work)
|
|
33
33
|
The user's message is the task source. If it has multiple distinct parts, decompose into discrete tasks (N); if it is one unit, N = 1.
|
|
34
34
|
|
|
35
|
+
**Parallel dispatch (independent tasks):** If the decomposed tasks are INDEPENDENT (no data dependency), dispatch them concurrently — issue **multiple `task` calls in the same turn** so they run as parallel subagents. (This is the only reliable parallel path — the deterministic script cannot parallelize due to opencode's single-server model.) Cap concurrency by quota coaching: "big tasks OK" → up to 3-4 parallel; "moderate/small" → 1-2; STOP → don't dispatch. For DEPENDENT tasks (B needs A's output), run sequentially.
|
|
36
|
+
|
|
35
37
|
1. Call `harness_start(name, N)` to register the run on the panel.
|
|
36
|
-
2. For each task i (1..N):
|
|
38
|
+
2. For each task i (1..N) — in parallel batches when independent:
|
|
37
39
|
a. `task_update(i, title, "generating")`.
|
|
38
40
|
b. **Generate** — delegate to a subagent via the `task` tool (`subagent_type: "general"`):
|
|
39
41
|
prompt: `"Task: {title}. Perform it for real in the current directory (write/edit files, run commands as needed)."`
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { mkdirSync, writeFileSync, appendFileSync, readFileSync, existsSync } from "fs";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
|
+
import { createHash } from "crypto";
|
|
4
5
|
import { homedir } from "os";
|
|
5
|
-
import { join } from "path";
|
|
6
|
+
import { join, resolve } from "path";
|
|
6
7
|
import { tool } from "@opencode-ai/plugin";
|
|
7
8
|
var PLUGIN_NAME = "opencode-usage-coach";
|
|
8
|
-
var
|
|
9
|
+
var DEBUG = process.env.UC_DEBUG === "1";
|
|
10
|
+
var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
|
|
11
|
+
var STATE_DIR = join(homedir(), ".cache", "opencode-usage-coach");
|
|
9
12
|
var STATE_FILE = join(STATE_DIR, "state.json");
|
|
10
13
|
var HARNESS_FILE = join(STATE_DIR, "harness.json");
|
|
11
14
|
var LOG_FILE = join(STATE_DIR, "coach.log");
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
function projectStateDir(dir) {
|
|
16
|
+
const abs = resolve(dir || ".");
|
|
17
|
+
const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
|
|
18
|
+
return join(homedir(), ".cache", "opencode-usage-coach", "projects", h);
|
|
19
|
+
}
|
|
20
|
+
function setStateDir(dir) {
|
|
21
|
+
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(dir);
|
|
22
|
+
STATE_FILE = join(STATE_DIR, "state.json");
|
|
23
|
+
HARNESS_FILE = join(STATE_DIR, "harness.json");
|
|
24
|
+
LOG_FILE = join(STATE_DIR, "coach.log");
|
|
25
|
+
}
|
|
14
26
|
var NOOP_HOOKS = {};
|
|
15
27
|
function log(msg) {
|
|
16
28
|
if (DEBUG) try {
|
|
@@ -63,14 +75,72 @@ function humanRemaining(iso) {
|
|
|
63
75
|
const mins = Math.floor((new Date(iso).getTime() - Date.now()) / 6e4);
|
|
64
76
|
if (mins < 0) return "resets soon";
|
|
65
77
|
if (mins < 60) return `resets in ${mins}m`;
|
|
66
|
-
if (mins < 1440) return `resets in ${Math.floor(mins / 60)}h`;
|
|
67
|
-
return
|
|
78
|
+
if (mins < 1440) return `resets in ${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
79
|
+
return `${Math.floor(mins / 1440)}d left`;
|
|
68
80
|
} catch {
|
|
69
81
|
return "";
|
|
70
82
|
}
|
|
71
83
|
}
|
|
84
|
+
function captureStdout(args) {
|
|
85
|
+
return new Promise((resolve2) => {
|
|
86
|
+
let out = "";
|
|
87
|
+
let p;
|
|
88
|
+
try {
|
|
89
|
+
p = spawn("codexbar", args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
90
|
+
} catch {
|
|
91
|
+
return resolve2("");
|
|
92
|
+
}
|
|
93
|
+
p.stdout?.on("data", (d) => {
|
|
94
|
+
out += d.toString();
|
|
95
|
+
});
|
|
96
|
+
p.on("error", () => resolve2(""));
|
|
97
|
+
p.on("close", () => resolve2(out));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async function fetchEnabledProviders() {
|
|
101
|
+
const out = await captureStdout(["config", "providers"]);
|
|
102
|
+
const ids = [];
|
|
103
|
+
for (const line of out.split("\n")) {
|
|
104
|
+
const m = line.match(/^\s*([a-zA-Z0-9_-]+):\s*enabled/);
|
|
105
|
+
if (m) ids.push(m[1]);
|
|
106
|
+
}
|
|
107
|
+
return ids;
|
|
108
|
+
}
|
|
109
|
+
function providerAdvice(h5, wk) {
|
|
110
|
+
const S5H = STOP_5H, SWK = STOP_WK, T5H = THR_5H, TWK = THR_WK;
|
|
111
|
+
if (h5 >= S5H || wk >= SWK) return "STOP \u2014 finish current only";
|
|
112
|
+
if (h5 >= T5H && wk >= TWK) return "small tasks only \u2014 big ones will hit both limits";
|
|
113
|
+
if (h5 >= T5H) return "small tasks only \u2014 5h window nearly full, big tasks after reset";
|
|
114
|
+
if (wk >= TWK) return "small tasks only \u2014 big ones will strain late-week";
|
|
115
|
+
if (h5 >= 50 || wk >= 50) return "moderate tasks OK \u2014 save big ones for headroom";
|
|
116
|
+
return "big tasks OK \u2014 short & long limits comfortable";
|
|
117
|
+
}
|
|
118
|
+
async function fetchProvidersCoach() {
|
|
119
|
+
const ids = await fetchEnabledProviders();
|
|
120
|
+
const results = await Promise.all(ids.map(async (id) => {
|
|
121
|
+
try {
|
|
122
|
+
const out = await captureStdout(["usage", "--provider", id, "--json"]);
|
|
123
|
+
const u = JSON.parse(out)[0]?.usage;
|
|
124
|
+
if (!u) return null;
|
|
125
|
+
const h5 = Math.round(u.tertiary?.usedPercent ?? 0);
|
|
126
|
+
const wk = Math.round(u.primary?.usedPercent ?? 0);
|
|
127
|
+
return {
|
|
128
|
+
id,
|
|
129
|
+
name: id,
|
|
130
|
+
fiveHour: h5,
|
|
131
|
+
weekly: wk,
|
|
132
|
+
fiveHourReset: humanRemaining(u.tertiary?.resetsAt),
|
|
133
|
+
weeklyReset: humanRemaining(u.primary?.resetsAt),
|
|
134
|
+
advice: providerAdvice(h5, wk)
|
|
135
|
+
};
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}));
|
|
140
|
+
return results.filter(Boolean);
|
|
141
|
+
}
|
|
72
142
|
function fetchQuota() {
|
|
73
|
-
return new Promise((
|
|
143
|
+
return new Promise((resolve2) => {
|
|
74
144
|
let out = "";
|
|
75
145
|
let p;
|
|
76
146
|
try {
|
|
@@ -78,21 +148,21 @@ function fetchQuota() {
|
|
|
78
148
|
stdio: ["ignore", "pipe", "ignore"]
|
|
79
149
|
});
|
|
80
150
|
} catch {
|
|
81
|
-
return
|
|
151
|
+
return resolve2(null);
|
|
82
152
|
}
|
|
83
153
|
p.stdout?.on("data", (d) => {
|
|
84
154
|
out += d.toString();
|
|
85
155
|
});
|
|
86
|
-
p.on("error", () =>
|
|
156
|
+
p.on("error", () => resolve2(null));
|
|
87
157
|
p.on("close", () => {
|
|
88
158
|
try {
|
|
89
159
|
const text = out.trim();
|
|
90
|
-
if (!text || text === "[]") return
|
|
160
|
+
if (!text || text === "[]") return resolve2(null);
|
|
91
161
|
const u = JSON.parse(text)[0]?.usage;
|
|
92
|
-
if (!u) return
|
|
93
|
-
|
|
162
|
+
if (!u) return resolve2(null);
|
|
163
|
+
resolve2({ weekly: u.primary ?? { usedPercent: 0 }, monthly: u.secondary ?? { usedPercent: 0 }, fiveHour: u.tertiary ?? { usedPercent: 0 } });
|
|
94
164
|
} catch {
|
|
95
|
-
|
|
165
|
+
resolve2(null);
|
|
96
166
|
}
|
|
97
167
|
});
|
|
98
168
|
});
|
|
@@ -113,6 +183,7 @@ function coach(q) {
|
|
|
113
183
|
var LOADING = { decision: "GO", advice: "quota loading\u2026", weekly: -1, monthly: -1, fiveHour: -1 };
|
|
114
184
|
async function UsageCoachPlugin(input) {
|
|
115
185
|
try {
|
|
186
|
+
setStateDir(input.directory);
|
|
116
187
|
let last = null;
|
|
117
188
|
let lastFetchedAt = 0;
|
|
118
189
|
let refreshing = false;
|
|
@@ -121,12 +192,17 @@ async function UsageCoachPlugin(input) {
|
|
|
121
192
|
if (refreshing) return;
|
|
122
193
|
if (last && Date.now() - lastFetchedAt < TTL_MS) return;
|
|
123
194
|
refreshing = true;
|
|
124
|
-
fetchQuota().then((q) => {
|
|
195
|
+
fetchQuota().then(async (q) => {
|
|
125
196
|
try {
|
|
126
197
|
last = coach(q);
|
|
127
198
|
lastFetchedAt = Date.now();
|
|
128
|
-
|
|
129
|
-
|
|
199
|
+
let providers = [];
|
|
200
|
+
try {
|
|
201
|
+
providers = await fetchProvidersCoach();
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
writeState({ ...last, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
205
|
+
log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
|
|
130
206
|
} catch (e) {
|
|
131
207
|
log(`refresh-in-then err: ${String(e)}`);
|
|
132
208
|
}
|
package/dist/tui.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
// src/tui.tsx
|
|
2
|
-
import { setProp as _$setProp } from "@opentui/solid";
|
|
3
2
|
import { insert as _$insert } from "@opentui/solid";
|
|
3
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
4
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
4
5
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
5
6
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
6
7
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
7
8
|
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
|
|
9
|
+
import { createHash } from "crypto";
|
|
8
10
|
import { homedir } from "os";
|
|
9
|
-
import { join } from "path";
|
|
11
|
+
import { join, resolve } from "path";
|
|
10
12
|
import { createRoot, createSignal, onCleanup } from "solid-js";
|
|
11
|
-
|
|
13
|
+
function projectStateDir(dir) {
|
|
14
|
+
const abs = resolve(dir || ".");
|
|
15
|
+
const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
|
|
16
|
+
return join(homedir(), ".cache", "opencode-usage-coach", "projects", h);
|
|
17
|
+
}
|
|
18
|
+
var STATE_DIR = join(homedir(), ".cache", "opencode-usage-coach");
|
|
12
19
|
var STATE_FILE = join(STATE_DIR, "state.json");
|
|
13
20
|
var HARNESS_FILE = join(STATE_DIR, "harness.json");
|
|
14
21
|
var MARKER = join(STATE_DIR, "tui-loaded.txt");
|
|
@@ -50,11 +57,15 @@ function short(s, n) {
|
|
|
50
57
|
return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
|
|
51
58
|
}
|
|
52
59
|
function initializeTui(api, disposeRoot) {
|
|
60
|
+
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(api.state.path.directory);
|
|
61
|
+
STATE_FILE = join(STATE_DIR, "state.json");
|
|
62
|
+
HARNESS_FILE = join(STATE_DIR, "harness.json");
|
|
63
|
+
MARKER = join(STATE_DIR, "tui-loaded.txt");
|
|
53
64
|
try {
|
|
54
65
|
mkdirSync(STATE_DIR, {
|
|
55
66
|
recursive: true
|
|
56
67
|
});
|
|
57
|
-
writeFileSync(MARKER, `loaded ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
68
|
+
writeFileSync(MARKER, `loaded ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
|
|
58
69
|
} catch {
|
|
59
70
|
}
|
|
60
71
|
const [getState, setState] = createSignal(readState());
|
|
@@ -68,16 +79,49 @@ function initializeTui(api, disposeRoot) {
|
|
|
68
79
|
}, 3e3);
|
|
69
80
|
onCleanup(() => clearInterval(timer));
|
|
70
81
|
onCleanup(() => disposeRoot());
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
const [collapsed, setCollapsed] = createSignal(false);
|
|
83
|
+
let cmdDispose;
|
|
84
|
+
try {
|
|
85
|
+
cmdDispose = api.command?.register?.(() => [{
|
|
86
|
+
title: "Toggle usage-coach panel",
|
|
87
|
+
value: "usage-coach-toggle",
|
|
88
|
+
category: "usage-coach",
|
|
89
|
+
keybind: "alt+h",
|
|
90
|
+
onSelect: () => {
|
|
91
|
+
setCollapsed((c) => !c);
|
|
92
|
+
}
|
|
93
|
+
}]);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
onCleanup(() => {
|
|
97
|
+
try {
|
|
98
|
+
cmdDispose?.();
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
const statusKey = {
|
|
103
|
+
generating: "info",
|
|
104
|
+
grading: "accent",
|
|
105
|
+
revising: "warning",
|
|
106
|
+
completed: "success",
|
|
107
|
+
failed: "error",
|
|
108
|
+
timed_out: "error",
|
|
109
|
+
halted_quota: "error"
|
|
79
110
|
};
|
|
80
111
|
const panel = (ctx) => {
|
|
112
|
+
const th = ctx.theme?.current ?? {};
|
|
113
|
+
const st = (k) => ({
|
|
114
|
+
fg: th[k]
|
|
115
|
+
});
|
|
116
|
+
if (collapsed()) {
|
|
117
|
+
return (() => {
|
|
118
|
+
var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
|
|
119
|
+
_$insertNode(_el$, _el$2);
|
|
120
|
+
_$insertNode(_el$2, _$createTextNode(`usage-coach (hidden \u2014 Alt+H)`));
|
|
121
|
+
_$effect((_$p) => _$setProp(_el$2, "style", st("textMuted"), _$p));
|
|
122
|
+
return _el$;
|
|
123
|
+
})();
|
|
124
|
+
}
|
|
81
125
|
let s = null;
|
|
82
126
|
try {
|
|
83
127
|
s = getState();
|
|
@@ -92,98 +136,148 @@ function initializeTui(api, disposeRoot) {
|
|
|
92
136
|
}
|
|
93
137
|
const nodes = [];
|
|
94
138
|
if (s) {
|
|
139
|
+
const dKey = s.decision === "GO" ? "success" : s.decision === "THROTTLE" ? "warning" : "error";
|
|
95
140
|
nodes.push((() => {
|
|
96
|
-
var _el$ = _$createElement("text"), _el$
|
|
97
|
-
_$insertNode(_el$, _el$2);
|
|
98
|
-
_$insertNode(_el$, _el$3);
|
|
99
|
-
_$insert(_el$, () => TAG[s.decision], _el$3);
|
|
100
|
-
return _el$;
|
|
101
|
-
})());
|
|
102
|
-
nodes.push((() => {
|
|
103
|
-
var _el$4 = _$createElement("text"), _el$5 = _$createTextNode(` 5h `), _el$6 = _$createTextNode(` `), _el$7 = _$createTextNode(`%`);
|
|
141
|
+
var _el$4 = _$createElement("text"), _el$5 = _$createTextNode(`usage-coach [`), _el$6 = _$createTextNode(`]`);
|
|
104
142
|
_$insertNode(_el$4, _el$5);
|
|
105
143
|
_$insertNode(_el$4, _el$6);
|
|
106
|
-
_$
|
|
107
|
-
_$
|
|
108
|
-
_$insert(_el$4, () => s.fiveHour, _el$7);
|
|
144
|
+
_$insert(_el$4, () => TAG[s.decision], _el$6);
|
|
145
|
+
_$effect((_$p) => _$setProp(_el$4, "style", st(dKey), _$p));
|
|
109
146
|
return _el$4;
|
|
110
147
|
})());
|
|
148
|
+
if (s.providers && s.providers.length > 0) {
|
|
149
|
+
for (const p of s.providers) {
|
|
150
|
+
nodes.push((() => {
|
|
151
|
+
var _el$7 = _$createElement("text"), _el$8 = _$createTextNode(` `);
|
|
152
|
+
_$insertNode(_el$7, _el$8);
|
|
153
|
+
_$insert(_el$7, () => p.name, null);
|
|
154
|
+
_$effect((_$p) => _$setProp(_el$7, "style", st("textMuted"), _$p));
|
|
155
|
+
return _el$7;
|
|
156
|
+
})());
|
|
157
|
+
const k5 = p.fiveHour >= 70 ? "error" : p.fiveHour >= 40 ? "warning" : "success";
|
|
158
|
+
const kw = p.weekly >= 85 ? "error" : p.weekly >= 60 ? "warning" : "success";
|
|
159
|
+
nodes.push((() => {
|
|
160
|
+
var _el$9 = _$createElement("text"), _el$0 = _$createTextNode(` 5h `), _el$1 = _$createTextNode(` `), _el$10 = _$createTextNode(`% `);
|
|
161
|
+
_$insertNode(_el$9, _el$0);
|
|
162
|
+
_$insertNode(_el$9, _el$1);
|
|
163
|
+
_$insertNode(_el$9, _el$10);
|
|
164
|
+
_$insert(_el$9, () => bar(p.fiveHour), _el$1);
|
|
165
|
+
_$insert(_el$9, () => p.fiveHour, _el$10);
|
|
166
|
+
_$insert(_el$9, () => p.fiveHourReset, null);
|
|
167
|
+
_$effect((_$p) => _$setProp(_el$9, "style", st(k5), _$p));
|
|
168
|
+
return _el$9;
|
|
169
|
+
})());
|
|
170
|
+
nodes.push((() => {
|
|
171
|
+
var _el$11 = _$createElement("text"), _el$12 = _$createTextNode(` wk `), _el$13 = _$createTextNode(` `), _el$14 = _$createTextNode(`% `);
|
|
172
|
+
_$insertNode(_el$11, _el$12);
|
|
173
|
+
_$insertNode(_el$11, _el$13);
|
|
174
|
+
_$insertNode(_el$11, _el$14);
|
|
175
|
+
_$insert(_el$11, () => bar(p.weekly), _el$13);
|
|
176
|
+
_$insert(_el$11, () => p.weekly, _el$14);
|
|
177
|
+
_$insert(_el$11, () => p.weeklyReset, null);
|
|
178
|
+
_$effect((_$p) => _$setProp(_el$11, "style", st(kw), _$p));
|
|
179
|
+
return _el$11;
|
|
180
|
+
})());
|
|
181
|
+
nodes.push((() => {
|
|
182
|
+
var _el$15 = _$createElement("text"), _el$16 = _$createTextNode(` -> `);
|
|
183
|
+
_$insertNode(_el$15, _el$16);
|
|
184
|
+
_$insert(_el$15, () => p.advice, null);
|
|
185
|
+
_$effect((_$p) => _$setProp(_el$15, "style", st(dKey), _$p));
|
|
186
|
+
return _el$15;
|
|
187
|
+
})());
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
nodes.push((() => {
|
|
191
|
+
var _el$19 = _$createElement("text"), _el$20 = _$createTextNode(` 5h `), _el$21 = _$createTextNode(` `), _el$22 = _$createTextNode(`%`);
|
|
192
|
+
_$insertNode(_el$19, _el$20);
|
|
193
|
+
_$insertNode(_el$19, _el$21);
|
|
194
|
+
_$insertNode(_el$19, _el$22);
|
|
195
|
+
_$insert(_el$19, () => bar(s.fiveHour), _el$21);
|
|
196
|
+
_$insert(_el$19, () => s.fiveHour, _el$22);
|
|
197
|
+
return _el$19;
|
|
198
|
+
})());
|
|
199
|
+
nodes.push((() => {
|
|
200
|
+
var _el$23 = _$createElement("text"), _el$24 = _$createTextNode(` wk `), _el$25 = _$createTextNode(` `), _el$26 = _$createTextNode(`%`);
|
|
201
|
+
_$insertNode(_el$23, _el$24);
|
|
202
|
+
_$insertNode(_el$23, _el$25);
|
|
203
|
+
_$insertNode(_el$23, _el$26);
|
|
204
|
+
_$insert(_el$23, () => bar(s.weekly), _el$25);
|
|
205
|
+
_$insert(_el$23, () => s.weekly, _el$26);
|
|
206
|
+
return _el$23;
|
|
207
|
+
})());
|
|
208
|
+
nodes.push((() => {
|
|
209
|
+
var _el$27 = _$createElement("text"), _el$28 = _$createTextNode(` mo `), _el$29 = _$createTextNode(` `), _el$30 = _$createTextNode(`%`);
|
|
210
|
+
_$insertNode(_el$27, _el$28);
|
|
211
|
+
_$insertNode(_el$27, _el$29);
|
|
212
|
+
_$insertNode(_el$27, _el$30);
|
|
213
|
+
_$insert(_el$27, () => bar(s.monthly), _el$29);
|
|
214
|
+
_$insert(_el$27, () => s.monthly, _el$30);
|
|
215
|
+
return _el$27;
|
|
216
|
+
})());
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
111
219
|
nodes.push((() => {
|
|
112
|
-
var _el$
|
|
113
|
-
_$insertNode(_el$
|
|
114
|
-
|
|
115
|
-
_$insertNode(_el$8, _el$1);
|
|
116
|
-
_$insert(_el$8, () => bar(s.weekly), _el$0);
|
|
117
|
-
_$insert(_el$8, () => s.weekly, _el$1);
|
|
118
|
-
return _el$8;
|
|
220
|
+
var _el$31 = _$createElement("text");
|
|
221
|
+
_$insertNode(_el$31, _$createTextNode(`usage-coach: ...`));
|
|
222
|
+
return _el$31;
|
|
119
223
|
})());
|
|
224
|
+
}
|
|
225
|
+
if (h && h.active !== false && h.tasks.length > 0) {
|
|
120
226
|
nodes.push((() => {
|
|
121
|
-
var _el$
|
|
122
|
-
_$insertNode(_el$
|
|
123
|
-
|
|
124
|
-
_$insertNode(_el$10, _el$13);
|
|
125
|
-
_$insert(_el$10, () => bar(s.monthly), _el$12);
|
|
126
|
-
_$insert(_el$10, () => s.monthly, _el$13);
|
|
127
|
-
return _el$10;
|
|
227
|
+
var _el$33 = _$createElement("text");
|
|
228
|
+
_$insertNode(_el$33, _$createTextNode(` `));
|
|
229
|
+
return _el$33;
|
|
128
230
|
})());
|
|
129
|
-
} else {
|
|
130
231
|
nodes.push((() => {
|
|
131
|
-
var _el$
|
|
132
|
-
_$insertNode(_el$
|
|
133
|
-
|
|
232
|
+
var _el$35 = _$createElement("text"), _el$36 = _$createTextNode(`harness: `), _el$37 = _$createTextNode(` `), _el$38 = _$createTextNode(`/`);
|
|
233
|
+
_$insertNode(_el$35, _el$36);
|
|
234
|
+
_$insertNode(_el$35, _el$37);
|
|
235
|
+
_$insertNode(_el$35, _el$38);
|
|
236
|
+
_$insert(_el$35, () => h.name, _el$37);
|
|
237
|
+
_$insert(_el$35, () => h.current, _el$38);
|
|
238
|
+
_$insert(_el$35, () => h.total, null);
|
|
239
|
+
_$effect((_$p) => _$setProp(_el$35, "style", st("textMuted"), _$p));
|
|
240
|
+
return _el$35;
|
|
134
241
|
})());
|
|
135
|
-
}
|
|
136
|
-
const hStatus = h && h.tasks.length > 0 ? `${h.name} ${h.current}/${h.total}` : "idle";
|
|
137
|
-
nodes.push((() => {
|
|
138
|
-
var _el$16 = _$createElement("text");
|
|
139
|
-
_$insertNode(_el$16, _$createTextNode(` `));
|
|
140
|
-
return _el$16;
|
|
141
|
-
})());
|
|
142
|
-
nodes.push((() => {
|
|
143
|
-
var _el$18 = _$createElement("text"), _el$19 = _$createTextNode(`harness: `);
|
|
144
|
-
_$insertNode(_el$18, _el$19);
|
|
145
|
-
_$insert(_el$18, hStatus, null);
|
|
146
|
-
return _el$18;
|
|
147
|
-
})());
|
|
148
|
-
if (h && h.tasks.length > 0) {
|
|
149
242
|
for (const t of h.tasks) {
|
|
150
|
-
const
|
|
243
|
+
const sKey = statusKey[t.status] ?? "text";
|
|
151
244
|
const lbl = TLABEL[t.status] ?? t.status;
|
|
152
245
|
const rev = t.revisions > 0 && t.status === "revising" ? `(${t.revisions})` : "";
|
|
153
246
|
const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
|
|
154
247
|
nodes.push((() => {
|
|
155
|
-
var _el$
|
|
156
|
-
_$insertNode(_el$
|
|
157
|
-
_$insertNode(_el$
|
|
158
|
-
_$insertNode(_el$
|
|
159
|
-
_$
|
|
160
|
-
_$insert(_el$
|
|
161
|
-
_$insert(_el$
|
|
162
|
-
_$insert(_el$
|
|
163
|
-
_$insert(_el$
|
|
164
|
-
_$
|
|
165
|
-
|
|
166
|
-
return _el$20;
|
|
248
|
+
var _el$39 = _$createElement("text"), _el$40 = _$createTextNode(` \u25CF `), _el$41 = _$createTextNode(` `), _el$42 = _$createTextNode(` `);
|
|
249
|
+
_$insertNode(_el$39, _el$40);
|
|
250
|
+
_$insertNode(_el$39, _el$41);
|
|
251
|
+
_$insertNode(_el$39, _el$42);
|
|
252
|
+
_$insert(_el$39, () => t.id, _el$41);
|
|
253
|
+
_$insert(_el$39, mdl, _el$41);
|
|
254
|
+
_$insert(_el$39, lbl, _el$42);
|
|
255
|
+
_$insert(_el$39, rev, _el$42);
|
|
256
|
+
_$insert(_el$39, () => short(t.title, 12), null);
|
|
257
|
+
_$effect((_$p) => _$setProp(_el$39, "style", st(sKey), _$p));
|
|
258
|
+
return _el$39;
|
|
167
259
|
})());
|
|
168
260
|
const pv = t.model ? t.model.startsWith("zai") ? "zai" : (t.model.split("/")[0] ?? "").split("-")[0] : "";
|
|
169
261
|
const q = pv && h.quotas?.[pv] ? h.quotas[pv] : null;
|
|
170
262
|
const pct = q ? q.fiveHour : 0;
|
|
263
|
+
const qKey = pct >= 70 ? "error" : pct >= 40 ? "warning" : "success";
|
|
171
264
|
nodes.push((() => {
|
|
172
|
-
var _el$
|
|
173
|
-
_$insertNode(_el$
|
|
174
|
-
_$insertNode(_el$
|
|
175
|
-
_$insertNode(_el$
|
|
176
|
-
_$insert(_el$
|
|
177
|
-
_$insert(_el$
|
|
178
|
-
|
|
265
|
+
var _el$43 = _$createElement("text"), _el$44 = _$createTextNode(` 5h `), _el$45 = _$createTextNode(` `), _el$46 = _$createTextNode(`%`);
|
|
266
|
+
_$insertNode(_el$43, _el$44);
|
|
267
|
+
_$insertNode(_el$43, _el$45);
|
|
268
|
+
_$insertNode(_el$43, _el$46);
|
|
269
|
+
_$insert(_el$43, () => bar(pct), _el$45);
|
|
270
|
+
_$insert(_el$43, pct, _el$46);
|
|
271
|
+
_$effect((_$p) => _$setProp(_el$43, "style", st(qKey), _$p));
|
|
272
|
+
return _el$43;
|
|
179
273
|
})());
|
|
180
274
|
}
|
|
181
275
|
}
|
|
182
276
|
return (() => {
|
|
183
|
-
var _el$
|
|
184
|
-
_$setProp(_el$
|
|
185
|
-
_$insert(_el$
|
|
186
|
-
return _el$
|
|
277
|
+
var _el$47 = _$createElement("box");
|
|
278
|
+
_$setProp(_el$47, "flexDirection", "column");
|
|
279
|
+
_$insert(_el$47, nodes);
|
|
280
|
+
return _el$47;
|
|
187
281
|
})();
|
|
188
282
|
};
|
|
189
283
|
api.slots.register({
|
|
@@ -194,9 +288,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
194
288
|
return panel(ctx);
|
|
195
289
|
} catch {
|
|
196
290
|
return (() => {
|
|
197
|
-
var _el$
|
|
198
|
-
_$insertNode(_el$
|
|
199
|
-
return _el$
|
|
291
|
+
var _el$48 = _$createElement("text");
|
|
292
|
+
_$insertNode(_el$48, _$createTextNode(`usage-coach`));
|
|
293
|
+
return _el$48;
|
|
200
294
|
})();
|
|
201
295
|
}
|
|
202
296
|
},
|
|
@@ -205,9 +299,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
205
299
|
return panel(ctx);
|
|
206
300
|
} catch {
|
|
207
301
|
return (() => {
|
|
208
|
-
var _el$
|
|
209
|
-
_$insertNode(_el$
|
|
210
|
-
return _el$
|
|
302
|
+
var _el$50 = _$createElement("text");
|
|
303
|
+
_$insertNode(_el$50, _$createTextNode(`usage-coach`));
|
|
304
|
+
return _el$50;
|
|
211
305
|
})();
|
|
212
306
|
}
|
|
213
307
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-usage-coach",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "opencode closed-loop usage coach
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "opencode closed-loop usage coach \u2014 quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -26,7 +26,16 @@
|
|
|
26
26
|
"README.md",
|
|
27
27
|
"LICENSE"
|
|
28
28
|
],
|
|
29
|
-
"keywords": [
|
|
29
|
+
"keywords": [
|
|
30
|
+
"opencode",
|
|
31
|
+
"opencode-plugin",
|
|
32
|
+
"quota",
|
|
33
|
+
"usage",
|
|
34
|
+
"coach",
|
|
35
|
+
"loop",
|
|
36
|
+
"zai",
|
|
37
|
+
"glm"
|
|
38
|
+
],
|
|
30
39
|
"license": "MIT",
|
|
31
40
|
"peerDependencies": {
|
|
32
41
|
"@opencode-ai/plugin": ">=1.14",
|
|
@@ -43,4 +52,4 @@
|
|
|
43
52
|
"tsup": "^8.5",
|
|
44
53
|
"typescript": "^5"
|
|
45
54
|
}
|
|
46
|
-
}
|
|
55
|
+
}
|