@krtclcdy/tersio 2.24.1 → 2.25.1
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 +27 -21
- package/cli/banner.js +7 -4
- package/cli/common.js +14 -13
- package/cli/currency.js +1 -2
- package/cli/dashboard.js +57 -14
- package/cli/doctor-fix.js +88 -21
- package/cli/doctor.js +32 -24
- package/cli/hosts.js +19 -18
- package/cli/install.js +123 -52
- package/cli/interactive.js +29 -19
- package/cli/manifest.js +7 -0
- package/cli/profile.js +42 -11
- package/cli/rtk-wiring.js +28 -26
- package/cli/settings.js +37 -8
- package/cli/uninstall.js +40 -7
- package/cli/update.js +22 -14
- package/cli/usage.js +39 -14
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +69 -15
- package/extensions/ai-addons-updater/index.ts +60 -17
- package/extensions/caveman-session/index.js +28 -55
- package/extensions/caveman-session/index.ts +28 -55
- package/extensions/combo-toggle/index.js +40 -58
- package/extensions/combo-toggle/index.ts +38 -67
- package/extensions/lib/utils.js +60 -15
- package/extensions/lib/utils.ts +59 -15
- package/extensions/opencode/index.js +104 -0
- package/extensions/opencode/server.js +190 -0
- package/extensions/opencode/server.ts +200 -0
- package/extensions/rtk-session/index.js +20 -35
- package/extensions/rtk-session/index.ts +19 -34
- package/extensions/shared/carbon.js +1 -2
- package/extensions/shared/carbon.ts +1 -2
- package/extensions/shared/host.js +95 -46
- package/extensions/shared/host.ts +106 -50
- package/extensions/shared/plugin-settings.js +4 -6
- package/extensions/shared/plugin-settings.ts +4 -6
- package/extensions/shared/pricing.js +79 -25
- package/extensions/shared/pricing.ts +80 -26
- package/extensions/shared/rtk-gain.js +3 -5
- package/extensions/shared/rtk-gain.ts +3 -5
- package/extensions/shared/session-state.js +10 -33
- package/extensions/shared/session-state.ts +12 -38
- package/extensions/shared/status.js +38 -0
- package/extensions/shared/status.ts +45 -0
- package/extensions/shared/types.js +1 -2
- package/extensions/shared/types.ts +2 -2
- package/extensions/shared/usage-ledger.js +190 -56
- package/extensions/shared/usage-ledger.ts +194 -58
- package/extensions/shared/usage-store.js +228 -64
- package/extensions/shared/usage-store.ts +235 -67
- package/extensions/tersio-commands/index.js +4 -9
- package/extensions/tersio-commands/index.ts +4 -9
- package/package.json +8 -2
- package/tersio.js +3 -3
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -22
- package/cli/omp-layer.js +0 -51
- package/cli/pi-layer.js +0 -37
- package/cli/pi-wiring.js +0 -92
- package/extensions/shared/omp-prompt.js +0 -12
package/cli/manifest.js
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
export const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
|
|
5
|
+
/** Root entrypoint OpenCode resolves for the installed tree; re-exports the versioned entry beside it. Built by join so the path never reads as a relative import (repo-integrity scans literals). */
|
|
6
|
+
const SERVER_SPEC = `./${['opencode', 'server.ts'].join('/')}`;
|
|
7
|
+
export const OPENCODE_SERVER_SHIM = `// Generated by \`tersio install --host opencode\`: OpenCode resolves <pluginDir>/server.ts.
|
|
8
|
+
export { default } from "${SERVER_SPEC}";
|
|
9
|
+
`;
|
|
5
10
|
/** Every file in the tree, as paths relative to the extensions directory. */
|
|
6
11
|
export const TREE_FILES = [
|
|
7
12
|
'shared/host.ts',
|
|
8
13
|
'shared/session-state.ts',
|
|
14
|
+
'shared/status.ts',
|
|
9
15
|
'shared/types.ts',
|
|
10
16
|
'shared/plugin-settings.ts',
|
|
11
17
|
'shared/usage-ledger.ts',
|
|
@@ -18,6 +24,7 @@ export const TREE_FILES = [
|
|
|
18
24
|
'combo-toggle/index.ts',
|
|
19
25
|
'tersio-commands/index.ts',
|
|
20
26
|
'ai-addons-updater/index.ts',
|
|
27
|
+
'opencode/server.ts',
|
|
21
28
|
];
|
|
22
29
|
export function sourcePath(relative) {
|
|
23
30
|
return path.join(EXT_DIR, ...relative.split('/'));
|
package/cli/profile.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
// cli/profile.ts — session-start defaults and the display-currency default,
|
|
2
|
-
|
|
3
|
-
import { existsSync, promises as fs } from 'node:fs';
|
|
2
|
+
import { existsSync, promises as fs, readFileSync } from 'node:fs';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, PACKAGE_NAME, PONYTAIL_DEFAULTS, verbose, } from './common.js';
|
|
6
5
|
import os from 'node:os';
|
|
7
6
|
import { DEFAULT_CURRENCY, isCurrencyCode } from './currency.js';
|
|
8
7
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
9
8
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
9
|
+
function readTextIfExistsSync(file) {
|
|
10
|
+
try {
|
|
11
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
10
17
|
export const BACKUP_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
11
18
|
function defaultProfile() {
|
|
12
19
|
return {
|
|
@@ -16,16 +23,19 @@ function defaultProfile() {
|
|
|
16
23
|
ponytailDefault: 'off',
|
|
17
24
|
currency: DEFAULT_CURRENCY,
|
|
18
25
|
backupSchedule: 'monthly',
|
|
26
|
+
subagentMarkers: [],
|
|
19
27
|
};
|
|
20
28
|
}
|
|
21
|
-
// Seed once from OMP plugin settings so a pre-~/.tersio install keeps its values.
|
|
22
|
-
|
|
29
|
+
// Seed once from OMP plugin settings so a pre-~/.tersio install keeps its values. Sync: awaiting let piped stdin arrive before `ask()` attached, so a scripted answer was swallowed and a destructive confirm defaulted to abort.
|
|
30
|
+
function storedProfileSync() {
|
|
23
31
|
const base = defaultProfile();
|
|
24
|
-
const stored = (
|
|
25
|
-
? parseStored(await readTextIfExists(tersioSettingsFile()))
|
|
26
|
-
: parseStored(await readTextIfExists(legacyOmpLockPath()));
|
|
32
|
+
const stored = parseStored(readTextIfExistsSync(tersioSettingsFile()) ?? readTextIfExistsSync(legacyOmpLockPath()));
|
|
27
33
|
if (!stored)
|
|
28
34
|
return base;
|
|
35
|
+
applyStored(base, stored);
|
|
36
|
+
return base;
|
|
37
|
+
}
|
|
38
|
+
function applyStored(base, stored) {
|
|
29
39
|
if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
|
|
30
40
|
base.comboDefault = stored.comboDefault;
|
|
31
41
|
if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
|
|
@@ -40,7 +50,13 @@ async function storedProfile() {
|
|
|
40
50
|
if (typeof stored.backupSchedule === 'string' && BACKUP_SCHEDULES.has(stored.backupSchedule)) {
|
|
41
51
|
base.backupSchedule = stored.backupSchedule;
|
|
42
52
|
}
|
|
43
|
-
|
|
53
|
+
if (Array.isArray(stored.subagentMarkers)) {
|
|
54
|
+
// An empty marker would match every turn.
|
|
55
|
+
base.subagentMarkers = stored.subagentMarkers.filter((m) => typeof m === 'string' && m.trim() !== '');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function storedProfile() {
|
|
59
|
+
return storedProfileSync();
|
|
44
60
|
}
|
|
45
61
|
function legacyOmpLockPath() {
|
|
46
62
|
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
@@ -64,8 +80,7 @@ function parseStored(raw) {
|
|
|
64
80
|
return null;
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
|
-
// Persist the profile where every host reads it. `omp plugin config get` no
|
|
68
|
-
// longer reports it; `tersio settings` does.
|
|
83
|
+
// Persist the profile where every host reads it. `omp plugin config get` no longer reports it; `tersio settings` does.
|
|
69
84
|
async function writePluginSettings(profile, options) {
|
|
70
85
|
const file = tersioSettingsFile();
|
|
71
86
|
const values = {
|
|
@@ -77,6 +92,7 @@ async function writePluginSettings(profile, options) {
|
|
|
77
92
|
ponytailDefault: profile.ponytailDefault,
|
|
78
93
|
currency: profile.currency,
|
|
79
94
|
backupSchedule: profile.backupSchedule,
|
|
95
|
+
...(profile.subagentMarkers.length > 0 ? { subagentMarkers: profile.subagentMarkers } : {}),
|
|
80
96
|
};
|
|
81
97
|
if (options.dryRun) {
|
|
82
98
|
if (verbose && !options.quiet)
|
|
@@ -90,4 +106,19 @@ async function writePluginSettings(profile, options) {
|
|
|
90
106
|
function formatProfile(profile) {
|
|
91
107
|
return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault}) · currency=${profile.currency}`;
|
|
92
108
|
}
|
|
93
|
-
export { defaultProfile, formatProfile, storedProfile, writePluginSettings };
|
|
109
|
+
export { defaultProfile, formatProfile, storedProfile, storedProfileSync, writePluginSettings };
|
|
110
|
+
/** The status line the extensions print, read from the persisted profile. */
|
|
111
|
+
export function formatCliStatus(profile) {
|
|
112
|
+
const { comboDefault, cavemanDefault, rtkDefault, ponytailDefault } = profile;
|
|
113
|
+
if (comboDefault === 'off' && cavemanDefault === 'off' && !rtkDefault && ponytailDefault === 'off') {
|
|
114
|
+
return '🧩 combo OFF';
|
|
115
|
+
}
|
|
116
|
+
const preset = COMBO_PRESET_MODES[comboDefault];
|
|
117
|
+
const isPreset = preset !== undefined
|
|
118
|
+
&& preset.caveman === cavemanDefault
|
|
119
|
+
&& preset.rtk === rtkDefault
|
|
120
|
+
&& preset.ponytail === ponytailDefault;
|
|
121
|
+
const level = isPreset ? comboDefault.toUpperCase() : 'CUSTOM';
|
|
122
|
+
const rtk = rtkDefault ? 'ON' : 'OFF';
|
|
123
|
+
return `🧩 combo ${level}: 🪨caveman=${cavemanDefault.toUpperCase()} ⚡rtk=${rtk} 🦥ponytail=${ponytailDefault.toUpperCase()}`;
|
|
124
|
+
}
|
package/cli/rtk-wiring.js
CHANGED
|
@@ -26,25 +26,39 @@ function execFileP(cmd, args, timeout) {
|
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
export async function wireRtkOmp(rtkBin, options = {}) {
|
|
29
|
+
async function runWire(rtkBin, spec, options) {
|
|
31
30
|
if (!options.dryRun)
|
|
32
|
-
debugWire(options,
|
|
31
|
+
debugWire(options, `Wiring rtk → ${spec.host} (${spec.mechanism} rewrite)…`);
|
|
33
32
|
if (options.dryRun)
|
|
34
33
|
return true;
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
try {
|
|
35
|
+
await tryRtkInit(rtkBin, spec.initArgs);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
console.log(` [warn] could not wire rtk → ${spec.host}: ${shortWiringError(e)}`);
|
|
39
|
+
console.log(` [hint] Manual: ${spec.hint}`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
export async function wireRtkOmp(rtkBin, options = {}) {
|
|
45
|
+
if (!(await runWire(rtkBin, { host: 'OMP', mechanism: 'bash tool_call', initArgs: ['init', '-g', '--agent', 'omp'], hint: 'rtk init -g --agent omp (needs rtk >= 0.49)' }, options)))
|
|
37
46
|
return false;
|
|
38
47
|
await ensureRtkInConfig(options);
|
|
39
48
|
return true;
|
|
40
49
|
}
|
|
50
|
+
export async function wireRtkOpencode(rtkBin, options = {}) {
|
|
51
|
+
return runWire(rtkBin, { host: 'OpenCode', mechanism: 'tool.execute.before', initArgs: ['init', '-g', '--opencode'], hint: 'rtk init -g --opencode' }, options);
|
|
52
|
+
}
|
|
53
|
+
export async function wireRtkPi(rtkBin, options = {}) {
|
|
54
|
+
return runWire(rtkBin, { host: 'pi', mechanism: 'bash tool_call', initArgs: ['init', '-g', '--agent', 'pi'], hint: 'rtk init -g --agent pi' }, options);
|
|
55
|
+
}
|
|
41
56
|
// The rtk.ts path rtk init writes (OMP_AGENT_DIR inlined; see header note).
|
|
42
57
|
function rtkExtensionPath() {
|
|
43
58
|
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
44
59
|
return path.join(home, '.omp', 'agent', 'extensions', 'rtk.ts');
|
|
45
60
|
}
|
|
46
|
-
// Append rtk.ts after the existing entries (or create the key) so the wire
|
|
47
|
-
// takes effect on the next OMP start.
|
|
61
|
+
// Append rtk.ts after the existing entries (or create the key) so the wire takes effect on the next OMP start.
|
|
48
62
|
export async function ensureRtkInConfig(options) {
|
|
49
63
|
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
50
64
|
const configPath = path.join(home, '.omp', 'agent', 'config.yml');
|
|
@@ -81,26 +95,14 @@ export async function ensureRtkInConfig(options) {
|
|
|
81
95
|
console.log(` [warn] could not register rtk in config.yml: ${e.message}`);
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
|
-
async function
|
|
98
|
+
async function tryRtkInit(rtkBin, initArgs) {
|
|
85
99
|
try {
|
|
86
|
-
await execFileP(rtkBin,
|
|
87
|
-
|
|
88
|
-
return true;
|
|
100
|
+
await execFileP(rtkBin, initArgs, 30000);
|
|
101
|
+
return;
|
|
89
102
|
}
|
|
90
|
-
catch
|
|
91
|
-
// A fresh binary can lose its first exec to macOS Gatekeeper,
|
|
92
|
-
// retry once before reporting failure.
|
|
93
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
94
|
-
try {
|
|
95
|
-
await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
|
|
96
|
-
debugWire(options, 'rtk OMP extension wired');
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
catch (e) {
|
|
100
|
-
void first;
|
|
101
|
-
console.log(` [warn] could not wire rtk → OMP: ${shortWiringError(e)}`);
|
|
102
|
-
console.log(' [hint] Manual: rtk init -g --agent omp (needs rtk >= 0.49)');
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
103
|
+
catch {
|
|
104
|
+
// A fresh binary can lose its first exec to macOS Gatekeeper: settle, then retry once.
|
|
105
105
|
}
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
107
|
+
await execFileP(rtkBin, initArgs, 30000);
|
|
106
108
|
}
|
package/cli/settings.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// cli/settings.ts — view/edit session-start defaults and the display currency.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, } from './common.js';
|
|
3
|
+
import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, subagentMarkerFlags, subagentMarkersGiven, } from './common.js';
|
|
4
4
|
import { CURRENCY_CODES } from './currency.js';
|
|
5
5
|
import { textTable } from './usage.js';
|
|
6
|
-
import { askInteractiveChoice, closeRL, tty } from './interactive.js';
|
|
7
|
-
import { formatProfile, storedProfile, writePluginSettings } from './profile.js';
|
|
6
|
+
import { ask, askInteractiveChoice, closeRL, tty } from './interactive.js';
|
|
7
|
+
import { formatCliStatus, formatProfile, storedProfile, writePluginSettings } from './profile.js';
|
|
8
8
|
import { readDiagSchedule, setDiagSchedule } from './dashboard.js';
|
|
9
9
|
function applyFlags(base) {
|
|
10
10
|
const next = { ...base };
|
|
@@ -20,6 +20,8 @@ function applyFlags(base) {
|
|
|
20
20
|
next.rtkDefault = rtkDefaultFlag === 'on';
|
|
21
21
|
if (ponytailDefaultFlag !== undefined)
|
|
22
22
|
next.ponytailDefault = ponytailDefaultFlag;
|
|
23
|
+
if (subagentMarkersGiven)
|
|
24
|
+
next.subagentMarkers = subagentMarkerFlags;
|
|
23
25
|
if (currencyGiven)
|
|
24
26
|
next.currency = currency;
|
|
25
27
|
return next;
|
|
@@ -41,6 +43,7 @@ async function askProfile(current) {
|
|
|
41
43
|
ponytailDefault: preset.ponytail,
|
|
42
44
|
currency: current.currency,
|
|
43
45
|
backupSchedule: current.backupSchedule,
|
|
46
|
+
subagentMarkers: current.subagentMarkers,
|
|
44
47
|
};
|
|
45
48
|
const caveman = await askInteractiveChoice('Caveman default', [...CAVEMAN_DEFAULTS].map((v) => ({
|
|
46
49
|
value: v, label: v,
|
|
@@ -67,8 +70,26 @@ async function askProfile(current) {
|
|
|
67
70
|
if (cur.status !== 'selected')
|
|
68
71
|
return null;
|
|
69
72
|
next.currency = cur.value;
|
|
73
|
+
const markers = await askSubagentMarkers(next.subagentMarkers);
|
|
74
|
+
if (markers === null)
|
|
75
|
+
return null;
|
|
76
|
+
next.subagentMarkers = markers;
|
|
70
77
|
return next;
|
|
71
78
|
}
|
|
79
|
+
// One marker per line; an empty line ends the list. Null aborts.
|
|
80
|
+
async function askSubagentMarkers(current) {
|
|
81
|
+
console.log(' Subagent prompt markers (one per line, empty line to finish).');
|
|
82
|
+
if (current.length > 0)
|
|
83
|
+
console.log(` Current: ${current.join(' | ')}`);
|
|
84
|
+
const entered = [];
|
|
85
|
+
for (;;) {
|
|
86
|
+
const line = (await ask(` marker ${entered.length + 1}: `)).trim();
|
|
87
|
+
if (line === '')
|
|
88
|
+
break;
|
|
89
|
+
entered.push(line);
|
|
90
|
+
}
|
|
91
|
+
return entered;
|
|
92
|
+
}
|
|
72
93
|
async function askDiagSchedule(current) {
|
|
73
94
|
const picked = await askInteractiveChoice('Diagnosis auto-check', [
|
|
74
95
|
{ value: 'manual', label: 'manual', hint: 'check on open' },
|
|
@@ -80,12 +101,11 @@ async function askDiagSchedule(current) {
|
|
|
80
101
|
return null;
|
|
81
102
|
return picked.value;
|
|
82
103
|
}
|
|
83
|
-
const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'currency', 'diagnosis'];
|
|
104
|
+
const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'currency', 'diagnosis', 'markers'];
|
|
84
105
|
function settingsUsage() {
|
|
85
|
-
console.log(' Usage: tersio settings [combo|caveman|rtk|ponytail|currency|diagnosis] [--combo-default off|medium|balanced|max] [--caveman-default off|lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra] [--rtk-default on|off] [--ponytail-default off|lite|full|ultra] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--diag-schedule manual|daily|weekly|monthly] [--dry-run]');
|
|
106
|
+
console.log(' Usage: tersio settings [combo|caveman|rtk|ponytail|currency|diagnosis|markers] [--combo-default off|medium|balanced|max] [--caveman-default off|lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra] [--rtk-default on|off] [--ponytail-default off|lite|full|ultra] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--diag-schedule manual|daily|weekly|monthly] [--subagent-marker <text>]... [--dry-run]');
|
|
86
107
|
}
|
|
87
|
-
// Single-setting jump: `tersio settings diagnosis` prompts only that value
|
|
88
|
-
// instead of walking the whole chain. Returns false when aborted.
|
|
108
|
+
// Single-setting jump: `tersio settings diagnosis` prompts only that value instead of walking the whole chain. Returns false when aborted.
|
|
89
109
|
async function runSingleSetting(name, current, nextDiag) {
|
|
90
110
|
const next = { ...current };
|
|
91
111
|
let diag = nextDiag;
|
|
@@ -150,6 +170,13 @@ async function runSingleSetting(name, current, nextDiag) {
|
|
|
150
170
|
diag = picked;
|
|
151
171
|
break;
|
|
152
172
|
}
|
|
173
|
+
case 'markers': {
|
|
174
|
+
const markers = await askSubagentMarkers(next.subagentMarkers);
|
|
175
|
+
if (markers === null)
|
|
176
|
+
return null;
|
|
177
|
+
next.subagentMarkers = markers;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
153
180
|
default: {
|
|
154
181
|
console.error(`[fail] Invalid setting: ${name}. Valid: ${SETTING_NAMES.join(', ')}`);
|
|
155
182
|
process.exit(1);
|
|
@@ -166,6 +193,7 @@ function printSettingsTable(current) {
|
|
|
166
193
|
['rtk', current.rtkDefault ? 'on' : 'off', 'on | off'],
|
|
167
194
|
['ponytail', current.ponytailDefault, [...PONYTAIL_DEFAULTS].join(' | ')],
|
|
168
195
|
['diagnosis', readDiagSchedule(), 'manual | daily | weekly | monthly'],
|
|
196
|
+
['markers', current.subagentMarkers.length === 0 ? 'default' : current.subagentMarkers.join(' | '), 'host subagent prompt text'],
|
|
169
197
|
];
|
|
170
198
|
for (const l of textTable(['Setting', 'Current', 'Valid values'], rows, [false, false, false], 64)) {
|
|
171
199
|
console.log(l);
|
|
@@ -201,7 +229,7 @@ async function runSettings() {
|
|
|
201
229
|
next = single.profile;
|
|
202
230
|
nextDiag = single.diag;
|
|
203
231
|
}
|
|
204
|
-
else if (profileFlagsGiven || currencyGiven || diagScheduleFlag !== undefined) {
|
|
232
|
+
else if (profileFlagsGiven || currencyGiven || diagScheduleFlag !== undefined || subagentMarkersGiven) {
|
|
205
233
|
next = applyFlags(current);
|
|
206
234
|
}
|
|
207
235
|
else if (tty()) {
|
|
@@ -232,6 +260,7 @@ async function runSettings() {
|
|
|
232
260
|
}
|
|
233
261
|
await writePluginSettings(next, {});
|
|
234
262
|
setDiagSchedule(nextDiag);
|
|
263
|
+
console.log(` ${formatCliStatus(next)}`);
|
|
235
264
|
console.log(` Defaults: ${formatProfile(next)} · diagnosis=${nextDiag}`);
|
|
236
265
|
console.log(' Applies to fresh sessions only — anything persisted with /combo, /caveman, or /rtk wins.');
|
|
237
266
|
console.log(' Currency applies to usage/dashboard reports; --currency overrides it per run.');
|
package/cli/uninstall.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// cli/uninstall.ts — remove managed extensions, plugins, and binaries.
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, RTK_BINARY_NAME, args, dryRun, keepPonytail, removePonytail, removeRtk, yes, debug, writeConfigLines, } from './common.js';
|
|
4
|
+
import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, RTK_BINARY_NAME, args, dryRun, keepPonytail, removePonytail, removeRtk, yes, debug, writeConfigLines, } from './common.js';
|
|
5
5
|
import { askInteractiveChoice, closeRL, confirmDestructive, tty, sayTagged } from './interactive.js';
|
|
6
6
|
import { detectHosts, hostHint, hostLabel, parseHostArg } from './hosts.js';
|
|
7
7
|
import { findHoistedPackage, piAgentDir, readTextIfExists } from '../extensions/lib/utils.js';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
10
|
-
// The pi tree mirrors the OMP one. Ponytail and rtk stay: a package and a
|
|
11
|
-
// shared binary are not ours to delete.
|
|
10
|
+
// The pi tree mirrors the OMP one. Ponytail and rtk stay: a package and a shared binary are not ours to delete.
|
|
12
11
|
const PONYTAIL_PKG = '@dietrichgebert/ponytail';
|
|
13
12
|
const PI_TREE_DIRS = [
|
|
14
13
|
'caveman-session',
|
|
@@ -18,6 +17,7 @@ const PI_TREE_DIRS = [
|
|
|
18
17
|
'tersio-commands',
|
|
19
18
|
'shared',
|
|
20
19
|
'lib',
|
|
20
|
+
'opencode',
|
|
21
21
|
];
|
|
22
22
|
// One `pi remove`, for both packages the installer adds there.
|
|
23
23
|
async function piRemove(spec, shouldDryRun) {
|
|
@@ -79,8 +79,7 @@ async function removePiLayer(host, shouldDryRun, shouldRemovePonytail, shouldRem
|
|
|
79
79
|
await removeCopiedPonytailSkills(shouldDryRun);
|
|
80
80
|
}
|
|
81
81
|
await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
|
|
82
|
-
// rtk's wiring is a loose file, not one of the tree dirs, so it survived
|
|
83
|
-
// --remove-rtk on pi while OMP removed its copy.
|
|
82
|
+
// rtk's wiring is a loose file, not one of the tree dirs, so it survived --remove-rtk on pi while OMP removed its copy.
|
|
84
83
|
if (shouldRemoveRtk) {
|
|
85
84
|
await removeUninstallTarget(rtkWiring, shouldDryRun, false);
|
|
86
85
|
await removeUninstallTarget(path.join(BUN_BIN_DIR, RTK_BINARY_NAME), shouldDryRun, false);
|
|
@@ -186,6 +185,39 @@ async function runUninstall(options = {}) {
|
|
|
186
185
|
closeRL();
|
|
187
186
|
return removed;
|
|
188
187
|
}
|
|
188
|
+
if (host === 'opencode') {
|
|
189
|
+
const dir = path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
|
|
190
|
+
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
191
|
+
if (!selected.installed && !selected.declared) {
|
|
192
|
+
sayTagged(` [skip] ${selected.label} — nothing installed (${selected.installCmd})`);
|
|
193
|
+
closeRL();
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
console.log('Will remove:');
|
|
197
|
+
console.log(` ${dir}`);
|
|
198
|
+
console.log(` ${path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts')} (rtk auto-wrap; reinstall with: rtk init -g --opencode)`);
|
|
199
|
+
console.log(` the tersio entry in ${configPath}`);
|
|
200
|
+
if (!confirmed && !(await confirmDestructive('Remove these Tersio files?'))) {
|
|
201
|
+
closeRL();
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
await removeUninstallTarget(dir, shouldDryRun);
|
|
205
|
+
await removeUninstallTarget(path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts'), shouldDryRun, false);
|
|
206
|
+
if (!shouldDryRun) {
|
|
207
|
+
try {
|
|
208
|
+
const raw = await readTextIfExists(configPath);
|
|
209
|
+
const config = raw ? JSON.parse(raw) : {};
|
|
210
|
+
if (Array.isArray(config.plugins)) {
|
|
211
|
+
config.plugins = config.plugins.filter((p) => typeof p !== 'string' || !p.includes('/opencode/plugins/tersio/'));
|
|
212
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch { /* config missing or not JSON; nothing to scrub */ }
|
|
216
|
+
}
|
|
217
|
+
console.log('\nDone. Restart OpenCode to drop the plugin.');
|
|
218
|
+
closeRL();
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
189
221
|
if (!selected.installed)
|
|
190
222
|
sayTagged(` [note] ${selected.label} has no tersio install; removing what is left behind.`);
|
|
191
223
|
const extDir = path.join(OMP_AGENT_DIR, 'extensions');
|
|
@@ -204,6 +236,8 @@ async function runUninstall(options = {}) {
|
|
|
204
236
|
'lib',
|
|
205
237
|
// Legacy: imports session-state.js, so it warns once that module is gone.
|
|
206
238
|
'aaa-combo-boot',
|
|
239
|
+
// Foreign to the OMP loader; never shipped here on purpose.
|
|
240
|
+
'opencode',
|
|
207
241
|
].map((dir) => path.join(extDir, dir));
|
|
208
242
|
console.log('Will remove:');
|
|
209
243
|
for (const t of targets) {
|
|
@@ -230,8 +264,7 @@ async function runUninstall(options = {}) {
|
|
|
230
264
|
return false;
|
|
231
265
|
if (shouldRemovePonytail && l.includes('ponytail') && l.includes('pi-extension'))
|
|
232
266
|
return false;
|
|
233
|
-
// Our rtk wiring, absolute or relative. Leaving it listed would keep
|
|
234
|
-
// OMP loading a hook for a package that is no longer installed.
|
|
267
|
+
// Our rtk wiring, absolute or relative. Leaving it listed would keep OMP loading a hook for a package that is no longer installed.
|
|
235
268
|
const entry = l.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '');
|
|
236
269
|
if (entry.endsWith(`extensions${path.sep}rtk.ts`) || entry.endsWith('extensions/rtk.ts'))
|
|
237
270
|
return false;
|
package/cli/update.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { IS_WINDOWS, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, dryRun, execP, parseJsonObject, verbose, } from './common.js';
|
|
5
|
+
import { IS_WINDOWS, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, execP, parseJsonObject, verbose, } from './common.js';
|
|
6
6
|
import { execNetwork, sayTagged } from './interactive.js';
|
|
7
7
|
import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/lib/utils.js';
|
|
8
8
|
import { refreshPrices } from '../extensions/shared/pricing.js';
|
|
9
9
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
|
|
10
|
+
import { parseHostArg } from './hosts.js';
|
|
10
11
|
const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
11
12
|
function newerThan(a, b) {
|
|
12
13
|
const pa = a.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
|
|
@@ -34,8 +35,7 @@ async function latestPublishedVersion() {
|
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
// A newer published version, null when up to date, 'unknown' when the registry
|
|
38
|
-
// is unreachable. Cached for a TTL unless force, which must not trust a cache.
|
|
38
|
+
// A newer published version, null when up to date, 'unknown' when the registry is unreachable. Cached for a TTL unless force, which must not trust a cache.
|
|
39
39
|
async function checkForUpdate(force = false) {
|
|
40
40
|
const cachePath = tersioDataPath('update-check.json', 'tersio-update-check.json');
|
|
41
41
|
const cached = parseJsonObject(await readTextIfExists(cachePath));
|
|
@@ -48,8 +48,7 @@ async function checkForUpdate(force = false) {
|
|
|
48
48
|
await fs.writeFile(cachePath, JSON.stringify({ latest, lastCheck: Date.now() }) + '\n', 'utf8').catch(() => { });
|
|
49
49
|
return newerThan(latest, PACKAGE_VERSION) ? latest : null;
|
|
50
50
|
}
|
|
51
|
-
// Unreachable registry: a stale cache naming a newer release is still
|
|
52
|
-
// actionable, else report unknown so nobody claims "latest".
|
|
51
|
+
// Unreachable registry: a stale cache naming a newer release is still actionable, else report unknown so nobody claims "latest".
|
|
53
52
|
return cached?.latest && newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : 'unknown';
|
|
54
53
|
}
|
|
55
54
|
// Race a probe against a timeout; failures resolve null so nothing blocks.
|
|
@@ -75,8 +74,7 @@ async function execInherit(cmd, args) {
|
|
|
75
74
|
if (code !== 0)
|
|
76
75
|
throw new Error(`${cmd} exited with code ${code}`);
|
|
77
76
|
}
|
|
78
|
-
// Current → latest per add-on. Every probe is capped and nullable, and the
|
|
79
|
-
// plan prints `unknown` for anything unreachable.
|
|
77
|
+
// Current → latest per add-on. Every probe is capped and nullable, and the plan prints `unknown` for anything unreachable.
|
|
80
78
|
async function probeUpdatePlan(cliLatest) {
|
|
81
79
|
const rtkBin = resolveRtkBinary();
|
|
82
80
|
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
@@ -97,8 +95,7 @@ async function probeUpdatePlan(cliLatest) {
|
|
|
97
95
|
ponytail: ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null,
|
|
98
96
|
};
|
|
99
97
|
}
|
|
100
|
-
// Every add-on prints a version jump when stale, "up to date" otherwise, and
|
|
101
|
-
// "unknown" when its source cannot be reached.
|
|
98
|
+
// Every add-on prints a version jump when stale, "up to date" otherwise, and "unknown" when its source cannot be reached.
|
|
102
99
|
function planLines(plan) {
|
|
103
100
|
const stale = [];
|
|
104
101
|
const status = [];
|
|
@@ -131,6 +128,18 @@ async function runLatestUpdate() {
|
|
|
131
128
|
forwardedArgs.push('--dry-run');
|
|
132
129
|
if (verbose)
|
|
133
130
|
forwardedArgs.push('--verbose');
|
|
131
|
+
// A pinned host refreshes that host's tree; bare update keeps the OMP default.
|
|
132
|
+
let pinned;
|
|
133
|
+
try {
|
|
134
|
+
pinned = parseHostArg(args);
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
console.error(`[fail] ${e.message}`);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (pinned)
|
|
142
|
+
forwardedArgs.push('--host', pinned);
|
|
134
143
|
const npmCommand = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
|
|
135
144
|
// `--prefer-online` beats the local cache, so a stale @latest cannot pin down.
|
|
136
145
|
const cliLatest = await latestPublishedVersion();
|
|
@@ -169,8 +178,7 @@ async function runLatestUpdate() {
|
|
|
169
178
|
process.exitCode = 1;
|
|
170
179
|
return;
|
|
171
180
|
}
|
|
172
|
-
// The npx delegation below only refreshes the OMP-side files; the globally
|
|
173
|
-
// installed CLI keeps its old version until npm -g runs. Refresh both.
|
|
181
|
+
// The npx delegation below only refreshes the OMP-side files; the globally installed CLI keeps its old version until npm -g runs. Refresh both.
|
|
174
182
|
const globalArgs = IS_WINDOWS
|
|
175
183
|
? ['/d', '/s', '/c', 'npm', 'install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online']
|
|
176
184
|
: ['install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online'];
|
|
@@ -200,11 +208,11 @@ async function runLatestUpdate() {
|
|
|
200
208
|
...forwardedArgs,
|
|
201
209
|
];
|
|
202
210
|
const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
|
|
203
|
-
// Inherited stdio, no outer spinner; the delegated installer runs quiet and
|
|
204
|
-
// this parent owns both the plan line and the closing summary.
|
|
211
|
+
// Inherited stdio, no outer spinner; the delegated installer runs quiet and this parent owns both the plan line and the closing summary.
|
|
205
212
|
try {
|
|
206
213
|
await execInherit(npmCommand, npmCommandArgs);
|
|
207
|
-
|
|
214
|
+
const hostName = pinned === 'pi' ? 'pi' : pinned === 'opencode' ? 'OpenCode' : 'OMP';
|
|
215
|
+
console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION}. Restart ${hostName}.`);
|
|
208
216
|
if (!dryRun) {
|
|
209
217
|
try {
|
|
210
218
|
await refreshPrices();
|
package/cli/usage.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// cli/usage.ts — ledger + session-token usage report, tokscale-style.
|
|
2
|
-
import { co2GramsFor, displayModelId, energyWhFor, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readRtkAdoption, readRtkRecallDiagnostics, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
2
|
+
import { co2GramsFor, displayModelId, energyWhFor, foldModelsByLabel, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readRtkAdoption, readRtkRecallDiagnostics, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
3
|
import { resolveRtkBinary } from '../extensions/lib/utils.js';
|
|
4
4
|
import { readRtkGain } from '../extensions/shared/rtk-gain.js';
|
|
5
|
-
import { readUsageDb, syncUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
5
|
+
import { readUsageDb, reparseGuardReport, syncUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
6
6
|
import { withInteractiveSpinner } from './interactive.js';
|
|
7
7
|
import { PACKAGE_VERSION, currency } from './common.js';
|
|
8
8
|
import { formatCurrency } from './currency.js';
|
|
9
|
-
// RTK's history is machine-wide; rows group by command alone. 2000 groups
|
|
10
|
-
// covers a long history while still bounding /data.json.
|
|
9
|
+
// RTK's history is machine-wide; rows group by command alone. 2000 groups covers a long history while still bounding /data.json.
|
|
11
10
|
const RTK_COMMAND_ROWS = 2000;
|
|
12
11
|
export function summarizeUsage(rows) {
|
|
13
12
|
refreshPricesIfStale();
|
|
@@ -18,8 +17,7 @@ export function summarizeUsage(rows) {
|
|
|
18
17
|
detailCounts[r.detail] = (detailCounts[r.detail] ?? 0) + 1;
|
|
19
18
|
}
|
|
20
19
|
const byDetail = Object.entries(detailCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
21
|
-
// usage.db is a cache of the same per-message rows: sync best-effort, read
|
|
22
|
-
// stored, fall back to the live parse when the store is missing or stale.
|
|
20
|
+
// usage.db is a cache of the same per-message rows: sync best-effort, read stored, fall back to the live parse when the store is missing or stale.
|
|
23
21
|
let synced = false;
|
|
24
22
|
try {
|
|
25
23
|
synced = syncUsageDb();
|
|
@@ -27,6 +25,9 @@ export function summarizeUsage(rows) {
|
|
|
27
25
|
catch {
|
|
28
26
|
synced = false;
|
|
29
27
|
}
|
|
28
|
+
const guard = reparseGuardReport();
|
|
29
|
+
if (guard)
|
|
30
|
+
console.warn(`[warn] usage mirror: ${guard}`);
|
|
30
31
|
let stored = null;
|
|
31
32
|
try {
|
|
32
33
|
stored = readUsageDb()?.tokens ?? null;
|
|
@@ -42,19 +43,43 @@ export function summarizeUsage(rows) {
|
|
|
42
43
|
let savedUsd = 0;
|
|
43
44
|
const byModelUsd = {};
|
|
44
45
|
const byModelBucketUsd = {};
|
|
46
|
+
const byHost = {};
|
|
47
|
+
const byDayModel = {};
|
|
45
48
|
let co2g = 0;
|
|
46
49
|
let energyWh = 0;
|
|
50
|
+
const addTok = (into, t) => {
|
|
51
|
+
into.input += t.input;
|
|
52
|
+
into.output += t.output;
|
|
53
|
+
into.cacheRead += t.cacheRead;
|
|
54
|
+
into.cacheWrite += t.cacheWrite;
|
|
55
|
+
};
|
|
56
|
+
// Price by exact key first (gateways can differ), then fold every map under the display label.
|
|
57
|
+
const folded = foldModelsByLabel(session.byModel, session.byModelMessages);
|
|
47
58
|
for (const [model, t] of Object.entries(session.byModel)) {
|
|
48
59
|
const c = usdCost(t, model);
|
|
49
60
|
usd += c.usd;
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
const label = displayModelId(model);
|
|
62
|
+
byModelUsd[label] = (byModelUsd[label] ?? 0) + c.usd;
|
|
63
|
+
byModelBucketUsd[label] ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
64
|
+
addTok(byModelBucketUsd[label], c.buckets);
|
|
65
|
+
for (const [host, hb] of Object.entries(session.byHost[model] ?? {})) {
|
|
66
|
+
byHost[label] ??= {};
|
|
67
|
+
byHost[label][host] ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
68
|
+
addTok(byHost[label][host], hb);
|
|
69
|
+
}
|
|
52
70
|
co2g += co2GramsFor(model, t.output);
|
|
53
71
|
energyWh += energyWhFor(model, t.output);
|
|
54
72
|
if (!c.priced)
|
|
55
73
|
priced = false;
|
|
56
74
|
savedUsd += (t.cacheRead / 1e6) * priceFor(model).price.input;
|
|
57
75
|
}
|
|
76
|
+
for (const [day, per] of Object.entries(session.byDayModel)) {
|
|
77
|
+
for (const [model, n] of Object.entries(per)) {
|
|
78
|
+
const label = displayModelId(model);
|
|
79
|
+
byDayModel[day] ??= {};
|
|
80
|
+
byDayModel[day][label] = (byDayModel[day][label] ?? 0) + n;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
58
83
|
const byTool = Object.entries(session.byTool).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
59
84
|
return {
|
|
60
85
|
total: rows.length,
|
|
@@ -64,12 +89,13 @@ export function summarizeUsage(rows) {
|
|
|
64
89
|
empty: rows.length === 0 && session.messages === 0,
|
|
65
90
|
messages: session.messages,
|
|
66
91
|
tokens: session.totals,
|
|
67
|
-
byModel:
|
|
92
|
+
byModel: folded.byModel,
|
|
68
93
|
byModelUsd,
|
|
69
94
|
byModelBucketUsd,
|
|
70
|
-
byModelMessages:
|
|
95
|
+
byModelMessages: folded.byModelMessages,
|
|
96
|
+
byHost,
|
|
71
97
|
byDay: session.byDay,
|
|
72
|
-
byDayModel
|
|
98
|
+
byDayModel,
|
|
73
99
|
byTool,
|
|
74
100
|
recent: session.recent.map((r) => ({
|
|
75
101
|
...r,
|
|
@@ -117,8 +143,7 @@ const ROW_CAP = 15;
|
|
|
117
143
|
function pad(s, n) {
|
|
118
144
|
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
119
145
|
}
|
|
120
|
-
// Box-drawing table, plain text (piped-safe, byte-stable). Numeric columns
|
|
121
|
-
// right-align; long cells truncate with an ellipsis. Shared with cli/settings.ts.
|
|
146
|
+
// Box-drawing table, plain text (piped-safe, byte-stable). Numeric columns right-align; long cells truncate with an ellipsis. Shared with cli/settings.ts.
|
|
122
147
|
function textTable(headers, rows, right = [], maxW = 32) {
|
|
123
148
|
const cells = [headers, ...rows].map((r) => r.map((c) => (c.length > maxW ? c.slice(0, maxW - 1) + '…' : c)));
|
|
124
149
|
const widths = headers.map((_, i) => Math.max(...cells.map((r) => r[i].length)));
|
|
@@ -153,7 +178,7 @@ function printReport(report) {
|
|
|
153
178
|
const mt = b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
154
179
|
const hit = b.input + b.cacheRead ? (b.cacheRead / (b.input + b.cacheRead)) * 100 : 0;
|
|
155
180
|
const usd = report.byModelUsd[model] ?? 0;
|
|
156
|
-
return [
|
|
181
|
+
return [model, fmt(b.input), fmt(b.output), `${fmt(b.cacheRead)}/${fmt(b.cacheWrite)}`, formatCurrency(usd, report.currency), `${hit.toFixed(1)}%`, `${bar(mt / top, 8)} ${fmtShort(mt)}`];
|
|
157
182
|
});
|
|
158
183
|
for (const l of textTable(['Model', 'Input', 'Output', 'Cache r/w', report.currency, 'Hit', 'Share'], mrows, [false, true, true, true, true, true, false])) {
|
|
159
184
|
console.log(l);
|