@krtclcdy/tersio 2.25.1 → 2.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dashboard/dist/index.html +1 -1
  3. package/{cli → dist/cli}/common.js +4 -3
  4. package/{cli → dist/cli}/dashboard.js +30 -13
  5. package/{cli → dist/cli}/doctor-fix.js +19 -30
  6. package/{cli → dist/cli}/doctor.js +19 -6
  7. package/dist/cli/headroom-install.js +266 -0
  8. package/dist/cli/headroom.js +492 -0
  9. package/{cli → dist/cli}/install.js +116 -71
  10. package/{cli → dist/cli}/interactive.js +2 -2
  11. package/{cli → dist/cli}/manifest.js +4 -1
  12. package/{cli → dist/cli}/rtk-wiring.js +5 -2
  13. package/{cli → dist/cli}/settings.js +5 -23
  14. package/{cli → dist/cli}/uninstall.js +23 -11
  15. package/{cli → dist/cli}/update.js +10 -8
  16. package/{extensions → dist/extensions}/ai-addons-updater/index.js +60 -44
  17. package/{extensions → dist/extensions}/caveman-session/index.js +11 -11
  18. package/dist/extensions/caveman-session/rule-megacave.md +63 -0
  19. package/dist/extensions/caveman-session/rule-ultra.md +63 -0
  20. package/dist/extensions/caveman-session/rule.md +79 -0
  21. package/{extensions → dist/extensions}/combo-toggle/index.js +37 -27
  22. package/dist/extensions/headroom-session/index.js +93 -0
  23. package/{extensions → dist/extensions}/lib/utils.js +27 -1
  24. package/{extensions → dist/extensions}/opencode/server.js +33 -3
  25. package/dist/extensions/shared/headroom.js +131 -0
  26. package/{extensions → dist/extensions}/shared/plugin-settings.js +1 -1
  27. package/{extensions → dist/extensions}/shared/session-state.js +14 -2
  28. package/{extensions → dist/extensions}/shared/status.js +7 -1
  29. package/{tersio.js → dist/tersio.js} +1 -1
  30. package/extensions/ai-addons-updater/index.ts +52 -40
  31. package/extensions/caveman-session/index.ts +10 -11
  32. package/extensions/caveman-session/rule-megacave.md +63 -0
  33. package/extensions/caveman-session/rule-ultra.md +63 -0
  34. package/extensions/caveman-session/rule.md +4 -15
  35. package/extensions/combo-toggle/index.ts +35 -27
  36. package/extensions/lib/utils.ts +23 -1
  37. package/extensions/opencode/server.ts +29 -3
  38. package/extensions/shared/plugin-settings.ts +1 -1
  39. package/extensions/shared/session-state.ts +15 -2
  40. package/extensions/shared/status.ts +8 -1
  41. package/package.json +17 -15
  42. package/extensions/opencode/index.js +0 -104
  43. /package/{cli → dist/cli}/banner.js +0 -0
  44. /package/{cli → dist/cli}/currency.js +0 -0
  45. /package/{cli → dist/cli}/hosts.js +0 -0
  46. /package/{cli → dist/cli}/profile.js +0 -0
  47. /package/{cli → dist/cli}/reset.js +0 -0
  48. /package/{cli → dist/cli}/usage.js +0 -0
  49. /package/{extensions → dist/extensions}/rtk-session/index.js +0 -0
  50. /package/{extensions → dist/extensions}/shared/carbon.js +0 -0
  51. /package/{extensions → dist/extensions}/shared/host.js +0 -0
  52. /package/{extensions → dist/extensions}/shared/pricing.js +0 -0
  53. /package/{extensions → dist/extensions}/shared/rtk-gain.js +0 -0
  54. /package/{extensions → dist/extensions}/shared/types.js +0 -0
  55. /package/{extensions → dist/extensions}/shared/usage-ledger.js +0 -0
  56. /package/{extensions → dist/extensions}/shared/usage-store.js +0 -0
  57. /package/{extensions → dist/extensions}/tersio-commands/index.js +0 -0
