@krtclcdy/tersio 2.25.1 → 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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dashboard/dist/index.html +1 -1
  3. package/{cli → dist/cli}/common.js +4 -3
  4. package/{cli → dist/cli}/dashboard.js +30 -13
  5. package/{cli → dist/cli}/doctor-fix.js +19 -30
  6. package/{cli → dist/cli}/doctor.js +19 -6
  7. package/dist/cli/headroom-install.js +266 -0
  8. package/dist/cli/headroom.js +492 -0
  9. package/{cli → dist/cli}/install.js +116 -71
  10. package/{cli → dist/cli}/interactive.js +2 -2
  11. package/{cli → dist/cli}/manifest.js +4 -1
  12. package/{cli → dist/cli}/rtk-wiring.js +5 -2
  13. package/{cli → dist/cli}/settings.js +5 -23
  14. package/{cli → dist/cli}/uninstall.js +23 -11
  15. package/{cli → dist/cli}/update.js +10 -8
  16. package/{extensions → dist/extensions}/ai-addons-updater/index.js +60 -44
  17. package/{extensions → dist/extensions}/caveman-session/index.js +11 -11
  18. package/dist/extensions/caveman-session/rule-megacave.md +63 -0
  19. package/dist/extensions/caveman-session/rule-ultra.md +63 -0
  20. package/dist/extensions/caveman-session/rule.md +79 -0
  21. package/{extensions → dist/extensions}/combo-toggle/index.js +37 -27
  22. package/dist/extensions/headroom-session/index.js +93 -0
  23. package/{extensions → dist/extensions}/lib/utils.js +27 -1
  24. package/{extensions → dist/extensions}/opencode/server.js +33 -3
  25. package/dist/extensions/shared/headroom.js +131 -0
  26. package/{extensions → dist/extensions}/shared/plugin-settings.js +1 -1
  27. package/{extensions → dist/extensions}/shared/session-state.js +14 -2
  28. package/{extensions → dist/extensions}/shared/status.js +7 -1
  29. package/{tersio.js → dist/tersio.js} +1 -1
  30. package/extensions/ai-addons-updater/index.ts +52 -40
  31. package/extensions/caveman-session/index.ts +10 -11
  32. package/extensions/caveman-session/rule-megacave.md +63 -0
  33. package/extensions/caveman-session/rule-ultra.md +63 -0
  34. package/extensions/caveman-session/rule.md +4 -15
  35. package/extensions/combo-toggle/index.ts +35 -27
  36. package/extensions/lib/utils.ts +23 -1
  37. package/extensions/opencode/server.ts +29 -3
  38. package/extensions/shared/plugin-settings.ts +1 -1
  39. package/extensions/shared/session-state.ts +15 -2
  40. package/extensions/shared/status.ts +8 -1
  41. package/package.json +17 -15
  42. package/extensions/opencode/index.js +0 -104
  43. /package/{cli → dist/cli}/banner.js +0 -0
  44. /package/{cli → dist/cli}/currency.js +0 -0
  45. /package/{cli → dist/cli}/hosts.js +0 -0
  46. /package/{cli → dist/cli}/profile.js +0 -0
  47. /package/{cli → dist/cli}/reset.js +0 -0
  48. /package/{cli → dist/cli}/usage.js +0 -0
  49. /package/{extensions → dist/extensions}/rtk-session/index.js +0 -0
  50. /package/{extensions → dist/extensions}/shared/carbon.js +0 -0
  51. /package/{extensions → dist/extensions}/shared/host.js +0 -0
  52. /package/{extensions → dist/extensions}/shared/pricing.js +0 -0
  53. /package/{extensions → dist/extensions}/shared/rtk-gain.js +0 -0
  54. /package/{extensions → dist/extensions}/shared/types.js +0 -0
  55. /package/{extensions → dist/extensions}/shared/usage-ledger.js +0 -0
  56. /package/{extensions → dist/extensions}/shared/usage-store.js +0 -0
  57. /package/{extensions → dist/extensions}/tersio-commands/index.js +0 -0
