@krtclcdy/tersio 2.25.4 → 2.25.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -30
- package/dashboard/dist/index.html +18 -16
- package/dist/cli/backups.js +194 -0
- package/dist/cli/common.js +2 -2
- package/dist/cli/dashboard.js +125 -98
- package/dist/cli/doctor-fix.js +41 -44
- package/dist/cli/doctor.js +69 -7
- package/dist/cli/hosts.js +4 -4
- package/dist/cli/install.js +71 -32
- package/dist/cli/manifest.js +45 -17
- package/dist/cli/profile.js +1 -1
- package/dist/cli/uninstall.js +21 -5
- package/dist/cli/update.js +3 -3
- package/dist/cli/usage.js +75 -4
- package/dist/extensions/{combo-toggle → combo}/index.js +1 -1
- package/dist/extensions/{tersio-commands → commands}/index.js +1 -1
- package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -1
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +1 -1
- package/dist/extensions/{opencode/server.js → server.js} +8 -8
- package/dist/extensions/shared/agent-stats.js +1262 -0
- package/dist/extensions/shared/brand-marks.js +154 -0
- package/dist/extensions/shared/plugin-settings.js +1 -1
- package/dist/extensions/shared/pricing.js +91 -27
- package/dist/extensions/shared/usage-ledger.js +79 -17
- package/dist/extensions/shared/usage-store.js +106 -22
- package/dist/extensions/{lib → shared}/utils.js +1 -1
- package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -3
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +2 -2
- package/extensions/{combo-toggle → combo}/index.ts +1 -1
- package/extensions/{tersio-commands → commands}/index.ts +1 -1
- package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -1
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +1 -1
- package/extensions/{opencode/server.ts → server.ts} +9 -9
- package/extensions/shared/agent-stats.ts +1657 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/plugin-settings.ts +1 -1
- package/extensions/shared/pricing.ts +107 -28
- package/extensions/shared/usage-ledger.ts +94 -19
- package/extensions/shared/usage-store.ts +106 -22
- package/extensions/{lib → shared}/utils.ts +1 -1
- package/extensions/{ai-addons-updater → updater}/index.ts +15 -4
- package/package.json +13 -13
- /package/dist/extensions/{caveman-session → caveman}/index.js +0 -0
- /package/dist/extensions/{caveman-session → caveman}/rule-megacave.md +0 -0
- /package/dist/extensions/{caveman-session → caveman}/rule-ultra.md +0 -0
- /package/dist/extensions/{caveman-session → caveman}/rule.md +0 -0
- /package/extensions/{caveman-session → caveman}/index.ts +0 -0
- /package/extensions/{caveman-session → caveman}/rule-megacave.md +0 -0
- /package/extensions/{caveman-session → caveman}/rule-ultra.md +0 -0
- /package/extensions/{caveman-session → caveman}/rule.md +0 -0
package/dist/cli/doctor.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
// cli/doctor.ts — installation health checks.
|
|
2
|
-
import { existsSync, promises as fs } from 'node:fs';
|
|
2
|
+
import { existsSync, statSync, promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
|
|
5
5
|
import { detectHosts, hostExtensionsDir, ompPackageDir } from './hosts.js';
|
|
6
6
|
import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
|
|
7
7
|
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
8
8
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
9
|
-
import { readTextIfExists, resolveRtkBinary, ruleBodyProblem } from '../extensions/
|
|
9
|
+
import { readTextIfExists, resolveRtkBinary, ruleBodyProblem } from '../extensions/shared/utils.js';
|
|
10
|
+
import { sqliteScript, statsDbPath } from '../extensions/shared/agent-stats.js';
|
|
10
11
|
import { TREE_FILES } from './manifest.js';
|
|
12
|
+
/** Bytes at the scale a transcript gap reads in: `5.6M`, not `5571992`. */
|
|
13
|
+
function shortBytes(bytes) {
|
|
14
|
+
if (bytes >= 1e9)
|
|
15
|
+
return `${(bytes / 1e9).toFixed(1)}G`;
|
|
16
|
+
if (bytes >= 1e6)
|
|
17
|
+
return `${(bytes / 1e6).toFixed(1)}M`;
|
|
18
|
+
if (bytes >= 1e3)
|
|
19
|
+
return `${Math.round(bytes / 1e3)}K`;
|
|
20
|
+
return `${bytes}B`;
|
|
21
|
+
}
|
|
11
22
|
async function runDoctor(recheck = false) {
|
|
12
23
|
console.log('\n=== Tersio Doctor ===');
|
|
13
24
|
// OMP loads the plugin from its plugins dir; pi loads the same package as a pi package, which its own settings.json declares and its npm dir holds.
|
|
@@ -15,11 +26,12 @@ async function runDoctor(recheck = false) {
|
|
|
15
26
|
const rtkBin = resolveRtkBinary();
|
|
16
27
|
// Ponytail
|
|
17
28
|
const ponytailPkg = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
29
|
+
// One source for the installed plugin path, so the doctor follows a package rename.
|
|
30
|
+
const tersioPluginDir = ompPackageDir();
|
|
31
|
+
const cavemanIndex = path.join(tersioPluginDir, 'extensions', 'caveman', 'index.ts');
|
|
32
|
+
const cavemanRule = (name) => path.join(tersioPluginDir, 'extensions', 'caveman', name);
|
|
33
|
+
const rtkIndex = path.join(tersioPluginDir, 'extensions', 'rtk', 'session.ts');
|
|
34
|
+
const updaterIndex = path.join(tersioPluginDir, 'extensions', 'updater', 'index.ts');
|
|
23
35
|
// Independent probes start concurrently; sections report in fixed order as their data settles. Every probe resolves instead of rejecting.
|
|
24
36
|
const hosts = detectHosts();
|
|
25
37
|
const probes = {
|
|
@@ -131,6 +143,11 @@ async function runDoctor(recheck = false) {
|
|
|
131
143
|
console.log(` Usage DB (tersio-owned · local hosted): ${usageDbPath()}`);
|
|
132
144
|
const pricesAge = pricesMtime ? `(pulled ${relTime(Date.now() - pricesMtime.mtimeMs)} · ${absDate(pricesMtime.mtimeMs)})` : '';
|
|
133
145
|
check('Prices feed', pricesMtime !== null, pricesAge || pricesCachePath());
|
|
146
|
+
// The pages read omp's own index, which only advances while an omp process owns it. A stalled
|
|
147
|
+
// index reads as an empty dashboard, so name the gap instead of leaving a user to guess.
|
|
148
|
+
const indexLag = statsIndexLag();
|
|
149
|
+
if (indexLag)
|
|
150
|
+
warnLine('omp stats index', indexLag);
|
|
134
151
|
section('Add-ons');
|
|
135
152
|
const ruleAge = ruleMtime ? `(updated ${relTime(Date.now() - ruleMtime.mtimeMs)} · ${absDate(ruleMtime.mtimeMs)})` : '';
|
|
136
153
|
const cavemanRules = [['rule.md', cavemanRuleText], ['rule-ultra.md', cavemanUltraText], ['rule-megacave.md', cavemanMegacaveText]];
|
|
@@ -256,6 +273,51 @@ async function runDoctorFix(tally) {
|
|
|
256
273
|
else
|
|
257
274
|
console.log(` Doctor --fix: still ${again.missing} missing + ${again.warn} warn — rerun or repair manually.`);
|
|
258
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* How far omp's own transcript index falls behind the transcripts on disk, or null when nothing is
|
|
278
|
+
* behind or the index cannot be read. omp advances the offsets only while one of its processes owns
|
|
279
|
+
* the index, so a closed dashboard leaves the newest turns unread and the pages read as empty.
|
|
280
|
+
*/
|
|
281
|
+
function statsIndexLag() {
|
|
282
|
+
const statsDb = statsDbPath();
|
|
283
|
+
if (!existsSync(statsDb))
|
|
284
|
+
return null;
|
|
285
|
+
try {
|
|
286
|
+
// The same WAL-safe reader the aggregate uses, so this probe never creates side files beside a
|
|
287
|
+
// database omp has open, and never trips over them either.
|
|
288
|
+
const rows = sqliteScript(statsDb, 'SELECT session_file, offset FROM file_offsets;\n')
|
|
289
|
+
.split('\n')
|
|
290
|
+
.filter((line) => line.includes('\t'))
|
|
291
|
+
.map((line) => line.split('\t'));
|
|
292
|
+
if (rows.length === 0)
|
|
293
|
+
return null;
|
|
294
|
+
// The gap behind the freshest transcript is what a page shows as "no requests", so the newest
|
|
295
|
+
// file sets the number rather than the sum of every file's gap.
|
|
296
|
+
let worstBytes = 0;
|
|
297
|
+
let freshest = '';
|
|
298
|
+
for (const [file, offsetText] of rows) {
|
|
299
|
+
let size;
|
|
300
|
+
try {
|
|
301
|
+
size = statSync(file).size;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
continue; // a rotated or deleted transcript is not a lag
|
|
305
|
+
}
|
|
306
|
+
const behind = size - Number(offsetText);
|
|
307
|
+
if (behind > worstBytes) {
|
|
308
|
+
worstBytes = behind;
|
|
309
|
+
freshest = file;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (worstBytes < 4096)
|
|
313
|
+
return null;
|
|
314
|
+
const when = relTime(Date.now() - statSync(statsDb).mtimeMs);
|
|
315
|
+
return `${shortBytes(worstBytes)} of ${path.basename(freshest)} unread, last written ${when === 'just now' ? 'moments ago' : when} — run: omp stats`;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return null; // no sqlite3, or an unreadable index: nothing to report
|
|
319
|
+
}
|
|
320
|
+
}
|
|
259
321
|
function section(name) {
|
|
260
322
|
console.log(`\n${name}`);
|
|
261
323
|
}
|
package/dist/cli/hosts.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { HOME, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION } from './common.js';
|
|
5
|
-
import {
|
|
6
|
-
import { piAgentDir } from '../extensions/
|
|
5
|
+
import { treeFilesFor } from './manifest.js';
|
|
6
|
+
import { piAgentDir } from '../extensions/shared/utils.js';
|
|
7
7
|
export function hostExtensionsDir(id, agentDir = piAgentDir()) {
|
|
8
8
|
if (id === 'omp')
|
|
9
9
|
return path.join(HOME, '.omp', 'agent', 'extensions');
|
|
@@ -11,9 +11,9 @@ export function hostExtensionsDir(id, agentDir = piAgentDir()) {
|
|
|
11
11
|
return path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
|
|
12
12
|
return path.join(agentDir, 'extensions');
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
function missingTreeFiles(id, agentDir = piAgentDir()) {
|
|
15
15
|
const dir = hostExtensionsDir(id, agentDir);
|
|
16
|
-
return
|
|
16
|
+
return treeFilesFor(id).filter((file) => !existsSync(path.join(dir, file)));
|
|
17
17
|
}
|
|
18
18
|
export function ompPackageDir() {
|
|
19
19
|
return path.join(OMP_PLUGINS_DIR, 'node_modules', ...PACKAGE_NAME.split('/'));
|
package/dist/cli/install.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// cli/install.ts — install flow and all setup steps.
|
|
2
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { BUN_BIN_DIR, COMBO_PRESET_MODES, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
6
|
+
import { BUN_BIN_DIR, COMBO_PRESET_MODES, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, currency, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
7
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';
|
|
@@ -13,11 +13,11 @@ import { runReset } from './reset.js';
|
|
|
13
13
|
import { runUsage } from './usage.js';
|
|
14
14
|
import { runDashboard } from './dashboard.js';
|
|
15
15
|
import { wireRtkOmp, wireRtkOpencode, wireRtkPi } from './rtk-wiring.js';
|
|
16
|
-
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, findPackageRoot, httpsDownload, parseChecksum, piAgentDir, readTextIfExists, resolveRtkBinary, ruleBodyProblem, rtkPlatformSpec, sha256File, } from '../extensions/
|
|
16
|
+
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, findPackageRoot, httpsDownload, parseChecksum, piAgentDir, readTextIfExists, resolveRtkBinary, ruleBodyProblem, rtkPlatformSpec, sha256File, } from '../extensions/shared/utils.js';
|
|
17
17
|
import { formatCliStatus, storedProfile, storedProfileSync, writePluginSettings } from './profile.js';
|
|
18
18
|
import { runSettings } from './settings.js';
|
|
19
19
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
20
|
-
import {
|
|
20
|
+
import { filesUnder, rootFiles, sourcePath } from './manifest.js';
|
|
21
21
|
import { detectHosts, hostHint, hostLabel, parseHostArg, piTersioSource } from './hosts.js';
|
|
22
22
|
async function stepPonytail(pluginsDir, options) {
|
|
23
23
|
if (!options.quiet)
|
|
@@ -229,6 +229,38 @@ async function extractRtkArchive(archivePath, extractDir) {
|
|
|
229
229
|
sayTagged(` [fail] Unknown archive format: ${archivePath}`);
|
|
230
230
|
return false;
|
|
231
231
|
}
|
|
232
|
+
// The agent tree keeps only rtk's own wiring file. Every Tersio extension loads from the
|
|
233
|
+
// OMP plugin manifest, which omp merges with the extension dir it discovers by default, so a
|
|
234
|
+
// copy left in the agent tree loads a second time. `caveman-session` was never written here
|
|
235
|
+
// after the plugin registration shipped, so an old install holds a set that cannot resolve
|
|
236
|
+
// its own rule bodies.
|
|
237
|
+
const RETIRED_OMP_AGENT_DIRS = [
|
|
238
|
+
'caveman-session',
|
|
239
|
+
'rtk-session',
|
|
240
|
+
'rtk-filter',
|
|
241
|
+
'combo-toggle',
|
|
242
|
+
'tersio-commands',
|
|
243
|
+
'ai-addons-updater',
|
|
244
|
+
'shared',
|
|
245
|
+
'lib',
|
|
246
|
+
];
|
|
247
|
+
/** Drop the copies an older OMP install left in the agent tree, keeping rtk's `rtk.ts`. */
|
|
248
|
+
async function stepRetireAgentCopies(extDir, options) {
|
|
249
|
+
const stale = RETIRED_OMP_AGENT_DIRS.filter((dir) => existsSync(path.join(extDir, dir)));
|
|
250
|
+
if (stale.length === 0) {
|
|
251
|
+
if (!options.quiet)
|
|
252
|
+
console.log(' Retired agent copies — none');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (!options.quiet)
|
|
256
|
+
console.log(` Retired agent copies — remove ${stale.join(', ')}`);
|
|
257
|
+
if (options.dryRun) {
|
|
258
|
+
if (verbose)
|
|
259
|
+
sayTagged(` [dry-run] would remove ${stale.map((d) => path.join(extDir, d)).join(', ')}`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await Promise.all(stale.map((dir) => fs.rm(path.join(extDir, dir), { recursive: true, force: true })));
|
|
263
|
+
}
|
|
232
264
|
// The binary is machine-wide; hosts without a hook only rebind. `tersio update` still refreshes it.
|
|
233
265
|
async function stepRtk(binDir, options, target = 'omp') {
|
|
234
266
|
const binDest = path.join(binDir, RTK_BINARY_NAME);
|
|
@@ -348,19 +380,18 @@ async function copySources(extDir, files, skipLabel, options) {
|
|
|
348
380
|
async function stepSharedSessionState(extDir, options) {
|
|
349
381
|
if (!options.quiet)
|
|
350
382
|
console.log(' Shared files — sync session bridge');
|
|
351
|
-
|
|
352
|
-
await copySources(extDir, shared, 'shared/session-state.js', options);
|
|
383
|
+
await copySources(extDir, filesUnder('shared'), 'shared/session-state.js', options);
|
|
353
384
|
}
|
|
354
385
|
async function stepRtkSession(extDir, options) {
|
|
355
386
|
if (!options.quiet)
|
|
356
387
|
console.log(' RTK session — install session mode');
|
|
357
|
-
await copySources(extDir, filesUnder('rtk
|
|
388
|
+
await copySources(extDir, filesUnder('rtk'), 'rtk/session.ts', options);
|
|
358
389
|
}
|
|
359
390
|
// pi's built-in grep and glob never reach the bash hook, so this one filters their output.
|
|
360
391
|
async function stepRtkFilter(extDir, options) {
|
|
361
392
|
if (!options.quiet)
|
|
362
393
|
console.log(' RTK filter — install built-in tool output filtering');
|
|
363
|
-
await copySources(extDir, filesUnder('rtk
|
|
394
|
+
await copySources(extDir, filesUnder('rtk'), 'rtk/filter.ts', options);
|
|
364
395
|
}
|
|
365
396
|
// rule.md ships with the package; the other bodies track upstream skills.
|
|
366
397
|
// rule.md is a deliberate fork of upstream skills/caveman/SKILL.md; a blind sync would drop the mode switch line and the STE100 rules.
|
|
@@ -372,11 +403,11 @@ const CAVEMAN_RULES = [
|
|
|
372
403
|
async function stepCaveman(extDir, options) {
|
|
373
404
|
if (!options.quiet)
|
|
374
405
|
console.log(' Caveman — install session mode and rule bodies');
|
|
375
|
-
const cavemanDir = path.join(extDir, 'caveman
|
|
406
|
+
const cavemanDir = path.join(extDir, 'caveman');
|
|
376
407
|
if (!options.dryRun)
|
|
377
408
|
await fs.mkdir(cavemanDir, { recursive: true });
|
|
378
409
|
for (const [name, remote] of CAVEMAN_RULES) {
|
|
379
|
-
const bundled = await readTextIfExists(sourcePath(`caveman
|
|
410
|
+
const bundled = await readTextIfExists(sourcePath(`caveman/${name}`));
|
|
380
411
|
let rule = bundled;
|
|
381
412
|
if (remote && !options.dryRun) {
|
|
382
413
|
try {
|
|
@@ -405,7 +436,7 @@ async function stepCaveman(extDir, options) {
|
|
|
405
436
|
await writeIfChanged(path.join(cavemanDir, name), rule, options);
|
|
406
437
|
}
|
|
407
438
|
}
|
|
408
|
-
await copySources(extDir, filesUnder('caveman
|
|
439
|
+
await copySources(extDir, filesUnder('caveman').filter(([, to]) => to.endsWith('index.ts')), 'caveman/index.ts', options);
|
|
409
440
|
// Renamed body: drop the stale copy an older install left behind.
|
|
410
441
|
if (!options.dryRun)
|
|
411
442
|
await fs.rm(path.join(cavemanDir, 'rule-wenyan.md'), { force: true });
|
|
@@ -413,15 +444,15 @@ async function stepCaveman(extDir, options) {
|
|
|
413
444
|
async function stepTersioCommands(extDir, options) {
|
|
414
445
|
if (!options.quiet)
|
|
415
446
|
console.log(' Tersio commands — install /tersio root command');
|
|
416
|
-
await copySources(extDir, filesUnder('
|
|
447
|
+
await copySources(extDir, filesUnder('commands'), 'commands/index.ts', options);
|
|
417
448
|
}
|
|
418
449
|
async function stepUpdater(extDir, options) {
|
|
419
|
-
await copySources(extDir, filesUnder('
|
|
450
|
+
await copySources(extDir, filesUnder('updater'), 'updater/index.ts', options);
|
|
420
451
|
}
|
|
421
452
|
async function stepCombo(extDir, options) {
|
|
422
453
|
if (!options.quiet)
|
|
423
454
|
console.log(' Combo — install preset switch');
|
|
424
|
-
await copySources(extDir, filesUnder('combo
|
|
455
|
+
await copySources(extDir, filesUnder('combo'), 'combo/index.ts', options);
|
|
425
456
|
}
|
|
426
457
|
// The tree ships no node_modules, so the full ruleset rides along as files.
|
|
427
458
|
const PONYTAIL_BUNDLE_FILES = [
|
|
@@ -457,18 +488,15 @@ async function stepOpencode(options) {
|
|
|
457
488
|
if (!options.quiet)
|
|
458
489
|
console.log(` OpenCode — write the plugin tree (${dir})`);
|
|
459
490
|
await stepSharedSessionState(dir, options);
|
|
460
|
-
await copySources(dir, filesUnder('rtk
|
|
491
|
+
await copySources(dir, filesUnder('rtk'), 'rtk/session.ts', options);
|
|
461
492
|
await stepRtkFilter(dir, options);
|
|
462
|
-
await copySources(dir, filesUnder('caveman
|
|
493
|
+
await copySources(dir, filesUnder('caveman'), 'caveman/index.ts', options);
|
|
463
494
|
await stepCombo(dir, options);
|
|
464
|
-
await copySources(dir, filesUnder('
|
|
495
|
+
await copySources(dir, filesUnder('commands'), 'commands/index.ts', options);
|
|
465
496
|
await stepUpdater(dir, options);
|
|
466
|
-
await copySources(dir, filesUnder('opencode'), 'opencode/server.ts', options);
|
|
467
497
|
await stepPonytailBundle(dir, options);
|
|
468
|
-
// The loader resolves <pluginDir>/server.ts,
|
|
469
|
-
await
|
|
470
|
-
if (!options.dryRun)
|
|
471
|
-
await fs.rm(path.join(dir, 'opencode', 'index.ts'), { force: true }).catch(() => { });
|
|
498
|
+
// The loader resolves <pluginDir>/server.ts, and the entry is that file: copy it in place.
|
|
499
|
+
await copySources(dir, rootFiles(), 'server.ts', options);
|
|
472
500
|
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
473
501
|
let config = {};
|
|
474
502
|
try {
|
|
@@ -616,7 +644,7 @@ async function runCommandMenu() {
|
|
|
616
644
|
closeRL();
|
|
617
645
|
break;
|
|
618
646
|
case 'dashboard':
|
|
619
|
-
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport });
|
|
647
|
+
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport, currency });
|
|
620
648
|
closeRL();
|
|
621
649
|
break;
|
|
622
650
|
case 'reset':
|
|
@@ -666,8 +694,8 @@ async function stepPiLayer(options) {
|
|
|
666
694
|
if (!options.quiet)
|
|
667
695
|
console.log(` Pi — write the extension tree (${extDir})`);
|
|
668
696
|
await stepSharedSessionState(extDir, options);
|
|
669
|
-
// OMP loads rtk
|
|
670
|
-
await copySources(extDir, filesUnder('rtk
|
|
697
|
+
// OMP loads rtk/session.ts from its plugin manifest, so only the pi tree copies it.
|
|
698
|
+
await copySources(extDir, filesUnder('rtk'), 'rtk/session.ts', options);
|
|
671
699
|
await stepRtkFilter(extDir, options);
|
|
672
700
|
await stepCaveman(extDir, options);
|
|
673
701
|
await stepCombo(extDir, options);
|
|
@@ -873,17 +901,28 @@ async function runInstall() {
|
|
|
873
901
|
closeRL();
|
|
874
902
|
return;
|
|
875
903
|
}
|
|
876
|
-
await capture('shared', () => stepSharedSessionState(userExtDir, installOptions));
|
|
877
904
|
let selfPlugin = false;
|
|
878
905
|
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, installOptions); });
|
|
879
906
|
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, installOptions));
|
|
880
907
|
await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions));
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
908
|
+
// A real install registers the plugin before it writes, so the OMP plugin manifest owns
|
|
909
|
+
// every mode extension. A dry run previews that same destination. A registration that
|
|
910
|
+
// failed is the one fallback: the agent tree then carries the tree so an offline install
|
|
911
|
+
// still loads its add-ons, and the retired copies stay untouched.
|
|
912
|
+
const pluginExtDir = path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions');
|
|
913
|
+
const ownsTree = selfPlugin || dryRun;
|
|
914
|
+
const extDir = ownsTree ? pluginExtDir : userExtDir;
|
|
915
|
+
if (!quiet)
|
|
916
|
+
console.log(` Add-ons load from: ${ownsTree ? 'the OMP plugin manifest' : 'the agent tree (plugin registration failed)'}`);
|
|
917
|
+
await capture('shared', () => stepSharedSessionState(extDir, installOptions));
|
|
918
|
+
await capture('rtk session', () => stepRtkSession(extDir, installOptions));
|
|
919
|
+
await capture('rtk filter', () => stepRtkFilter(extDir, installOptions));
|
|
920
|
+
await capture('caveman', () => stepCaveman(extDir, installOptions));
|
|
921
|
+
await capture('combo', () => stepCombo(extDir, installOptions));
|
|
922
|
+
await capture('commands', () => stepTersioCommands(extDir, installOptions));
|
|
923
|
+
await capture('updater', () => stepUpdater(extDir, installOptions));
|
|
924
|
+
if (ownsTree)
|
|
925
|
+
await capture('retired agent copies', () => stepRetireAgentCopies(userExtDir, installOptions));
|
|
887
926
|
if (selfPlugin)
|
|
888
927
|
await capture('settings', () => writePluginSettings(profile, installOptions));
|
|
889
928
|
if (quiet) {
|
package/dist/cli/manifest.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
// Single list of install writes, shared by installer, doctor, and tests.
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { findPackageRoot } from '../extensions/
|
|
4
|
+
import { findPackageRoot, readTextIfExists } from '../extensions/shared/utils.js';
|
|
5
|
+
import { writeIfChanged } from './common.js';
|
|
5
6
|
export const EXT_DIR = path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'extensions');
|
|
6
|
-
/** 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). */
|
|
7
|
-
const SERVER_SPEC = `./${['opencode', 'server.ts'].join('/')}`;
|
|
8
|
-
export const OPENCODE_SERVER_SHIM = `// Generated by \`tersio install --host opencode\`: OpenCode resolves <pluginDir>/server.ts.
|
|
9
|
-
export { default } from "${SERVER_SPEC}";
|
|
10
|
-
`;
|
|
11
7
|
/** Every file in the tree, as paths relative to the extensions directory. */
|
|
12
8
|
export const TREE_FILES = [
|
|
13
9
|
'shared/host.ts',
|
|
@@ -18,17 +14,17 @@ export const TREE_FILES = [
|
|
|
18
14
|
'shared/usage-ledger.ts',
|
|
19
15
|
'shared/pricing.ts',
|
|
20
16
|
'shared/carbon.ts',
|
|
21
|
-
'
|
|
22
|
-
'caveman
|
|
23
|
-
'caveman
|
|
24
|
-
'caveman
|
|
25
|
-
'caveman
|
|
26
|
-
'rtk
|
|
27
|
-
'rtk
|
|
28
|
-
'combo
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
17
|
+
'shared/utils.ts',
|
|
18
|
+
'caveman/index.ts',
|
|
19
|
+
'caveman/rule.md',
|
|
20
|
+
'caveman/rule-ultra.md',
|
|
21
|
+
'caveman/rule-megacave.md',
|
|
22
|
+
'rtk/session.ts',
|
|
23
|
+
'rtk/filter.ts',
|
|
24
|
+
'combo/index.ts',
|
|
25
|
+
'commands/index.ts',
|
|
26
|
+
'updater/index.ts',
|
|
27
|
+
'server.ts',
|
|
32
28
|
];
|
|
33
29
|
export function sourcePath(relative) {
|
|
34
30
|
return path.join(EXT_DIR, ...relative.split('/'));
|
|
@@ -42,3 +38,35 @@ export function filesUnder(dir) {
|
|
|
42
38
|
}
|
|
43
39
|
return found;
|
|
44
40
|
}
|
|
41
|
+
/** The entries at the extensions root, which `filesUnder` cannot name because it takes a directory. */
|
|
42
|
+
export function rootFiles() {
|
|
43
|
+
return TREE_FILES.filter((file) => !file.includes('/')).map((file) => [sourcePath(file), file]);
|
|
44
|
+
}
|
|
45
|
+
/** The OpenCode plugin entry. It imports `@opencode/plugin`, which the omp and pi loaders lack, so it ships in the OpenCode tree alone. */
|
|
46
|
+
export const OPENCODE_ENTRY = 'server.ts';
|
|
47
|
+
/** The files one host's extension tree holds. The package tree holds all of them. */
|
|
48
|
+
export function treeFilesFor(id) {
|
|
49
|
+
return id === 'opencode' ? TREE_FILES : TREE_FILES.filter((file) => file !== OPENCODE_ENTRY);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Copy every bundled file into `dest`, creating the directories it needs. A file the bundle does not
|
|
53
|
+
* carry is reported and skipped rather than aborting, so one missing source cannot stop the rest.
|
|
54
|
+
*
|
|
55
|
+
* `skip` answers "leave this file alone", which is how the rule bodies survive a repair: an upstream
|
|
56
|
+
* fetch can be newer than the bundled copy, and a re-fetch never overwrites it.
|
|
57
|
+
*/
|
|
58
|
+
export async function syncTreeFiles(dest, files, options, skip) {
|
|
59
|
+
const missing = [];
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
if (skip?.(file))
|
|
62
|
+
continue;
|
|
63
|
+
const from = sourcePath(file);
|
|
64
|
+
const text = await readTextIfExists(from);
|
|
65
|
+
if (text === null) {
|
|
66
|
+
missing.push(from);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
await writeIfChanged(path.join(dest, ...file.split('/')), text, options);
|
|
70
|
+
}
|
|
71
|
+
return missing;
|
|
72
|
+
}
|
package/dist/cli/profile.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, PACKAGE_NAME, PONYTAIL_DEFAULTS, verbose, } from './common.js';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { DEFAULT_CURRENCY, isCurrencyCode } from './currency.js';
|
|
7
|
-
import { readTextIfExists } from '../extensions/
|
|
7
|
+
import { readTextIfExists } from '../extensions/shared/utils.js';
|
|
8
8
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
9
9
|
function readTextIfExistsSync(file) {
|
|
10
10
|
try {
|
package/dist/cli/uninstall.js
CHANGED
|
@@ -4,19 +4,29 @@ import path from 'node:path';
|
|
|
4
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
|
-
import { piAgentDir, readTextIfExists } from '../extensions/
|
|
7
|
+
import { piAgentDir, readTextIfExists } from '../extensions/shared/utils.js';
|
|
8
8
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
9
9
|
// The pi tree mirrors the OMP one. Ponytail and rtk stay: a package and a shared binary are not ours to delete.
|
|
10
10
|
const PONYTAIL_PKG = '@dietrichgebert/ponytail';
|
|
11
11
|
const PI_TREE_DIRS = [
|
|
12
|
+
// What an install writes now.
|
|
13
|
+
'caveman',
|
|
14
|
+
'rtk',
|
|
15
|
+
'updater',
|
|
16
|
+
'combo',
|
|
17
|
+
'commands',
|
|
18
|
+
'shared',
|
|
19
|
+
'server.ts',
|
|
20
|
+
// Legacy: a tree an older build wrote, or a foreign name that was never shipped here.
|
|
12
21
|
'caveman-session',
|
|
13
22
|
'rtk-session',
|
|
23
|
+
'rtk-filter',
|
|
14
24
|
'ai-addons-updater',
|
|
15
25
|
'combo-toggle',
|
|
16
26
|
'tersio-commands',
|
|
17
|
-
'shared',
|
|
18
27
|
'lib',
|
|
19
28
|
'opencode',
|
|
29
|
+
'aaa-combo-boot',
|
|
20
30
|
];
|
|
21
31
|
// One `pi remove`, for both packages the installer adds there.
|
|
22
32
|
async function piRemove(spec, shouldDryRun) {
|
|
@@ -237,15 +247,21 @@ async function runUninstall(options = {}) {
|
|
|
237
247
|
const pluginsDir = OMP_PLUGINS_DIR;
|
|
238
248
|
const ponytailPkgDir = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
|
|
239
249
|
const targets = [
|
|
250
|
+
// What an install writes now.
|
|
251
|
+
'caveman',
|
|
252
|
+
'rtk',
|
|
253
|
+
'updater',
|
|
254
|
+
'combo',
|
|
255
|
+
'commands',
|
|
256
|
+
'shared',
|
|
257
|
+
// Legacy: a tree an older build wrote into the agent tree, or a foreign name never shipped here.
|
|
240
258
|
'caveman-session',
|
|
241
259
|
'rtk-session',
|
|
260
|
+
'rtk-filter',
|
|
242
261
|
'ai-addons-updater',
|
|
243
262
|
'combo-toggle',
|
|
244
263
|
'tersio-commands',
|
|
245
|
-
'shared',
|
|
246
|
-
// Only the updater reads it, and it runs before this.
|
|
247
264
|
'lib',
|
|
248
|
-
// Legacy: imports session-state.js, so it warns once that module is gone.
|
|
249
265
|
'aaa-combo-boot',
|
|
250
266
|
// Foreign to the OMP loader; never shipped here on purpose.
|
|
251
267
|
'opencode',
|
package/dist/cli/update.js
CHANGED
|
@@ -4,9 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
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
|
-
import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/
|
|
7
|
+
import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/shared/utils.js';
|
|
8
8
|
import { refreshPrices } from '../extensions/shared/pricing.js';
|
|
9
|
-
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/
|
|
9
|
+
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/shared/utils.js';
|
|
10
10
|
import { parseHostArg } from './hosts.js';
|
|
11
11
|
const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
12
12
|
function newerThan(a, b) {
|
|
@@ -79,7 +79,7 @@ async function probeUpdatePlan(cliLatest) {
|
|
|
79
79
|
const rtkBin = resolveRtkBinary();
|
|
80
80
|
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
81
81
|
// rule.md ships with the release; only the tracked bodies can go stale.
|
|
82
|
-
const tersioRule = (name) => path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman
|
|
82
|
+
const tersioRule = (name) => path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman', name);
|
|
83
83
|
const controller = new AbortController();
|
|
84
84
|
const [rtkLocal, rtkRelease, ruleLocal, ruleRemote, ponytailLocalText] = await Promise.all([
|
|
85
85
|
settle(rtkBin ? execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => normalizeRtkVersion(r.stdout.trim() || r.stderr.trim()) || null) : Promise.resolve(null), 6000),
|