@krtclcdy/tersio 2.25.4 → 2.25.5
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 +13 -30
- package/dashboard/dist/index.html +18 -16
- package/dist/cli/backups.js +194 -0
- package/dist/cli/common.js +2 -2
- package/dist/cli/dashboard.js +125 -98
- package/dist/cli/doctor-fix.js +41 -44
- package/dist/cli/doctor.js +69 -7
- package/dist/cli/hosts.js +4 -4
- package/dist/cli/install.js +71 -32
- package/dist/cli/manifest.js +45 -17
- package/dist/cli/profile.js +1 -1
- package/dist/cli/uninstall.js +21 -5
- package/dist/cli/update.js +3 -3
- package/dist/cli/usage.js +75 -4
- package/dist/extensions/{combo-toggle → combo}/index.js +1 -1
- package/dist/extensions/{tersio-commands → commands}/index.js +1 -1
- package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -1
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +1 -1
- package/dist/extensions/{opencode/server.js → server.js} +8 -8
- package/dist/extensions/shared/agent-stats.js +1262 -0
- package/dist/extensions/shared/brand-marks.js +154 -0
- package/dist/extensions/shared/plugin-settings.js +1 -1
- package/dist/extensions/shared/pricing.js +91 -27
- package/dist/extensions/shared/usage-ledger.js +79 -17
- package/dist/extensions/shared/usage-store.js +106 -22
- package/dist/extensions/{lib → shared}/utils.js +1 -1
- package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -3
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +2 -2
- package/extensions/{combo-toggle → combo}/index.ts +1 -1
- package/extensions/{tersio-commands → commands}/index.ts +1 -1
- package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -1
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +1 -1
- package/extensions/{opencode/server.ts → server.ts} +9 -9
- package/extensions/shared/agent-stats.ts +1657 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/plugin-settings.ts +1 -1
- package/extensions/shared/pricing.ts +107 -28
- package/extensions/shared/usage-ledger.ts +94 -19
- package/extensions/shared/usage-store.ts +106 -22
- package/extensions/{lib → shared}/utils.ts +1 -1
- package/extensions/{ai-addons-updater → updater}/index.ts +15 -4
- package/package.json +13 -13
- /package/dist/extensions/{caveman-session → caveman}/index.js +0 -0
- /package/dist/extensions/{caveman-session → caveman}/rule-megacave.md +0 -0
- /package/dist/extensions/{caveman-session → caveman}/rule-ultra.md +0 -0
- /package/dist/extensions/{caveman-session → caveman}/rule.md +0 -0
- /package/extensions/{caveman-session → caveman}/index.ts +0 -0
- /package/extensions/{caveman-session → caveman}/rule-megacave.md +0 -0
- /package/extensions/{caveman-session → caveman}/rule-ultra.md +0 -0
- /package/extensions/{caveman-session → caveman}/rule.md +0 -0
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { RECENT_LIMIT, SQLITE_READ_BUFFER, canonicalModelId, classifySessionLine, classifyOpencodeMessage, costOf, durOf, hostOfSessionFile, ingestSessionRow, messageRowId, OPENCODE_DB_OVERLAP_MS, opencodeDbPath, opencodeSessionsDir, newSessionAccum, readOpencodeDbRows, sessionsDirs, walkJsonl, } from './usage-ledger.js';
|
|
6
|
-
import { tersioDataPath } from '
|
|
5
|
+
import { RECENT_LIMIT, SQLITE_READ_BUFFER, canonicalModelId, classifySessionLine, classifyOpencodeMessage, costOf, durOf, agentOfSessionText, hostOfSessionFile, ingestSessionRow, messageRowId, OPENCODE_DB_OVERLAP_MS, opencodeDbPath, opencodeSessionsDir, newSessionAccum, readOpencodeDbRows, sessionsDirs, walkJsonl, } from './usage-ledger.js';
|
|
6
|
+
import { tersioDataPath } from './utils.js';
|
|
7
7
|
import { MODEL_ALIASES } from './pricing.js';
|
|
8
8
|
import { tersioSettingsFile } from './plugin-settings.js';
|
|
9
9
|
export function usageDbPath() {
|
|
@@ -41,7 +41,7 @@ function query(db, sql) {
|
|
|
41
41
|
.map((line) => line.split('\t'));
|
|
42
42
|
}
|
|
43
43
|
// Bump on a parse change: unchanged transcripts are never re-read.
|
|
44
|
-
const PARSER_VERSION = '
|
|
44
|
+
const PARSER_VERSION = '12';
|
|
45
45
|
// A re-parse that keeps less than half the rows means transcripts vanished mid-migration; the backup is restored instead of publishing the loss.
|
|
46
46
|
const REPARSE_GUARD_RATIO = 0.5;
|
|
47
47
|
let guardReport = null;
|
|
@@ -53,8 +53,25 @@ const SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT
|
|
|
53
53
|
`CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL, mtime REAL NOT NULL, size INTEGER NOT NULL);` +
|
|
54
54
|
`CREATE TABLE IF NOT EXISTS messages (file_id INTEGER NOT NULL REFERENCES files(id), t REAL, model TEXT NOT NULL,` +
|
|
55
55
|
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
56
|
-
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, id TEXT);` +
|
|
57
|
-
|
|
56
|
+
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, a TEXT, id TEXT);` +
|
|
57
|
+
// Every read filters or orders by one of these three. Without them each sync and each read
|
|
58
|
+
// walks the whole table: the watermark scan, the day buckets, and the recent list all scan.
|
|
59
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_file ON messages(file_id);` +
|
|
60
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_t ON messages(t);` +
|
|
61
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_h_t ON messages(h, t);` +
|
|
62
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_model ON messages(model);`;
|
|
63
|
+
/**
|
|
64
|
+
* WAL plus a checkpoint at the end of a sync: the write path stops paying an fsync per commit,
|
|
65
|
+
* and the main file still holds every row, so a backup that copies only usage.db loses nothing.
|
|
66
|
+
*/
|
|
67
|
+
function pragmas(db) {
|
|
68
|
+
run(db, 'PRAGMA journal_mode=WAL;' +
|
|
69
|
+
'PRAGMA synchronous=NORMAL;' +
|
|
70
|
+
'PRAGMA wal_autocheckpoint=256;' +
|
|
71
|
+
'PRAGMA cache_size=-8000;' +
|
|
72
|
+
'PRAGMA temp_store=MEMORY;' +
|
|
73
|
+
'PRAGMA mmap_size=268435456;');
|
|
74
|
+
}
|
|
58
75
|
function storedParserVersion(db) {
|
|
59
76
|
try {
|
|
60
77
|
const stored = query(db, `SELECT v FROM meta WHERE k='parser_version';`);
|
|
@@ -64,6 +81,14 @@ function storedParserVersion(db) {
|
|
|
64
81
|
return null;
|
|
65
82
|
}
|
|
66
83
|
}
|
|
84
|
+
// A store written by a newer tersio: this code has no forward migration, so a re-parse would swap rotated history for whatever transcripts are left.
|
|
85
|
+
function isDowngrade(stored) {
|
|
86
|
+
if (stored === null)
|
|
87
|
+
return false;
|
|
88
|
+
const storedNum = Number(stored);
|
|
89
|
+
const currentNum = Number(PARSER_VERSION);
|
|
90
|
+
return Number.isFinite(storedNum) && Number.isFinite(currentNum) && storedNum > currentNum;
|
|
91
|
+
}
|
|
67
92
|
function countMessages(db) {
|
|
68
93
|
try {
|
|
69
94
|
const rows = query(db, `SELECT COUNT(*) FROM messages;`);
|
|
@@ -117,7 +142,7 @@ function migrateInPlace(db, preCount) {
|
|
|
117
142
|
`INSERT INTO files_new (path, mtime, size) SELECT path, mtime, size FROM files;` +
|
|
118
143
|
`CREATE TABLE messages_new (file_id INTEGER NOT NULL REFERENCES files_new(id), t REAL, model TEXT NOT NULL,` +
|
|
119
144
|
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
120
|
-
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, id TEXT);` +
|
|
145
|
+
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, a TEXT, id TEXT);` +
|
|
121
146
|
`INSERT INTO messages_new (file_id, ${cols}) SELECT (SELECT id FROM files_new WHERE files_new.path = messages.file), ${cols} FROM messages;` +
|
|
122
147
|
`DROP TABLE messages;DROP TABLE files;` +
|
|
123
148
|
`ALTER TABLE files_new RENAME TO files;ALTER TABLE messages_new RENAME TO messages;` +
|
|
@@ -135,6 +160,29 @@ function migrateInPlace(db, preCount) {
|
|
|
135
160
|
return false;
|
|
136
161
|
}
|
|
137
162
|
}
|
|
163
|
+
// v12 shape gains the agent kind (`main`, `subagent`) with a bare ALTER for the two names the
|
|
164
|
+
// store already holds. No rows move and no re-parse is forced, so the existing rows read
|
|
165
|
+
// `NULL` for it: a report then shows them under the main agent only, and new rows carry it.
|
|
166
|
+
function canAddAgent(db) {
|
|
167
|
+
try {
|
|
168
|
+
const names = query(db, `PRAGMA table_info(messages);`).map((r) => r[1]);
|
|
169
|
+
return names.includes('file_id') && !names.includes('a');
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// v12 shape gains the agent kind (`main`, `subagent`) with a bare ALTER: no rows move, so no
|
|
176
|
+
// re-parse is forced. Existing rows read NULL, which a report folds under the main agent.
|
|
177
|
+
function addAgentColumn(db) {
|
|
178
|
+
try {
|
|
179
|
+
run(db, `ALTER TABLE messages ADD COLUMN a TEXT;`);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
138
186
|
// v10 shape gains the message id with a bare ALTER: no rows move, so no re-parse and no guard trip.
|
|
139
187
|
function canAlterInPlace(db) {
|
|
140
188
|
try {
|
|
@@ -154,15 +202,21 @@ function alterInPlace(db) {
|
|
|
154
202
|
}
|
|
155
203
|
return !canAlterInPlace(db);
|
|
156
204
|
}
|
|
157
|
-
function ensureSchema(db) {
|
|
205
|
+
function ensureSchema(db, forceWipe) {
|
|
158
206
|
fs.mkdirSync(path.dirname(db), { recursive: true });
|
|
159
207
|
// Fold in rows stored under a pre-alias spelling; the mtime ledger will not re-read those transcripts.
|
|
160
208
|
const rekeys = MODEL_ALIASES.map((a) => `UPDATE messages SET model='${a.id}' WHERE model='${a.feed}';`).join('');
|
|
161
209
|
const stored = storedParserVersion(db);
|
|
210
|
+
pragmas(db);
|
|
162
211
|
if (stored === PARSER_VERSION) {
|
|
163
212
|
run(db, SCHEMA_SQL + rekeys);
|
|
164
213
|
return;
|
|
165
214
|
}
|
|
215
|
+
// A newer store keeps its rows: only an explicit force re-parse may wipe it.
|
|
216
|
+
if (!forceWipe && isDowngrade(stored)) {
|
|
217
|
+
run(db, SCHEMA_SQL + rekeys);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
166
220
|
// A version bump rebuilds both tables, so no ALTER patchwork survives a migration.
|
|
167
221
|
try {
|
|
168
222
|
if (countMessages(db) > 0 && !newestBackupMatches(stored))
|
|
@@ -179,6 +233,12 @@ function ensureSchema(db) {
|
|
|
179
233
|
run(db, `INSERT OR REPLACE INTO meta (k,v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
180
234
|
return;
|
|
181
235
|
}
|
|
236
|
+
// The agent column is additive on the current shape: adding it keeps every row, and the
|
|
237
|
+
// unchanged transcripts that already hold their rows simply read NULL until a re-parse.
|
|
238
|
+
if (stored !== null && canAddAgent(db) && addAgentColumn(db)) {
|
|
239
|
+
run(db, `INSERT OR REPLACE INTO meta (k,v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
182
242
|
run(db, `DROP TABLE IF EXISTS messages;DROP TABLE IF EXISTS files;` + SCHEMA_SQL +
|
|
183
243
|
`INSERT OR REPLACE INTO meta (k,v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
184
244
|
}
|
|
@@ -287,7 +347,8 @@ function readFiles(db) {
|
|
|
287
347
|
catch { /* fresh db */ }
|
|
288
348
|
return known;
|
|
289
349
|
}
|
|
290
|
-
// OpenCode rows carry no status, code, note,
|
|
350
|
+
// OpenCode rows carry no status, code, note, tool list, or subagent marker: only tokens and the
|
|
351
|
+
// msg id. They record as the main agent, so a store never holds a NULL kind.
|
|
291
352
|
function opencodeStoredRow(oc, host, id) {
|
|
292
353
|
return {
|
|
293
354
|
t: oc.ms ?? null,
|
|
@@ -303,10 +364,11 @@ function opencodeStoredRow(oc, host, id) {
|
|
|
303
364
|
note: undefined,
|
|
304
365
|
tools: [],
|
|
305
366
|
host,
|
|
367
|
+
agent: 'main',
|
|
306
368
|
id,
|
|
307
369
|
};
|
|
308
370
|
}
|
|
309
|
-
function parseFile(text, host, file) {
|
|
371
|
+
function parseFile(text, host, file, agent) {
|
|
310
372
|
const rows = [];
|
|
311
373
|
// Guarded: a JSONL transcript must fall through, not abort the sync.
|
|
312
374
|
let oc = null;
|
|
@@ -357,6 +419,7 @@ function parseFile(text, host, file) {
|
|
|
357
419
|
note: undefined,
|
|
358
420
|
tools: [],
|
|
359
421
|
host,
|
|
422
|
+
agent,
|
|
360
423
|
id: rid,
|
|
361
424
|
});
|
|
362
425
|
continue;
|
|
@@ -381,6 +444,7 @@ function parseFile(text, host, file) {
|
|
|
381
444
|
note,
|
|
382
445
|
tools: parsed.tools ?? [],
|
|
383
446
|
host,
|
|
447
|
+
agent,
|
|
384
448
|
id: rid,
|
|
385
449
|
});
|
|
386
450
|
}
|
|
@@ -389,10 +453,10 @@ function parseFile(text, host, file) {
|
|
|
389
453
|
return rows;
|
|
390
454
|
}
|
|
391
455
|
function insertSql(file, r) {
|
|
392
|
-
return `INSERT INTO messages (file_id, t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, id) VALUES (` +
|
|
456
|
+
return `INSERT INTO messages (file_id, t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, a, id) VALUES (` +
|
|
393
457
|
`(SELECT id FROM files WHERE path=${esc(file)}),${r.t === null ? 'NULL' : String(r.t)},${esc(r.model)},${r.i},${r.o},` +
|
|
394
458
|
`${nullNum(r.d)},${r.cr},${r.cw},${nullNum(r.usd)},${esc(r.st)},` +
|
|
395
|
-
`${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))},${nullStr(r.host)},${nullStr(r.id)});`;
|
|
459
|
+
`${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))},${nullStr(r.host)},${nullStr(r.agent ?? undefined)},${nullStr(r.id)});`;
|
|
396
460
|
}
|
|
397
461
|
// Unchanged transcripts are skipped via mtime+size; rows for deleted ones are kept so rotation never erases history. False when sqlite3 is unavailable.
|
|
398
462
|
export function syncUsageDb() {
|
|
@@ -403,12 +467,16 @@ export function syncUsageDb() {
|
|
|
403
467
|
try {
|
|
404
468
|
db = usageDbPath();
|
|
405
469
|
const stored = storedParserVersion(db);
|
|
406
|
-
migrated = stored !== null && stored !== PARSER_VERSION;
|
|
407
470
|
// The operator deleted sessions on purpose and wants the mirror to match: bypass the guard.
|
|
408
471
|
const forced = process.env.TERSIO_FORCE_REPARSE === '1';
|
|
472
|
+
// A downgrade keeps its rows: the wipe below cannot rebuild them from rotated transcripts.
|
|
473
|
+
const downgrade = !forced && isDowngrade(stored);
|
|
474
|
+
migrated = stored !== null && stored !== PARSER_VERSION && !downgrade;
|
|
475
|
+
if (downgrade)
|
|
476
|
+
guardReport = `store was written by a newer tersio (parser ${stored} > ${PARSER_VERSION}); kept its rows and skipped the re-parse`;
|
|
409
477
|
if (migrated && !forced)
|
|
410
478
|
preWipe = countMessages(db);
|
|
411
|
-
ensureSchema(db);
|
|
479
|
+
ensureSchema(db, forced);
|
|
412
480
|
maybeScheduledBackup(db);
|
|
413
481
|
}
|
|
414
482
|
catch {
|
|
@@ -489,7 +557,7 @@ export function syncUsageDb() {
|
|
|
489
557
|
try {
|
|
490
558
|
for (const file of changed) {
|
|
491
559
|
const text = fs.readFileSync(file, 'utf8');
|
|
492
|
-
for (const r of parseFile(text, hostOfSessionFile(file), file)) {
|
|
560
|
+
for (const r of parseFile(text, hostOfSessionFile(file), file, agentOfSessionText(text))) {
|
|
493
561
|
if (!push(insertSql(file, r)))
|
|
494
562
|
return false;
|
|
495
563
|
}
|
|
@@ -531,6 +599,11 @@ export function syncUsageDb() {
|
|
|
531
599
|
}
|
|
532
600
|
catch { /* best-effort */ }
|
|
533
601
|
}
|
|
602
|
+
// Fold the WAL back into the main file: a backup copies one file, and a checkpoint keeps it whole.
|
|
603
|
+
try {
|
|
604
|
+
run(db, `PRAGMA wal_checkpoint(TRUNCATE);`);
|
|
605
|
+
}
|
|
606
|
+
catch { /* best-effort */ }
|
|
534
607
|
return true;
|
|
535
608
|
}
|
|
536
609
|
export function readUsageDb() {
|
|
@@ -544,21 +617,28 @@ export function readUsageDb() {
|
|
|
544
617
|
}
|
|
545
618
|
let rows;
|
|
546
619
|
try {
|
|
547
|
-
|
|
620
|
+
// The `tools` column is the widest by far and the common case is the literal `[]`, so it is
|
|
621
|
+
// read as one column and skipped in JS rather than parsed per row. A store written before
|
|
622
|
+
// the agent column does not hold it, so the row is read without that column.
|
|
623
|
+
const names = query(db, `PRAGMA table_info(messages);`).map((r) => r[1]);
|
|
624
|
+
const agentCol = names.includes('a') ? 'a' : "''";
|
|
625
|
+
rows = query(db, `SELECT t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, ${agentCol}, id FROM messages;`);
|
|
548
626
|
}
|
|
549
627
|
catch {
|
|
550
628
|
return null;
|
|
551
629
|
}
|
|
552
630
|
const accum = newSessionAccum();
|
|
553
|
-
for (const [t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, id] of rows) {
|
|
631
|
+
for (const [t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, a, id] of rows) {
|
|
554
632
|
const ts = t === '' ? undefined : Number(t);
|
|
555
633
|
let toolNames = [];
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
634
|
+
if (tools && tools !== '[]' && tools !== '') {
|
|
635
|
+
try {
|
|
636
|
+
const parsed = JSON.parse(tools);
|
|
637
|
+
if (Array.isArray(parsed))
|
|
638
|
+
toolNames = parsed.filter((x) => typeof x === 'string');
|
|
639
|
+
}
|
|
640
|
+
catch { /* keep empty */ }
|
|
560
641
|
}
|
|
561
|
-
catch { /* keep empty */ }
|
|
562
642
|
const usage = {
|
|
563
643
|
input: Number(i) || 0,
|
|
564
644
|
output: Number(o) || 0,
|
|
@@ -574,7 +654,7 @@ export function readUsageDb() {
|
|
|
574
654
|
st: st === 'error' || st === 'aborted' ? st : 'completed',
|
|
575
655
|
code: code === '' ? undefined : Number(code),
|
|
576
656
|
note: note === '' ? undefined : note,
|
|
577
|
-
}, toolNames, h === '' ? undefined : h, id === '' || id === undefined ? undefined : id);
|
|
657
|
+
}, toolNames, h === '' ? undefined : h, id === '' || id === undefined ? undefined : id, a === '' ? undefined : a);
|
|
578
658
|
}
|
|
579
659
|
accum.recent.sort((a, b) => b.t - a.t);
|
|
580
660
|
let syncedAt = 0;
|
|
@@ -594,6 +674,10 @@ export function readUsageDb() {
|
|
|
594
674
|
byTool: accum.byTool,
|
|
595
675
|
byModelMessages: accum.byModelMessages,
|
|
596
676
|
byHost: accum.byHost,
|
|
677
|
+
byAgent: accum.byAgent,
|
|
678
|
+
byAgentMessages: accum.byAgentMessages,
|
|
679
|
+
byModelMeasured: accum.byModelMeasured,
|
|
680
|
+
byModelCostMessages: accum.byModelCostMessages,
|
|
597
681
|
costMeasured: accum.costMeasured,
|
|
598
682
|
recent: accum.recent.slice(0, RECENT_LIMIT),
|
|
599
683
|
},
|
|
@@ -6,7 +6,7 @@ import https from 'node:https';
|
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
// Promise.withResolvers is Node 22+; the package supports Node 18+.
|
|
9
|
-
|
|
9
|
+
function withResolvers() {
|
|
10
10
|
let resolve;
|
|
11
11
|
let reject;
|
|
12
12
|
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
// /ai-addons manual updater. Node built-ins only. RTK ships SHA256 checksums with no signature, so checksum-only is the best available.
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
+
import fsSync from 'node:fs';
|
|
4
5
|
import fs from 'node:fs/promises';
|
|
5
6
|
import os from 'node:os';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { setExtensionLabel } from '../shared/host.js';
|
|
9
|
-
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../
|
|
10
|
+
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../shared/utils.js';
|
|
10
11
|
const IS_WINDOWS = process.platform === 'win32';
|
|
11
12
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
|
|
13
14
|
const RTK_BINARY = resolveRtkBinary();
|
|
14
|
-
// Beside this file, not a host path: these are the rules /caveman reads.
|
|
15
|
-
|
|
15
|
+
// Beside this file, not a host path: these are the rules /caveman reads. A retired
|
|
16
|
+
// agent-tree copy has no `caveman` sibling, so the OMP plugin install is the
|
|
17
|
+
// fallback: it is the one the plugin manifest loads from.
|
|
18
|
+
const CAVEMAN_SIBLING = (name) => path.join(EXTENSION_DIR, '..', 'caveman', name);
|
|
19
|
+
function cavemanRule(name) {
|
|
20
|
+
const beside = CAVEMAN_SIBLING(name);
|
|
21
|
+
if (fsSync.existsSync(beside))
|
|
22
|
+
return beside;
|
|
23
|
+
const plugin = path.join(os.homedir(), '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman', name);
|
|
24
|
+
return fsSync.existsSync(plugin) ? plugin : beside;
|
|
25
|
+
}
|
|
16
26
|
const CAVEMAN_LOCAL = cavemanRule('rule.md');
|
|
17
27
|
const CAVEMAN_TRACKED = [
|
|
18
28
|
{ file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Throwaway: read the real rate cards out of the pi-catalog omp already ships.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
const root = process.env.HOME + "/.bun/install/global/node_modules/@oh-my-pi/pi-catalog/src";
|
|
4
|
+
const catalog = JSON.parse(readFileSync(root + "/models.json", "utf8"));
|
|
5
|
+
const wanted = [
|
|
6
|
+
"space-bunny",
|
|
7
|
+
"muse-spark-1.3-contributor",
|
|
8
|
+
"glm-5.3-flash",
|
|
9
|
+
"qwen38-flash-next",
|
|
10
|
+
"muse-spark-1.2-contributor",
|
|
11
|
+
"qwen3.8-27b",
|
|
12
|
+
"m3",
|
|
13
|
+
"ox-alpha",
|
|
14
|
+
"deepseek-v4-flash",
|
|
15
|
+
"ling-3.0-flash-sante",
|
|
16
|
+
"qwen3.6-35b-a3b-fp8",
|
|
17
|
+
"mimo-v2.6-flash",
|
|
18
|
+
"deepseek-v4.1-flash",
|
|
19
|
+
"step-5-preview",
|
|
20
|
+
"ling-3.1-flash",
|
|
21
|
+
"longcat-2.5-preview",
|
|
22
|
+
];
|
|
23
|
+
let providers = 0;
|
|
24
|
+
let rows = 0;
|
|
25
|
+
const hits = new Map();
|
|
26
|
+
for (const [provider, models] of Object.entries(catalog)) {
|
|
27
|
+
if (typeof models !== "object" || models === null)
|
|
28
|
+
continue;
|
|
29
|
+
providers += 1;
|
|
30
|
+
for (const [id, row] of Object.entries(models)) {
|
|
31
|
+
rows += 1;
|
|
32
|
+
const bare = id.toLowerCase().split("/").pop();
|
|
33
|
+
if (wanted.includes(bare)) {
|
|
34
|
+
const list = hits.get(bare) ?? [];
|
|
35
|
+
list.push({ provider, id, cost: row.cost, status: row.pricingStatus });
|
|
36
|
+
hits.set(bare, list);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log("providers:", providers, "model rows:", rows, "\n");
|
|
41
|
+
for (const w of wanted) {
|
|
42
|
+
const list = hits.get(w) ?? [];
|
|
43
|
+
console.log(w.padEnd(26) + (list.length === 0 ? " — NOT IN CATALOG —" : " " + list.length + " row(s)"));
|
|
44
|
+
for (const h of list.slice(0, 4)) {
|
|
45
|
+
const c = h.cost;
|
|
46
|
+
console.log(" " + (h.provider + "/" + h.id).padEnd(52) + " in=" + String(c?.input ?? "?") + " out=" + String(c?.output ?? "?") + " status=" + JSON.stringify(h.status));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Throwaway: the current omp catalog's real rates for the fifteen models in the screenshot.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
const root = process.env.HOME + "/.bun/install/global/node_modules/@oh-my-pi/pi-catalog/src";
|
|
4
|
+
const catalog = JSON.parse(readFileSync(root + "/models.json", "utf8"));
|
|
5
|
+
const wanted = [
|
|
6
|
+
"space-bunny",
|
|
7
|
+
"muse-spark-1.3-contributor",
|
|
8
|
+
"glm-5.3-flash",
|
|
9
|
+
"qwen38-flash-next",
|
|
10
|
+
"muse-spark-1.2-contributor",
|
|
11
|
+
"qwen3.8-27b",
|
|
12
|
+
"m3",
|
|
13
|
+
"ox-alpha",
|
|
14
|
+
"deepseek-v4-flash",
|
|
15
|
+
"ling-3.0-flash-sante",
|
|
16
|
+
"qwen3.6-35b-a3b-fp8",
|
|
17
|
+
"mimo-v2.6-flash",
|
|
18
|
+
"deepseek-v4.1-flash",
|
|
19
|
+
"step-5-preview",
|
|
20
|
+
"ling-3.1-flash",
|
|
21
|
+
"longcat-2.5-preview",
|
|
22
|
+
];
|
|
23
|
+
const hits = new Map();
|
|
24
|
+
for (const [provider, models] of Object.entries(catalog)) {
|
|
25
|
+
if (!models || typeof models !== "object")
|
|
26
|
+
continue;
|
|
27
|
+
for (const [id, row] of Object.entries(models)) {
|
|
28
|
+
const bare = id.toLowerCase().split("/").pop();
|
|
29
|
+
if (!wanted.includes(bare))
|
|
30
|
+
continue;
|
|
31
|
+
const list = hits.get(bare) ?? [];
|
|
32
|
+
list.push({ key: provider + "/" + id, cost: row.cost, status: row.pricingStatus });
|
|
33
|
+
hits.set(bare, list);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const w of wanted) {
|
|
37
|
+
const list = hits.get(w) ?? [];
|
|
38
|
+
console.log("\n" + w.toUpperCase() + (list.length === 0 ? " -- NOT IN CATALOG --" : " -- " + list.length + " rows --"));
|
|
39
|
+
for (const h of list) {
|
|
40
|
+
const c = h.cost;
|
|
41
|
+
const zero = c && c.input === 0 && c.output === 0 && c.cacheRead === 0 && c.cacheWrite === 0;
|
|
42
|
+
console.log(" " + h.key.padEnd(50) + (zero ? "FREE ($0)" : "in=" + c?.input + " out=" + c?.output) + " status=" + JSON.stringify(h.status));
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/tersio.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Install Tersio add-ons on any OMP device; see help for commands.
|
|
3
|
-
import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, dashboard, dashboardExport, dashboardOpen, dashboardPort, doctor, reset, settings, showHelp, showVersion, uninstall, unknownCommand, update, usage, } from './cli/common.js';
|
|
3
|
+
import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, dashboard, dashboardExport, dashboardOpen, dashboardPort, doctor, reset, settings, showHelp, showVersion, uninstall, unknownCommand, update, usage, currency, } from './cli/common.js';
|
|
4
4
|
import { closeRL } from './cli/interactive.js';
|
|
5
5
|
import { runInstall } from './cli/install.js';
|
|
6
6
|
import { runDoctor } from './cli/doctor.js';
|
|
@@ -79,7 +79,7 @@ async function main() {
|
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
if (dashboard) {
|
|
82
|
-
await runDashboard({ port: dashboardPort, open: dashboardOpen, exportFile: dashboardExport });
|
|
82
|
+
await runDashboard({ port: dashboardPort, open: dashboardOpen, exportFile: dashboardExport, currency });
|
|
83
83
|
closeRL();
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
readRtkDefault,
|
|
28
28
|
saveComboSetup,
|
|
29
29
|
} from '../shared/plugin-settings.ts';
|
|
30
|
-
import { findHoistedPackage, isPiProcess } from '../
|
|
30
|
+
import { findHoistedPackage, isPiProcess } from '../shared/utils.ts';
|
|
31
31
|
import type { ComboState, ExtensionApi, ExtensionCtx, SystemPromptEvent } from '../shared/types.ts';
|
|
32
32
|
|
|
33
33
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -9,7 +9,7 @@ import { setExtensionLabel } from '../shared/host.ts';
|
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { appendUsage, readUsage } from '../shared/usage-ledger.ts';
|
|
12
|
-
import { checkAddonsSummary, runAddonUpdate } from '../
|
|
12
|
+
import { checkAddonsSummary, runAddonUpdate } from '../updater/index.ts';
|
|
13
13
|
import type { ExtensionApi, ExtensionCtx } from '../shared/types.ts';
|
|
14
14
|
|
|
15
15
|
const HELP = [
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { resolveRtkBinary } from '../
|
|
5
|
+
import { resolveRtkBinary } from '../shared/utils.ts';
|
|
6
6
|
import { readRtkDefault } from '../shared/plugin-settings.ts';
|
|
7
7
|
import { getSharedComboState, lastCustomValue, sessionEntries } from '../shared/session-state.ts';
|
|
8
8
|
import type { ExtensionApi, ExtensionCtx, SessionEntry, ToolResultEvent } from '../shared/types.ts';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.ts';
|
|
2
2
|
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.ts';
|
|
3
3
|
import { readRtkDefault } from '../shared/plugin-settings.ts';
|
|
4
|
-
import { resolveRtkBinary } from '../
|
|
4
|
+
import { resolveRtkBinary } from '../shared/utils.ts';
|
|
5
5
|
import { announceStatus } from '../shared/status.ts';
|
|
6
6
|
import type { ExtensionApi, ExtensionCtx, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
7
7
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// OpenCode plugin entry: maps its plugin API onto the shared ExtensionApi the mode extensions consume.
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import type { Plugin } from '@opencode/plugin';
|
|
4
|
-
import cavemanSessionExtension from '
|
|
5
|
-
import rtkSessionExtension from '
|
|
6
|
-
import rtkFilterExtension, { FILTER_BY_TOOL, MIN_FILTER_BYTES, filterThroughRtk, filterableText, rtkActive } from '
|
|
7
|
-
import comboToggleExtension from '
|
|
8
|
-
import tersioCommandsExtension from '
|
|
9
|
-
import { getSharedComboState } from '
|
|
10
|
-
import { formatStatus } from '
|
|
11
|
-
import { resolveRtkBinary } from '
|
|
12
|
-
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '
|
|
4
|
+
import cavemanSessionExtension from './caveman/index.ts';
|
|
5
|
+
import rtkSessionExtension from './rtk/session.ts';
|
|
6
|
+
import rtkFilterExtension, { FILTER_BY_TOOL, MIN_FILTER_BYTES, filterThroughRtk, filterableText, rtkActive } from './rtk/filter.ts';
|
|
7
|
+
import comboToggleExtension from './combo/index.ts';
|
|
8
|
+
import tersioCommandsExtension from './commands/index.ts';
|
|
9
|
+
import { getSharedComboState } from './shared/session-state.ts';
|
|
10
|
+
import { formatStatus } from './shared/status.ts';
|
|
11
|
+
import { resolveRtkBinary } from './shared/utils.ts';
|
|
12
|
+
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from './shared/types.ts';
|
|
13
13
|
|
|
14
14
|
interface CommandConfig {
|
|
15
15
|
description: string;
|