@krtclcdy/tersio 2.24.0 → 2.24.1

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 (49) hide show
  1. package/README.md +1 -3
  2. package/cli/banner.js +2 -3
  3. package/cli/currency.js +3 -7
  4. package/cli/dashboard.js +167 -3
  5. package/cli/doctor-fix.js +15 -12
  6. package/cli/doctor.js +1 -12
  7. package/cli/hosts.js +7 -11
  8. package/cli/install.js +113 -71
  9. package/cli/interactive.js +36 -2
  10. package/cli/layer.js +1 -3
  11. package/cli/manifest.js +1 -4
  12. package/cli/profile.js +9 -7
  13. package/cli/reset.js +3 -7
  14. package/cli/rtk-wiring.js +1 -6
  15. package/cli/settings.js +3 -8
  16. package/cli/uninstall.js +43 -24
  17. package/cli/update.js +7 -8
  18. package/cli/usage.js +15 -5
  19. package/dashboard/dist/index.html +18 -18
  20. package/extensions/ai-addons-updater/index.js +6 -5
  21. package/extensions/ai-addons-updater/index.ts +7 -6
  22. package/extensions/caveman-session/index.js +14 -29
  23. package/extensions/caveman-session/index.ts +15 -30
  24. package/extensions/combo-toggle/index.js +10 -14
  25. package/extensions/combo-toggle/index.ts +10 -15
  26. package/extensions/lib/utils.js +4 -0
  27. package/extensions/lib/utils.ts +3 -0
  28. package/extensions/rtk-session/index.js +27 -31
  29. package/extensions/rtk-session/index.ts +25 -29
  30. package/extensions/shared/carbon.js +3 -8
  31. package/extensions/shared/carbon.ts +4 -8
  32. package/extensions/shared/host.js +1 -11
  33. package/extensions/shared/host.ts +1 -11
  34. package/extensions/shared/plugin-settings.js +4 -14
  35. package/extensions/shared/plugin-settings.ts +4 -14
  36. package/extensions/shared/pricing.js +4 -6
  37. package/extensions/shared/pricing.ts +4 -6
  38. package/extensions/shared/rtk-gain.js +2 -7
  39. package/extensions/shared/rtk-gain.ts +2 -7
  40. package/extensions/shared/session-state.js +30 -18
  41. package/extensions/shared/session-state.ts +33 -16
  42. package/extensions/shared/usage-ledger.js +138 -17
  43. package/extensions/shared/usage-ledger.ts +143 -19
  44. package/extensions/shared/usage-store.js +175 -23
  45. package/extensions/shared/usage-store.ts +171 -22
  46. package/extensions/tersio-commands/index.js +1 -5
  47. package/extensions/tersio-commands/index.ts +1 -5
  48. package/package.json +6 -7
  49. package/tersio.js +1 -3
@@ -310,15 +310,16 @@ export default function aiAddonsUpdaterExtension(pi) {
310
310
  const cleanParts = parts.filter((p) => p !== '--dry-run' && p !== 'dry-run');
311
311
  const sub = cleanParts[0];
312
312
  if (sub === 'check' || sub === 'status') {
313
- const summary = await checkAddons(ctx);
313
+ await checkAddons(ctx);
314
314
  notify(ctx, 'ai-addons check complete.', 'info');
315
- return summary;
315
+ return;
316
316
  }
317
317
  if (sub === 'update' && cleanParts[1]) {
318
- return runAddonUpdate(pi, ctx, cleanParts.slice(1).join(' '), dryRun);
318
+ await runAddonUpdate(pi, ctx, cleanParts.slice(1).join(' '), dryRun);
319
+ return;
319
320
  }
320
- const m = 'Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]';
321
- return report(ctx, m, 'warning');
321
+ report(ctx, 'Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]', 'warning');
322
+ return;
322
323
  },
323
324
  });
324
325
  }
