@krtclcdy/tersio 2.15.0 → 2.17.0
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.md +0 -18
- package/cli/common.js +17 -10
- package/cli/dashboard.js +4 -0
- package/cli/doctor.js +3 -0
- package/cli/install.js +109 -111
- package/cli/rtk-wiring.js +66 -8
- package/cli/update.js +54 -42
- package/cli/usage.js +3 -0
- package/dashboard/app.js +220 -55
- package/dashboard/styles.css +15 -4
- package/dashboard/template.html +29 -9
- package/extensions/shared/usage-ledger.js +17 -5
- package/package.json +1 -1
package/cli/rtk-wiring.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
// cli/rtk-wiring.ts — wire the installed rtk binary into OMP.
|
|
2
2
|
// `rtk init -g --agent omp` writes rtk's tool_call extension to
|
|
3
|
-
// ~/.omp/agent/extensions/rtk.ts
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// ~/.omp/agent/extensions/rtk.ts, and we register that path in config.yml
|
|
4
|
+
// (OMP only loads listed extensions — rtk init does not register it).
|
|
5
|
+
// Once loaded, OMP rewrites bash commands to rtk before execution, so every
|
|
6
|
+
// rewritten call lands in rtk's history.db and shows metered savings.
|
|
6
7
|
// Fail-open: a failed wire only downgrades to manual `rtk` prefixing.
|
|
7
8
|
// Deliberately free of cli/common.ts imports (argv side effects) so tests
|
|
8
9
|
// can load it directly.
|
|
10
|
+
import { promises as fs } from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
9
13
|
import { execFile } from 'node:child_process';
|
|
14
|
+
function debugWire(options, msg) {
|
|
15
|
+
if (!options.quiet)
|
|
16
|
+
console.log(` ${msg}`);
|
|
17
|
+
}
|
|
10
18
|
function shortWiringError(e) {
|
|
11
19
|
const err = e;
|
|
12
20
|
const why = [err.code !== undefined ? `code ${err.code}` : '', err.signal ?? '', err.stderr?.trim(), err.message]
|
|
@@ -29,14 +37,64 @@ function execFileP(cmd, args, timeout) {
|
|
|
29
37
|
// Idempotent: rtk rewrites its extension file on every init run, so
|
|
30
38
|
// reinstalling tersio refreshes the wiring for free.
|
|
31
39
|
export async function wireRtkOmp(rtkBin, options = {}) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
if (!options.dryRun)
|
|
41
|
+
debugWire(options, 'Wiring rtk → OMP (bash tool_call rewrite)…');
|
|
42
|
+
if (options.dryRun)
|
|
35
43
|
return true;
|
|
44
|
+
const wired = await runRtkInit(rtkBin, options);
|
|
45
|
+
if (!wired)
|
|
46
|
+
return false;
|
|
47
|
+
await ensureRtkInConfig(options);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// The rtk.ts path rtk init writes (mirrors OMP_AGENT_DIR in cli/common.ts
|
|
51
|
+
// without importing it — see header note on argv side effects).
|
|
52
|
+
function rtkExtensionPath() {
|
|
53
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
54
|
+
return path.join(home, '.omp', 'agent', 'extensions', 'rtk.ts');
|
|
55
|
+
}
|
|
56
|
+
// OMP loads only listed extensions: append rtk.ts after the existing entries
|
|
57
|
+
// (or create the key) so a fresh wire takes effect on next OMP start.
|
|
58
|
+
async function ensureRtkInConfig(options) {
|
|
59
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
60
|
+
const configPath = path.join(home, '.omp', 'agent', 'config.yml');
|
|
61
|
+
const extPath = rtkExtensionPath().replace(/\\/g, '/');
|
|
62
|
+
let raw = null;
|
|
63
|
+
try {
|
|
64
|
+
raw = await fs.readFile(configPath, 'utf8');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (raw.includes(extPath)) {
|
|
70
|
+
debugWire(options, 'rtk already in config.yml');
|
|
71
|
+
return;
|
|
36
72
|
}
|
|
73
|
+
const lines = raw.split('\n');
|
|
74
|
+
const keyIdx = lines.findIndex((l) => /^\s*extensions\s*:/i.test(l));
|
|
75
|
+
const line = ` - ${extPath}`;
|
|
76
|
+
if (keyIdx === -1) {
|
|
77
|
+
lines.push('extensions:', line, '');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
if (/^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i.test(lines[keyIdx]))
|
|
81
|
+
lines[keyIdx] = 'extensions:';
|
|
82
|
+
lines.splice(keyIdx + 1, 0, line);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
86
|
+
await fs.copyFile(configPath, `${configPath}.bak`).catch(() => { });
|
|
87
|
+
await fs.writeFile(configPath, lines.join('\n'), 'utf8');
|
|
88
|
+
debugWire(options, 'rtk registered in config.yml');
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
console.log(` [warn] could not register rtk in config.yml: ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function runRtkInit(rtkBin, options) {
|
|
37
95
|
try {
|
|
38
96
|
await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
|
|
39
|
-
|
|
97
|
+
debugWire(options, 'rtk OMP extension wired');
|
|
40
98
|
return true;
|
|
41
99
|
}
|
|
42
100
|
catch (first) {
|
|
@@ -45,7 +103,7 @@ export async function wireRtkOmp(rtkBin, options = {}) {
|
|
|
45
103
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
46
104
|
try {
|
|
47
105
|
await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
|
|
48
|
-
|
|
106
|
+
debugWire(options, 'rtk OMP extension wired');
|
|
49
107
|
return true;
|
|
50
108
|
}
|
|
51
109
|
catch (e) {
|
package/cli/update.js
CHANGED
|
@@ -116,15 +116,28 @@ async function probeUpdatePlan(cliLatest) {
|
|
|
116
116
|
ponytail: [ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null, ponytailLatest],
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
119
|
+
// Every add-on prints its status: version jump when stale, "up to date"
|
|
120
|
+
// otherwise, honest "unknown" when the probe cannot reach its source.
|
|
121
|
+
function planLines(plan) {
|
|
122
|
+
const stale = [];
|
|
123
|
+
const status = [];
|
|
124
|
+
const push = (name, current, latest) => {
|
|
125
|
+
if (!current && !latest)
|
|
126
|
+
status.push(`${name}: unknown`);
|
|
127
|
+
else if (!latest)
|
|
128
|
+
status.push(`${name}: ${current} (latest unknown)`);
|
|
129
|
+
else if (current === latest)
|
|
130
|
+
status.push(`${name}: ${current} (up to date)`);
|
|
131
|
+
else {
|
|
132
|
+
status.push(`${name}: ${current ?? 'missing'} → ${latest}`);
|
|
133
|
+
stale.push(`${name}: ${current ?? 'missing'} → ${latest}`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
push('Tersio', PACKAGE_VERSION, plan.cli);
|
|
137
|
+
push('RTK', plan.rtk[0], plan.rtk[1]);
|
|
138
|
+
push('Caveman rule', plan.rule[0], plan.rule[1]);
|
|
139
|
+
push('Ponytail', plan.ponytail[0], plan.ponytail[1]);
|
|
140
|
+
return { stale, status };
|
|
128
141
|
}
|
|
129
142
|
async function runLatestUpdate() {
|
|
130
143
|
const forwardedArgs = ['--yes'];
|
|
@@ -133,7 +146,6 @@ async function runLatestUpdate() {
|
|
|
133
146
|
if (verbose)
|
|
134
147
|
forwardedArgs.push('--verbose');
|
|
135
148
|
const npmCommand = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
|
|
136
|
-
console.log('=== Updating Tersio ===');
|
|
137
149
|
// Resolve the target explicitly: `npm view --prefer-online` beats the local
|
|
138
150
|
// metadata cache, so a stale `@latest` can never pin an older release.
|
|
139
151
|
const cliLatest = await latestPublishedVersion();
|
|
@@ -141,37 +153,38 @@ async function runLatestUpdate() {
|
|
|
141
153
|
// Current → latest per add-on. Best-effort; unreachable probes print as
|
|
142
154
|
// unknown and never block the update.
|
|
143
155
|
const plan = await probeUpdatePlan(cliLatest);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
const { stale, status } = planLines(plan);
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
console.log(`tersio update (dry-run):\n ${status.join('\n ')}`);
|
|
159
|
+
console.log(` [dry-run] would run: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
|
|
160
|
+
console.log(` [dry-run] would delegate: npm exec --yes --prefer-online --package=${PACKAGE_NAME}${target} -- tersio --apply-update ${forwardedArgs.join(' ')}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
console.log(`Checking for updates:\n ${status.join('\n ')}`);
|
|
164
|
+
if (stale.length === 0) {
|
|
165
|
+
console.log(`tersio ${PACKAGE_VERSION} — up to date`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
console.log(`Updating ${stale.join(', ')}`);
|
|
149
169
|
// The npx delegation below only refreshes the OMP-side files; the globally
|
|
150
170
|
// installed CLI keeps its old version until npm -g runs. Refresh both.
|
|
151
171
|
const globalArgs = IS_WINDOWS
|
|
152
172
|
? ['/d', '/s', '/c', 'npm', 'install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online']
|
|
153
173
|
: ['install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online'];
|
|
154
|
-
|
|
155
|
-
|
|
174
|
+
try {
|
|
175
|
+
const globalResult = await execNetwork(`Updating global Tersio CLI to ${PACKAGE_NAME}${target}`, npmCommand, globalArgs, {
|
|
176
|
+
timeout: 300000,
|
|
177
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
178
|
+
windowsHide: true,
|
|
179
|
+
shell: false,
|
|
180
|
+
});
|
|
181
|
+
// npm -g echoes its own added/changed summary; only failures surface here.
|
|
182
|
+
if (globalResult.stderr)
|
|
183
|
+
process.stderr.write(globalResult.stderr);
|
|
156
184
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
timeout: 300000,
|
|
161
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
162
|
-
windowsHide: true,
|
|
163
|
-
shell: false,
|
|
164
|
-
});
|
|
165
|
-
if (globalResult.stdout)
|
|
166
|
-
process.stdout.write(globalResult.stdout);
|
|
167
|
-
if (globalResult.stderr)
|
|
168
|
-
process.stderr.write(globalResult.stderr);
|
|
169
|
-
console.log(` [ok] CLI updated to ${PACKAGE_NAME}${target}`);
|
|
170
|
-
}
|
|
171
|
-
catch (e) {
|
|
172
|
-
console.log(` [warn] Global CLI update skipped: ${e.message}`);
|
|
173
|
-
console.log(` [hint] Manual: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
|
|
174
|
-
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
console.log(` [warn] Global CLI update skipped: ${e.message}`);
|
|
187
|
+
console.log(` [hint] Manual: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
|
|
175
188
|
}
|
|
176
189
|
const npmArgs = [
|
|
177
190
|
'exec',
|
|
@@ -184,23 +197,22 @@ async function runLatestUpdate() {
|
|
|
184
197
|
...forwardedArgs,
|
|
185
198
|
];
|
|
186
199
|
const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
|
|
187
|
-
// Inherited stdio, no outer spinner
|
|
188
|
-
//
|
|
189
|
-
|
|
200
|
+
// Inherited stdio, no outer spinner. The delegated installer runs quiet
|
|
201
|
+
// (banner and per-file writes suppressed); this parent owns both the plan
|
|
202
|
+
// line above and the closing summary below — one voice, no repeats.
|
|
190
203
|
try {
|
|
191
204
|
await execInherit(npmCommand, npmCommandArgs);
|
|
192
|
-
console.log(
|
|
205
|
+
console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION}. Restart OMP.`);
|
|
193
206
|
if (!dryRun) {
|
|
194
207
|
try {
|
|
195
|
-
|
|
196
|
-
console.log(' [ok] model price table refreshed');
|
|
208
|
+
await refreshPrices();
|
|
197
209
|
}
|
|
198
210
|
catch { /* pricing is best-effort; update already succeeded */ }
|
|
199
211
|
}
|
|
200
212
|
}
|
|
201
213
|
catch (e) {
|
|
202
214
|
const err = e;
|
|
203
|
-
console.error(
|
|
215
|
+
console.error(`[fail] Could not run ${PACKAGE_NAME}${target}: ${err.message}`);
|
|
204
216
|
process.exitCode = 1;
|
|
205
217
|
}
|
|
206
218
|
}
|
package/cli/usage.js
CHANGED
|
@@ -18,12 +18,14 @@ export function summarizeUsage(rows) {
|
|
|
18
18
|
let priced = true;
|
|
19
19
|
let savedUsd = 0;
|
|
20
20
|
const byModelUsd = {};
|
|
21
|
+
const byModelBucketUsd = {};
|
|
21
22
|
let co2g = 0;
|
|
22
23
|
let energyWh = 0;
|
|
23
24
|
for (const [model, t] of Object.entries(session.byModel)) {
|
|
24
25
|
const c = usdCost(t, model);
|
|
25
26
|
usd += c.usd;
|
|
26
27
|
byModelUsd[model] = c.usd;
|
|
28
|
+
byModelBucketUsd[model] = c.buckets;
|
|
27
29
|
co2g += co2GramsFor(model, t.output);
|
|
28
30
|
energyWh += energyWhFor(model, t.output);
|
|
29
31
|
if (!c.priced)
|
|
@@ -41,6 +43,7 @@ export function summarizeUsage(rows) {
|
|
|
41
43
|
tokens: session.totals,
|
|
42
44
|
byModel: session.byModel,
|
|
43
45
|
byModelUsd,
|
|
46
|
+
byModelBucketUsd,
|
|
44
47
|
byModelMessages: session.byModelMessages,
|
|
45
48
|
byDay: session.byDay,
|
|
46
49
|
byDayModel: session.byDayModel,
|