@krtclcdy/tersio 2.23.0 → 2.24.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.
Files changed (55) hide show
  1. package/README.md +58 -45
  2. package/cli/agent-hosts.js +42 -0
  3. package/cli/agents.js +266 -0
  4. package/cli/common.js +16 -20
  5. package/cli/currency.js +8 -12
  6. package/cli/dashboard.js +49 -37
  7. package/cli/doctor-fix.js +28 -17
  8. package/cli/doctor.js +52 -41
  9. package/cli/hosts.js +118 -0
  10. package/cli/install.js +180 -117
  11. package/cli/interactive.js +27 -1
  12. package/cli/layer.js +24 -0
  13. package/cli/manifest.js +36 -0
  14. package/cli/omp-layer.js +51 -0
  15. package/cli/pi-layer.js +37 -0
  16. package/cli/pi-wiring.js +92 -0
  17. package/cli/profile.js +44 -46
  18. package/cli/reset.js +3 -26
  19. package/cli/rtk-wiring.js +10 -15
  20. package/cli/uninstall.js +135 -44
  21. package/cli/update.js +12 -19
  22. package/dashboard/dist/index.html +16 -16
  23. package/extensions/ai-addons-updater/index.js +31 -24
  24. package/extensions/ai-addons-updater/index.ts +32 -32
  25. package/extensions/caveman-session/index.js +6 -6
  26. package/extensions/caveman-session/index.ts +6 -6
  27. package/extensions/combo-toggle/index.js +42 -40
  28. package/extensions/combo-toggle/index.ts +36 -47
  29. package/extensions/lib/utils.js +36 -9
  30. package/extensions/lib/utils.ts +37 -9
  31. package/extensions/rtk-session/index.js +9 -9
  32. package/extensions/rtk-session/index.ts +9 -14
  33. package/extensions/shared/carbon.js +5 -8
  34. package/extensions/shared/carbon.ts +5 -8
  35. package/extensions/shared/host.js +68 -0
  36. package/extensions/shared/host.ts +104 -0
  37. package/extensions/shared/omp-prompt.js +12 -0
  38. package/extensions/shared/plugin-settings.js +43 -30
  39. package/extensions/shared/plugin-settings.ts +45 -26
  40. package/extensions/shared/pricing.js +26 -15
  41. package/extensions/shared/pricing.ts +28 -16
  42. package/extensions/shared/rtk-gain.js +11 -22
  43. package/extensions/shared/rtk-gain.ts +11 -22
  44. package/extensions/shared/session-state.js +21 -25
  45. package/extensions/shared/session-state.ts +22 -26
  46. package/extensions/shared/types.ts +10 -3
  47. package/extensions/shared/usage-ledger.js +24 -36
  48. package/extensions/shared/usage-ledger.ts +29 -50
  49. package/extensions/shared/usage-store.js +14 -12
  50. package/extensions/shared/usage-store.ts +16 -14
  51. package/extensions/tersio-commands/index.js +2 -1
  52. package/extensions/tersio-commands/index.ts +2 -1
  53. package/package.json +4 -2
  54. package/tersio.js +4 -4
  55. package/extensions/shared/mode-reinforcement.js +0 -26
@@ -1,21 +1,26 @@
1
- // OMP extension: /ai-addons manual updater for Ponytail, RTK, Caveman.
2
- // Built-in Node modules only. Default off; registers a single slash command.
3
- // ponytail: `skipped: none` — semantics match one-liner: fetch + compare + run install.
4
- // rtk: `skipped: signature verification` — checksums.txt ships only SHA256 of release assets; add sigchain when upstream publishes a signing key.
5
- // caveman: `skipped: none` — exactly the ask: write rule.md, report old/new hash.
1
+ // /ai-addons manual updater. Node built-ins only. RTK ships SHA256 checksums
2
+ // with no signature, so checksum-only is the best available.
6
3
  import { execFileSync } from 'node:child_process';
7
4
  import { createHash } from 'node:crypto';
8
5
  import fs from 'node:fs/promises';
9
6
  import os from 'node:os';
10
7
  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, resolveRtkBinary, } from '../lib/utils.js';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { setExtensionLabel } from '../shared/host.js';
10
+ 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';
12
11
  const IS_WINDOWS = process.platform === 'win32';
