@krtclcdy/tersio 2.24.0 → 2.25.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.
- package/README.md +28 -24
- package/cli/banner.js +7 -5
- package/cli/common.js +14 -13
- package/cli/currency.js +2 -7
- package/cli/dashboard.js +220 -13
- package/cli/doctor-fix.js +99 -29
- package/cli/doctor.js +32 -35
- package/cli/hosts.js +23 -26
- package/cli/install.js +228 -115
- package/cli/interactive.js +65 -21
- package/cli/manifest.js +8 -4
- package/cli/profile.js +49 -16
- package/cli/reset.js +3 -7
- package/cli/rtk-wiring.js +29 -32
- package/cli/settings.js +40 -16
- package/cli/uninstall.js +79 -29
- package/cli/update.js +27 -20
- package/cli/usage.js +54 -19
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +75 -20
- package/extensions/ai-addons-updater/index.ts +67 -23
- package/extensions/caveman-session/index.js +33 -75
- package/extensions/caveman-session/index.ts +35 -77
- package/extensions/combo-toggle/index.js +40 -62
- package/extensions/combo-toggle/index.ts +38 -72
- package/extensions/lib/utils.js +64 -15
- package/extensions/lib/utils.ts +62 -15
- package/extensions/opencode/index.js +104 -0
- package/extensions/opencode/server.js +190 -0
- package/extensions/opencode/server.ts +200 -0
- package/extensions/rtk-session/index.js +38 -57
- package/extensions/rtk-session/index.ts +36 -55
- package/extensions/shared/carbon.js +4 -10
- package/extensions/shared/carbon.ts +5 -10
- package/extensions/shared/host.js +95 -56
- package/extensions/shared/host.ts +106 -60
- package/extensions/shared/plugin-settings.js +6 -18
- package/extensions/shared/plugin-settings.ts +6 -18
- package/extensions/shared/pricing.js +79 -27
- package/extensions/shared/pricing.ts +80 -28
- package/extensions/shared/rtk-gain.js +4 -11
- package/extensions/shared/rtk-gain.ts +4 -11
- package/extensions/shared/session-state.js +28 -39
- package/extensions/shared/session-state.ts +29 -38
- package/extensions/shared/status.js +38 -0
- package/extensions/shared/status.ts +45 -0
- package/extensions/shared/types.js +1 -2
- package/extensions/shared/types.ts +2 -2
- package/extensions/shared/usage-ledger.js +312 -57
- package/extensions/shared/usage-ledger.ts +321 -61
- package/extensions/shared/usage-store.js +354 -38
- package/extensions/shared/usage-store.ts +360 -43
- package/extensions/tersio-commands/index.js +5 -14
- package/extensions/tersio-commands/index.ts +5 -14
- package/package.json +12 -7
- package/tersio.js +4 -6
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -24
- package/cli/omp-layer.js +0 -51
- package/cli/pi-layer.js +0 -37
- package/cli/pi-wiring.js +0 -92
- package/extensions/shared/omp-prompt.js +0 -12
|
@@ -1,45 +1,28 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import {
|
|
3
|
+
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, 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
|
+
import { announceStatus } from '../shared/status.ts';
|
|
6
7
|
import { readCavemanDefault } from '../shared/plugin-settings.ts';
|
|
7
8
|
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
8
9
|
|
|
9
10
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const RULE_PATH = join(CAVERN_DIR, 'rule.md');
|
|
11
12
|
|
|
12
|
-
|
|
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.
|
|
13
|
+
// Fail loud: a wrong rule reads to the model as the real one.
|
|
28
14
|
function readFullRule(): string {
|
|
29
|
-
try { return readFileSync(RULE_PATH, 'utf8'); } catch {
|
|
15
|
+
try { return readFileSync(RULE_PATH, 'utf8'); } catch {
|
|
16
|
+
return 'Caveman full requested, but rule.md is missing. Run: tersio doctor --fix extensions';
|
|
17
|
+
}
|
|
30
18
|
}
|
|
31
19
|
|
|
32
20
|
const DEFAULT_MODE = 'off';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Maximum terse prose. Strip conjunctions only when meaning stays clear. State each fact once. Use no invented abbreviations or causal arrows. Keep code, commands, commits, PR text, paths, API names, numbers, units, and errors exact. Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or user confusion.`,
|
|
39
|
-
'wenyan-lite': `Caveman wenyan-lite active for this session. Use semi-classical terse prose. Keep grammar structure, user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
|
|
40
|
-
'wenyan-full': `Caveman wenyan-full active for this session. Use maximum classical terseness where meaning stays clear. Use classical sentence patterns, verbs before objects, and optional omitted subjects. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
|
|
41
|
-
'wenyan-ultra': `Caveman wenyan-ultra active for this session. Use extreme classical abbreviation while keeping a classical Chinese feel and meaning clear. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
|
|
42
|
-
};
|
|
21
|
+
|
|
22
|
+
// Upstream ships one SKILL.md; the level is a directive inside it, so swap only the header.
|
|
23
|
+
function buildInstruction(mode: string): string {
|
|
24
|
+
return `Caveman ${mode} active for this session.\n${readFullRule()}`;
|
|
25
|
+
}
|
|
43
26
|
|
|
44
27
|
function resolveMode(entries: SessionEntry[] | null | undefined, fallback: string = DEFAULT_MODE): string {
|
|
45
28
|
return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
|
|
@@ -52,20 +35,7 @@ function isOffCommand(text: unknown): boolean {
|
|
|
52
35
|
|
|
53
36
|
export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
54
37
|
let currentMode = DEFAULT_MODE;
|
|
55
|
-
let
|
|
56
|
-
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
57
|
-
|
|
58
|
-
function syncStatus(ctx?: ExtensionCtx): void {
|
|
59
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
60
|
-
const c = lastCtx;
|
|
61
|
-
if (!c?.ui?.setStatus) return;
|
|
62
|
-
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
63
|
-
if (isComboPresetActive() || currentMode === 'off') {
|
|
64
|
-
c.ui.setStatus('caveman', undefined);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
paintStatusBar(c.ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
|
|
68
|
-
}
|
|
38
|
+
let lastInjected: string | undefined = undefined;
|
|
69
39
|
|
|
70
40
|
function setMode(mode: string, ctx?: ExtensionCtx): boolean {
|
|
71
41
|
const normalized = normalizeMode('caveman', mode);
|
|
@@ -73,28 +43,18 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
73
43
|
currentMode = normalized;
|
|
74
44
|
pi.appendEntry?.('caveman-mode', { mode: normalized });
|
|
75
45
|
setSharedComboMode('caveman', normalized);
|
|
76
|
-
|
|
77
|
-
const active = activeModesSummary(getSharedComboState());
|
|
78
|
-
const msg = normalized === 'off'
|
|
79
|
-
? `Caveman off. Active: ${active}.`
|
|
80
|
-
: `Caveman ${normalized} on — terse replies for this session. Active: ${active}.`;
|
|
81
|
-
ctx?.ui?.notify?.(msg, 'info');
|
|
46
|
+
announceStatus(ctx);
|
|
82
47
|
return true;
|
|
83
48
|
}
|
|
84
49
|
|
|
85
50
|
setExtensionLabel(pi, 'Caveman session toggle');
|
|
86
51
|
|
|
87
|
-
//
|
|
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.
|
|
52
|
+
// Adopt a /tersio or /combo switch at once, so the next turn needs no reload.
|
|
90
53
|
function syncFromShared(state: { caveman: string }): void {
|
|
91
54
|
const mode = normalizeMode('caveman', state.caveman);
|
|
92
|
-
if (mode)
|
|
93
|
-
currentMode = mode;
|
|
94
|
-
syncStatus();
|
|
95
|
-
}
|
|
55
|
+
if (mode) currentMode = mode;
|
|
96
56
|
}
|
|
97
|
-
setSharedComboListener(syncFromShared);
|
|
57
|
+
setSharedComboListener('caveman', syncFromShared);
|
|
98
58
|
pi.registerCommand?.('caveman', {
|
|
99
59
|
description: 'Toggle terse caveman replies for this session',
|
|
100
60
|
handler: async (args, ctx) => {
|
|
@@ -104,7 +64,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
104
64
|
return;
|
|
105
65
|
}
|
|
106
66
|
if (arg === 'status') {
|
|
107
|
-
ctx
|
|
67
|
+
announceStatus(ctx);
|
|
108
68
|
return;
|
|
109
69
|
}
|
|
110
70
|
if (!setMode(arg, ctx)) {
|
|
@@ -120,21 +80,18 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
120
80
|
|
|
121
81
|
function restoreMode(ctx?: ExtensionCtx): void {
|
|
122
82
|
const entries = sessionEntries(ctx);
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
// UI-gated. Deriving it here makes suppression independent of load order.
|
|
127
|
-
reconcileSharedComboEntries(entries);
|
|
128
|
-
// Persisted session state wins; a fresh session falls back to the
|
|
129
|
-
// installer/user-configured default (off unless configured).
|
|
83
|
+
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
84
|
+
if (hasModeState(entries)) reconcileSharedComboEntries(entries);
|
|
85
|
+
// Persisted state wins, then a mode already chosen this session, then the configured default. `off` is the start value, not "unset".
|
|
130
86
|
const persisted = resolveMode(entries, '');
|
|
131
|
-
|
|
132
|
-
|
|
87
|
+
const alreadyChosen = currentMode !== DEFAULT_MODE;
|
|
88
|
+
currentMode = persisted || (alreadyChosen ? currentMode : normalizeMode('caveman', readCavemanDefault())) || DEFAULT_MODE;
|
|
89
|
+
announceStatus(ctx);
|
|
133
90
|
}
|
|
134
91
|
|
|
92
|
+
// Start, resume, and branch all land here, so every session type shows it.
|
|
135
93
|
pi.on('session_start', async (_event, ctx) => {
|
|
136
94
|
restoreMode(ctx);
|
|
137
|
-
ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
|
|
138
95
|
});
|
|
139
96
|
|
|
140
97
|
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
@@ -145,22 +102,23 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
|
|
|
145
102
|
restoreMode(ctx);
|
|
146
103
|
});
|
|
147
104
|
|
|
148
|
-
pi
|
|
149
|
-
|
|
150
|
-
syncStatus(ctx);
|
|
105
|
+
onHostEvent(pi, 'session_switch', async (_event, ctx) => {
|
|
106
|
+
restoreMode(ctx);
|
|
151
107
|
});
|
|
152
108
|
|
|
153
|
-
pi
|
|
154
|
-
|
|
155
|
-
syncStatus(ctx);
|
|
109
|
+
onHostEvent(pi, 'session_compact', async (_event, ctx) => {
|
|
110
|
+
restoreMode(ctx);
|
|
156
111
|
});
|
|
157
112
|
|
|
158
113
|
pi.on('before_agent_start', async (event: SystemPromptEvent) => {
|
|
159
114
|
const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
|
|
160
115
|
if (!mode || mode === 'off') return;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
116
|
+
const instruction = buildInstruction(mode);
|
|
117
|
+
// Match the whole instruction so a re-presented prompt cannot stack it.
|
|
118
|
+
if (systemPromptIncludes(event.systemPrompt, instruction)) return;
|
|
119
|
+
// The previous level's block is replaced, not stacked on.
|
|
120
|
+
const stale = lastInjected;
|
|
121
|
+
lastInjected = instruction;
|
|
122
|
+
return injectPromptText(pi, event, instruction, stale);
|
|
165
123
|
});
|
|
166
124
|
}
|
|
@@ -8,11 +8,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
return path;
|
|
9
9
|
};
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
11
12
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
|
-
import { activeModesSummary, COMBO_LEVELS, getSharedComboState,
|
|
13
|
+
import { activeModesSummary, COMBO_LEVELS, getSharedComboState, hasModeState, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, systemPromptIncludes, } from '../shared/session-state.js';
|
|
13
14
|
import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
|
|
15
|
+
import { announceStatus } from '../shared/status.js';
|
|
14
16
|
import { isComboSetupComplete, readComboDefault, readPonytailDefault, saveComboSetup, } from '../shared/plugin-settings.js';
|
|
15
|
-
import { findHoistedPackage } from '../lib/utils.js';
|
|
17
|
+
import { findHoistedPackage, isPiProcess } from '../lib/utils.js';
|
|
16
18
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
17
19
|
const PONYTAIL_FALLBACK_INTENSITY = {
|
|
18
20
|
lite: 'Prefer the simplest correct solution.',
|
|
@@ -22,16 +24,15 @@ export function ponytailFallback(mode) {
|
|
|
22
24
|
const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
|
|
23
25
|
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.`;
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
|
|
27
|
-
}
|
|
28
|
-
// Ponytail is hoisted, so one upward walk covers every install layout. The hook
|
|
29
|
-
// is CommonJS, so both namespace shapes are tried.
|
|
27
|
+
// Ponytail is hoisted, so one upward walk covers every install layout. The hook is CommonJS, so both namespace shapes are tried.
|
|
30
28
|
async function loadPonytailInstructions(mode) {
|
|
31
29
|
const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
|
|
32
|
-
|
|
30
|
+
// The OpenCode tree ships no node_modules; the installer bundles the ruleset beside the extensions.
|
|
31
|
+
const bundled = path.join(EXTENSION_DIR, '..', 'ponytail-bundle', 'hooks', 'ponytail-instructions.js');
|
|
32
|
+
const treeLocal = installed ?? (existsSync(bundled) ? bundled : null);
|
|
33
|
+
if (treeLocal) {
|
|
33
34
|
try {
|
|
34
|
-
const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(
|
|
35
|
+
const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(treeLocal).href));
|
|
35
36
|
const build = loaded.getPonytailInstructions ?? loaded.default?.getPonytailInstructions;
|
|
36
37
|
if (build)
|
|
37
38
|
return build(mode);
|
|
@@ -42,30 +43,8 @@ async function loadPonytailInstructions(mode) {
|
|
|
42
43
|
}
|
|
43
44
|
export default function comboToggleExtension(pi) {
|
|
44
45
|
setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
|
|
45
|
-
let lastCtx = undefined;
|
|
46
46
|
let setupPrompted = false;
|
|
47
|
-
|
|
48
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
49
|
-
const c = lastCtx;
|
|
50
|
-
if (!c?.ui?.setStatus)
|
|
51
|
-
return;
|
|
52
|
-
// One unified bar replaces the per-extension bars while a preset is active.
|
|
53
|
-
if (!isComboPresetActive()) {
|
|
54
|
-
c.ui.setStatus('combo', undefined);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
// Read the live bridge, not a cache: siblings reconcile it first.
|
|
58
|
-
const state = getSharedComboState();
|
|
59
|
-
const c0 = state.caveman.toUpperCase();
|
|
60
|
-
const r = state.rtk.toUpperCase();
|
|
61
|
-
const p = state.ponytail.toUpperCase();
|
|
62
|
-
const lvl = state.level.toUpperCase();
|
|
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.
|
|
65
|
-
c.ui.setStatus('caveman', undefined);
|
|
66
|
-
c.ui.setStatus('rtk', undefined);
|
|
67
|
-
c.ui.setStatus('ponytail', undefined);
|
|
68
|
-
}
|
|
47
|
+
let lastInjected = undefined;
|
|
69
48
|
// Siblings restore from these entries, so the fallback must write them too.
|
|
70
49
|
function persistPreset(level) {
|
|
71
50
|
const modes = COMBO_LEVELS[level];
|
|
@@ -74,19 +53,15 @@ export default function comboToggleExtension(pi) {
|
|
|
74
53
|
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
75
54
|
pi.appendEntry?.('combo-level', { level });
|
|
76
55
|
}
|
|
77
|
-
function useState(state, ctx) {
|
|
78
|
-
syncStatus(ctx);
|
|
79
|
-
return state;
|
|
80
|
-
}
|
|
81
56
|
function reconcile(ctx) {
|
|
82
57
|
if (!ctx?.hasUI)
|
|
83
58
|
return getSharedComboState();
|
|
84
|
-
return
|
|
59
|
+
return reconcileSharedComboEntries(sessionEntries(ctx));
|
|
85
60
|
}
|
|
86
61
|
function listen(ctx) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
62
|
+
if (!ctx?.hasUI)
|
|
63
|
+
return;
|
|
64
|
+
setSharedComboListener('combo', (state) => { announceStatus(ctx); return state; });
|
|
90
65
|
}
|
|
91
66
|
function track(ctx) {
|
|
92
67
|
listen(ctx);
|
|
@@ -99,8 +74,8 @@ export default function comboToggleExtension(pi) {
|
|
|
99
74
|
listen(ctx);
|
|
100
75
|
const arg = String(args || '').trim().toLowerCase();
|
|
101
76
|
if (!arg || arg === 'status') {
|
|
102
|
-
|
|
103
|
-
ctx
|
|
77
|
+
reconcile(ctx);
|
|
78
|
+
announceStatus(ctx);
|
|
104
79
|
return;
|
|
105
80
|
}
|
|
106
81
|
if (arg === 'help') {
|
|
@@ -116,9 +91,8 @@ export default function comboToggleExtension(pi) {
|
|
|
116
91
|
return;
|
|
117
92
|
}
|
|
118
93
|
persistPreset(level);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
ctx?.ui?.notify?.(level === 'off' ? `Combo off — all tersio modes inactive for this session. Active: ${active}.` : `Combo ${level} on — ${active} active for this session.`, 'info');
|
|
94
|
+
setSharedComboLevel(level);
|
|
95
|
+
announceStatus(ctx);
|
|
122
96
|
},
|
|
123
97
|
});
|
|
124
98
|
async function runFirstRunSetup(ctx) {
|
|
@@ -134,40 +108,39 @@ export default function comboToggleExtension(pi) {
|
|
|
134
108
|
const level = choice || 'off';
|
|
135
109
|
if (saveComboSetup(level) && level !== 'off') {
|
|
136
110
|
persistPreset(level);
|
|
137
|
-
|
|
111
|
+
setSharedComboLevel(level);
|
|
138
112
|
ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
|
|
139
113
|
}
|
|
140
114
|
}
|
|
141
115
|
pi.on('session_start', async (_event, ctx) => {
|
|
142
116
|
track(ctx);
|
|
143
|
-
if (!ctx?.hasUI)
|
|
144
|
-
syncStatus(ctx);
|
|
145
117
|
await runFirstRunSetup(ctx);
|
|
146
118
|
// The configured default applies only when no persisted mode state exists.
|
|
147
119
|
const entries = sessionEntries(ctx);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
if (getSharedComboState().level === 'off' && !hasModeState) {
|
|
120
|
+
const persisted = hasModeState(entries);
|
|
121
|
+
if (getSharedComboState().level === 'off' && !persisted) {
|
|
151
122
|
const fallback = readComboDefault();
|
|
152
123
|
if (fallback !== 'off') {
|
|
153
124
|
persistPreset(fallback);
|
|
154
|
-
|
|
155
|
-
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
125
|
+
setSharedComboLevel(fallback);
|
|
156
126
|
}
|
|
157
127
|
}
|
|
158
|
-
// No sibling restores a standalone ponytail default, so apply it here; skip
|
|
159
|
-
// it when the active preset already writes the same mode.
|
|
128
|
+
// No sibling restores a standalone ponytail default, so apply it here; skip it when the active preset already writes the same mode.
|
|
160
129
|
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
161
130
|
const ponytailFallback = readPonytailDefault();
|
|
162
131
|
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
163
132
|
pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
|
|
164
|
-
|
|
133
|
+
setSharedComboMode('ponytail', ponytailFallback);
|
|
165
134
|
}
|
|
166
135
|
}
|
|
136
|
+
// Announced last, so any default above is reflected.
|
|
137
|
+
announceStatus(ctx);
|
|
167
138
|
});
|
|
168
|
-
|
|
139
|
+
// Resume, branch, and compaction re-announce; dedupe keeps repeats silent.
|
|
140
|
+
for (const event of ['session_branch', 'session_tree', 'agent_start', 'session_switch', 'session_compact']) {
|
|
169
141
|
onHostEvent(pi, event, async (_event, ctx) => {
|
|
170
142
|
track(ctx);
|
|
143
|
+
announceStatus(ctx);
|
|
171
144
|
if (event === 'agent_start')
|
|
172
145
|
await runFirstRunSetup(ctx);
|
|
173
146
|
});
|
|
@@ -175,13 +148,18 @@ export default function comboToggleExtension(pi) {
|
|
|
175
148
|
pi.on('before_agent_start', async (event, ctx) => {
|
|
176
149
|
if (ctx?.hasUI)
|
|
177
150
|
reconcile(ctx);
|
|
178
|
-
if (!isOmpSubagentPrompt(event.systemPrompt))
|
|
179
|
-
return;
|
|
180
151
|
const mode = getSharedComboState().ponytail;
|
|
181
|
-
|
|
152
|
+
// On pi the upstream ponytail pi-extension is the single injector; it reads these 'ponytail-mode' entries at session start, so Tersio only persists.
|
|
153
|
+
if (isPiProcess())
|
|
154
|
+
return;
|
|
155
|
+
// Match the level's own header: the shared phrase alone makes every level block every other, so a `/combo` level switch is ignored all session.
|
|
156
|
+
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, `PONYTAIL MODE ACTIVE — level: ${mode}`))
|
|
182
157
|
return;
|
|
183
|
-
const
|
|
184
|
-
|
|
158
|
+
const instruction = await loadPonytailInstructions(mode);
|
|
159
|
+
// The previous level's block is replaced, not stacked on.
|
|
160
|
+
const stale = lastInjected;
|
|
161
|
+
lastInjected = instruction;
|
|
162
|
+
return injectPromptText(pi, event, instruction, stale);
|
|
185
163
|
});
|
|
186
164
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|
|
187
165
|
}
|
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
// /combo session toggle: off | medium | balanced | max.
|
|
2
2
|
|
|
3
|
-
import os from 'node:os';
|
|
4
3
|
import path from 'node:path';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import {
|
|
7
7
|
activeModesSummary,
|
|
8
8
|
COMBO_LEVELS,
|
|
9
9
|
getSharedComboState,
|
|
10
|
-
|
|
11
|
-
isOmpSubagentPrompt,
|
|
10
|
+
hasModeState,
|
|
12
11
|
normalizeComboLevel,
|
|
13
|
-
paintableCtx,
|
|
14
12
|
reconcileSharedComboEntries,
|
|
15
13
|
sessionEntries,
|
|
16
14
|
setSharedComboLevel,
|
|
17
15
|
setSharedComboListener,
|
|
18
16
|
setSharedComboMode,
|
|
19
17
|
systemPromptIncludes,
|
|
20
|
-
themeStatus,
|
|
21
18
|
} from '../shared/session-state.ts';
|
|
22
19
|
import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
|
|
20
|
+
import { announceStatus } from '../shared/status.ts';
|
|
23
21
|
import {
|
|
24
22
|
isComboSetupComplete,
|
|
25
23
|
readComboDefault,
|
|
26
24
|
readPonytailDefault,
|
|
27
25
|
saveComboSetup,
|
|
28
26
|
} from '../shared/plugin-settings.ts';
|
|
29
|
-
import { findHoistedPackage } from '../lib/utils.ts';
|
|
27
|
+
import { findHoistedPackage, isPiProcess } from '../lib/utils.ts';
|
|
30
28
|
import type { ComboState, ExtensionApi, ExtensionCtx, SystemPromptEvent } from '../shared/types.ts';
|
|
31
29
|
|
|
32
30
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -41,17 +39,16 @@ export function ponytailFallback(mode: string): string {
|
|
|
41
39
|
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.`;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
function levelSummary(state: ComboState): string {
|
|
45
|
-
return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
|
|
46
|
-
}
|
|
47
42
|
|
|
48
|
-
// Ponytail is hoisted, so one upward walk covers every install layout. The hook
|
|
49
|
-
// is CommonJS, so both namespace shapes are tried.
|
|
43
|
+
// Ponytail is hoisted, so one upward walk covers every install layout. The hook is CommonJS, so both namespace shapes are tried.
|
|
50
44
|
async function loadPonytailInstructions(mode: string): Promise<string> {
|
|
51
45
|
const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
|
|
52
|
-
|
|
46
|
+
// The OpenCode tree ships no node_modules; the installer bundles the ruleset beside the extensions.
|
|
47
|
+
const bundled = path.join(EXTENSION_DIR, '..', 'ponytail-bundle', 'hooks', 'ponytail-instructions.js');
|
|
48
|
+
const treeLocal = installed ?? (existsSync(bundled) ? bundled : null);
|
|
49
|
+
if (treeLocal) {
|
|
53
50
|
try {
|
|
54
|
-
const loaded = await import(pathToFileURL(
|
|
51
|
+
const loaded = await import(pathToFileURL(treeLocal).href) as {
|
|
55
52
|
getPonytailInstructions?: (level: string) => string;
|
|
56
53
|
default?: { getPonytailInstructions?: (level: string) => string };
|
|
57
54
|
};
|
|
@@ -65,30 +62,9 @@ async function loadPonytailInstructions(mode: string): Promise<string> {
|
|
|
65
62
|
export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
66
63
|
setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
|
|
67
64
|
|
|
68
|
-
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
69
65
|
let setupPrompted = false;
|
|
66
|
+
let lastInjected: string | undefined = undefined;
|
|
70
67
|
|
|
71
|
-
function syncStatus(ctx?: ExtensionCtx): void {
|
|
72
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
73
|
-
const c = lastCtx;
|
|
74
|
-
if (!c?.ui?.setStatus) return;
|
|
75
|
-
// One unified bar replaces the per-extension bars while a preset is active.
|
|
76
|
-
if (!isComboPresetActive()) {
|
|
77
|
-
c.ui.setStatus('combo', undefined);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
// Read the live bridge, not a cache: siblings reconcile it first.
|
|
81
|
-
const state = getSharedComboState();
|
|
82
|
-
const c0 = state.caveman.toUpperCase();
|
|
83
|
-
const r = state.rtk.toUpperCase();
|
|
84
|
-
const p = state.ponytail.toUpperCase();
|
|
85
|
-
const lvl = state.level.toUpperCase();
|
|
86
|
-
c.ui.setStatus('combo', themeStatus(c.ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
|
|
87
|
-
// 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);
|
|
91
|
-
}
|
|
92
68
|
|
|
93
69
|
// Siblings restore from these entries, so the fallback must write them too.
|
|
94
70
|
function persistPreset(level: string): void {
|
|
@@ -99,18 +75,13 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
99
75
|
pi.appendEntry?.('combo-level', { level });
|
|
100
76
|
}
|
|
101
77
|
|
|
102
|
-
function useState(state: Readonly<ComboState>, ctx?: ExtensionCtx): Readonly<ComboState> {
|
|
103
|
-
syncStatus(ctx);
|
|
104
|
-
return state;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
78
|
function reconcile(ctx?: ExtensionCtx): Readonly<ComboState> {
|
|
108
79
|
if (!ctx?.hasUI) return getSharedComboState();
|
|
109
|
-
return
|
|
80
|
+
return reconcileSharedComboEntries(sessionEntries(ctx));
|
|
110
81
|
}
|
|
111
82
|
function listen(ctx?: ExtensionCtx): void {
|
|
112
|
-
|
|
113
|
-
|
|
83
|
+
if (!ctx?.hasUI) return;
|
|
84
|
+
setSharedComboListener('combo', (state) => { announceStatus(ctx); return state; });
|
|
114
85
|
}
|
|
115
86
|
|
|
116
87
|
function track(ctx?: ExtensionCtx): void {
|
|
@@ -125,11 +96,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
125
96
|
const arg = String(args || '').trim().toLowerCase();
|
|
126
97
|
|
|
127
98
|
if (!arg || arg === 'status') {
|
|
128
|
-
|
|
129
|
-
ctx
|
|
130
|
-
`Combo: ${state.level === 'custom' ? 'INACTIVE' : state.level.toUpperCase()} (${levelSummary(state)})`,
|
|
131
|
-
'info'
|
|
132
|
-
);
|
|
99
|
+
reconcile(ctx);
|
|
100
|
+
announceStatus(ctx);
|
|
133
101
|
return;
|
|
134
102
|
}
|
|
135
103
|
|
|
@@ -154,13 +122,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
154
122
|
}
|
|
155
123
|
|
|
156
124
|
persistPreset(level);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const active = activeModesSummary(getSharedComboState());
|
|
160
|
-
ctx?.ui?.notify?.(
|
|
161
|
-
level === 'off' ? `Combo off — all tersio modes inactive for this session. Active: ${active}.` : `Combo ${level} on — ${active} active for this session.`,
|
|
162
|
-
'info'
|
|
163
|
-
);
|
|
125
|
+
setSharedComboLevel(level);
|
|
126
|
+
announceStatus(ctx);
|
|
164
127
|
},
|
|
165
128
|
});
|
|
166
129
|
|
|
@@ -176,54 +139,57 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
176
139
|
const level = choice || 'off';
|
|
177
140
|
if (saveComboSetup(level) && level !== 'off') {
|
|
178
141
|
persistPreset(level);
|
|
179
|
-
|
|
142
|
+
setSharedComboLevel(level);
|
|
180
143
|
ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
|
|
181
144
|
}
|
|
182
145
|
}
|
|
183
146
|
|
|
184
147
|
pi.on('session_start', async (_event, ctx) => {
|
|
185
148
|
track(ctx);
|
|
186
|
-
if (!ctx?.hasUI) syncStatus(ctx);
|
|
187
149
|
await runFirstRunSetup(ctx);
|
|
188
150
|
// The configured default applies only when no persisted mode state exists.
|
|
189
151
|
const entries = sessionEntries(ctx);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
e.customType === 'rtk-mode' || e.customType === 'ponytail-mode'));
|
|
193
|
-
if (getSharedComboState().level === 'off' && !hasModeState) {
|
|
152
|
+
const persisted = hasModeState(entries);
|
|
153
|
+
if (getSharedComboState().level === 'off' && !persisted) {
|
|
194
154
|
const fallback = readComboDefault();
|
|
195
155
|
if (fallback !== 'off') {
|
|
196
156
|
persistPreset(fallback);
|
|
197
|
-
|
|
198
|
-
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
157
|
+
setSharedComboLevel(fallback);
|
|
199
158
|
}
|
|
200
159
|
}
|
|
201
|
-
// No sibling restores a standalone ponytail default, so apply it here; skip
|
|
202
|
-
// it when the active preset already writes the same mode.
|
|
160
|
+
// No sibling restores a standalone ponytail default, so apply it here; skip it when the active preset already writes the same mode.
|
|
203
161
|
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
204
162
|
const ponytailFallback = readPonytailDefault();
|
|
205
163
|
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
206
164
|
pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
|
|
207
|
-
|
|
165
|
+
setSharedComboMode('ponytail', ponytailFallback);
|
|
208
166
|
}
|
|
209
167
|
}
|
|
168
|
+
// Announced last, so any default above is reflected.
|
|
169
|
+
announceStatus(ctx);
|
|
210
170
|
});
|
|
211
171
|
|
|
212
|
-
|
|
172
|
+
// Resume, branch, and compaction re-announce; dedupe keeps repeats silent.
|
|
173
|
+
for (const event of ['session_branch', 'session_tree', 'agent_start', 'session_switch', 'session_compact']) {
|
|
213
174
|
onHostEvent(pi, event, async (_event, ctx) => {
|
|
214
175
|
track(ctx);
|
|
176
|
+
announceStatus(ctx);
|
|
215
177
|
if (event === 'agent_start') await runFirstRunSetup(ctx);
|
|
216
178
|
});
|
|
217
179
|
}
|
|
218
180
|
|
|
219
181
|
pi.on<SystemPromptEvent>('before_agent_start', async (event, ctx) => {
|
|
220
182
|
if (ctx?.hasUI) reconcile(ctx);
|
|
221
|
-
if (!isOmpSubagentPrompt(event.systemPrompt)) return;
|
|
222
|
-
|
|
223
183
|
const mode = getSharedComboState().ponytail;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
184
|
+
// On pi the upstream ponytail pi-extension is the single injector; it reads these 'ponytail-mode' entries at session start, so Tersio only persists.
|
|
185
|
+
if (isPiProcess()) return;
|
|
186
|
+
// Match the level's own header: the shared phrase alone makes every level block every other, so a `/combo` level switch is ignored all session.
|
|
187
|
+
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, `PONYTAIL MODE ACTIVE — level: ${mode}`)) return;
|
|
188
|
+
const instruction = await loadPonytailInstructions(mode);
|
|
189
|
+
// The previous level's block is replaced, not stacked on.
|
|
190
|
+
const stale = lastInjected;
|
|
191
|
+
lastInjected = instruction;
|
|
192
|
+
return injectPromptText(pi, event, instruction, stale);
|
|
227
193
|
});
|
|
228
194
|
|
|
229
195
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|