@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.
- package/README.md +1 -1
- package/dashboard/dist/index.html +1 -1
- package/{cli → dist/cli}/common.js +4 -3
- package/{cli → dist/cli}/dashboard.js +30 -13
- package/{cli → dist/cli}/doctor-fix.js +19 -30
- package/{cli → dist/cli}/doctor.js +19 -6
- package/dist/cli/headroom-install.js +266 -0
- package/dist/cli/headroom.js +492 -0
- package/{cli → dist/cli}/install.js +116 -71
- package/{cli → dist/cli}/interactive.js +2 -2
- package/{cli → dist/cli}/manifest.js +4 -1
- package/{cli → dist/cli}/rtk-wiring.js +5 -2
- package/{cli → dist/cli}/settings.js +5 -23
- package/{cli → dist/cli}/uninstall.js +23 -11
- package/{cli → dist/cli}/update.js +10 -8
- package/{extensions → dist/extensions}/ai-addons-updater/index.js +60 -44
- package/{extensions → dist/extensions}/caveman-session/index.js +11 -11
- package/dist/extensions/caveman-session/rule-megacave.md +63 -0
- package/dist/extensions/caveman-session/rule-ultra.md +63 -0
- package/dist/extensions/caveman-session/rule.md +79 -0
- package/{extensions → dist/extensions}/combo-toggle/index.js +37 -27
- package/dist/extensions/headroom-session/index.js +93 -0
- package/{extensions → dist/extensions}/lib/utils.js +27 -1
- package/{extensions → dist/extensions}/opencode/server.js +33 -3
- package/dist/extensions/shared/headroom.js +131 -0
- package/{extensions → dist/extensions}/shared/plugin-settings.js +1 -1
- package/{extensions → dist/extensions}/shared/session-state.js +14 -2
- package/{extensions → dist/extensions}/shared/status.js +7 -1
- package/{tersio.js → dist/tersio.js} +1 -1
- package/extensions/ai-addons-updater/index.ts +52 -40
- package/extensions/caveman-session/index.ts +10 -11
- package/extensions/caveman-session/rule-megacave.md +63 -0
- package/extensions/caveman-session/rule-ultra.md +63 -0
- package/extensions/caveman-session/rule.md +4 -15
- package/extensions/combo-toggle/index.ts +35 -27
- package/extensions/lib/utils.ts +23 -1
- package/extensions/opencode/server.ts +29 -3
- package/extensions/shared/plugin-settings.ts +1 -1
- package/extensions/shared/session-state.ts +15 -2
- package/extensions/shared/status.ts +8 -1
- package/package.json +17 -15
- package/extensions/opencode/index.js +0 -104
- /package/{cli → dist/cli}/banner.js +0 -0
- /package/{cli → dist/cli}/currency.js +0 -0
- /package/{cli → dist/cli}/hosts.js +0 -0
- /package/{cli → dist/cli}/profile.js +0 -0
- /package/{cli → dist/cli}/reset.js +0 -0
- /package/{cli → dist/cli}/usage.js +0 -0
- /package/{extensions → dist/extensions}/rtk-session/index.js +0 -0
- /package/{extensions → dist/extensions}/shared/carbon.js +0 -0
- /package/{extensions → dist/extensions}/shared/host.js +0 -0
- /package/{extensions → dist/extensions}/shared/pricing.js +0 -0
- /package/{extensions → dist/extensions}/shared/rtk-gain.js +0 -0
- /package/{extensions → dist/extensions}/shared/types.js +0 -0
- /package/{extensions → dist/extensions}/shared/usage-ledger.js +0 -0
- /package/{extensions → dist/extensions}/shared/usage-store.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
});
|
package/extensions/lib/utils.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
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
|
-
"
|
|
118
|
-
"
|
|
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 -
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|