@krtclcdy/tersio 2.25.2 → 2.25.3
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 +1 -1
- 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 +1 -3
- package/{cli → dist/cli}/doctor.js +12 -5
- package/dist/cli/headroom-install.js +266 -0
- package/dist/cli/headroom.js +492 -0
- package/{cli → dist/cli}/install.js +19 -7
- package/{cli → dist/cli}/manifest.js +3 -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 +2 -2
- package/{extensions → dist/extensions}/caveman-session/index.js +2 -2
- 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 +37 -27
- package/dist/extensions/headroom-session/index.js +93 -0
- package/{extensions → dist/extensions}/lib/utils.js +26 -1
- package/dist/extensions/shared/headroom.js +131 -0
- package/{extensions → dist/extensions}/shared/plugin-settings.js +1 -1
- package/{extensions → dist/extensions}/shared/session-state.js +14 -2
- package/{extensions → dist/extensions}/shared/status.js +7 -1
- package/{tersio.js → dist/tersio.js} +1 -1
- package/extensions/ai-addons-updater/index.ts +2 -2
- package/extensions/caveman-session/index.ts +2 -2
- package/extensions/caveman-session/rule-megacave.md +63 -0
- package/extensions/caveman-session/rule.md +4 -2
- package/extensions/combo-toggle/index.ts +35 -27
- package/extensions/lib/utils.ts +22 -1
- package/extensions/shared/plugin-settings.ts +1 -1
- package/extensions/shared/session-state.ts +15 -2
- package/extensions/shared/status.ts +8 -1
- package/package.json +15 -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}/opencode/server.js +0 -0
- /package/{extensions → dist/extensions}/rtk-session/index.js +0 -0
- /package/{extensions → dist/extensions}/shared/carbon.js +0 -0
- /package/{extensions → dist/extensions}/shared/host.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
|
@@ -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,
|
|
@@ -37,7 +37,7 @@ const cavemanRule = (name: string) => path.join(EXTENSION_DIR, '..', 'caveman-se
|
|
|
37
37
|
const CAVEMAN_LOCAL = cavemanRule('rule.md');
|
|
38
38
|
const CAVEMAN_TRACKED: ReadonlyArray<{ file: string; remote: string }> = [
|
|
39
39
|
{ file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
|
|
40
|
-
{ file: cavemanRule('rule-
|
|
40
|
+
{ file: cavemanRule('rule-megacave.md'), remote: CAVEMAN_REMOTE_MEGACAVE },
|
|
41
41
|
];
|
|
42
42
|
const HOST_NAME = isPiProcess() ? 'Pi' : 'OMP';
|
|
43
43
|
const RELOAD_MSG = `Reminder: restart ${HOST_NAME} (or reload extensions) for updates to take effect.`;
|
|
@@ -9,7 +9,7 @@ import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPrompt
|
|
|
9
9
|
|
|
10
10
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
// Upstream ships one skill per register: default, ultracave, megacave.
|
|
12
|
-
const RULE_FILES = { lite: 'rule.md', full: 'rule.md', ultra: 'rule-ultra.md', '
|
|
12
|
+
const RULE_FILES = { lite: 'rule.md', full: 'rule.md', ultra: 'rule-ultra.md', 'megacave-lite': 'rule-megacave.md', 'megacave-full': 'rule-megacave.md', 'megacave-ultra': 'rule-megacave.md' };
|
|
13
13
|
|
|
14
14
|
const DEFAULT_MODE = 'off';
|
|
15
15
|
|
|
@@ -67,7 +67,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
if (!setMode(arg, ctx)) {
|
|
70
|
-
ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|
|
|
70
|
+
ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|megacave-lite|megacave-full|megacave-ultra|off|status]', 'warning');
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
73
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: megacave
|
|
3
|
+
description: >
|
|
4
|
+
Caveman in Classical Chinese: 文言文 register, far fewer characters, technical
|
|
5
|
+
terms verbatim. Invoke only with /megacave or /caveman wenyan. Stays on until
|
|
6
|
+
"stop caveman" or "normal mode".
|
|
7
|
+
disable-model-invocation: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# megacave
|
|
11
|
+
|
|
12
|
+
以文言答。技術之實皆存,唯贅言去之。
|
|
13
|
+
|
|
14
|
+
Megacave is caveman in Classical Chinese. 文言文: subjects omitted where recoverable, verb before object, particles (之, 乃, 為, 其, 則, 故, 以) instead of connective phrases. Fewer characters on screen. Token savings are small and noisy; never claim them.
|
|
15
|
+
|
|
16
|
+
## Persistence
|
|
17
|
+
|
|
18
|
+
Every response, whole session, until "stop caveman" or "normal mode". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value or say `Caveman mode: unknown`.
|
|
19
|
+
|
|
20
|
+
## Floor
|
|
21
|
+
|
|
22
|
+
Never cut or translate: code, commands, paths, API names, error strings, in their original script. Negation (不, 非, 勿, 未, 毋). Numbers and units, Arabic numerals. One term per thing. A clause ambiguous in 文言 becomes 白話.
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
### 1. Classical register
|
|
27
|
+
|
|
28
|
+
文言, not 白話. Verb before object. Subjects omitted where recoverable.
|
|
29
|
+
|
|
30
|
+
Bad: "你的組件每次渲染都會重新渲染,因為你每次都創建了新的對象引用。請用 useMemo 包起來。"
|
|
31
|
+
Good: "每繪新生對象參照,故重繪;以 `useMemo` 包之則免。"
|
|
32
|
+
|
|
33
|
+
### 2. Particles carry structure
|
|
34
|
+
|
|
35
|
+
One particle, one relation.
|
|
36
|
+
|
|
37
|
+
Bad: "因為連接池會重複使用已經打開的連接,所以不需要每次請求都新建連接,這樣就省去了握手的開銷。"
|
|
38
|
+
Good: "池蓄已開之連,不逐請而新開,省握手之費。"
|
|
39
|
+
|
|
40
|
+
### 3. Each fact once
|
|
41
|
+
|
|
42
|
+
Good: "新參照則重繪。`useMemo` 包之。"
|
|
43
|
+
|
|
44
|
+
### 4. Payload in original script
|
|
45
|
+
|
|
46
|
+
Bad: "以記憶化鉤子包之。"
|
|
47
|
+
Good: "以 `useMemo` 包之。"
|
|
48
|
+
|
|
49
|
+
### 5. Tool runs
|
|
50
|
+
|
|
51
|
+
One 文言 line in, one out. Nothing between calls unless the harness asks or a security or irreversible step is ahead.
|
|
52
|
+
|
|
53
|
+
### 6. Never perform
|
|
54
|
+
|
|
55
|
+
No "文言模式啟", no modern answer plus classical copy. 文言 not shorter than 白話 for a sentence? Use 白話.
|
|
56
|
+
|
|
57
|
+
## When to break the rules
|
|
58
|
+
|
|
59
|
+
白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirm first. Any clause with two readings. User confused. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.
|
|
60
|
+
|
|
61
|
+
## Pre-send check
|
|
62
|
+
|
|
63
|
+
Identifiers verbatim, original script? Negations present? Two readings? 白話. Flavor character? Cut.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: caveman
|
|
3
3
|
description: >
|
|
4
4
|
Ultra-compressed communication mode that cuts output tokens while keeping
|
|
5
|
-
technical accuracy. Levels: lite, full, ultra and the
|
|
5
|
+
technical accuracy. Levels: lite, full, ultra and the megacave variants. Use for
|
|
6
6
|
/caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
|
|
12
12
|
|
|
13
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
14
|
|
|
15
|
-
Default: **full**. Switch: `/caveman lite|full|ultra|
|
|
15
|
+
Default: **full**. Switch: `/caveman lite|full|ultra|megacave-lite|megacave-full|megacave-ultra|off`.
|
|
16
16
|
|
|
17
17
|
## Rules
|
|
18
18
|
|
|
@@ -28,6 +28,8 @@ Tool calls: fire direct. No preamble, plan, or progress note before or between c
|
|
|
28
28
|
|
|
29
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
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
|
+
|
|
31
33
|
'Drop articles' = article languages only. Where small markers carry case/role (particles, postpositions), keep them grammar, not filler; compress politeness/filler instead.
|
|
32
34
|
|
|
33
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.
|
|
@@ -13,15 +13,18 @@ import {
|
|
|
13
13
|
sessionEntries,
|
|
14
14
|
setSharedComboLevel,
|
|
15
15
|
setSharedComboListener,
|
|
16
|
-
|
|
16
|
+
setSharedComboModes,
|
|
17
17
|
systemPromptIncludes,
|
|
18
18
|
} from '../shared/session-state.ts';
|
|
19
19
|
import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
|
|
20
20
|
import { announceStatus } from '../shared/status.ts';
|
|
21
21
|
import {
|
|
22
22
|
isComboSetupComplete,
|
|
23
|
+
readCavemanDefault,
|
|
23
24
|
readComboDefault,
|
|
25
|
+
readPluginSettings,
|
|
24
26
|
readPonytailDefault,
|
|
27
|
+
readRtkDefault,
|
|
25
28
|
saveComboSetup,
|
|
26
29
|
} from '../shared/plugin-settings.ts';
|
|
27
30
|
import { findHoistedPackage, isPiProcess } from '../lib/utils.ts';
|
|
@@ -64,7 +67,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
64
67
|
|
|
65
68
|
let setupPrompted = false;
|
|
66
69
|
let lastInjected: string | undefined = undefined;
|
|
67
|
-
|
|
70
|
+
// opencode has no UI event stream, so only its empty branch reconciles.
|
|
71
|
+
const OPENCODE = pi.hostId === 'opencode';
|
|
68
72
|
|
|
69
73
|
// Siblings restore from these entries, so the fallback must write them too.
|
|
70
74
|
function persistPreset(level: string): void {
|
|
@@ -74,19 +78,38 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
74
78
|
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
75
79
|
pi.appendEntry?.('combo-level', { level });
|
|
76
80
|
}
|
|
81
|
+
function restoreConfiguredDefaults(): void {
|
|
82
|
+
const settings = readPluginSettings();
|
|
83
|
+
const hasModeDefaults = ['cavemanDefault', 'rtkDefault', 'ponytailDefault'].some((key) => key in settings);
|
|
84
|
+
const combo = readComboDefault();
|
|
85
|
+
const preset = COMBO_LEVELS[combo];
|
|
86
|
+
const modes = hasModeDefaults
|
|
87
|
+
? { caveman: readCavemanDefault(), rtk: readRtkDefault() ? 'on' : 'off', ponytail: readPonytailDefault() }
|
|
88
|
+
: preset;
|
|
89
|
+
setSharedComboModes(modes);
|
|
90
|
+
if (modes.caveman === 'off' && modes.rtk === 'off' && modes.ponytail === 'off') return;
|
|
91
|
+
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
92
|
+
pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
|
|
93
|
+
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
94
|
+
if (preset && modes.caveman === preset.caveman && modes.rtk === preset.rtk && modes.ponytail === preset.ponytail) {
|
|
95
|
+
pi.appendEntry?.('combo-level', { level: combo });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
77
99
|
|
|
78
|
-
function reconcile(ctx?: ExtensionCtx): Readonly<ComboState> {
|
|
79
|
-
if (!ctx?.hasUI) return getSharedComboState();
|
|
80
|
-
|
|
100
|
+
function reconcile(ctx?: ExtensionCtx, resetEmpty = false): Readonly<ComboState> {
|
|
101
|
+
if (!ctx?.hasUI && !(resetEmpty && OPENCODE)) return getSharedComboState();
|
|
102
|
+
const entries = sessionEntries(ctx);
|
|
103
|
+
if (!resetEmpty && entries.length === 0) return getSharedComboState();
|
|
104
|
+
return reconcileSharedComboEntries(entries);
|
|
81
105
|
}
|
|
82
106
|
function listen(ctx?: ExtensionCtx): void {
|
|
83
107
|
if (!ctx?.hasUI) return;
|
|
84
108
|
setSharedComboListener('combo', (state) => { announceStatus(ctx); return state; });
|
|
85
109
|
}
|
|
86
|
-
|
|
87
|
-
function track(ctx?: ExtensionCtx): void {
|
|
110
|
+
function track(ctx?: ExtensionCtx, resetEmpty = false): void {
|
|
88
111
|
listen(ctx);
|
|
89
|
-
if (ctx?.hasUI) reconcile(ctx);
|
|
112
|
+
if (ctx?.hasUI || (resetEmpty && OPENCODE)) reconcile(ctx, resetEmpty);
|
|
90
113
|
}
|
|
91
114
|
|
|
92
115
|
pi.registerCommand?.('combo', {
|
|
@@ -145,26 +168,11 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
145
168
|
}
|
|
146
169
|
|
|
147
170
|
pi.on('session_start', async (_event, ctx) => {
|
|
148
|
-
track(ctx);
|
|
149
|
-
await runFirstRunSetup(ctx);
|
|
150
|
-
// The configured
|
|
171
|
+
track(ctx, true);
|
|
172
|
+
if (!isComboSetupComplete() && ctx?.hasUI && ctx.ui?.select) await runFirstRunSetup(ctx);
|
|
173
|
+
// The configured defaults apply when a fresh session has no persisted mode state.
|
|
151
174
|
const entries = sessionEntries(ctx);
|
|
152
|
-
|
|
153
|
-
if (getSharedComboState().level === 'off' && !persisted) {
|
|
154
|
-
const fallback = readComboDefault();
|
|
155
|
-
if (fallback !== 'off') {
|
|
156
|
-
persistPreset(fallback);
|
|
157
|
-
setSharedComboLevel(fallback);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// No sibling restores a standalone ponytail default, so apply it here; skip it when the active preset already writes the same mode.
|
|
161
|
-
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
162
|
-
const ponytailFallback = readPonytailDefault();
|
|
163
|
-
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
164
|
-
pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
|
|
165
|
-
setSharedComboMode('ponytail', ponytailFallback);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
175
|
+
if (!hasModeState(entries) && (ctx?.hasUI || OPENCODE)) restoreConfiguredDefaults();
|
|
168
176
|
// Announced last, so any default above is reflected.
|
|
169
177
|
announceStatus(ctx);
|
|
170
178
|
});
|
package/extensions/lib/utils.ts
CHANGED
|
@@ -28,7 +28,17 @@ export function homeDir(): string {
|
|
|
28
28
|
|
|
29
29
|
export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
|
|
30
30
|
export const CAVEMAN_REMOTE_ULTRA = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/ultracave/SKILL.md';
|
|
31
|
-
export const
|
|
31
|
+
export const CAVEMAN_REMOTE_MEGACAVE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/megacave/SKILL.md';
|
|
32
|
+
|
|
33
|
+
// A fetch that returns an error page or a truncated body must not reach the extension tree.
|
|
34
|
+
export function ruleBodyProblem(text: string): string | null {
|
|
35
|
+
const trimmed = text.trim();
|
|
36
|
+
if (trimmed === '') return 'empty body';
|
|
37
|
+
if (/^\s*<(!doctype|html)/i.test(trimmed)) return 'HTML page, not markdown';
|
|
38
|
+
if (/^\s*404\b/im.test(trimmed)) return 'upstream 404 page';
|
|
39
|
+
if (trimmed.length < 800) return `truncated (${trimmed.length} chars)`;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
32
42
|
|
|
33
43
|
export interface RtkPlatformSpec {
|
|
34
44
|
triple: string;
|
|
@@ -83,6 +93,17 @@ export function rtkPlatformSpec(platform: string = process.platform, arch: strin
|
|
|
83
93
|
return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
// Walk up to the nearest package.json; identical for the source and compiled trees.
|
|
97
|
+
export function findPackageRoot(fromDir: string): string {
|
|
98
|
+
let dir = fromDir;
|
|
99
|
+
for (;;) {
|
|
100
|
+
if (existsSync(path.join(dir, 'package.json'))) return dir;
|
|
101
|
+
const parent = path.dirname(dir);
|
|
102
|
+
if (parent === dir) throw new Error('package root not found');
|
|
103
|
+
dir = parent;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
// The 3xx+location redirect rule shared by httpsGet/httpsDownload.
|
|
87
108
|
function redirectNext(res: { statusCode?: number; headers: { location?: string } }, url: string): string | null {
|
|
88
109
|
if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
@@ -45,7 +45,7 @@ export function readPluginSettings(): Record<string, unknown> {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const COMBO_LEVELS = new Set(['off', 'medium', 'balanced', 'max']);
|
|
48
|
-
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
|
|
48
|
+
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'megacave', 'megacave-lite', 'megacave-full', 'megacave-ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
|
|
49
49
|
const PONYTAIL_MODES = new Set(['off', 'lite', 'full', 'ultra']);
|
|
50
50
|
|
|
51
51
|
function readStringDefault(key: string, valid: Set<string>): string {
|
|
@@ -14,11 +14,14 @@ export const COMBO_LEVELS: Record<string, Readonly<ComboState>> = Object.freeze(
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
const MODE_VALUES: Record<string, Set<string>> = {
|
|
17
|
-
caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']),
|
|
17
|
+
caveman: new Set(['off', 'lite', 'full', 'ultra', 'megacave', 'megacave-lite', 'megacave-full', 'megacave-ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']),
|
|
18
18
|
rtk: new Set(['off', 'on']),
|
|
19
19
|
ponytail: new Set(['off', 'lite', 'full', 'ultra']),
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
// Upstream renamed wenyan to megacave and kept the old spelling as an alias.
|
|
23
|
+
const CAVEMAN_LEGACY_PREFIX = /^wenyan/;
|
|
24
|
+
|
|
22
25
|
type ModeName = 'caveman' | 'rtk' | 'ponytail';
|
|
23
26
|
type Modes = Record<ModeName, string>;
|
|
24
27
|
|
|
@@ -43,7 +46,9 @@ export function normalizeMode(name: ModeName, value: unknown): string | null {
|
|
|
43
46
|
if (name === 'rtk' && typeof value === 'boolean') return value ? 'on' : 'off';
|
|
44
47
|
const mode = String(value ?? '').trim().toLowerCase();
|
|
45
48
|
if (!MODE_VALUES[name]?.has(mode)) return null;
|
|
46
|
-
|
|
49
|
+
if (name !== 'caveman') return mode;
|
|
50
|
+
const canonical = mode.replace(CAVEMAN_LEGACY_PREFIX, 'megacave');
|
|
51
|
+
return canonical === 'megacave' ? 'megacave-full' : canonical;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
export function deriveLevel(modes: Modes | null | undefined): ComboLevel {
|
|
@@ -153,6 +158,14 @@ export function setSharedComboMode(name: ModeName, value: unknown): Readonly<Com
|
|
|
153
158
|
return publish(normalizedState(modes));
|
|
154
159
|
}
|
|
155
160
|
|
|
161
|
+
export function setSharedComboModes(modes: { caveman?: unknown; rtk?: unknown; ponytail?: unknown }): Readonly<ComboState> {
|
|
162
|
+
return publish(normalizedState({
|
|
163
|
+
caveman: normalizeMode('caveman', modes.caveman) || 'off',
|
|
164
|
+
rtk: normalizeMode('rtk', modes.rtk) || 'off',
|
|
165
|
+
ponytail: normalizeMode('ponytail', modes.ponytail) || 'off',
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
156
169
|
export function reconcileSharedComboEntries(entries: SessionEntry[] | null | undefined): Readonly<ComboState> {
|
|
157
170
|
let modes: Modes = { ...COMBO_LEVELS.off };
|
|
158
171
|
if (Array.isArray(entries)) {
|
|
@@ -17,6 +17,11 @@ export function formatStatus(state: StatusState): string {
|
|
|
17
17
|
return `🧩 combo ${label}: 🪨caveman=${caveman.toUpperCase()} ⚡rtk=${rtk.toUpperCase()} 🦥ponytail=${ponytail.toUpperCase()}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// The string upstream's skill tells the model to relay for `/caveman status`.
|
|
21
|
+
export function formatCavemanStatus(mode: string): string {
|
|
22
|
+
return `Caveman mode: ${mode && mode !== 'off' ? mode : 'unknown'}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
// Stale ctx access throws, so never let it abort a mode restore.
|
|
21
26
|
function statusUi(ctx: ExtensionCtx | undefined): UiApi | undefined {
|
|
22
27
|
try {
|
|
@@ -34,7 +39,8 @@ let lastUi: UiApi | undefined;
|
|
|
34
39
|
|
|
35
40
|
// Paint the host footer bar; same text on the same UI is skipped, a new ctx repaints it.
|
|
36
41
|
export function announceStatus(ctx: ExtensionCtx | undefined): void {
|
|
37
|
-
const
|
|
42
|
+
const state = getSharedComboState();
|
|
43
|
+
const text = formatStatus(state);
|
|
38
44
|
const ui = statusUi(ctx) ?? statusUi(remembered);
|
|
39
45
|
if (statusUi(ctx)) remembered = ctx;
|
|
40
46
|
if (!ui) return;
|
|
@@ -42,4 +48,5 @@ export function announceStatus(ctx: ExtensionCtx | undefined): void {
|
|
|
42
48
|
lastText = text;
|
|
43
49
|
lastUi = ui;
|
|
44
50
|
ui.setStatus?.('tersio', text);
|
|
51
|
+
ui.setStatus?.('caveman', formatCavemanStatus(state.caveman));
|
|
45
52
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krtclcdy/tersio",
|
|
3
|
-
"version": "2.25.
|
|
3
|
+
"version": "2.25.3",
|
|
4
4
|
"description": "✂️ Token saver and usage tracker for coding agents: terse Caveman replies, compact RTK shell output, lean Ponytail code calls, and a gain dashboard — one-command combo presets over one shared extension tree.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"token-saving",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
},
|
|
22
22
|
"type": "module",
|
|
23
23
|
"bin": {
|
|
24
|
-
"tersio": "tersio.js"
|
|
24
|
+
"tersio": "dist/tersio.js"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
|
-
"./server": "./extensions/opencode/server.js"
|
|
27
|
+
"./server": "./dist/extensions/opencode/server.js"
|
|
28
28
|
},
|
|
29
29
|
"omp": {
|
|
30
30
|
"name": "Tersio",
|
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
"lite",
|
|
73
73
|
"full",
|
|
74
74
|
"ultra",
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
75
|
+
"megacave-lite",
|
|
76
|
+
"megacave-full",
|
|
77
|
+
"megacave-ultra"
|
|
78
78
|
],
|
|
79
79
|
"default": "off"
|
|
80
80
|
},
|
|
@@ -114,24 +114,23 @@
|
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
116
|
"files": [
|
|
117
|
-
"
|
|
118
|
-
"
|
|
117
|
+
"dist/**/*.js",
|
|
118
|
+
"dist/extensions/caveman-session/*.md",
|
|
119
119
|
"dashboard/dist/**/*",
|
|
120
|
-
"extensions/**/*.js",
|
|
121
120
|
"extensions/**/*.ts",
|
|
122
121
|
"extensions/caveman-session/rule.md",
|
|
123
122
|
"extensions/caveman-session/rule-ultra.md",
|
|
124
|
-
"extensions/caveman-session/rule-
|
|
123
|
+
"extensions/caveman-session/rule-megacave.md"
|
|
125
124
|
],
|
|
126
125
|
"scripts": {
|
|
127
|
-
"clean": "rm -
|
|
128
|
-
"build": "tsc && bun run build:dashboard",
|
|
126
|
+
"clean": "rm -rf dist dashboard/dist",
|
|
127
|
+
"build": "tsc && bun run build:dashboard && cp extensions/caveman-session/*.md dist/extensions/caveman-session/",
|
|
129
128
|
"build:dashboard": "bun install --cwd dashboard/app --frozen-lockfile && bun run --cwd dashboard/app build",
|
|
130
129
|
"prepublishOnly": "bun run build",
|
|
131
130
|
"check": "tsc --noEmit",
|
|
132
131
|
"verify": "bun install --frozen-lockfile && bun run build && bun run test",
|
|
133
|
-
"doctor": "node tersio.js --doctor",
|
|
134
|
-
"dry-run": "node tersio.js --dry-run --scope user --yes",
|
|
132
|
+
"doctor": "node dist/tersio.js --doctor",
|
|
133
|
+
"dry-run": "node dist/tersio.js --dry-run --scope user --yes",
|
|
135
134
|
"lint": "oxlint cli extensions test tersio.ts",
|
|
136
135
|
"lint:dashboard": "bun run --cwd dashboard/app lint",
|
|
137
136
|
"lint:fix": "oxlint cli extensions test tersio.ts --fix",
|
|
@@ -143,6 +142,7 @@
|
|
|
143
142
|
"devDependencies": {
|
|
144
143
|
"@opencode/plugin": "^2.0.22",
|
|
145
144
|
"@types/node": "^26.4.1",
|
|
145
|
+
"happy-dom": "^20.14.5",
|
|
146
146
|
"oxlint": "^1.85.0",
|
|
147
147
|
"typescript": "^7.0.2",
|
|
148
148
|
"vitest": "^5.0.1"
|
|
@@ -150,6 +150,6 @@
|
|
|
150
150
|
"dependencies": {
|
|
151
151
|
"@clack/core": "^1.5.1",
|
|
152
152
|
"@clack/prompts": "^1.8.0",
|
|
153
|
-
"@dietrichgebert/ponytail": "^4.
|
|
153
|
+
"@dietrichgebert/ponytail": "^4.12.0"
|
|
154
154
|
}
|
|
155
155
|
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// OpenCode plugin entry: maps its plugin API onto the shared ExtensionApi the mode extensions consume.
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
|
-
import { Plugin } from '@opencode/plugin';
|
|
4
|
-
import cavemanSessionExtension from '../caveman-session/index.js';
|
|
5
|
-
import rtkSessionExtension from '../rtk-session/index.js';
|
|
6
|
-
import comboToggleExtension from '../combo-toggle/index.js';
|
|
7
|
-
import tersioCommandsExtension from '../tersio-commands/index.js';
|
|
8
|
-
export default Plugin.define({
|
|
9
|
-
id: 'tersio',
|
|
10
|
-
async setup(ctx) {
|
|
11
|
-
const entries = (await ctx.storage.get('entries')) ?? [];
|
|
12
|
-
const persist = () => { void ctx.storage.set('entries', entries).catch(() => { }); };
|
|
13
|
-
const stubCtx = () => ({
|
|
14
|
-
hasUI: false,
|
|
15
|
-
cwd: ctx.location.directory,
|
|
16
|
-
sessionManager: { getBranch: () => entries, getEntries: () => entries },
|
|
17
|
-
});
|
|
18
|
-
const commands = new Map();
|
|
19
|
-
const tools = new Map();
|
|
20
|
-
const beforeStart = [];
|
|
21
|
-
const inputHandlers = [];
|
|
22
|
-
const lifecycle = new Map();
|
|
23
|
-
const host = {
|
|
24
|
-
hostId: 'opencode',
|
|
25
|
-
setLabel() { },
|
|
26
|
-
registerCommand(name, config) { commands.set(name, config); },
|
|
27
|
-
registerTool(tool) { tools.set(tool.name, tool); },
|
|
28
|
-
appendEntry(type, data) { entries.push({ type: 'custom', customType: type, data }); persist(); },
|
|
29
|
-
exec: (cmd, args, opts) => new Promise((resolve) => {
|
|
30
|
-
execFile(cmd, args, { signal: opts?.signal, cwd: opts?.cwd }, (error, stdout, stderr) => {
|
|
31
|
-
const code = error && typeof error.code === 'number' ? error.code : 0;
|
|
32
|
-
resolve({ stdout: stdout ?? '', stderr: stderr ?? '', code, killed: !!error?.killed });
|
|
33
|
-
});
|
|
34
|
-
}),
|
|
35
|
-
cwd: ctx.location.directory,
|
|
36
|
-
on: (event, handler) => {
|
|
37
|
-
if (event === 'before_agent_start') {
|
|
38
|
-
beforeStart.push(handler);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (event === 'input') {
|
|
42
|
-
inputHandlers.push(handler);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const list = lifecycle.get(event) ?? [];
|
|
46
|
-
list.push(handler);
|
|
47
|
-
lifecycle.set(event, list);
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
for (const extension of [cavemanSessionExtension, rtkSessionExtension, comboToggleExtension, tersioCommandsExtension])
|
|
51
|
-
extension(host);
|
|
52
|
-
// OpenCode has no session_start event surfaced to plugins, so restore once at setup.
|
|
53
|
-
for (const handler of lifecycle.get('session_start') ?? [])
|
|
54
|
-
await handler({}, stubCtx());
|
|
55
|
-
await ctx.command.transform((editor) => {
|
|
56
|
-
for (const [name, config] of commands) {
|
|
57
|
-
editor.add({
|
|
58
|
-
name,
|
|
59
|
-
description: config.description,
|
|
60
|
-
execute: async (input) => {
|
|
61
|
-
const text = typeof input.prompt?.text === 'string' ? input.prompt.text : '';
|
|
62
|
-
const args = text.includes(' ') ? text.slice(text.indexOf(' ') + 1).trim() : '';
|
|
63
|
-
await config.handler(args, stubCtx());
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
await ctx.tool.transform((editor) => {
|
|
69
|
-
for (const tool of tools.values()) {
|
|
70
|
-
editor.add({
|
|
71
|
-
name: tool.name,
|
|
72
|
-
description: tool.description,
|
|
73
|
-
input: tool.parameters,
|
|
74
|
-
execute: async (input) => {
|
|
75
|
-
const args = Array.isArray(input.args) ? (input.args) : [];
|
|
76
|
-
const result = await tool.execute('t', { args }, undefined, undefined, stubCtx());
|
|
77
|
-
return { content: result.content.map((part) => part.text).join('\n') };
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
await ctx.session.hook('context', async (event) => {
|
|
83
|
-
const systemParts = event.system.map((part) => (part.type === 'text' ? part.text : '')).filter(Boolean);
|
|
84
|
-
for (const handler of beforeStart) {
|
|
85
|
-
const result = await handler({ systemPrompt: [...systemParts] }, stubCtx());
|
|
86
|
-
if (!result || typeof result !== 'object' || !('systemPrompt' in result))
|
|
87
|
-
continue;
|
|
88
|
-
const parts = typeof result.systemPrompt === 'string'
|
|
89
|
-
? [result.systemPrompt]
|
|
90
|
-
: Array.isArray(result.systemPrompt)
|
|
91
|
-
? result.systemPrompt
|
|
92
|
-
: [];
|
|
93
|
-
for (const part of parts) {
|
|
94
|
-
if (!systemParts.includes(part))
|
|
95
|
-
event.system.push({ type: 'text', text: part });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
await ctx.session.hook('prompt', (event) => {
|
|
100
|
-
for (const handler of inputHandlers)
|
|
101
|
-
void handler({ text: event.prompt.text, source: 'interactive' });
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|