@krtclcdy/tersio 2.25.0 → 2.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/doctor-fix.js +18 -27
- package/cli/doctor.js +11 -5
- package/cli/install.js +102 -69
- package/cli/interactive.js +2 -2
- package/cli/manifest.js +2 -0
- package/cli/rtk-wiring.js +5 -2
- package/cli/uninstall.js +25 -11
- package/cli/update.js +9 -6
- package/extensions/ai-addons-updater/index.js +60 -44
- package/extensions/ai-addons-updater/index.ts +52 -40
- package/extensions/caveman-session/index.js +10 -10
- package/extensions/caveman-session/index.ts +9 -10
- package/extensions/caveman-session/rule-ultra.md +63 -0
- package/extensions/caveman-session/rule-wenyan.md +63 -0
- package/extensions/caveman-session/rule.md +0 -13
- package/extensions/lib/utils.js +2 -1
- package/extensions/lib/utils.ts +2 -1
- package/extensions/opencode/server.js +33 -3
- package/extensions/opencode/server.ts +29 -3
- package/package.json +4 -2
package/cli/doctor-fix.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, execP, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
6
6
|
import { execNetwork, sayTagged } from './interactive.js';
|
|
7
7
|
import { wireRtkOmp, wireRtkOpencode, wireRtkPi, ensureRtkInConfig } from './rtk-wiring.js';
|
|
8
|
-
import {
|
|
8
|
+
import { RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
|
|
9
9
|
import { runLatestUpdate } from './update.js';
|
|
10
10
|
import { OPENCODE_SERVER_SHIM, TREE_FILES, sourcePath } from './manifest.js';
|
|
11
11
|
import { detectHosts, hostExtensionsDir } from './hosts.js';
|
|
@@ -50,16 +50,20 @@ async function fixExtensionTrees() {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
// The tree alone does nothing on OpenCode without its entrypoint registered in opencode.json.
|
|
53
|
+
// The tersio tree ships the rtk rewrite, so the rtk plugin entry is redundant.
|
|
53
54
|
async function fixOpencodeEntry() {
|
|
54
55
|
const { HOME } = await import('./common.js');
|
|
55
56
|
const dir = path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
|
|
56
57
|
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
57
58
|
const nested = path.join(dir, 'opencode', 'server.ts');
|
|
58
|
-
|
|
59
|
-
if (!nestedText)
|
|
59
|
+
if (!(await readTextIfExists(nested)))
|
|
60
60
|
return;
|
|
61
61
|
// The loader resolves <pluginDir>/server.ts, so mirror the entry at the tree root.
|
|
62
62
|
await writeIfChanged(path.join(dir, 'server.ts'), OPENCODE_SERVER_SHIM, { dryRun, verbose });
|
|
63
|
+
if (dryRun)
|
|
64
|
+
sayTagged(` [dry-run] would remove the redundant rtk plugin entry`);
|
|
65
|
+
else
|
|
66
|
+
await fs.rm(path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts'), { force: true }).catch(() => { });
|
|
63
67
|
const raw = await readTextIfExists(configPath);
|
|
64
68
|
let config = {};
|
|
65
69
|
try {
|
|
@@ -69,11 +73,11 @@ async function fixOpencodeEntry() {
|
|
|
69
73
|
return;
|
|
70
74
|
}
|
|
71
75
|
const plugins = Array.isArray(config.plugins) ? [...config.plugins] : [];
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
76
|
+
const kept = plugins.filter((p) => typeof p !== 'string' || !p.endsWith('/opencode/plugins/rtk.ts'));
|
|
77
|
+
if (kept.length === plugins.length && kept.includes(dir))
|
|
74
78
|
return;
|
|
75
|
-
|
|
76
|
-
config.plugins =
|
|
79
|
+
kept.push(dir);
|
|
80
|
+
config.plugins = kept;
|
|
77
81
|
if (dryRun) {
|
|
78
82
|
sayTagged(` [dry-run] would register tersio in ${configPath}`);
|
|
79
83
|
return;
|
|
@@ -88,30 +92,17 @@ async function fixExtensions(pluginsDir) {
|
|
|
88
92
|
console.log(' Doctor --fix: restoring plugin extension files');
|
|
89
93
|
const pluginExtDir = path.join(pluginsDir, 'node_modules', '@krtclcdy', 'tersio', 'extensions');
|
|
90
94
|
await fs.mkdir(pluginExtDir, { recursive: true });
|
|
91
|
-
//
|
|
92
|
-
|
|
95
|
+
// Rule bodies are never overwritten: they can be newer upstream fetches.
|
|
96
|
+
const ruleFile = (f) => /^caveman-session\/rule.*\.md$/.test(f);
|
|
97
|
+
for (const file of TREE_FILES) {
|
|
98
|
+
const dest = path.join(pluginExtDir, ...file.split('/'));
|
|
99
|
+
if (ruleFile(file) && existsSync(dest))
|
|
100
|
+
continue;
|
|
93
101
|
const text = await readTextIfExists(sourcePath(file));
|
|
94
102
|
if (text === null)
|
|
95
103
|
sayTagged(` [warn] bundled source missing: ${sourcePath(file)}`);
|
|
96
104
|
else
|
|
97
|
-
await writeIfChanged(
|
|
98
|
-
}
|
|
99
|
-
const ruleDest = path.join(pluginExtDir, 'caveman-session', 'rule.md');
|
|
100
|
-
if ((await readTextIfExists(ruleDest)) === null) {
|
|
101
|
-
const bundled = await readTextIfExists(path.join(EXT_DIR, 'caveman-session', 'rule.md'));
|
|
102
|
-
if (bundled !== null) {
|
|
103
|
-
await writeIfChanged(ruleDest, bundled, { dryRun, verbose });
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
const rule = await Promise.race([
|
|
107
|
-
httpsGet(CAVEMAN_REMOTE_RULE).catch(() => null),
|
|
108
|
-
new Promise((resolve) => setTimeout(() => resolve(null), 8000)),
|
|
109
|
-
]);
|
|
110
|
-
if (rule !== null)
|
|
111
|
-
await writeIfChanged(ruleDest, rule, { dryRun, verbose });
|
|
112
|
-
else
|
|
113
|
-
console.log(' [warn] Caveman rule unreachable and no bundled rule exists');
|
|
114
|
-
}
|
|
105
|
+
await writeIfChanged(dest, text, { dryRun, verbose });
|
|
115
106
|
}
|
|
116
107
|
// Writing is not atomic, so re-read rather than assume the tree is whole.
|
|
117
108
|
if (dryRun)
|
package/cli/doctor.js
CHANGED
|
@@ -17,7 +17,7 @@ async function runDoctor(recheck = false) {
|
|
|
17
17
|
const ponytailPkg = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
18
18
|
const tersioPluginDir = path.join(pluginsDir, 'node_modules', '@krtclcdy', 'tersio');
|
|
19
19
|
const cavemanIndex = path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts');
|
|
20
|
-
const cavemanRule = path.join(tersioPluginDir, 'extensions', 'caveman-session',
|
|
20
|
+
const cavemanRule = (name) => path.join(tersioPluginDir, 'extensions', 'caveman-session', name);
|
|
21
21
|
const rtkIndex = path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts');
|
|
22
22
|
const updaterIndex = path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts');
|
|
23
23
|
// Independent probes start concurrently; sections report in fixed order as their data settles. Every probe resolves instead of rejecting.
|
|
@@ -28,11 +28,13 @@ async function runDoctor(recheck = false) {
|
|
|
28
28
|
ponytailPkgText: readTextIfExists(ponytailPkg),
|
|
29
29
|
rtkBinText: rtkBin ? readTextIfExists(rtkBin) : Promise.resolve(null),
|
|
30
30
|
cavemanIndexText: readTextIfExists(cavemanIndex),
|
|
31
|
-
cavemanRuleText: readTextIfExists(cavemanRule),
|
|
31
|
+
cavemanRuleText: readTextIfExists(cavemanRule('rule.md')),
|
|
32
|
+
cavemanUltraText: readTextIfExists(cavemanRule('rule-ultra.md')),
|
|
33
|
+
cavemanWenyanText: readTextIfExists(cavemanRule('rule-wenyan.md')),
|
|
32
34
|
rtkIndexText: readTextIfExists(rtkIndex),
|
|
33
35
|
updaterIndexText: readTextIfExists(updaterIndex),
|
|
34
36
|
rtkMtime: rtkBin ? fs.stat(rtkBin).catch(() => null) : Promise.resolve(null),
|
|
35
|
-
ruleMtime: fs.stat(cavemanRule).catch(() => null),
|
|
37
|
+
ruleMtime: fs.stat(cavemanRule('rule.md')).catch(() => null),
|
|
36
38
|
ponytailMtime: fs.stat(ponytailPkg).catch(() => null),
|
|
37
39
|
pricesMtime: fs.stat(pricesCachePath()).catch(() => null),
|
|
38
40
|
};
|
|
@@ -40,7 +42,7 @@ async function runDoctor(recheck = false) {
|
|
|
40
42
|
const err = e;
|
|
41
43
|
return err.stdout?.trim() || err.stderr?.trim() || null;
|
|
42
44
|
}) : Promise.resolve(null);
|
|
43
|
-
const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
|
|
45
|
+
const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, cavemanUltraText, cavemanWenyanText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
|
|
44
46
|
Promise.all([
|
|
45
47
|
probes.ompPkgText,
|
|
46
48
|
probes.configText,
|
|
@@ -53,6 +55,8 @@ async function runDoctor(recheck = false) {
|
|
|
53
55
|
]),
|
|
54
56
|
Promise.all([
|
|
55
57
|
probes.cavemanRuleText,
|
|
58
|
+
probes.cavemanUltraText,
|
|
59
|
+
probes.cavemanWenyanText,
|
|
56
60
|
probes.ruleMtime,
|
|
57
61
|
probes.rtkBinText,
|
|
58
62
|
probes.rtkMtime,
|
|
@@ -129,7 +133,9 @@ async function runDoctor(recheck = false) {
|
|
|
129
133
|
check('Prices feed', pricesMtime !== null, pricesAge || pricesCachePath());
|
|
130
134
|
section('Add-ons');
|
|
131
135
|
const ruleAge = ruleMtime ? `(updated ${relTime(Date.now() - ruleMtime.mtimeMs)} · ${absDate(ruleMtime.mtimeMs)})` : '';
|
|
132
|
-
|
|
136
|
+
const cavemanRules = [['rule.md', cavemanRuleText], ['rule-ultra.md', cavemanUltraText], ['rule-wenyan.md', cavemanWenyanText]];
|
|
137
|
+
const missingRules = cavemanRules.filter(([, text]) => text === null).map(([name]) => name);
|
|
138
|
+
check('Caveman rule', missingRules.length === 0, missingRules.length ? `missing: ${missingRules.join(', ')}` : ruleAge);
|
|
133
139
|
const rtkAge = rtkMtime ? `(updated ${relTime(Date.now() - rtkMtime.mtimeMs)} · ${absDate(rtkMtime.mtimeMs)})` : '';
|
|
134
140
|
check('RTK binary', rtkBin !== null, rtkBinText === null ? 'not found in PATH' : [rtkVersion, rtkAge].filter(Boolean).join(' '));
|
|
135
141
|
// rtk_run needs a host exec API, which doctor cannot assume.
|
package/cli/install.js
CHANGED
|
@@ -13,7 +13,7 @@ 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 {
|
|
16
|
+
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_WENYAN, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, parseChecksum, piAgentDir, readTextIfExists, resolveRtkBinary, rtkPlatformSpec, sha256File, } from '../extensions/lib/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';
|
|
@@ -356,39 +356,40 @@ async function stepRtkSession(extDir, options) {
|
|
|
356
356
|
console.log(' RTK session — install session mode');
|
|
357
357
|
await copySources(extDir, filesUnder('rtk-session'), 'rtk-session/index.ts', options);
|
|
358
358
|
}
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
catch (e) {
|
|
368
|
-
if (bundled !== null) {
|
|
369
|
-
debug(`Using bundled Caveman rule: ${e.message}`);
|
|
370
|
-
return bundled;
|
|
371
|
-
}
|
|
372
|
-
sayTagged(` [warn] Could not fetch caveman rule: ${e.message}`);
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
async function stepCaveman(extDir, rule, options) {
|
|
359
|
+
// rule.md ships with the package; the other bodies track upstream skills.
|
|
360
|
+
const CAVEMAN_RULES = [
|
|
361
|
+
['rule.md', null],
|
|
362
|
+
['rule-ultra.md', CAVEMAN_REMOTE_ULTRA],
|
|
363
|
+
['rule-wenyan.md', CAVEMAN_REMOTE_WENYAN],
|
|
364
|
+
];
|
|
365
|
+
async function stepCaveman(extDir, options) {
|
|
377
366
|
if (!options.quiet)
|
|
378
|
-
console.log(' Caveman —
|
|
367
|
+
console.log(' Caveman — install session mode and rule bodies');
|
|
379
368
|
const cavemanDir = path.join(extDir, 'caveman-session');
|
|
380
369
|
if (!options.dryRun)
|
|
381
370
|
await fs.mkdir(cavemanDir, { recursive: true });
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
371
|
+
for (const [name, remote] of CAVEMAN_RULES) {
|
|
372
|
+
const bundled = await readTextIfExists(sourcePath(`caveman-session/${name}`));
|
|
373
|
+
let rule = bundled;
|
|
374
|
+
if (remote && !options.dryRun) {
|
|
375
|
+
try {
|
|
376
|
+
rule = await withInteractiveSpinner(`Fetching Caveman ${name}`, () => httpsGet(remote));
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
if (bundled === null)
|
|
380
|
+
sayTagged(` [warn] Could not fetch caveman ${name}: ${e.message}`);
|
|
381
|
+
else
|
|
382
|
+
debug(`Using bundled Caveman ${name}: ${e.message}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (rule === null) {
|
|
386
|
+
if (remote)
|
|
387
|
+
sayTagged(` [hint] Manual: ${remote}`);
|
|
388
|
+
console.log(` [skip] Caveman ${name} unavailable`);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
await writeIfChanged(path.join(cavemanDir, name), rule, options);
|
|
392
|
+
}
|
|
392
393
|
}
|
|
393
394
|
await copySources(extDir, filesUnder('caveman-session').filter(([, to]) => to.endsWith('index.ts')), 'caveman-session/index.ts', options);
|
|
394
395
|
}
|
|
@@ -459,18 +460,22 @@ async function stepOpencode(options) {
|
|
|
459
460
|
return;
|
|
460
461
|
}
|
|
461
462
|
const plugins = Array.isArray(config.plugins) ? [...config.plugins] : [];
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
463
|
+
// The tree ships the rtk rewrite, so its own plugin entry is redundant.
|
|
464
|
+
const kept = plugins.filter((p) => typeof p !== 'string' || !p.endsWith('/opencode/plugins/rtk.ts'));
|
|
465
|
+
if (!kept.includes(dir))
|
|
466
|
+
kept.push(dir);
|
|
467
|
+
config.plugins = kept;
|
|
468
|
+
if (!options.dryRun) {
|
|
467
469
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
470
|
+
await fs.rm(path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts'), { force: true }).catch(() => { });
|
|
471
|
+
}
|
|
468
472
|
await writeIfChanged(configPath, `${JSON.stringify(config, null, 2)}\n`, options);
|
|
469
473
|
}
|
|
470
474
|
async function resolveProfile(opts = {}) {
|
|
471
475
|
// Seed from the stored defaults so flag-less runs keep them.
|
|
472
476
|
const profile = await storedProfile();
|
|
473
477
|
// One numbered prompt for all three modes, for a real terminal user with no default flags. Never for --apply-update, and never for a script.
|
|
478
|
+
let comboChosen = comboDefaultFlag !== undefined;
|
|
474
479
|
if (tty() && !profileFlagsGiven && !applyUpdate) {
|
|
475
480
|
const choice = await askInteractiveChoice('Session-start defaults — Combo preset', [
|
|
476
481
|
{ value: 'off', label: 'off' },
|
|
@@ -478,8 +483,10 @@ async function resolveProfile(opts = {}) {
|
|
|
478
483
|
{ value: 'balanced', label: 'balanced', hint: 'caveman=full, rtk=on, ponytail=full' },
|
|
479
484
|
{ value: 'max', label: 'max', hint: 'caveman=ultra, rtk=on, ponytail=ultra' },
|
|
480
485
|
], profile.comboDefault);
|
|
481
|
-
if (choice.status === 'selected')
|
|
486
|
+
if (choice.status === 'selected') {
|
|
482
487
|
profile.comboDefault = choice.value;
|
|
488
|
+
comboChosen = true;
|
|
489
|
+
}
|
|
483
490
|
else {
|
|
484
491
|
closeRL();
|
|
485
492
|
process.exit(130);
|
|
@@ -488,10 +495,13 @@ async function resolveProfile(opts = {}) {
|
|
|
488
495
|
// Explicit flags always win over the Combo preset.
|
|
489
496
|
if (comboDefaultFlag !== undefined)
|
|
490
497
|
profile.comboDefault = comboDefaultFlag;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
498
|
+
// A stored per-mode choice survives installs and updates; only a chosen preset rewrites it.
|
|
499
|
+
if (comboChosen) {
|
|
500
|
+
const preset = COMBO_PRESET_MODES[profile.comboDefault] ?? COMBO_PRESET_MODES.off;
|
|
501
|
+
profile.cavemanDefault = preset.caveman;
|
|
502
|
+
profile.rtkDefault = preset.rtk;
|
|
503
|
+
profile.ponytailDefault = preset.ponytail;
|
|
504
|
+
}
|
|
495
505
|
if (cavemanDefaultFlag !== undefined)
|
|
496
506
|
profile.cavemanDefault = cavemanDefaultFlag;
|
|
497
507
|
if (rtkDefaultFlag !== undefined)
|
|
@@ -506,7 +516,8 @@ let updatePromptDone = false;
|
|
|
506
516
|
// Bare `tersio` at a terminal picks a command; scripts and flags install.
|
|
507
517
|
async function runCommandMenu() {
|
|
508
518
|
printWelcome();
|
|
509
|
-
|
|
519
|
+
// Interactive launches always check; scripts use the cached verdict.
|
|
520
|
+
const newer = await checkForUpdate(true);
|
|
510
521
|
if (typeof newer === 'string' && !dryRun) {
|
|
511
522
|
const answer = await askInteractiveConfirm(`tersio ${newer} is available (installed ${PACKAGE_VERSION}). Install it now?`);
|
|
512
523
|
if (answer.status === 'confirmed' && answer.value) {
|
|
@@ -636,50 +647,73 @@ async function stepPiLayer(options) {
|
|
|
636
647
|
sayTagged(` [note] pi also has a tersio package (${declared}) — remove one, or both copies load: pi remove npm:${PACKAGE_NAME}`);
|
|
637
648
|
if (!options.quiet)
|
|
638
649
|
console.log(` Pi — write the extension tree (${extDir})`);
|
|
639
|
-
const cavemanRule = await fetchCavemanRule(options);
|
|
640
650
|
await stepSharedSessionState(extDir, options);
|
|
641
651
|
// OMP loads rtk-session from its plugin manifest, so only the pi tree copies it.
|
|
642
652
|
await copySources(extDir, filesUnder('rtk-session'), 'rtk-session/index.ts', options);
|
|
643
|
-
await stepCaveman(extDir,
|
|
653
|
+
await stepCaveman(extDir, options);
|
|
644
654
|
await stepCombo(extDir, options);
|
|
645
655
|
await stepTersioCommands(extDir, options);
|
|
646
656
|
await stepUpdater(extDir, options);
|
|
647
657
|
await stepPonytailForPi(agentDir, options);
|
|
648
658
|
}
|
|
649
|
-
//
|
|
659
|
+
// A local dir, not an npm spec: pi only version-checks npm entries, so this avoids the update nag.
|
|
660
|
+
const PI_PONYTAIL_FILES = [
|
|
661
|
+
'pi-extension/index.js',
|
|
662
|
+
'hooks/ponytail-instructions.js',
|
|
663
|
+
'hooks/ponytail-config.js',
|
|
664
|
+
'skills/ponytail/SKILL.md',
|
|
665
|
+
];
|
|
666
|
+
const PI_PONYTAIL_PACKAGE = `${JSON.stringify({ name: 'ponytail', version: '0.0.0', private: true, pi: { extensions: ['./pi-extension/index.js'], skills: ['./skills'] } }, null, 2)}\n`;
|
|
650
667
|
async function stepPonytailForPi(agentDir, options) {
|
|
651
668
|
if (!options.quiet)
|
|
652
|
-
console.log(' Ponytail —
|
|
653
|
-
|
|
669
|
+
console.log(' Ponytail — write the local pi package');
|
|
670
|
+
const root = findHoistedPackage('@dietrichgebert/ponytail', path.dirname(fileURLToPath(import.meta.url)));
|
|
671
|
+
const dest = path.join(agentDir, 'ponytail');
|
|
672
|
+
if (!root) {
|
|
673
|
+
console.log(' [skip] Ponytail package not found next to the CLI');
|
|
674
|
+
console.log(' [hint] The /ponytail command will not work; run: tersio update --host pi');
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
await writeIfChanged(path.join(dest, 'package.json'), PI_PONYTAIL_PACKAGE, options);
|
|
678
|
+
for (const rel of PI_PONYTAIL_FILES) {
|
|
679
|
+
let text;
|
|
654
680
|
try {
|
|
655
|
-
await
|
|
656
|
-
return;
|
|
681
|
+
text = await fs.readFile(path.join(root, ...rel.split('/')), 'utf8');
|
|
657
682
|
}
|
|
658
|
-
catch
|
|
659
|
-
sayTagged(` [fail]
|
|
683
|
+
catch {
|
|
684
|
+
sayTagged(` [fail] Ponytail file missing: ${rel}`);
|
|
685
|
+
return;
|
|
660
686
|
}
|
|
687
|
+
await writeIfChanged(path.join(dest, ...rel.split('/')), text, options);
|
|
661
688
|
}
|
|
662
|
-
|
|
663
|
-
if (!skills) {
|
|
664
|
-
console.log(' [skip] Ponytail package not found next to the CLI — skills not copied');
|
|
665
|
-
console.log(' [hint] The /ponytail command will not work; run: pi install npm:@dietrichgebert/ponytail');
|
|
689
|
+
if (options.dryRun)
|
|
666
690
|
return;
|
|
691
|
+
try {
|
|
692
|
+
await execNetwork('Removing the pi npm copy of Ponytail', 'pi', ['remove', 'npm:@dietrichgebert/ponytail'], { timeout: 120000 });
|
|
667
693
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (verbose && !options.quiet)
|
|
671
|
-
sayTagged(` [dry-run] would copy Ponytail skills to ${dest}`);
|
|
672
|
-
return;
|
|
694
|
+
catch (e) {
|
|
695
|
+
sayTagged(` [warn] pi remove ponytail: ${shortError(e)}`);
|
|
673
696
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
697
|
+
await registerPiLocalPonytail(agentDir, dest, options);
|
|
698
|
+
}
|
|
699
|
+
// pi lists package sources in settings; swap the npm spec for the local dir.
|
|
700
|
+
async function registerPiLocalPonytail(agentDir, dest, options) {
|
|
701
|
+
const settingsPath = path.join(agentDir, 'settings.json');
|
|
702
|
+
let settings;
|
|
703
|
+
try {
|
|
704
|
+
settings = JSON.parse(await fs.readFile(settingsPath, 'utf8'));
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
return;
|
|
682
708
|
}
|
|
709
|
+
const packages = (Array.isArray(settings.packages) ? settings.packages : []);
|
|
710
|
+
const kept = packages.filter((p) => typeof p !== 'string' || !p.startsWith('npm:@dietrichgebert/ponytail'));
|
|
711
|
+
if (!kept.includes(dest))
|
|
712
|
+
kept.push(dest);
|
|
713
|
+
settings.packages = kept;
|
|
714
|
+
await fs.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
715
|
+
if (!options.quiet)
|
|
716
|
+
sayTagged(` [write] pi package source → ${dest}`);
|
|
683
717
|
}
|
|
684
718
|
async function runInstall() {
|
|
685
719
|
// Without this guard a picked command re-enters here with command === null and loops straight back into the picker.
|
|
@@ -799,14 +833,13 @@ async function runInstall() {
|
|
|
799
833
|
closeRL();
|
|
800
834
|
return;
|
|
801
835
|
}
|
|
802
|
-
const cavemanRule = await fetchCavemanRule(installOptions);
|
|
803
836
|
await capture('shared', () => stepSharedSessionState(userExtDir, installOptions));
|
|
804
837
|
let selfPlugin = false;
|
|
805
838
|
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, installOptions); });
|
|
806
839
|
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, installOptions));
|
|
807
840
|
await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions));
|
|
808
841
|
await capture('rtk session', () => stepRtkSession(userExtDir, installOptions));
|
|
809
|
-
await capture('caveman', () => stepCaveman(path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions'),
|
|
842
|
+
await capture('caveman', () => stepCaveman(path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions'), installOptions));
|
|
810
843
|
await capture('combo', () => stepCombo(userExtDir, installOptions));
|
|
811
844
|
await capture('commands', () => stepTersioCommands(userExtDir, installOptions));
|
|
812
845
|
await capture('updater', () => stepUpdater(userExtDir, installOptions));
|
package/cli/interactive.js
CHANGED
|
@@ -26,9 +26,9 @@ function tty() {
|
|
|
26
26
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
27
27
|
}
|
|
28
28
|
let spinnerDepth = 0;
|
|
29
|
-
//
|
|
29
|
+
// Clack's spinner stop writes an extra newline under CI, shifting later output by a row.
|
|
30
30
|
async function withInteractiveSpinner(message, work) {
|
|
31
|
-
if (!tty() || spinnerDepth > 0)
|
|
31
|
+
if (!tty() || spinnerDepth > 0 || process.env.CI === 'true')
|
|
32
32
|
return work(() => { });
|
|
33
33
|
// Clack manages stdin itself. Close the legacy question interface before its first use; non-interactive callers never reach this branch.
|
|
34
34
|
closeRL();
|
package/cli/manifest.js
CHANGED
|
@@ -20,6 +20,8 @@ export const TREE_FILES = [
|
|
|
20
20
|
'lib/utils.ts',
|
|
21
21
|
'caveman-session/index.ts',
|
|
22
22
|
'caveman-session/rule.md',
|
|
23
|
+
'caveman-session/rule-ultra.md',
|
|
24
|
+
'caveman-session/rule-wenyan.md',
|
|
23
25
|
'rtk-session/index.ts',
|
|
24
26
|
'combo-toggle/index.ts',
|
|
25
27
|
'tersio-commands/index.ts',
|
package/cli/rtk-wiring.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// OMP loads only listed extensions, so register rtk's path; fail-open.
|
|
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 { execFile } from 'node:child_process';
|
|
@@ -47,8 +47,11 @@ export async function wireRtkOmp(rtkBin, options = {}) {
|
|
|
47
47
|
await ensureRtkInConfig(options);
|
|
48
48
|
return true;
|
|
49
49
|
}
|
|
50
|
+
// The Tersio plugin does the same rewrite, so OpenCode needs no rtk plugin of its own.
|
|
50
51
|
export async function wireRtkOpencode(rtkBin, options = {}) {
|
|
51
|
-
|
|
52
|
+
if (!options.dryRun && !options.quiet)
|
|
53
|
+
console.log(' RTK rewrite runs inside the Tersio plugin — no separate entry');
|
|
54
|
+
return existsSync(rtkBin);
|
|
52
55
|
}
|
|
53
56
|
export async function wireRtkPi(rtkBin, options = {}) {
|
|
54
57
|
return runWire(rtkBin, { host: 'pi', mechanism: 'bash tool_call', initArgs: ['init', '-g', '--agent', 'pi'], hint: 'rtk init -g --agent pi' }, options);
|
package/cli/uninstall.js
CHANGED
|
@@ -4,8 +4,7 @@ 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 {
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { piAgentDir, readTextIfExists } from '../extensions/lib/utils.js';
|
|
9
8
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
10
9
|
// The pi tree mirrors the OMP one. Ponytail and rtk stay: a package and a shared binary are not ours to delete.
|
|
11
10
|
const PONYTAIL_PKG = '@dietrichgebert/ponytail';
|
|
@@ -42,20 +41,30 @@ async function piRemove(spec, shouldDryRun) {
|
|
|
42
41
|
sayTagged(` [hint] Manual: pi remove ${spec}`);
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
|
-
//
|
|
44
|
+
// The installer writes a local package dir; drop it and its settings entry.
|
|
46
45
|
async function removeCopiedPonytailSkills(shouldDryRun) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
46
|
+
const agentDir = piAgentDir();
|
|
47
|
+
const settingsPath = path.join(agentDir, 'settings.json');
|
|
48
|
+
try {
|
|
49
|
+
const settings = JSON.parse(await fs.readFile(settingsPath, 'utf8'));
|
|
50
|
+
if (!Array.isArray(settings.packages))
|
|
51
|
+
return;
|
|
52
|
+
const kept = settings.packages.filter((p) => p !== path.join(agentDir, 'ponytail'));
|
|
53
|
+
if (kept.length === settings.packages.length)
|
|
54
|
+
return;
|
|
55
|
+
if (shouldDryRun)
|
|
56
|
+
sayTagged(` [dry-run] would drop the local ponytail entry from ${settingsPath}`);
|
|
57
|
+
else
|
|
58
|
+
await fs.writeFile(settingsPath, `${JSON.stringify({ ...settings, packages: kept }, null, 2)}\n`, 'utf8');
|
|
53
59
|
}
|
|
60
|
+
catch { /* no settings file, nothing to clean */ }
|
|
61
|
+
await removeUninstallTarget(path.join(agentDir, 'ponytail'), shouldDryRun);
|
|
54
62
|
}
|
|
55
63
|
// pi removal delegates to pi remove; always list and confirm first.
|
|
56
64
|
async function removePiLayer(host, shouldDryRun, shouldRemovePonytail, shouldRemoveRtk, confirmed) {
|
|
57
65
|
const targets = PI_TREE_DIRS.map((dir) => path.join(piAgentDir(), 'extensions', dir));
|
|
58
66
|
const rtkWiring = path.join(piAgentDir(), 'extensions', 'rtk.ts');
|
|
67
|
+
const piPonytail = path.join(piAgentDir(), 'ponytail');
|
|
59
68
|
if (!host.installed && !host.declared) {
|
|
60
69
|
sayTagged(` [skip] ${host.label} — nothing installed (${host.installCmd})`);
|
|
61
70
|
return false;
|
|
@@ -63,8 +72,10 @@ async function removePiLayer(host, shouldDryRun, shouldRemovePonytail, shouldRem
|
|
|
63
72
|
console.log('Will remove:');
|
|
64
73
|
for (const t of targets)
|
|
65
74
|
console.log(` ${t}`);
|
|
66
|
-
if (shouldRemovePonytail)
|
|
75
|
+
if (shouldRemovePonytail) {
|
|
67
76
|
console.log(` npm:${PONYTAIL_PKG} (pi package)`);
|
|
77
|
+
console.log(` ${piPonytail} (local Ponytail package)`);
|
|
78
|
+
}
|
|
68
79
|
if (shouldRemoveRtk)
|
|
69
80
|
console.log(` ${rtkWiring} (rtk wiring)`);
|
|
70
81
|
console.log(` ${tersioSettingsFile()} (session defaults)`);
|
|
@@ -195,12 +206,14 @@ async function runUninstall(options = {}) {
|
|
|
195
206
|
}
|
|
196
207
|
console.log('Will remove:');
|
|
197
208
|
console.log(` ${dir}`);
|
|
209
|
+
console.log(` ${path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts')} (rtk auto-wrap; reinstall with: rtk init -g --opencode)`);
|
|
198
210
|
console.log(` the tersio entry in ${configPath}`);
|
|
199
211
|
if (!confirmed && !(await confirmDestructive('Remove these Tersio files?'))) {
|
|
200
212
|
closeRL();
|
|
201
213
|
return false;
|
|
202
214
|
}
|
|
203
215
|
await removeUninstallTarget(dir, shouldDryRun);
|
|
216
|
+
await removeUninstallTarget(path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts'), shouldDryRun, false);
|
|
204
217
|
if (!shouldDryRun) {
|
|
205
218
|
try {
|
|
206
219
|
const raw = await readTextIfExists(configPath);
|
|
@@ -276,7 +289,8 @@ async function runUninstall(options = {}) {
|
|
|
276
289
|
}
|
|
277
290
|
}
|
|
278
291
|
// Cleared last: writeConfigLines above would just make a fresh one.
|
|
279
|
-
|
|
292
|
+
const cavemanDir = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'extensions', 'caveman-session');
|
|
293
|
+
for (const backup of [`${configPath}.bak`, path.join(cavemanDir, 'rule.md.bak'), path.join(cavemanDir, 'rule-ultra.md.bak'), path.join(cavemanDir, 'rule-wenyan.md.bak')]) {
|
|
280
294
|
await removeUninstallTarget(backup, shouldDryRun, false);
|
|
281
295
|
}
|
|
282
296
|
// Remove the bundled Ponytail copy; its config.yml entry is already filtered.
|
package/cli/update.js
CHANGED
|
@@ -6,7 +6,7 @@ import { IS_WINDOWS, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION
|
|
|
6
6
|
import { execNetwork, sayTagged } from './interactive.js';
|
|
7
7
|
import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/lib/utils.js';
|
|
8
8
|
import { refreshPrices } from '../extensions/shared/pricing.js';
|
|
9
|
-
import {
|
|
9
|
+
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_WENYAN, RTK_RELEASE_API, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/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) {
|
|
@@ -78,20 +78,23 @@ async function execInherit(cmd, args) {
|
|
|
78
78
|
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-session', name);
|
|
82
83
|
const controller = new AbortController();
|
|
83
|
-
const [rtkLocal, rtkRelease,
|
|
84
|
+
const [rtkLocal, rtkRelease, ruleLocal, ruleRemote, ponytailLocalText] = await Promise.all([
|
|
84
85
|
settle(rtkBin ? execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => normalizeRtkVersion(r.stdout.trim() || r.stderr.trim()) || null) : Promise.resolve(null), 6000),
|
|
85
86
|
settle(httpsGet(RTK_RELEASE_API, { signal: controller.signal }).then((text) => normalizeRtkVersion(JSON.parse(text).tag_name) || null), 1500),
|
|
86
|
-
readTextIfExists(tersioRule),
|
|
87
|
-
|
|
87
|
+
Promise.all([readTextIfExists(tersioRule('rule-ultra.md')), readTextIfExists(tersioRule('rule-wenyan.md'))])
|
|
88
|
+
.then((texts) => (texts.every(Boolean) ? texts.map((t) => sha256Hex(t).slice(0, 8)).join('+') : null)),
|
|
89
|
+
settle(Promise.all([httpsGet(CAVEMAN_REMOTE_ULTRA, { signal: controller.signal }), httpsGet(CAVEMAN_REMOTE_WENYAN, { signal: controller.signal })])
|
|
90
|
+
.then((texts) => texts.map((t) => sha256Hex(t).slice(0, 8)).join('+')), 1500),
|
|
88
91
|
readTextIfExists(ponytailPkg),
|
|
89
92
|
]);
|
|
90
93
|
controller.abort();
|
|
91
94
|
return {
|
|
92
95
|
cli: cliLatest,
|
|
93
96
|
rtk: [rtkLocal, rtkRelease],
|
|
94
|
-
rule: [
|
|
97
|
+
rule: [ruleLocal, ruleRemote],
|
|
95
98
|
ponytail: ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null,
|
|
96
99
|
};
|
|
97
100
|
}
|
|
@@ -6,13 +6,18 @@ import os from 'node:os';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { setExtensionLabel } from '../shared/host.js';
|
|
9
|
-
import {
|
|
9
|
+
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_WENYAN, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, isPiProcess, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../lib/utils.js';
|
|
10
10
|
const IS_WINDOWS = process.platform === 'win32';
|
|
11
11
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
|
|
13
13
|
const RTK_BINARY = resolveRtkBinary();
|
|
14
|
-
// Beside this file, not a host path:
|
|
15
|
-
const
|
|
14
|
+
// Beside this file, not a host path: these are the rules /caveman reads.
|
|
15
|
+
const cavemanRule = (name) => path.join(EXTENSION_DIR, '..', 'caveman-session', name);
|
|
16
|
+
const CAVEMAN_LOCAL = cavemanRule('rule.md');
|
|
17
|
+
const CAVEMAN_TRACKED = [
|
|
18
|
+
{ file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
|
|
19
|
+
{ file: cavemanRule('rule-wenyan.md'), remote: CAVEMAN_REMOTE_WENYAN },
|
|
20
|
+
];
|
|
16
21
|
const HOST_NAME = isPiProcess() ? 'Pi' : 'OMP';
|
|
17
22
|
const RELOAD_MSG = `Reminder: restart ${HOST_NAME} (or reload extensions) for updates to take effect.`;
|
|
18
23
|
// Ponytail is a hoisted dependency, so the installed copy is found by walking up rather than by naming a host directory.
|
|
@@ -82,17 +87,20 @@ function checkRtk() {
|
|
|
82
87
|
return `RTK ${status}: local=${localVer || '—'} latest=${latestTag || '—'}`;
|
|
83
88
|
});
|
|
84
89
|
}
|
|
85
|
-
// Caveman (rule.md)
|
|
86
90
|
function checkCaveman() {
|
|
87
91
|
return runCheck('Caveman', async () => {
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
const parts = [(await readTextIfExists(CAVEMAN_LOCAL)) ? 'rule.md ok' : 'rule.md missing'];
|
|
93
|
+
for (const { file, remote } of CAVEMAN_TRACKED) {
|
|
94
|
+
const name = path.basename(file);
|
|
95
|
+
try {
|
|
96
|
+
const [remoteText, localText] = await Promise.all([httpsGet(remote), readTextIfExists(file)]);
|
|
97
|
+
parts.push(!localText ? `${name} missing` : shortHash(localText) === shortHash(remoteText) ? `${name} up to date` : `${name} update available`);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
parts.push(`${name} check skipped`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return `Caveman ${parts.join(', ')}`;
|
|
96
104
|
});
|
|
97
105
|
}
|
|
98
106
|
export async function checkAddonsSummary(ctx) {
|
|
@@ -317,41 +325,49 @@ async function updateRtk(ctx, dryRun = false) {
|
|
|
317
325
|
}
|
|
318
326
|
}
|
|
319
327
|
async function updateCaveman(ctx, dryRun = false) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
328
|
+
const results = [];
|
|
329
|
+
for (const { file, remote } of CAVEMAN_TRACKED) {
|
|
330
|
+
const name = path.basename(file);
|
|
331
|
+
let remoteText;
|
|
332
|
+
try {
|
|
333
|
+
remoteText = await httpsGet(remote);
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
results.push(report(ctx, `Caveman ${name} update failed: ${e.message}`, 'warning'));
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const remoteHash = shortHash(remoteText);
|
|
340
|
+
const oldLocal = await readTextIfExists(file);
|
|
341
|
+
const oldHash = oldLocal ? shortHash(oldLocal) : null;
|
|
342
|
+
if (oldHash === remoteHash) {
|
|
343
|
+
results.push(`${name} up to date: ${remoteHash}`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (dryRun) {
|
|
347
|
+
results.push(`Caveman dry-run: would write ${file}\nold=${oldHash || '—'} new=${remoteHash}.`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
352
|
+
const backupPath = `${file}.bak`;
|
|
343
353
|
if (oldLocal !== null)
|
|
344
|
-
await fs.writeFile(
|
|
345
|
-
|
|
354
|
+
await fs.writeFile(backupPath, oldLocal, 'utf8');
|
|
355
|
+
await fs.writeFile(file, remoteText, 'utf8');
|
|
356
|
+
const writtenHash = shortHash(await fs.readFile(file, 'utf8'));
|
|
357
|
+
if (writtenHash !== remoteHash) {
|
|
358
|
+
if (oldLocal !== null)
|
|
359
|
+
await fs.writeFile(file, oldLocal, 'utf8');
|
|
360
|
+
throw new Error(`written hash ${writtenHash} did not match remote ${remoteHash}${oldLocal !== null ? '; restored backup' : ''}`);
|
|
361
|
+
}
|
|
362
|
+
results.push(`Caveman ${name} updated → ${file}\nold=${oldHash || '—'} new=${remoteHash}\nbackup=${oldLocal !== null ? backupPath : '—'}\n${RELOAD_MSG}`);
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
results.push(`Caveman ${name} update failed: ${e.message}`);
|
|
346
366
|
}
|
|
347
|
-
const m = `Caveman rule.md updated → ${CAVEMAN_LOCAL}\nold=${oldHash || '—'} new=${remoteHash}\nbackup=${oldLocal !== null ? backupPath : '—'}\n${RELOAD_MSG}`;
|
|
348
|
-
notify(ctx, 'Caveman rule.md updated. ' + RELOAD_MSG, 'info');
|
|
349
|
-
return m;
|
|
350
|
-
}
|
|
351
|
-
catch (e) {
|
|
352
|
-
const m = `Caveman update failed: ${e.message}`;
|
|
353
|
-
return report(ctx, m, 'warning');
|
|
354
367
|
}
|
|
368
|
+
const summary = results.join('\n');
|
|
369
|
+
notify(ctx, `Caveman update finished. ${RELOAD_MSG}`, 'info');
|
|
370
|
+
return summary;
|
|
355
371
|
}
|
|
356
372
|
export default function aiAddonsUpdaterExtension(pi) {
|
|
357
373
|
setExtensionLabel(pi, 'AI add-ons updater');
|
|
@@ -9,7 +9,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
|
|
10
10
|
import { setExtensionLabel } from '../shared/host.ts';
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
CAVEMAN_REMOTE_ULTRA,
|
|
13
|
+
CAVEMAN_REMOTE_WENYAN,
|
|
13
14
|
RTK_RELEASE_API,
|
|
14
15
|
RtkRelease,
|
|
15
16
|
fetchJson,
|
|
@@ -31,8 +32,13 @@ const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
31
32
|
|
|
32
33
|
const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
|
|
33
34
|
const RTK_BINARY = resolveRtkBinary();
|
|
34
|
-
// Beside this file, not a host path:
|
|
35
|
-
const
|
|
35
|
+
// Beside this file, not a host path: these are the rules /caveman reads.
|
|
36
|
+
const cavemanRule = (name: string) => path.join(EXTENSION_DIR, '..', 'caveman-session', name);
|
|
37
|
+
const CAVEMAN_LOCAL = cavemanRule('rule.md');
|
|
38
|
+
const CAVEMAN_TRACKED: ReadonlyArray<{ file: string; remote: string }> = [
|
|
39
|
+
{ file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
|
|
40
|
+
{ file: cavemanRule('rule-wenyan.md'), remote: CAVEMAN_REMOTE_WENYAN },
|
|
41
|
+
];
|
|
36
42
|
const HOST_NAME = isPiProcess() ? 'Pi' : 'OMP';
|
|
37
43
|
const RELOAD_MSG = `Reminder: restart ${HOST_NAME} (or reload extensions) for updates to take effect.`;
|
|
38
44
|
|
|
@@ -129,17 +135,19 @@ function checkRtk(): Promise<AddonStatus> {
|
|
|
129
135
|
});
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
// Caveman (rule.md)
|
|
133
138
|
function checkCaveman(): Promise<AddonStatus> {
|
|
134
139
|
return runCheck('Caveman', async () => {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
const parts: string[] = [(await readTextIfExists(CAVEMAN_LOCAL)) ? 'rule.md ok' : 'rule.md missing'];
|
|
141
|
+
for (const { file, remote } of CAVEMAN_TRACKED) {
|
|
142
|
+
const name = path.basename(file);
|
|
143
|
+
try {
|
|
144
|
+
const [remoteText, localText] = await Promise.all([httpsGet(remote), readTextIfExists(file)]);
|
|
145
|
+
parts.push(!localText ? `${name} missing` : shortHash(localText) === shortHash(remoteText) ? `${name} up to date` : `${name} update available`);
|
|
146
|
+
} catch {
|
|
147
|
+
parts.push(`${name} check skipped`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return `Caveman ${parts.join(', ')}`;
|
|
143
151
|
});
|
|
144
152
|
}
|
|
145
153
|
|
|
@@ -352,37 +360,41 @@ async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string>
|
|
|
352
360
|
}
|
|
353
361
|
|
|
354
362
|
async function updateCaveman(ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
363
|
+
const results: string[] = [];
|
|
364
|
+
for (const { file, remote } of CAVEMAN_TRACKED) {
|
|
365
|
+
const name = path.basename(file);
|
|
366
|
+
let remoteText: string;
|
|
367
|
+
try { remoteText = await httpsGet(remote); }
|
|
368
|
+
catch (e) { results.push(report(ctx, `Caveman ${name} update failed: ${(e as Error).message}`, 'warning')); continue; }
|
|
369
|
+
|
|
370
|
+
const remoteHash = shortHash(remoteText);
|
|
371
|
+
const oldLocal = await readTextIfExists(file);
|
|
372
|
+
const oldHash = oldLocal ? shortHash(oldLocal) : null;
|
|
373
|
+
if (oldHash === remoteHash) { results.push(`${name} up to date: ${remoteHash}`); continue; }
|
|
374
|
+
|
|
375
|
+
if (dryRun) {
|
|
376
|
+
results.push(`Caveman dry-run: would write ${file}\nold=${oldHash || '—'} new=${remoteHash}.`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
367
379
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
380
|
+
try {
|
|
381
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
382
|
+
const backupPath = `${file}.bak`;
|
|
383
|
+
if (oldLocal !== null) await fs.writeFile(backupPath, oldLocal, 'utf8');
|
|
384
|
+
await fs.writeFile(file, remoteText, 'utf8');
|
|
385
|
+
const writtenHash = shortHash(await fs.readFile(file, 'utf8'));
|
|
386
|
+
if (writtenHash !== remoteHash) {
|
|
387
|
+
if (oldLocal !== null) await fs.writeFile(file, oldLocal, 'utf8');
|
|
388
|
+
throw new Error(`written hash ${writtenHash} did not match remote ${remoteHash}${oldLocal !== null ? '; restored backup' : ''}`);
|
|
389
|
+
}
|
|
390
|
+
results.push(`Caveman ${name} updated → ${file}\nold=${oldHash || '—'} new=${remoteHash}\nbackup=${oldLocal !== null ? backupPath : '—'}\n${RELOAD_MSG}`);
|
|
391
|
+
} catch (e) {
|
|
392
|
+
results.push(`Caveman ${name} update failed: ${(e as Error).message}`);
|
|
378
393
|
}
|
|
379
|
-
const m = `Caveman rule.md updated → ${CAVEMAN_LOCAL}\nold=${oldHash || '—'} new=${remoteHash}\nbackup=${oldLocal !== null ? backupPath : '—'}\n${RELOAD_MSG}`;
|
|
380
|
-
notify(ctx, 'Caveman rule.md updated. ' + RELOAD_MSG, 'info');
|
|
381
|
-
return m;
|
|
382
|
-
} catch (e) {
|
|
383
|
-
const m = `Caveman update failed: ${(e as Error).message}`;
|
|
384
|
-
return report(ctx, m, 'warning');
|
|
385
394
|
}
|
|
395
|
+
const summary = results.join('\n');
|
|
396
|
+
notify(ctx, `Caveman update finished. ${RELOAD_MSG}`, 'info');
|
|
397
|
+
return summary;
|
|
386
398
|
}
|
|
387
399
|
|
|
388
400
|
export default function aiAddonsUpdaterExtension(pi: AddonUpdaterPi): void {
|
|
@@ -6,20 +6,20 @@ import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host
|
|
|
6
6
|
import { announceStatus } from '../shared/status.js';
|
|
7
7
|
import { readCavemanDefault } from '../shared/plugin-settings.js';
|
|
8
8
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// Upstream ships one skill per register: default, ultracave, megacave.
|
|
10
|
+
const RULE_FILES = { lite: 'rule.md', full: 'rule.md', ultra: 'rule-ultra.md', 'wenyan-lite': 'rule-wenyan.md', 'wenyan-full': 'rule-wenyan.md', 'wenyan-ultra': 'rule-wenyan.md' };
|
|
11
|
+
const DEFAULT_MODE = 'off';
|
|
12
|
+
function buildInstruction(mode) {
|
|
13
|
+
const name = RULE_FILES[mode] ?? RULE_FILES.full;
|
|
14
|
+
// Fail loud: a wrong rule reads to the model as the real one.
|
|
15
|
+
let body;
|
|
12
16
|
try {
|
|
13
|
-
|
|
17
|
+
body = readFileSync(join(CAVERN_DIR, name), 'utf8');
|
|
14
18
|
}
|
|
15
19
|
catch {
|
|
16
|
-
|
|
20
|
+
body = `Caveman ${mode} requested, but ${name} is missing. Run: tersio doctor --fix extensions`;
|
|
17
21
|
}
|
|
18
|
-
}
|
|
19
|
-
const DEFAULT_MODE = 'off';
|
|
20
|
-
// Upstream ships one SKILL.md; the level is a directive inside it, so swap only the header.
|
|
21
|
-
function buildInstruction(mode) {
|
|
22
|
-
return `Caveman ${mode} active for this session.\n${readFullRule()}`;
|
|
22
|
+
return `Caveman ${mode} active for this session.\n${body}`;
|
|
23
23
|
}
|
|
24
24
|
function resolveMode(entries, fallback = DEFAULT_MODE) {
|
|
25
25
|
return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
|
|
@@ -8,20 +8,19 @@ import { readCavemanDefault } from '../shared/plugin-settings.ts';
|
|
|
8
8
|
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
9
9
|
|
|
10
10
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// Fail loud: a wrong rule reads to the model as the real one.
|
|
14
|
-
function readFullRule(): string {
|
|
15
|
-
try { return readFileSync(RULE_PATH, 'utf8'); } catch {
|
|
16
|
-
return 'Caveman full requested, but rule.md is missing. Run: tersio doctor --fix extensions';
|
|
17
|
-
}
|
|
18
|
-
}
|
|
11
|
+
// Upstream ships one skill per register: default, ultracave, megacave.
|
|
12
|
+
const RULE_FILES = { lite: 'rule.md', full: 'rule.md', ultra: 'rule-ultra.md', 'wenyan-lite': 'rule-wenyan.md', 'wenyan-full': 'rule-wenyan.md', 'wenyan-ultra': 'rule-wenyan.md' };
|
|
19
13
|
|
|
20
14
|
const DEFAULT_MODE = 'off';
|
|
21
15
|
|
|
22
|
-
// Upstream ships one SKILL.md; the level is a directive inside it, so swap only the header.
|
|
23
16
|
function buildInstruction(mode: string): string {
|
|
24
|
-
|
|
17
|
+
const name = RULE_FILES[mode as keyof typeof RULE_FILES] ?? RULE_FILES.full;
|
|
18
|
+
// Fail loud: a wrong rule reads to the model as the real one.
|
|
19
|
+
let body: string;
|
|
20
|
+
try { body = readFileSync(join(CAVERN_DIR, name), 'utf8'); } catch {
|
|
21
|
+
body = `Caveman ${mode} requested, but ${name} is missing. Run: tersio doctor --fix extensions`;
|
|
22
|
+
}
|
|
23
|
+
return `Caveman ${mode} active for this session.\n${body}`;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
function resolveMode(entries: SessionEntry[] | null | undefined, fallback: string = DEFAULT_MODE): string {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ultracave
|
|
3
|
+
description: >
|
|
4
|
+
Caveman at maximum compression: fragments, one word when one word is enough,
|
|
5
|
+
each fact once. Invoke only with /ultracave or /caveman ultra. Stays on until
|
|
6
|
+
"stop caveman" or "normal mode".
|
|
7
|
+
disable-model-invocation: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# ultracave
|
|
11
|
+
|
|
12
|
+
Respond terse like smart caveman. All technical substance stay. Only fluff die. Then cut again.
|
|
13
|
+
|
|
14
|
+
Ultracave is caveman with the grammar stripped. Payload only.
|
|
15
|
+
|
|
16
|
+
## Persistence
|
|
17
|
+
|
|
18
|
+
Every response, whole session, until "stop caveman" or "normal mode". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value or say `Caveman mode: unknown`.
|
|
19
|
+
|
|
20
|
+
## Floor
|
|
21
|
+
|
|
22
|
+
Never cut: code, commands, paths, API names, error strings (verbatim). not/never/no/only/except. Numbers and units. The user's language. One term per thing. No invented abbreviations, no arrows.
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
### 1. Fragments
|
|
27
|
+
|
|
28
|
+
Drop articles, copulas, connectives when order stays clear.
|
|
29
|
+
|
|
30
|
+
Bad: "The component re-renders because an inline object prop creates a new reference each render."
|
|
31
|
+
Good: "Inline object prop, new ref, re-render. `useMemo`."
|
|
32
|
+
|
|
33
|
+
### 2. One word when one word is enough
|
|
34
|
+
|
|
35
|
+
Bad: "Yes, that should work, though you'll want a null check first."
|
|
36
|
+
Good: "Yes. Null check first."
|
|
37
|
+
|
|
38
|
+
### 3. Each fact once
|
|
39
|
+
|
|
40
|
+
No restating, no summary after a list.
|
|
41
|
+
|
|
42
|
+
Good: "Pool reuses open DB connections. No per-request handshake."
|
|
43
|
+
|
|
44
|
+
### 4. Cut conjunctions only when order survives
|
|
45
|
+
|
|
46
|
+
Bad: "Migrate table drop column backup first."
|
|
47
|
+
Good: "Back up first. Then migrate: drops column."
|
|
48
|
+
|
|
49
|
+
### 5. Tool runs
|
|
50
|
+
|
|
51
|
+
One line in, one line out. Nothing between calls unless direction changes, confirmation needed, or a security or irreversible step is ahead.
|
|
52
|
+
|
|
53
|
+
### 6. Never perform
|
|
54
|
+
|
|
55
|
+
No prefix, no announcement, no mangled verbs for flavor. Fragment not shorter than the sentence? Use the sentence.
|
|
56
|
+
|
|
57
|
+
## When to break the rules
|
|
58
|
+
|
|
59
|
+
Plain prose, then resume: security warning. Irreversible action, confirm first. Any fragment with two readings. User confused. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.
|
|
60
|
+
|
|
61
|
+
## Pre-send check
|
|
62
|
+
|
|
63
|
+
Two readings? Full sentence. Negations present? Payload verbatim? Flavor word? Cut.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: megacave
|
|
3
|
+
description: >
|
|
4
|
+
Caveman in Classical Chinese: 文言文 register, far fewer characters, technical
|
|
5
|
+
terms verbatim. Invoke only with /megacave or /caveman wenyan. Stays on until
|
|
6
|
+
"stop caveman" or "normal mode".
|
|
7
|
+
disable-model-invocation: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# megacave
|
|
11
|
+
|
|
12
|
+
以文言答。技術之實皆存,唯贅言去之。
|
|
13
|
+
|
|
14
|
+
Megacave is caveman in Classical Chinese. 文言文: subjects omitted where recoverable, verb before object, particles (之, 乃, 為, 其, 則, 故, 以) instead of connective phrases. Fewer characters on screen. Token savings are small and noisy; never claim them.
|
|
15
|
+
|
|
16
|
+
## Persistence
|
|
17
|
+
|
|
18
|
+
Every response, whole session, until "stop caveman" or "normal mode". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value or say `Caveman mode: unknown`.
|
|
19
|
+
|
|
20
|
+
## Floor
|
|
21
|
+
|
|
22
|
+
Never cut or translate: code, commands, paths, API names, error strings, in their original script. Negation (不, 非, 勿, 未, 毋). Numbers and units, Arabic numerals. One term per thing. A clause ambiguous in 文言 becomes 白話.
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
### 1. Classical register
|
|
27
|
+
|
|
28
|
+
文言, not 白話. Verb before object. Subjects omitted where recoverable.
|
|
29
|
+
|
|
30
|
+
Bad: "你的組件每次渲染都會重新渲染,因為你每次都創建了新的對象引用。請用 useMemo 包起來。"
|
|
31
|
+
Good: "每繪新生對象參照,故重繪;以 `useMemo` 包之則免。"
|
|
32
|
+
|
|
33
|
+
### 2. Particles carry structure
|
|
34
|
+
|
|
35
|
+
One particle, one relation.
|
|
36
|
+
|
|
37
|
+
Bad: "因為連接池會重複使用已經打開的連接,所以不需要每次請求都新建連接,這樣就省去了握手的開銷。"
|
|
38
|
+
Good: "池蓄已開之連,不逐請而新開,省握手之費。"
|
|
39
|
+
|
|
40
|
+
### 3. Each fact once
|
|
41
|
+
|
|
42
|
+
Good: "新參照則重繪。`useMemo` 包之。"
|
|
43
|
+
|
|
44
|
+
### 4. Payload in original script
|
|
45
|
+
|
|
46
|
+
Bad: "以記憶化鉤子包之。"
|
|
47
|
+
Good: "以 `useMemo` 包之。"
|
|
48
|
+
|
|
49
|
+
### 5. Tool runs
|
|
50
|
+
|
|
51
|
+
One 文言 line in, one out. Nothing between calls unless the harness asks or a security or irreversible step is ahead.
|
|
52
|
+
|
|
53
|
+
### 6. Never perform
|
|
54
|
+
|
|
55
|
+
No "文言模式啟", no modern answer plus classical copy. 文言 not shorter than 白話 for a sentence? Use 白話.
|
|
56
|
+
|
|
57
|
+
## When to break the rules
|
|
58
|
+
|
|
59
|
+
白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirm first. Any clause with two readings. User confused. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.
|
|
60
|
+
|
|
61
|
+
## Pre-send check
|
|
62
|
+
|
|
63
|
+
Identifiers verbatim, original script? Negations present? Two readings? 白話. Flavor character? Cut.
|
|
@@ -43,27 +43,14 @@ Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
|
|
43
43
|
|-------|------------|
|
|
44
44
|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
|
|
45
45
|
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations |
|
|
46
|
-
| **ultra** | Strip conjunctions when cause-then-effect stay unambiguous. One word when one word enough. State each fact once. NO prose abbreviations (cfg/impl/req/res/fn/auth), NO arrows (X → Y) measured zero token saving under tokenizer, cost decode clarity. Code symbols, function names, API names, error strings: never touch |
|
|
47
|
-
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
|
|
48
|
-
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction chars, not tokens. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
|
|
49
|
-
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
|
|
50
46
|
|
|
51
47
|
Example "Why React component re-render?"
|
|
52
48
|
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
|
53
49
|
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
|
54
|
-
- ultra: "Inline obj prop, new ref, re-render. `useMemo`."
|
|
55
|
-
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
|
|
56
|
-
- wenyan-full: "每繪新生對象參照,故重繪;以 useMemo 包之則免。"
|
|
57
|
-
- wenyan-ultra: "新參照則重繪。useMemo 包之。"
|
|
58
50
|
|
|
59
51
|
Example "Explain database connection pooling."
|
|
60
52
|
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
|
|
61
53
|
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
|
|
62
|
-
- ultra: "Pool reuse open DB connections. No per-request handshake."
|
|
63
|
-
- wenyan-full: "池蓄已開之連,不逐請而新開,省握手之費。"
|
|
64
|
-
- wenyan-ultra: "池蓄連,免逐請新開,省握手。"
|
|
65
|
-
|
|
66
|
-
Classical chars = wenyan modes only. Never swap a word to a classical char to shrink at non-wenyan levels.
|
|
67
54
|
|
|
68
55
|
## Auto-Clarity
|
|
69
56
|
|
package/extensions/lib/utils.js
CHANGED
|
@@ -18,7 +18,8 @@ export function homeDir() {
|
|
|
18
18
|
return home && home.trim() !== '' ? home : os.homedir();
|
|
19
19
|
}
|
|
20
20
|
export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
|
|
21
|
-
export const
|
|
21
|
+
export const CAVEMAN_REMOTE_ULTRA = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/ultracave/SKILL.md';
|
|
22
|
+
export const CAVEMAN_REMOTE_WENYAN = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/megacave/SKILL.md';
|
|
22
23
|
const RTK_PLATFORM_SPECS = {
|
|
23
24
|
'win32/x64': { triple: 'x86_64-pc-windows-msvc', ext: '.zip', binary: 'rtk.exe' },
|
|
24
25
|
'linux/x64': { triple: 'x86_64-unknown-linux-musl', ext: '.tar.gz', binary: 'rtk' },
|
package/extensions/lib/utils.ts
CHANGED
|
@@ -27,7 +27,8 @@ export function homeDir(): string {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
|
|
30
|
-
export const
|
|
30
|
+
export const CAVEMAN_REMOTE_ULTRA = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/ultracave/SKILL.md';
|
|
31
|
+
export const CAVEMAN_REMOTE_WENYAN = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/megacave/SKILL.md';
|
|
31
32
|
|
|
32
33
|
export interface RtkPlatformSpec {
|
|
33
34
|
triple: string;
|
|
@@ -6,10 +6,10 @@ import comboToggleExtension from '../combo-toggle/index.js';
|
|
|
6
6
|
import tersioCommandsExtension from '../tersio-commands/index.js';
|
|
7
7
|
import { getSharedComboState } from '../shared/session-state.js';
|
|
8
8
|
import { formatStatus } from '../shared/status.js';
|
|
9
|
-
|
|
10
|
-
//
|
|
9
|
+
import { resolveRtkBinary } from '../lib/utils.js';
|
|
10
|
+
// The npm name, so the plugin list reads the same as the OMP and pi installs.
|
|
11
11
|
const plugin = {
|
|
12
|
-
id: 'tersio',
|
|
12
|
+
id: '@krtclcdy/tersio',
|
|
13
13
|
async setup(ctx) {
|
|
14
14
|
// Entries are scoped per session; an unseen ID restores on its first hook.
|
|
15
15
|
const sessions = new Map();
|
|
@@ -20,7 +20,10 @@ const plugin = {
|
|
|
20
20
|
if (own.length)
|
|
21
21
|
return own;
|
|
22
22
|
// Pre-scoped installs kept one global list; adopt it once, then diverge.
|
|
23
|
+
// Clear it after adoption so it can never shadow saved defaults later.
|
|
23
24
|
const legacy = (await ctx.storage.get('entries')) ?? [];
|
|
25
|
+
if (legacy.length)
|
|
26
|
+
void ctx.storage.set('entries', []).catch(() => { });
|
|
24
27
|
return [...legacy];
|
|
25
28
|
};
|
|
26
29
|
const sessionEntriesOf = (sid) => {
|
|
@@ -185,6 +188,33 @@ const plugin = {
|
|
|
185
188
|
for (const handler of inputHandlers)
|
|
186
189
|
void handler({ text: event.prompt.text, source: 'interactive' });
|
|
187
190
|
});
|
|
191
|
+
// rtk has its own OpenCode plugin; doing this here keeps one entry in the plugin list.
|
|
192
|
+
await ctx.tool.hook('execute.before', async (event) => {
|
|
193
|
+
const tool = String(event.tool ?? '').toLowerCase();
|
|
194
|
+
if (tool !== 'bash' && tool !== 'shell')
|
|
195
|
+
return;
|
|
196
|
+
const input = event.input;
|
|
197
|
+
if (typeof input?.command !== 'string' || !input.command)
|
|
198
|
+
return;
|
|
199
|
+
const rewritten = await rewriteCommand(input.command);
|
|
200
|
+
if (rewritten)
|
|
201
|
+
input.command = rewritten;
|
|
202
|
+
});
|
|
188
203
|
},
|
|
189
204
|
};
|
|
205
|
+
function rewriteCommand(command) {
|
|
206
|
+
const bin = resolveRtkBinary();
|
|
207
|
+
if (!bin)
|
|
208
|
+
return Promise.resolve(null);
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
execFile(bin, ['rewrite', command], { timeout: 5000 }, (error, stdout) => {
|
|
211
|
+
// A kill or timeout means untrustworthy output; pass the command through.
|
|
212
|
+
const err = error;
|
|
213
|
+
if (err && (err.killed || err.signal))
|
|
214
|
+
return resolve(null);
|
|
215
|
+
const out = String(stdout).trim();
|
|
216
|
+
resolve(out && out !== command ? out : null);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
190
220
|
export default plugin;
|
|
@@ -7,6 +7,7 @@ import comboToggleExtension from '../combo-toggle/index.ts';
|
|
|
7
7
|
import tersioCommandsExtension from '../tersio-commands/index.ts';
|
|
8
8
|
import { getSharedComboState } from '../shared/session-state.ts';
|
|
9
9
|
import { formatStatus } from '../shared/status.ts';
|
|
10
|
+
import { resolveRtkBinary } from '../lib/utils.ts';
|
|
10
11
|
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
11
12
|
|
|
12
13
|
interface CommandConfig {
|
|
@@ -27,10 +28,9 @@ interface ToolConfig {
|
|
|
27
28
|
) => Promise<{ isError: boolean; content: { type: string; text: string }[]; details: Record<string, unknown> }>;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
//
|
|
31
|
-
// import would force every install to vendor the whole @opencode dependency tree.
|
|
31
|
+
// The npm name, so the plugin list reads the same as the OMP and pi installs.
|
|
32
32
|
const plugin: Plugin.Plugin = {
|
|
33
|
-
id: 'tersio',
|
|
33
|
+
id: '@krtclcdy/tersio',
|
|
34
34
|
async setup(ctx) {
|
|
35
35
|
// Entries are scoped per session; an unseen ID restores on its first hook.
|
|
36
36
|
const sessions = new Map<string, SessionEntry[]>();
|
|
@@ -40,7 +40,9 @@ const plugin: Plugin.Plugin = {
|
|
|
40
40
|
const own = ((await ctx.storage.get(`entries:${sid}`)) as SessionEntry[] | undefined) ?? [];
|
|
41
41
|
if (own.length) return own;
|
|
42
42
|
// Pre-scoped installs kept one global list; adopt it once, then diverge.
|
|
43
|
+
// Clear it after adoption so it can never shadow saved defaults later.
|
|
43
44
|
const legacy = ((await ctx.storage.get('entries')) as SessionEntry[] | undefined) ?? [];
|
|
45
|
+
if (legacy.length) void ctx.storage.set('entries', []).catch(() => {});
|
|
44
46
|
return [...legacy];
|
|
45
47
|
};
|
|
46
48
|
const sessionEntriesOf = (sid: string): SessionEntry[] => {
|
|
@@ -194,7 +196,31 @@ const plugin: Plugin.Plugin = {
|
|
|
194
196
|
if (sid) await ensureSession(sid);
|
|
195
197
|
for (const handler of inputHandlers) void handler({ text: event.prompt.text, source: 'interactive' });
|
|
196
198
|
});
|
|
199
|
+
|
|
200
|
+
// rtk has its own OpenCode plugin; doing this here keeps one entry in the plugin list.
|
|
201
|
+
await ctx.tool.hook('execute.before', async (event) => {
|
|
202
|
+
const tool = String(event.tool ?? '').toLowerCase();
|
|
203
|
+
if (tool !== 'bash' && tool !== 'shell') return;
|
|
204
|
+
const input = event.input as { command?: unknown } | null | undefined;
|
|
205
|
+
if (typeof input?.command !== 'string' || !input.command) return;
|
|
206
|
+
const rewritten = await rewriteCommand(input.command);
|
|
207
|
+
if (rewritten) input.command = rewritten;
|
|
208
|
+
});
|
|
197
209
|
},
|
|
198
210
|
};
|
|
199
211
|
|
|
212
|
+
function rewriteCommand(command: string): Promise<string | null> {
|
|
213
|
+
const bin = resolveRtkBinary();
|
|
214
|
+
if (!bin) return Promise.resolve(null);
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
execFile(bin, ['rewrite', command], { timeout: 5000 }, (error, stdout) => {
|
|
217
|
+
// A kill or timeout means untrustworthy output; pass the command through.
|
|
218
|
+
const err = error as { killed?: boolean; signal?: unknown } | null;
|
|
219
|
+
if (err && (err.killed || err.signal)) return resolve(null);
|
|
220
|
+
const out = String(stdout).trim();
|
|
221
|
+
resolve(out && out !== command ? out : null);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
200
226
|
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krtclcdy/tersio",
|
|
3
|
-
"version": "2.25.
|
|
3
|
+
"version": "2.25.2",
|
|
4
4
|
"description": "✂️ Token saver and usage tracker for coding agents: terse Caveman replies, compact RTK shell output, lean Ponytail code calls, and a gain dashboard — one-command combo presets over one shared extension tree.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"token-saving",
|
|
@@ -119,7 +119,9 @@
|
|
|
119
119
|
"dashboard/dist/**/*",
|
|
120
120
|
"extensions/**/*.js",
|
|
121
121
|
"extensions/**/*.ts",
|
|
122
|
-
"extensions/caveman-session/rule.md"
|
|
122
|
+
"extensions/caveman-session/rule.md",
|
|
123
|
+
"extensions/caveman-session/rule-ultra.md",
|
|
124
|
+
"extensions/caveman-session/rule-wenyan.md"
|
|
123
125
|
],
|
|
124
126
|
"scripts": {
|
|
125
127
|
"clean": "rm -f tersio.js cli/*.js extensions/**/*.js && rm -rf dashboard/dist",
|