@krtclcdy/tersio 2.25.3 → 2.25.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -31
- package/dashboard/dist/index.html +18 -16
- package/dist/cli/backups.js +194 -0
- package/dist/cli/common.js +2 -2
- package/dist/cli/dashboard.js +125 -98
- package/dist/cli/doctor-fix.js +42 -45
- package/dist/cli/doctor.js +70 -8
- package/dist/cli/hosts.js +4 -4
- package/dist/cli/install.js +106 -38
- package/dist/cli/manifest.js +45 -16
- package/dist/cli/profile.js +1 -1
- package/dist/cli/uninstall.js +21 -5
- package/dist/cli/update.js +3 -3
- package/dist/cli/usage.js +75 -4
- package/dist/extensions/{caveman-session → caveman}/index.js +10 -3
- package/dist/extensions/{combo-toggle → combo}/index.js +3 -3
- package/dist/extensions/{tersio-commands → commands}/index.js +1 -1
- package/dist/extensions/rtk/filter.js +87 -0
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +4 -3
- package/dist/extensions/{opencode/server.js → server.js} +52 -8
- package/dist/extensions/shared/agent-stats.js +1262 -0
- package/dist/extensions/shared/brand-marks.js +154 -0
- package/dist/extensions/shared/host.js +18 -12
- package/dist/extensions/shared/plugin-settings.js +1 -1
- package/dist/extensions/shared/pricing.js +91 -27
- package/dist/extensions/shared/session-state.js +3 -1
- package/dist/extensions/shared/usage-ledger.js +79 -17
- package/dist/extensions/shared/usage-store.js +106 -22
- package/dist/extensions/{lib → shared}/utils.js +1 -1
- package/dist/extensions/{ai-addons-updater → updater}/index.js +15 -5
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +2 -2
- package/extensions/{caveman-session → caveman}/index.ts +10 -3
- package/extensions/{combo-toggle → combo}/index.ts +3 -3
- package/extensions/{tersio-commands → commands}/index.ts +1 -1
- package/extensions/rtk/filter.ts +90 -0
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +4 -3
- package/extensions/{opencode/server.ts → server.ts} +46 -9
- package/extensions/shared/agent-stats.ts +1657 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/host.ts +26 -13
- package/extensions/shared/plugin-settings.ts +1 -1
- package/extensions/shared/pricing.ts +107 -28
- package/extensions/shared/session-state.ts +3 -1
- package/extensions/shared/types.ts +10 -2
- package/extensions/shared/usage-ledger.ts +94 -19
- package/extensions/shared/usage-store.ts +106 -22
- package/extensions/{lib → shared}/utils.ts +1 -1
- package/extensions/{ai-addons-updater → updater}/index.ts +17 -7
- package/package.json +13 -12
- package/dist/cli/headroom-install.js +0 -266
- package/dist/cli/headroom.js +0 -492
- package/dist/extensions/headroom-session/index.js +0 -93
- package/dist/extensions/shared/headroom.js +0 -131
- /package/dist/extensions/{caveman-session → caveman}/rule-megacave.md +0 -0
- /package/dist/extensions/{caveman-session → caveman}/rule-ultra.md +0 -0
- /package/dist/extensions/{caveman-session → caveman}/rule.md +0 -0
- /package/extensions/{caveman-session → caveman}/rule-megacave.md +0 -0
- /package/extensions/{caveman-session → caveman}/rule-ultra.md +0 -0
- /package/extensions/{caveman-session → caveman}/rule.md +0 -0
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]];
|
|
@@ -252,10 +269,55 @@ async function runDoctorFix(tally) {
|
|
|
252
269
|
console.log('\n Doctor --fix: repairs done — rechecking.');
|
|
253
270
|
const again = await runDoctor(true);
|
|
254
271
|
if (again.missing + again.warn === 0)
|
|
255
|
-
console.log(' Doctor --fix: all checks pass. Restart
|
|
272
|
+
console.log(' Doctor --fix: all checks pass. Restart your agent.');
|
|
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,13 +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);
|
|
389
|
+
}
|
|
390
|
+
// pi's built-in grep and glob never reach the bash hook, so this one filters their output.
|
|
391
|
+
async function stepRtkFilter(extDir, options) {
|
|
392
|
+
if (!options.quiet)
|
|
393
|
+
console.log(' RTK filter — install built-in tool output filtering');
|
|
394
|
+
await copySources(extDir, filesUnder('rtk'), 'rtk/filter.ts', options);
|
|
358
395
|
}
|
|
359
396
|
// rule.md ships with the package; the other bodies track upstream skills.
|
|
360
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.
|
|
@@ -366,11 +403,11 @@ const CAVEMAN_RULES = [
|
|
|
366
403
|
async function stepCaveman(extDir, options) {
|
|
367
404
|
if (!options.quiet)
|
|
368
405
|
console.log(' Caveman — install session mode and rule bodies');
|
|
369
|
-
const cavemanDir = path.join(extDir, 'caveman
|
|
406
|
+
const cavemanDir = path.join(extDir, 'caveman');
|
|
370
407
|
if (!options.dryRun)
|
|
371
408
|
await fs.mkdir(cavemanDir, { recursive: true });
|
|
372
409
|
for (const [name, remote] of CAVEMAN_RULES) {
|
|
373
|
-
const bundled = await readTextIfExists(sourcePath(`caveman
|
|
410
|
+
const bundled = await readTextIfExists(sourcePath(`caveman/${name}`));
|
|
374
411
|
let rule = bundled;
|
|
375
412
|
if (remote && !options.dryRun) {
|
|
376
413
|
try {
|
|
@@ -399,7 +436,7 @@ async function stepCaveman(extDir, options) {
|
|
|
399
436
|
await writeIfChanged(path.join(cavemanDir, name), rule, options);
|
|
400
437
|
}
|
|
401
438
|
}
|
|
402
|
-
await copySources(extDir, filesUnder('caveman
|
|
439
|
+
await copySources(extDir, filesUnder('caveman').filter(([, to]) => to.endsWith('index.ts')), 'caveman/index.ts', options);
|
|
403
440
|
// Renamed body: drop the stale copy an older install left behind.
|
|
404
441
|
if (!options.dryRun)
|
|
405
442
|
await fs.rm(path.join(cavemanDir, 'rule-wenyan.md'), { force: true });
|
|
@@ -407,15 +444,15 @@ async function stepCaveman(extDir, options) {
|
|
|
407
444
|
async function stepTersioCommands(extDir, options) {
|
|
408
445
|
if (!options.quiet)
|
|
409
446
|
console.log(' Tersio commands — install /tersio root command');
|
|
410
|
-
await copySources(extDir, filesUnder('
|
|
447
|
+
await copySources(extDir, filesUnder('commands'), 'commands/index.ts', options);
|
|
411
448
|
}
|
|
412
449
|
async function stepUpdater(extDir, options) {
|
|
413
|
-
await copySources(extDir, filesUnder('
|
|
450
|
+
await copySources(extDir, filesUnder('updater'), 'updater/index.ts', options);
|
|
414
451
|
}
|
|
415
452
|
async function stepCombo(extDir, options) {
|
|
416
453
|
if (!options.quiet)
|
|
417
454
|
console.log(' Combo — install preset switch');
|
|
418
|
-
await copySources(extDir, filesUnder('combo
|
|
455
|
+
await copySources(extDir, filesUnder('combo'), 'combo/index.ts', options);
|
|
419
456
|
}
|
|
420
457
|
// The tree ships no node_modules, so the full ruleset rides along as files.
|
|
421
458
|
const PONYTAIL_BUNDLE_FILES = [
|
|
@@ -451,17 +488,15 @@ async function stepOpencode(options) {
|
|
|
451
488
|
if (!options.quiet)
|
|
452
489
|
console.log(` OpenCode — write the plugin tree (${dir})`);
|
|
453
490
|
await stepSharedSessionState(dir, options);
|
|
454
|
-
await copySources(dir, filesUnder('rtk
|
|
455
|
-
await
|
|
491
|
+
await copySources(dir, filesUnder('rtk'), 'rtk/session.ts', options);
|
|
492
|
+
await stepRtkFilter(dir, options);
|
|
493
|
+
await copySources(dir, filesUnder('caveman'), 'caveman/index.ts', options);
|
|
456
494
|
await stepCombo(dir, options);
|
|
457
|
-
await copySources(dir, filesUnder('
|
|
495
|
+
await copySources(dir, filesUnder('commands'), 'commands/index.ts', options);
|
|
458
496
|
await stepUpdater(dir, options);
|
|
459
|
-
await copySources(dir, filesUnder('opencode'), 'opencode/server.ts', options);
|
|
460
497
|
await stepPonytailBundle(dir, options);
|
|
461
|
-
// The loader resolves <pluginDir>/server.ts,
|
|
462
|
-
await
|
|
463
|
-
if (!options.dryRun)
|
|
464
|
-
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);
|
|
465
500
|
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
466
501
|
let config = {};
|
|
467
502
|
try {
|
|
@@ -609,7 +644,7 @@ async function runCommandMenu() {
|
|
|
609
644
|
closeRL();
|
|
610
645
|
break;
|
|
611
646
|
case 'dashboard':
|
|
612
|
-
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport });
|
|
647
|
+
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport, currency });
|
|
613
648
|
closeRL();
|
|
614
649
|
break;
|
|
615
650
|
case 'reset':
|
|
@@ -659,8 +694,9 @@ async function stepPiLayer(options) {
|
|
|
659
694
|
if (!options.quiet)
|
|
660
695
|
console.log(` Pi — write the extension tree (${extDir})`);
|
|
661
696
|
await stepSharedSessionState(extDir, options);
|
|
662
|
-
// OMP loads rtk
|
|
663
|
-
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);
|
|
699
|
+
await stepRtkFilter(extDir, options);
|
|
664
700
|
await stepCaveman(extDir, options);
|
|
665
701
|
await stepCombo(extDir, options);
|
|
666
702
|
await stepTersioCommands(extDir, options);
|
|
@@ -700,14 +736,30 @@ async function stepPonytailForPi(agentDir, options) {
|
|
|
700
736
|
}
|
|
701
737
|
if (options.dryRun)
|
|
702
738
|
return;
|
|
739
|
+
// Only when pi still declares the npm spec: with nothing to remove, `pi remove` still
|
|
740
|
+
// shells out to npm, which warns on stderr and exits non-zero.
|
|
741
|
+
const declared = await piDeclaredPackages(agentDir);
|
|
742
|
+
if (declared.some(isNpmPonytailPackage)) {
|
|
743
|
+
try {
|
|
744
|
+
await execNetwork('Removing the pi npm copy of Ponytail', 'pi', ['remove', 'npm:@dietrichgebert/ponytail'], { timeout: 120000 });
|
|
745
|
+
}
|
|
746
|
+
catch (e) {
|
|
747
|
+
sayTagged(` [warn] pi remove ponytail: ${shortError(e)}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
await registerPiLocalPonytail(agentDir, dest, options);
|
|
751
|
+
}
|
|
752
|
+
// pi declares its package sources in settings.json; [] when the file is absent or unreadable.
|
|
753
|
+
async function piDeclaredPackages(agentDir) {
|
|
703
754
|
try {
|
|
704
|
-
|
|
755
|
+
const settings = JSON.parse(await fs.readFile(path.join(agentDir, 'settings.json'), 'utf8'));
|
|
756
|
+
return Array.isArray(settings.packages) ? settings.packages : [];
|
|
705
757
|
}
|
|
706
|
-
catch
|
|
707
|
-
|
|
758
|
+
catch {
|
|
759
|
+
return [];
|
|
708
760
|
}
|
|
709
|
-
await registerPiLocalPonytail(agentDir, dest, options);
|
|
710
761
|
}
|
|
762
|
+
const isNpmPonytailPackage = (value) => typeof value === 'string' && value.startsWith('npm:@dietrichgebert/ponytail');
|
|
711
763
|
// pi lists package sources in settings; swap the npm spec for the local dir.
|
|
712
764
|
async function registerPiLocalPonytail(agentDir, dest, options) {
|
|
713
765
|
const settingsPath = path.join(agentDir, 'settings.json');
|
|
@@ -719,7 +771,7 @@ async function registerPiLocalPonytail(agentDir, dest, options) {
|
|
|
719
771
|
return;
|
|
720
772
|
}
|
|
721
773
|
const packages = (Array.isArray(settings.packages) ? settings.packages : []);
|
|
722
|
-
const kept = packages.filter((p) =>
|
|
774
|
+
const kept = packages.filter((p) => !isNpmPonytailPackage(p));
|
|
723
775
|
if (!kept.includes(dest))
|
|
724
776
|
kept.push(dest);
|
|
725
777
|
settings.packages = kept;
|
|
@@ -727,6 +779,10 @@ async function registerPiLocalPonytail(agentDir, dest, options) {
|
|
|
727
779
|
if (!options.quiet)
|
|
728
780
|
sayTagged(` [write] pi package source → ${dest}`);
|
|
729
781
|
}
|
|
782
|
+
// Name the preset this run actually chose, instead of a hardcoded level.
|
|
783
|
+
function comboHint(host, comboDefault) {
|
|
784
|
+
return comboDefault === 'off' ? `\nDone — restart ${host}.` : `\nDone — restart ${host}, then /combo ${comboDefault}.`;
|
|
785
|
+
}
|
|
730
786
|
async function runInstall() {
|
|
731
787
|
// Without this guard a picked command re-enters here with command === null and loops straight back into the picker.
|
|
732
788
|
if (command === null && tty() && !yes && !updatePromptDone) {
|
|
@@ -830,7 +886,7 @@ async function runInstall() {
|
|
|
830
886
|
if (failures.length > 0)
|
|
831
887
|
console.log(`\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
|
|
832
888
|
else
|
|
833
|
-
console.log('
|
|
889
|
+
console.log(comboHint('OpenCode', profile.comboDefault));
|
|
834
890
|
closeRL();
|
|
835
891
|
return;
|
|
836
892
|
}
|
|
@@ -841,20 +897,32 @@ async function runInstall() {
|
|
|
841
897
|
if (failures.length > 0)
|
|
842
898
|
console.log(`\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
|
|
843
899
|
else
|
|
844
|
-
console.log('
|
|
900
|
+
console.log(comboHint('pi', profile.comboDefault));
|
|
845
901
|
closeRL();
|
|
846
902
|
return;
|
|
847
903
|
}
|
|
848
|
-
await capture('shared', () => stepSharedSessionState(userExtDir, installOptions));
|
|
849
904
|
let selfPlugin = false;
|
|
850
905
|
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, installOptions); });
|
|
851
906
|
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, installOptions));
|
|
852
907
|
await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions));
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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));
|
|
858
926
|
if (selfPlugin)
|
|
859
927
|
await capture('settings', () => writePluginSettings(profile, installOptions));
|
|
860
928
|
if (quiet) {
|
|
@@ -862,7 +930,7 @@ async function runInstall() {
|
|
|
862
930
|
console.log(` add-ons: ${failures.length} failed (${failures.join(', ')}) — see [fail] lines above`);
|
|
863
931
|
}
|
|
864
932
|
else {
|
|
865
|
-
console.log(failures.length === 0 ? '
|
|
933
|
+
console.log(failures.length === 0 ? comboHint('OMP', profile.comboDefault) : `\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
|
|
866
934
|
}
|
|
867
935
|
closeRL();
|
|
868
936
|
}
|
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,16 +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
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
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',
|
|
31
28
|
];
|
|
32
29
|
export function sourcePath(relative) {
|
|
33
30
|
return path.join(EXT_DIR, ...relative.split('/'));
|
|
@@ -41,3 +38,35 @@ export function filesUnder(dir) {
|
|
|
41
38
|
}
|
|
42
39
|
return found;
|
|
43
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),
|