@@ -6,13 +6,18 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { setExtensionLabel } from '../shared/host.js';
9
- import { CAVEMAN_REMOTE_RULE as CAVEMAN_REMOTE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, isPiProcess, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../lib/utils.js';
9
+ import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, isPiProcess, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../lib/utils.js';
10
10
  const IS_WINDOWS = process.platform === 'win32';
11
11
  const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
12
12
  const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
13
13
  const RTK_BINARY = resolveRtkBinary();
14
- // Beside this file, not a host path: that is the rule.md /caveman reads.
15
- const CAVEMAN_LOCAL = path.join(EXTENSION_DIR, '..', 'caveman-session', 'rule.md');
14
+ // Beside this file, not a host path: these are the rules /caveman reads.
15
+ const cavemanRule = (name) => path.join(EXTENSION_DIR, '..', 'caveman-session', name);
16
+ const CAVEMAN_LOCAL = cavemanRule('rule.md');
17
+ const CAVEMAN_TRACKED = [
18
+ { file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
19
+ { file: cavemanRule('rule-megacave.md'), remote: CAVEMAN_REMOTE_MEGACAVE },
20
+ ];
16
21
  const HOST_NAME = isPiProcess() ? 'Pi' : 'OMP';
17
22
  const RELOAD_MSG = `Reminder: restart ${HOST_NAME} (or reload extensions) for updates to take effect.`;
18
23
  // Ponytail is a hoisted dependency, so the installed copy is found by walking up rather than by naming a host directory.
@@ -82,17 +87,20 @@ function checkRtk() {
82
87
  return `RTK ${status}: local=${localVer || '—'} latest=${latestTag || '—'}`;
83
88
  });
84
89
  }
85
- // Caveman (rule.md)
86
90
  function checkCaveman() {
87
91
  return runCheck('Caveman', async () => {
88
- const remote = await httpsGet(CAVEMAN_REMOTE);
89
- const remoteHash = shortHash(remote);
90
- const local = await readTextIfExists(CAVEMAN_LOCAL);
91
- const localHash = local ? shortHash(local) : null;
92
- const status = !local ? 'rule.md missing'
93
- : localHash === remoteHash ? 'rule.md up to date'
94
- : 'rule.md update available';
95
- return `Caveman ${status}: local=${localHash || '—'} remote=${remoteHash}`;
92
+ const parts = [(await readTextIfExists(CAVEMAN_LOCAL)) ? 'rule.md ok' : 'rule.md missing'];
93
+ for (const { file, remote } of CAVEMAN_TRACKED) {
94
+ const name = path.basename(file);
95
+ try {
96
+ const [remoteText, localText] = await Promise.all([httpsGet(remote), readTextIfExists(file)]);
97
+ parts.push(!localText ? `${name} missing` : shortHash(localText) === shortHash(remoteText) ? `${name} up to date` : `${name} update available`);
98
+ }
99
+ catch {
100
+ parts.push(`${name} check skipped`);
101
+ }
102
+ }
103
+ return `Caveman ${parts.join(', ')}`;
96
104
  });
97
105
  }
98
106
  export async function checkAddonsSummary(ctx) {
@@ -317,41 +325,49 @@ async function updateRtk(ctx, dryRun = false) {
317
325
  }
318
326
  }
