@krtclcdy/tersio 2.25.3 → 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 +14 -31
- 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 +42 -45
- package/dist/cli/doctor.js +70 -8
- package/dist/cli/hosts.js +4 -4
- package/dist/cli/install.js +106 -38
- package/dist/cli/manifest.js +45 -16
- 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/{caveman-session → caveman}/index.js +10 -3
- package/dist/extensions/{combo-toggle → combo}/index.js +3 -3
- package/dist/extensions/{tersio-commands → commands}/index.js +1 -1
- package/dist/extensions/rtk/filter.js +87 -0
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +4 -3
- package/dist/extensions/{opencode/server.js → server.js} +52 -8
- package/dist/extensions/shared/agent-stats.js +1262 -0
- package/dist/extensions/shared/brand-marks.js +154 -0
- package/dist/extensions/shared/host.js +18 -12
- package/dist/extensions/shared/plugin-settings.js +1 -1
- package/dist/extensions/shared/pricing.js +91 -27
- package/dist/extensions/shared/session-state.js +3 -1
- 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 +15 -5
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +2 -2
- package/extensions/{caveman-session → caveman}/index.ts +10 -3
- package/extensions/{combo-toggle → combo}/index.ts +3 -3
- package/extensions/{tersio-commands → commands}/index.ts +1 -1
- package/extensions/rtk/filter.ts +90 -0
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +4 -3
- package/extensions/{opencode/server.ts → server.ts} +46 -9
- package/extensions/shared/agent-stats.ts +1657 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/host.ts +26 -13
- package/extensions/shared/plugin-settings.ts +1 -1
- package/extensions/shared/pricing.ts +107 -28
- package/extensions/shared/session-state.ts +3 -1
- package/extensions/shared/types.ts +10 -2
- 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 +17 -7
- package/package.json +13 -12
- package/dist/cli/headroom-install.js +0 -266
- package/dist/cli/headroom.js +0 -492
- package/dist/extensions/headroom-session/index.js +0 -93
- package/dist/extensions/shared/headroom.js +0 -131
- /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}/rule-megacave.md +0 -0
- /package/extensions/{caveman-session → caveman}/rule-ultra.md +0 -0
- /package/extensions/{caveman-session → caveman}/rule.md +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Shared usage ledger: append-only JSON lines, read by the CLI, `/tersio`, and the Dashboard. Best-effort; corrupt lines are skipped on read.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from '
|
|
4
|
+
import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from './utils.ts';
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
6
|
|
|
7
7
|
export type UsageKind = 'command' | 'toggle' | 'install' | 'update' | 'rtk-audit';
|
|
@@ -89,6 +89,14 @@ export interface SessionTokens {
|
|
|
89
89
|
byTool: Record<string, number>;
|
|
90
90
|
byModelMessages: Record<string, number>;
|
|
91
91
|
byHost: Record<string, Record<string, TokenBreakdown>>;
|
|
92
|
+
/** Model -> agent kind (`main`, `subagent`), so a subagent's work reads apart from its root's. */
|
|
93
|
+
byAgent: Record<string, Record<string, TokenBreakdown>>;
|
|
94
|
+
/** Model -> agent kind -> message count, so a share can read requests beside the tokens. */
|
|
95
|
+
byAgentMessages: Record<string, Record<string, number>>;
|
|
96
|
+
/** Model -> cost the host reported, over the messages that carried one. */
|
|
97
|
+
byModelMeasured: Record<string, number>;
|
|
98
|
+
/** Model -> messages that carried a cost. Compare to `byModelMessages` before trusting the sum. */
|
|
99
|
+
byModelCostMessages: Record<string, number>;
|
|
92
100
|
costMeasured: number;
|
|
93
101
|
recent: RecentRequest[];
|
|
94
102
|
}
|
|
@@ -292,15 +300,25 @@ export interface SessionAccum {
|
|
|
292
300
|
byTool: Record<string, number>;
|
|
293
301
|
byModelMessages: Record<string, number>;
|
|
294
302
|
byHost: Record<string, Record<string, TokenBreakdown>>;
|
|
303
|
+
/** Model -> agent kind (`main`, `subagent`), so a subagent's work reads apart from its root's. */
|
|
304
|
+
byAgent: Record<string, Record<string, TokenBreakdown>>;
|
|
305
|
+
/** Model -> agent kind -> message count. */
|
|
306
|
+
byAgentMessages: Record<string, Record<string, number>>;
|
|
307
|
+
/** Model -> cost the host itself reported, summed over the messages that carried one. */
|
|
308
|
+
byModelMeasured: Record<string, number>;
|
|
309
|
+
/** Model -> messages that carried a cost. Compare to `byModelMessages` before trusting the sum. */
|
|
310
|
+
byModelCostMessages: Record<string, number>;
|
|
295
311
|
totals: TokenBreakdown;
|
|
296
312
|
messages: number;
|
|
297
313
|
costMeasured: number;
|
|
298
314
|
recent: RecentRequest[];
|
|
315
|
+
/** Oldest timestamp held in `recent`, so the list stays bounded instead of growing with the store. */
|
|
316
|
+
recentFloor: number;
|
|
299
317
|
}
|
|
300
318
|
export function newSessionAccum(): SessionAccum {
|
|
301
|
-
return { byModel: {}, byDay: {}, byDayModel: {}, byTool: {}, byModelMessages: {}, byHost: {}, totals: zeroBreakdown(), messages: 0, costMeasured: 0, recent: [] };
|
|
319
|
+
return { byModel: {}, byDay: {}, byDayModel: {}, byTool: {}, byModelMessages: {}, byHost: {}, byAgent: {}, byAgentMessages: {}, byModelMeasured: {}, byModelCostMessages: {}, totals: zeroBreakdown(), messages: 0, costMeasured: 0, recent: [], recentFloor: Number.POSITIVE_INFINITY };
|
|
302
320
|
}
|
|
303
|
-
export function ingestSessionRow(accum: SessionAccum, model: string, usage: Record<string, unknown>, ts: string | number | undefined, durMs?: unknown, run?: { st: RunStatus; code?: number; note?: string }, toolNames?: string[], host?: string, id?: string): boolean {
|
|
321
|
+
export function ingestSessionRow(accum: SessionAccum, model: string, usage: Record<string, unknown>, ts: string | number | undefined, durMs?: unknown, run?: { st: RunStatus; code?: number; note?: string }, toolNames?: string[], host?: string, id?: string, agent?: string): boolean {
|
|
304
322
|
const watermark = readResetWatermark();
|
|
305
323
|
const ms = ts === undefined ? NaN : typeof ts === 'number' ? ts : Date.parse(ts);
|
|
306
324
|
if (watermark > 0 && (!Number.isFinite(ms) || ms < watermark)) return false;
|
|
@@ -314,10 +332,30 @@ export function ingestSessionRow(accum: SessionAccum, model: string, usage: Reco
|
|
|
314
332
|
hs[host] ??= zeroBreakdown();
|
|
315
333
|
addInto(hs[host], usage);
|
|
316
334
|
}
|
|
335
|
+
if (agent) {
|
|
336
|
+
const as = (accum.byAgent[key] ??= {});
|
|
337
|
+
as[agent] ??= zeroBreakdown();
|
|
338
|
+
addInto(as[agent], usage);
|
|
339
|
+
const am = (accum.byAgentMessages[key] ??= {});
|
|
340
|
+
am[agent] = (am[agent] ?? 0) + 1;
|
|
341
|
+
}
|
|
317
342
|
const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
|
|
318
343
|
if (Number.isFinite(ms)) {
|
|
319
344
|
const outcome = run ?? { st: 'completed' as RunStatus };
|
|
320
|
-
|
|
345
|
+
// The store holds every message ever recorded, but only the newest few are ever read. Skip
|
|
346
|
+
// anything older than the floor, and re-read the floor when the list is full, so a 50k-row
|
|
347
|
+
// store costs 2000 rows of memory instead of 50k.
|
|
348
|
+
if (accum.recent.length < RECENT_LIMIT || ms >= accum.recentFloor) {
|
|
349
|
+
accum.recent.push({ m: key, i: num(usage.input), o: num(usage.output), t: ms, d: durOf(durMs), h: host, cr: num(usage.cacheRead), cw: num(usage.cacheWrite), usd: costOf(usage), st: outcome.st, code: outcome.code, note: outcome.note, id });
|
|
350
|
+
if (accum.recent.length > RECENT_LIMIT) {
|
|
351
|
+
let drop = 0;
|
|
352
|
+
for (let k = 1; k < accum.recent.length; k += 1) if (accum.recent[k].t < accum.recent[drop].t) drop = k;
|
|
353
|
+
accum.recent.splice(drop, 1);
|
|
354
|
+
accum.recentFloor = accum.recent.reduce((lo, r) => Math.min(lo, r.t), Number.POSITIVE_INFINITY);
|
|
355
|
+
} else if (ms < accum.recentFloor) {
|
|
356
|
+
accum.recentFloor = ms;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
321
359
|
}
|
|
322
360
|
const day = ts !== undefined ? dayKey(ts) : null;
|
|
323
361
|
if (day) {
|
|
@@ -331,7 +369,11 @@ export function ingestSessionRow(accum: SessionAccum, model: string, usage: Reco
|
|
|
331
369
|
}
|
|
332
370
|
accum.messages += 1;
|
|
333
371
|
const measured = costOf(usage);
|
|
334
|
-
if (measured !== undefined)
|
|
372
|
+
if (measured !== undefined) {
|
|
373
|
+
accum.costMeasured += measured;
|
|
374
|
+
accum.byModelMeasured[key] = (accum.byModelMeasured[key] ?? 0) + measured;
|
|
375
|
+
accum.byModelCostMessages[key] = (accum.byModelCostMessages[key] ?? 0) + 1;
|
|
376
|
+
}
|
|
335
377
|
for (const name of toolNames ?? []) accum.byTool[name] = (accum.byTool[name] ?? 0) + 1;
|
|
336
378
|
return true;
|
|
337
379
|
}
|
|
@@ -428,14 +470,14 @@ export function processOpencodeFile(accum: SessionAccum, text: string, file?: st
|
|
|
428
470
|
const parsed = classifyOpencodeMessage(obj as OpencodeMessage);
|
|
429
471
|
if (!parsed) return;
|
|
430
472
|
const id = file ? path.basename(file).replace(/\.[^.]+$/, '') || undefined : undefined;
|
|
431
|
-
ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, undefined, 'opencode', id);
|
|
473
|
+
ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, undefined, 'opencode', id, 'main');
|
|
432
474
|
}
|
|
433
475
|
|
|
434
476
|
// One v2 message row into the shared accum. The msg id is the row primary key.
|
|
435
477
|
export function ingestOpencodeDbRow(accum: SessionAccum, row: OpencodeDbRow): void {
|
|
436
478
|
const parsed = classifyOpencodeMessage(row.row);
|
|
437
479
|
if (!parsed) return;
|
|
438
|
-
ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, undefined, 'opencode', row.id);
|
|
480
|
+
ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, undefined, 'opencode', row.id, 'main');
|
|
439
481
|
}
|
|
440
482
|
|
|
441
483
|
// Fold `:free`/`-free` suffixes and case variants into one chart key.
|
|
@@ -560,10 +602,10 @@ export function importSessionTokens(): SessionTokens {
|
|
|
560
602
|
set codexProvider(v: string | null) { codexProvider = v; },
|
|
561
603
|
get codexModel() { return codexModel; },
|
|
562
604
|
set codexModel(v: string | null) { codexModel = v; },
|
|
563
|
-
}, text, hostOfSessionFile(file));
|
|
605
|
+
}, text, hostOfSessionFile(file), agentOfSessionText(text));
|
|
564
606
|
}
|
|
565
607
|
accum.recent.sort((a, b) => b.t - a.t);
|
|
566
|
-
return { messages: accum.messages, totals: accum.totals, byModel: accum.byModel, byDay: accum.byDay, byDayModel: accum.byDayModel, byTool: accum.byTool, byModelMessages: accum.byModelMessages, byHost: accum.byHost, costMeasured: accum.costMeasured, recent: accum.recent.slice(0, RECENT_LIMIT) };
|
|
608
|
+
return { messages: accum.messages, totals: accum.totals, byModel: accum.byModel, byDay: accum.byDay, byDayModel: accum.byDayModel, byTool: accum.byTool, byModelMessages: accum.byModelMessages, byHost: accum.byHost, byAgent: accum.byAgent, byAgentMessages: accum.byAgentMessages, byModelMeasured: accum.byModelMeasured, byModelCostMessages: accum.byModelCostMessages, costMeasured: accum.costMeasured, recent: accum.recent.slice(0, RECENT_LIMIT) };
|
|
567
609
|
}
|
|
568
610
|
|
|
569
611
|
// Which agent wrote a session file, from the directory it sits in.
|
|
@@ -575,6 +617,21 @@ export function hostOfSessionFile(file: string): string {
|
|
|
575
617
|
return 'pi';
|
|
576
618
|
}
|
|
577
619
|
|
|
620
|
+
// Which agent ran a session file, from its own `session` row: a transcript with a parent
|
|
621
|
+
// session is a subagent's, and everything else is the main agent. The row is the host's own
|
|
622
|
+
// record, so a mirrored transcript carries the same answer as the stats database.
|
|
623
|
+
export function agentOfSessionText(text: string): string {
|
|
624
|
+
if (!text.includes('parentSession')) return 'main';
|
|
625
|
+
for (const line of text.split('\n')) {
|
|
626
|
+
if (!line.includes('parentSession')) continue;
|
|
627
|
+
try {
|
|
628
|
+
const row = JSON.parse(line) as { type?: unknown; parentSession?: unknown };
|
|
629
|
+
if (row.type === 'session' && typeof row.parentSession === 'string' && row.parentSession !== '') return 'subagent';
|
|
630
|
+
} catch { /* skip a corrupt line */ }
|
|
631
|
+
}
|
|
632
|
+
return 'main';
|
|
633
|
+
}
|
|
634
|
+
|
|
578
635
|
const adoptionFileCache = new Map<string, { size: number; mtimeMs: number; counts: { bashCalls: number; eligibleCalls: number; rtkCalls: number } }>();
|
|
579
636
|
|
|
580
637
|
function parseAdoptionFile(file: string): { counts: { bashCalls: number; eligibleCalls: number; rtkCalls: number }; sessions: number } {
|
|
@@ -645,7 +702,7 @@ export function readRtkAdoption(): RtkAdoption {
|
|
|
645
702
|
const missedCalls = Math.max(0, eligibleCalls - rtkCalls);
|
|
646
703
|
return { sessions, bashCalls, eligibleCalls, rtkCalls, missedCalls, adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0 };
|
|
647
704
|
}
|
|
648
|
-
export function processSessionText(accum: SessionAccum, state: { codexProvider: string | null; codexModel?: string | null }, text: string, host?: string): void {
|
|
705
|
+
export function processSessionText(accum: SessionAccum, state: { codexProvider: string | null; codexModel?: string | null }, text: string, host?: string, agent?: string): void {
|
|
649
706
|
for (const line of text.split('\n')) {
|
|
650
707
|
if (!line.trim()) continue;
|
|
651
708
|
try {
|
|
@@ -664,11 +721,11 @@ export function processSessionText(accum: SessionAccum, state: { codexProvider:
|
|
|
664
721
|
const base = parsed.provider ? `codex/${parsed.provider}` : (state.codexProvider ? `codex/${state.codexProvider}` : 'codex');
|
|
665
722
|
// The provider alone told you nothing about which model ran.
|
|
666
723
|
const model = state.codexModel ? `${base}/${state.codexModel}` : base;
|
|
667
|
-
ingestSessionRow(accum, model, parsed.usage ?? {}, row.timestamp, parsed.durMs, { st: 'completed' as RunStatus }, undefined, host ?? 'codex', rid);
|
|
724
|
+
ingestSessionRow(accum, model, parsed.usage ?? {}, row.timestamp, parsed.durMs, { st: 'completed' as RunStatus }, undefined, host ?? 'codex', rid, agent);
|
|
668
725
|
continue;
|
|
669
726
|
}
|
|
670
727
|
if (parsed.kind !== 'assistant_row' || !parsed.model || !parsed.usage) continue;
|
|
671
|
-
ingestSessionRow(accum, parsed.model, parsed.usage, row.timestamp, parsed.durMs, parsed.run, parsed.tools, host, rid);
|
|
728
|
+
ingestSessionRow(accum, parsed.model, parsed.usage, row.timestamp, parsed.durMs, parsed.run, parsed.tools, host, rid, agent);
|
|
672
729
|
} catch { /* skip corrupt lines */ }
|
|
673
730
|
}
|
|
674
731
|
}
|
|
@@ -706,17 +763,32 @@ export function leadBinary(command: unknown): string {
|
|
|
706
763
|
}
|
|
707
764
|
|
|
708
765
|
// --- Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table) ---
|
|
709
|
-
import { DEFAULT_PRICE, canonicalPriceId, priceFor, refreshPricesIfStale } from './pricing.ts';
|
|
766
|
+
import { DEFAULT_PRICE, PRICE_TIERS_FIRM, canonicalPriceId, priceFor, pricesOverridePath, refreshPricesIfStale, type PriceHit, type PriceTier } from './pricing.ts';
|
|
710
767
|
import { co2GramsFor, energyWhFor } from './carbon.ts';
|
|
711
768
|
import type { ModelPrice } from './pricing.ts';
|
|
712
769
|
|
|
713
|
-
export { DEFAULT_PRICE, priceFor, refreshPricesIfStale };
|
|
770
|
+
export { DEFAULT_PRICE, PRICE_TIERS_FIRM, priceFor, pricesOverridePath, refreshPricesIfStale };
|
|
714
771
|
export { co2GramsFor, energyWhFor };
|
|
715
|
-
export type { ModelPrice };
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
772
|
+
export type { PriceHit, PriceTier, ModelPrice };
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Price one model's tokens. `measured` is the cost the host reported for those tokens; it outranks
|
|
776
|
+
* every rate card, because it is the only figure from the party that billed. Pass undefined rather
|
|
777
|
+
* than 0 for a cost nobody reported: `costOf` keeps the two apart.
|
|
778
|
+
*/
|
|
779
|
+
export function usdCost(t: TokenBreakdown, model?: string, measured?: number): { usd: number; priced: boolean; tier: PriceTier; buckets: { input: number; output: number; cacheRead: number; cacheWrite: number } } {
|
|
719
780
|
const m = 1 / 1_000_000;
|
|
781
|
+
if (measured !== undefined) {
|
|
782
|
+
// Only a bare total arrives, so it attributes to input, the largest bucket for every model here.
|
|
783
|
+
return {
|
|
784
|
+
usd: measured,
|
|
785
|
+
priced: true,
|
|
786
|
+
tier: 'measured',
|
|
787
|
+
buckets: { input: measured, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
const hit: PriceHit = model ? priceFor(model) : { price: DEFAULT_PRICE, known: false, live: false, tier: 'default' };
|
|
791
|
+
const price = hit.price;
|
|
720
792
|
const buckets = {
|
|
721
793
|
input: t.input * price.input * m,
|
|
722
794
|
output: t.output * price.output * m,
|
|
@@ -725,7 +797,10 @@ export function usdCost(t: TokenBreakdown, model?: string): { usd: number; price
|
|
|
725
797
|
};
|
|
726
798
|
return {
|
|
727
799
|
usd: buckets.input + buckets.output + buckets.cacheRead + buckets.cacheWrite,
|
|
728
|
-
|
|
800
|
+
// A tier the caller cannot stand behind is not a price. `tail` and `default` are estimates,
|
|
801
|
+
// and a report that sums them beside measured cost reads as a bill it is not.
|
|
802
|
+
priced: PRICE_TIERS_FIRM.includes(hit.tier),
|
|
803
|
+
tier: hit.tier,
|
|
729
804
|
buckets,
|
|
730
805
|
};
|
|
731
806
|
}
|
|
@@ -8,14 +8,13 @@ import {
|
|
|
8
8
|
SQLITE_READ_BUFFER,
|
|
9
9
|
canonicalModelId,
|
|
10
10
|
classifySessionLine,
|
|
11
|
-
OpencodeMessage,
|
|
12
11
|
classifyOpencodeMessage,
|
|
13
12
|
costOf,
|
|
14
13
|
durOf,
|
|
14
|
+
agentOfSessionText,
|
|
15
15
|
hostOfSessionFile,
|
|
16
16
|
ingestSessionRow,
|
|
17
17
|
messageRowId,
|
|
18
|
-
OpencodeDbRow,
|
|
19
18
|
OPENCODE_DB_OVERLAP_MS,
|
|
20
19
|
opencodeDbPath,
|
|
21
20
|
opencodeSessionsDir,
|
|
@@ -24,10 +23,10 @@ import {
|
|
|
24
23
|
sessionsDirs,
|
|
25
24
|
walkJsonl,
|
|
26
25
|
} from './usage-ledger.ts';
|
|
27
|
-
import { tersioDataPath } from '
|
|
26
|
+
import { tersioDataPath } from './utils.ts';
|
|
28
27
|
import { MODEL_ALIASES } from './pricing.ts';
|
|
29
28
|
import { tersioSettingsFile } from './plugin-settings.ts';
|
|
30
|
-
import type { RunStatus, SessionTokens } from './usage-ledger.ts';
|
|
29
|
+
import type { OpencodeDbRow, OpencodeMessage, RunStatus, SessionTokens } from './usage-ledger.ts';
|
|
31
30
|
|
|
32
31
|
export function usageDbPath(): string {
|
|
33
32
|
const override = process.env.TERSIO_USAGE_DB;
|
|
@@ -75,7 +74,7 @@ function query(db: string, sql: string): string[][] {
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
// Bump on a parse change: unchanged transcripts are never re-read.
|
|
78
|
-
const PARSER_VERSION = '
|
|
77
|
+
const PARSER_VERSION = '12';
|
|
79
78
|
|
|
80
79
|
// A re-parse that keeps less than half the rows means transcripts vanished mid-migration; the backup is restored instead of publishing the loss.
|
|
81
80
|
const REPARSE_GUARD_RATIO = 0.5;
|
|
@@ -92,8 +91,27 @@ const SCHEMA_SQL =
|
|
|
92
91
|
`CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL, mtime REAL NOT NULL, size INTEGER NOT NULL);` +
|
|
93
92
|
`CREATE TABLE IF NOT EXISTS messages (file_id INTEGER NOT NULL REFERENCES files(id), t REAL, model TEXT NOT NULL,` +
|
|
94
93
|
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
95
|
-
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, id TEXT);` +
|
|
96
|
-
|
|
94
|
+
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, a TEXT, id TEXT);` +
|
|
95
|
+
// Every read filters or orders by one of these three. Without them each sync and each read
|
|
96
|
+
// walks the whole table: the watermark scan, the day buckets, and the recent list all scan.
|
|
97
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_file ON messages(file_id);` +
|
|
98
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_t ON messages(t);` +
|
|
99
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_h_t ON messages(h, t);` +
|
|
100
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_model ON messages(model);`;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* WAL plus a checkpoint at the end of a sync: the write path stops paying an fsync per commit,
|
|
104
|
+
* and the main file still holds every row, so a backup that copies only usage.db loses nothing.
|
|
105
|
+
*/
|
|
106
|
+
function pragmas(db: string): void {
|
|
107
|
+
run(db,
|
|
108
|
+
'PRAGMA journal_mode=WAL;' +
|
|
109
|
+
'PRAGMA synchronous=NORMAL;' +
|
|
110
|
+
'PRAGMA wal_autocheckpoint=256;' +
|
|
111
|
+
'PRAGMA cache_size=-8000;' +
|
|
112
|
+
'PRAGMA temp_store=MEMORY;' +
|
|
113
|
+
'PRAGMA mmap_size=268435456;');
|
|
114
|
+
}
|
|
97
115
|
|
|
98
116
|
function storedParserVersion(db: string): string | null {
|
|
99
117
|
try {
|
|
@@ -104,6 +122,14 @@ function storedParserVersion(db: string): string | null {
|
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
124
|
|
|
125
|
+
// 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.
|
|
126
|
+
function isDowngrade(stored: string | null): boolean {
|
|
127
|
+
if (stored === null) return false;
|
|
128
|
+
const storedNum = Number(stored);
|
|
129
|
+
const currentNum = Number(PARSER_VERSION);
|
|
130
|
+
return Number.isFinite(storedNum) && Number.isFinite(currentNum) && storedNum > currentNum;
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
function countMessages(db: string): number {
|
|
108
134
|
try {
|
|
109
135
|
const rows = query(db, `SELECT COUNT(*) FROM messages;`);
|
|
@@ -156,7 +182,7 @@ function migrateInPlace(db: string, preCount: number): boolean {
|
|
|
156
182
|
`INSERT INTO files_new (path, mtime, size) SELECT path, mtime, size FROM files;` +
|
|
157
183
|
`CREATE TABLE messages_new (file_id INTEGER NOT NULL REFERENCES files_new(id), t REAL, model TEXT NOT NULL,` +
|
|
158
184
|
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
159
|
-
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, id TEXT);` +
|
|
185
|
+
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, a TEXT, id TEXT);` +
|
|
160
186
|
`INSERT INTO messages_new (file_id, ${cols}) SELECT (SELECT id FROM files_new WHERE files_new.path = messages.file), ${cols} FROM messages;` +
|
|
161
187
|
`DROP TABLE messages;DROP TABLE files;` +
|
|
162
188
|
`ALTER TABLE files_new RENAME TO files;ALTER TABLE messages_new RENAME TO messages;` +
|
|
@@ -173,6 +199,29 @@ function migrateInPlace(db: string, preCount: number): boolean {
|
|
|
173
199
|
}
|
|
174
200
|
}
|
|
175
201
|
|
|
202
|
+
// v12 shape gains the agent kind (`main`, `subagent`) with a bare ALTER for the two names the
|
|
203
|
+
// store already holds. No rows move and no re-parse is forced, so the existing rows read
|
|
204
|
+
// `NULL` for it: a report then shows them under the main agent only, and new rows carry it.
|
|
205
|
+
function canAddAgent(db: string): boolean {
|
|
206
|
+
try {
|
|
207
|
+
const names = query(db, `PRAGMA table_info(messages);`).map((r) => r[1]);
|
|
208
|
+
return names.includes('file_id') && !names.includes('a');
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// v12 shape gains the agent kind (`main`, `subagent`) with a bare ALTER: no rows move, so no
|
|
215
|
+
// re-parse is forced. Existing rows read NULL, which a report folds under the main agent.
|
|
216
|
+
function addAgentColumn(db: string): boolean {
|
|
217
|
+
try {
|
|
218
|
+
run(db, `ALTER TABLE messages ADD COLUMN a TEXT;`);
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
176
225
|
// v10 shape gains the message id with a bare ALTER: no rows move, so no re-parse and no guard trip.
|
|
177
226
|
function canAlterInPlace(db: string): boolean {
|
|
178
227
|
try {
|
|
@@ -192,15 +241,21 @@ function alterInPlace(db: string): boolean {
|
|
|
192
241
|
return !canAlterInPlace(db);
|
|
193
242
|
}
|
|
194
243
|
|
|
195
|
-
function ensureSchema(db: string): void {
|
|
244
|
+
function ensureSchema(db: string, forceWipe: boolean): void {
|
|
196
245
|
fs.mkdirSync(path.dirname(db), { recursive: true });
|
|
197
246
|
// Fold in rows stored under a pre-alias spelling; the mtime ledger will not re-read those transcripts.
|
|
198
247
|
const rekeys = MODEL_ALIASES.map((a) => `UPDATE messages SET model='${a.id}' WHERE model='${a.feed}';`).join('');
|
|
199
248
|
const stored = storedParserVersion(db);
|
|
249
|
+
pragmas(db);
|
|
200
250
|
if (stored === PARSER_VERSION) {
|
|
201
251
|
run(db, SCHEMA_SQL + rekeys);
|
|
202
252
|
return;
|
|
203
253
|
}
|
|
254
|
+
// A newer store keeps its rows: only an explicit force re-parse may wipe it.
|
|
255
|
+
if (!forceWipe && isDowngrade(stored)) {
|
|
256
|
+
run(db, SCHEMA_SQL + rekeys);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
204
259
|
// A version bump rebuilds both tables, so no ALTER patchwork survives a migration.
|
|
205
260
|
try {
|
|
206
261
|
if (countMessages(db) > 0 && !newestBackupMatches(stored)) backupUsageDb(db);
|
|
@@ -213,6 +268,12 @@ function ensureSchema(db: string): void {
|
|
|
213
268
|
run(db, `INSERT OR REPLACE INTO meta (k,v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
214
269
|
return;
|
|
215
270
|
}
|
|
271
|
+
// The agent column is additive on the current shape: adding it keeps every row, and the
|
|
272
|
+
// unchanged transcripts that already hold their rows simply read NULL until a re-parse.
|
|
273
|
+
if (stored !== null && canAddAgent(db) && addAgentColumn(db)) {
|
|
274
|
+
run(db, `INSERT OR REPLACE INTO meta (k,v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
216
277
|
run(db, `DROP TABLE IF EXISTS messages;DROP TABLE IF EXISTS files;` + SCHEMA_SQL +
|
|
217
278
|
`INSERT OR REPLACE INTO meta (k,v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
218
279
|
} catch { /* fresh db, nothing to invalidate */ }
|
|
@@ -335,10 +396,13 @@ interface StoredRow {
|
|
|
335
396
|
note: string | undefined;
|
|
336
397
|
tools: string[];
|
|
337
398
|
host?: string;
|
|
399
|
+
/** `main` or `subagent`; null on a row written before the store carried it. */
|
|
400
|
+
agent?: string | null;
|
|
338
401
|
id?: string;
|
|
339
402
|
}
|
|
340
403
|
|
|
341
|
-
// OpenCode rows carry no status, code, note,
|
|
404
|
+
// OpenCode rows carry no status, code, note, tool list, or subagent marker: only tokens and the
|
|
405
|
+
// msg id. They record as the main agent, so a store never holds a NULL kind.
|
|
342
406
|
function opencodeStoredRow(oc: NonNullable<ReturnType<typeof classifyOpencodeMessage>>, host: string | undefined, id: string | undefined): StoredRow {
|
|
343
407
|
return {
|
|
344
408
|
t: oc.ms ?? null,
|
|
@@ -354,11 +418,12 @@ function opencodeStoredRow(oc: NonNullable<ReturnType<typeof classifyOpencodeMes
|
|
|
354
418
|
note: undefined,
|
|
355
419
|
tools: [],
|
|
356
420
|
host,
|
|
421
|
+
agent: 'main',
|
|
357
422
|
id,
|
|
358
423
|
};
|
|
359
424
|
}
|
|
360
425
|
|
|
361
|
-
function parseFile(text: string, host?: string, file?: string): StoredRow[] {
|
|
426
|
+
function parseFile(text: string, host?: string, file?: string, agent?: string): StoredRow[] {
|
|
362
427
|
const rows: StoredRow[] = [];
|
|
363
428
|
// Guarded: a JSONL transcript must fall through, not abort the sync.
|
|
364
429
|
let oc: ReturnType<typeof classifyOpencodeMessage> = null;
|
|
@@ -403,6 +468,7 @@ function parseFile(text: string, host?: string, file?: string): StoredRow[] {
|
|
|
403
468
|
note: undefined,
|
|
404
469
|
tools: [],
|
|
405
470
|
host,
|
|
471
|
+
agent,
|
|
406
472
|
id: rid,
|
|
407
473
|
});
|
|
408
474
|
continue;
|
|
@@ -426,6 +492,7 @@ function parseFile(text: string, host?: string, file?: string): StoredRow[] {
|
|
|
426
492
|
note,
|
|
427
493
|
tools: parsed.tools ?? [],
|
|
428
494
|
host,
|
|
495
|
+
agent,
|
|
429
496
|
id: rid,
|
|
430
497
|
});
|
|
431
498
|
} catch { /* skip corrupt lines */ }
|
|
@@ -434,10 +501,10 @@ function parseFile(text: string, host?: string, file?: string): StoredRow[] {
|
|
|
434
501
|
}
|
|
435
502
|
|
|
436
503
|
function insertSql(file: string, r: StoredRow): string {
|
|
437
|
-
return `INSERT INTO messages (file_id, t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, id) VALUES (` +
|
|
504
|
+
return `INSERT INTO messages (file_id, t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, a, id) VALUES (` +
|
|
438
505
|
`(SELECT id FROM files WHERE path=${esc(file)}),${r.t === null ? 'NULL' : String(r.t)},${esc(r.model)},${r.i},${r.o},` +
|
|
439
506
|
`${nullNum(r.d)},${r.cr},${r.cw},${nullNum(r.usd)},${esc(r.st)},` +
|
|
440
|
-
`${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))},${nullStr(r.host)},${nullStr(r.id)});`;
|
|
507
|
+
`${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))},${nullStr(r.host)},${nullStr(r.agent ?? undefined)},${nullStr(r.id)});`;
|
|
441
508
|
}
|
|
442
509
|
|
|
443
510
|
// Unchanged transcripts are skipped via mtime+size; rows for deleted ones are kept so rotation never erases history. False when sqlite3 is unavailable.
|
|
@@ -449,11 +516,14 @@ export function syncUsageDb(): boolean {
|
|
|
449
516
|
try {
|
|
450
517
|
db = usageDbPath();
|
|
451
518
|
const stored = storedParserVersion(db);
|
|
452
|
-
migrated = stored !== null && stored !== PARSER_VERSION;
|
|
453
519
|
// The operator deleted sessions on purpose and wants the mirror to match: bypass the guard.
|
|
454
520
|
const forced = process.env.TERSIO_FORCE_REPARSE === '1';
|
|
521
|
+
// A downgrade keeps its rows: the wipe below cannot rebuild them from rotated transcripts.
|
|
522
|
+
const downgrade = !forced && isDowngrade(stored);
|
|
523
|
+
migrated = stored !== null && stored !== PARSER_VERSION && !downgrade;
|
|
524
|
+
if (downgrade) guardReport = `store was written by a newer tersio (parser ${stored} > ${PARSER_VERSION}); kept its rows and skipped the re-parse`;
|
|
455
525
|
if (migrated && !forced) preWipe = countMessages(db);
|
|
456
|
-
ensureSchema(db);
|
|
526
|
+
ensureSchema(db, forced);
|
|
457
527
|
maybeScheduledBackup(db);
|
|
458
528
|
} catch {
|
|
459
529
|
return false;
|
|
@@ -526,7 +596,7 @@ export function syncUsageDb(): boolean {
|
|
|
526
596
|
try {
|
|
527
597
|
for (const file of changed) {
|
|
528
598
|
const text = fs.readFileSync(file, 'utf8');
|
|
529
|
-
for (const r of parseFile(text, hostOfSessionFile(file), file)) {
|
|
599
|
+
for (const r of parseFile(text, hostOfSessionFile(file), file, agentOfSessionText(text))) {
|
|
530
600
|
if (!push(insertSql(file, r))) return false;
|
|
531
601
|
}
|
|
532
602
|
}
|
|
@@ -561,6 +631,8 @@ export function syncUsageDb(): boolean {
|
|
|
561
631
|
if (migrated) {
|
|
562
632
|
try { run(db, `VACUUM;`); } catch { /* best-effort */ }
|
|
563
633
|
}
|
|
634
|
+
// Fold the WAL back into the main file: a backup copies one file, and a checkpoint keeps it whole.
|
|
635
|
+
try { run(db, `PRAGMA wal_checkpoint(TRUNCATE);`); } catch { /* best-effort */ }
|
|
564
636
|
return true;
|
|
565
637
|
}
|
|
566
638
|
|
|
@@ -573,18 +645,25 @@ export function readUsageDb(): StoredUsage | null {
|
|
|
573
645
|
}
|
|
574
646
|
let rows: string[][];
|
|
575
647
|
try {
|
|
576
|
-
|
|
648
|
+
// The `tools` column is the widest by far and the common case is the literal `[]`, so it is
|
|
649
|
+
// read as one column and skipped in JS rather than parsed per row. A store written before
|
|
650
|
+
// the agent column does not hold it, so the row is read without that column.
|
|
651
|
+
const names = query(db, `PRAGMA table_info(messages);`).map((r) => r[1]);
|
|
652
|
+
const agentCol = names.includes('a') ? 'a' : "''";
|
|
653
|
+
rows = query(db, `SELECT t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, ${agentCol}, id FROM messages;`);
|
|
577
654
|
} catch {
|
|
578
655
|
return null;
|
|
579
656
|
}
|
|
580
657
|
const accum = newSessionAccum();
|
|
581
|
-
for (const [t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, id] of rows) {
|
|
658
|
+
for (const [t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, a, id] of rows) {
|
|
582
659
|
const ts = t === '' ? undefined : Number(t);
|
|
583
660
|
let toolNames: string[] = [];
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
661
|
+
if (tools && tools !== '[]' && tools !== '') {
|
|
662
|
+
try {
|
|
663
|
+
const parsed: unknown = JSON.parse(tools);
|
|
664
|
+
if (Array.isArray(parsed)) toolNames = parsed.filter((x): x is string => typeof x === 'string');
|
|
665
|
+
} catch { /* keep empty */ }
|
|
666
|
+
}
|
|
588
667
|
const usage: Record<string, unknown> = {
|
|
589
668
|
input: Number(i) || 0,
|
|
590
669
|
output: Number(o) || 0,
|
|
@@ -609,6 +688,7 @@ export function readUsageDb(): StoredUsage | null {
|
|
|
609
688
|
toolNames,
|
|
610
689
|
h === '' ? undefined : h,
|
|
611
690
|
id === '' || id === undefined ? undefined : id,
|
|
691
|
+
a === '' ? undefined : a,
|
|
612
692
|
);
|
|
613
693
|
}
|
|
614
694
|
accum.recent.sort((a, b) => b.t - a.t);
|
|
@@ -627,6 +707,10 @@ export function readUsageDb(): StoredUsage | null {
|
|
|
627
707
|
byTool: accum.byTool,
|
|
628
708
|
byModelMessages: accum.byModelMessages,
|
|
629
709
|
byHost: accum.byHost,
|
|
710
|
+
byAgent: accum.byAgent,
|
|
711
|
+
byAgentMessages: accum.byAgentMessages,
|
|
712
|
+
byModelMeasured: accum.byModelMeasured,
|
|
713
|
+
byModelCostMessages: accum.byModelCostMessages,
|
|
630
714
|
costMeasured: accum.costMeasured,
|
|
631
715
|
recent: accum.recent.slice(0, RECENT_LIMIT),
|
|
632
716
|
},
|
|
@@ -13,7 +13,7 @@ export interface HttpOptions {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// Promise.withResolvers is Node 22+; the package supports Node 18+.
|
|
16
|
-
|
|
16
|
+
function withResolvers<T>(): PromiseWithResolvers<T> {
|
|
17
17
|
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
18
18
|
let reject!: (reason?: unknown) => void;
|
|
19
19
|
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
+
import fsSync from 'node:fs';
|
|
5
6
|
import fs from 'node:fs/promises';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
@@ -12,35 +13,44 @@ import {
|
|
|
12
13
|
CAVEMAN_REMOTE_ULTRA,
|
|
13
14
|
CAVEMAN_REMOTE_MEGACAVE,
|
|
14
15
|
RTK_RELEASE_API,
|
|
15
|
-
RtkRelease,
|
|
16
16
|
fetchJson,
|
|
17
17
|
findFile,
|
|
18
18
|
findHoistedPackage,
|
|
19
19
|
httpsGet,
|
|
20
20
|
httpsDownload,
|
|
21
|
-
isPiProcess,
|
|
22
21
|
sha256Hex,
|
|
23
22
|
parseChecksum,
|
|
24
23
|
normalizeRtkVersion,
|
|
25
24
|
readTextIfExists,
|
|
26
25
|
rtkPlatformSpec,
|
|
27
26
|
resolveRtkBinary,
|
|
28
|
-
} from '../
|
|
27
|
+
} from '../shared/utils.ts';
|
|
28
|
+
import type { RtkRelease } from '../shared/utils.ts';
|
|
29
29
|
|
|
30
30
|
const IS_WINDOWS = process.platform === 'win32';
|
|
31
31
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
32
32
|
|
|
33
33
|
const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
|
|
34
34
|
const RTK_BINARY = resolveRtkBinary();
|
|
35
|
-
// Beside this file, not a host path: these are the rules /caveman reads.
|
|
36
|
-
|
|
35
|
+
// Beside this file, not a host path: these are the rules /caveman reads. A retired
|
|
36
|
+
// agent-tree copy has no `caveman` sibling, so the OMP plugin install is the
|
|
37
|
+
// fallback: it is the one the plugin manifest loads from.
|
|
38
|
+
const CAVEMAN_SIBLING = (name: string): string => path.join(EXTENSION_DIR, '..', 'caveman', name);
|
|
39
|
+
|
|
40
|
+
function cavemanRule(name: string): string {
|
|
41
|
+
const beside = CAVEMAN_SIBLING(name);
|
|
42
|
+
if (fsSync.existsSync(beside)) return beside;
|
|
43
|
+
const plugin = path.join(os.homedir(), '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman', name);
|
|
44
|
+
return fsSync.existsSync(plugin) ? plugin : beside;
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
const CAVEMAN_LOCAL = cavemanRule('rule.md');
|
|
38
48
|
const CAVEMAN_TRACKED: ReadonlyArray<{ file: string; remote: string }> = [
|
|
39
49
|
{ file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
|
|
40
50
|
{ file: cavemanRule('rule-megacave.md'), remote: CAVEMAN_REMOTE_MEGACAVE },
|
|
41
51
|
];
|
|
42
|
-
|
|
43
|
-
const RELOAD_MSG =
|
|
52
|
+
// Host-neutral: this updater runs inside pi, OMP, and OpenCode alike.
|
|
53
|
+
const RELOAD_MSG = 'Reminder: restart your agent (or reload extensions) for updates to take effect.';
|
|
44
54
|
|
|
45
55
|
// Ponytail is a hoisted dependency, so the installed copy is found by walking up rather than by naming a host directory.
|
|
46
56
|
function ponytailLocal(): string | null {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krtclcdy/tersio",
|
|
3
|
-
"version": "2.25.
|
|
3
|
+
"version": "2.25.5",
|
|
4
4
|
"description": "✂️ Token saver and usage tracker for coding agents: terse Caveman replies, compact RTK shell output, lean Ponytail code calls, and a gain dashboard — one-command combo presets over one shared extension tree.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"token-saving",
|
|
@@ -24,16 +24,17 @@
|
|
|
24
24
|
"tersio": "dist/tersio.js"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
|
-
"./server": "./dist/extensions/
|
|
27
|
+
"./server": "./dist/extensions/server.js"
|
|
28
28
|
},
|
|
29
29
|
"omp": {
|
|
30
30
|
"name": "Tersio",
|
|
31
31
|
"description": "Toggleable Caveman/RTK/Ponytail session modes with Combo presets",
|
|
32
32
|
"extensions": [
|
|
33
|
-
"./extensions/caveman
|
|
34
|
-
"./extensions/rtk
|
|
35
|
-
"./extensions/
|
|
36
|
-
"./extensions/
|
|
33
|
+
"./extensions/caveman/index.ts",
|
|
34
|
+
"./extensions/rtk/session.ts",
|
|
35
|
+
"./extensions/rtk/filter.ts",
|
|
36
|
+
"./extensions/combo/index.ts",
|
|
37
|
+
"./extensions/commands/index.ts"
|
|
37
38
|
],
|
|
38
39
|
"skills": [
|
|
39
40
|
"./node_modules/@dietrichgebert/ponytail/skills"
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"description": "In-session updater for Ponytail, RTK, and Caveman (/ai-addons)",
|
|
44
45
|
"default": true,
|
|
45
46
|
"extensions": [
|
|
46
|
-
"./extensions/
|
|
47
|
+
"./extensions/updater/index.ts"
|
|
47
48
|
]
|
|
48
49
|
}
|
|
49
50
|
},
|
|
@@ -115,16 +116,16 @@
|
|
|
115
116
|
},
|
|
116
117
|
"files": [
|
|
117
118
|
"dist/**/*.js",
|
|
118
|
-
"dist/extensions/caveman
|
|
119
|
+
"dist/extensions/caveman/*.md",
|
|
119
120
|
"dashboard/dist/**/*",
|
|
120
121
|
"extensions/**/*.ts",
|
|
121
|
-
"extensions/caveman
|
|
122
|
-
"extensions/caveman
|
|
123
|
-
"extensions/caveman
|
|
122
|
+
"extensions/caveman/rule.md",
|
|
123
|
+
"extensions/caveman/rule-ultra.md",
|
|
124
|
+
"extensions/caveman/rule-megacave.md"
|
|
124
125
|
],
|
|
125
126
|
"scripts": {
|
|
126
127
|
"clean": "rm -rf dist dashboard/dist",
|
|
127
|
-
"build": "tsc && bun run build:dashboard && cp extensions/caveman
|
|
128
|
+
"build": "tsc && bun run build:dashboard && cp extensions/caveman/*.md dist/extensions/caveman/",
|
|
128
129
|
"build:dashboard": "bun install --cwd dashboard/app --frozen-lockfile && bun run --cwd dashboard/app build",
|
|
129
130
|
"prepublishOnly": "bun run build",
|
|
130
131
|
"check": "tsc --noEmit",
|