@krtclcdy/tersio 2.24.1 → 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 +27 -21
- package/cli/banner.js +7 -4
- package/cli/common.js +14 -13
- package/cli/currency.js +1 -2
- package/cli/dashboard.js +57 -14
- package/cli/doctor-fix.js +88 -21
- package/cli/doctor.js +32 -24
- package/cli/hosts.js +19 -18
- package/cli/install.js +123 -52
- package/cli/interactive.js +29 -19
- package/cli/manifest.js +7 -0
- package/cli/profile.js +42 -11
- package/cli/rtk-wiring.js +28 -26
- package/cli/settings.js +37 -8
- package/cli/uninstall.js +38 -7
- package/cli/update.js +22 -14
- package/cli/usage.js +39 -14
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +69 -15
- package/extensions/ai-addons-updater/index.ts +60 -17
- package/extensions/caveman-session/index.js +28 -55
- package/extensions/caveman-session/index.ts +28 -55
- package/extensions/combo-toggle/index.js +40 -58
- package/extensions/combo-toggle/index.ts +38 -67
- package/extensions/lib/utils.js +60 -15
- package/extensions/lib/utils.ts +59 -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 +20 -35
- package/extensions/rtk-session/index.ts +19 -34
- package/extensions/shared/carbon.js +1 -2
- package/extensions/shared/carbon.ts +1 -2
- package/extensions/shared/host.js +95 -46
- package/extensions/shared/host.ts +106 -50
- package/extensions/shared/plugin-settings.js +4 -6
- package/extensions/shared/plugin-settings.ts +4 -6
- package/extensions/shared/pricing.js +79 -25
- package/extensions/shared/pricing.ts +80 -26
- package/extensions/shared/rtk-gain.js +3 -5
- package/extensions/shared/rtk-gain.ts +3 -5
- package/extensions/shared/session-state.js +10 -33
- package/extensions/shared/session-state.ts +12 -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 +190 -56
- package/extensions/shared/usage-ledger.ts +194 -58
- package/extensions/shared/usage-store.js +228 -64
- package/extensions/shared/usage-store.ts +235 -67
- package/extensions/tersio-commands/index.js +4 -9
- package/extensions/tersio-commands/index.ts +4 -9
- package/package.json +8 -2
- package/tersio.js +3 -3
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -22
- 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');
|
|
@@ -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
|
}
|
|
@@ -59,7 +57,7 @@ interface AddonUpdaterPi {
|
|
|
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
|
|
|
@@ -1,8 +1,9 @@
|
|
|
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');
|
|
@@ -16,16 +17,10 @@ function readFullRule() {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
const DEFAULT_MODE = 'off';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
ultra: `Caveman ultra active for this session.
|
|
24
|
-
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.`,
|
|
25
|
-
'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.`,
|
|
26
|
-
'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.`,
|
|
27
|
-
'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.`,
|
|
28
|
-
};
|
|
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
|
+
}
|
|
29
24
|
function resolveMode(entries, fallback = DEFAULT_MODE) {
|
|
30
25
|
return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
|
|
31
26
|
}
|
|
@@ -35,20 +30,7 @@ function isOffCommand(text) {
|
|
|
35
30
|
}
|
|
36
31
|
export default function cavemanSessionExtension(pi) {
|
|
37
32
|
let currentMode = DEFAULT_MODE;
|
|
38
|
-
let
|
|
39
|
-
let lastCtx = undefined;
|
|
40
|
-
function syncStatus(ctx) {
|
|
41
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
42
|
-
const ui = statusUi(lastCtx);
|
|
43
|
-
if (!ui)
|
|
44
|
-
return;
|
|
45
|
-
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
46
|
-
if (isComboPresetActive() || currentMode === 'off') {
|
|
47
|
-
ui.setStatus('caveman', undefined);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
paintStatusBar(ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
|
|
51
|
-
}
|
|
33
|
+
let lastInjected = undefined;
|
|
52
34
|
function setMode(mode, ctx) {
|
|
53
35
|
const normalized = normalizeMode('caveman', mode);
|
|
54
36
|
if (!normalized)
|
|
@@ -56,22 +38,15 @@ export default function cavemanSessionExtension(pi) {
|
|
|
56
38
|
currentMode = normalized;
|
|
57
39
|
pi.appendEntry?.('caveman-mode', { mode: normalized });
|
|
58
40
|
setSharedComboMode('caveman', normalized);
|
|
59
|
-
|
|
60
|
-
const active = activeModesSummary(getSharedComboState());
|
|
61
|
-
const msg = normalized === 'off'
|
|
62
|
-
? `Caveman off. Active: ${active}.`
|
|
63
|
-
: `Caveman ${normalized} on — terse replies for this session. Active: ${active}.`;
|
|
64
|
-
ctx?.ui?.notify?.(msg, 'info');
|
|
41
|
+
announceStatus(ctx);
|
|
65
42
|
return true;
|
|
66
43
|
}
|
|
67
44
|
setExtensionLabel(pi, 'Caveman session toggle');
|
|
68
45
|
// Adopt a /tersio or /combo switch at once, so the next turn needs no reload.
|
|
69
46
|
function syncFromShared(state) {
|
|
70
47
|
const mode = normalizeMode('caveman', state.caveman);
|
|
71
|
-
if (mode)
|
|
48
|
+
if (mode)
|
|
72
49
|
currentMode = mode;
|
|
73
|
-
syncStatus();
|
|
74
|
-
}
|
|
75
50
|
}
|
|
76
51
|
setSharedComboListener('caveman', syncFromShared);
|
|
77
52
|
pi.registerCommand?.('caveman', {
|
|
@@ -83,7 +58,7 @@ export default function cavemanSessionExtension(pi) {
|
|
|
83
58
|
return;
|
|
84
59
|
}
|
|
85
60
|
if (arg === 'status') {
|
|
86
|
-
ctx
|
|
61
|
+
announceStatus(ctx);
|
|
87
62
|
return;
|
|
88
63
|
}
|
|
89
64
|
if (!setMode(arg, ctx)) {
|
|
@@ -99,17 +74,18 @@ export default function cavemanSessionExtension(pi) {
|
|
|
99
74
|
});
|
|
100
75
|
function restoreMode(ctx) {
|
|
101
76
|
const entries = sessionEntries(ctx);
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
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".
|
|
106
81
|
const persisted = resolveMode(entries, '');
|
|
107
|
-
|
|
108
|
-
|
|
82
|
+
const alreadyChosen = currentMode !== DEFAULT_MODE;
|
|
83
|
+
currentMode = persisted || (alreadyChosen ? currentMode : normalizeMode('caveman', readCavemanDefault())) || DEFAULT_MODE;
|
|
84
|
+
announceStatus(ctx);
|
|
109
85
|
}
|
|
86
|
+
// Start, resume, and branch all land here, so every session type shows it.
|
|
110
87
|
pi.on('session_start', async (_event, ctx) => {
|
|
111
88
|
restoreMode(ctx);
|
|
112
|
-
ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
|
|
113
89
|
});
|
|
114
90
|
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
115
91
|
restoreMode(ctx);
|
|
@@ -117,26 +93,23 @@ export default function cavemanSessionExtension(pi) {
|
|
|
117
93
|
pi.on('session_tree', async (_event, ctx) => {
|
|
118
94
|
restoreMode(ctx);
|
|
119
95
|
});
|
|
120
|
-
pi
|
|
121
|
-
|
|
122
|
-
syncStatus(ctx);
|
|
96
|
+
onHostEvent(pi, 'session_switch', async (_event, ctx) => {
|
|
97
|
+
restoreMode(ctx);
|
|
123
98
|
});
|
|
124
|
-
pi
|
|
125
|
-
|
|
126
|
-
syncStatus(ctx);
|
|
99
|
+
onHostEvent(pi, 'session_compact', async (_event, ctx) => {
|
|
100
|
+
restoreMode(ctx);
|
|
127
101
|
});
|
|
128
102
|
pi.on('before_agent_start', async (event) => {
|
|
129
103
|
const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
|
|
130
104
|
if (!mode || mode === 'off')
|
|
131
105
|
return;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
return;
|
|
135
|
-
const instruction = typeof def === 'function' ? def() : def;
|
|
136
|
-
// Skip when already present, so a host that re-presents the mutated prompt
|
|
137
|
-
// cannot stack it; matching the whole instruction keeps upstream rule.md intact.
|
|
106
|
+
const instruction = buildInstruction(mode);
|
|
107
|
+
// Match the whole instruction so a re-presented prompt cannot stack it.
|
|
138
108
|
if (systemPromptIncludes(event.systemPrompt, instruction))
|
|
139
109
|
return;
|
|
140
|
-
|
|
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);
|
|
141
114
|
});
|
|
142
115
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
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.ts';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
|
|
6
|
+
import { announceStatus } from '../shared/status.ts';
|
|
6
7
|
import { readCavemanDefault } from '../shared/plugin-settings.ts';
|
|
7
8
|
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
8
9
|
|
|
@@ -17,16 +18,11 @@ function readFullRule(): string {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const DEFAULT_MODE = 'off';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
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.`,
|
|
26
|
-
'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.`,
|
|
27
|
-
'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.`,
|
|
28
|
-
'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.`,
|
|
29
|
-
};
|
|
21
|
+
|
|
22
|
+
// Upstream ships one SKILL.md; the level is a directive inside it, so swap only the header.
|
|
23
|
+
function buildInstruction(mode: string): string {
|
|
24
|
+
return `Caveman ${mode} active for this session.\n${readFullRule()}`;
|
|
25
|
+
}
|
|
30
26
|
|
|
31
27
|
function resolveMode(entries: SessionEntry[] | null | undefined, fallback: string = DEFAULT_MODE): string {
|
|
32
28
|
return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
|
|
@@ -39,20 +35,7 @@ function isOffCommand(text: unknown): boolean {
|
|
|
39
35
|
|
|
40
36
|
export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
41
37
|
let currentMode = DEFAULT_MODE;
|
|
42
|
-
let
|
|
43
|
-
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
44
|
-
|
|
45
|
-
function syncStatus(ctx?: ExtensionCtx): void {
|
|
46
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
47
|
-
const ui = statusUi(lastCtx);
|
|
48
|
-
if (!ui) return;
|
|
49
|
-
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
50
|
-
if (isComboPresetActive() || currentMode === 'off') {
|
|
51
|
-
ui.setStatus('caveman', undefined);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
paintStatusBar(ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
|
|
55
|
-
}
|
|
38
|
+
let lastInjected: string | undefined = undefined;
|
|
56
39
|
|
|
57
40
|
function setMode(mode: string, ctx?: ExtensionCtx): boolean {
|
|
58
41
|
const normalized = normalizeMode('caveman', mode);
|
|
@@ -60,12 +43,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
60
43
|
currentMode = normalized;
|
|
61
44
|
pi.appendEntry?.('caveman-mode', { mode: normalized });
|
|
62
45
|
setSharedComboMode('caveman', normalized);
|
|
63
|
-
|
|
64
|
-
const active = activeModesSummary(getSharedComboState());
|
|
65
|
-
const msg = normalized === 'off'
|
|
66
|
-
? `Caveman off. Active: ${active}.`
|
|
67
|
-
: `Caveman ${normalized} on — terse replies for this session. Active: ${active}.`;
|
|
68
|
-
ctx?.ui?.notify?.(msg, 'info');
|
|
46
|
+
announceStatus(ctx);
|
|
69
47
|
return true;
|
|
70
48
|
}
|
|
71
49
|
|
|
@@ -74,10 +52,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
74
52
|
// Adopt a /tersio or /combo switch at once, so the next turn needs no reload.
|
|
75
53
|
function syncFromShared(state: { caveman: string }): void {
|
|
76
54
|
const mode = normalizeMode('caveman', state.caveman);
|
|
77
|
-
if (mode)
|
|
78
|
-
currentMode = mode;
|
|
79
|
-
syncStatus();
|
|
80
|
-
}
|
|
55
|
+
if (mode) currentMode = mode;
|
|
81
56
|
}
|
|
82
57
|
setSharedComboListener('caveman', syncFromShared);
|
|
83
58
|
pi.registerCommand?.('caveman', {
|
|
@@ -89,7 +64,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
89
64
|
return;
|
|
90
65
|
}
|
|
91
66
|
if (arg === 'status') {
|
|
92
|
-
ctx
|
|
67
|
+
announceStatus(ctx);
|
|
93
68
|
return;
|
|
94
69
|
}
|
|
95
70
|
if (!setMode(arg, ctx)) {
|
|
@@ -105,18 +80,18 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
105
80
|
|
|
106
81
|
function restoreMode(ctx?: ExtensionCtx): void {
|
|
107
82
|
const entries = sessionEntries(ctx);
|
|
108
|
-
//
|
|
109
|
-
reconcileSharedComboEntries(entries);
|
|
110
|
-
// Persisted
|
|
111
|
-
// installer/user-configured default (off unless configured).
|
|
83
|
+
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
84
|
+
if (hasModeState(entries)) reconcileSharedComboEntries(entries);
|
|
85
|
+
// Persisted state wins, then a mode already chosen this session, then the configured default. `off` is the start value, not "unset".
|
|
112
86
|
const persisted = resolveMode(entries, '');
|
|
113
|
-
|
|
114
|
-
|
|
87
|
+
const alreadyChosen = currentMode !== DEFAULT_MODE;
|
|
88
|
+
currentMode = persisted || (alreadyChosen ? currentMode : normalizeMode('caveman', readCavemanDefault())) || DEFAULT_MODE;
|
|
89
|
+
announceStatus(ctx);
|
|
115
90
|
}
|
|
116
91
|
|
|
92
|
+
// Start, resume, and branch all land here, so every session type shows it.
|
|
117
93
|
pi.on('session_start', async (_event, ctx) => {
|
|
118
94
|
restoreMode(ctx);
|
|
119
|
-
ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
|
|
120
95
|
});
|
|
121
96
|
|
|
122
97
|
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
@@ -127,25 +102,23 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
127
102
|
restoreMode(ctx);
|
|
128
103
|
});
|
|
129
104
|
|
|
130
|
-
pi
|
|
131
|
-
|
|
132
|
-
syncStatus(ctx);
|
|
105
|
+
onHostEvent(pi, 'session_switch', async (_event, ctx) => {
|
|
106
|
+
restoreMode(ctx);
|
|
133
107
|
});
|
|
134
108
|
|
|
135
|
-
pi
|
|
136
|
-
|
|
137
|
-
syncStatus(ctx);
|
|
109
|
+
onHostEvent(pi, 'session_compact', async (_event, ctx) => {
|
|
110
|
+
restoreMode(ctx);
|
|
138
111
|
});
|
|
139
112
|
|
|
140
113
|
pi.on('before_agent_start', async (event: SystemPromptEvent) => {
|
|
141
114
|
const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
|
|
142
115
|
if (!mode || mode === 'off') return;
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
const instruction = typeof def === 'function' ? def() : def;
|
|
146
|
-
// Skip when already present, so a host that re-presents the mutated prompt
|
|
147
|
-
// cannot stack it; matching the whole instruction keeps upstream rule.md intact.
|
|
116
|
+
const instruction = buildInstruction(mode);
|
|
117
|
+
// Match the whole instruction so a re-presented prompt cannot stack it.
|
|
148
118
|
if (systemPromptIncludes(event.systemPrompt, instruction)) return;
|
|
149
|
-
|
|
119
|
+
// The previous level's block is replaced, not stacked on.
|
|
120
|
+
const stale = lastInjected;
|
|
121
|
+
lastInjected = instruction;
|
|
122
|
+
return injectPromptText(pi, event, instruction, stale);
|
|
150
123
|
});
|
|
151
124
|
}
|