319
327
  async function updateCaveman(ctx, dryRun = false) {
320
- let remote;
321
- try {
322
- remote = await httpsGet(CAVEMAN_REMOTE);
323
- }
324
- catch (e) {
325
- return report(ctx, `Caveman update failed: ${e.message}`, 'warning');
326
- }
327
- const remoteHash = shortHash(remote);
328
- const oldLocal = await readTextIfExists(CAVEMAN_LOCAL);
329
- const oldHash = oldLocal ? shortHash(oldLocal) : null;
330
- if (dryRun) {
331
- const m = `Caveman dry-run: would write ${CAVEMAN_LOCAL}\nold=${oldHash || '—'} new=${remoteHash}.`;
332
- return report(ctx, m, 'info');
333
- }
334
- try {
335
- await fs.mkdir(path.dirname(CAVEMAN_LOCAL), { recursive: true });
336
- const backupPath = `${CAVEMAN_LOCAL}.bak`;
337
- if (oldLocal !== null)
338
- await fs.writeFile(backupPath, oldLocal, 'utf8');
339
- await fs.writeFile(CAVEMAN_LOCAL, remote, 'utf8');
340
- const written = await fs.readFile(CAVEMAN_LOCAL, 'utf8');
341
- const writtenHash = shortHash(written);
342
- if (writtenHash !== remoteHash) {
328
+ const results = [];
329
+ for (const { file, remote } of CAVEMAN_TRACKED) {
330
+ const name = path.basename(file);
331
+ let remoteText;
332
+ try {
333
+ remoteText = await httpsGet(remote);
334
+ }
335
+ catch (e) {
336
+ results.push(report(ctx, `Caveman ${name} update failed: ${e.message}`, 'warning'));
337
+ continue;
338
+ }
339
+ const remoteHash = shortHash(remoteText);
340
+ const oldLocal = await readTextIfExists(file);
341
+ const oldHash = oldLocal ? shortHash(oldLocal) : null;
342
+ if (oldHash === remoteHash) {
343
+ results.push(`${name} up to date: ${remoteHash}`);
344
+ continue;
345
+ }
346
+ if (dryRun) {
347
+ results.push(`Caveman dry-run: would write ${file}\nold=${oldHash || '—'} new=${remoteHash}.`);
348
+ continue;
349
+ }
350
+ try {
351
+ await fs.mkdir(path.dirname(file), { recursive: true });
352
+ const backupPath = `${file}.bak`;
343
353
  if (oldLocal !== null)
344
- await fs.writeFile(CAVEMAN_LOCAL, oldLocal, 'utf8');
345
- throw new Error(`written hash ${writtenHash} did not match remote ${remoteHash}${oldLocal !== null ? '; restored backup' : ''}`);
354
+ await fs.writeFile(backupPath, oldLocal, 'utf8');
355
+ await fs.writeFile(file, remoteText, 'utf8');
356
+ const writtenHash = shortHash(await fs.readFile(file, 'utf8'));
357
+ if (writtenHash !== remoteHash) {
358
+ if (oldLocal !== null)
359
+ await fs.writeFile(file, oldLocal, 'utf8');
360
+ throw new Error(`written hash ${writtenHash} did not match remote ${remoteHash}${oldLocal !== null ? '; restored backup' : ''}`);
361
+ }
362
+ results.push(`Caveman ${name} updated → ${file}\nold=${oldHash || '—'} new=${remoteHash}\nbackup=${oldLocal !== null ? backupPath : '—'}\n${RELOAD_MSG}`);
363
+ }
364
+ catch (e) {
365
+ results.push(`Caveman ${name} update failed: ${e.message}`);
346
366
  }
347
- const m = `Caveman rule.md updated → ${CAVEMAN_LOCAL}\nold=${oldHash || '—'} new=${remoteHash}\nbackup=${oldLocal !== null ? backupPath : '—'}\n${RELOAD_MSG}`;
348
- notify(ctx, 'Caveman rule.md updated. ' + RELOAD_MSG, 'info');
349
- return m;
350
- }
351
- catch (e) {
352
- const m = `Caveman update failed: ${e.message}`;
353
- return report(ctx, m, 'warning');
354
367
  }
368
+ const summary = results.join('\n');
369
+ notify(ctx, `Caveman update finished. ${RELOAD_MSG}`, 'info');
370
+ return summary;
355
371
  }
356
372
  export default function aiAddonsUpdaterExtension(pi) {
357
373
  setExtensionLabel(pi, 'AI add-ons updater');
@@ -6,20 +6,20 @@ import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host
6
6
  import { announceStatus } from '../shared/status.js';
7
7
  import { readCavemanDefault } from '../shared/plugin-settings.js';
8
8
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
9
- const RULE_PATH = join(CAVERN_DIR, 'rule.md');
10
- // Fail loud: a wrong rule reads to the model as the real one.
11
- function readFullRule() {
9
+ // Upstream ships one skill per register: default, ultracave, megacave.
10
+ 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' };
11
+ const DEFAULT_MODE = 'off';
12
+ function buildInstruction(mode) {
13
+ const name = RULE_FILES[mode] ?? RULE_FILES.full;
14
+ // Fail loud: a wrong rule reads to the model as the real one.
15
+ let body;
12
16
  try {
13
- return readFileSync(RULE_PATH, 'utf8');
17
+ body = readFileSync(join(CAVERN_DIR, name), 'utf8');
14
18
  }
15
19
  catch {
16
- return 'Caveman full requested, but rule.md is missing. Run: tersio doctor --fix extensions';
20
+ body = `Caveman ${mode} requested, but ${name} is missing. Run: tersio doctor --fix extensions`;
17
21
  }
18
- }
19
- const DEFAULT_MODE = 'off';
20
- // Upstream ships one SKILL.md; the level is a directive inside it, so swap only the header.
21
- function buildInstruction(mode) {
22
- return `Caveman ${mode} active for this session.\n${readFullRule()}`;
22
+ return `Caveman ${mode} active for this session.\n${body}`;
23
23
  }
24
24
  function resolveMode(entries, fallback = DEFAULT_MODE) {
25
25
  return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
@@ -62,7 +62,7 @@ export default function cavemanSessionExtension(pi) {
62
62
  return;
63
63
  }
64
64
  if (!setMode(arg, ctx)) {
65
- ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off|status]', 'warning');
65
+ ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|megacave-lite|megacave-full|megacave-ultra|off|status]', 'warning');
66
66
  }
67
67
  },
68
68
  });
