@krtclcdy/tersio 2.24.0 → 2.24.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 +1 -3
- package/cli/banner.js +2 -3
- package/cli/currency.js +3 -7
- package/cli/dashboard.js +167 -3
- package/cli/doctor-fix.js +15 -12
- package/cli/doctor.js +1 -12
- package/cli/hosts.js +7 -11
- package/cli/install.js +113 -71
- package/cli/interactive.js +36 -2
- package/cli/layer.js +1 -3
- package/cli/manifest.js +1 -4
- package/cli/profile.js +9 -7
- package/cli/reset.js +3 -7
- package/cli/rtk-wiring.js +1 -6
- package/cli/settings.js +3 -8
- package/cli/uninstall.js +43 -24
- package/cli/update.js +7 -8
- package/cli/usage.js +15 -5
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +6 -5
- package/extensions/ai-addons-updater/index.ts +7 -6
- package/extensions/caveman-session/index.js +14 -29
- package/extensions/caveman-session/index.ts +15 -30
- package/extensions/combo-toggle/index.js +10 -14
- package/extensions/combo-toggle/index.ts +10 -15
- package/extensions/lib/utils.js +4 -0
- package/extensions/lib/utils.ts +3 -0
- package/extensions/rtk-session/index.js +27 -31
- package/extensions/rtk-session/index.ts +25 -29
- package/extensions/shared/carbon.js +3 -8
- package/extensions/shared/carbon.ts +4 -8
- package/extensions/shared/host.js +1 -11
- package/extensions/shared/host.ts +1 -11
- package/extensions/shared/plugin-settings.js +4 -14
- package/extensions/shared/plugin-settings.ts +4 -14
- package/extensions/shared/pricing.js +4 -6
- package/extensions/shared/pricing.ts +4 -6
- package/extensions/shared/rtk-gain.js +2 -7
- package/extensions/shared/rtk-gain.ts +2 -7
- package/extensions/shared/session-state.js +30 -18
- package/extensions/shared/session-state.ts +33 -16
- package/extensions/shared/usage-ledger.js +138 -17
- package/extensions/shared/usage-ledger.ts +143 -19
- package/extensions/shared/usage-store.js +175 -23
- package/extensions/shared/usage-store.ts +171 -22
- package/extensions/tersio-commands/index.js +1 -5
- package/extensions/tersio-commands/index.ts +1 -5
- package/package.json +6 -7
- package/tersio.js +1 -3
package/cli/install.js
CHANGED
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { BUN_BIN_DIR, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
7
|
-
import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
|
|
7
|
+
import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, withInteractiveTask, sayTagged } from './interactive.js';
|
|
8
8
|
import { printWelcome } from './banner.js';
|
|
9
9
|
import { checkForUpdate, runLatestUpdate } from './update.js';
|
|
10
10
|
import { runUninstall } from './uninstall.js';
|
|
@@ -15,6 +15,8 @@ import { runDashboard } from './dashboard.js';
|
|
|
15
15
|
import { wireRtkOmp } from './rtk-wiring.js';
|
|
16
16
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, parseChecksum, piAgentDir, readTextIfExists, resolveRtkBinary, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
|
|
17
17
|
import { storedProfile, writePluginSettings } from './profile.js';
|
|
18
|
+
import { runSettings } from './settings.js';
|
|
19
|
+
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
18
20
|
import { filesUnder, sourcePath } from './manifest.js';
|
|
19
21
|
import { detectHosts, hostHint, hostLabel, parseHostArg, piTersioSource } from './hosts.js';
|
|
20
22
|
async function stepPonytail(pluginsDir, options) {
|
|
@@ -31,9 +33,9 @@ async function stepPonytail(pluginsDir, options) {
|
|
|
31
33
|
}
|
|
32
34
|
if (options.dryRun) {
|
|
33
35
|
if (verbose && !options.quiet)
|
|
34
|
-
|
|
36
|
+
sayTagged(` [dry-run] would write ${pkgPath}`);
|
|
35
37
|
if (migrated && verbose && !options.quiet)
|
|
36
|
-
|
|
38
|
+
sayTagged(` [dry-run] would drop the separate @dietrichgebert/ponytail dependency (bundled with tersio)`);
|
|
37
39
|
}
|
|
38
40
|
else if (migrated) {
|
|
39
41
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
@@ -63,7 +65,7 @@ async function stepPonytail(pluginsDir, options) {
|
|
|
63
65
|
await execNetwork('Installing bundled Ponytail', 'bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
|
|
64
66
|
}
|
|
65
67
|
catch (e) {
|
|
66
|
-
|
|
68
|
+
sayTagged(` [fail] Could not install bundled ponytail: ${e.message}`);
|
|
67
69
|
console.log(' [hint] Manual: cd ~/.omp/plugins && npm install --no-audit --no-fund');
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -91,14 +93,14 @@ async function stepSelfPlugin(pluginsDir, options) {
|
|
|
91
93
|
if (legacy in pkg.dependencies) {
|
|
92
94
|
delete pkg.dependencies[legacy];
|
|
93
95
|
if (!options.dryRun && !options.quiet)
|
|
94
|
-
|
|
96
|
+
sayTagged(` [migrate] dropped legacy ${legacy} dependency`);
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
if (options.dryRun) {
|
|
98
100
|
if (verbose && !options.quiet)
|
|
99
|
-
|
|
101
|
+
sayTagged(` [dry-run] would add ${PACKAGE_NAME}@^${PACKAGE_VERSION} to ${pkgPath}`);
|
|
100
102
|
if (verbose && !options.quiet)
|
|
101
|
-
|
|
103
|
+
sayTagged(` [dry-run] would run: npm install --no-audit --no-fund (in ${pluginsDir})`);
|
|
102
104
|
return false;
|
|
103
105
|
}
|
|
104
106
|
// pkg.dependencies[PACKAGE_NAME] was assigned above, so only reinstall skips this fast path.
|
|
@@ -119,14 +121,14 @@ async function stepSelfPlugin(pluginsDir, options) {
|
|
|
119
121
|
await execNetwork('Installing Tersio plugin dependencies', 'bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
|
|
120
122
|
}
|
|
121
123
|
catch (e) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
sayTagged(` [fail] Could not install ${PACKAGE_NAME} into plugins dir: ${e.message}`);
|
|
125
|
+
sayTagged(` [hint] Manual: cd ~/.omp/plugins && npm install ${PACKAGE_NAME}@^${PACKAGE_VERSION} --save --no-audit --no-fund`);
|
|
124
126
|
return false;
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
const installedPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
|
|
128
130
|
if ((await readTextIfExists(installedPkg)) === null) {
|
|
129
|
-
|
|
131
|
+
sayTagged(` [warn] ${PACKAGE_NAME} not found in plugins/node_modules after install`);
|
|
130
132
|
return false;
|
|
131
133
|
}
|
|
132
134
|
debug('tersio listed in OMP plugins');
|
|
@@ -136,7 +138,7 @@ function resolveRtkTriple() {
|
|
|
136
138
|
const triple = rtkPlatformSpec()?.triple;
|
|
137
139
|
if (triple)
|
|
138
140
|
return triple;
|
|
139
|
-
|
|
141
|
+
sayTagged(` [fail] Unsupported platform: ${process.platform}/${process.arch}`);
|
|
140
142
|
console.log(' [hint] Manual: https://github.com/rtk-ai/rtk/releases');
|
|
141
143
|
return null;
|
|
142
144
|
}
|
|
@@ -145,8 +147,8 @@ function findRtkAsset(release, triple) {
|
|
|
145
147
|
const asset = assets.find((a) => a.name === `rtk-${triple}.zip` || a.name === `rtk-${triple}.tar.gz`);
|
|
146
148
|
if (asset)
|
|
147
149
|
return asset;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
sayTagged(` [fail] No rtk-${triple}.<zip|tar.gz> in release ${release.tag_name}`);
|
|
151
|
+
sayTagged(` [hint] Available: ${assets.map((a) => a.name).filter((n) => n.startsWith('rtk-')).join(', ')}`);
|
|
150
152
|
return null;
|
|
151
153
|
}
|
|
152
154
|
async function downloadRtkChecksums(release) {
|
|
@@ -171,16 +173,16 @@ async function verifyRtkArchive(archivePath, assetName, checksumsText, options =
|
|
|
171
173
|
const actual = await sha256File(archivePath);
|
|
172
174
|
if (!expected) {
|
|
173
175
|
if (!options.quiet)
|
|
174
|
-
|
|
176
|
+
sayTagged(` [warn] checksums.txt missing entry for ${assetName} — skipping verification`);
|
|
175
177
|
return true;
|
|
176
178
|
}
|
|
177
179
|
if (actual === expected) {
|
|
178
180
|
debug(`Checksum verified for ${assetName}`);
|
|
179
181
|
return true;
|
|
180
182
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
sayTagged(` [fail] Checksum mismatch for ${assetName}`);
|
|
184
|
+
sayTagged(` [fail] Expected: ${expected}`);
|
|
185
|
+
sayTagged(` [fail] Got: ${actual}`);
|
|
184
186
|
// Caller finally removes the temp dir.
|
|
185
187
|
return false;
|
|
186
188
|
}
|
|
@@ -214,13 +216,13 @@ async function extractRtkArchive(archivePath, extractDir) {
|
|
|
214
216
|
}
|
|
215
217
|
catch (e) {
|
|
216
218
|
debug(`tar xzf failed: ${shortError(e)}`);
|
|
217
|
-
|
|
219
|
+
sayTagged(` [fail] tar could not extract ${path.basename(archivePath)}`);
|
|
218
220
|
console.log(' [hint] Manual: https://github.com/rtk-ai/rtk/releases');
|
|
219
221
|
return false;
|
|
220
222
|
}
|
|
221
223
|
return true;
|
|
222
224
|
}
|
|
223
|
-
|
|
225
|
+
sayTagged(` [fail] Unknown archive format: ${archivePath}`);
|
|
224
226
|
return false;
|
|
225
227
|
}
|
|
226
228
|
// The binary is machine-wide and only OMP needs the wiring, so an install that
|
|
@@ -253,59 +255,69 @@ async function stepRtk(binDir, options, target = 'omp') {
|
|
|
253
255
|
// OMP hook just as well. Dry runs stay offline.
|
|
254
256
|
if (options.dryRun) {
|
|
255
257
|
if (verbose && !options.quiet)
|
|
256
|
-
|
|
258
|
+
sayTagged(` [dry-run] would download rtk binary and install to ${binDest}`);
|
|
257
259
|
await bind(found ?? binDest);
|
|
258
260
|
return;
|
|
259
261
|
}
|
|
260
262
|
try {
|
|
261
|
-
const release = await withInteractiveSpinner('Finding latest RTK release', () => fetchJson(RTK_RELEASE_API));
|
|
262
263
|
const triple = resolveRtkTriple();
|
|
263
264
|
if (!triple)
|
|
264
265
|
return;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const [downloadedChecksums] = await Promise.all([
|
|
273
|
-
downloadRtkChecksums(release),
|
|
274
|
-
httpsDownload(asset.browser_download_url, archivePath),
|
|
275
|
-
]);
|
|
276
|
-
update('Verifying RTK checksum');
|
|
277
|
-
return downloadedChecksums;
|
|
278
|
-
});
|
|
279
|
-
if (!await verifyRtkArchive(archivePath, asset.name, checksumsText, options))
|
|
280
|
-
return;
|
|
281
|
-
const extractDir = path.join(tmpDir, 'extracted');
|
|
282
|
-
if (!await extractRtkArchive(archivePath, extractDir))
|
|
283
|
-
return;
|
|
284
|
-
const found = await findFile(extractDir, RTK_BINARY_NAME);
|
|
285
|
-
if (!found) {
|
|
286
|
-
console.log(` [fail] Could not find ${RTK_BINARY_NAME} in extracted archive`);
|
|
266
|
+
// One task with streamed sub-steps: release lookup, download, checksum,
|
|
267
|
+
// extract, install. Non-TTY keeps the plain lines each step already prints.
|
|
268
|
+
await withInteractiveTask('Installing RTK', async (update) => {
|
|
269
|
+
const release = await fetchJson(RTK_RELEASE_API);
|
|
270
|
+
const asset = findRtkAsset(release, triple);
|
|
271
|
+
if (!asset) {
|
|
272
|
+
sayTagged(` [fail] No rtk-${triple}.<zip|tar.gz> in release ${release.tag_name}`);
|
|
287
273
|
return;
|
|
288
274
|
}
|
|
289
|
-
await fs.
|
|
290
|
-
await fs.copyFile(binDest, `${binDest}.bak`).catch(() => { });
|
|
291
|
-
await fs.copyFile(found, binDest);
|
|
292
|
-
if (!IS_WINDOWS) {
|
|
293
|
-
await fs.chmod(binDest, 0o755);
|
|
294
|
-
debug(`chmod 755 ${binDest}`);
|
|
295
|
-
}
|
|
275
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'omp-rtk-'));
|
|
296
276
|
try {
|
|
297
|
-
|
|
277
|
+
const archivePath = path.join(tmpDir, asset.name);
|
|
278
|
+
update(`Downloading ${asset.name}`);
|
|
279
|
+
const [checksumsText] = await Promise.all([
|
|
280
|
+
downloadRtkChecksums(release),
|
|
281
|
+
httpsDownload(asset.browser_download_url, archivePath),
|
|
282
|
+
]);
|
|
283
|
+
update('Verifying checksum');
|
|
284
|
+
if (!await verifyRtkArchive(archivePath, asset.name, checksumsText, options)) {
|
|
285
|
+
sayTagged(' [hint] RTK install skipped for this run');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
update('Extracting');
|
|
289
|
+
const extractDir = path.join(tmpDir, 'extracted');
|
|
290
|
+
if (!await extractRtkArchive(archivePath, extractDir)) {
|
|
291
|
+
sayTagged(` [fail] Could not extract ${asset.name}`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
update('Installing binary');
|
|
295
|
+
const found = await findFile(extractDir, RTK_BINARY_NAME);
|
|
296
|
+
if (!found) {
|
|
297
|
+
sayTagged(` [fail] Could not find ${RTK_BINARY_NAME} in extracted archive`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
await fs.mkdir(path.dirname(binDest), { recursive: true });
|
|
301
|
+
await fs.copyFile(binDest, `${binDest}.bak`).catch(() => { });
|
|
302
|
+
await fs.copyFile(found, binDest);
|
|
303
|
+
if (!IS_WINDOWS) {
|
|
304
|
+
await fs.chmod(binDest, 0o755);
|
|
305
|
+
debug(`chmod 755 ${binDest}`);
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
await execP(binDest, ['--version'], { timeout: 30000, shell: false });
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
sayTagged(` [hint] Verify manually: ${binDest} --version`);
|
|
312
|
+
}
|
|
298
313
|
}
|
|
299
|
-
|
|
300
|
-
|
|
314
|
+
finally {
|
|
315
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
301
316
|
}
|
|
302
|
-
}
|
|
303
|
-
finally {
|
|
304
|
-
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
305
|
-
}
|
|
317
|
+
});
|
|
306
318
|
}
|
|
307
319
|
catch (e) {
|
|
308
|
-
|
|
320
|
+
sayTagged(` [fail] RTK: ${e.message}`);
|
|
309
321
|
console.log(' [hint] Manual: https://github.com/rtk-ai/rtk/releases');
|
|
310
322
|
}
|
|
311
323
|
// Wire rtk's tool_call extension: the binary alone never meters a session.
|
|
@@ -317,7 +329,7 @@ async function copySources(extDir, files, skipLabel, options) {
|
|
|
317
329
|
const src = await readTextIfExists(files[0][0]);
|
|
318
330
|
if (!src) {
|
|
319
331
|
if (!options.quiet && options.dryRun)
|
|
320
|
-
|
|
332
|
+
sayTagged(` [skip] ${skipLabel} not found in repo`);
|
|
321
333
|
return false;
|
|
322
334
|
}
|
|
323
335
|
await writeIfChanged(path.join(extDir, files[0][1]), src, options);
|
|
@@ -355,7 +367,7 @@ async function fetchCavemanRule(options) {
|
|
|
355
367
|
debug(`Using bundled Caveman rule: ${e.message}`);
|
|
356
368
|
return bundled;
|
|
357
369
|
}
|
|
358
|
-
|
|
370
|
+
sayTagged(` [warn] Could not fetch caveman rule: ${e.message}`);
|
|
359
371
|
return null;
|
|
360
372
|
}
|
|
361
373
|
}
|
|
@@ -371,7 +383,7 @@ async function stepCaveman(extDir, rule, options) {
|
|
|
371
383
|
}
|
|
372
384
|
else if (rule === null) {
|
|
373
385
|
console.log(' [skip] Caveman rule.md unavailable');
|
|
374
|
-
|
|
386
|
+
sayTagged(` [hint] Manual: ${CAVEMAN_REMOTE_RULE}`);
|
|
375
387
|
}
|
|
376
388
|
else {
|
|
377
389
|
await writeIfChanged(ruleDest, rule, options);
|
|
@@ -450,6 +462,7 @@ async function runCommandMenu() {
|
|
|
450
462
|
{ value: 'install', label: 'Install add-ons', hint: 'user scope + combo defaults' },
|
|
451
463
|
{ value: 'update', label: 'Update', hint: 'CLI version check, then refresh add-ons (RTK, Caveman rule, Ponytail)' },
|
|
452
464
|
{ value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
|
|
465
|
+
{ value: 'settings', label: 'Settings', hint: 'default combo mode, currency, diagnosis schedule' },
|
|
453
466
|
{ value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
|
|
454
467
|
{ value: 'dashboard', label: 'Dashboard', hint: 'open the report in your browser' },
|
|
455
468
|
{ value: 'reset', label: 'Reset statistics', hint: 'clear statistics; transcripts and RTK history stay' },
|
|
@@ -503,6 +516,10 @@ async function runCommandMenu() {
|
|
|
503
516
|
closeRL();
|
|
504
517
|
break;
|
|
505
518
|
}
|
|
519
|
+
case 'settings':
|
|
520
|
+
await runSettings();
|
|
521
|
+
closeRL();
|
|
522
|
+
break;
|
|
506
523
|
case 'usage':
|
|
507
524
|
await runUsage();
|
|
508
525
|
closeRL();
|
|
@@ -524,13 +541,24 @@ async function runCommandMenu() {
|
|
|
524
541
|
// Which host this run targets. --host pins it; a TTY user picks; scripts,
|
|
525
542
|
// pipes, --yes, and --dry-run keep the pre-multi-host default of Oh My Pi.
|
|
526
543
|
let targetHost = 'omp';
|
|
544
|
+
// Hosts the menu advertises but cannot install yet. Listed disabled so the
|
|
545
|
+
// roadmap is visible without offering a target that does nothing.
|
|
546
|
+
const SOON_HOSTS = [
|
|
547
|
+
{ value: 'claude', label: 'Claude Code', hint: 'Coming soon' },
|
|
548
|
+
{ value: 'opencode', label: 'OpenCode', hint: 'Coming soon' },
|
|
549
|
+
{ value: 'codex', label: 'Codex', hint: 'Coming soon' },
|
|
550
|
+
];
|
|
527
551
|
async function chooseHost(title, only) {
|
|
528
552
|
const hosts = detectHosts().filter((host) => !only || only.includes(host.id));
|
|
529
|
-
const
|
|
553
|
+
const options = hosts.map((host) => ({
|
|
530
554
|
value: host.id,
|
|
531
555
|
label: hostLabel(host),
|
|
532
556
|
hint: hostHint(host),
|
|
533
|
-
}))
|
|
557
|
+
}));
|
|
558
|
+
// A scoped menu (uninstall --host) lists only real hosts.
|
|
559
|
+
if (!only)
|
|
560
|
+
options.push(...SOON_HOSTS.map((h) => ({ ...h, disabled: true })));
|
|
561
|
+
const choice = await askInteractiveChoice(title, options, targetHost);
|
|
534
562
|
if (choice.status !== 'selected') {
|
|
535
563
|
closeRL();
|
|
536
564
|
process.exit(130);
|
|
@@ -542,12 +570,12 @@ function hostEntry(id) {
|
|
|
542
570
|
}
|
|
543
571
|
// pi auto-discovers `<agent-dir>/extensions/**/index.ts`, so this route writes
|
|
544
572
|
// the same tree as OMP's and registers nothing.
|
|
545
|
-
async function stepPiLayer(
|
|
573
|
+
async function stepPiLayer(options) {
|
|
546
574
|
const agentDir = piAgentDir();
|
|
547
575
|
const extDir = path.join(agentDir, 'extensions');
|
|
548
576
|
const declared = piTersioSource(agentDir);
|
|
549
577
|
if (declared)
|
|
550
|
-
|
|
578
|
+
sayTagged(` [note] pi also has a tersio package (${declared}) — remove one, or both copies load: pi remove npm:${PACKAGE_NAME}`);
|
|
551
579
|
if (!options.quiet)
|
|
552
580
|
console.log(` Pi — write the extension tree (${extDir})`);
|
|
553
581
|
const cavemanRule = await fetchCavemanRule(options);
|
|
@@ -570,7 +598,7 @@ async function stepPonytailForPi(agentDir, options) {
|
|
|
570
598
|
return;
|
|
571
599
|
}
|
|
572
600
|
catch (e) {
|
|
573
|
-
|
|
601
|
+
sayTagged(` [fail] pi install ponytail: ${shortError(e)}`);
|
|
574
602
|
}
|
|
575
603
|
}
|
|
576
604
|
const skills = findHoistedPackage('@dietrichgebert/ponytail', path.dirname(fileURLToPath(import.meta.url)), 'skills');
|
|
@@ -582,7 +610,7 @@ async function stepPonytailForPi(agentDir, options) {
|
|
|
582
610
|
const dest = path.join(agentDir, 'skills');
|
|
583
611
|
if (options.dryRun) {
|
|
584
612
|
if (verbose && !options.quiet)
|
|
585
|
-
|
|
613
|
+
sayTagged(` [dry-run] would copy Ponytail skills to ${dest}`);
|
|
586
614
|
return;
|
|
587
615
|
}
|
|
588
616
|
for (const entry of await fs.readdir(skills, { withFileTypes: true })) {
|
|
@@ -592,7 +620,7 @@ async function stepPonytailForPi(agentDir, options) {
|
|
|
592
620
|
await fs.rm(to, { recursive: true, force: true });
|
|
593
621
|
await fs.cp(path.join(skills, entry.name), to, { recursive: true });
|
|
594
622
|
if (!options.quiet)
|
|
595
|
-
|
|
623
|
+
sayTagged(` [write] skill ${entry.name} → ${to}`);
|
|
596
624
|
}
|
|
597
625
|
}
|
|
598
626
|
async function runInstall() {
|
|
@@ -616,7 +644,6 @@ async function runInstall() {
|
|
|
616
644
|
else if (tty() && !yes && !dryRun && !applyUpdate && command !== 'uninstall') {
|
|
617
645
|
targetHost = await chooseHost('Install Tersio into which agent?');
|
|
618
646
|
}
|
|
619
|
-
const host = hostEntry(targetHost);
|
|
620
647
|
// apply-update stays silent: the parent printed the plan.
|
|
621
648
|
const quiet = applyUpdate;
|
|
622
649
|
if (!quiet)
|
|
@@ -656,6 +683,21 @@ async function runInstall() {
|
|
|
656
683
|
const userExtDir = path.join(userDir, 'extensions');
|
|
657
684
|
// apply-update refreshes every add-on like a reinstall.
|
|
658
685
|
const installOptions = { dryRun, verbose, yes, reinstall: applyUpdate, quiet };
|
|
686
|
+
// Point of no return: disk writes start here; pipes stay unattended.
|
|
687
|
+
if (!yes && !dryRun && !applyUpdate && tty()) {
|
|
688
|
+
const target = hostEntry(targetHost);
|
|
689
|
+
const where = targetHost === 'pi' ? path.join(piAgentDir(), 'extensions') : path.join(OMP_AGENT_DIR, 'extensions');
|
|
690
|
+
console.log(`\nWill install into ${target.label}:`);
|
|
691
|
+
console.log(` ${where}`);
|
|
692
|
+
console.log(' caveman · rtk · ponytail session modes');
|
|
693
|
+
console.log(` session defaults → ${tersioSettingsFile()}`);
|
|
694
|
+
const go = await askInteractiveConfirm(`Install Tersio into ${target.label}?`);
|
|
695
|
+
if (go.status !== 'confirmed' || !go.value) {
|
|
696
|
+
closeRL();
|
|
697
|
+
console.log('\nAborted. Nothing was changed.');
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
659
701
|
try {
|
|
660
702
|
const v = (await execP(OMP_BIN, ['--version'])).stdout.trim();
|
|
661
703
|
if (verbose && !quiet)
|
|
@@ -672,11 +714,11 @@ async function runInstall() {
|
|
|
672
714
|
}
|
|
673
715
|
catch (e) {
|
|
674
716
|
failures.push(label);
|
|
675
|
-
|
|
717
|
+
sayTagged(` [fail] ${label}: ${shortError(e)}`);
|
|
676
718
|
}
|
|
677
719
|
};
|
|
678
720
|
if (targetHost === 'pi') {
|
|
679
|
-
await capture('pi tree', () => stepPiLayer(
|
|
721
|
+
await capture('pi tree', () => stepPiLayer(installOptions));
|
|
680
722
|
await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions, 'pi'));
|
|
681
723
|
await capture('settings', () => writePluginSettings(profile, installOptions));
|
|
682
724
|
if (failures.length > 0)
|
package/cli/interactive.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cli/interactive.ts — TTY layer: readline, Clack spinners, selects, task phases.
|
|
2
2
|
import readline from 'node:readline';
|
|
3
|
-
import { cancel as clackCancel, confirm as clackConfirm, select as clackSelect, spinner as clackSpinner, tasks as clackTasks } from '@clack/prompts';
|
|
3
|
+
import { cancel as clackCancel, confirm as clackConfirm, log as clackLog, select as clackSelect, spinner as clackSpinner, tasks as clackTasks } from '@clack/prompts';
|
|
4
4
|
import { execP } from './common.js';
|
|
5
5
|
const RL = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6
6
|
let rlOpen = true;
|
|
@@ -103,7 +103,41 @@ async function runInteractivePhase(title, collect) {
|
|
|
103
103
|
await clackTasks([{ title, task: async () => { result = await collect(); } }]);
|
|
104
104
|
return result;
|
|
105
105
|
}
|
|
106
|
+
const TAG_KIND = {
|
|
107
|
+
fail: 'error', warn: 'warn', hint: 'info', ok: 'success', note: 'message',
|
|
108
|
+
write: 'step', rm: 'step', migrate: 'step', skip: 'info', 'dry-run': 'info',
|
|
109
|
+
};
|
|
110
|
+
function sayTagged(line) {
|
|
111
|
+
const m = /^\s*\[([\w-]+)\]\s?([\s\S]*)$/.exec(line);
|
|
112
|
+
const kind = (m && TAG_KIND[m[1]]) || 'info';
|
|
113
|
+
if (tty() && m)
|
|
114
|
+
clackLog[kind](m[2]);
|
|
115
|
+
else
|
|
116
|
+
console.log(line);
|
|
117
|
+
}
|
|
118
|
+
// One live-updating task line instead of a spinner per step.
|
|
119
|
+
async function withInteractiveTask(title, work) {
|
|
120
|
+
if (!tty())
|
|
121
|
+
return work(() => { });
|
|
122
|
+
closeRL();
|
|
123
|
+
let result;
|
|
124
|
+
const spin = clackSpinner();
|
|
125
|
+
spin.start(title);
|
|
126
|
+
try {
|
|
127
|
+
result = await work((message) => spin.message(message));
|
|
128
|
+
spin.stop('done');
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
spin.error(shortSpinnerError(e));
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
function shortSpinnerError(e) {
|
|
137
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
138
|
+
return (msg.split('\n')[0] || 'failed').slice(0, 120);
|
|
139
|
+
}
|
|
106
140
|
async function execNetwork(label, cmd, args, opts = {}) {
|
|
107
141
|
return withInteractiveSpinner(label, () => execP(cmd, args, opts));
|
|
108
142
|
}
|
|
109
|
-
export { ask, closeRL, tty, withInteractiveSpinner, execNetwork, askInteractiveChoice, askInteractiveConfirm, confirmDestructive, runInteractivePhase, };
|
|
143
|
+
export { ask, closeRL, tty, withInteractiveSpinner, withInteractiveTask, execNetwork, sayTagged, askInteractiveChoice, askInteractiveConfirm, confirmDestructive, runInteractivePhase, };
|
package/cli/layer.js
CHANGED
|
@@ -8,9 +8,7 @@ export function layerEntries(extDir, table) {
|
|
|
8
8
|
export function layerDirs(table) {
|
|
9
9
|
return table.map(([dir]) => dir);
|
|
10
10
|
}
|
|
11
|
-
// A port source sits two levels below extensions
|
|
12
|
-
// below <agent-dir>/extensions/. Rewriting on the way in keeps one spelling in
|
|
13
|
-
// the repo and the same other spelling in every installed tree.
|
|
11
|
+
// A port source sits two levels below extensions/, one level once installed.
|
|
14
12
|
export function rewritePortImports(content) {
|
|
15
13
|
return content.replace(/from '\.\.\/\.\.\/(shared|lib)\//g, "from '../$1/");
|
|
16
14
|
}
|
package/cli/manifest.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// extension tree. The installer, `doctor --fix extensions`, the doctor's
|
|
3
|
-
// per-host tree row, and the loadability test all read it, so none of them
|
|
4
|
-
// can drift from what actually ships.
|
|
1
|
+
// Single list of install writes, shared by installer, doctor, and tests.
|
|
5
2
|
import path from 'node:path';
|
|
6
3
|
import { fileURLToPath } from 'node:url';
|
|
7
4
|
export const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
|
package/cli/profile.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// cli/profile.ts — session-start defaults
|
|
2
|
-
//
|
|
3
|
-
// Stored in ~/.tersio/settings.json, which every host reads. Extracted from
|
|
4
|
-
// cli/install.ts so both install and settings share it.
|
|
1
|
+
// cli/profile.ts — session-start defaults and the display-currency default,
|
|
2
|
+
// stored in ~/.tersio/settings.json. Shared by install and settings.
|
|
5
3
|
import { existsSync, promises as fs } from 'node:fs';
|
|
6
4
|
import path from 'node:path';
|
|
7
5
|
import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, PACKAGE_NAME, PONYTAIL_DEFAULTS, verbose, } from './common.js';
|
|
@@ -9,6 +7,7 @@ import os from 'node:os';
|
|
|
9
7
|
import { DEFAULT_CURRENCY, isCurrencyCode } from './currency.js';
|
|
10
8
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
11
9
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
10
|
+
export const BACKUP_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
12
11
|
function defaultProfile() {
|
|
13
12
|
return {
|
|
14
13
|
comboDefault: 'off',
|
|
@@ -16,11 +15,10 @@ function defaultProfile() {
|
|
|
16
15
|
rtkDefault: false,
|
|
17
16
|
ponytailDefault: 'off',
|
|
18
17
|
currency: DEFAULT_CURRENCY,
|
|
18
|
+
backupSchedule: 'monthly',
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
-
//
|
|
22
|
-
// kept the same values as OMP plugin settings, so seed from there once and
|
|
23
|
-
// never lose a user's choice.
|
|
21
|
+
// Seed once from OMP plugin settings so a pre-~/.tersio install keeps its values.
|
|
24
22
|
async function storedProfile() {
|
|
25
23
|
const base = defaultProfile();
|
|
26
24
|
const stored = (await readTextIfExists(tersioSettingsFile())) !== null
|
|
@@ -39,6 +37,9 @@ async function storedProfile() {
|
|
|
39
37
|
if (typeof stored.currency === 'string' && isCurrencyCode(stored.currency.trim().toUpperCase())) {
|
|
40
38
|
base.currency = stored.currency.trim().toUpperCase();
|
|
41
39
|
}
|
|
40
|
+
if (typeof stored.backupSchedule === 'string' && BACKUP_SCHEDULES.has(stored.backupSchedule)) {
|
|
41
|
+
base.backupSchedule = stored.backupSchedule;
|
|
42
|
+
}
|
|
42
43
|
return base;
|
|
43
44
|
}
|
|
44
45
|
function legacyOmpLockPath() {
|
|
@@ -75,6 +76,7 @@ async function writePluginSettings(profile, options) {
|
|
|
75
76
|
rtkDefault: profile.rtkDefault,
|
|
76
77
|
ponytailDefault: profile.ponytailDefault,
|
|
77
78
|
currency: profile.currency,
|
|
79
|
+
backupSchedule: profile.backupSchedule,
|
|
78
80
|
};
|
|
79
81
|
if (options.dryRun) {
|
|
80
82
|
if (verbose && !options.quiet)
|
package/cli/reset.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
// watermark that filters session-derived and RTK-metered rows out of every
|
|
3
|
-
// derived view. Session transcripts (~/.omp/agent/sessions) and the RTK
|
|
4
|
-
// database are host/tool-owned and are never touched; the watermark only
|
|
5
|
-
// changes what tersio shows.
|
|
1
|
+
// Clears tersio-owned stats only; transcripts and RTK data stay.
|
|
6
2
|
import { dryRun, yes } from './common.js';
|
|
7
|
-
import { confirmDestructive } from './interactive.js';
|
|
3
|
+
import { confirmDestructive, sayTagged } from './interactive.js';
|
|
8
4
|
import { clearUsageLedger, importSessionTokens, ledgerPath, markReset, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
9
5
|
import { clearUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
10
6
|
import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
@@ -29,7 +25,7 @@ async function runReset() {
|
|
|
29
25
|
const cleared = clearUsageLedger();
|
|
30
26
|
const dbCleared = clearUsageDb();
|
|
31
27
|
const ts = markReset();
|
|
32
|
-
|
|
28
|
+
sayTagged(`[ok] reset — removed ${cleared} usage rows${dbCleared ? ' + usage.db' : ''}; statistics view starts at ${new Date(ts).toISOString()}`);
|
|
33
29
|
return true;
|
|
34
30
|
}
|
|
35
31
|
export { runReset };
|
package/cli/rtk-wiring.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// `rtk init -g --agent omp` writes rtk's tool_call extension to
|
|
3
|
-
// ~/.omp/agent/extensions/rtk.ts but does not register it, and OMP loads only
|
|
4
|
-
// listed extensions, so we add the path to config.yml. Every rewritten bash
|
|
5
|
-
// call then lands in rtk's history.db. Fail-open: a failed wire only downgrades
|
|
6
|
-
// to manual `rtk` prefixing. No cli/common.ts import (argv side effects).
|
|
1
|
+
// OMP loads only listed extensions, so register rtk's path; fail-open.
|
|
7
2
|
import { promises as fs } from 'node:fs';
|
|
8
3
|
import os from 'node:os';
|
|
9
4
|
import path from 'node:path';
|
package/cli/settings.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
// cli/settings.ts — view/edit session-start defaults
|
|
2
|
-
// plus the display-currency default for usage/dashboard reports.
|
|
3
|
-
// Non-interactive: `tersio settings --combo-default balanced` (plus per-mode
|
|
4
|
-
// override flags and --currency). Interactive: clack selects seeded from the
|
|
5
|
-
// stored profile.
|
|
1
|
+
// cli/settings.ts — view/edit session-start defaults and the display currency.
|
|
6
2
|
import path from 'node:path';
|
|
7
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';
|
|
8
4
|
import { CURRENCY_CODES } from './currency.js';
|
|
@@ -44,6 +40,7 @@ async function askProfile(current) {
|
|
|
44
40
|
rtkDefault: preset.rtk,
|
|
45
41
|
ponytailDefault: preset.ponytail,
|
|
46
42
|
currency: current.currency,
|
|
43
|
+
backupSchedule: current.backupSchedule,
|
|
47
44
|
};
|
|
48
45
|
const caveman = await askInteractiveChoice('Caveman default', [...CAVEMAN_DEFAULTS].map((v) => ({
|
|
49
46
|
value: v, label: v,
|
|
@@ -162,9 +159,7 @@ async function runSingleSetting(name, current, nextDiag) {
|
|
|
162
159
|
}
|
|
163
160
|
function printSettingsTable(current) {
|
|
164
161
|
console.log('\n=== Tersio Settings ===');
|
|
165
|
-
//
|
|
166
|
-
// persisting it (its picker POSTs to /currency); settings can still set
|
|
167
|
-
// the stored default via --currency or the prompt below.
|
|
162
|
+
// The dashboard owns displaying currency; settings only sets the default.
|
|
168
163
|
const rows = [
|
|
169
164
|
['combo', current.comboDefault, [...COMBO_DEFAULTS].join(' | ')],
|
|
170
165
|
['caveman', current.cavemanDefault, [...CAVEMAN_DEFAULTS].join(' | ')],
|