@krtclcdy/tersio 2.24.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -24
- package/cli/banner.js +7 -5
- package/cli/common.js +14 -13
- package/cli/currency.js +2 -7
- package/cli/dashboard.js +220 -13
- package/cli/doctor-fix.js +99 -29
- package/cli/doctor.js +32 -35
- package/cli/hosts.js +23 -26
- package/cli/install.js +228 -115
- package/cli/interactive.js +65 -21
- package/cli/manifest.js +8 -4
- package/cli/profile.js +49 -16
- package/cli/reset.js +3 -7
- package/cli/rtk-wiring.js +29 -32
- package/cli/settings.js +40 -16
- package/cli/uninstall.js +79 -29
- package/cli/update.js +27 -20
- package/cli/usage.js +54 -19
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +75 -20
- package/extensions/ai-addons-updater/index.ts +67 -23
- package/extensions/caveman-session/index.js +33 -75
- package/extensions/caveman-session/index.ts +35 -77
- package/extensions/combo-toggle/index.js +40 -62
- package/extensions/combo-toggle/index.ts +38 -72
- package/extensions/lib/utils.js +64 -15
- package/extensions/lib/utils.ts +62 -15
- package/extensions/opencode/index.js +104 -0
- package/extensions/opencode/server.js +190 -0
- package/extensions/opencode/server.ts +200 -0
- package/extensions/rtk-session/index.js +38 -57
- package/extensions/rtk-session/index.ts +36 -55
- package/extensions/shared/carbon.js +4 -10
- package/extensions/shared/carbon.ts +5 -10
- package/extensions/shared/host.js +95 -56
- package/extensions/shared/host.ts +106 -60
- package/extensions/shared/plugin-settings.js +6 -18
- package/extensions/shared/plugin-settings.ts +6 -18
- package/extensions/shared/pricing.js +79 -27
- package/extensions/shared/pricing.ts +80 -28
- package/extensions/shared/rtk-gain.js +4 -11
- package/extensions/shared/rtk-gain.ts +4 -11
- package/extensions/shared/session-state.js +28 -39
- package/extensions/shared/session-state.ts +29 -38
- package/extensions/shared/status.js +38 -0
- package/extensions/shared/status.ts +45 -0
- package/extensions/shared/types.js +1 -2
- package/extensions/shared/types.ts +2 -2
- package/extensions/shared/usage-ledger.js +312 -57
- package/extensions/shared/usage-ledger.ts +321 -61
- package/extensions/shared/usage-store.js +354 -38
- package/extensions/shared/usage-store.ts +360 -43
- package/extensions/tersio-commands/index.js +5 -14
- package/extensions/tersio-commands/index.ts +5 -14
- package/package.json +12 -7
- package/tersio.js +4 -6
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -24
- package/cli/omp-layer.js +0 -51
- package/cli/pi-layer.js +0 -37
- package/cli/pi-wiring.js +0 -92
- package/extensions/shared/omp-prompt.js +0 -12
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// /ai-addons manual updater. Node built-ins only. RTK ships SHA256 checksums
|
|
2
|
-
// with no signature, so checksum-only is the best available.
|
|
1
|
+
// /ai-addons manual updater. Node built-ins only. RTK ships SHA256 checksums with no signature, so checksum-only is the best available.
|
|
3
2
|
import { execFileSync } from 'node:child_process';
|
|
4
3
|
import { createHash } from 'node:crypto';
|
|
5
4
|
import fs from 'node:fs/promises';
|
|
@@ -16,8 +15,7 @@ const RTK_BINARY = resolveRtkBinary();
|
|
|
16
15
|
const CAVEMAN_LOCAL = path.join(EXTENSION_DIR, '..', 'caveman-session', 'rule.md');
|
|
17
16
|
const HOST_NAME = isPiProcess() ? 'Pi' : 'OMP';
|
|
18
17
|
const RELOAD_MSG = `Reminder: restart ${HOST_NAME} (or reload extensions) for updates to take effect.`;
|
|
19
|
-
// Ponytail is a hoisted dependency, so the installed copy is found by walking
|
|
20
|
-
// up rather than by naming a host directory.
|
|
18
|
+
// Ponytail is a hoisted dependency, so the installed copy is found by walking up rather than by naming a host directory.
|
|
21
19
|
function ponytailLocal() {
|
|
22
20
|
return findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'package.json');
|
|
23
21
|
}
|
|
@@ -133,20 +131,76 @@ async function checkAddons(ctx) {
|
|
|
133
131
|
}
|
|
134
132
|
return lines.join('\n');
|
|
135
133
|
}
|
|
134
|
+
// Ponytail is a hoisted dependency; replace its package dir from the npm tarball.
|
|
136
135
|
async function updatePonytail(pi, ctx, dryRun = false) {
|
|
137
|
-
|
|
136
|
+
void pi;
|
|
137
|
+
const localPkg = ponytailLocal();
|
|
138
138
|
let localVer = null;
|
|
139
139
|
try {
|
|
140
|
-
localVer = parsePackageVersion(await readTextIfExists(
|
|
140
|
+
localVer = localPkg ? parsePackageVersion(await readTextIfExists(localPkg)) : null;
|
|
141
141
|
}
|
|
142
142
|
catch {
|
|
143
143
|
localVer = null;
|
|
144
144
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
if (!localPkg)
|
|
146
|
+
return report(ctx, 'Ponytail: package.json not found; reinstall with `tersio update`.', 'warning');
|
|
147
|
+
let meta;
|
|
148
|
+
try {
|
|
149
|
+
meta = await fetchJson('https://registry.npmjs.org/@dietrichgebert/ponytail/latest');
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return report(ctx, `Ponytail: cannot fetch npm metadata: ${e.message}`, 'warning');
|
|
153
|
+
}
|
|
154
|
+
const remoteVer = meta.version;
|
|
155
|
+
if (!remoteVer)
|
|
156
|
+
return report(ctx, 'Ponytail: npm metadata has no version.', 'warning');
|
|
157
|
+
if (localVer === remoteVer)
|
|
158
|
+
return report(ctx, `Ponytail up to date: local=${localVer} latest=${remoteVer}`, 'info');
|
|
159
|
+
if (!meta.dist?.tarball)
|
|
160
|
+
return report(ctx, 'Ponytail: npm metadata has no tarball.', 'warning');
|
|
161
|
+
if (dryRun)
|
|
162
|
+
return report(ctx, `Ponytail dry-run: would install ${remoteVer} over ${localVer || '—'} at ${path.dirname(localPkg)}.`, 'info');
|
|
163
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'ponytail-update-'));
|
|
164
|
+
try {
|
|
165
|
+
const tarballPath = path.join(tmp, 'pkg.tgz');
|
|
166
|
+
notify(ctx, `Ponytail: downloading ${remoteVer}…`, 'info');
|
|
167
|
+
await httpsDownload(meta.dist.tarball, tarballPath);
|
|
168
|
+
if (meta.dist.shasum) {
|
|
169
|
+
const actual = createHash('sha1').update(await fs.readFile(tarballPath)).digest('hex');
|
|
170
|
+
if (actual !== meta.dist.shasum)
|
|
171
|
+
return report(ctx, `Ponytail: sha1 mismatch; expected ${meta.dist.shasum.slice(0, 12)}…, got ${actual.slice(0, 12)}…; not installing`, 'error');
|
|
172
|
+
}
|
|
173
|
+
const extractDir = path.join(tmp, 'out');
|
|
174
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
175
|
+
execFileSync('tar', ['xzf', tarballPath, '-C', extractDir], { encoding: 'utf8', shell: false });
|
|
176
|
+
const newPkgJson = await findFile(extractDir, 'package.json');
|
|
177
|
+
if (!newPkgJson)
|
|
178
|
+
return report(ctx, 'Ponytail: package.json not in tarball.', 'warning');
|
|
179
|
+
const newDir = path.dirname(newPkgJson);
|
|
180
|
+
const destDir = path.dirname(localPkg);
|
|
181
|
+
const backupDir = `${destDir}.bak`;
|
|
182
|
+
// fs.rename cannot cross filesystems (tmp vs home), so copy then verify.
|
|
183
|
+
await fs.rename(destDir, backupDir);
|
|
184
|
+
try {
|
|
185
|
+
await fs.cp(newDir, destDir, { recursive: true });
|
|
186
|
+
const written = parsePackageVersion(await readTextIfExists(path.join(destDir, 'package.json')));
|
|
187
|
+
if (written !== remoteVer)
|
|
188
|
+
throw new Error(`installed version ${written} !== ${remoteVer}`);
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
await fs.rm(destDir, { recursive: true, force: true }).catch(() => { });
|
|
192
|
+
await fs.rename(backupDir, destDir).catch(() => { });
|
|
193
|
+
throw new Error(`${e.message}; restored backup`);
|
|
194
|
+
}
|
|
195
|
+
await fs.rm(backupDir, { recursive: true, force: true }).catch(() => { });
|
|
196
|
+
return report(ctx, `Ponytail updated → ${remoteVer} at ${destDir}\n${RELOAD_MSG}`, 'info');
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
return report(ctx, `Ponytail update failed: ${e.message}`, 'warning');
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
fs.rm(tmp, { recursive: true, force: true }).catch(() => { });
|
|
203
|
+
}
|
|
150
204
|
}
|
|
151
205
|
async function updateRtk(ctx, dryRun = false) {
|
|
152
206
|
let release;
|
|
@@ -192,14 +246,14 @@ async function updateRtk(ctx, dryRun = false) {
|
|
|
192
246
|
const checks = await fs.readFile(checksPath, 'utf8');
|
|
193
247
|
const expected = parseChecksum(checks, asset.name);
|
|
194
248
|
if (!expected) {
|
|
195
|
-
const m = `RTK: checksums.txt has no entry for ${asset.name}`;
|
|
196
|
-
return report(ctx, m, '
|
|
249
|
+
const m = `RTK: checksums.txt has no entry for ${asset.name}; not installing`;
|
|
250
|
+
return report(ctx, m, 'error');
|
|
197
251
|
}
|
|
198
252
|
const archiveBuf = await fs.readFile(archivePath);
|
|
199
253
|
const actual = createHash('sha256').update(archiveBuf).digest('hex').toLowerCase();
|
|
200
254
|
if (actual !== expected) {
|
|
201
|
-
const m = `RTK: checksum mismatch! expected=${expected.slice(0, 12)}… actual=${actual.slice(0, 12)}
|
|
202
|
-
return report(ctx, m, '
|
|
255
|
+
const m = `RTK: checksum mismatch! expected=${expected.slice(0, 12)}… actual=${actual.slice(0, 12)}…; not installing`;
|
|
256
|
+
return report(ctx, m, 'error');
|
|
203
257
|
}
|
|
204
258
|
notify(ctx, 'RTK: checksum verified.', 'info');
|
|
205
259
|
const extractDir = path.join(tmp, 'extracted');
|
|
@@ -310,15 +364,16 @@ export default function aiAddonsUpdaterExtension(pi) {
|
|
|
310
364
|
const cleanParts = parts.filter((p) => p !== '--dry-run' && p !== 'dry-run');
|
|
311
365
|
const sub = cleanParts[0];
|
|
312
366
|
if (sub === 'check' || sub === 'status') {
|
|
313
|
-
|
|
367
|
+
await checkAddons(ctx);
|
|
314
368
|
notify(ctx, 'ai-addons check complete.', 'info');
|
|
315
|
-
return
|
|
369
|
+
return;
|
|
316
370
|
}
|
|
317
371
|
if (sub === 'update' && cleanParts[1]) {
|
|
318
|
-
|
|
372
|
+
await runAddonUpdate(pi, ctx, cleanParts.slice(1).join(' '), dryRun);
|
|
373
|
+
return;
|
|
319
374
|
}
|
|
320
|
-
|
|
321
|
-
return
|
|
375
|
+
report(ctx, 'Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]', 'warning');
|
|
376
|
+
return;
|
|
322
377
|
},
|
|
323
378
|
});
|
|
324
379
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// /ai-addons manual updater. Node built-ins only. RTK ships SHA256 checksums
|
|
2
|
-
// with no signature, so checksum-only is the best available.
|
|
1
|
+
// /ai-addons manual updater. Node built-ins only. RTK ships SHA256 checksums with no signature, so checksum-only is the best available.
|
|
3
2
|
|
|
4
3
|
import { execFileSync } from 'node:child_process';
|
|
5
4
|
import { createHash } from 'node:crypto';
|
|
@@ -37,8 +36,7 @@ const CAVEMAN_LOCAL = path.join(EXTENSION_DIR, '..', 'caveman-session', 'rule.md
|
|
|
37
36
|
const HOST_NAME = isPiProcess() ? 'Pi' : 'OMP';
|
|
38
37
|
const RELOAD_MSG = `Reminder: restart ${HOST_NAME} (or reload extensions) for updates to take effect.`;
|
|
39
38
|
|
|
40
|
-
// Ponytail is a hoisted dependency, so the installed copy is found by walking
|
|
41
|
-
// up rather than by naming a host directory.
|
|
39
|
+
// Ponytail is a hoisted dependency, so the installed copy is found by walking up rather than by naming a host directory.
|
|
42
40
|
function ponytailLocal(): string | null {
|
|
43
41
|
return findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'package.json');
|
|
44
42
|
}
|
|
@@ -54,12 +52,12 @@ interface AddonUpdaterCtx {
|
|
|
54
52
|
|
|
55
53
|
interface AddonUpdaterPi {
|
|
56
54
|
setLabel?: (label: string) => void;
|
|
57
|
-
registerCommand?: (name: string, config: { description: string; handler: (args: string, ctx: AddonUpdaterCtx) => Promise<
|
|
55
|
+
registerCommand?: (name: string, config: { description: string; handler: (args: string, ctx: AddonUpdaterCtx) => Promise<void> }) => void;
|
|
58
56
|
exec?: (cmd: string, args: string[], opts?: { cwd?: string }) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
59
57
|
cwd?: string;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
type NotifyLevel = 'info' | 'warning';
|
|
60
|
+
type NotifyLevel = 'info' | 'warning' | 'error';
|
|
63
61
|
|
|
64
62
|
function notify(ctx: AddonUpdaterCtx | undefined, msg: string, level: NotifyLevel): void {
|
|
65
63
|
ctx?.ui?.notify?.(String(msg), level);
|
|
@@ -180,17 +178,62 @@ async function checkAddons(ctx: AddonUpdaterCtx): Promise<string> {
|
|
|
180
178
|
return lines.join('\n');
|
|
181
179
|
}
|
|
182
180
|
|
|
181
|
+
// Ponytail is a hoisted dependency; replace its package dir from the npm tarball.
|
|
183
182
|
async function updatePonytail(pi: AddonUpdaterPi, ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
|
|
184
|
-
|
|
183
|
+
void pi;
|
|
184
|
+
const localPkg = ponytailLocal();
|
|
185
185
|
let localVer: string | null = null;
|
|
186
|
+
try { localVer = localPkg ? parsePackageVersion(await readTextIfExists(localPkg)) : null; } catch { localVer = null; }
|
|
187
|
+
if (!localPkg) return report(ctx, 'Ponytail: package.json not found; reinstall with `tersio update`.', 'warning');
|
|
188
|
+
|
|
189
|
+
let meta: { version?: string; dist?: { tarball?: string; shasum?: string } };
|
|
186
190
|
try {
|
|
187
|
-
|
|
188
|
-
} catch {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return report(ctx,
|
|
191
|
+
meta = await fetchJson('https://registry.npmjs.org/@dietrichgebert/ponytail/latest');
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return report(ctx, `Ponytail: cannot fetch npm metadata: ${(e as Error).message}`, 'warning');
|
|
194
|
+
}
|
|
195
|
+
const remoteVer = meta.version;
|
|
196
|
+
if (!remoteVer) return report(ctx, 'Ponytail: npm metadata has no version.', 'warning');
|
|
197
|
+
if (localVer === remoteVer) return report(ctx, `Ponytail up to date: local=${localVer} latest=${remoteVer}`, 'info');
|
|
198
|
+
if (!meta.dist?.tarball) return report(ctx, 'Ponytail: npm metadata has no tarball.', 'warning');
|
|
199
|
+
|
|
200
|
+
if (dryRun) return report(ctx, `Ponytail dry-run: would install ${remoteVer} over ${localVer || '—'} at ${path.dirname(localPkg)}.`, 'info');
|
|
201
|
+
|
|
202
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'ponytail-update-'));
|
|
203
|
+
try {
|
|
204
|
+
const tarballPath = path.join(tmp, 'pkg.tgz');
|
|
205
|
+
notify(ctx, `Ponytail: downloading ${remoteVer}…`, 'info');
|
|
206
|
+
await httpsDownload(meta.dist.tarball, tarballPath);
|
|
207
|
+
if (meta.dist.shasum) {
|
|
208
|
+
const actual = createHash('sha1').update(await fs.readFile(tarballPath)).digest('hex');
|
|
209
|
+
if (actual !== meta.dist.shasum) return report(ctx, `Ponytail: sha1 mismatch; expected ${meta.dist.shasum.slice(0, 12)}…, got ${actual.slice(0, 12)}…; not installing`, 'error');
|
|
210
|
+
}
|
|
211
|
+
const extractDir = path.join(tmp, 'out');
|
|
212
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
213
|
+
execFileSync('tar', ['xzf', tarballPath, '-C', extractDir], { encoding: 'utf8', shell: false });
|
|
214
|
+
const newPkgJson = await findFile(extractDir, 'package.json');
|
|
215
|
+
if (!newPkgJson) return report(ctx, 'Ponytail: package.json not in tarball.', 'warning');
|
|
216
|
+
const newDir = path.dirname(newPkgJson);
|
|
217
|
+
const destDir = path.dirname(localPkg);
|
|
218
|
+
const backupDir = `${destDir}.bak`;
|
|
219
|
+
// fs.rename cannot cross filesystems (tmp vs home), so copy then verify.
|
|
220
|
+
await fs.rename(destDir, backupDir);
|
|
221
|
+
try {
|
|
222
|
+
await fs.cp(newDir, destDir, { recursive: true });
|
|
223
|
+
const written = parsePackageVersion(await readTextIfExists(path.join(destDir, 'package.json')));
|
|
224
|
+
if (written !== remoteVer) throw new Error(`installed version ${written} !== ${remoteVer}`);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
await fs.rm(destDir, { recursive: true, force: true }).catch(() => { });
|
|
227
|
+
await fs.rename(backupDir, destDir).catch(() => { });
|
|
228
|
+
throw new Error(`${(e as Error).message}; restored backup`);
|
|
229
|
+
}
|
|
230
|
+
await fs.rm(backupDir, { recursive: true, force: true }).catch(() => { });
|
|
231
|
+
return report(ctx, `Ponytail updated → ${remoteVer} at ${destDir}\n${RELOAD_MSG}`, 'info');
|
|
232
|
+
} catch (e) {
|
|
233
|
+
return report(ctx, `Ponytail update failed: ${(e as Error).message}`, 'warning');
|
|
234
|
+
} finally {
|
|
235
|
+
fs.rm(tmp, { recursive: true, force: true }).catch(() => { });
|
|
236
|
+
}
|
|
194
237
|
}
|
|
195
238
|
|
|
196
239
|
async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
|
|
@@ -240,14 +283,14 @@ async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string>
|
|
|
240
283
|
const checks = await fs.readFile(checksPath, 'utf8');
|
|
241
284
|
const expected = parseChecksum(checks, asset.name);
|
|
242
285
|
if (!expected) {
|
|
243
|
-
const m = `RTK: checksums.txt has no entry for ${asset.name}`;
|
|
244
|
-
return report(ctx, m, '
|
|
286
|
+
const m = `RTK: checksums.txt has no entry for ${asset.name}; not installing`;
|
|
287
|
+
return report(ctx, m, 'error');
|
|
245
288
|
}
|
|
246
289
|
const archiveBuf = await fs.readFile(archivePath);
|
|
247
290
|
const actual = createHash('sha256').update(archiveBuf).digest('hex').toLowerCase();
|
|
248
291
|
if (actual !== expected) {
|
|
249
|
-
const m = `RTK: checksum mismatch! expected=${expected.slice(0, 12)}… actual=${actual.slice(0, 12)}
|
|
250
|
-
return report(ctx, m, '
|
|
292
|
+
const m = `RTK: checksum mismatch! expected=${expected.slice(0, 12)}… actual=${actual.slice(0, 12)}…; not installing`;
|
|
293
|
+
return report(ctx, m, 'error');
|
|
251
294
|
}
|
|
252
295
|
notify(ctx, 'RTK: checksum verified.', 'info');
|
|
253
296
|
|
|
@@ -355,15 +398,16 @@ export default function aiAddonsUpdaterExtension(pi: AddonUpdaterPi): void {
|
|
|
355
398
|
const sub = cleanParts[0];
|
|
356
399
|
|
|
357
400
|
if (sub === 'check' || sub === 'status') {
|
|
358
|
-
|
|
401
|
+
await checkAddons(ctx);
|
|
359
402
|
notify(ctx, 'ai-addons check complete.', 'info');
|
|
360
|
-
return
|
|
403
|
+
return;
|
|
361
404
|
}
|
|
362
405
|
if (sub === 'update' && cleanParts[1]) {
|
|
363
|
-
|
|
406
|
+
await runAddonUpdate(pi, ctx, cleanParts.slice(1).join(' '), dryRun);
|
|
407
|
+
return;
|
|
364
408
|
}
|
|
365
|
-
|
|
366
|
-
return
|
|
409
|
+
report(ctx, 'Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]', 'warning');
|
|
410
|
+
return;
|
|
367
411
|
},
|
|
368
412
|
});
|
|
369
413
|
}
|
|
@@ -1,45 +1,26 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import {
|
|
3
|
+
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.js';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
|
|
6
|
+
import { announceStatus } from '../shared/status.js';
|
|
6
7
|
import { readCavemanDefault } from '../shared/plugin-settings.js';
|
|
7
8
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const RULE_PATH = join(CAVERN_DIR, 'rule.md');
|
|
9
|
-
|
|
10
|
-
Respond terse like smart caveman. Keep all technical substance. Drop filler, pleasantries, and hedging.
|
|
11
|
-
|
|
12
|
-
Rules:
|
|
13
|
-
- Drop articles where meaning stays clear. Keep fragments and short sentences.
|
|
14
|
-
- Keep technical terms, code, commands, paths, API names, numbers, units, and errors exact.
|
|
15
|
-
- Never invent abbreviations or causal arrows. Do not add words to sound caveman.
|
|
16
|
-
- Use ASD-STE100 Simplified Technical English. Keep one idea per sentence, target 20 words, active voice, and consistent terms.
|
|
17
|
-
- Make no tool-call narration. Use no decorative tables or emoji.
|
|
18
|
-
- Preserve the user's reply language. Compress style, not language.
|
|
19
|
-
- Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or requests to clarify.
|
|
20
|
-
- Write code, comments, commits, docs, issues, pull requests, and third-party messages in normal prose.
|
|
21
|
-
|
|
22
|
-
Default: **full**. Switch: \`/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off\`. Stop: "stop caveman" or "normal mode".`;
|
|
23
|
-
// ponytail: synchronous read each full-mode injection; ceiling = small file, cold session start. Upgrade path: cache file contents + mtime, invalidate on change.
|
|
10
|
+
// Fail loud: a wrong rule reads to the model as the real one.
|
|
24
11
|
function readFullRule() {
|
|
25
12
|
try {
|
|
26
13
|
return readFileSync(RULE_PATH, 'utf8');
|
|
27
14
|
}
|
|
28
15
|
catch {
|
|
29
|
-
return
|
|
16
|
+
return 'Caveman full requested, but rule.md is missing. Run: tersio doctor --fix extensions';
|
|
30
17
|
}
|
|
31
18
|
}
|
|
32
19
|
const DEFAULT_MODE = 'off';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ultra: `Caveman ultra active for this session.
|
|
38
|
-
Maximum terse prose. Strip conjunctions only when meaning stays clear. State each fact once. Use no invented abbreviations or causal arrows. Keep code, commands, commits, PR text, paths, API names, numbers, units, and errors exact. Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or user confusion.`,
|
|
39
|
-
'wenyan-lite': `Caveman wenyan-lite active for this session. Use semi-classical terse prose. Keep grammar structure, user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
|
|
40
|
-
'wenyan-full': `Caveman wenyan-full active for this session. Use maximum classical terseness where meaning stays clear. Use classical sentence patterns, verbs before objects, and optional omitted subjects. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
|
|
41
|
-
'wenyan-ultra': `Caveman wenyan-ultra active for this session. Use extreme classical abbreviation while keeping a classical Chinese feel and meaning clear. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
|
|
42
|
-
};
|
|
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()}`;
|
|
23
|
+
}
|
|
43
24
|
function resolveMode(entries, fallback = DEFAULT_MODE) {
|
|
44
25
|
return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
|
|
45
26
|
}
|
|
@@ -49,20 +30,7 @@ function isOffCommand(text) {
|
|
|
49
30
|
}
|
|
50
31
|
export default function cavemanSessionExtension(pi) {
|
|
51
32
|
let currentMode = DEFAULT_MODE;
|
|
52
|
-
let
|
|
53
|
-
let lastCtx = undefined;
|
|
54
|
-
function syncStatus(ctx) {
|
|
55
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
56
|
-
const c = lastCtx;
|
|
57
|
-
if (!c?.ui?.setStatus)
|
|
58
|
-
return;
|
|
59
|
-
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
60
|
-
if (isComboPresetActive() || currentMode === 'off') {
|
|
61
|
-
c.ui.setStatus('caveman', undefined);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
paintStatusBar(c.ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
|
|
65
|
-
}
|
|
33
|
+
let lastInjected = undefined;
|
|
66
34
|
function setMode(mode, ctx) {
|
|
67
35
|
const normalized = normalizeMode('caveman', mode);
|
|
68
36
|
if (!normalized)
|
|
@@ -70,26 +38,17 @@ export default function cavemanSessionExtension(pi) {
|
|
|
70
38
|
currentMode = normalized;
|
|
71
39
|
pi.appendEntry?.('caveman-mode', { mode: normalized });
|
|
72
40
|
setSharedComboMode('caveman', normalized);
|
|
73
|
-
|
|
74
|
-
const active = activeModesSummary(getSharedComboState());
|
|
75
|
-
const msg = normalized === 'off'
|
|
76
|
-
? `Caveman off. Active: ${active}.`
|
|
77
|
-
: `Caveman ${normalized} on — terse replies for this session. Active: ${active}.`;
|
|
78
|
-
ctx?.ui?.notify?.(msg, 'info');
|
|
41
|
+
announceStatus(ctx);
|
|
79
42
|
return true;
|
|
80
43
|
}
|
|
81
44
|
setExtensionLabel(pi, 'Caveman session toggle');
|
|
82
|
-
//
|
|
83
|
-
// it at once so the next turn injects the new mode with no session reload.
|
|
84
|
-
// Stable identity, so the bridge set dedupes across re-inits.
|
|
45
|
+
// Adopt a /tersio or /combo switch at once, so the next turn needs no reload.
|
|
85
46
|
function syncFromShared(state) {
|
|
86
47
|
const mode = normalizeMode('caveman', state.caveman);
|
|
87
|
-
if (mode)
|
|
48
|
+
if (mode)
|
|
88
49
|
currentMode = mode;
|
|
89
|
-
syncStatus();
|
|
90
|
-
}
|
|
91
50
|
}
|
|
92
|
-
setSharedComboListener(syncFromShared);
|
|
51
|
+
setSharedComboListener('caveman', syncFromShared);
|
|
93
52
|
pi.registerCommand?.('caveman', {
|
|
94
53
|
description: 'Toggle terse caveman replies for this session',
|
|
95
54
|
handler: async (args, ctx) => {
|
|
@@ -99,7 +58,7 @@ export default function cavemanSessionExtension(pi) {
|
|
|
99
58
|
return;
|
|
100
59
|
}
|
|
101
60
|
if (arg === 'status') {
|
|
102
|
-
ctx
|
|
61
|
+
announceStatus(ctx);
|
|
103
62
|
return;
|
|
104
63
|
}
|
|
105
64
|
if (!setMode(arg, ctx)) {
|
|
@@ -115,20 +74,18 @@ export default function cavemanSessionExtension(pi) {
|
|
|
115
74
|
});
|
|
116
75
|
function restoreMode(ctx) {
|
|
117
76
|
const entries = sessionEntries(ctx);
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
reconcileSharedComboEntries(entries);
|
|
123
|
-
// Persisted session state wins; a fresh session falls back to the
|
|
124
|
-
// installer/user-configured default (off unless configured).
|
|
77
|
+
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
78
|
+
if (hasModeState(entries))
|
|
79
|
+
reconcileSharedComboEntries(entries);
|
|
80
|
+
// Persisted state wins, then a mode already chosen this session, then the configured default. `off` is the start value, not "unset".
|
|
125
81
|
const persisted = resolveMode(entries, '');
|
|
126
|
-
|
|
127
|
-
|
|
82
|
+
const alreadyChosen = currentMode !== DEFAULT_MODE;
|
|
83
|
+
currentMode = persisted || (alreadyChosen ? currentMode : normalizeMode('caveman', readCavemanDefault())) || DEFAULT_MODE;
|
|
84
|
+
announceStatus(ctx);
|
|
128
85
|
}
|
|
86
|
+
// Start, resume, and branch all land here, so every session type shows it.
|
|
129
87
|
pi.on('session_start', async (_event, ctx) => {
|
|
130
88
|
restoreMode(ctx);
|
|
131
|
-
ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
|
|
132
89
|
});
|
|
133
90
|
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
134
91
|
restoreMode(ctx);
|
|
@@ -136,22 +93,23 @@ export default function cavemanSessionExtension(pi) {
|
|
|
136
93
|
pi.on('session_tree', async (_event, ctx) => {
|
|
137
94
|
restoreMode(ctx);
|
|
138
95
|
});
|
|
139
|
-
pi
|
|
140
|
-
|
|
141
|
-
syncStatus(ctx);
|
|
96
|
+
onHostEvent(pi, 'session_switch', async (_event, ctx) => {
|
|
97
|
+
restoreMode(ctx);
|
|
142
98
|
});
|
|
143
|
-
pi
|
|
144
|
-
|
|
145
|
-
syncStatus(ctx);
|
|
99
|
+
onHostEvent(pi, 'session_compact', async (_event, ctx) => {
|
|
100
|
+
restoreMode(ctx);
|
|
146
101
|
});
|
|
147
102
|
pi.on('before_agent_start', async (event) => {
|
|
148
103
|
const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
|
|
149
104
|
if (!mode || mode === 'off')
|
|
150
105
|
return;
|
|
151
|
-
const
|
|
152
|
-
|
|
106
|
+
const instruction = buildInstruction(mode);
|
|
107
|
+
// Match the whole instruction so a re-presented prompt cannot stack it.
|
|
108
|
+
if (systemPromptIncludes(event.systemPrompt, instruction))
|
|
153
109
|
return;
|
|
154
|
-
|
|
155
|
-
|
|
110
|
+
// The previous level's block is replaced, not stacked on.
|
|
111
|
+
const stale = lastInjected;
|
|
112
|
+
lastInjected = instruction;
|
|
113
|
+
return injectPromptText(pi, event, instruction, stale);
|
|
156
114
|
});
|
|
157
115
|
}
|