13
- const HOME = os.homedir();
12
+ const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
14
13
  const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
15
- const PONYTAIL_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
16
14
  const RTK_BINARY = resolveRtkBinary();
17
- const CAVEMAN_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman-session', 'rule.md');
18
- const RELOAD_MSG = 'Reminder: restart OMP (or reload extensions) for updates to take effect.';
15
+ // Beside this file, not a host path: that is the rule.md /caveman reads.
16
+ const CAVEMAN_LOCAL = path.join(EXTENSION_DIR, '..', 'caveman-session', 'rule.md');
17
+ const HOST_NAME = isPiProcess() ? 'Pi' : 'OMP';
18
+ const RELOAD_MSG = `Reminder: restart ${HOST_NAME} (or reload extensions) for updates to take effect.`;
19
+ // Ponytail is a hoisted dependency, so the installed copy is found by walking
20
+ // up rather than by naming a host directory.
21
+ function ponytailLocal() {
22
+ return findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'package.json');
23
+ }
19
24
  function notify(ctx, msg, level) {
20
25
  ctx?.ui?.notify?.(String(msg), level);
21
26
  }
@@ -29,16 +34,25 @@ function shortHash(text) {
29
34
  function checkFailed(name, e) {
30
35
  return { text: `${name} check failed: ${e.message}`, level: 'warning' };
31
36
  }
32
- // Single error-handling source for the three probes; messages unchanged.
33
37
  function runCheck(name, probe) {
34
38
  return probe().then((text) => ({ text, level: 'info' }), (e) => checkFailed(name, e));
35
39
  }
36
- // Check: no mutation. Probes run concurrently via checkAddons below.
40
+ // A missing file is "not installed"; an unreadable one is a reportable fault.
41
+ function parsePackageVersion(raw) {
42
+ if (!raw)
43
+ return null;
44
+ try {
45
+ return JSON.parse(raw).version ?? null;
46
+ }
47
+ catch {
48
+ throw new Error('ponytail package.json is not readable JSON');
49
+ }
50
+ }
37
51
  function checkPonytail() {
38
52
  return runCheck('Ponytail', async () => {
39
53
  const remoteJson = await fetchJson(PONYTAIL_REMOTE);
40
- const localRaw = await readTextIfExists(PONYTAIL_LOCAL);
41
- const localVer = localRaw ? JSON.parse(localRaw).version ?? null : null;
54
+ const localRaw = await readTextIfExists(ponytailLocal() ?? '');
55
+ const localVer = parsePackageVersion(localRaw);
42
56
  const remoteVer = remoteJson.version;
43
57
  const status = !localVer ? 'not installed'
44
58
  : localVer === remoteVer ? 'up to date'
@@ -120,13 +134,10 @@ async function checkAddons(ctx) {
120
134
  return lines.join('\n');
121
135
  }
122
136
  async function updatePonytail(pi, ctx, dryRun = false) {
123
- // Bundled with tersio: no separate package to refresh. `tersio update`
124
- // pulls the bundled copy with the CLI.
137
+ // Bundled with tersio: `tersio update` pulls the copy with the CLI.
125
138
  let localVer = null;
126
139
  try {
127
- const raw = await readTextIfExists(PONYTAIL_LOCAL);
128
- if (raw)
129
- localVer = JSON.parse(raw).version ?? null;
140
+ localVer = parsePackageVersion(await readTextIfExists(ponytailLocal() ?? ''));
130
141
  }
131
142
  catch {
132
143
  localVer = null;
@@ -150,7 +161,6 @@ async function updateRtk(ctx, dryRun = false) {
150
161
  const assets = Array.isArray(release.assets) ? release.assets : [];
151
162
  if (!RTK_BINARY)
152
163
  return report(ctx, 'RTK: executable not found in PATH', 'warning');
153
- // Cross-platform asset selection (mirrors installer stepRtk)
154
164
  const PLATFORM = process.platform;
155
165
  const ARCH = process.arch;
156
166
  const spec = rtkPlatformSpec(PLATFORM, ARCH);
@@ -179,7 +189,6 @@ async function updateRtk(ctx, dryRun = false) {
179
189
  httpsDownload(asset.browser_download_url, archivePath),
180
190
  httpsDownload(checksAsset.browser_download_url, checksPath),
181
191
  ]);
182
- // Verify SHA256 against checksums.txt
183
192
  const checks = await fs.readFile(checksPath, 'utf8');
184
193
  const expected = parseChecksum(checks, asset.name);
185
194
  if (!expected) {
@@ -193,7 +202,6 @@ async function updateRtk(ctx, dryRun = false) {
193
202
  return report(ctx, m, 'warning');
194
203
  }
195
204
  notify(ctx, 'RTK: checksum verified.', 'info');
196
- // Extract by archive format
197
205
  const extractDir = path.join(tmp, 'extracted');
198
206
  await fs.mkdir(extractDir, { recursive: true });
199
207
  if (asset.name.endsWith('.zip')) {
@@ -225,7 +233,6 @@ async function updateRtk(ctx, dryRun = false) {
225
233
  const backupPath = `${RTK_BINARY}.bak`;
226
234
  const backedUp = await fs.copyFile(RTK_BINARY, backupPath).then(() => true, () => false);
227
235
  await fs.copyFile(rtkExtracted, RTK_BINARY);
228
- // Set executable bit on Unix
229
236
  if (!IS_WINDOWS) {
230
237
  await fs.chmod(RTK_BINARY, 0o755);
231
238
  }
@@ -293,7 +300,7 @@ async function updateCaveman(ctx, dryRun = false) {
293
300
  }
294
301
  }
295
302
  export default function aiAddonsUpdaterExtension(pi) {
296
- pi.setLabel?.('AI add-ons updater');
303
+ setExtensionLabel(pi, 'AI add-ons updater');
297
304
  pi.registerCommand?.('ai-addons', {
298
305
  description: 'Check or update AI add-ons (ponytail/rtk/caveman/all). Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]',
299
306
  handler: async (args, ctx) => {
@@ -1,23 +1,24 @@
1
- // OMP extension: /ai-addons manual updater for Ponytail, RTK, Caveman.
2
- // Built-in Node modules only. Default off; registers a single slash command.
3
- // ponytail: `skipped: none` — semantics match one-liner: fetch + compare + run install.
4
- // rtk: `skipped: signature verification` — checksums.txt ships only SHA256 of release assets; add sigchain when upstream publishes a signing key.
5
- // caveman: `skipped: none` — exactly the ask: write rule.md, report old/new hash.
1
+ // /ai-addons manual updater. Node built-ins only. RTK ships SHA256 checksums
2
+ // with no signature, so checksum-only is the best available.
6
3
 
7
4
  import { execFileSync } from 'node:child_process';
8
5
  import { createHash } from 'node:crypto';
9
6
  import fs from 'node:fs/promises';
10
7
  import os from 'node:os';
11
8
  import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
12
10
 
11
+ import { setExtensionLabel } from '../shared/host.ts';
13
12
  import {
14
13
  CAVEMAN_REMOTE_RULE as CAVEMAN_REMOTE,
15
14
  RTK_RELEASE_API,
16
15
  RtkRelease,
17
16
  fetchJson,
18
17
  findFile,
18
+ findHoistedPackage,
19
19
  httpsGet,
20
20
  httpsDownload,
21
+ isPiProcess,
21
22
  sha256Hex,
22
23
  parseChecksum,
23
24
  normalizeRtkVersion,
@@ -27,23 +28,20 @@ import {
27
28
  } from '../lib/utils.ts';
28
29
 
29
30
  const IS_WINDOWS = process.platform === 'win32';
30
- const HOME = os.homedir();
31
+ const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
31
32
 
32
33
  const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
33
- const PONYTAIL_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
34
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
- );
46
- const RELOAD_MSG = 'Reminder: restart OMP (or reload extensions) for updates to take effect.';
35
+ // Beside this file, not a host path: that is the rule.md /caveman reads.
36
+ const CAVEMAN_LOCAL = path.join(EXTENSION_DIR, '..', 'caveman-session', 'rule.md');
37
+ const HOST_NAME = isPiProcess() ? 'Pi' : 'OMP';
38
+ const RELOAD_MSG = `Reminder: restart ${HOST_NAME} (or reload extensions) for updates to take effect.`;
39
+
40
+ // Ponytail is a hoisted dependency, so the installed copy is found by walking
41
+ // up rather than by naming a host directory.
42
+ function ponytailLocal(): string | null {
43
+ return findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'package.json');
44
+ }
47
45
 
48
46
  // --- Types ---
49
47
 
@@ -85,17 +83,25 @@ function checkFailed(name: string, e: unknown): AddonStatus {
85
83
  return { text: `${name} check failed: ${(e as Error).message}`, level: 'warning' };
86
84
  }
87
85
 
88
- // Single error-handling source for the three probes; messages unchanged.
89
86
  function runCheck(name: string, probe: () => Promise<string>): Promise<AddonStatus> {
90
87
  return probe().then((text) => ({ text, level: 'info' as const }), (e) => checkFailed(name, e));
91
88
  }
92
89
 
93
- // Check: no mutation. Probes run concurrently via checkAddons below.
90
+ // A missing file is "not installed"; an unreadable one is a reportable fault.
91
+ function parsePackageVersion(raw: string | null): string | null {
92
+ if (!raw) return null;
93
+ try {
94
+ return (JSON.parse(raw) as { version?: string }).version ?? null;
95
+ } catch {
96
+ throw new Error('ponytail package.json is not readable JSON');
97
+ }
98
+ }
99
+
94
100
  function checkPonytail(): Promise<AddonStatus> {
95
101
  return runCheck('Ponytail', async () => {
96
102
  const remoteJson = await fetchJson<{ version?: string }>(PONYTAIL_REMOTE);
97
- const localRaw = await readTextIfExists(PONYTAIL_LOCAL);
98
- const localVer = localRaw ? (JSON.parse(localRaw) as { version?: string }).version ?? null : null;
103
+ const localRaw = await readTextIfExists(ponytailLocal() ?? '');
104
+ const localVer = parsePackageVersion(localRaw);
99
105
  const remoteVer = remoteJson.version;
100
106
  const status = !localVer ? 'not installed'
101
107
  : localVer === remoteVer ? 'up to date'
@@ -175,12 +181,10 @@ async function checkAddons(ctx: AddonUpdaterCtx): Promise<string> {
175
181
  }
176
182
 
177
183
  async function updatePonytail(pi: AddonUpdaterPi, ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
178
- // Bundled with tersio: no separate package to refresh. `tersio update`
179
- // pulls the bundled copy with the CLI.
184
+ // Bundled with tersio: `tersio update` pulls the copy with the CLI.
180
185
  let localVer: string | null = null;
181
186
  try {
182
- const raw = await readTextIfExists(PONYTAIL_LOCAL);
183
- if (raw) localVer = (JSON.parse(raw) as { version?: string }).version ?? null;
187
+ localVer = parsePackageVersion(await readTextIfExists(ponytailLocal() ?? ''));
184
188
  } catch { localVer = null; }
185
189
  void pi;
186
190
  const m = dryRun
@@ -201,7 +205,6 @@ async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string>
201
205
  const assets = Array.isArray(release.assets) ? release.assets : [];
202
206
  if (!RTK_BINARY) return report(ctx, 'RTK: executable not found in PATH', 'warning');
203
207
 
204
- // Cross-platform asset selection (mirrors installer stepRtk)
205
208
  const PLATFORM = process.platform;
206
209
  const ARCH = process.arch;
207
210
  const spec = rtkPlatformSpec(PLATFORM, ARCH);
@@ -234,7 +237,6 @@ async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string>
234
237
  httpsDownload(asset.browser_download_url, archivePath),
235
238
  httpsDownload(checksAsset.browser_download_url, checksPath),
236
239
  ]);
237
- // Verify SHA256 against checksums.txt
238
240
  const checks = await fs.readFile(checksPath, 'utf8');
239
241
  const expected = parseChecksum(checks, asset.name);
240
242
  if (!expected) {
@@ -249,7 +251,6 @@ async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string>
249
251
  }
250
252
  notify(ctx, 'RTK: checksum verified.', 'info');
251
253
 
252
- // Extract by archive format
253
254
  const extractDir = path.join(tmp, 'extracted');
254
255
  await fs.mkdir(extractDir, { recursive: true });
255
256
 
@@ -281,7 +282,6 @@ async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string>
281
282
 
282
283
  await fs.copyFile(rtkExtracted, RTK_BINARY);
283
284
 
284
- // Set executable bit on Unix
285
285
  if (!IS_WINDOWS) {
286
286
  await fs.chmod(RTK_BINARY, 0o755);
287
287
  }
@@ -343,7 +343,7 @@ async function updateCaveman(ctx: AddonUpdaterCtx, dryRun = false): Promise<stri
343
343
  }
344
344
 
345
345
  export default function aiAddonsUpdaterExtension(pi: AddonUpdaterPi): void {
346
- pi.setLabel?.('AI add-ons updater');
346
+ setExtensionLabel(pi, 'AI add-ons updater');
347
347
 
348
348
  pi.registerCommand?.('ai-addons', {
349
349
  description: 'Check or update AI add-ons (ponytail/rtk/caveman/all). Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]',
@@ -1,7 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { activeModesSummary, asPromptArray, 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 } from '../shared/session-state.js';
4
4
  import { dirname, join } from 'node:path';
5
+ import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
5
6
  import { readCavemanDefault } from '../shared/plugin-settings.js';
6
7
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
7
8
  const RULE_PATH = join(CAVERN_DIR, 'rule.md');
@@ -77,7 +78,7 @@ export default function cavemanSessionExtension(pi) {
77
78
  ctx?.ui?.notify?.(msg, 'info');
78
79
  return true;
79
80
  }
80
- pi.setLabel?.('Caveman session toggle');
81
+ setExtensionLabel(pi, 'Caveman session toggle');
81
82
  // Live mirror: a /tersio or /combo switch publishes shared state — adopt
82
83
  // it at once so the next turn injects the new mode with no session reload.
83
84
  // Stable identity, so the bridge set dedupes across re-inits.
@@ -106,7 +107,7 @@ export default function cavemanSessionExtension(pi) {
106
107
  }
107
108
  },
108
109
  });
109
- pi.on('input', async (event) => {
110
+ onHostEvent(pi, 'input', async (event) => {
110
111
  if (event?.source === 'extension')
111
112
  return;
112
113
  if (currentMode !== 'off' && isOffCommand(event?.text))
@@ -129,7 +130,7 @@ export default function cavemanSessionExtension(pi) {
129
130
  restoreMode(ctx);
130
131
  ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
131
132
  });
132
- pi.on('session_branch', async (_event, ctx) => {
133
+ onHostEvent(pi, 'session_branch', async (_event, ctx) => {
133
134
  restoreMode(ctx);
134
135
  });
135
136
  pi.on('session_tree', async (_event, ctx) => {
@@ -151,7 +152,6 @@ export default function cavemanSessionExtension(pi) {
151
152
  if (!def)
152
153
  return;
153
154
  const instruction = typeof def === 'function' ? def() : def;
154
- const base = asPromptArray(event.systemPrompt);
155
- return { systemPrompt: [...base, instruction] };
155
+ return injectPromptText(pi, event, instruction);
156
156
  });
157
157
  }
@@ -1,7 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { activeModesSummary, asPromptArray, 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 } from '../shared/session-state.ts';
4
4
  import { dirname, join } from 'node:path';
5
+ import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
5
6
  import { readCavemanDefault } from '../shared/plugin-settings.ts';
6
7
  import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
7
8
 
@@ -81,7 +82,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
81
82
  return true;
82
83
  }
83
84
 
84
- pi.setLabel?.('Caveman session toggle');
85
+ setExtensionLabel(pi, 'Caveman session toggle');
85
86
 
86
87
  // Live mirror: a /tersio or /combo switch publishes shared state — adopt
87
88
  // it at once so the next turn injects the new mode with no session reload.
@@ -112,7 +113,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
112
113
  },
113
114
  });
114
115
 
115
- pi.on<InputEvent>('input', async (event) => {
116
+ onHostEvent<InputEvent>(pi, 'input', async (event) => {
116
117
  if (event?.source === 'extension') return;
117
118
  if (currentMode !== 'off' && isOffCommand(event?.text)) setMode('off');
118
119
  });
@@ -136,7 +137,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
136
137
  ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
137
138
  });
138
139
 
139
- pi.on('session_branch', async (_event, ctx) => {
140
+ onHostEvent(pi, 'session_branch', async (_event, ctx) => {
140
141
  restoreMode(ctx);
141
142
  });
142
143
 
@@ -160,7 +161,6 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
160
161
  const def = INSTRUCTIONS[mode];
161
162
  if (!def) return;
162
163
  const instruction = typeof def === 'function' ? def() : def;
163
- const base = asPromptArray(event.systemPrompt);
164
- return { systemPrompt: [...base, instruction] };
164
+ return injectPromptText(pi, event, instruction);
165
165
  });
166
166
  }
@@ -1,33 +1,47 @@
1
- // /combo session toggle — set all three (caveman, rtk, ponytail) at once.
2
- // Modes: off | medium | balanced | max
3
- import os from 'node:os';
1
+ // /combo session toggle: off | medium | balanced | max.
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
4
10
  import path from 'node:path';
5
- import { createRequire } from 'node:module';
6
- import { activeModesSummary, asPromptArray, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, systemPromptIncludes, } from '../shared/session-state.js';
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';
13
+ import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
7
14
  import { isComboSetupComplete, readComboDefault, readPonytailDefault, saveComboSetup, } from '../shared/plugin-settings.js';
8
- const require = createRequire(import.meta.url);
15
+ import { findHoistedPackage } from '../lib/utils.js';
16
+ const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
9
17
  const PONYTAIL_FALLBACK_INTENSITY = {
10
18
  lite: 'Prefer the simplest correct solution.',
11
19
  };
12
- function ponytailFallback(mode) {
20
+ // Fallback text when Ponytail cannot be found; exported for its own test.
21
+ export function ponytailFallback(mode) {
13
22
  const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
14
23
  return `🦥 PONYTAIL MODE ACTIVE — level: ${mode}\n${intensity} Understand the path first and fix root causes, not symptoms. Prefer the standard library and YAGNI. Avoid speculative abstractions and dependencies. Preserve correctness. Verify changed behavior.`;
15
24
  }
16
25
  function levelSummary(state) {
17
26
  return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
18
27
  }
19
- function loadPonytailInstructions(mode) {
20
- try {
21
- const installed = path.join(os.homedir(), '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'hooks', 'ponytail-instructions.js');
22
- const { getPonytailInstructions } = require(installed);
23
- if (typeof getPonytailInstructions === 'function')
24
- return getPonytailInstructions(mode);
28
+ // Ponytail is hoisted, so one upward walk covers every install layout. The hook
29
+ // is CommonJS, so both namespace shapes are tried.
30
+ async function loadPonytailInstructions(mode) {
31
+ const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
32
+ if (installed) {
33
+ try {
34
+ const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(installed).href));
35
+ const build = loaded.getPonytailInstructions ?? loaded.default?.getPonytailInstructions;
36
+ if (build)
37
+ return build(mode);
38
+ }
39
+ catch { /* fall through to the built-in text */ }
25
40
  }
26
- catch { }
27
41
  return ponytailFallback(mode);
28
42
  }
29
43
  export default function comboToggleExtension(pi) {
30
- pi.setLabel?.('Combo session toggle (all 3 add-ons)');
44
+ setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
31
45
  let lastCtx = undefined;
32
46
  let setupPrompted = false;
33
47
  function syncStatus(ctx) {
@@ -35,31 +49,24 @@ export default function comboToggleExtension(pi) {
35
49
  const c = lastCtx;
36
50
  if (!c?.ui?.setStatus)
37
51
  return;
38
- // Single unified bar replaces the three per-extension bars while a preset is
39
- // active. In custom/off, the individual bars come back and combo stays clear.
52
+ // One unified bar replaces the per-extension bars while a preset is active.
40
53
  if (!isComboPresetActive()) {
41
54
  c.ui.setStatus('combo', undefined);
42
55
  return;
43
56
  }
44
- // Read the live bridge, not cached extension state: sibling extensions
45
- // reconcile it from persisted entries before this one runs.
57
+ // Read the live bridge, not a cache: siblings reconcile it first.
46
58
  const state = getSharedComboState();
47
- const theme = c.ui.theme;
48
59
  const c0 = state.caveman.toUpperCase();
49
60
  const r = state.rtk.toUpperCase();
50
61
  const p = state.ponytail.toUpperCase();
51
62
  const lvl = state.level.toUpperCase();
52
- const label = `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`;
53
- c.ui.setStatus('combo', theme?.fg ? `${theme.fg('accent', '🧩')} ${theme.fg('muted', label)}` : `🧩 ${label}`);
54
- // Clobber any sibling bars another extension may have painted in a race
55
- // during session_start — our bar is canonical for the duration of the preset.
63
+ c.ui.setStatus('combo', themeStatus(c.ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
64
+ // Our bar is canonical for the preset, so clear any sibling bar.
56
65
  c.ui.setStatus('caveman', undefined);
57
66
  c.ui.setStatus('rtk', undefined);
58
67
  c.ui.setStatus('ponytail', undefined);
59
68
  }
60
- // ponytail: same persistence as /combo — siblings (incl. upstream ponytail)
61
- // restore from these entries, so the fallback must write them too or the
62
- // preset evaporates on resume and ponytail never activates.
69
+ // Siblings restore from these entries, so the fallback must write them too.
63
70
  function persistPreset(level) {
64
71
  const modes = COMBO_LEVELS[level];
65
72
  pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
@@ -77,8 +84,7 @@ export default function comboToggleExtension(pi) {
77
84
  return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
78
85
  }
79
86
  function listen(ctx) {
80
- // Stable identity: the bridge set dedupes, so repeated track()/command
81
- // calls register once instead of stacking duplicate listeners.
87
+ // Stable identity: the bridge set dedupes, so repeat calls register once.
82
88
  if (ctx?.hasUI)
83
89
  setSharedComboListener(useState);
84
90
  }
@@ -119,7 +125,7 @@ export default function comboToggleExtension(pi) {
119
125
  if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete())
120
126
  return;
121
127
  setupPrompted = true;
122
- const choice = await ctx.ui.select('Session-start defaults — Combo preset', [
128
+ const choice = await hostSelect(pi, ctx.ui, 'Session-start defaults — Combo preset', [
123
129
  { label: 'off', description: 'Keep every Tersio mode inactive' },
124
130
  { label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
125
131
  { label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
@@ -137,9 +143,7 @@ export default function comboToggleExtension(pi) {
137
143
  if (!ctx?.hasUI)
138
144
  syncStatus(ctx);
139
145
  await runFirstRunSetup(ctx);
140
- // Installer/user-configured default applies only when no persisted *mode*
141
- // state exists — unrelated session entries must not block it, or the
142
- // combo bar never paints on sessions that already carry other entries.
146
+ // The configured default applies only when no persisted mode state exists.
143
147
  const entries = sessionEntries(ctx);
144
148
  const hasModeState = entries.some((e) => e?.type === 'custom' && (e.customType === 'combo-level' || e.customType === 'caveman-mode' ||
145
149
  e.customType === 'rtk-mode' || e.customType === 'ponytail-mode'));
@@ -151,10 +155,8 @@ export default function comboToggleExtension(pi) {
151
155
  ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
152
156
  }
153
157
  }
154
- // Standalone ponytail default: no sibling extension restores it (upstream
155
- // owns the command), so apply it here when nothing persisted a
156
- // ponytail-mode entry. Skipped when it matches the active preset — the
157
- // preset entry already covers that, no redundant write.
158
+ // No sibling restores a standalone ponytail default, so apply it here; skip
159
+ // it when the active preset already writes the same mode.
158
160
  if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
159
161
  const ponytailFallback = readPonytailDefault();
160
162
  if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
@@ -164,7 +166,7 @@ export default function comboToggleExtension(pi) {
164
166
  }
165
167
  });
166
168
  for (const event of ['session_branch', 'session_tree', 'agent_start']) {
167
- pi.on(event, async (_event, ctx) => {
169
+ onHostEvent(pi, event, async (_event, ctx) => {
168
170
  track(ctx);
169
171
  if (event === 'agent_start')
170
172
  await runFirstRunSetup(ctx);
@@ -178,8 +180,8 @@ export default function comboToggleExtension(pi) {
178
180
  const mode = getSharedComboState().ponytail;
179
181
  if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE'))
180
182
  return;
181
- const base = asPromptArray(event.systemPrompt);
182
- return { systemPrompt: [...base, loadPonytailInstructions(mode)] };
183
+ const instructions = await loadPonytailInstructions(mode);
184
+ return injectPromptText(pi, event, instructions);
183
185
  });
184
186
  // Slash commands only; natural-language input caused accidental toggles and has no reload context.
185
187
  }