@krtclcdy/tersio 2.22.1 → 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.
- package/README.md +62 -46
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/common.js +55 -55
- package/cli/currency.js +8 -12
- package/cli/dashboard.js +76 -49
- package/cli/doctor-fix.js +61 -44
- package/cli/doctor.js +66 -44
- package/cli/hosts.js +118 -0
- package/cli/install.js +187 -143
- package/cli/interactive.js +27 -1
- package/cli/layer.js +24 -0
- package/cli/manifest.js +36 -0
- package/cli/omp-layer.js +51 -0
- package/cli/pi-layer.js +37 -0
- package/cli/pi-wiring.js +92 -0
- package/cli/profile.js +45 -46
- package/cli/reset.js +3 -26
- package/cli/rtk-wiring.js +10 -15
- package/cli/settings.js +1 -1
- package/cli/uninstall.js +135 -44
- package/cli/update.js +24 -30
- package/cli/usage.js +6 -1
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +36 -25
- package/extensions/ai-addons-updater/index.ts +36 -24
- package/extensions/caveman-session/index.js +23 -18
- package/extensions/caveman-session/index.ts +23 -18
- package/extensions/caveman-session/rule.md +90 -0
- package/extensions/combo-toggle/index.js +63 -41
- package/extensions/combo-toggle/index.ts +61 -48
- package/extensions/lib/utils.js +56 -13
- package/extensions/lib/utils.ts +55 -13
- package/extensions/rtk-session/index.js +20 -19
- package/extensions/rtk-session/index.ts +18 -24
- package/extensions/shared/carbon.js +5 -8
- package/extensions/shared/carbon.ts +5 -8
- package/extensions/shared/host.js +68 -0
- package/extensions/shared/host.ts +104 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +63 -16
- package/extensions/shared/plugin-settings.ts +67 -17
- package/extensions/shared/pricing.js +26 -15
- package/extensions/shared/pricing.ts +28 -16
- package/extensions/shared/rtk-gain.js +13 -24
- package/extensions/shared/rtk-gain.ts +14 -25
- package/extensions/shared/session-state.js +26 -28
- package/extensions/shared/session-state.ts +26 -29
- package/extensions/shared/types.ts +15 -3
- package/extensions/shared/usage-ledger.js +136 -38
- package/extensions/shared/usage-ledger.ts +149 -52
- package/extensions/shared/usage-store.js +14 -12
- package/extensions/shared/usage-store.ts +16 -14
- package/extensions/tersio-commands/index.js +2 -1
- package/extensions/tersio-commands/index.ts +2 -1
- package/package.json +15 -7
- package/tersio.js +6 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
- package/extensions/shared/mode-reinforcement.ts +0 -29
|
@@ -1,65 +1,72 @@
|
|
|
1
|
-
// /combo session toggle
|
|
2
|
-
|
|
3
|
-
|
|
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 {
|
|
6
|
-
import { activeModesSummary,
|
|
7
|
-
import {
|
|
8
|
-
|
|
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';
|
|
14
|
+
import { isComboSetupComplete, readComboDefault, readPonytailDefault, saveComboSetup, } from '../shared/plugin-settings.js';
|
|
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
|
-
review: 'Review only for avoidable complexity; recommend the smallest correct replacement.',
|
|
12
19
|
};
|
|
13
|
-
|
|
20
|
+
// Fallback text when Ponytail cannot be found; exported for its own test.
|
|
21
|
+
export function ponytailFallback(mode) {
|
|
14
22
|
const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
|
|
15
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.`;
|
|
16
24
|
}
|
|
17
25
|
function levelSummary(state) {
|
|
18
26
|
return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
|
|
19
27
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 */ }
|
|
26
40
|
}
|
|
27
|
-
catch { }
|
|
28
41
|
return ponytailFallback(mode);
|
|
29
42
|
}
|
|
30
43
|
export default function comboToggleExtension(pi) {
|
|
31
|
-
pi
|
|
44
|
+
setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
|
|
32
45
|
let lastCtx = undefined;
|
|
46
|
+
let setupPrompted = false;
|
|
33
47
|
function syncStatus(ctx) {
|
|
34
48
|
lastCtx = paintableCtx(lastCtx, ctx);
|
|
35
49
|
const c = lastCtx;
|
|
36
50
|
if (!c?.ui?.setStatus)
|
|
37
51
|
return;
|
|
38
|
-
//
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
}
|
|
@@ -115,13 +121,29 @@ export default function comboToggleExtension(pi) {
|
|
|
115
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');
|
|
116
122
|
},
|
|
117
123
|
});
|
|
124
|
+
async function runFirstRunSetup(ctx) {
|
|
125
|
+
if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete())
|
|
126
|
+
return;
|
|
127
|
+
setupPrompted = true;
|
|
128
|
+
const choice = await hostSelect(pi, ctx.ui, 'Session-start defaults — Combo preset', [
|
|
129
|
+
{ label: 'off', description: 'Keep every Tersio mode inactive' },
|
|
130
|
+
{ label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
131
|
+
{ label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
|
|
132
|
+
{ label: 'max', description: 'caveman=ultra, rtk=on, ponytail=ultra' },
|
|
133
|
+
], { selectionMarker: 'radio', helpText: 'You can change this later with /tersio settings.' });
|
|
134
|
+
const level = choice || 'off';
|
|
135
|
+
if (saveComboSetup(level) && level !== 'off') {
|
|
136
|
+
persistPreset(level);
|
|
137
|
+
useState(setSharedComboLevel(level), ctx);
|
|
138
|
+
ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
118
141
|
pi.on('session_start', async (_event, ctx) => {
|
|
119
142
|
track(ctx);
|
|
120
143
|
if (!ctx?.hasUI)
|
|
121
144
|
syncStatus(ctx);
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
// combo bar never paints on sessions that already carry other entries.
|
|
145
|
+
await runFirstRunSetup(ctx);
|
|
146
|
+
// The configured default applies only when no persisted mode state exists.
|
|
125
147
|
const entries = sessionEntries(ctx);
|
|
126
148
|
const hasModeState = entries.some((e) => e?.type === 'custom' && (e.customType === 'combo-level' || e.customType === 'caveman-mode' ||
|
|
127
149
|
e.customType === 'rtk-mode' || e.customType === 'ponytail-mode'));
|
|
@@ -133,10 +155,8 @@ export default function comboToggleExtension(pi) {
|
|
|
133
155
|
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
134
156
|
}
|
|
135
157
|
}
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
// ponytail-mode entry. Skipped when it matches the active preset — the
|
|
139
|
-
// 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.
|
|
140
160
|
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
141
161
|
const ponytailFallback = readPonytailDefault();
|
|
142
162
|
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
@@ -146,8 +166,10 @@ export default function comboToggleExtension(pi) {
|
|
|
146
166
|
}
|
|
147
167
|
});
|
|
148
168
|
for (const event of ['session_branch', 'session_tree', 'agent_start']) {
|
|
149
|
-
pi
|
|
169
|
+
onHostEvent(pi, event, async (_event, ctx) => {
|
|
150
170
|
track(ctx);
|
|
171
|
+
if (event === 'agent_start')
|
|
172
|
+
await runFirstRunSetup(ctx);
|
|
151
173
|
});
|
|
152
174
|
}
|
|
153
175
|
pi.on('before_agent_start', async (event, ctx) => {
|
|
@@ -158,8 +180,8 @@ export default function comboToggleExtension(pi) {
|
|
|
158
180
|
const mode = getSharedComboState().ponytail;
|
|
159
181
|
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE'))
|
|
160
182
|
return;
|
|
161
|
-
const
|
|
162
|
-
return
|
|
183
|
+
const instructions = await loadPonytailInstructions(mode);
|
|
184
|
+
return injectPromptText(pi, event, instructions);
|
|
163
185
|
});
|
|
164
186
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|
|
165
187
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
// /combo session toggle
|
|
2
|
-
// Modes: off | medium | balanced | max
|
|
1
|
+
// /combo session toggle: off | medium | balanced | max.
|
|
3
2
|
|
|
4
3
|
import os from 'node:os';
|
|
5
4
|
import path from 'node:path';
|
|
6
|
-
import {
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
6
|
import {
|
|
8
7
|
activeModesSummary,
|
|
9
|
-
asPromptArray,
|
|
10
8
|
COMBO_LEVELS,
|
|
11
9
|
getSharedComboState,
|
|
12
10
|
isComboPresetActive,
|
|
@@ -19,18 +17,26 @@ import {
|
|
|
19
17
|
setSharedComboListener,
|
|
20
18
|
setSharedComboMode,
|
|
21
19
|
systemPromptIncludes,
|
|
20
|
+
themeStatus,
|
|
22
21
|
} from '../shared/session-state.ts';
|
|
23
|
-
import {
|
|
22
|
+
import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
|
|
23
|
+
import {
|
|
24
|
+
isComboSetupComplete,
|
|
25
|
+
readComboDefault,
|
|
26
|
+
readPonytailDefault,
|
|
27
|
+
saveComboSetup,
|
|
28
|
+
} from '../shared/plugin-settings.ts';
|
|
29
|
+
import { findHoistedPackage } from '../lib/utils.ts';
|
|
24
30
|
import type { ComboState, ExtensionApi, ExtensionCtx, SystemPromptEvent } from '../shared/types.ts';
|
|
25
31
|
|
|
26
|
-
const
|
|
32
|
+
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
27
33
|
|
|
28
34
|
const PONYTAIL_FALLBACK_INTENSITY: Record<string, string> = {
|
|
29
35
|
lite: 'Prefer the simplest correct solution.',
|
|
30
|
-
review: 'Review only for avoidable complexity; recommend the smallest correct replacement.',
|
|
31
36
|
};
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
// Fallback text when Ponytail cannot be found; exported for its own test.
|
|
39
|
+
export function ponytailFallback(mode: string): string {
|
|
34
40
|
const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
|
|
35
41
|
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.`;
|
|
36
42
|
}
|
|
@@ -39,59 +45,52 @@ function levelSummary(state: ComboState): string {
|
|
|
39
45
|
return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} catch { }
|
|
48
|
+
// Ponytail is hoisted, so one upward walk covers every install layout. The hook
|
|
49
|
+
// is CommonJS, so both namespace shapes are tried.
|
|
50
|
+
async function loadPonytailInstructions(mode: string): Promise<string> {
|
|
51
|
+
const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
|
|
52
|
+
if (installed) {
|
|
53
|
+
try {
|
|
54
|
+
const loaded = await import(pathToFileURL(installed).href) as {
|
|
55
|
+
getPonytailInstructions?: (level: string) => string;
|
|
56
|
+
default?: { getPonytailInstructions?: (level: string) => string };
|
|
57
|
+
};
|
|
58
|
+
const build = loaded.getPonytailInstructions ?? loaded.default?.getPonytailInstructions;
|
|
59
|
+
if (build) return build(mode);
|
|
60
|
+
} catch { /* fall through to the built-in text */ }
|
|
61
|
+
}
|
|
57
62
|
return ponytailFallback(mode);
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
61
|
-
pi
|
|
66
|
+
setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
|
|
62
67
|
|
|
63
68
|
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
69
|
+
let setupPrompted = false;
|
|
64
70
|
|
|
65
71
|
function syncStatus(ctx?: ExtensionCtx): void {
|
|
66
72
|
lastCtx = paintableCtx(lastCtx, ctx);
|
|
67
73
|
const c = lastCtx;
|
|
68
74
|
if (!c?.ui?.setStatus) return;
|
|
69
|
-
//
|
|
70
|
-
// active. In custom/off, the individual bars come back and combo stays clear.
|
|
75
|
+
// One unified bar replaces the per-extension bars while a preset is active.
|
|
71
76
|
if (!isComboPresetActive()) {
|
|
72
77
|
c.ui.setStatus('combo', undefined);
|
|
73
78
|
return;
|
|
74
79
|
}
|
|
75
|
-
// Read the live bridge, not
|
|
76
|
-
// reconcile it from persisted entries before this one runs.
|
|
80
|
+
// Read the live bridge, not a cache: siblings reconcile it first.
|
|
77
81
|
const state = getSharedComboState();
|
|
78
|
-
const theme = c.ui.theme;
|
|
79
82
|
const c0 = state.caveman.toUpperCase();
|
|
80
83
|
const r = state.rtk.toUpperCase();
|
|
81
84
|
const p = state.ponytail.toUpperCase();
|
|
82
85
|
const lvl = state.level.toUpperCase();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Clobber any sibling bars another extension may have painted in a race
|
|
86
|
-
// during session_start — our bar is canonical for the duration of the preset.
|
|
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.
|
|
87
88
|
c.ui.setStatus('caveman', undefined);
|
|
88
89
|
c.ui.setStatus('rtk', undefined);
|
|
89
90
|
c.ui.setStatus('ponytail', undefined);
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
//
|
|
93
|
-
// restore from these entries, so the fallback must write them too or the
|
|
94
|
-
// preset evaporates on resume and ponytail never activates.
|
|
93
|
+
// Siblings restore from these entries, so the fallback must write them too.
|
|
95
94
|
function persistPreset(level: string): void {
|
|
96
95
|
const modes = COMBO_LEVELS[level];
|
|
97
96
|
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
@@ -110,8 +109,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
110
109
|
return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
|
|
111
110
|
}
|
|
112
111
|
function listen(ctx?: ExtensionCtx): void {
|
|
113
|
-
// Stable identity: the bridge set dedupes, so
|
|
114
|
-
// calls register once instead of stacking duplicate listeners.
|
|
112
|
+
// Stable identity: the bridge set dedupes, so repeat calls register once.
|
|
115
113
|
if (ctx?.hasUI) setSharedComboListener(useState);
|
|
116
114
|
}
|
|
117
115
|
|
|
@@ -166,12 +164,28 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
166
164
|
},
|
|
167
165
|
});
|
|
168
166
|
|
|
167
|
+
async function runFirstRunSetup(ctx?: ExtensionCtx): Promise<void> {
|
|
168
|
+
if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete()) return;
|
|
169
|
+
setupPrompted = true;
|
|
170
|
+
const choice = await hostSelect(pi, ctx.ui, 'Session-start defaults — Combo preset', [
|
|
171
|
+
{ label: 'off', description: 'Keep every Tersio mode inactive' },
|
|
172
|
+
{ label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
173
|
+
{ label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
|
|
174
|
+
{ label: 'max', description: 'caveman=ultra, rtk=on, ponytail=ultra' },
|
|
175
|
+
], { selectionMarker: 'radio', helpText: 'You can change this later with /tersio settings.' });
|
|
176
|
+
const level = choice || 'off';
|
|
177
|
+
if (saveComboSetup(level) && level !== 'off') {
|
|
178
|
+
persistPreset(level);
|
|
179
|
+
useState(setSharedComboLevel(level), ctx);
|
|
180
|
+
ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
169
184
|
pi.on('session_start', async (_event, ctx) => {
|
|
170
185
|
track(ctx);
|
|
171
186
|
if (!ctx?.hasUI) syncStatus(ctx);
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
// combo bar never paints on sessions that already carry other entries.
|
|
187
|
+
await runFirstRunSetup(ctx);
|
|
188
|
+
// The configured default applies only when no persisted mode state exists.
|
|
175
189
|
const entries = sessionEntries(ctx);
|
|
176
190
|
const hasModeState = entries.some((e) => e?.type === 'custom' && (
|
|
177
191
|
e.customType === 'combo-level' || e.customType === 'caveman-mode' ||
|
|
@@ -184,10 +198,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
184
198
|
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
185
199
|
}
|
|
186
200
|
}
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// ponytail-mode entry. Skipped when it matches the active preset — the
|
|
190
|
-
// preset entry already covers that, no redundant write.
|
|
201
|
+
// No sibling restores a standalone ponytail default, so apply it here; skip
|
|
202
|
+
// it when the active preset already writes the same mode.
|
|
191
203
|
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
192
204
|
const ponytailFallback = readPonytailDefault();
|
|
193
205
|
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
@@ -198,8 +210,9 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
198
210
|
});
|
|
199
211
|
|
|
200
212
|
for (const event of ['session_branch', 'session_tree', 'agent_start']) {
|
|
201
|
-
pi
|
|
213
|
+
onHostEvent(pi, event, async (_event, ctx) => {
|
|
202
214
|
track(ctx);
|
|
215
|
+
if (event === 'agent_start') await runFirstRunSetup(ctx);
|
|
203
216
|
});
|
|
204
217
|
}
|
|
205
218
|
|
|
@@ -209,8 +222,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
209
222
|
|
|
210
223
|
const mode = getSharedComboState().ponytail;
|
|
211
224
|
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE')) return;
|
|
212
|
-
const
|
|
213
|
-
return
|
|
225
|
+
const instructions = await loadPonytailInstructions(mode);
|
|
226
|
+
return injectPromptText(pi, event, instructions);
|
|
214
227
|
});
|
|
215
228
|
|
|
216
229
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|
package/extensions/lib/utils.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
// Shared helpers for the installer and the AI add-ons updater.
|
|
2
|
-
//
|
|
1
|
+
// Shared helpers for the installer and the AI add-ons updater. Node built-ins
|
|
2
|
+
// only — this module must stay dependency-free.
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { createWriteStream, existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
4
|
+
import { accessSync, constants, createWriteStream, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
5
5
|
import fs from 'node:fs/promises';
|
|
6
6
|
import https from 'node:https';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
// Promise.withResolvers is Node 22+; the package supports Node 18+.
|
|
10
|
-
// ponytail: swap for the built-in when engines bumps to >=22.
|
|
11
10
|
export function withResolvers() {
|
|
12
11
|
let resolve;
|
|
13
12
|
let reject;
|
|
@@ -15,7 +14,7 @@ export function withResolvers() {
|
|
|
15
14
|
return { promise, resolve, reject };
|
|
16
15
|
}
|
|
17
16
|
export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
|
|
18
|
-
export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/
|
|
17
|
+
export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/caveman/SKILL.md';
|
|
19
18
|
const RTK_PLATFORM_SPECS = {
|
|
20
19
|
'win32/x64': { triple: 'x86_64-pc-windows-msvc', ext: '.zip', binary: 'rtk.exe' },
|
|
21
20
|
'linux/x64': { triple: 'x86_64-unknown-linux-musl', ext: '.tar.gz', binary: 'rtk' },
|
|
@@ -23,20 +22,59 @@ const RTK_PLATFORM_SPECS = {
|
|
|
23
22
|
'darwin/x64': { triple: 'x86_64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
|
|
24
23
|
'darwin/arm64': { triple: 'aarch64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
|
|
25
24
|
};
|
|
25
|
+
export function resolveRtkBinary(managedDir = path.join(os.homedir(), '.bun', 'bin')) {
|
|
26
|
+
const name = process.platform === 'win32' ? 'rtk.exe' : 'rtk';
|
|
27
|
+
const candidates = (process.env.PATH || '').split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, name));
|
|
28
|
+
if (managedDir)
|
|
29
|
+
candidates.push(path.join(managedDir, name));
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
try {
|
|
32
|
+
if (!statSync(candidate).isFile())
|
|
33
|
+
continue;
|
|
34
|
+
accessSync(candidate, constants.X_OK);
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
catch { /* keep searching */ }
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
26
41
|
export function rtkPlatformSpec(platform = process.platform, arch = process.arch) {
|
|
27
42
|
return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
|
|
28
43
|
}
|
|
29
|
-
//
|
|
44
|
+
// The 3xx+location redirect rule shared by httpsGet/httpsDownload.
|
|
30
45
|
function redirectNext(res, url) {
|
|
31
46
|
if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
32
47
|
return new URL(res.headers.location, url).href;
|
|
33
48
|
}
|
|
34
49
|
return null;
|
|
35
50
|
}
|
|
51
|
+
// pi sets PI_CODING_AGENT; OMP does not. OMP does read PI_CODING_AGENT_DIR as
|
|
52
|
+
// its own relocation variable, so the override only counts under that marker.
|
|
53
|
+
export function isPiProcess() {
|
|
54
|
+
return process.env.PI_CODING_AGENT === 'true';
|
|
55
|
+
}
|
|
56
|
+
export function piAgentDir() {
|
|
57
|
+
const override = process.env.PI_CODING_AGENT_DIR;
|
|
58
|
+
return isPiProcess() && override ? override : path.join(os.homedir(), '.pi', 'agent');
|
|
59
|
+
}
|
|
60
|
+
// Ponytail is a Tersio dependency that npm hoists, so it can sit in Tersio's
|
|
61
|
+
// node_modules, a host's plugin dir, or beside it. Walking up covers all.
|
|
62
|
+
export function findHoistedPackage(pkg, fromDir, ...relInside) {
|
|
63
|
+
let dir = fromDir;
|
|
64
|
+
for (;;) {
|
|
65
|
+
const candidate = path.join(dir, 'node_modules', ...pkg.split('/'), ...relInside);
|
|
66
|
+
if (existsSync(candidate))
|
|
67
|
+
return candidate;
|
|
68
|
+
const parent = path.dirname(dir);
|
|
69
|
+
if (parent === dir)
|
|
70
|
+
return null;
|
|
71
|
+
dir = parent;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
36
74
|
export function httpsGet(url, opts = {}) {
|
|
37
75
|
const { promise, resolve, reject } = withResolvers();
|
|
38
76
|
const maxRedirects = opts.maxRedirects ?? 5;
|
|
39
|
-
const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' } }, (res) => {
|
|
77
|
+
const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' }, signal: opts.signal }, (res) => {
|
|
40
78
|
const next = redirectNext(res, url);
|
|
41
79
|
if (next) {
|
|
42
80
|
if (maxRedirects <= 0) {
|
|
@@ -45,7 +83,7 @@ export function httpsGet(url, opts = {}) {
|
|
|
45
83
|
return;
|
|
46
84
|
}
|
|
47
85
|
res.resume();
|
|
48
|
-
resolve(httpsGet(next, { maxRedirects: maxRedirects - 1 }));
|
|
86
|
+
resolve(httpsGet(next, { maxRedirects: maxRedirects - 1, signal: opts.signal }));
|
|
49
87
|
return;
|
|
50
88
|
}
|
|
51
89
|
if (res.statusCode !== 200) {
|
|
@@ -54,7 +92,7 @@ export function httpsGet(url, opts = {}) {
|
|
|
54
92
|
return;
|
|
55
93
|
}
|
|
56
94
|
let body = '';
|
|
57
|
-
//
|
|
95
|
+
// Streamed accumulation: fine for tens of KB.
|
|
58
96
|
res.setEncoding('utf8');
|
|
59
97
|
res.on('data', (chunk) => { body += chunk; });
|
|
60
98
|
res.on('end', () => resolve(body));
|
|
@@ -97,7 +135,13 @@ export function httpsDownload(url, dest, opts = {}) {
|
|
|
97
135
|
return promise;
|
|
98
136
|
}
|
|
99
137
|
export async function fetchJson(url) {
|
|
100
|
-
|
|
138
|
+
const body = await httpsGet(url);
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(body);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new Error(`${url} did not return JSON`);
|
|
144
|
+
}
|
|
101
145
|
}
|
|
102
146
|
export function sha256Hex(text) {
|
|
103
147
|
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
@@ -150,9 +194,8 @@ export async function readTextIfExists(p) {
|
|
|
150
194
|
return null;
|
|
151
195
|
}
|
|
152
196
|
}
|
|
153
|
-
// Tersio-owned data home: ~/.tersio
|
|
154
|
-
//
|
|
155
|
-
// installs keep their history.
|
|
197
|
+
// Tersio-owned data home: ~/.tersio. First use moves legacy
|
|
198
|
+
// ~/.omp/plugins/tersio-* files over so existing installs keep their history.
|
|
156
199
|
const migratedTersioFiles = new Set();
|
|
157
200
|
export function tersioHome() {
|
|
158
201
|
return path.join(os.homedir(), '.tersio');
|