@krtclcdy/tersio 2.25.4 → 2.25.6
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 -63
- package/dashboard/dist/index.html +18 -16
- package/dist/cli/backups.js +187 -0
- package/dist/cli/banner.js +4 -3
- package/dist/cli/common.js +29 -13
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +168 -110
- package/dist/cli/doctor-fix.js +232 -50
- package/dist/cli/doctor.js +281 -32
- package/dist/cli/hosts.js +29 -9
- package/dist/cli/install.js +204 -69
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +125 -17
- package/dist/cli/profile.js +14 -9
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +24 -8
- package/dist/cli/update.js +9 -10
- package/dist/cli/usage.js +75 -7
- package/dist/extensions/{caveman-session → caveman}/index.js +4 -2
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-megacave.md +5 -3
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +89 -0
- package/dist/extensions/{combo-toggle → combo}/index.js +62 -21
- package/dist/extensions/{tersio-commands → commands}/index.js +2 -2
- package/dist/extensions/pstack/index.js +119 -0
- package/dist/extensions/pstack/rule.md +65 -0
- package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -2
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +5 -5
- package/dist/extensions/{opencode/server.js → server.js} +98 -14
- package/dist/extensions/shared/agent-stats.js +1198 -0
- package/dist/extensions/shared/brand-marks.js +153 -0
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +12 -7
- package/dist/extensions/shared/pricing.js +86 -30
- package/dist/extensions/shared/rtk-gain.js +4 -9
- package/dist/extensions/shared/session-state.js +15 -10
- package/dist/extensions/shared/sqlite.js +168 -0
- package/dist/extensions/shared/status.js +8 -8
- package/dist/extensions/shared/types.js +1 -1
- package/dist/extensions/shared/usage-ledger.js +113 -39
- package/dist/extensions/shared/usage-store.js +191 -48
- package/dist/extensions/{lib → shared}/utils.js +6 -6
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -6
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +9 -4
- package/extensions/{caveman-session → caveman}/index.ts +4 -2
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-megacave.md +5 -3
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +89 -0
- package/extensions/{combo-toggle → combo}/index.ts +62 -19
- package/extensions/{tersio-commands → commands}/index.ts +2 -2
- package/extensions/pstack/index.ts +128 -0
- package/extensions/pstack/rule.md +65 -0
- package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -2
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +5 -5
- package/extensions/{opencode/server.ts → server.ts} +81 -16
- package/extensions/shared/agent-stats.ts +1605 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +13 -7
- package/extensions/shared/pricing.ts +95 -32
- package/extensions/shared/rtk-gain.ts +4 -9
- package/extensions/shared/session-state.ts +18 -13
- package/extensions/shared/sqlite.ts +168 -0
- package/extensions/shared/status.ts +9 -8
- package/extensions/shared/types.ts +2 -1
- package/extensions/shared/usage-ledger.ts +134 -46
- package/extensions/shared/usage-store.ts +202 -50
- package/extensions/{lib → shared}/utils.ts +6 -6
- package/extensions/timing/index.ts +45 -0
- package/extensions/{ai-addons-updater → updater}/index.ts +15 -9
- package/package.json +27 -18
- package/dist/extensions/caveman-session/rule.md +0 -79
- package/extensions/caveman-session/rule.md +0 -79
package/dist/cli/usage.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// cli/usage.ts — ledger + session-token usage report, tokscale-style.
|
|
2
|
-
import { co2GramsFor, displayModelId, energyWhFor, foldModelsByLabel, importSessionTokens, ledgerPath, priceFor, readResetWatermark, readRtkAdoption, readRtkRecallDiagnostics, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
|
-
import { resolveRtkBinary } from '../extensions/
|
|
2
|
+
import { PRICE_TIERS_FIRM, co2GramsFor, displayModelId, energyWhFor, foldModelsByLabel, importSessionTokens, ledgerPath, priceFor, pricesOverridePath, readResetWatermark, readRtkAdoption, readRtkRecallDiagnostics, readUsage, refreshPricesIfStale, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
|
+
import { resolveRtkBinary } from '../extensions/shared/utils.js';
|
|
4
4
|
import { readRtkGain } from '../extensions/shared/rtk-gain.js';
|
|
5
5
|
import { readUsageDb, reparseGuardReport, syncUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
6
6
|
import { withInteractiveSpinner } from './interactive.js';
|
|
7
7
|
import { PACKAGE_VERSION, currency } from './common.js';
|
|
8
8
|
import { formatCurrency } from './currency.js';
|
|
9
|
-
// RTK
|
|
9
|
+
// RTK history is machine-wide; rows group by command. 2000 groups covers a long history while bounding /data.json.
|
|
10
10
|
const RTK_COMMAND_ROWS = 2000;
|
|
11
11
|
export function summarizeUsage(rows) {
|
|
12
12
|
refreshPricesIfStale();
|
|
@@ -17,7 +17,7 @@ export function summarizeUsage(rows) {
|
|
|
17
17
|
detailCounts[r.detail] = (detailCounts[r.detail] ?? 0) + 1;
|
|
18
18
|
}
|
|
19
19
|
const byDetail = Object.entries(detailCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
20
|
-
// usage.db
|
|
20
|
+
// usage.db caches the same per-message rows: sync best-effort, read stored, fall back to the live parse when stale.
|
|
21
21
|
let synced = false;
|
|
22
22
|
try {
|
|
23
23
|
synced = syncUsageDb();
|
|
@@ -39,11 +39,13 @@ export function summarizeUsage(rows) {
|
|
|
39
39
|
const source = stored ? (synced ? 'stored' : 'stored-stale') : 'live';
|
|
40
40
|
const watermark = readResetWatermark();
|
|
41
41
|
let usd = 0;
|
|
42
|
+
let estimatedUsd = 0;
|
|
42
43
|
let priced = true;
|
|
43
44
|
let savedUsd = 0;
|
|
44
45
|
const byModelUsd = {};
|
|
45
46
|
const byModelBucketUsd = {};
|
|
46
47
|
const byHost = {};
|
|
48
|
+
const byAgent = {};
|
|
47
49
|
const byDayModel = {};
|
|
48
50
|
let co2g = 0;
|
|
49
51
|
let energyWh = 0;
|
|
@@ -55,9 +57,24 @@ export function summarizeUsage(rows) {
|
|
|
55
57
|
};
|
|
56
58
|
// Price by exact key first (gateways can differ), then fold every map under the display label.
|
|
57
59
|
const folded = foldModelsByLabel(session.byModel, session.byModelMessages);
|
|
60
|
+
const priceTiers = {};
|
|
61
|
+
// Named in the loop so the list is built in one pass, then sorted by token volume below.
|
|
62
|
+
const weakModels = [];
|
|
63
|
+
const measuredModels = [];
|
|
58
64
|
for (const [model, t] of Object.entries(session.byModel)) {
|
|
59
|
-
|
|
65
|
+
// A model counts as measured only when the host priced every message, or unpriced ones would read as zero.
|
|
66
|
+
const covered = (session.byModelCostMessages[model] ?? 0) >= (session.byModelMessages[model] ?? 0);
|
|
67
|
+
const measured = covered ? session.byModelMeasured[model] : undefined;
|
|
68
|
+
const c = usdCost(t, model, measured);
|
|
60
69
|
usd += c.usd;
|
|
70
|
+
priceTiers[c.tier] = (priceTiers[c.tier] ?? 0) + 1;
|
|
71
|
+
if (c.tier === 'measured')
|
|
72
|
+
measuredModels.push(model);
|
|
73
|
+
// A non-firm tier is kept out of the firm total, or a guessed cost would make the figure read as a bill.
|
|
74
|
+
if (!PRICE_TIERS_FIRM.includes(c.tier)) {
|
|
75
|
+
estimatedUsd += c.usd;
|
|
76
|
+
weakModels.push(model);
|
|
77
|
+
}
|
|
61
78
|
const label = displayModelId(model);
|
|
62
79
|
byModelUsd[label] = (byModelUsd[label] ?? 0) + c.usd;
|
|
63
80
|
byModelBucketUsd[label] ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
@@ -67,6 +84,11 @@ export function summarizeUsage(rows) {
|
|
|
67
84
|
byHost[label][host] ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
68
85
|
addTok(byHost[label][host], hb);
|
|
69
86
|
}
|
|
87
|
+
for (const [agent, ab] of Object.entries(session.byAgent[model] ?? {})) {
|
|
88
|
+
byAgent[agent] ??= { tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, msgs: 0 };
|
|
89
|
+
addTok(byAgent[agent].tokens, ab);
|
|
90
|
+
byAgent[agent].msgs += session.byAgentMessages[model]?.[agent] ?? 0;
|
|
91
|
+
}
|
|
70
92
|
co2g += co2GramsFor(model, t.output);
|
|
71
93
|
energyWh += energyWhFor(model, t.output);
|
|
72
94
|
if (!c.priced)
|
|
@@ -81,6 +103,10 @@ export function summarizeUsage(rows) {
|
|
|
81
103
|
}
|
|
82
104
|
}
|
|
83
105
|
const byTool = Object.entries(session.byTool).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
106
|
+
// Heaviest first, then capped to five: the line is guidance, not a dump of the model table.
|
|
107
|
+
const weakKeys = weakModels
|
|
108
|
+
.toSorted((a, b) => tokenTotal(session.byModel[b]) - tokenTotal(session.byModel[a]))
|
|
109
|
+
.slice(0, 5);
|
|
84
110
|
return {
|
|
85
111
|
total: rows.length,
|
|
86
112
|
byKind,
|
|
@@ -94,6 +120,7 @@ export function summarizeUsage(rows) {
|
|
|
94
120
|
byModelBucketUsd,
|
|
95
121
|
byModelMessages: folded.byModelMessages,
|
|
96
122
|
byHost,
|
|
123
|
+
byAgent,
|
|
97
124
|
byDay: session.byDay,
|
|
98
125
|
byDayModel,
|
|
99
126
|
byTool,
|
|
@@ -106,6 +133,10 @@ export function summarizeUsage(rows) {
|
|
|
106
133
|
rtkRecall: readRtkRecallDiagnostics(resolveRtkBinary()),
|
|
107
134
|
usd,
|
|
108
135
|
priced,
|
|
136
|
+
estimatedUsd,
|
|
137
|
+
priceTiers,
|
|
138
|
+
weakModels: weakKeys,
|
|
139
|
+
measuredModels,
|
|
109
140
|
savedUsd,
|
|
110
141
|
costMeasured: session.costMeasured,
|
|
111
142
|
co2g,
|
|
@@ -119,6 +150,10 @@ export function summarizeUsage(rows) {
|
|
|
119
150
|
function fmt(n) {
|
|
120
151
|
return n.toLocaleString('en-US');
|
|
121
152
|
}
|
|
153
|
+
/** Every billed bucket, so a model sorts by how much of the estimate it carries. */
|
|
154
|
+
function tokenTotal(t) {
|
|
155
|
+
return t.input + t.output + t.cacheRead + t.cacheWrite;
|
|
156
|
+
}
|
|
122
157
|
function fmtShort(n) {
|
|
123
158
|
if (n >= 1e12)
|
|
124
159
|
return (n / 1e12).toFixed(1) + 'T';
|
|
@@ -143,7 +178,7 @@ const ROW_CAP = 15;
|
|
|
143
178
|
function pad(s, n) {
|
|
144
179
|
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
145
180
|
}
|
|
146
|
-
// Box-drawing table, plain text (piped-safe, byte-stable).
|
|
181
|
+
// Box-drawing table, plain text (piped-safe, byte-stable). Numbers right-align; long cells truncate with an ellipsis.
|
|
147
182
|
function textTable(headers, rows, right = [], maxW = 32) {
|
|
148
183
|
const cells = [headers, ...rows].map((r) => r.map((c) => (c.length > maxW ? c.slice(0, maxW - 1) + '…' : c)));
|
|
149
184
|
const widths = headers.map((_, i) => Math.max(...cells.map((r) => r[i].length)));
|
|
@@ -163,12 +198,33 @@ function printReport(report) {
|
|
|
163
198
|
}
|
|
164
199
|
const t = report.tokens;
|
|
165
200
|
const a = report.rtkAdoption;
|
|
201
|
+
// A tier that is not firm is named, so a figure that is partly a guess never reads as a bill.
|
|
202
|
+
const weak = ['tail', 'default'].reduce((sum, tier) => sum + (report.priceTiers[tier] ?? 0), 0);
|
|
203
|
+
const overridden = report.priceTiers.override ?? 0;
|
|
204
|
+
// The count stays in the cell; names go below, since a 118-column cell would truncate a model-id list.
|
|
205
|
+
const listed = report.weakModels.map((m) => displayModelId(m));
|
|
206
|
+
const measuredCount = report.measuredModels.length;
|
|
207
|
+
const costNote = weak > 0
|
|
208
|
+
? ` · ${weak} model${weak === 1 ? '' : 's'} priced from another provider or the default table — add them to ${pricesOverridePath()}`
|
|
209
|
+
: overridden > 0
|
|
210
|
+
? ` · ${overridden} model${overridden === 1 ? '' : 's'} priced from your override file`
|
|
211
|
+
: measuredCount > 0
|
|
212
|
+
? ` · ${measuredCount} model${measuredCount === 1 ? '' : 's'} priced by the host`
|
|
213
|
+
: '';
|
|
166
214
|
for (const l of textTable(['Metric', 'Value'], [
|
|
167
215
|
['Tokens', `${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`],
|
|
168
|
-
['Cost', `${formatCurrency(report.usd, report.currency)}${report.priced ? '' : ' (includes default pricing)'} · ~${formatCurrency(report.savedUsd, report.currency)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)`],
|
|
216
|
+
['Cost', `${formatCurrency(report.usd, report.currency)}${report.priced ? '' : ' (includes default pricing)'} · ~${formatCurrency(report.savedUsd, report.currency)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)${costNote}`],
|
|
169
217
|
['RTK adoption', `${fmt(a.rtkCalls)}/${fmt(a.eligibleCalls)} eligible Bash calls use RTK (${a.adoptionPct.toFixed(1)}%) · ${fmt(a.missedCalls)} missed · recall ${report.rtkRecall.available ? `${report.rtkRecall.mode} (${report.rtkRecall.entries})` : 'unavailable'}`],
|
|
170
218
|
], [false, false], 118))
|
|
171
219
|
console.log(l);
|
|
220
|
+
if (measuredCount > 0) {
|
|
221
|
+
// Named, because a measured figure is only worth trusting when a reader sees whose it is.
|
|
222
|
+
console.log(` Priced by the host: ${measuredCount} model${measuredCount === 1 ? '' : 's'}`);
|
|
223
|
+
}
|
|
224
|
+
if (weak > 0) {
|
|
225
|
+
// Heaviest first, one per host spelling, so the operator sees exactly what to write.
|
|
226
|
+
console.log(` Unpriced (heaviest first): ${listed.join(', ')}`);
|
|
227
|
+
}
|
|
172
228
|
const allModels = Object.entries(report.byModel).filter(([, b]) => b.input + b.output + b.cacheRead + b.cacheWrite > 0).sort((a, b) => (b[1].input + b[1].output) - (a[1].input + a[1].output));
|
|
173
229
|
const models = allModels.slice(0, ROW_CAP);
|
|
174
230
|
if (models.length) {
|
|
@@ -186,6 +242,18 @@ function printReport(report) {
|
|
|
186
242
|
if (allModels.length > ROW_CAP)
|
|
187
243
|
console.log(` showing top ${ROW_CAP} of ${fmt(allModels.length)} models — full set in the dashboard`);
|
|
188
244
|
}
|
|
245
|
+
const agentKinds = Object.entries(report.byAgent).filter(([, per]) => per.msgs > 0).toSorted((x, y) => y[1].msgs - x[1].msgs);
|
|
246
|
+
if (agentKinds.length > 1) {
|
|
247
|
+
console.log(' BY AGENT');
|
|
248
|
+
const top = agentKinds.reduce((m, [, per]) => Math.max(m, per.tokens.input + per.tokens.output + per.tokens.cacheRead + per.tokens.cacheWrite), 1);
|
|
249
|
+
const arows = agentKinds.map(([kind, per]) => {
|
|
250
|
+
const total = per.tokens.input + per.tokens.output + per.tokens.cacheRead + per.tokens.cacheWrite;
|
|
251
|
+
return [kind === 'subagent' ? 'Subagents' : 'Main agent', fmt(per.msgs), fmt(per.tokens.input), fmt(per.tokens.output), fmt(per.tokens.cacheRead), `${bar(total / top, 8)} ${fmtShort(total)}`];
|
|
252
|
+
});
|
|
253
|
+
for (const line of textTable(['Agent', 'Msgs', 'Input', 'Output', 'Cache read', 'Share'], arows, [false, true, true, true, true, false])) {
|
|
254
|
+
console.log(line);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
189
257
|
const cmdRows = report.byTool.map(([tool, n]) => ({ name: tool, count: n, saved: null, avgPct: null, avgMs: null }));
|
|
190
258
|
for (const r of report.rtkGain.byCommand) {
|
|
191
259
|
cmdRows.push({ name: r.command, count: r.count, saved: r.saved, avgPct: r.avgPct, avgMs: r.avgMs });
|
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
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
|
+
import { announceStatus, formatCavemanStatus } from '../shared/status.js';
|
|
7
7
|
import { readCavemanDefault } from '../shared/plugin-settings.js';
|
|
8
8
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
// Upstream ships one skill per register: default, ultracave, megacave.
|
|
@@ -66,6 +66,8 @@ export default function cavemanSessionExtension(pi) {
|
|
|
66
66
|
}
|
|
67
67
|
if (arg === 'status') {
|
|
68
68
|
announceStatus(ctx);
|
|
69
|
+
// The footer carries one line, so the mode is answered here instead of standing as a second status key.
|
|
70
|
+
ctx?.ui?.notify?.(formatCavemanStatus(currentMode), 'info');
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
73
|
if (!setMode(arg, ctx)) {
|
|
@@ -84,7 +86,7 @@ export default function cavemanSessionExtension(pi) {
|
|
|
84
86
|
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
85
87
|
if (hasModeState(entries))
|
|
86
88
|
reconcileSharedComboEntries(entries);
|
|
87
|
-
// Persisted state wins, then a mode already chosen
|
|
89
|
+
// Persisted state wins, then a mode already chosen, then the default. `off` is the start value, not unset.
|
|
88
90
|
const persisted = resolveMode(entries, '');
|
|
89
91
|
const alreadyChosen = currentMode !== DEFAULT_MODE;
|
|
90
92
|
currentMode = persisted || (alreadyChosen ? currentMode : normalizeMode('caveman', readCavemanDefault())) || DEFAULT_MODE;
|
|
@@ -15,11 +15,11 @@ Megacave is caveman in Classical Chinese. 文言文: subjects omitted where reco
|
|
|
15
15
|
|
|
16
16
|
## Persistence
|
|
17
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
|
|
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; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.
|
|
19
19
|
|
|
20
20
|
## Floor
|
|
21
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 白話.
|
|
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 白話. Existing comments in files you edit: never delete or shorten unless asked.
|
|
23
23
|
|
|
24
24
|
## Rules
|
|
25
25
|
|
|
@@ -39,6 +39,8 @@ Good: "池蓄已開之連,不逐請而新開,省握手之費。"
|
|
|
39
39
|
|
|
40
40
|
### 3. Each fact once
|
|
41
41
|
|
|
42
|
+
Code change: changed lines plus context, not the whole file.
|
|
43
|
+
|
|
42
44
|
Good: "新參照則重繪。`useMemo` 包之。"
|
|
43
45
|
|
|
44
46
|
### 4. Payload in original script
|
|
@@ -56,7 +58,7 @@ No "文言模式啟", no modern answer plus classical copy. 文言 not shorter t
|
|
|
56
58
|
|
|
57
59
|
## When to break the rules
|
|
58
60
|
|
|
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.
|
|
61
|
+
白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirm first. Any clause with two readings. User confused. Question to the user, with its options. 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
62
|
|
|
61
63
|
## Pre-send check
|
|
62
64
|
|
|
@@ -15,11 +15,11 @@ Ultracave is caveman with the grammar stripped. Payload only.
|
|
|
15
15
|
|
|
16
16
|
## Persistence
|
|
17
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
|
|
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; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.
|
|
19
19
|
|
|
20
20
|
## Floor
|
|
21
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.
|
|
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. Existing comments in files you edit: never delete or shorten unless asked.
|
|
23
23
|
|
|
24
24
|
## Rules
|
|
25
25
|
|
|
@@ -37,7 +37,7 @@ Good: "Yes. Null check first."
|
|
|
37
37
|
|
|
38
38
|
### 3. Each fact once
|
|
39
39
|
|
|
40
|
-
No restating, no summary after a list.
|
|
40
|
+
No restating, no summary after a list. Code change: changed lines plus context, not the whole file.
|
|
41
41
|
|
|
42
42
|
Good: "Pool reuses open DB connections. No per-request handshake."
|
|
43
43
|
|
|
@@ -56,7 +56,7 @@ No prefix, no announcement, no mangled verbs for flavor. Fragment not shorter th
|
|
|
56
56
|
|
|
57
57
|
## When to break the rules
|
|
58
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.
|
|
59
|
+
Plain prose, then resume: security warning. Irreversible action, confirm first. Any fragment with two readings. User confused. Question to the user, with its options. 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
60
|
|
|
61
61
|
## Pre-send check
|
|
62
62
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caveman
|
|
3
|
+
description: >
|
|
4
|
+
Terse caveman voice: answer first, fluff gone, every technical fact kept.
|
|
5
|
+
Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less
|
|
6
|
+
tokens". Stays on until "stop caveman" or "normal mode".
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# caveman
|
|
10
|
+
|
|
11
|
+
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
|
12
|
+
|
|
13
|
+
Caveman is a voice, not broken grammar. Reader pays per token and reads in a terminal. Every word earns its place. Every fact survives.
|
|
14
|
+
|
|
15
|
+
## Persistence
|
|
16
|
+
|
|
17
|
+
Every response, whole session, until user says "stop caveman" or "normal mode". Unsure if still on? It is. Confirm the switch-off in one line.
|
|
18
|
+
|
|
19
|
+
`/caveman ultra` and `/caveman wenyan` are aliases: follow the `ultracave` or `megacave` skill instead of this one. `/caveman status` reports the mode and changes nothing. Relay the hook's `Caveman mode: <mode>` value when present. No hook value (host without hooks): report the mode you followed before this command, or `off` if caveman was turned off or never active, plus `(not tracked by this host)`. Example: `Caveman mode: caveman (not tracked by this host)`. Loading this skill to answer status is not activation. Never infer a mode from the configured default.
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
1. Every output token is billed and read. Filler costs twice.
|
|
24
|
+
2. Code, commands, paths, numbers, errors are the payload. One changed character breaks them.
|
|
25
|
+
3. Ceremony is expensive, grammar is cheap. "Sure, I'd be happy to help" is ten tokens. "the" is one.
|
|
26
|
+
4. A dropped negation costs more than every token saved. Clarity beats compression.
|
|
27
|
+
|
|
28
|
+
## Rules
|
|
29
|
+
|
|
30
|
+
### 1. Answer first
|
|
31
|
+
|
|
32
|
+
Answer, then reason, then next step. Pattern: `[thing] [action] [reason]. [next step].`
|
|
33
|
+
|
|
34
|
+
Bad: "Sure! I'd be happy to help. The issue you're experiencing is likely caused by..."
|
|
35
|
+
Good: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
|
36
|
+
|
|
37
|
+
### 2. Kill ceremony
|
|
38
|
+
|
|
39
|
+
No greeting, hedging, pleasantries, recap, or closer. No "Sure!", "Let me", "I'll now", "Hope this helps". No just/really/basically/actually/simply.
|
|
40
|
+
|
|
41
|
+
### 3. Short word
|
|
42
|
+
|
|
43
|
+
"fix" not "implement a solution for". Standard acronyms fine (DB, API, HTTP). Invented abbreviations not (cfg, impl, fn): same tokens, harder read. No arrows.
|
|
44
|
+
|
|
45
|
+
### 4. Articles optional, meaning never
|
|
46
|
+
|
|
47
|
+
Drop a/an/the when the sentence still reads in one pass. Fragments fine. Never drop not/never/no/only/except. Numbers and units exact.
|
|
48
|
+
|
|
49
|
+
Bad: "Migration drop column backup first."
|
|
50
|
+
Good: "Back up first. Then run migration: it drops the column."
|
|
51
|
+
|
|
52
|
+
### 5. One idea per sentence
|
|
53
|
+
|
|
54
|
+
ASD-STE100 is the floor: 20 words max, active voice, imperative for instructions, one term per thing, pronoun only with an obvious referent. Compression and clarity conflict? Clarity wins.
|
|
55
|
+
|
|
56
|
+
### 6. Payload verbatim
|
|
57
|
+
|
|
58
|
+
Code blocks unchanged. Commands, paths, API names exact. Errors quoted exact, shortest decisive line only. Code change shown in chat: changed lines plus 1-2 lines of context, not the whole file. Whole file only if the user asks, the file is new, or most of it changes. Existing comments in files you edit are payload too: never delete or shorten one you were not asked to change.
|
|
59
|
+
|
|
60
|
+
### 7. Tool runs: bounded status
|
|
61
|
+
|
|
62
|
+
No text between routine calls. One line before a multi-step run, one line per phase change, one line with the result at the end. Otherwise text before a call only to clarify, warn, or disambiguate.
|
|
63
|
+
|
|
64
|
+
### 8. User's language
|
|
65
|
+
|
|
66
|
+
Compress the style, not the language. An explicit reply-language instruction wins. Never switch because of quoted text. Technical terms and errors stay verbatim. Particles and case markers are grammar, not filler.
|
|
67
|
+
|
|
68
|
+
### 9. Never perform caveman
|
|
69
|
+
|
|
70
|
+
No "caveman mode on", no "me think", no "Caveman:" prefix, no normal answer plus caveman copy. No decorative tables or emoji. Never add a word to sound caveman. Caveman phrasing not shorter than plain? Use plain.
|
|
71
|
+
|
|
72
|
+
## When to break the rules
|
|
73
|
+
|
|
74
|
+
Plain prose, then resume:
|
|
75
|
+
|
|
76
|
+
1. Security warning.
|
|
77
|
+
2. Irreversible action. Confirm in full sentences first.
|
|
78
|
+
3. Step order a fragment could scramble.
|
|
79
|
+
4. User confused or repeats the question.
|
|
80
|
+
5. Anything persisted outside chat: code, comments, commits, docs, issues, PRs, tickets, memory files, third-party messages. `/caveman-compress` exempt.
|
|
81
|
+
6. Harness asks for a status line or confirmation. Give it. Harness decides *when* you speak, caveman decides *how*.
|
|
82
|
+
7. You ask the user a question or offer options. Full sentences, so the answer comes back right first time.
|
|
83
|
+
|
|
84
|
+
## Pre-send check
|
|
85
|
+
|
|
86
|
+
1. First sentence announces what you will do? Delete.
|
|
87
|
+
2. Last sentence recaps or offers help? Delete.
|
|
88
|
+
3. Every not/never/no/only present? Every code span, path, number, error verbatim?
|
|
89
|
+
4. Any sentence with two readings? Make it a full sentence.
|
|
@@ -8,14 +8,31 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
return path;
|
|
9
9
|
};
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { existsSync } from 'node:fs';
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
-
import { activeModesSummary, COMBO_LEVELS, getSharedComboState, hasModeState, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboModes, systemPromptIncludes, } from '../shared/session-state.js';
|
|
13
|
+
import { activeModesSummary, COMBO_LEVELS, getSharedComboState, hasModeState, normalizeComboLevel, normalizeMode, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, setSharedComboModes, systemPromptIncludes, } from '../shared/session-state.js';
|
|
14
14
|
import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
|
|
15
15
|
import { announceStatus } from '../shared/status.js';
|
|
16
|
-
import { isComboSetupComplete, readCavemanDefault, readComboDefault, readPluginSettings, readPonytailDefault, readRtkDefault, saveComboSetup, } from '../shared/plugin-settings.js';
|
|
17
|
-
import { findHoistedPackage, isPiProcess } from '../
|
|
16
|
+
import { isComboSetupComplete, readCavemanDefault, readComboDefault, readPluginSettings, readPonytailDefault, readPstackDefault, readRtkDefault, saveComboSetup, } from '../shared/plugin-settings.js';
|
|
17
|
+
import { findHoistedPackage, isPiProcess, piAgentDir } from '../shared/utils.js';
|
|
18
18
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
/** Whether pi declares an upstream Ponytail; read per call so a mid-session settings.json change lands. */
|
|
20
|
+
function piHasUpstreamPonytail() {
|
|
21
|
+
const agentDir = piAgentDir();
|
|
22
|
+
const localEntry = path.join(agentDir, 'ponytail', 'pi-extension', 'index.js');
|
|
23
|
+
if (existsSync(localEntry))
|
|
24
|
+
return true;
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(path.join(agentDir, 'settings.json'), 'utf8');
|
|
27
|
+
const settings = JSON.parse(raw);
|
|
28
|
+
const list = Array.isArray(settings.packages) ? settings.packages : [];
|
|
29
|
+
// An npm spec names the package; the installer's local copy is a bare `.../ponytail` dir.
|
|
30
|
+
return list.some((p) => typeof p === 'string' && /ponytail/.test(p));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
19
36
|
const PONYTAIL_FALLBACK_INTENSITY = {
|
|
20
37
|
lite: 'Prefer the simplest correct solution.',
|
|
21
38
|
};
|
|
@@ -24,7 +41,7 @@ export function ponytailFallback(mode) {
|
|
|
24
41
|
const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
|
|
25
42
|
return `🦥 PONYTAIL MODE ACTIVE — level: ${mode}\n${intensity} Understand the path first and fix root causes, not symptoms. Prefer the standard library and YAGNI. Avoid speculative abstractions and dependencies. Preserve correctness. Verify changed behavior.`;
|
|
26
43
|
}
|
|
27
|
-
// Ponytail is hoisted, so one upward walk covers every
|
|
44
|
+
// Ponytail is hoisted, so one upward walk covers every layout. The hook is CommonJS, so both namespaces are tried.
|
|
28
45
|
async function loadPonytailInstructions(mode) {
|
|
29
46
|
const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
|
|
30
47
|
// The OpenCode tree ships no node_modules; the installer bundles the ruleset beside the extensions.
|
|
@@ -42,7 +59,7 @@ async function loadPonytailInstructions(mode) {
|
|
|
42
59
|
return ponytailFallback(mode);
|
|
43
60
|
}
|
|
44
61
|
export default function comboToggleExtension(pi) {
|
|
45
|
-
setExtensionLabel(pi, 'Combo session toggle (all
|
|
62
|
+
setExtensionLabel(pi, 'Combo session toggle (all four add-ons)');
|
|
46
63
|
let setupPrompted = false;
|
|
47
64
|
let lastInjected = undefined;
|
|
48
65
|
// opencode has no UI event stream, so only its empty branch reconciles.
|
|
@@ -53,6 +70,7 @@ export default function comboToggleExtension(pi) {
|
|
|
53
70
|
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
54
71
|
pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
|
|
55
72
|
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
73
|
+
pi.appendEntry?.('pstack-mode', { mode: modes.pstack });
|
|
56
74
|
pi.appendEntry?.('combo-level', { level });
|
|
57
75
|
}
|
|
58
76
|
function restoreConfiguredDefaults() {
|
|
@@ -60,16 +78,18 @@ export default function comboToggleExtension(pi) {
|
|
|
60
78
|
const hasModeDefaults = ['cavemanDefault', 'rtkDefault', 'ponytailDefault'].some((key) => key in settings);
|
|
61
79
|
const combo = readComboDefault();
|
|
62
80
|
const preset = COMBO_LEVELS[combo];
|
|
81
|
+
// A stored per-mode default wins; with none, the preset supplies all four.
|
|
63
82
|
const modes = hasModeDefaults
|
|
64
|
-
? { caveman: readCavemanDefault(), rtk: readRtkDefault() ? 'on' : 'off', ponytail: readPonytailDefault() }
|
|
65
|
-
: preset;
|
|
83
|
+
? { caveman: readCavemanDefault(), rtk: readRtkDefault() ? 'on' : 'off', ponytail: readPonytailDefault(), pstack: readPstackDefault() }
|
|
84
|
+
: { caveman: preset.caveman, rtk: preset.rtk, ponytail: preset.ponytail, pstack: preset.pstack };
|
|
66
85
|
setSharedComboModes(modes);
|
|
67
|
-
if (modes.caveman === 'off' && modes.rtk === 'off' && modes.ponytail === 'off')
|
|
86
|
+
if (modes.caveman === 'off' && modes.rtk === 'off' && modes.ponytail === 'off' && modes.pstack === 'off')
|
|
68
87
|
return;
|
|
69
88
|
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
70
89
|
pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
|
|
71
90
|
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
72
|
-
|
|
91
|
+
pi.appendEntry?.('pstack-mode', { mode: modes.pstack });
|
|
92
|
+
if (preset && modes.caveman === preset.caveman && modes.rtk === preset.rtk && modes.ponytail === preset.ponytail && modes.pstack === preset.pstack) {
|
|
73
93
|
pi.appendEntry?.('combo-level', { level: combo });
|
|
74
94
|
}
|
|
75
95
|
}
|
|
@@ -92,7 +112,7 @@ export default function comboToggleExtension(pi) {
|
|
|
92
112
|
reconcile(ctx, resetEmpty);
|
|
93
113
|
}
|
|
94
114
|
pi.registerCommand?.('combo', {
|
|
95
|
-
description: 'Toggle all
|
|
115
|
+
description: 'Toggle all four Tersio modes at once. Usage: /combo <off|medium|balanced|max|status>',
|
|
96
116
|
handler: async (args, ctx) => {
|
|
97
117
|
listen(ctx);
|
|
98
118
|
const arg = String(args || '').trim().toLowerCase();
|
|
@@ -102,10 +122,10 @@ export default function comboToggleExtension(pi) {
|
|
|
102
122
|
return;
|
|
103
123
|
}
|
|
104
124
|
if (arg === 'help') {
|
|
105
|
-
ctx?.ui?.notify?.('/combo off — disables
|
|
106
|
-
'/combo medium — light: caveman=lite, rtk=on, ponytail=lite\n' +
|
|
107
|
-
'/combo balanced — middle: caveman=full, rtk=on, ponytail=full\n' +
|
|
108
|
-
'/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra', 'info');
|
|
125
|
+
ctx?.ui?.notify?.('/combo off — disables every mode (caveman, rtk, ponytail, pstack)\n' +
|
|
126
|
+
'/combo medium — light: caveman=lite, rtk=on, ponytail=lite, pstack=on\n' +
|
|
127
|
+
'/combo balanced — middle: caveman=full, rtk=on, ponytail=full, pstack=on\n' +
|
|
128
|
+
'/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra, pstack=on', 'info');
|
|
109
129
|
return;
|
|
110
130
|
}
|
|
111
131
|
const level = normalizeComboLevel(arg);
|
|
@@ -118,15 +138,36 @@ export default function comboToggleExtension(pi) {
|
|
|
118
138
|
announceStatus(ctx);
|
|
119
139
|
},
|
|
120
140
|
});
|
|
141
|
+
// Upstream owns /ponytail on pi; this tree registers it only where nothing else does.
|
|
142
|
+
if (!(isPiProcess() && piHasUpstreamPonytail()))
|
|
143
|
+
pi.registerCommand?.('ponytail', {
|
|
144
|
+
description: 'Switch Ponytail intensity (off|lite|full|ultra). Usage: /ponytail [level|status]',
|
|
145
|
+
handler: async (args, ctx) => {
|
|
146
|
+
listen(ctx);
|
|
147
|
+
const arg = String(args || '').trim().toLowerCase();
|
|
148
|
+
if (!arg || arg === 'status') {
|
|
149
|
+
announceStatus(ctx);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const mode = normalizeMode('ponytail', arg);
|
|
153
|
+
if (!mode) {
|
|
154
|
+
ctx?.ui?.notify?.(`Unknown Ponytail level: ${arg}. Use: off | lite | full | ultra`, 'warning');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
pi.appendEntry?.('ponytail-mode', { mode });
|
|
158
|
+
setSharedComboMode('ponytail', mode);
|
|
159
|
+
announceStatus(ctx);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
121
162
|
async function runFirstRunSetup(ctx) {
|
|
122
163
|
if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete())
|
|
123
164
|
return;
|
|
124
165
|
setupPrompted = true;
|
|
125
166
|
const choice = await hostSelect(pi, ctx.ui, 'Session-start defaults — Combo preset', [
|
|
126
167
|
{ label: 'off', description: 'Keep every Tersio mode inactive' },
|
|
127
|
-
{ label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
128
|
-
{ label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
|
|
129
|
-
{ label: 'max', description: 'caveman=ultra, rtk=on, ponytail=ultra' },
|
|
168
|
+
{ label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite, pstack=on' },
|
|
169
|
+
{ label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full, pstack=on' },
|
|
170
|
+
{ label: 'max', description: 'caveman=ultra, rtk=on, ponytail=ultra, pstack=on' },
|
|
130
171
|
], { selectionMarker: 'radio', helpText: 'You can change this later with /tersio settings.' });
|
|
131
172
|
const level = choice || 'off';
|
|
132
173
|
if (saveComboSetup(level) && level !== 'off') {
|
|
@@ -159,10 +200,10 @@ export default function comboToggleExtension(pi) {
|
|
|
159
200
|
if (ctx?.hasUI)
|
|
160
201
|
reconcile(ctx);
|
|
161
202
|
const mode = getSharedComboState().ponytail;
|
|
162
|
-
// On pi
|
|
163
|
-
if (isPiProcess())
|
|
203
|
+
// On pi, an installed upstream pi-extension is the sole injector, so Tersio only falls back with none.
|
|
204
|
+
if (isPiProcess() && piHasUpstreamPonytail())
|
|
164
205
|
return;
|
|
165
|
-
// Match the level's own header:
|
|
206
|
+
// Match the level's own header: a shared phrase makes every level block the rest, so a `/combo` switch does nothing.
|
|
166
207
|
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, `PONYTAIL MODE ACTIVE — level: ${mode}`))
|
|
167
208
|
return;
|
|
168
209
|
const instruction = await loadPonytailInstructions(mode);
|
|
@@ -5,7 +5,7 @@ import { setExtensionLabel } from '../shared/host.js';
|
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { appendUsage, readUsage } from '../shared/usage-ledger.js';
|
|
8
|
-
import { checkAddonsSummary, runAddonUpdate } from '../
|
|
8
|
+
import { checkAddonsSummary, runAddonUpdate } from '../updater/index.js';
|
|
9
9
|
const HELP = [
|
|
10
10
|
'/tersio status — active modes + combo level',
|
|
11
11
|
'/tersio check — add-on version check',
|
|
@@ -80,7 +80,7 @@ export default function tersioCommandsExtension(pi) {
|
|
|
80
80
|
ctx?.ui?.notify?.(usageSummary(), 'info');
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
|
-
// Removed mode switches redirect to their own commands so
|
|
83
|
+
// Removed mode switches redirect to their own commands, so there is one spelling: /caveman, /rtk, /combo, /ponytail.
|
|
84
84
|
if (sub === 'caveman' || sub === 'rtk' || sub === 'combo' || sub === 'ponytail') {
|
|
85
85
|
ctx?.ui?.notify?.(`Use /${sub} instead — /tersio no longer duplicates mode switches.\n${HELP}`, 'warning');
|
|
86
86
|
return;
|