@@ -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.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: ultracave
3
+ description: >
4
+ Caveman at maximum compression: fragments, one word when one word is enough,
5
+ each fact once. Invoke only with /ultracave or /caveman ultra. Stays on until
6
+ "stop caveman" or "normal mode".
7
+ disable-model-invocation: true
8
+ ---
9
+
10
+ # ultracave
11
+
12
+ Respond terse like smart caveman. All technical substance stay. Only fluff die. Then cut again.
13
+
14
+ Ultracave is caveman with the grammar stripped. Payload only.
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: 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.
23
+
24
+ ## Rules
25
+
26
+ ### 1. Fragments
27
+
28
+ Drop articles, copulas, connectives when order stays clear.
29
+
30
+ Bad: "The component re-renders because an inline object prop creates a new reference each render."
31
+ Good: "Inline object prop, new ref, re-render. `useMemo`."
32
+
33
+ ### 2. One word when one word is enough
34
+
35
+ Bad: "Yes, that should work, though you'll want a null check first."
36
+ Good: "Yes. Null check first."
37
+
38
+ ### 3. Each fact once
39
+
40
+ No restating, no summary after a list.
41
+
42
+ Good: "Pool reuses open DB connections. No per-request handshake."
43
+
44
+ ### 4. Cut conjunctions only when order survives
45
+
46
+ Bad: "Migrate table drop column backup first."
47
+ Good: "Back up first. Then migrate: drops column."
48
+
49
+ ### 5. Tool runs
50
+
51
+ One line in, one line out. Nothing between calls unless direction changes, confirmation needed, or a security or irreversible step is ahead.
52
+
53
+ ### 6. Never perform
54
+
55
+ No prefix, no announcement, no mangled verbs for flavor. Fragment not shorter than the sentence? Use the sentence.
56
+
57
+ ## When to break the rules
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.
60
+
61
+ ## Pre-send check
62
+
63
+ Two readings? Full sentence. Negations present? Payload verbatim? Flavor word? Cut.
@@ -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, setSharedComboMode, systemPromptIncludes, } from '../shared/session-state.js';
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,20 +55,41 @@ 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 reconcile(ctx) {
57
- if (!ctx?.hasUI)
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
- return reconcileSharedComboEntries(sessionEntries(ctx));
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
95
  description: 'Toggle all 3 OMP add-ons at once. Usage: /combo <off|medium|balanced|max|status>',
@@ -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
- await runFirstRunSetup(ctx);
118
- // The configured default applies only when no persisted mode state exists.
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
- const persisted = hasModeState(entries);
121
- if (getSharedComboState().level === 'off' && !persisted) {
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
  });
@@ -0,0 +1,93 @@
1
+ // /headroom session toggle. Headroom is a machine-level add-on, so this extension mirrors the stored per-agent flags and delegates every state change to the CLI.
2
+ import { HEADROOM_INSTALL_HINT, headroomAgentState, headroomCapability, headroomProxyUp, headroomStatusFor, readHeadroomConfig, setHeadroomStatus, } from '../shared/headroom.js';
3
+ import { onHostEvent, setExtensionLabel } from '../shared/host.js';
4
+ import { announceStatus } from '../shared/status.js';
5
+ /** The host's own key in the per-agent Headroom flags. */
6
+ function agentOf(pi) {
7
+ return pi.hostId ?? 'omp';
8
+ }
9
+ function statusText(agent, status) {
10
+ const manual = headroomCapability(agent) === 'manual';
11
+ const port = readHeadroomConfig().port;
12
+ if (status === 'down')
13
+ return `Headroom is on for ${agent}${manual ? ' (manual base URL)' : ''}, but the proxy does not answer. Restart it with: headroom proxy --port ${port}`;
14
+ if (status === 'manual')
15
+ return `Headroom is on for ${agent} (manual base URL). Point the model base URL at the proxy.`;
16
+ if (status === 'on')
17
+ return `Headroom is on for ${agent}.`;
18
+ return `Headroom is off for ${agent}.`;
19
+ }
20
+ export default function headroomSessionExtension(pi) {
21
+ setExtensionLabel(pi, 'Headroom session toggle');
22
+ const agent = agentOf(pi);
23
+ let shaperWarned = false;
24
+ // Reads the stored flags, then probes the proxy only when this agent is wired. Off installs never touch the network.
25
+ async function refresh(ctx) {
26
+ const config = readHeadroomConfig();
27
+ const state = headroomAgentState(config, agent);
28
+ // No segment until the user opts in, so a machine that merely has Headroom installed keeps the old status line.
29
+ if (!config.support || !state.enabled) {
30
+ setHeadroomStatus(null);
31
+ return null;
32
+ }
33
+ const up = await headroomProxyUp(config.port);
34
+ const status = headroomStatusFor(config, agent, up);
35
+ setHeadroomStatus(status);
36
+ // Caveman already owns terse output, so two shapers would fight over one prompt.
37
+ if (!shaperWarned && process.env.HEADROOM_OUTPUT_SHAPER && process.env.HEADROOM_OUTPUT_SHAPER !== '0') {
38
+ shaperWarned = true;
39
+ ctx?.ui?.notify?.(`HEADROOM_OUTPUT_SHAPER is set while Caveman is active. Unset it: one prompt owner only.`, 'warning');
40
+ }
41
+ return status;
42
+ }
43
+ async function runCli(action, ctx) {
44
+ if (typeof pi.exec !== 'function') {
45
+ ctx?.ui?.notify?.(`Run in a shell: tersio headroom ${action} --agent ${agent}`, 'warning');
46
+ return;
47
+ }
48
+ try {
49
+ const result = await pi.exec('tersio', ['headroom', action, '--agent', agent], { cwd: ctx?.cwd ?? pi.cwd });
50
+ const text = [result.stdout, result.stderr].map((part) => part.trim()).filter(Boolean).join('\n');
51
+ ctx?.ui?.notify?.(text || `tersio headroom ${action} exited ${result.code}`, result.code === 0 ? 'info' : 'warning');
52
+ }
53
+ catch (e) {
54
+ ctx?.ui?.notify?.(`tersio headroom ${action} failed (${e.message}). Run it in a shell.`, 'warning');
55
+ }
56
+ await refresh(ctx);
57
+ announceStatus(ctx);
58
+ }
59
+ pi.registerCommand?.('headroom', {
60
+ description: 'Toggle the Headroom compression proxy for this agent',
61
+ handler: async (args, ctx) => {
62
+ const arg = String(args || '').trim().toLowerCase();
63
+ if (!arg || arg === 'status') {
64
+ const status = await refresh(ctx);
65
+ ctx?.ui?.notify?.(status === null
66
+ ? `Headroom is not installed — ${HEADROOM_INSTALL_HINT}`
67
+ : statusText(agent, status), 'info');
68
+ announceStatus(ctx);
69
+ return;
70
+ }
71
+ if (['on', 'enable', 'enabled', 'true'].includes(arg)) {
72
+ await runCli('enable', ctx);
73
+ return;
74
+ }
75
+ if (['off', 'disable', 'disabled', 'false'].includes(arg)) {
76
+ await runCli('disable', ctx);
77
+ return;
78
+ }
79
+ ctx?.ui?.notify?.('Usage: /headroom [on|off|status]', 'warning');
80
+ },
81
+ });
82
+ // Start, resume, branch, and compaction all land here, so every session type shows the state.
83
+ pi.on('session_start', async (_event, ctx) => {
84
+ await refresh(ctx);
85
+ announceStatus(ctx);
86
+ });
87
+ for (const event of ['session_branch', 'session_tree', 'session_switch', 'session_compact']) {
88
+ onHostEvent(pi, event, async (_event, ctx) => {
89
+ await refresh(ctx);
90
+ announceStatus(ctx);
91
+ });
92
+ }
93
+ }
@@ -18,7 +18,21 @@ export function homeDir() {
18
18
  return home && home.trim() !== '' ? home : os.homedir();
19
19
  }
20
20
  export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
21
- export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/caveman/SKILL.md';
21
+ export const CAVEMAN_REMOTE_ULTRA = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/ultracave/SKILL.md';
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
+ }
22
36
  const RTK_PLATFORM_SPECS = {
23
37
  'win32/x64': { triple: 'x86_64-pc-windows-msvc', ext: '.zip', binary: 'rtk.exe' },
24
38
  'linux/x64': { triple: 'x86_64-unknown-linux-musl', ext: '.tar.gz', binary: 'rtk' },
@@ -54,6 +68,18 @@ export function resolveHostBinary(name, managedDir = path.join(homeDir(), '.bun'
54
68
  export function rtkPlatformSpec(platform = process.platform, arch = process.arch) {
55
69
  return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
56
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
+ }
57
83
  // The 3xx+location redirect rule shared by httpsGet/httpsDownload.
58
84
  function redirectNext(res, url) {
59
85
  if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {