@krtclcdy/tersio 2.25.2 → 2.25.4
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 +3 -3
- package/dashboard/dist/index.html +1 -1
- package/{cli → dist/cli}/common.js +4 -3
- package/{cli → dist/cli}/dashboard.js +30 -13
- package/{cli → dist/cli}/doctor-fix.js +2 -4
- package/{cli → dist/cli}/doctor.js +13 -6
- package/{cli → dist/cli}/install.js +56 -15
- package/{cli → dist/cli}/manifest.js +4 -2
- package/{cli → dist/cli}/settings.js +5 -23
- package/{cli → dist/cli}/uninstall.js +1 -1
- package/{cli → dist/cli}/update.js +4 -5
- package/{extensions → dist/extensions}/ai-addons-updater/index.js +4 -4
- package/{extensions → dist/extensions}/caveman-session/index.js +12 -5
- package/dist/extensions/caveman-session/rule-ultra.md +63 -0
- package/dist/extensions/caveman-session/rule.md +79 -0
- package/{extensions → dist/extensions}/combo-toggle/index.js +39 -29
- package/{extensions → dist/extensions}/lib/utils.js +26 -1
- package/{extensions → dist/extensions}/opencode/server.js +45 -1
- package/dist/extensions/rtk-filter/index.js +87 -0
- package/{extensions → dist/extensions}/rtk-session/index.js +3 -2
- package/{extensions → dist/extensions}/shared/host.js +18 -12
- package/{extensions → dist/extensions}/shared/plugin-settings.js +1 -1
- package/{extensions → dist/extensions}/shared/session-state.js +17 -3
- package/{extensions → dist/extensions}/shared/status.js +7 -1
- package/{tersio.js → dist/tersio.js} +1 -1
- package/extensions/ai-addons-updater/index.ts +4 -5
- package/extensions/caveman-session/index.ts +12 -5
- package/extensions/caveman-session/rule-megacave.md +63 -0
- package/extensions/caveman-session/rule.md +4 -2
- package/extensions/combo-toggle/index.ts +37 -29
- package/extensions/lib/utils.ts +22 -1
- package/extensions/opencode/server.ts +38 -1
- package/extensions/rtk-filter/index.ts +90 -0
- package/extensions/rtk-session/index.ts +3 -2
- package/extensions/shared/host.ts +26 -13
- package/extensions/shared/plugin-settings.ts +1 -1
- package/extensions/shared/session-state.ts +18 -3
- package/extensions/shared/status.ts +8 -1
- package/extensions/shared/types.ts +10 -2
- package/package.json +16 -15
- package/extensions/opencode/index.js +0 -104
- /package/{cli → dist/cli}/banner.js +0 -0
- /package/{cli → dist/cli}/currency.js +0 -0
- /package/{cli → dist/cli}/hosts.js +0 -0
- /package/{cli → dist/cli}/interactive.js +0 -0
- /package/{cli → dist/cli}/profile.js +0 -0
- /package/{cli → dist/cli}/reset.js +0 -0
- /package/{cli → dist/cli}/rtk-wiring.js +0 -0
- /package/{cli → dist/cli}/usage.js +0 -0
- /package/{extensions/caveman-session/rule-wenyan.md → dist/extensions/caveman-session/rule-megacave.md} +0 -0
- /package/{extensions → dist/extensions}/shared/carbon.js +0 -0
- /package/{extensions → dist/extensions}/shared/pricing.js +0 -0
- /package/{extensions → dist/extensions}/shared/rtk-gain.js +0 -0
- /package/{extensions → dist/extensions}/shared/types.js +0 -0
- /package/{extensions → dist/extensions}/shared/usage-ledger.js +0 -0
- /package/{extensions → dist/extensions}/shared/usage-store.js +0 -0
- /package/{extensions → dist/extensions}/tersio-commands/index.js +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caveman
|
|
3
|
+
description: >
|
|
4
|
+
Ultra-compressed communication mode that cuts output tokens while keeping
|
|
5
|
+
technical accuracy. Levels: lite, full, ultra and the megacave variants. Use for
|
|
6
|
+
/caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
|
10
|
+
|
|
11
|
+
## Persistence
|
|
12
|
+
|
|
13
|
+
Default style for this whole session, every response, until user say "stop caveman" or "normal mode". Keep terse on long sessions no filler drift.
|
|
14
|
+
|
|
15
|
+
Default: **full**. Switch: `/caveman lite|full|ultra|megacave-lite|megacave-full|megacave-ultra|off`.
|
|
16
|
+
|
|
17
|
+
## Rules
|
|
18
|
+
|
|
19
|
+
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations (cfg/impl/req/res/fn) tokenizer split them same as full word: zero token saved, reader still decode. Full word cheaper AND clearer. No causal arrows (→) either own token, save nothing. Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
|
20
|
+
|
|
21
|
+
Never drop not/never/no/only/except flip meaning worse than any token saved. Numbers, units exact.
|
|
22
|
+
|
|
23
|
+
Never ADD word to sound caveman. Compression only style never grow output. No inserted pronoun or copula to fake broken grammar: "when it not" cost one token more than "when not" and say same thing. Keep correct verb form when correct form cost same "sees" one token, "see" one token, so mangle buy nothing and read worse. Same rule as abbreviations and arrows: if caveman phrasing not shorter than plain phrasing, use plain.
|
|
24
|
+
|
|
25
|
+
Clarity register: mix ASD-STE100 Simplified Technical English into caveman, always. One idea per sentence. Sentence short, target 20 words max. Active voice. Present tense where true. One word one meaning: same term for same thing every time, no synonym rotation. Instruction = imperative: "Run X", not "X should be run". Noun cluster 3 words max. Pronoun only with one clear referent, else repeat noun. Caveman cut filler; STE keep what make meaning unambiguous. Conflict between them → clarity win.
|
|
26
|
+
|
|
27
|
+
Tool calls: fire direct. No preamble, plan, or progress note before or between calls. After result: next call direct or final answer never announce next call. Text before call only to clarify, warn security/irreversible, or resolve ambiguity.
|
|
28
|
+
|
|
29
|
+
Follow explicit reply-language instructions from the user or project. Otherwise preserve the user's dominant language. Never switch because of example text or multilingual context elsewhere. Compress the style, not the language. Every emitted line in that language openings, pre-tool status lines, all not just final reply. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim unless user explicitly ask for translation.
|
|
30
|
+
|
|
31
|
+
Ownership: caveman owns reply compression, so project or host prose rules that set article use, sentence length, or filler never override it. Treat those as applying to persisted artifacts instead: code, comments, commits, docs, and issue text. Any project rule naming caveman or tersio directly wins over both.
|
|
32
|
+
|
|
33
|
+
'Drop articles' = article languages only. Where small markers carry case/role (particles, postpositions), keep them grammar, not filler; compress politeness/filler instead.
|
|
34
|
+
|
|
35
|
+
Answer directly in this style. Skip "caveman mode on", "me caveman think", "Caveman:" prefix or recap redundant with the reply itself. No normal answer plus caveman duplicate. User ask what mode is → say so plainly.
|
|
36
|
+
|
|
37
|
+
Pattern: `[thing] [action] [reason]. [next step].`
|
|
38
|
+
|
|
39
|
+
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
|
40
|
+
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
|
41
|
+
|
|
42
|
+
## Intensity
|
|
43
|
+
|
|
44
|
+
| Level | What change |
|
|
45
|
+
|-------|------------|
|
|
46
|
+
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
|
|
47
|
+
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations |
|
|
48
|
+
|
|
49
|
+
Example "Why React component re-render?"
|
|
50
|
+
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
|
51
|
+
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
|
52
|
+
|
|
53
|
+
Example "Explain database connection pooling."
|
|
54
|
+
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
|
|
55
|
+
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
|
|
56
|
+
|
|
57
|
+
## Auto-Clarity
|
|
58
|
+
|
|
59
|
+
Drop caveman when:
|
|
60
|
+
- Security warnings
|
|
61
|
+
- Irreversible action confirmations
|
|
62
|
+
- Multi-step sequences where fragment order or omitted conjunctions risk misread
|
|
63
|
+
- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` order unclear without articles/conjunctions)
|
|
64
|
+
- User asks to clarify or repeats question
|
|
65
|
+
|
|
66
|
+
Resume caveman after clear part done.
|
|
67
|
+
|
|
68
|
+
Example shows FORMAT only write warning in session language, not example's.
|
|
69
|
+
|
|
70
|
+
Example destructive op:
|
|
71
|
+
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
|
|
72
|
+
> ```sql
|
|
73
|
+
> DROP TABLE users;
|
|
74
|
+
> ```
|
|
75
|
+
> Caveman resume. Verify backup exist first.
|
|
76
|
+
|
|
77
|
+
## Boundaries
|
|
78
|
+
|
|
79
|
+
Persisted outside chat: write normal prose code, comments, commits, docs, issue/PR/MR/defect/ticket/bug-report text, memory files, third-party messages (/caveman-compress exempt). "Open a defect" or "file a bug" mean the same as "open issue": body go to other humans, so body normal English. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
|
|
@@ -10,10 +10,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
-
import { activeModesSummary, COMBO_LEVELS, getSharedComboState, hasModeState, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener,
|
|
13
|
+
import { activeModesSummary, COMBO_LEVELS, getSharedComboState, hasModeState, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, 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, readComboDefault, readPonytailDefault, saveComboSetup, } from '../shared/plugin-settings.js';
|
|
16
|
+
import { isComboSetupComplete, readCavemanDefault, readComboDefault, readPluginSettings, readPonytailDefault, readRtkDefault, saveComboSetup, } from '../shared/plugin-settings.js';
|
|
17
17
|
import { findHoistedPackage, isPiProcess } from '../lib/utils.js';
|
|
18
18
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const PONYTAIL_FALLBACK_INTENSITY = {
|
|
@@ -45,6 +45,8 @@ export default function comboToggleExtension(pi) {
|
|
|
45
45
|
setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
|
|
46
46
|
let setupPrompted = false;
|
|
47
47
|
let lastInjected = undefined;
|
|
48
|
+
// opencode has no UI event stream, so only its empty branch reconciles.
|
|
49
|
+
const OPENCODE = pi.hostId === 'opencode';
|
|
48
50
|
// Siblings restore from these entries, so the fallback must write them too.
|
|
49
51
|
function persistPreset(level) {
|
|
50
52
|
const modes = COMBO_LEVELS[level];
|
|
@@ -53,23 +55,44 @@ export default function comboToggleExtension(pi) {
|
|
|
53
55
|
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
54
56
|
pi.appendEntry?.('combo-level', { level });
|
|
55
57
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
+
function restoreConfiguredDefaults() {
|
|
59
|
+
const settings = readPluginSettings();
|
|
60
|
+
const hasModeDefaults = ['cavemanDefault', 'rtkDefault', 'ponytailDefault'].some((key) => key in settings);
|
|
61
|
+
const combo = readComboDefault();
|
|
62
|
+
const preset = COMBO_LEVELS[combo];
|
|
63
|
+
const modes = hasModeDefaults
|
|
64
|
+
? { caveman: readCavemanDefault(), rtk: readRtkDefault() ? 'on' : 'off', ponytail: readPonytailDefault() }
|
|
65
|
+
: preset;
|
|
66
|
+
setSharedComboModes(modes);
|
|
67
|
+
if (modes.caveman === 'off' && modes.rtk === 'off' && modes.ponytail === 'off')
|
|
68
|
+
return;
|
|
69
|
+
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
70
|
+
pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
|
|
71
|
+
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
72
|
+
if (preset && modes.caveman === preset.caveman && modes.rtk === preset.rtk && modes.ponytail === preset.ponytail) {
|
|
73
|
+
pi.appendEntry?.('combo-level', { level: combo });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function reconcile(ctx, resetEmpty = false) {
|
|
77
|
+
if (!ctx?.hasUI && !(resetEmpty && OPENCODE))
|
|
58
78
|
return getSharedComboState();
|
|
59
|
-
|
|
79
|
+
const entries = sessionEntries(ctx);
|
|
80
|
+
if (!resetEmpty && entries.length === 0)
|
|
81
|
+
return getSharedComboState();
|
|
82
|
+
return reconcileSharedComboEntries(entries);
|
|
60
83
|
}
|
|
61
84
|
function listen(ctx) {
|
|
62
85
|
if (!ctx?.hasUI)
|
|
63
86
|
return;
|
|
64
87
|
setSharedComboListener('combo', (state) => { announceStatus(ctx); return state; });
|
|
65
88
|
}
|
|
66
|
-
function track(ctx) {
|
|
89
|
+
function track(ctx, resetEmpty = false) {
|
|
67
90
|
listen(ctx);
|
|
68
|
-
if (ctx?.hasUI)
|
|
69
|
-
reconcile(ctx);
|
|
91
|
+
if (ctx?.hasUI || (resetEmpty && OPENCODE))
|
|
92
|
+
reconcile(ctx, resetEmpty);
|
|
70
93
|
}
|
|
71
94
|
pi.registerCommand?.('combo', {
|
|
72
|
-
description: 'Toggle all 3
|
|
95
|
+
description: 'Toggle all 3 Tersio modes at once. Usage: /combo <off|medium|balanced|max|status>',
|
|
73
96
|
handler: async (args, ctx) => {
|
|
74
97
|
listen(ctx);
|
|
75
98
|
const arg = String(args || '').trim().toLowerCase();
|
|
@@ -113,26 +136,13 @@ export default function comboToggleExtension(pi) {
|
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
138
|
pi.on('session_start', async (_event, ctx) => {
|
|
116
|
-
track(ctx);
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
track(ctx, true);
|
|
140
|
+
if (!isComboSetupComplete() && ctx?.hasUI && ctx.ui?.select)
|
|
141
|
+
await runFirstRunSetup(ctx);
|
|
142
|
+
// The configured defaults apply when a fresh session has no persisted mode state.
|
|
119
143
|
const entries = sessionEntries(ctx);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const fallback = readComboDefault();
|
|
123
|
-
if (fallback !== 'off') {
|
|
124
|
-
persistPreset(fallback);
|
|
125
|
-
setSharedComboLevel(fallback);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// No sibling restores a standalone ponytail default, so apply it here; skip it when the active preset already writes the same mode.
|
|
129
|
-
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
130
|
-
const ponytailFallback = readPonytailDefault();
|
|
131
|
-
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
132
|
-
pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
|
|
133
|
-
setSharedComboMode('ponytail', ponytailFallback);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
144
|
+
if (!hasModeState(entries) && (ctx?.hasUI || OPENCODE))
|
|
145
|
+
restoreConfiguredDefaults();
|
|
136
146
|
// Announced last, so any default above is reflected.
|
|
137
147
|
announceStatus(ctx);
|
|
138
148
|
});
|
|
@@ -159,7 +169,7 @@ export default function comboToggleExtension(pi) {
|
|
|
159
169
|
// The previous level's block is replaced, not stacked on.
|
|
160
170
|
const stale = lastInjected;
|
|
161
171
|
lastInjected = instruction;
|
|
162
|
-
return injectPromptText(pi, event, instruction, stale);
|
|
172
|
+
return injectPromptText(pi, event, { text: instruction, staleMarker: stale, sectionTag: 'ponytail' });
|
|
163
173
|
});
|
|
164
174
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|
|
165
175
|
}
|
|
@@ -19,7 +19,20 @@ export function homeDir() {
|
|
|
19
19
|
}
|
|
20
20
|
export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
|
|
21
21
|
export const CAVEMAN_REMOTE_ULTRA = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/ultracave/SKILL.md';
|
|
22
|
-
export const
|
|
22
|
+
export const CAVEMAN_REMOTE_MEGACAVE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/megacave/SKILL.md';
|
|
23
|
+
// A fetch that returns an error page or a truncated body must not reach the extension tree.
|
|
24
|
+
export function ruleBodyProblem(text) {
|
|
25
|
+
const trimmed = text.trim();
|
|
26
|
+
if (trimmed === '')
|
|
27
|
+
return 'empty body';
|
|
28
|
+
if (/^\s*<(!doctype|html)/i.test(trimmed))
|
|
29
|
+
return 'HTML page, not markdown';
|
|
30
|
+
if (/^\s*404\b/im.test(trimmed))
|
|
31
|
+
return 'upstream 404 page';
|
|
32
|
+
if (trimmed.length < 800)
|
|
33
|
+
return `truncated (${trimmed.length} chars)`;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
23
36
|
const RTK_PLATFORM_SPECS = {
|
|
24
37
|
'win32/x64': { triple: 'x86_64-pc-windows-msvc', ext: '.zip', binary: 'rtk.exe' },
|
|
25
38
|
'linux/x64': { triple: 'x86_64-unknown-linux-musl', ext: '.tar.gz', binary: 'rtk' },
|
|
@@ -55,6 +68,18 @@ export function resolveHostBinary(name, managedDir = path.join(homeDir(), '.bun'
|
|
|
55
68
|
export function rtkPlatformSpec(platform = process.platform, arch = process.arch) {
|
|
56
69
|
return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
|
|
57
70
|
}
|
|
71
|
+
// Walk up to the nearest package.json; identical for the source and compiled trees.
|
|
72
|
+
export function findPackageRoot(fromDir) {
|
|
73
|
+
let dir = fromDir;
|
|
74
|
+
for (;;) {
|
|
75
|
+
if (existsSync(path.join(dir, 'package.json')))
|
|
76
|
+
return dir;
|
|
77
|
+
const parent = path.dirname(dir);
|
|
78
|
+
if (parent === dir)
|
|
79
|
+
throw new Error('package root not found');
|
|
80
|
+
dir = parent;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
58
83
|
// The 3xx+location redirect rule shared by httpsGet/httpsDownload.
|
|
59
84
|
function redirectNext(res, url) {
|
|
60
85
|
if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import cavemanSessionExtension from '../caveman-session/index.js';
|
|
4
4
|
import rtkSessionExtension from '../rtk-session/index.js';
|
|
5
|
+
import rtkFilterExtension, { FILTER_BY_TOOL, MIN_FILTER_BYTES, filterThroughRtk, filterableText, rtkActive } from '../rtk-filter/index.js';
|
|
5
6
|
import comboToggleExtension from '../combo-toggle/index.js';
|
|
6
7
|
import tersioCommandsExtension from '../tersio-commands/index.js';
|
|
7
8
|
import { getSharedComboState } from '../shared/session-state.js';
|
|
@@ -35,6 +36,7 @@ const plugin = {
|
|
|
35
36
|
return list;
|
|
36
37
|
};
|
|
37
38
|
const persist = (sid) => {
|
|
39
|
+
// SAFETY: the storage API is generic over its payload; the value round-trips unchanged.
|
|
38
40
|
void ctx.storage.set(`entries:${sid}`, sessionEntriesOf(sid)).catch(() => { });
|
|
39
41
|
};
|
|
40
42
|
const stubCtx = (sid) => ({
|
|
@@ -110,7 +112,7 @@ const plugin = {
|
|
|
110
112
|
lifecycle.set(event, list);
|
|
111
113
|
},
|
|
112
114
|
};
|
|
113
|
-
for (const extension of [cavemanSessionExtension, rtkSessionExtension, comboToggleExtension, tersioCommandsExtension])
|
|
115
|
+
for (const extension of [cavemanSessionExtension, rtkSessionExtension, rtkFilterExtension, comboToggleExtension, tersioCommandsExtension])
|
|
114
116
|
extension(host);
|
|
115
117
|
// The first hook of an unseen session runs its session_start handlers.
|
|
116
118
|
const sidOf = (value) => typeof value === 'string' && value ? value : undefined;
|
|
@@ -200,8 +202,50 @@ const plugin = {
|
|
|
200
202
|
if (rewritten)
|
|
201
203
|
input.command = rewritten;
|
|
202
204
|
});
|
|
205
|
+
// Built-in grep and glob output never reaches the bash hook; mutate result.content in place.
|
|
206
|
+
try {
|
|
207
|
+
await ctx.tool.hook('execute.after', async (event) => {
|
|
208
|
+
const hook = event;
|
|
209
|
+
if (hook.status !== 'completed')
|
|
210
|
+
return;
|
|
211
|
+
const filter = FILTER_BY_TOOL.get(String(hook.tool ?? '').toLowerCase());
|
|
212
|
+
if (!filter || !hook.result)
|
|
213
|
+
return;
|
|
214
|
+
const text = filterableText(hook.result.content);
|
|
215
|
+
if (text === null || text.length < MIN_FILTER_BYTES)
|
|
216
|
+
return;
|
|
217
|
+
// Read the mode from the payload's own session, not the last one to run a start hook.
|
|
218
|
+
const sid = typeof hook.sessionID === 'string' ? hook.sessionID : activeSid;
|
|
219
|
+
if (sid !== undefined)
|
|
220
|
+
await ensureSession(sid);
|
|
221
|
+
if (!rtkActive(sid === undefined ? undefined : sessionEntriesOf(sid)))
|
|
222
|
+
return;
|
|
223
|
+
const bin = resolveRtkBinary();
|
|
224
|
+
if (!bin)
|
|
225
|
+
return;
|
|
226
|
+
const filtered = await filterThroughRtk(execRtk, { bin, filter, text });
|
|
227
|
+
if (filtered)
|
|
228
|
+
hook.result.content = [{ type: 'text', text: filtered }];
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Hosts without the after-hook keep the unfiltered result.
|
|
233
|
+
}
|
|
203
234
|
},
|
|
204
235
|
};
|
|
236
|
+
/** execFile with the promise shape `filterThroughRtk` expects. */
|
|
237
|
+
function execRtk(cmd, args, opts) {
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
execFile(cmd, args, { signal: opts?.signal, timeout: opts?.timeout }, (error, stdout) => {
|
|
240
|
+
const out = stdout ?? '';
|
|
241
|
+
if (!error)
|
|
242
|
+
return resolve({ stdout: out, code: 0 });
|
|
243
|
+
// A non-numeric code is a spawn failure (ENOENT, EACCES, killed) and must not read as success.
|
|
244
|
+
const raw = error.code;
|
|
245
|
+
resolve({ stdout: out, code: typeof raw === 'number' ? raw : 1 });
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
205
249
|
function rewriteCommand(command) {
|
|
206
250
|
const bin = resolveRtkBinary();
|
|
207
251
|
if (!bin)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// pi's built-in grep and glob never reach the bash hook; `read` is absent because rtk has no filter for it.
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { resolveRtkBinary } from '../lib/utils.js';
|
|
6
|
+
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
7
|
+
import { getSharedComboState, lastCustomValue, sessionEntries } from '../shared/session-state.js';
|
|
8
|
+
/** Tool name to `rtk pipe --filter` name. Exported so the OpenCode host reuses one map. */
|
|
9
|
+
export const FILTER_BY_TOOL = new Map([
|
|
10
|
+
['grep', 'grep'],
|
|
11
|
+
['glob', 'find'],
|
|
12
|
+
['find', 'find'],
|
|
13
|
+
]);
|
|
14
|
+
/** Below this the rtk spawn costs more than the filter saves. */
|
|
15
|
+
export const MIN_FILTER_BYTES = 1200;
|
|
16
|
+
const FILTER_TIMEOUT_MS = 5000;
|
|
17
|
+
/** Absolute, so a minimal PATH in the host cannot break the spawn. */
|
|
18
|
+
const SHELL = '/bin/sh';
|
|
19
|
+
function modeOf(value) {
|
|
20
|
+
if (value === true)
|
|
21
|
+
return true;
|
|
22
|
+
if (value === false)
|
|
23
|
+
return false;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** True when rtk is on for the session: a persisted entry wins, then a live switch, then the configured default. */
|
|
27
|
+
export function rtkActive(entries) {
|
|
28
|
+
const persisted = lastCustomValue(entries, 'rtk-mode', (data) => modeOf(data?.enabled));
|
|
29
|
+
if (persisted !== null)
|
|
30
|
+
return persisted;
|
|
31
|
+
return getSharedComboState().rtk === 'on' || readRtkDefault();
|
|
32
|
+
}
|
|
33
|
+
/** The text in a result's content parts, or null when the parts are not plain text. */
|
|
34
|
+
export function filterableText(parts) {
|
|
35
|
+
if (!Array.isArray(parts) || parts.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
// An image or a mixed result is not text to filter.
|
|
38
|
+
if (parts.some((part) => part?.type !== 'text'))
|
|
39
|
+
return null;
|
|
40
|
+
return parts.map((part) => part?.text ?? '').join('\n');
|
|
41
|
+
}
|
|
42
|
+
/** Pipe text through one rtk filter; null on any failure, so the caller keeps its original. */
|
|
43
|
+
export async function filterThroughRtk(exec, request) {
|
|
44
|
+
const { bin, filter, text, signal } = request;
|
|
45
|
+
let dir;
|
|
46
|
+
try {
|
|
47
|
+
dir = mkdtempSync(join(tmpdir(), 'tersio-rtk-'));
|
|
48
|
+
const file = join(dir, 'in.txt');
|
|
49
|
+
writeFileSync(file, text, 'utf8');
|
|
50
|
+
// `rtk pipe` reads stdin only and execFile has no stdin, so one redirect.
|
|
51
|
+
const result = await exec(SHELL, ['-c', `"${bin}" pipe -f ${filter} < "${file}"`], { signal, timeout: FILTER_TIMEOUT_MS });
|
|
52
|
+
if (result.code !== 0)
|
|
53
|
+
return null;
|
|
54
|
+
const out = result.stdout;
|
|
55
|
+
if (!out.trim() || out.length >= text.length)
|
|
56
|
+
return null;
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (dir)
|
|
64
|
+
rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export default function rtkFilterExtension(pi) {
|
|
68
|
+
const bin = resolveRtkBinary();
|
|
69
|
+
const exec = pi.exec;
|
|
70
|
+
if (!bin || !exec)
|
|
71
|
+
return;
|
|
72
|
+
pi.on('tool_result', async (event, ctx) => {
|
|
73
|
+
const filter = FILTER_BY_TOOL.get(String(event?.toolName ?? '').toLowerCase());
|
|
74
|
+
if (!filter || event?.isError)
|
|
75
|
+
return;
|
|
76
|
+
const text = filterableText(event?.content);
|
|
77
|
+
if (text === null || text.length < MIN_FILTER_BYTES)
|
|
78
|
+
return;
|
|
79
|
+
if (!rtkActive(sessionEntries(ctx)))
|
|
80
|
+
return;
|
|
81
|
+
const filtered = await filterThroughRtk(exec, { bin, filter, text, signal: ctx?.signal });
|
|
82
|
+
if (!filtered)
|
|
83
|
+
return;
|
|
84
|
+
// Built-in grep and glob declare no output schema, so no structuredContent is lost.
|
|
85
|
+
return { content: [{ type: 'text', text: filtered }] };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -22,7 +22,8 @@ function setRtkProcessEnabled(enabled) {
|
|
|
22
22
|
else
|
|
23
23
|
process.env.RTK_DISABLED = '1';
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
// rtk's awareness contract: state the output contract, never tell the model to type `rtk`.
|
|
26
|
+
const RTK_PROMPT = `RTK guidance active. Bash output, and large grep and glob results, are condensed to save tokens: every signal kept, costly noise dropped. Treat a condensed result as complete. Re-run something unfiltered only when its result is empty, garbled, or contradicts its exit code.`;
|
|
26
27
|
export default function rtkSessionExtension(pi) {
|
|
27
28
|
let enabled = DEFAULT_ENABLED;
|
|
28
29
|
function fail(text) {
|
|
@@ -125,6 +126,6 @@ export default function rtkSessionExtension(pi) {
|
|
|
125
126
|
// Skip when already present, so a host that re-presents the prompt cannot stack it.
|
|
126
127
|
if (systemPromptIncludes(event.systemPrompt, RTK_PROMPT))
|
|
127
128
|
return;
|
|
128
|
-
return injectPromptText(pi, event, RTK_PROMPT);
|
|
129
|
+
return injectPromptText(pi, event, { text: RTK_PROMPT, sectionTag: 'rtk' });
|
|
129
130
|
});
|
|
130
131
|
}
|
|
@@ -16,15 +16,21 @@ const piAdapter = {
|
|
|
16
16
|
return undefined;
|
|
17
17
|
return options[shown.indexOf(picked)]?.label ?? picked;
|
|
18
18
|
},
|
|
19
|
-
inject(event,
|
|
20
|
-
const marker = staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
19
|
+
inject(event, block) {
|
|
20
|
+
const marker = block.staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
21
21
|
const dropStale = (parts) => (marker ? parts.filter((part) => !part.includes(marker)) : parts);
|
|
22
22
|
const options = event.systemPromptOptions;
|
|
23
|
+
// A named section is replaced in place; returning systemPrompt replaces the whole prompt.
|
|
24
|
+
const sections = options?.sections;
|
|
25
|
+
if (block.sectionTag && sections && typeof sections === 'object') {
|
|
26
|
+
sections[block.sectionTag] = block.text;
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
23
29
|
if (options && typeof options.appendSystemPrompt === 'string') {
|
|
24
|
-
options.appendSystemPrompt = dropStale([options.appendSystemPrompt]).concat(text).filter(Boolean).join('\n\n');
|
|
30
|
+
options.appendSystemPrompt = dropStale([options.appendSystemPrompt]).concat(block.text).filter(Boolean).join('\n\n');
|
|
25
31
|
return undefined;
|
|
26
32
|
}
|
|
27
|
-
return { systemPrompt: `${dropStale(asPromptArray(event.systemPrompt)).join('\n\n')}\n\n${text}` };
|
|
33
|
+
return { systemPrompt: `${dropStale(asPromptArray(event.systemPrompt)).join('\n\n')}\n\n${block.text}` };
|
|
28
34
|
},
|
|
29
35
|
stringArray(_pi, name, spec) {
|
|
30
36
|
const array = { type: 'array', items: { type: 'string' } };
|
|
@@ -43,10 +49,10 @@ const ompAdapter = {
|
|
|
43
49
|
return undefined;
|
|
44
50
|
return ui.select(title, options, dialogOptions);
|
|
45
51
|
},
|
|
46
|
-
inject(event,
|
|
47
|
-
const marker = staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
52
|
+
inject(event, block) {
|
|
53
|
+
const marker = block.staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
48
54
|
const dropStale = (parts) => (marker ? parts.filter((part) => !part.includes(marker)) : parts);
|
|
49
|
-
return { systemPrompt: [...dropStale(asPromptArray(event.systemPrompt)), text] };
|
|
55
|
+
return { systemPrompt: [...dropStale(asPromptArray(event.systemPrompt)), block.text] };
|
|
50
56
|
},
|
|
51
57
|
stringArray(pi, name, spec) {
|
|
52
58
|
const { z } = pi.zod;
|
|
@@ -63,10 +69,10 @@ const opencodeAdapter = {
|
|
|
63
69
|
return undefined;
|
|
64
70
|
return ui.select(title, options, dialogOptions);
|
|
65
71
|
},
|
|
66
|
-
inject(event,
|
|
67
|
-
const marker = staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
72
|
+
inject(event, block) {
|
|
73
|
+
const marker = block.staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
68
74
|
const dropStale = (parts) => (marker ? parts.filter((part) => !part.includes(marker)) : parts);
|
|
69
|
-
return { systemPrompt: [...dropStale(asPromptArray(event.systemPrompt)), text] };
|
|
75
|
+
return { systemPrompt: [...dropStale(asPromptArray(event.systemPrompt)), block.text] };
|
|
70
76
|
},
|
|
71
77
|
stringArray(_pi, name, spec) {
|
|
72
78
|
const array = { type: 'array', items: { type: 'string' } };
|
|
@@ -99,8 +105,8 @@ export function onHostEvent(pi, event, handler) {
|
|
|
99
105
|
export async function hostSelect(pi, ui, title, options, dialogOptions) {
|
|
100
106
|
return adapterFor(pi).select(ui, title, options, dialogOptions);
|
|
101
107
|
}
|
|
102
|
-
export function injectPromptText(pi, event,
|
|
103
|
-
return adapterFor(pi).inject(event,
|
|
108
|
+
export function injectPromptText(pi, event, block) {
|
|
109
|
+
return adapterFor(pi).inject(event, block);
|
|
104
110
|
}
|
|
105
111
|
export function stringArrayToolParams(pi, name, spec = {}) {
|
|
106
112
|
return adapterFor(pi).stringArray(pi, name, spec);
|
|
@@ -43,7 +43,7 @@ export function readPluginSettings() {
|
|
|
43
43
|
return readSettingsFile() ?? legacyOmpSettings() ?? {};
|
|
44
44
|
}
|
|
45
45
|
const COMBO_LEVELS = new Set(['off', 'medium', 'balanced', 'max']);
|
|
46
|
-
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
|
|
46
|
+
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'megacave', 'megacave-lite', 'megacave-full', 'megacave-ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
|
|
47
47
|
const PONYTAIL_MODES = new Set(['off', 'lite', 'full', 'ultra']);
|
|
48
48
|
function readStringDefault(key, valid) {
|
|
49
49
|
const raw = readPluginSettings()[key];
|
|
@@ -9,10 +9,12 @@ export const COMBO_LEVELS = Object.freeze({
|
|
|
9
9
|
max: Object.freeze({ level: 'max', caveman: 'ultra', rtk: 'on', ponytail: 'ultra' }),
|
|
10
10
|
});
|
|
11
11
|
const MODE_VALUES = {
|
|
12
|
-
caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']),
|
|
12
|
+
caveman: new Set(['off', 'lite', 'full', 'ultra', 'megacave', 'megacave-lite', 'megacave-full', 'megacave-ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']),
|
|
13
13
|
rtk: new Set(['off', 'on']),
|
|
14
14
|
ponytail: new Set(['off', 'lite', 'full', 'ultra']),
|
|
15
15
|
};
|
|
16
|
+
// Upstream renamed wenyan to megacave and kept the old spelling as an alias.
|
|
17
|
+
const CAVEMAN_LEGACY_PREFIX = /^wenyan/;
|
|
16
18
|
const MODE_ENTRY_TYPES = {
|
|
17
19
|
'caveman-mode': 'caveman',
|
|
18
20
|
'rtk-mode': 'rtk',
|
|
@@ -29,7 +31,10 @@ export function normalizeMode(name, value) {
|
|
|
29
31
|
const mode = String(value ?? '').trim().toLowerCase();
|
|
30
32
|
if (!MODE_VALUES[name]?.has(mode))
|
|
31
33
|
return null;
|
|
32
|
-
|
|
34
|
+
if (name !== 'caveman')
|
|
35
|
+
return mode;
|
|
36
|
+
const canonical = mode.replace(CAVEMAN_LEGACY_PREFIX, 'megacave');
|
|
37
|
+
return canonical === 'megacave' ? 'megacave-full' : canonical;
|
|
33
38
|
}
|
|
34
39
|
export function deriveLevel(modes) {
|
|
35
40
|
for (const level of Object.keys(COMBO_LEVELS)) {
|
|
@@ -71,7 +76,9 @@ export function normalizeInputCommand(value) {
|
|
|
71
76
|
return String(value || '').trim().toLowerCase().replace(/[.!?\s]+$/, '');
|
|
72
77
|
}
|
|
73
78
|
export function asPromptArray(systemPrompt) {
|
|
74
|
-
|
|
79
|
+
const parts = Array.isArray(systemPrompt) ? systemPrompt : [systemPrompt];
|
|
80
|
+
// OMP can hand back a hole in the array; a non-string breaks every marker check.
|
|
81
|
+
return parts.filter((part) => typeof part === 'string');
|
|
75
82
|
}
|
|
76
83
|
export function systemPromptIncludes(systemPrompt, marker) {
|
|
77
84
|
return asPromptArray(systemPrompt).some((prompt) => typeof prompt === 'string' && prompt.includes(marker));
|
|
@@ -127,6 +134,13 @@ export function setSharedComboMode(name, value) {
|
|
|
127
134
|
// Derive the level from the final triplet so a redundant same-value write cannot strand a matching preset at 'custom' and drop the combo bar.
|
|
128
135
|
return publish(normalizedState(modes));
|
|
129
136
|
}
|
|
137
|
+
export function setSharedComboModes(modes) {
|
|
138
|
+
return publish(normalizedState({
|
|
139
|
+
caveman: normalizeMode('caveman', modes.caveman) || 'off',
|
|
140
|
+
rtk: normalizeMode('rtk', modes.rtk) || 'off',
|
|
141
|
+
ponytail: normalizeMode('ponytail', modes.ponytail) || 'off',
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
130
144
|
export function reconcileSharedComboEntries(entries) {
|
|
131
145
|
let modes = { ...COMBO_LEVELS.off };
|
|
132
146
|
if (Array.isArray(entries)) {
|
|
@@ -8,6 +8,10 @@ export function formatStatus(state) {
|
|
|
8
8
|
const label = level === 'custom' ? 'CUSTOM' : level.toUpperCase();
|
|
9
9
|
return `🧩 combo ${label}: 🪨caveman=${caveman.toUpperCase()} ⚡rtk=${rtk.toUpperCase()} 🦥ponytail=${ponytail.toUpperCase()}`;
|
|
10
10
|
}
|
|
11
|
+
// The string upstream's skill tells the model to relay for `/caveman status`.
|
|
12
|
+
export function formatCavemanStatus(mode) {
|
|
13
|
+
return `Caveman mode: ${mode && mode !== 'off' ? mode : 'unknown'}`;
|
|
14
|
+
}
|
|
11
15
|
// Stale ctx access throws, so never let it abort a mode restore.
|
|
12
16
|
function statusUi(ctx) {
|
|
13
17
|
try {
|
|
@@ -24,7 +28,8 @@ let lastText;
|
|
|
24
28
|
let lastUi;
|
|
25
29
|
// Paint the host footer bar; same text on the same UI is skipped, a new ctx repaints it.
|
|
26
30
|
export function announceStatus(ctx) {
|
|
27
|
-
const
|
|
31
|
+
const state = getSharedComboState();
|
|
32
|
+
const text = formatStatus(state);
|
|
28
33
|
const ui = statusUi(ctx) ?? statusUi(remembered);
|
|
29
34
|
if (statusUi(ctx))
|
|
30
35
|
remembered = ctx;
|
|
@@ -35,4 +40,5 @@ export function announceStatus(ctx) {
|
|
|
35
40
|
lastText = text;
|
|
36
41
|
lastUi = ui;
|
|
37
42
|
ui.setStatus?.('tersio', text);
|
|
43
|
+
ui.setStatus?.('caveman', formatCavemanStatus(state.caveman));
|
|
38
44
|
}
|
|
@@ -32,7 +32,7 @@ Options:
|
|
|
32
32
|
--keep-ponytail (uninstall: keep the bundled Ponytail copy — removed by default)
|
|
33
33
|
--remove-rtk (uninstall: also remove the RTK binary; its OMP wiring always goes)
|
|
34
34
|
--combo-default off|medium|balanced|max (implies caveman, rtk, ponytail)
|
|
35
|
-
--caveman-default off|lite|full|ultra|
|
|
35
|
+
--caveman-default off|lite|full|ultra|megacave-lite|megacave-full|megacave-ultra (override; default follows combo)
|
|
36
36
|
--ponytail-default off|lite|full|ultra (override; default follows combo)
|
|
37
37
|
--rtk-default on|off (override; default follows combo)
|
|
38
38
|
--yes, -y
|
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
import { setExtensionLabel } from '../shared/host.ts';
|
|
11
11
|
import {
|
|
12
12
|
CAVEMAN_REMOTE_ULTRA,
|
|
13
|
-
|
|
13
|
+
CAVEMAN_REMOTE_MEGACAVE,
|
|
14
14
|
RTK_RELEASE_API,
|
|
15
15
|
RtkRelease,
|
|
16
16
|
fetchJson,
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
findHoistedPackage,
|
|
19
19
|
httpsGet,
|
|
20
20
|
httpsDownload,
|
|
21
|
-
isPiProcess,
|
|
22
21
|
sha256Hex,
|
|
23
22
|
parseChecksum,
|
|
24
23
|
normalizeRtkVersion,
|
|
@@ -37,10 +36,10 @@ const cavemanRule = (name: string) => path.join(EXTENSION_DIR, '..', 'caveman-se
|
|
|
37
36
|
const CAVEMAN_LOCAL = cavemanRule('rule.md');
|
|
38
37
|
const CAVEMAN_TRACKED: ReadonlyArray<{ file: string; remote: string }> = [
|
|
39
38
|
{ file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
|
|
40
|
-
{ file: cavemanRule('rule-
|
|
39
|
+
{ file: cavemanRule('rule-megacave.md'), remote: CAVEMAN_REMOTE_MEGACAVE },
|
|
41
40
|
];
|
|
42
|
-
|
|
43
|
-
const RELOAD_MSG =
|
|
41
|
+
// Host-neutral: this updater runs inside pi, OMP, and OpenCode alike.
|
|
42
|
+
const RELOAD_MSG = 'Reminder: restart your agent (or reload extensions) for updates to take effect.';
|
|
44
43
|
|
|
45
44
|
// Ponytail is a hoisted dependency, so the installed copy is found by walking up rather than by naming a host directory.
|
|
46
45
|
function ponytailLocal(): string | null {
|