@krtclcdy/tersio 2.15.0 → 2.17.0

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