@@ -13,15 +13,18 @@ import {
13
13
  sessionEntries,
14
14
  setSharedComboLevel,
15
15
  setSharedComboListener,
16
- setSharedComboMode,
16
+ setSharedComboModes,
17
17
  systemPromptIncludes,
18
18
  } from '../shared/session-state.ts';
19
19
  import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
20
20
  import { announceStatus } from '../shared/status.ts';
21
21
  import {
22
22
  isComboSetupComplete,
23
+ readCavemanDefault,
23
24
  readComboDefault,
25
+ readPluginSettings,
24
26
  readPonytailDefault,
27
+ readRtkDefault,
25
28
  saveComboSetup,
26
29
  } from '../shared/plugin-settings.ts';
27
30
  import { findHoistedPackage, isPiProcess } from '../lib/utils.ts';
@@ -64,7 +67,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
64
67
 
65
68
  let setupPrompted = false;
66
69
  let lastInjected: string | undefined = undefined;
67
-
70
+ // opencode has no UI event stream, so only its empty branch reconciles.
71
+ const OPENCODE = pi.hostId === 'opencode';
68
72
 
69
73
  // Siblings restore from these entries, so the fallback must write them too.
70
74
  function persistPreset(level: string): void {
@@ -74,19 +78,38 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
74
78
  pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
75
79
  pi.appendEntry?.('combo-level', { level });
76
80
  }
81
+ function restoreConfiguredDefaults(): void {
82
+ const settings = readPluginSettings();
83
+ const hasModeDefaults = ['cavemanDefault', 'rtkDefault', 'ponytailDefault'].some((key) => key in settings);
84
+ const combo = readComboDefault();
85
+ const preset = COMBO_LEVELS[combo];
86
+ const modes = hasModeDefaults
87
+ ? { caveman: readCavemanDefault(), rtk: readRtkDefault() ? 'on' : 'off', ponytail: readPonytailDefault() }
88
+ : preset;
89
+ setSharedComboModes(modes);
90
+ if (modes.caveman === 'off' && modes.rtk === 'off' && modes.ponytail === 'off') return;
91
+ pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
92
+ pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
93
+ pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
94
+ if (preset && modes.caveman === preset.caveman && modes.rtk === preset.rtk && modes.ponytail === preset.ponytail) {
95
+ pi.appendEntry?.('combo-level', { level: combo });
96
+ }
97
+ }
98
+
77
99
 
