@krtclcdy/tersio 2.22.1 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,13 +8,13 @@ import { createHash } from 'node:crypto';
8
8
  import fs from 'node:fs/promises';
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
- import { CAVEMAN_REMOTE_RULE as CAVEMAN_REMOTE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, } from '../lib/utils.js';
11
+ import { CAVEMAN_REMOTE_RULE as CAVEMAN_REMOTE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../lib/utils.js';
12
12
  const IS_WINDOWS = process.platform === 'win32';
13
13
  const HOME = os.homedir();
14
14
  const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
15
15
  const PONYTAIL_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
16
- const RTK_BINARY = path.join(HOME, '.bun', 'bin', IS_WINDOWS ? 'rtk.exe' : 'rtk');
17
- const CAVEMAN_LOCAL = path.join(HOME, '.omp', 'agent', 'extensions', 'caveman-session', 'rule.md');
16
+ const RTK_BINARY = resolveRtkBinary();
17
+ const CAVEMAN_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman-session', 'rule.md');
18
18
  const RELOAD_MSG = 'Reminder: restart OMP (or reload extensions) for updates to take effect.';
19
19
  function notify(ctx, msg, level) {
20
20
  ctx?.ui?.notify?.(String(msg), level);
@@ -55,6 +55,8 @@ function checkRtk() {
55
55
  const latestTag = release.tag_name || null;
56
56
  let localVer = null;
57
57
  try {
58
+ if (!RTK_BINARY)
59
+ throw new Error('rtk not found in PATH');
58
60
  const out = execFileSync(RTK_BINARY, ['--version'], { encoding: 'utf8', windowsHide: true, shell: false, timeout: 10000 }) || '';
59
61
  if (out)
60
62
  localVer = out.trim().split(/\r?\n/)[0];
@@ -146,6 +148,8 @@ async function updateRtk(ctx, dryRun = false) {
146
148
  }
147
149
  const tag = release.tag_name || 'unknown';
148
150
  const assets = Array.isArray(release.assets) ? release.assets : [];
151
+ if (!RTK_BINARY)
152
+ return report(ctx, 'RTK: executable not found in PATH', 'warning');
149
153
  // Cross-platform asset selection (mirrors installer stepRtk)
150
154
  const PLATFORM = process.platform;
151
155
  const ARCH = process.arch;
@@ -23,6 +23,7 @@ import {
23
23
  normalizeRtkVersion,
24
24
  readTextIfExists,
25
25
  rtkPlatformSpec,
26
+ resolveRtkBinary,
26
27
  } from '../lib/utils.ts';
27
28
 
28
29
  const IS_WINDOWS = process.platform === 'win32';
@@ -30,9 +31,18 @@ const HOME = os.homedir();
30
31
 
31
32
  const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
32
33
  const PONYTAIL_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
33
- const RTK_BINARY = path.join(HOME, '.bun', 'bin', IS_WINDOWS ? 'rtk.exe' : 'rtk');
34
- const CAVEMAN_LOCAL = path.join(HOME, '.omp', 'agent', 'extensions', 'caveman-session', 'rule.md');
35
-
34
+ const RTK_BINARY = resolveRtkBinary();
35
+ const CAVEMAN_LOCAL = path.join(
36
+ HOME,
37
+ '.omp',
38
+ 'plugins',
39
+ 'node_modules',
40
+ '@krtclcdy',
41
+ 'tersio',
42
+ 'extensions',
43
+ 'caveman-session',
44
+ 'rule.md',
45
+ );
36
46
  const RELOAD_MSG = 'Reminder: restart OMP (or reload extensions) for updates to take effect.';
37
47
 
38
48
  // --- Types ---
@@ -104,6 +114,7 @@ function checkRtk(): Promise<AddonStatus> {
104
114
  const latestTag = release.tag_name || null;
105
115
  let localVer: string | null = null;
106
116
  try {
117
+ if (!RTK_BINARY) throw new Error('rtk not found in PATH');
107
118
  const out = execFileSync(RTK_BINARY, ['--version'], { encoding: 'utf8', windowsHide: true, shell: false, timeout: 10000 }) || '';
108
119
  if (out) localVer = out.trim().split(/\r?\n/)[0];
109
120
  } catch { localVer = null; }
@@ -188,6 +199,7 @@ async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string>
188
199
  }
189
200
  const tag = release.tag_name || 'unknown';
190
201
  const assets = Array.isArray(release.assets) ? release.assets : [];
202
+ if (!RTK_BINARY) return report(ctx, 'RTK: executable not found in PATH', 'warning');
191
203
 
192
204
  // Cross-platform asset selection (mirrors installer stepRtk)
193
205
  const PLATFORM = process.platform;
@@ -5,16 +5,20 @@ import { dirname, join } from 'node:path';
5
5
  import { readCavemanDefault } from '../shared/plugin-settings.js';
6
6
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
7
7
  const RULE_PATH = join(CAVERN_DIR, 'rule.md');
8
- const FALLBACK_FULL_RULE = `Respond terse like smart caveman. All technical substance stay. Only fluff die.
8
+ const FALLBACK_FULL_RULE = `Caveman full active for this session.
9
+ Respond terse like smart caveman. Keep all technical substance. Drop filler, pleasantries, and hedging.
9
10
 
10
11
  Rules:
11
- - Drop articles (a/an/the), filler (just/really/basically), pleasantries, hedging.
12
- - Fragments OK. Short synonyms. Technical terms exact. Code unchanged.
13
- - Pattern: [thing] [action] [reason]. [next step].
14
- - Not: 'Sure! I'd be happy to help you with that.'
15
- - Yes: 'Bug in auth middleware. Fix:'
12
+ - Drop articles where meaning stays clear. Keep fragments and short sentences.
13
+ - Keep technical terms, code, commands, paths, API names, numbers, units, and errors exact.
14
+ - Never invent abbreviations or causal arrows. Do not add words to sound caveman.
15
+ - Use ASD-STE100 Simplified Technical English. Keep one idea per sentence, target 20 words, active voice, and consistent terms.
16
+ - Make no tool-call narration. Use no decorative tables or emoji.
17
+ - Preserve the user's reply language. Compress style, not language.
18
+ - Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or requests to clarify.
19
+ - Write code, comments, commits, docs, issues, pull requests, and third-party messages in normal prose.
16
20
 
17
- Auto-clarity: drop caveman for security warnings, irreversible actions, or when user seems confused. Resume after.`;
21
+ Default: **full**. Switch: \`/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off\`. Stop: "stop caveman" or "normal mode".`;
18
22
  // ponytail: synchronous read each full-mode injection; ceiling = small file, cold session start. Upgrade path: cache file contents + mtime, invalidate on change.
19
23
  function readFullRule() {
20
24
  try {
@@ -30,9 +34,10 @@ const INSTRUCTIONS = {
30
34
  Respond concise. Drop pleasantries, filler, and hedging. Keep complete technical substance. Code, commands, paths, errors, commits, and PR text stay normal/exact.`,
31
35
  full: () => `Caveman full active for this session.\n${readFullRule()}`,
32
36
  ultra: `Caveman ultra active for this session.
33
- Maximum terse prose. Fragments preferred. No pleasantries, no tour, no recap unless needed. Keep all technical substance exact. Code, commands, commits, PR text, paths, and errors stay normal/exact. Drop caveman for security warnings, irreversible actions, or user confusion.`,
34
- wenyan: `Caveman wenyan active for this session.
35
- Use ultra-terse classical-Chinese-style prose only where it preserves clarity for the user. Keep technical terms, code, commands, commits, PR text, paths, and errors exact. If clarity would suffer, use caveman full instead.`,
37
+ Maximum terse prose. Strip conjunctions only when meaning stays clear. State each fact once. Use no invented abbreviations or causal arrows. Keep code, commands, commits, PR text, paths, API names, numbers, units, and errors exact. Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or user confusion.`,
38
+ 'wenyan-lite': `Caveman wenyan-lite active for this session. Use semi-classical terse prose. Keep grammar structure, user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
39
+ 'wenyan-full': `Caveman wenyan-full active for this session. Use maximum classical terseness where meaning stays clear. Use classical sentence patterns, verbs before objects, and optional omitted subjects. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
40
+ 'wenyan-ultra': `Caveman wenyan-ultra active for this session. Use extreme classical abbreviation while keeping a classical Chinese feel and meaning clear. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
36
41
  };
37
42
  function resolveMode(entries, fallback = DEFAULT_MODE) {
38
43
  return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
@@ -97,7 +102,7 @@ export default function cavemanSessionExtension(pi) {
97
102
  return;
98
103
  }
99
104
  if (!setMode(arg, ctx)) {
100
- ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|wenyan|off|status]', 'warning');
105
+ ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off|status]', 'warning');
101
106
  }
102
107
  },
103
108
  });
@@ -117,7 +122,7 @@ export default function cavemanSessionExtension(pi) {
117
122
  // Persisted session state wins; a fresh session falls back to the
118
123
  // installer/user-configured default (off unless configured).
119
124
  const persisted = resolveMode(entries, '');
120
- currentMode = persisted || readCavemanDefault();
125
+ currentMode = persisted || normalizeMode('caveman', readCavemanDefault()) || DEFAULT_MODE;
121
126
  syncStatus(ctx);
122
127
  }
123
128
  pi.on('session_start', async (_event, ctx) => {
@@ -8,16 +8,20 @@ import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPrompt
8
8
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
9
9
  const RULE_PATH = join(CAVERN_DIR, 'rule.md');
10
10
 
11
- const FALLBACK_FULL_RULE = `Respond terse like smart caveman. All technical substance stay. Only fluff die.
11
+ const FALLBACK_FULL_RULE = `Caveman full active for this session.
12
+ Respond terse like smart caveman. Keep all technical substance. Drop filler, pleasantries, and hedging.
12
13
 
13
14
  Rules:
14
- - Drop articles (a/an/the), filler (just/really/basically), pleasantries, hedging.
15
- - Fragments OK. Short synonyms. Technical terms exact. Code unchanged.
16
- - Pattern: [thing] [action] [reason]. [next step].
17
- - Not: 'Sure! I'd be happy to help you with that.'
18
- - Yes: 'Bug in auth middleware. Fix:'
15
+ - Drop articles where meaning stays clear. Keep fragments and short sentences.
16
+ - Keep technical terms, code, commands, paths, API names, numbers, units, and errors exact.
17
+ - Never invent abbreviations or causal arrows. Do not add words to sound caveman.
18
+ - Use ASD-STE100 Simplified Technical English. Keep one idea per sentence, target 20 words, active voice, and consistent terms.
19
+ - Make no tool-call narration. Use no decorative tables or emoji.
20
+ - Preserve the user's reply language. Compress style, not language.
21
+ - Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or requests to clarify.
22
+ - Write code, comments, commits, docs, issues, pull requests, and third-party messages in normal prose.
19
23
 
20
- Auto-clarity: drop caveman for security warnings, irreversible actions, or when user seems confused. Resume after.`;
24
+ Default: **full**. Switch: \`/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off\`. Stop: "stop caveman" or "normal mode".`;
21
25
 
22
26
  // ponytail: synchronous read each full-mode injection; ceiling = small file, cold session start. Upgrade path: cache file contents + mtime, invalidate on change.
23
27
  function readFullRule(): string {
@@ -30,9 +34,10 @@ const INSTRUCTIONS: Record<string, string | (() => string)> = {
30
34
  Respond concise. Drop pleasantries, filler, and hedging. Keep complete technical substance. Code, commands, paths, errors, commits, and PR text stay normal/exact.`,
31
35
  full: () => `Caveman full active for this session.\n${readFullRule()}`,
32
36
  ultra: `Caveman ultra active for this session.
33
- Maximum terse prose. Fragments preferred. No pleasantries, no tour, no recap unless needed. Keep all technical substance exact. Code, commands, commits, PR text, paths, and errors stay normal/exact. Drop caveman for security warnings, irreversible actions, or user confusion.`,
34
- wenyan: `Caveman wenyan active for this session.
35
- Use ultra-terse classical-Chinese-style prose only where it preserves clarity for the user. Keep technical terms, code, commands, commits, PR text, paths, and errors exact. If clarity would suffer, use caveman full instead.`,
37
+ Maximum terse prose. Strip conjunctions only when meaning stays clear. State each fact once. Use no invented abbreviations or causal arrows. Keep code, commands, commits, PR text, paths, API names, numbers, units, and errors exact. Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or user confusion.`,
38
+ 'wenyan-lite': `Caveman wenyan-lite active for this session. Use semi-classical terse prose. Keep grammar structure, user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
39
+ 'wenyan-full': `Caveman wenyan-full active for this session. Use maximum classical terseness where meaning stays clear. Use classical sentence patterns, verbs before objects, and optional omitted subjects. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
40
+ 'wenyan-ultra': `Caveman wenyan-ultra active for this session. Use extreme classical abbreviation while keeping a classical Chinese feel and meaning clear. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
36
41
  };
37
42
 
38
43
  function resolveMode(entries: SessionEntry[] | null | undefined, fallback: string = DEFAULT_MODE): string {
@@ -102,7 +107,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
102
107
  return;
103
108
  }
104
109
  if (!setMode(arg, ctx)) {
105
- ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|wenyan|off|status]', 'warning');
110
+ ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off|status]', 'warning');
106
111
  }
107
112
  },
108
113
  });
@@ -122,7 +127,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
122
127
  // Persisted session state wins; a fresh session falls back to the
123
128
  // installer/user-configured default (off unless configured).
124
129
  const persisted = resolveMode(entries, '');
125
- currentMode = persisted || readCavemanDefault();
130
+ currentMode = persisted || normalizeMode('caveman', readCavemanDefault()) || DEFAULT_MODE;
126
131
  syncStatus(ctx);
127
132
  }
128
133
 
@@ -0,0 +1,90 @@
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 wenyan 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|wenyan-lite|wenyan-full|wenyan-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
+ 'Drop articles' = article languages only. Where small markers carry case/role (particles, postpositions), keep them grammar, not filler; compress politeness/filler instead.
32
+
33
+ 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.
34
+
35
+ Pattern: `[thing] [action] [reason]. [next step].`
36
+
37
+ Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
38
+ Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
39
+
40
+ ## Intensity
41
+
42
+ | Level | What change |
43
+ |-------|------------|
44
+ | **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
45
+ | **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 |
46
+ | **ultra** | Strip conjunctions when cause-then-effect stay unambiguous. One word when one word enough. State each fact once. NO prose abbreviations (cfg/impl/req/res/fn/auth), NO arrows (X → Y) measured zero token saving under tokenizer, cost decode clarity. Code symbols, function names, API names, error strings: never touch |
47
+ | **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
48
+ | **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction chars, not tokens. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
49
+ | **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
50
+
51
+ Example "Why React component re-render?"
52
+ - lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
53
+ - full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
54
+ - ultra: "Inline obj prop, new ref, re-render. `useMemo`."
55
+ - wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
56
+ - wenyan-full: "每繪新生對象參照,故重繪;以 useMemo 包之則免。"
57
+ - wenyan-ultra: "新參照則重繪。useMemo 包之。"
58
+
59
+ Example "Explain database connection pooling."
60
+ - lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
61
+ - full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
62
+ - ultra: "Pool reuse open DB connections. No per-request handshake."
63
+ - wenyan-full: "池蓄已開之連,不逐請而新開,省握手之費。"
64
+ - wenyan-ultra: "池蓄連,免逐請新開,省握手。"
65
+
66
+ Classical chars = wenyan modes only. Never swap a word to a classical char to shrink at non-wenyan levels.
67
+
68
+ ## Auto-Clarity
69
+
70
+ Drop caveman when:
71
+ - Security warnings
72
+ - Irreversible action confirmations
73
+ - Multi-step sequences where fragment order or omitted conjunctions risk misread
74
+ - Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` order unclear without articles/conjunctions)
75
+ - User asks to clarify or repeats question
76
+
77
+ Resume caveman after clear part done.
78
+
79
+ Example shows FORMAT only write warning in session language, not example's.
80
+
81
+ Example destructive op:
82
+ > **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
83
+ > ```sql
84
+ > DROP TABLE users;
85
+ > ```
86
+ > Caveman resume. Verify backup exist first.
87
+
88
+ ## Boundaries
89
+
90
+ 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.
@@ -4,11 +4,10 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { createRequire } from 'node:module';
6
6
  import { activeModesSummary, asPromptArray, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, systemPromptIncludes, } from '../shared/session-state.js';
7
- import { readComboDefault, readPonytailDefault } from '../shared/plugin-settings.js';
7
+ import { isComboSetupComplete, readComboDefault, readPonytailDefault, saveComboSetup, } from '../shared/plugin-settings.js';
8
8
  const require = createRequire(import.meta.url);
9
9
  const PONYTAIL_FALLBACK_INTENSITY = {
10
10
  lite: 'Prefer the simplest correct solution.',
11
- review: 'Review only for avoidable complexity; recommend the smallest correct replacement.',
12
11
  };
13
12
  function ponytailFallback(mode) {
14
13
  const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
@@ -30,6 +29,7 @@ function loadPonytailInstructions(mode) {
30
29
  export default function comboToggleExtension(pi) {
31
30
  pi.setLabel?.('Combo session toggle (all 3 add-ons)');
32
31
  let lastCtx = undefined;
32
+ let setupPrompted = false;
33
33
  function syncStatus(ctx) {
34
34
  lastCtx = paintableCtx(lastCtx, ctx);
35
35
  const c = lastCtx;
@@ -115,10 +115,28 @@ export default function comboToggleExtension(pi) {
115
115
  ctx?.ui?.notify?.(level === 'off' ? `Combo off — all tersio modes inactive for this session. Active: ${active}.` : `Combo ${level} on — ${active} active for this session.`, 'info');
116
116
  },
117
117
  });
118
+ async function runFirstRunSetup(ctx) {
119
+ if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete())
120
+ return;
121
+ setupPrompted = true;
122
+ const choice = await ctx.ui.select('Session-start defaults — Combo preset', [
123
+ { label: 'off', description: 'Keep every Tersio mode inactive' },
124
+ { label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
125
+ { label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
126
+ { label: 'max', description: 'caveman=ultra, rtk=on, ponytail=ultra' },
127
+ ], { selectionMarker: 'radio', helpText: 'You can change this later with /tersio settings.' });
128
+ const level = choice || 'off';
129
+ if (saveComboSetup(level) && level !== 'off') {
130
+ persistPreset(level);
131
+ useState(setSharedComboLevel(level), ctx);
132
+ ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
133
+ }
134
+ }
118
135
  pi.on('session_start', async (_event, ctx) => {
119
136
  track(ctx);
120
137
  if (!ctx?.hasUI)
121
138
  syncStatus(ctx);
139
+ await runFirstRunSetup(ctx);
122
140
  // Installer/user-configured default applies only when no persisted *mode*
123
141
  // state exists — unrelated session entries must not block it, or the
124
142
  // combo bar never paints on sessions that already carry other entries.
@@ -148,6 +166,8 @@ export default function comboToggleExtension(pi) {
148
166
  for (const event of ['session_branch', 'session_tree', 'agent_start']) {
149
167
  pi.on(event, async (_event, ctx) => {
150
168
  track(ctx);
169
+ if (event === 'agent_start')
170
+ await runFirstRunSetup(ctx);
151
171
  });
152
172
  }
153
173
  pi.on('before_agent_start', async (event, ctx) => {
@@ -20,14 +20,18 @@ import {
20
20
  setSharedComboMode,
21
21
  systemPromptIncludes,
22
22
  } from '../shared/session-state.ts';
23
- import { readComboDefault, readPonytailDefault } from '../shared/plugin-settings.ts';
23
+ import {
24
+ isComboSetupComplete,
25
+ readComboDefault,
26
+ readPonytailDefault,
27
+ saveComboSetup,
28
+ } from '../shared/plugin-settings.ts';
24
29
  import type { ComboState, ExtensionApi, ExtensionCtx, SystemPromptEvent } from '../shared/types.ts';
25
30
 
26
31
  const require = createRequire(import.meta.url);
27
32
 
28
33
  const PONYTAIL_FALLBACK_INTENSITY: Record<string, string> = {
29
34
  lite: 'Prefer the simplest correct solution.',
30
- review: 'Review only for avoidable complexity; recommend the smallest correct replacement.',
31
35
  };
32
36
 
33
37
  function ponytailFallback(mode: string): string {
@@ -61,6 +65,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
61
65
  pi.setLabel?.('Combo session toggle (all 3 add-ons)');
62
66
 
63
67
  let lastCtx: ExtensionCtx | undefined = undefined;
68
+ let setupPrompted = false;
64
69
 
65
70
  function syncStatus(ctx?: ExtensionCtx): void {
66
71
  lastCtx = paintableCtx(lastCtx, ctx);
@@ -166,9 +171,27 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
166
171
  },
167
172
  });
168
173
 
174
+ async function runFirstRunSetup(ctx?: ExtensionCtx): Promise<void> {
175
+ if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete()) return;
176
+ setupPrompted = true;
177
+ const choice = await ctx.ui.select('Session-start defaults — Combo preset', [
178
+ { label: 'off', description: 'Keep every Tersio mode inactive' },
179
+ { label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
180
+ { label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
181
+ { label: 'max', description: 'caveman=ultra, rtk=on, ponytail=ultra' },
182
+ ], { selectionMarker: 'radio', helpText: 'You can change this later with /tersio settings.' });
183
+ const level = choice || 'off';
184
+ if (saveComboSetup(level) && level !== 'off') {
185
+ persistPreset(level);
186
+ useState(setSharedComboLevel(level), ctx);
187
+ ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
188
+ }
189
+ }
190
+
169
191
  pi.on('session_start', async (_event, ctx) => {
170
192
  track(ctx);
171
193
  if (!ctx?.hasUI) syncStatus(ctx);
194
+ await runFirstRunSetup(ctx);
172
195
  // Installer/user-configured default applies only when no persisted *mode*
173
196
  // state exists — unrelated session entries must not block it, or the
174
197
  // combo bar never paints on sessions that already carry other entries.
@@ -200,6 +223,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
200
223
  for (const event of ['session_branch', 'session_tree', 'agent_start']) {
201
224
  pi.on(event, async (_event, ctx) => {
202
225
  track(ctx);
226
+ if (event === 'agent_start') await runFirstRunSetup(ctx);
203
227
  });
204
228
  }
205
229
 
@@ -1,7 +1,7 @@
1
1
  // Shared helpers for the installer and the AI add-ons updater.
2
2
  // Node built-ins only — this module must stay dependency-free.
3
3
  import { createHash } from 'node:crypto';
4
- import { createWriteStream, existsSync, mkdirSync, renameSync } from 'node:fs';
4
+ import { accessSync, constants, createWriteStream, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
5
5
  import fs from 'node:fs/promises';
6
6
  import https from 'node:https';
7
7
  import os from 'node:os';
@@ -15,7 +15,7 @@ export function withResolvers() {
15
15
  return { promise, resolve, reject };
16
16
  }
17
17
  export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
18
- export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rules/caveman-activate.md';
18
+ export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/caveman/SKILL.md';
19
19
  const RTK_PLATFORM_SPECS = {
20
20
  'win32/x64': { triple: 'x86_64-pc-windows-msvc', ext: '.zip', binary: 'rtk.exe' },
21
21
  'linux/x64': { triple: 'x86_64-unknown-linux-musl', ext: '.tar.gz', binary: 'rtk' },
@@ -23,6 +23,22 @@ const RTK_PLATFORM_SPECS = {
23
23
  'darwin/x64': { triple: 'x86_64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
24
24
  'darwin/arm64': { triple: 'aarch64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
25
25
  };
26
+ export function resolveRtkBinary(managedDir = path.join(os.homedir(), '.bun', 'bin')) {
27
+ const name = process.platform === 'win32' ? 'rtk.exe' : 'rtk';
28
+ const candidates = (process.env.PATH || '').split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, name));
29
+ if (managedDir)
30
+ candidates.push(path.join(managedDir, name));
31
+ for (const candidate of candidates) {
32
+ try {
33
+ if (!statSync(candidate).isFile())
34
+ continue;
35
+ accessSync(candidate, constants.X_OK);
36
+ return candidate;
37
+ }
38
+ catch { /* keep searching */ }
39
+ }
40
+ return null;
41
+ }
26
42
  export function rtkPlatformSpec(platform = process.platform, arch = process.arch) {
27
43
  return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
28
44
  }
@@ -36,7 +52,7 @@ function redirectNext(res, url) {
36
52
  export function httpsGet(url, opts = {}) {
37
53
  const { promise, resolve, reject } = withResolvers();
38
54
  const maxRedirects = opts.maxRedirects ?? 5;
39
- const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' } }, (res) => {
55
+ const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' }, signal: opts.signal }, (res) => {
40
56
  const next = redirectNext(res, url);
41
57
  if (next) {
42
58
  if (maxRedirects <= 0) {
@@ -45,7 +61,7 @@ export function httpsGet(url, opts = {}) {
45
61
  return;
46
62
  }
47
63
  res.resume();
48
- resolve(httpsGet(next, { maxRedirects: maxRedirects - 1 }));
64
+ resolve(httpsGet(next, { maxRedirects: maxRedirects - 1, signal: opts.signal }));
49
65
  return;
50
66
  }
51
67
  if (res.statusCode !== 200) {
@@ -2,7 +2,7 @@
2
2
  // Node built-ins only — this module must stay dependency-free.
3
3
 
4
4
  import { createHash } from 'node:crypto';
5
- import { createWriteStream, existsSync, mkdirSync, renameSync } from 'node:fs';
5
+ import { accessSync, constants, createWriteStream, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
6
6
  import fs from 'node:fs/promises';
7
7
  import https from 'node:https';
8
8
  import os from 'node:os';
@@ -10,6 +10,7 @@ import path from 'node:path';
10
10
 
11
11
  export interface HttpOptions {
12
12
  maxRedirects?: number;
13
+ signal?: AbortSignal;
13
14
  }
14
15
 
15
16
  // Promise.withResolvers is Node 22+; the package supports Node 18+.
@@ -22,7 +23,7 @@ export function withResolvers<T>(): PromiseWithResolvers<T> {
22
23
  }
23
24
 
24
25
  export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
25
- export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rules/caveman-activate.md';
26
+ export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/caveman/SKILL.md';
26
27
 
27
28
  export interface RtkPlatformSpec {
28
29
  triple: string;
@@ -47,6 +48,19 @@ const RTK_PLATFORM_SPECS: Record<string, RtkPlatformSpec> = {
47
48
  'darwin/x64': { triple: 'x86_64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
48
49
  'darwin/arm64': { triple: 'aarch64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
49
50
  };
51
+ export function resolveRtkBinary(managedDir = path.join(os.homedir(), '.bun', 'bin')): string | null {
52
+ const name = process.platform === 'win32' ? 'rtk.exe' : 'rtk';
53
+ const candidates = (process.env.PATH || '').split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, name));
54
+ if (managedDir) candidates.push(path.join(managedDir, name));
55
+ for (const candidate of candidates) {
56
+ try {
57
+ if (!statSync(candidate).isFile()) continue;
58
+ accessSync(candidate, constants.X_OK);
59
+ return candidate;
60
+ } catch { /* keep searching */ }
61
+ }
62
+ return null;
63
+ }
50
64
 
51
65
  export function rtkPlatformSpec(platform: string = process.platform, arch: string = process.arch): RtkPlatformSpec | null {
52
66
  return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
@@ -63,12 +77,12 @@ function redirectNext(res: { statusCode?: number; headers: { location?: string }
63
77
  export function httpsGet(url: string, opts: HttpOptions = {}): Promise<string> {
64
78
  const { promise, resolve, reject } = withResolvers<string>();
65
79
  const maxRedirects = opts.maxRedirects ?? 5;
66
- const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' } }, (res) => {
80
+ const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' }, signal: opts.signal }, (res) => {
67
81
  const next = redirectNext(res, url);
68
82
  if (next) {
69
83
  if (maxRedirects <= 0) { res.resume(); reject(new Error(`Too many redirects fetching ${url}`)); return; }
70
84
  res.resume();
71
- resolve(httpsGet(next, { maxRedirects: maxRedirects - 1 }));
85
+ resolve(httpsGet(next, { maxRedirects: maxRedirects - 1, signal: opts.signal }));
72
86
  return;
73
87
  }
74
88
  if (res.statusCode !== 200) { res.resume(); reject(new Error(`HTTP ${res.statusCode} for ${url}`)); return; }
@@ -1,5 +1,3 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
1
  import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
4
2
  import { readRtkDefault } from '../shared/plugin-settings.js';
5
3
  const DEFAULT_ENABLED = false;
@@ -15,12 +13,13 @@ function asBoolean(value) {
15
13
  function resolveEnabled(entries) {
16
14
  return lastCustomValue(entries, 'rtk-mode', (data) => asBoolean(data?.enabled));
17
15
  }
18
- const IS_WINDOWS = process.platform === 'win32';
19
- const HOME = os.homedir();
20
- const RTK_BINARY = path.join(HOME, '.bun', 'bin', IS_WINDOWS ? 'rtk.exe' : 'rtk');
21
- const RTK_PROMPT = `RTK mode active for this session.
22
- Use Rust Token Killer for shell output that would otherwise be noisy. Prefer explicit RTK commands in bash: \`rtk git status\`, \`rtk git diff\`, \`rtk read <file>\`, \`rtk grep <pattern> <path>\`, \`rtk find <glob> <path>\`, \`rtk test <cmd...>\`, \`rtk tsc\`, \`rtk lint\`, or \`rtk <tool> ...\` for supported dev commands.
23
- Do not use RTK when exact raw output is required, when a specialized OMP tool is required by system policy, or when the command changes state and RTK would hide important confirmation text. Specialized OMP tools still win: read/glob/grep/edit/lsp stay preferred over shell equivalents.`;
16
+ function setRtkProcessEnabled(enabled) {
17
+ if (enabled)
18
+ delete process.env.RTK_DISABLED;
19
+ else
20
+ process.env.RTK_DISABLED = '1';
21
+ }
22
+ const RTK_PROMPT = `RTK guidance active. RTK automatically rewrites eligible Bash calls through the installed rtk hook. Prefer rtk for noisy shell output, but use exact raw output for state changes, checksums, patches, and diagnostics that need full bytes.`;
24
23
  export default function rtkSessionExtension(pi) {
25
24
  const { z } = pi.zod;
26
25
  let enabled = DEFAULT_ENABLED;
@@ -42,7 +41,7 @@ export default function rtkSessionExtension(pi) {
42
41
  enabled = Boolean(next);
43
42
  pi.appendEntry?.('rtk-mode', { enabled });
44
43
  setSharedComboMode('rtk', enabled);
45
- syncStatus(ctx);
44
+ setRtkProcessEnabled(enabled);
46
45
  const active = activeModesSummary(getSharedComboState());
47
46
  ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
48
47
  }
@@ -51,6 +50,7 @@ export default function rtkSessionExtension(pi) {
51
50
  // it at once so the next turn and the rtk_run gate see it, no reload.
52
51
  function syncFromShared(state) {
53
52
  enabled = state.rtk === 'on';
53
+ setRtkProcessEnabled(enabled);
54
54
  syncStatus();
55
55
  }
56
56
  setSharedComboListener(syncFromShared);
@@ -89,7 +89,7 @@ export default function rtkSessionExtension(pi) {
89
89
  };
90
90
  }
91
91
  onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
92
- const result = await pi.exec(RTK_BINARY, params.args, { signal, cwd: ctx?.cwd || pi.cwd });
92
+ const result = await pi.exec('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
93
93
  const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
94
94
  return {
95
95
  isError: result.code !== 0,
@@ -118,6 +118,7 @@ export default function rtkSessionExtension(pi) {
118
118
  // installer/user-configured default (off unless configured).
119
119
  const persisted = resolveEnabled(entries);
120
120
  enabled = typeof persisted === 'boolean' ? persisted : readRtkDefault();
121
+ setRtkProcessEnabled(enabled);
121
122
  syncStatus(ctx);
122
123
  }
123
124
  pi.on('session_start', async (_event, ctx) => {