@@ -54,7 +54,7 @@ interface AddonUpdaterCtx {
54
54
 
55
55
  interface AddonUpdaterPi {
56
56
  setLabel?: (label: string) => void;
57
- registerCommand?: (name: string, config: { description: string; handler: (args: string, ctx: AddonUpdaterCtx) => Promise<string> }) => void;
57
+ registerCommand?: (name: string, config: { description: string; handler: (args: string, ctx: AddonUpdaterCtx) => Promise<void> }) => void;
58
58
  exec?: (cmd: string, args: string[], opts?: { cwd?: string }) => Promise<{ stdout: string; stderr: string; code: number }>;
59
59
  cwd?: string;
60
60
  }
@@ -355,15 +355,16 @@ export default function aiAddonsUpdaterExtension(pi: AddonUpdaterPi): void {
355
355
  const sub = cleanParts[0];
356
356
 
357
357
  if (sub === 'check' || sub === 'status') {
358
- const summary = await checkAddons(ctx);
358
+ await checkAddons(ctx);
359
359
  notify(ctx, 'ai-addons check complete.', 'info');
360
- return summary;
360
+ return;
361
361
  }
362
362
  if (sub === 'update' && cleanParts[1]) {
363
- return runAddonUpdate(pi, ctx, cleanParts.slice(1).join(' '), dryRun);
363
+ await runAddonUpdate(pi, ctx, cleanParts.slice(1).join(' '), dryRun);
364
+ return;
364
365
  }
365
- const m = 'Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]';
366
- return report(ctx, m, 'warning');
366
+ report(ctx, 'Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]', 'warning');
367
+ return;
367
368
  },
368
369
  });
369
370
  }
@@ -1,32 +1,18 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
3
+ import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, statusUi, systemPromptIncludes } from '../shared/session-state.js';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
6
6
  import { readCavemanDefault } from '../shared/plugin-settings.js';
7
7
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
8
8
  const RULE_PATH = join(CAVERN_DIR, 'rule.md');
9
- const FALLBACK_FULL_RULE = `Caveman full active for this session.
10
- Respond terse like smart caveman. Keep all technical substance. Drop filler, pleasantries, and hedging.
11
-
12
- Rules:
13
- - Drop articles where meaning stays clear. Keep fragments and short sentences.
14
- - Keep technical terms, code, commands, paths, API names, numbers, units, and errors exact.
15
- - Never invent abbreviations or causal arrows. Do not add words to sound caveman.
16
- - Use ASD-STE100 Simplified Technical English. Keep one idea per sentence, target 20 words, active voice, and consistent terms.
17
- - Make no tool-call narration. Use no decorative tables or emoji.
18
- - Preserve the user's reply language. Compress style, not language.
19
- - Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or requests to clarify.
20
- - Write code, comments, commits, docs, issues, pull requests, and third-party messages in normal prose.
21
-
22
- Default: **full**. Switch: \`/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off\`. Stop: "stop caveman" or "normal mode".`;
23
- // ponytail: synchronous read each full-mode injection; ceiling = small file, cold session start. Upgrade path: cache file contents + mtime, invalidate on change.
9
+ // Fail loud: a wrong rule reads to the model as the real one.
24
10
  function readFullRule() {
25
11
  try {
26
12
  return readFileSync(RULE_PATH, 'utf8');
27
13
  }
28
14
  catch {
29
- return FALLBACK_FULL_RULE;
15
+ return 'Caveman full requested, but rule.md is missing. Run: tersio doctor --fix extensions';
30
16
  }
31
17
  }
32
18
  const DEFAULT_MODE = 'off';
@@ -53,15 +39,15 @@ export default function cavemanSessionExtension(pi) {
53
39
  let lastCtx = undefined;
54
40
  function syncStatus(ctx) {
55
41
  lastCtx = paintableCtx(lastCtx, ctx);
56
- const c = lastCtx;
57
- if (!c?.ui?.setStatus)
42
+ const ui = statusUi(lastCtx);
43
+ if (!ui)
58
44
  return;
59
45
  // Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
60
46
  if (isComboPresetActive() || currentMode === 'off') {
61
- c.ui.setStatus('caveman', undefined);
47
+ ui.setStatus('caveman', undefined);
62
48
  return;
63
49
  }
64
- paintStatusBar(c.ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
50
+ paintStatusBar(ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
65
51
  }
66
52
  function setMode(mode, ctx) {
67
53
  const normalized = normalizeMode('caveman', mode);
@@ -79,9 +65,7 @@ export default function cavemanSessionExtension(pi) {
79
65
  return true;
80
66
  }
81
67
  setExtensionLabel(pi, 'Caveman session toggle');
82
- // Live mirror: a /tersio or /combo switch publishes shared state — adopt
83
- // it at once so the next turn injects the new mode with no session reload.
84
- // Stable identity, so the bridge set dedupes across re-inits.
68
+ // Adopt a /tersio or /combo switch at once, so the next turn needs no reload.
85
69
  function syncFromShared(state) {
86
70
  const mode = normalizeMode('caveman', state.caveman);
87
71
  if (mode) {
@@ -89,7 +73,7 @@ export default function cavemanSessionExtension(pi) {
89
73
  syncStatus();
90
74
  }
91
75
  }
92
- setSharedComboListener(syncFromShared);
76
+ setSharedComboListener('caveman', syncFromShared);
93
77
  pi.registerCommand?.('caveman', {
94
78
  description: 'Toggle terse caveman replies for this session',
95
79
  handler: async (args, ctx) => {
@@ -115,10 +99,7 @@ export default function cavemanSessionExtension(pi) {
115
99
  });
116
100
  function restoreMode(ctx) {
117
101
  const entries = sessionEntries(ctx);
118
- // Publish the persisted combo state (incl. combo-level) before painting:
119
- // bar suppression reads the in-process bridge, which is empty in a fresh
120
- // host until the combo extension reconciles — and its reconcile is
121
- // UI-gated. Deriving it here makes suppression independent of load order.
102
+ // Derive the bridge here so bar suppression does not depend on load order.
122
103
  reconcileSharedComboEntries(entries);
123
104
  // Persisted session state wins; a fresh session falls back to the
124
105
  // installer/user-configured default (off unless configured).
@@ -152,6 +133,10 @@ export default function cavemanSessionExtension(pi) {
152
133
  if (!def)
153
134
  return;
154
135
  const instruction = typeof def === 'function' ? def() : def;
136
+ // Skip when already present, so a host that re-presents the mutated prompt
137
+ // cannot stack it; matching the whole instruction keeps upstream rule.md intact.
138
+ if (systemPromptIncludes(event.systemPrompt, instruction))
139
+ return;
155
140
  return injectPromptText(pi, event, instruction);
156
141
  });
157
142
  }
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.ts';
3
+ import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, statusUi, systemPromptIncludes } from '../shared/session-state.ts';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
6
6
  import { readCavemanDefault } from '../shared/plugin-settings.ts';
@@ -9,24 +9,11 @@ import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPrompt
9
9
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
10
10
  const RULE_PATH = join(CAVERN_DIR, 'rule.md');
11
11
 
12
- const FALLBACK_FULL_RULE = `Caveman full active for this session.
13
- Respond terse like smart caveman. Keep all technical substance. Drop filler, pleasantries, and hedging.
14
-
15
- Rules:
16
- - Drop articles where meaning stays clear. Keep fragments and short sentences.
17
- - Keep technical terms, code, commands, paths, API names, numbers, units, and errors exact.
18
- - Never invent abbreviations or causal arrows. Do not add words to sound caveman.
19
- - Use ASD-STE100 Simplified Technical English. Keep one idea per sentence, target 20 words, active voice, and consistent terms.
20
- - Make no tool-call narration. Use no decorative tables or emoji.
21
- - Preserve the user's reply language. Compress style, not language.
22
- - Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or requests to clarify.
23
- - Write code, comments, commits, docs, issues, pull requests, and third-party messages in normal prose.
24
-
25
- Default: **full**. Switch: \`/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off\`. Stop: "stop caveman" or "normal mode".`;
26
-
27
- // ponytail: synchronous read each full-mode injection; ceiling = small file, cold session start. Upgrade path: cache file contents + mtime, invalidate on change.
12
+ // Fail loud: a wrong rule reads to the model as the real one.
28
13
  function readFullRule(): string {
29
- try { return readFileSync(RULE_PATH, 'utf8'); } catch { return FALLBACK_FULL_RULE; }
14
+ try { return readFileSync(RULE_PATH, 'utf8'); } catch {
15
+ return 'Caveman full requested, but rule.md is missing. Run: tersio doctor --fix extensions';
16
+ }
30
17
  }
31
18
 
32
19
  const DEFAULT_MODE = 'off';
@@ -57,14 +44,14 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
57
44
 
58
45
  function syncStatus(ctx?: ExtensionCtx): void {
59
46
  lastCtx = paintableCtx(lastCtx, ctx);
60
- const c = lastCtx;
61
- if (!c?.ui?.setStatus) return;
47
+ const ui = statusUi(lastCtx);
48
+ if (!ui) return;
62
49
  // Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
63
50
  if (isComboPresetActive() || currentMode === 'off') {
64
- c.ui.setStatus('caveman', undefined);
51
+ ui.setStatus('caveman', undefined);
65
52
  return;
66
53
  }
67
- paintStatusBar(c.ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
54
+ paintStatusBar(ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
68
55
  }
69
56
 
70
57
  function setMode(mode: string, ctx?: ExtensionCtx): boolean {
@@ -84,9 +71,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
84
71
 
85
72
  setExtensionLabel(pi, 'Caveman session toggle');
86
73
 
87
- // Live mirror: a /tersio or /combo switch publishes shared state — adopt
88
- // it at once so the next turn injects the new mode with no session reload.
89
- // Stable identity, so the bridge set dedupes across re-inits.
74
+ // Adopt a /tersio or /combo switch at once, so the next turn needs no reload.
90
75
  function syncFromShared(state: { caveman: string }): void {
91
76
  const mode = normalizeMode('caveman', state.caveman);
92
77
  if (mode) {
@@ -94,7 +79,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
94
79
  syncStatus();
95
80
  }
96
81
  }
97
- setSharedComboListener(syncFromShared);
82
+ setSharedComboListener('caveman', syncFromShared);
98
83
  pi.registerCommand?.('caveman', {
99
84
  description: 'Toggle terse caveman replies for this session',
100
85
  handler: async (args, ctx) => {
@@ -120,10 +105,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
120
105
 
121
106
  function restoreMode(ctx?: ExtensionCtx): void {
122
107
  const entries = sessionEntries(ctx);
123
- // Publish the persisted combo state (incl. combo-level) before painting:
124
- // bar suppression reads the in-process bridge, which is empty in a fresh
125
- // host until the combo extension reconciles — and its reconcile is
126
- // UI-gated. Deriving it here makes suppression independent of load order.
108
+ // Derive the bridge here so bar suppression does not depend on load order.
127
109
  reconcileSharedComboEntries(entries);
128
110
  // Persisted session state wins; a fresh session falls back to the
129
111
  // installer/user-configured default (off unless configured).
@@ -161,6 +143,9 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
161
143
  const def = INSTRUCTIONS[mode];
162
144
  if (!def) return;
163
145
  const instruction = typeof def === 'function' ? def() : def;
146
+ // Skip when already present, so a host that re-presents the mutated prompt
147
+ // cannot stack it; matching the whole instruction keeps upstream rule.md intact.
148
+ if (systemPromptIncludes(event.systemPrompt, instruction)) return;
164
149
  return injectPromptText(pi, event, instruction);
165
150
  });
166
151
  }
@@ -9,7 +9,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  };
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath, pathToFileURL } from 'node:url';
12
- import { activeModesSummary, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, systemPromptIncludes, themeStatus, } from '../shared/session-state.js';
12
+ import { activeModesSummary, COMBO_LEVELS, getSharedComboState, isComboPresetActive, normalizeComboLevel, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, statusUi, setSharedComboListener, setSharedComboMode, systemPromptIncludes, themeStatus, } from '../shared/session-state.js';
13
13
  import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
14
14
  import { isComboSetupComplete, readComboDefault, readPonytailDefault, saveComboSetup, } from '../shared/plugin-settings.js';
15
15
  import { findHoistedPackage } from '../lib/utils.js';
@@ -46,12 +46,12 @@ export default function comboToggleExtension(pi) {
46
46
  let setupPrompted = false;
47
47
  function syncStatus(ctx) {
48
48
  lastCtx = paintableCtx(lastCtx, ctx);
49
- const c = lastCtx;
50
- if (!c?.ui?.setStatus)
49
+ const ui = statusUi(lastCtx);
50
+ if (!ui)
51
51
  return;
52
52
  // One unified bar replaces the per-extension bars while a preset is active.
53
53
  if (!isComboPresetActive()) {
54
- c.ui.setStatus('combo', undefined);
54
+ ui.setStatus('combo', undefined);
55
55
  return;
56
56
  }
57
57
  // Read the live bridge, not a cache: siblings reconcile it first.
@@ -60,11 +60,11 @@ export default function comboToggleExtension(pi) {
60
60
  const r = state.rtk.toUpperCase();
61
61
  const p = state.ponytail.toUpperCase();
62
62
  const lvl = state.level.toUpperCase();
63
- c.ui.setStatus('combo', themeStatus(c.ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
63
+ ui.setStatus('combo', themeStatus(ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
64
64
  // Our bar is canonical for the preset, so clear any sibling bar.
65
- c.ui.setStatus('caveman', undefined);
66
- c.ui.setStatus('rtk', undefined);
67
- c.ui.setStatus('ponytail', undefined);
65
+ ui.setStatus('caveman', undefined);
66
+ ui.setStatus('rtk', undefined);
67
+ ui.setStatus('ponytail', undefined);
68
68
  }
69
69
  // Siblings restore from these entries, so the fallback must write them too.
70
70
  function persistPreset(level) {
@@ -84,9 +84,8 @@ export default function comboToggleExtension(pi) {
84
84
  return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
85
85
  }
86
86
  function listen(ctx) {
87
- // Stable identity: the bridge set dedupes, so repeat calls register once.
88
87
  if (ctx?.hasUI)
89
- setSharedComboListener(useState);
88
+ setSharedComboListener('combo', useState);
90
89
  }
91
90
  function track(ctx) {
92
91
  listen(ctx);
@@ -175,13 +174,10 @@ export default function comboToggleExtension(pi) {
175
174
  pi.on('before_agent_start', async (event, ctx) => {
176
175
  if (ctx?.hasUI)
177
176
  reconcile(ctx);
178
- if (!isOmpSubagentPrompt(event.systemPrompt))
179
- return;
180
177
  const mode = getSharedComboState().ponytail;
181
178
  if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE'))
182
179
  return;
183
- const instructions = await loadPonytailInstructions(mode);
184
- return injectPromptText(pi, event, instructions);
180
+ return injectPromptText(pi, event, await loadPonytailInstructions(mode));
185
181
  });
186
182
  // Slash commands only; natural-language input caused accidental toggles and has no reload context.
187
183
  }
@@ -1,6 +1,5 @@
1
1
  // /combo session toggle: off | medium | balanced | max.
2
2
 
3
- import os from 'node:os';
4
3
  import path from 'node:path';
5
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
5
  import {
@@ -8,12 +7,12 @@ import {
8
7
  COMBO_LEVELS,
9
8
  getSharedComboState,
10
9
  isComboPresetActive,
11
- isOmpSubagentPrompt,
12
10
  normalizeComboLevel,
13
11
  paintableCtx,
14
12
  reconcileSharedComboEntries,
15
13
  sessionEntries,
16
14
  setSharedComboLevel,
15
+ statusUi,
17
16
  setSharedComboListener,
18
17
  setSharedComboMode,
19
18
  systemPromptIncludes,
@@ -70,11 +69,11 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
70
69
 
71
70
  function syncStatus(ctx?: ExtensionCtx): void {
72
71
  lastCtx = paintableCtx(lastCtx, ctx);
73
- const c = lastCtx;
74
- if (!c?.ui?.setStatus) return;
72
+ const ui = statusUi(lastCtx);
73
+ if (!ui) return;
75
74
  // One unified bar replaces the per-extension bars while a preset is active.
76
75
  if (!isComboPresetActive()) {
77
- c.ui.setStatus('combo', undefined);
76
+ ui.setStatus('combo', undefined);
78
77
  return;
79
78
  }
80
79
  // Read the live bridge, not a cache: siblings reconcile it first.
@@ -83,11 +82,11 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
83
82
  const r = state.rtk.toUpperCase();
84
83
  const p = state.ponytail.toUpperCase();
85
84
  const lvl = state.level.toUpperCase();
86
- c.ui.setStatus('combo', themeStatus(c.ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
85
+ ui.setStatus('combo', themeStatus(ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
87
86
  // Our bar is canonical for the preset, so clear any sibling bar.
88
- c.ui.setStatus('caveman', undefined);
89
- c.ui.setStatus('rtk', undefined);
90
- c.ui.setStatus('ponytail', undefined);
87
+ ui.setStatus('caveman', undefined);
88
+ ui.setStatus('rtk', undefined);
89
+ ui.setStatus('ponytail', undefined);
91
90
  }
92
91
 
93
92
  // Siblings restore from these entries, so the fallback must write them too.
@@ -109,8 +108,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
109
108
  return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
110
109
  }
111
110
  function listen(ctx?: ExtensionCtx): void {
112
- // Stable identity: the bridge set dedupes, so repeat calls register once.
113
- if (ctx?.hasUI) setSharedComboListener(useState);
111
+ if (ctx?.hasUI) setSharedComboListener('combo', useState);
114
112
  }
115
113
 
116
114
  function track(ctx?: ExtensionCtx): void {
@@ -218,12 +216,9 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
218
216
 
219
217
  pi.on<SystemPromptEvent>('before_agent_start', async (event, ctx) => {
220
218
  if (ctx?.hasUI) reconcile(ctx);
221
- if (!isOmpSubagentPrompt(event.systemPrompt)) return;
222
-
223
219
  const mode = getSharedComboState().ponytail;
224
220
  if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE')) return;
225
- const instructions = await loadPonytailInstructions(mode);
226
- return injectPromptText(pi, event, instructions);
221
+ return injectPromptText(pi, event, await loadPonytailInstructions(mode));
227
222
  });
228
223
 
229
224
  // Slash commands only; natural-language input caused accidental toggles and has no reload context.
@@ -198,6 +198,10 @@ export async function readTextIfExists(p) {
198
198
  // ~/.omp/plugins/tersio-* files over so existing installs keep their history.
199
199
  const migratedTersioFiles = new Set();
200
200
  export function tersioHome() {
201
+ // Matches the other TERSIO_* overrides so a test never reads the real home.
202
+ const override = process.env.TERSIO_HOME;
203
+ if (override && override.trim() !== '')
204
+ return override;
201
205
  return path.join(os.homedir(), '.tersio');
202
206
  }
203
207
  export function tersioDataPath(name, legacy) {
@@ -205,6 +205,9 @@ export async function readTextIfExists(p: string): Promise<string | null> {
205
205
  const migratedTersioFiles = new Set<string>();
206
206
 
207
207
  export function tersioHome(): string {
208
+ // Matches the other TERSIO_* overrides so a test never reads the real home.
209
+ const override = process.env.TERSIO_HOME;
210
+ if (override && override.trim() !== '') return override;
208
211
  return path.join(os.homedir(), '.tersio');
209
212
  }
210
213
 
@@ -1,4 +1,4 @@
1
- import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
1
+ import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, statusUi, systemPromptIncludes } from '../shared/session-state.js';
2
2
  import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.js';
3
3
  import { readRtkDefault } from '../shared/plugin-settings.js';
4
4
  const DEFAULT_ENABLED = false;
@@ -25,17 +25,20 @@ export default function rtkSessionExtension(pi) {
25
25
  let enabled = DEFAULT_ENABLED;
26
26
  let isActive = false;
27
27
  let lastCtx = undefined;
28
+ function fail(text) {
29
+ return { isError: true, content: [{ type: 'text', text }], details: { enabled } };
30
+ }
28
31
  function syncStatus(ctx) {
29
32
  lastCtx = paintableCtx(lastCtx, ctx);
30
- const c = lastCtx;
31
- if (!c?.ui?.setStatus)
33
+ const ui = statusUi(lastCtx);
34
+ if (!ui)
32
35
  return;
33
36
  // Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
34
37
  if (isComboPresetActive() || !enabled) {
35
- c.ui.setStatus('rtk', undefined);
38
+ ui.setStatus('rtk', undefined);
36
39
  return;
37
40
  }
38
- paintStatusBar(c.ui, 'rtk', '⚡', 'rtk: ON', isActive);
41
+ paintStatusBar(ui, 'rtk', '⚡', 'rtk: ON', isActive);
39
42
  }
40
43
  function setEnabled(next, ctx) {
41
44
  enabled = Boolean(next);
@@ -53,7 +56,7 @@ export default function rtkSessionExtension(pi) {
53
56
  setRtkProcessEnabled(enabled);
54
57
  syncStatus();
55
58
  }
56
- setSharedComboListener(syncFromShared);
59
+ setSharedComboListener('rtk', syncFromShared);
57
60
  pi.registerCommand?.('rtk', {
58
61
  description: 'Toggle RTK compact shell-output guidance for this session',
59
62
  handler: async (args, ctx) => {
@@ -82,38 +85,28 @@ export default function rtkSessionExtension(pi) {
82
85
  description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
83
86
  }),
84
87
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
85
- if (!enabled) {
88
+ if (!enabled)
89
+ return fail('RTK mode is off. Run /rtk on for this session, or use bash explicitly.');
90
+ if (typeof pi.exec !== 'function')
91
+ return fail('This host cannot run rtk.');
92
+ onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
93
+ try {
94
+ const result = await pi.exec('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
95
+ const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
86
96
  return {
87
- isError: true,
88
- content: [{ type: 'text', text: 'RTK mode is off. Run /rtk on for this session, or use bash explicitly.' }],
89
- details: { enabled },
97
+ isError: result.code !== 0,
98
+ content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
99
+ details: { code: result.code, killed: result.killed, enabled },
90
100
  };
91
101
  }
92
- onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
93
- const result = await pi.exec('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
94
- const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
95
- return {
96
- isError: result.code !== 0,
97
- content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
98
- details: { code: result.code, killed: result.killed, enabled },
99
- };
102
+ catch (e) {
103
+ return fail(`rtk could not run: ${e instanceof Error ? e.message : String(e)}`);
104
+ }
100
105
  },
101
106
  });
102
- onHostEvent(pi, 'input', async (event) => {
103
- if (event?.source === 'extension')
104
- return;
105
- const t = normalizeInputCommand(event?.text);
106
- if (t === 'rtk on' || t === 'use rtk')
107
- setEnabled(true);
108
- if (t === 'rtk off' || t === 'stop rtk')
109
- setEnabled(false);
110
- });
111
107
  function restoreEnabled(ctx) {
112
108
  const entries = sessionEntries(ctx);
113
- // Publish the persisted combo state (incl. combo-level) before painting:
114
- // bar suppression reads the in-process bridge, which is empty in a fresh
115
- // host until the combo extension reconciles — and its reconcile is
116
- // UI-gated. Deriving it here makes suppression independent of load order.
109
+ // Derive the bridge here so bar suppression does not depend on load order.
117
110
  reconcileSharedComboEntries(entries);
118
111
  // Persisted session state wins; a fresh session falls back to the
119
112
  // installer/user-configured default (off unless configured).
@@ -144,6 +137,9 @@ export default function rtkSessionExtension(pi) {
144
137
  const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
145
138
  if (!active)
146
139
  return;
140
+ // Skip when already present, so a host that re-presents the prompt cannot stack it.
141
+ if (systemPromptIncludes(event.systemPrompt, RTK_PROMPT))
142
+ return;
147
143
  return injectPromptText(pi, event, RTK_PROMPT);
148
144
  });
149
145
  }
@@ -1,7 +1,7 @@
1
- import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.ts';
1
+ import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, statusUi, systemPromptIncludes } from '../shared/session-state.ts';
2
2
  import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.ts';
3
3
  import { readRtkDefault } from '../shared/plugin-settings.ts';
4
- import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
4
+ import type { ExtensionApi, ExtensionCtx, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
5
5
 
6
6
  const DEFAULT_ENABLED = false;
7
7
 
@@ -28,16 +28,20 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
28
28
  let isActive = false;
29
29
  let lastCtx: ExtensionCtx | undefined = undefined;
30
30
 
31
+ function fail(text: string) {
32
+ return { isError: true as const, content: [{ type: 'text' as const, text }], details: { enabled } };
33
+ }
34
+
31
35
  function syncStatus(ctx?: ExtensionCtx): void {
32
36
  lastCtx = paintableCtx(lastCtx, ctx);
33
- const c = lastCtx;
34
- if (!c?.ui?.setStatus) return;
37
+ const ui = statusUi(lastCtx);
38
+ if (!ui) return;
35
39
  // Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
36
40
  if (isComboPresetActive() || !enabled) {
37
- c.ui.setStatus('rtk', undefined);
41
+ ui.setStatus('rtk', undefined);
38
42
  return;
39
43
  }
40
- paintStatusBar(c.ui, 'rtk', '⚡', 'rtk: ON', isActive);
44
+ paintStatusBar(ui, 'rtk', '⚡', 'rtk: ON', isActive);
41
45
  }
42
46
 
43
47
  function setEnabled(next: unknown, ctx?: ExtensionCtx): void {
@@ -58,7 +62,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
58
62
  setRtkProcessEnabled(enabled);
59
63
  syncStatus();
60
64
  }
61
- setSharedComboListener(syncFromShared);
65
+ setSharedComboListener('rtk', syncFromShared);
62
66
  pi.registerCommand?.('rtk', {
63
67
  description: 'Toggle RTK compact shell-output guidance for this session',
64
68
  handler: async (args, ctx) => {
@@ -88,36 +92,26 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
88
92
  description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
89
93
  }),
90
94
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
91
- if (!enabled) {
95
+ if (!enabled) return fail('RTK mode is off. Run /rtk on for this session, or use bash explicitly.');
96
+ if (typeof pi.exec !== 'function') return fail('This host cannot run rtk.');
97
+ onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
98
+ try {
99
+ const result = await pi.exec('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
100
+ const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
92
101
  return {
93
- isError: true,
94
- content: [{ type: 'text', text: 'RTK mode is off. Run /rtk on for this session, or use bash explicitly.' }],
95
- details: { enabled },
102
+ isError: result.code !== 0,
103
+ content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
104
+ details: { code: result.code, killed: result.killed, enabled },
96
105
  };
106
+ } catch (e) {
107
+ return fail(`rtk could not run: ${e instanceof Error ? e.message : String(e)}`);
97
108
  }
98
- onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
99
- const result = await pi.exec!('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
100
- const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
101
- return {
102
- isError: result.code !== 0,
103
- content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
104
- details: { code: result.code, killed: result.killed, enabled },
105
- };
106
109
  },
107
110
  });
108
111
 
109
- onHostEvent<InputEvent>(pi, 'input', async (event) => {
110
- if (event?.source === 'extension') return;
111
- const t = normalizeInputCommand(event?.text);
112
- if (t === 'rtk on' || t === 'use rtk') setEnabled(true);
113
- if (t === 'rtk off' || t === 'stop rtk') setEnabled(false);
114
- });
115
112
  function restoreEnabled(ctx?: ExtensionCtx): void {
116
113
  const entries = sessionEntries(ctx);
117
- // Publish the persisted combo state (incl. combo-level) before painting:
118
- // bar suppression reads the in-process bridge, which is empty in a fresh
119
- // host until the combo extension reconciles — and its reconcile is
120
- // UI-gated. Deriving it here makes suppression independent of load order.
114
+ // Derive the bridge here so bar suppression does not depend on load order.
121
115
  reconcileSharedComboEntries(entries);
122
116
  // Persisted session state wins; a fresh session falls back to the
123
117
  // installer/user-configured default (off unless configured).
@@ -153,6 +147,8 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
153
147
  pi.on<SystemPromptEvent>('before_agent_start', async (event) => {
154
148
  const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
155
149
  if (!active) return;
150
+ // Skip when already present, so a host that re-presents the prompt cannot stack it.
151
+ if (systemPromptIncludes(event.systemPrompt, RTK_PROMPT)) return;
156
152
  return injectPromptText(pi, event, RTK_PROMPT);
157
153
  });
158
154
  }
@@ -1,11 +1,4 @@
1
- // extensions/shared/carbon.ts — CO2/energy footprints, EcoLogits-style.
2
- //
3
- // Port of the EcoLogits v0.8.2 LLM impact model (genai-impact/ecologits,
4
- // MPL-2.0 — see ECO_NOTICE.md): the single-stream (batch-1) ceiling divided by
5
- // serving_concurrency for continuous-batching amortization. Model params come
6
- // from EcoLogits' registry, else a documented class borrow; unknown ids fall
7
- // to the gpt-4o default. Render everything from here as ~est., never merged
8
- // with measured figures.
1
+ // EcoLogits v0.8.2 port; always render as ~est, never merged with measured.
9
2
  // EcoLogits 0.8.2 constants — kWh, kgCO2eq, seconds, GB.
10
3
  const Q_BITS = 4;
11
4
  const E_ALPHA = 8.91e-8;
@@ -28,6 +21,8 @@ const GRIDS = {
28
21
  };
29
22
  // Serving amortization: single-stream ceiling ÷ concurrency ≈ served impact.
30
23
  export const SERVING_CONCURRENCY = 32;
24
+ /** EcoLogits release this port tracks, so a UI can name it instead of guessing. */
25
+ export const ECOLOGITS_VERSION = '0.8.2';
31
26
  // Registry values verbatim from EcoLogits' model_repository; borrows cite
32
27
  // the public param disclosure they stand in for (MoE active/total).
33
28
  const TABLE = [