78
- function reconcile(ctx?: ExtensionCtx): Readonly<ComboState> {
79
- if (!ctx?.hasUI) return getSharedComboState();
80
- return reconcileSharedComboEntries(sessionEntries(ctx));
100
+ function reconcile(ctx?: ExtensionCtx, resetEmpty = false): Readonly<ComboState> {
101
+ if (!ctx?.hasUI && !(resetEmpty && OPENCODE)) return getSharedComboState();
102
+ const entries = sessionEntries(ctx);
103
+ if (!resetEmpty && entries.length === 0) return getSharedComboState();
104
+ return reconcileSharedComboEntries(entries);
81
105
  }
82
106
  function listen(ctx?: ExtensionCtx): void {
83
107
  if (!ctx?.hasUI) return;
84
108
  setSharedComboListener('combo', (state) => { announceStatus(ctx); return state; });
85
109
  }
86
-
87
- function track(ctx?: ExtensionCtx): void {
110
+ function track(ctx?: ExtensionCtx, resetEmpty = false): void {
88
111
  listen(ctx);
89
- if (ctx?.hasUI) reconcile(ctx);
112
+ if (ctx?.hasUI || (resetEmpty && OPENCODE)) reconcile(ctx, resetEmpty);
90
113
  }
91
114
 
92
115
  pi.registerCommand?.('combo', {
@@ -145,26 +168,11 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
145
168
  }
146
169
 
147
170
  pi.on('session_start', async (_event, ctx) => {
148
- track(ctx);
149
- await runFirstRunSetup(ctx);
150
- // The configured default applies only when no persisted mode state exists.
171
+ track(ctx, true);
172
+ if (!isComboSetupComplete() && ctx?.hasUI && ctx.ui?.select) await runFirstRunSetup(ctx);
173
+ // The configured defaults apply when a fresh session has no persisted mode state.
151
174
  const entries = sessionEntries(ctx);
152
- const persisted = hasModeState(entries);
153
- if (getSharedComboState().level === 'off' && !persisted) {
154
- const fallback = readComboDefault();
155
- if (fallback !== 'off') {
156
- persistPreset(fallback);
157
- setSharedComboLevel(fallback);
158
- }
159
- }
160
- // No sibling restores a standalone ponytail default, so apply it here; skip it when the active preset already writes the same mode.
161
- if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
162
- const ponytailFallback = readPonytailDefault();
163
- if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
164
- pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
165
- setSharedComboMode('ponytail', ponytailFallback);
166
- }
167
- }
175
+ if (!hasModeState(entries) && (ctx?.hasUI || OPENCODE)) restoreConfiguredDefaults();
168
176
  // Announced last, so any default above is reflected.
169
177
  announceStatus(ctx);
170
178
  });
@@ -27,7 +27,18 @@ export function homeDir(): string {
27
27
  }
28
28
 
29
29
  export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
30
- export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/caveman/SKILL.md';
30
+ export const CAVEMAN_REMOTE_ULTRA = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/ultracave/SKILL.md';
31
+ export const CAVEMAN_REMOTE_MEGACAVE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/megacave/SKILL.md';
32
+
33
+ // A fetch that returns an error page or a truncated body must not reach the extension tree.
34
+ export function ruleBodyProblem(text: string): string | null {
35
+ const trimmed = text.trim();
36
+ if (trimmed === '') return 'empty body';
37
+ if (/^\s*<(!doctype|html)/i.test(trimmed)) return 'HTML page, not markdown';
38
+ if (/^\s*404\b/im.test(trimmed)) return 'upstream 404 page';
39
+ if (trimmed.length < 800) return `truncated (${trimmed.length} chars)`;
40
+ return null;
41
+ }
31
42
 
32
43
  export interface RtkPlatformSpec {
33
44
  triple: string;
@@ -82,6 +93,17 @@ export function rtkPlatformSpec(platform: string = process.platform, arch: strin
82
93
  return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
83
94
  }
84
95
 
96
+ // Walk up to the nearest package.json; identical for the source and compiled trees.
97
+ export function findPackageRoot(fromDir: string): string {
98
+ let dir = fromDir;
99
+ for (;;) {
100
+ if (existsSync(path.join(dir, 'package.json'))) return dir;
101
+ const parent = path.dirname(dir);
102
+ if (parent === dir) throw new Error('package root not found');
103
+ dir = parent;
104
+ }
105
+ }
106
+
85
107
  // The 3xx+location redirect rule shared by httpsGet/httpsDownload.
86
108
  function redirectNext(res: { statusCode?: number; headers: { location?: string } }, url: string): string | null {
87
109
  if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
@@ -7,6 +7,7 @@ import comboToggleExtension from '../combo-toggle/index.ts';
7
7
  import tersioCommandsExtension from '../tersio-commands/index.ts';
8
8
  import { getSharedComboState } from '../shared/session-state.ts';
9
9
  import { formatStatus } from '../shared/status.ts';
10
+ import { resolveRtkBinary } from '../lib/utils.ts';
10
11
  import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
11
12
 
12
13
  interface CommandConfig {
@@ -27,10 +28,9 @@ interface ToolConfig {
27
28
  ) => Promise<{ isError: boolean; content: { type: string; text: string }[]; details: Record<string, unknown> }>;
28
29
  }
29
30
 
30
- // Plain object, not Plugin.define: define is an identity wrapper, and a value
31
- // import would force every install to vendor the whole @opencode dependency tree.
31
+ // The npm name, so the plugin list reads the same as the OMP and pi installs.
32
32
  const plugin: Plugin.Plugin = {
33
- id: 'tersio',
33
+ id: '@krtclcdy/tersio',
34
34
  async setup(ctx) {
35
35
  // Entries are scoped per session; an unseen ID restores on its first hook.
36
36
  const sessions = new Map<string, SessionEntry[]>();
@@ -40,7 +40,9 @@ const plugin: Plugin.Plugin = {
40
40
  const own = ((await ctx.storage.get(`entries:${sid}`)) as SessionEntry[] | undefined) ?? [];
41
41
  if (own.length) return own;
42
42
  // Pre-scoped installs kept one global list; adopt it once, then diverge.
43
+ // Clear it after adoption so it can never shadow saved defaults later.
43
44
  const legacy = ((await ctx.storage.get('entries')) as SessionEntry[] | undefined) ?? [];
45
+ if (legacy.length) void ctx.storage.set('entries', []).catch(() => {});
44
46
  return [...legacy];
45
47
  };
46
48
  const sessionEntriesOf = (sid: string): SessionEntry[] => {
@@ -194,7 +196,31 @@ const plugin: Plugin.Plugin = {
194
196
  if (sid) await ensureSession(sid);
195
197
  for (const handler of inputHandlers) void handler({ text: event.prompt.text, source: 'interactive' });
196
198
  });
199
+
200
+ // rtk has its own OpenCode plugin; doing this here keeps one entry in the plugin list.
201
+ await ctx.tool.hook('execute.before', async (event) => {
202
+ const tool = String(event.tool ?? '').toLowerCase();
203
+ if (tool !== 'bash' && tool !== 'shell') return;
204
+ const input = event.input as { command?: unknown } | null | undefined;
205
+ if (typeof input?.command !== 'string' || !input.command) return;
206
+ const rewritten = await rewriteCommand(input.command);
207
+ if (rewritten) input.command = rewritten;
208
+ });
197
209
  },
198
210
  };
199
211
 
212
+ function rewriteCommand(command: string): Promise<string | null> {
213
+ const bin = resolveRtkBinary();
214
+ if (!bin) return Promise.resolve(null);
215
+ return new Promise((resolve) => {
216
+ execFile(bin, ['rewrite', command], { timeout: 5000 }, (error, stdout) => {
217
+ // A kill or timeout means untrustworthy output; pass the command through.
218
+ const err = error as { killed?: boolean; signal?: unknown } | null;
219
+ if (err && (err.killed || err.signal)) return resolve(null);
220
+ const out = String(stdout).trim();
221
+ resolve(out && out !== command ? out : null);
222
+ });
223
+ });
224
+ }
225
+
200
226
  export default plugin;
@@ -45,7 +45,7 @@ export function readPluginSettings(): Record<string, unknown> {
45
45
  }
46
46
 
47
47
  const COMBO_LEVELS = new Set(['off', 'medium', 'balanced', 'max']);
48
- const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
48
+ const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'megacave', 'megacave-lite', 'megacave-full', 'megacave-ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
49
49
  const PONYTAIL_MODES = new Set(['off', 'lite', 'full', 'ultra']);
50
50
 
51
51
  function readStringDefault(key: string, valid: Set<string>): string {
@@ -14,11 +14,14 @@ export const COMBO_LEVELS: Record<string, Readonly<ComboState>> = Object.freeze(
14
14
  });
15
15
 
16
16
  const MODE_VALUES: Record<string, Set<string>> = {
17
- caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']),
17
+ caveman: new Set(['off', 'lite', 'full', 'ultra', 'megacave', 'megacave-lite', 'megacave-full', 'megacave-ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']),
18
18
  rtk: new Set(['off', 'on']),
19
19
  ponytail: new Set(['off', 'lite', 'full', 'ultra']),
20
20
  };
21
21
 
22
+ // Upstream renamed wenyan to megacave and kept the old spelling as an alias.
23
+ const CAVEMAN_LEGACY_PREFIX = /^wenyan/;
24
+
22
25
  type ModeName = 'caveman' | 'rtk' | 'ponytail';
23
26
  type Modes = Record<ModeName, string>;
24
27
 
@@ -43,7 +46,9 @@ export function normalizeMode(name: ModeName, value: unknown): string | null {
43
46
  if (name === 'rtk' && typeof value === 'boolean') return value ? 'on' : 'off';
44
47
  const mode = String(value ?? '').trim().toLowerCase();
45
48
  if (!MODE_VALUES[name]?.has(mode)) return null;
46
- return name === 'caveman' && mode === 'wenyan' ? 'wenyan-full' : mode;
49
+ if (name !== 'caveman') return mode;
50
+ const canonical = mode.replace(CAVEMAN_LEGACY_PREFIX, 'megacave');
51
+ return canonical === 'megacave' ? 'megacave-full' : canonical;
47
52
  }
48
53
 
49
54
  export function deriveLevel(modes: Modes | null | undefined): ComboLevel {
@@ -153,6 +158,14 @@ export function setSharedComboMode(name: ModeName, value: unknown): Readonly<Com
153
158
  return publish(normalizedState(modes));
154
159
  }
155
160
 
161
+ export function setSharedComboModes(modes: { caveman?: unknown; rtk?: unknown; ponytail?: unknown }): Readonly<ComboState> {
162
+ return publish(normalizedState({
163
+ caveman: normalizeMode('caveman', modes.caveman) || 'off',
164
+ rtk: normalizeMode('rtk', modes.rtk) || 'off',
165
+ ponytail: normalizeMode('ponytail', modes.ponytail) || 'off',
166
+ }));
167
+ }
168
+
156
169
  export function reconcileSharedComboEntries(entries: SessionEntry[] | null | undefined): Readonly<ComboState> {
157
170
  let modes: Modes = { ...COMBO_LEVELS.off };
158
171
  if (Array.isArray(entries)) {
@@ -17,6 +17,11 @@ export function formatStatus(state: StatusState): string {
17
17
  return `🧩 combo ${label}: 🪨caveman=${caveman.toUpperCase()} ⚡rtk=${rtk.toUpperCase()} 🦥ponytail=${ponytail.toUpperCase()}`;
18
18
  }
19
19
 
20
+ // The string upstream's skill tells the model to relay for `/caveman status`.
21
+ export function formatCavemanStatus(mode: string): string {
22
+ return `Caveman mode: ${mode && mode !== 'off' ? mode : 'unknown'}`;
23
+ }
24
+
20
25
  // Stale ctx access throws, so never let it abort a mode restore.
21
26
  function statusUi(ctx: ExtensionCtx | undefined): UiApi | undefined {
22
27
  try {
@@ -34,7 +39,8 @@ let lastUi: UiApi | undefined;
34
39
 
35
40
  // Paint the host footer bar; same text on the same UI is skipped, a new ctx repaints it.
36
41
  export function announceStatus(ctx: ExtensionCtx | undefined): void {
37
- const text = formatStatus(getSharedComboState());
42
+ const state = getSharedComboState();
43
+ const text = formatStatus(state);
38
44
  const ui = statusUi(ctx) ?? statusUi(remembered);
39
45
  if (statusUi(ctx)) remembered = ctx;
40
46
  if (!ui) return;
@@ -42,4 +48,5 @@ export function announceStatus(ctx: ExtensionCtx | undefined): void {
42
48
  lastText = text;
43
49
  lastUi = ui;
44
50
  ui.setStatus?.('tersio', text);
51
+ ui.setStatus?.('caveman', formatCavemanStatus(state.caveman));
45
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krtclcdy/tersio",
3
- "version": "2.25.1",
3
+ "version": "2.25.3",
4
4
  "description": "✂️ Token saver and usage tracker for coding agents: terse Caveman replies, compact RTK shell output, lean Ponytail code calls, and a gain dashboard — one-command combo presets over one shared extension tree.",
5
5
  "keywords": [
6
6
  "token-saving",
@@ -21,10 +21,10 @@
21
21
  },
22
22
  "type": "module",
23
23
  "bin": {
24
- "tersio": "tersio.js"
24
+ "tersio": "dist/tersio.js"
25
25
  },
26
26
  "exports": {
27
- "./server": "./extensions/opencode/server.js"
27
+ "./server": "./dist/extensions/opencode/server.js"
28
28
  },
29
29
  "omp": {
30
30
  "name": "Tersio",
@@ -72,9 +72,9 @@
72
72
  "lite",
73
73
  "full",
74
74
  "ultra",
75
- "wenyan-lite",
76
- "wenyan-full",
77
- "wenyan-ultra"
75
+ "megacave-lite",
76
+ "megacave-full",
77
+ "megacave-ultra"
78
78
  ],
79
79
  "default": "off"
80
80
  },
@@ -114,22 +114,23 @@
114
114
  }
115
115
  },
116
116
  "files": [
117
- "tersio.js",
118
- "cli/**/*.js",
117
+ "dist/**/*.js",
118
+ "dist/extensions/caveman-session/*.md",
119
119
  "dashboard/dist/**/*",
120
- "extensions/**/*.js",
121
120
  "extensions/**/*.ts",
122
- "extensions/caveman-session/rule.md"
121
+ "extensions/caveman-session/rule.md",
122
+ "extensions/caveman-session/rule-ultra.md",
123
+ "extensions/caveman-session/rule-megacave.md"
123
124
  ],
124
125
  "scripts": {
125
- "clean": "rm -f tersio.js cli/*.js extensions/**/*.js && rm -rf dashboard/dist",
126
- "build": "tsc && bun run build:dashboard",
126
+ "clean": "rm -rf dist dashboard/dist",
127
+ "build": "tsc && bun run build:dashboard && cp extensions/caveman-session/*.md dist/extensions/caveman-session/",
127
128
  "build:dashboard": "bun install --cwd dashboard/app --frozen-lockfile && bun run --cwd dashboard/app build",
128
129
  "prepublishOnly": "bun run build",
129
130
  "check": "tsc --noEmit",
130
131
  "verify": "bun install --frozen-lockfile && bun run build && bun run test",
131
- "doctor": "node tersio.js --doctor",
132
- "dry-run": "node tersio.js --dry-run --scope user --yes",
132
+ "doctor": "node dist/tersio.js --doctor",
133
+ "dry-run": "node dist/tersio.js --dry-run --scope user --yes",
133
134
  "lint": "oxlint cli extensions test tersio.ts",
134
135
  "lint:dashboard": "bun run --cwd dashboard/app lint",
135
136
  "lint:fix": "oxlint cli extensions test tersio.ts --fix",
@@ -141,6 +142,7 @@
141
142
  "devDependencies": {
142
143
  "@opencode/plugin": "^2.0.22",
143
144
  "@types/node": "^26.4.1",
145
+ "happy-dom": "^20.14.5",
144
146
  "oxlint": "^1.85.0",
145
147
  "typescript": "^7.0.2",
146
148
  "vitest": "^5.0.1"
@@ -148,6 +150,6 @@
148
150
  "dependencies": {
149
151
  "@clack/core": "^1.5.1",
150
152
  "@clack/prompts": "^1.8.0",
151
- "@dietrichgebert/ponytail": "^4.10.0"
153
+ "@dietrichgebert/ponytail": "^4.12.0"
152
154
  }
153
155
  }
@@ -1,104 +0,0 @@
1
- // OpenCode plugin entry: maps its plugin API onto the shared ExtensionApi the mode extensions consume.
2
- import { execFile } from 'node:child_process';
3
- import { Plugin } from '@opencode/plugin';
4
- import cavemanSessionExtension from '../caveman-session/index.js';
5
- import rtkSessionExtension from '../rtk-session/index.js';
6
- import comboToggleExtension from '../combo-toggle/index.js';
7
- import tersioCommandsExtension from '../tersio-commands/index.js';
8
- export default Plugin.define({
9
- id: 'tersio',
10
- async setup(ctx) {
11
- const entries = (await ctx.storage.get('entries')) ?? [];
12
- const persist = () => { void ctx.storage.set('entries', entries).catch(() => { }); };
13
- const stubCtx = () => ({
14
- hasUI: false,
15
- cwd: ctx.location.directory,
16
- sessionManager: { getBranch: () => entries, getEntries: () => entries },
17
- });
18
- const commands = new Map();
19
- const tools = new Map();
20
- const beforeStart = [];
21
- const inputHandlers = [];
22
- const lifecycle = new Map();
23
- const host = {
24
- hostId: 'opencode',
25
- setLabel() { },
26
- registerCommand(name, config) { commands.set(name, config); },
27
- registerTool(tool) { tools.set(tool.name, tool); },
28
- appendEntry(type, data) { entries.push({ type: 'custom', customType: type, data }); persist(); },
29
- exec: (cmd, args, opts) => new Promise((resolve) => {
30
- execFile(cmd, args, { signal: opts?.signal, cwd: opts?.cwd }, (error, stdout, stderr) => {
31
- const code = error && typeof error.code === 'number' ? error.code : 0;
32
- resolve({ stdout: stdout ?? '', stderr: stderr ?? '', code, killed: !!error?.killed });
33
- });
34
- }),
35
- cwd: ctx.location.directory,
36
- on: (event, handler) => {
37
- if (event === 'before_agent_start') {
38
- beforeStart.push(handler);
39
- return;
40
- }
41
- if (event === 'input') {
42
- inputHandlers.push(handler);
43
- return;
44
- }
45
- const list = lifecycle.get(event) ?? [];
46
- list.push(handler);
47
- lifecycle.set(event, list);
48
- },
49
- };
50
- for (const extension of [cavemanSessionExtension, rtkSessionExtension, comboToggleExtension, tersioCommandsExtension])
51
- extension(host);
52
- // OpenCode has no session_start event surfaced to plugins, so restore once at setup.
53
- for (const handler of lifecycle.get('session_start') ?? [])
54
- await handler({}, stubCtx());
55
- await ctx.command.transform((editor) => {
56
- for (const [name, config] of commands) {
57
- editor.add({
58
- name,
59
- description: config.description,
60
- execute: async (input) => {
61
- const text = typeof input.prompt?.text === 'string' ? input.prompt.text : '';
62
- const args = text.includes(' ') ? text.slice(text.indexOf(' ') + 1).trim() : '';
63
- await config.handler(args, stubCtx());
64
- },
65
- });
66
- }
67
- });
68
- await ctx.tool.transform((editor) => {
69
- for (const tool of tools.values()) {
70
- editor.add({
71
- name: tool.name,
72
- description: tool.description,
73
- input: tool.parameters,
74
- execute: async (input) => {
75
- const args = Array.isArray(input.args) ? (input.args) : [];
76
- const result = await tool.execute('t', { args }, undefined, undefined, stubCtx());
77
- return { content: result.content.map((part) => part.text).join('\n') };
78
- },
79
- });
80
- }
81
- });
82
- await ctx.session.hook('context', async (event) => {
83
- const systemParts = event.system.map((part) => (part.type === 'text' ? part.text : '')).filter(Boolean);
84
- for (const handler of beforeStart) {
85
- const result = await handler({ systemPrompt: [...systemParts] }, stubCtx());
86
- if (!result || typeof result !== 'object' || !('systemPrompt' in result))
87
- continue;
88
- const parts = typeof result.systemPrompt === 'string'
89
- ? [result.systemPrompt]
90
- : Array.isArray(result.systemPrompt)
91
- ? result.systemPrompt
92
- : [];
93
- for (const part of parts) {
94
- if (!systemParts.includes(part))
95
- event.system.push({ type: 'text', text: part });
96
- }
97
- }
98
- });
99
- await ctx.session.hook('prompt', (event) => {
100
- for (const handler of inputHandlers)
101
- void handler({ text: event.prompt.text, source: 'interactive' });
102
- });
103
- },
104
- });
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes