@krtclcdy/tersio 2.23.0 → 2.24.1
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 +56 -45
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/banner.js +2 -3
- package/cli/common.js +16 -20
- package/cli/currency.js +11 -19
- package/cli/dashboard.js +216 -40
- package/cli/doctor-fix.js +40 -26
- package/cli/doctor.js +41 -41
- package/cli/hosts.js +114 -0
- package/cli/install.js +284 -179
- package/cli/interactive.js +62 -2
- package/cli/layer.js +22 -0
- package/cli/manifest.js +33 -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 +48 -48
- package/cli/reset.js +5 -32
- package/cli/rtk-wiring.js +7 -17
- package/cli/settings.js +3 -8
- package/cli/uninstall.js +162 -52
- package/cli/update.js +19 -27
- package/cli/usage.js +15 -5
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +37 -29
- package/extensions/ai-addons-updater/index.ts +39 -38
- package/extensions/caveman-session/index.js +19 -34
- package/extensions/caveman-session/index.ts +20 -35
- package/extensions/combo-toggle/index.js +47 -49
- package/extensions/combo-toggle/index.ts +42 -58
- package/extensions/lib/utils.js +40 -9
- package/extensions/lib/utils.ts +40 -9
- package/extensions/rtk-session/index.js +34 -38
- package/extensions/rtk-session/index.ts +32 -41
- package/extensions/shared/carbon.js +3 -11
- package/extensions/shared/carbon.ts +4 -11
- package/extensions/shared/host.js +58 -0
- package/extensions/shared/host.ts +94 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +33 -30
- package/extensions/shared/plugin-settings.ts +35 -26
- package/extensions/shared/pricing.js +25 -16
- package/extensions/shared/pricing.ts +27 -17
- package/extensions/shared/rtk-gain.js +6 -22
- package/extensions/shared/rtk-gain.ts +6 -22
- package/extensions/shared/session-state.js +44 -36
- package/extensions/shared/session-state.ts +48 -35
- package/extensions/shared/types.ts +10 -3
- package/extensions/shared/usage-ledger.js +160 -51
- package/extensions/shared/usage-ledger.ts +170 -67
- package/extensions/shared/usage-store.js +178 -24
- package/extensions/shared/usage-store.ts +176 -25
- package/extensions/tersio-commands/index.js +3 -6
- package/extensions/tersio-commands/index.ts +3 -6
- package/package.json +6 -5
- package/tersio.js +4 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
|
@@ -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,
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
|
+
import { activeModesSummary, COMBO_LEVELS, getSharedComboState, isComboPresetActive, normalizeComboLevel, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, statusUi, setSharedComboListener, setSharedComboMode, systemPromptIncludes, themeStatus, } from '../shared/session-state.js';
|
|
13
|
+
import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
|
|
7
14
|
import { isComboSetupComplete, readComboDefault, readPonytailDefault, saveComboSetup, } from '../shared/plugin-settings.js';
|
|
8
|
-
|
|
15
|
+
import { findHoistedPackage } from '../lib/utils.js';
|
|
16
|
+
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
17
|
const PONYTAIL_FALLBACK_INTENSITY = {
|
|
10
18
|
lite: 'Prefer the simplest correct solution.',
|
|
11
19
|
};
|
|
12
|
-
|
|
20
|
+
// Fallback text when Ponytail cannot be found; exported for its own test.
|
|
21
|
+
export function ponytailFallback(mode) {
|
|
13
22
|
const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
|
|
14
23
|
return `🦥 PONYTAIL MODE ACTIVE — level: ${mode}\n${intensity} Understand the path first and fix root causes, not symptoms. Prefer the standard library and YAGNI. Avoid speculative abstractions and dependencies. Preserve correctness. Verify changed behavior.`;
|
|
15
24
|
}
|
|
16
25
|
function levelSummary(state) {
|
|
17
26
|
return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
|
|
18
27
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
// Ponytail is hoisted, so one upward walk covers every install layout. The hook
|
|
29
|
+
// is CommonJS, so both namespace shapes are tried.
|
|
30
|
+
async function loadPonytailInstructions(mode) {
|
|
31
|
+
const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
|
|
32
|
+
if (installed) {
|
|
33
|
+
try {
|
|
34
|
+
const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(installed).href));
|
|
35
|
+
const build = loaded.getPonytailInstructions ?? loaded.default?.getPonytailInstructions;
|
|
36
|
+
if (build)
|
|
37
|
+
return build(mode);
|
|
38
|
+
}
|
|
39
|
+
catch { /* fall through to the built-in text */ }
|
|
25
40
|
}
|
|
26
|
-
catch { }
|
|
27
41
|
return ponytailFallback(mode);
|
|
28
42
|
}
|
|
29
43
|
export default function comboToggleExtension(pi) {
|
|
30
|
-
pi
|
|
44
|
+
setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
|
|
31
45
|
let lastCtx = undefined;
|
|
32
46
|
let setupPrompted = false;
|
|
33
47
|
function syncStatus(ctx) {
|
|
34
48
|
lastCtx = paintableCtx(lastCtx, ctx);
|
|
35
|
-
const
|
|
36
|
-
if (!
|
|
49
|
+
const ui = statusUi(lastCtx);
|
|
50
|
+
if (!ui)
|
|
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
|
+
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
c.ui.setStatus('rtk', undefined);
|
|
58
|
-
c.ui.setStatus('ponytail', undefined);
|
|
63
|
+
ui.setStatus('combo', themeStatus(ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
|
|
64
|
+
// Our bar is canonical for the preset, so clear any sibling bar.
|
|
65
|
+
ui.setStatus('caveman', undefined);
|
|
66
|
+
ui.setStatus('rtk', undefined);
|
|
67
|
+
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,10 +84,8 @@ export default function comboToggleExtension(pi) {
|
|
|
77
84
|
return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
|
|
78
85
|
}
|
|
79
86
|
function listen(ctx) {
|
|
80
|
-
// Stable identity: the bridge set dedupes, so repeated track()/command
|
|
81
|
-
// calls register once instead of stacking duplicate listeners.
|
|
82
87
|
if (ctx?.hasUI)
|
|
83
|
-
setSharedComboListener(useState);
|
|
88
|
+
setSharedComboListener('combo', useState);
|
|
84
89
|
}
|
|
85
90
|
function track(ctx) {
|
|
86
91
|
listen(ctx);
|
|
@@ -119,7 +124,7 @@ export default function comboToggleExtension(pi) {
|
|
|
119
124
|
if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete())
|
|
120
125
|
return;
|
|
121
126
|
setupPrompted = true;
|
|
122
|
-
const choice = await ctx.ui
|
|
127
|
+
const choice = await hostSelect(pi, ctx.ui, 'Session-start defaults — Combo preset', [
|
|
123
128
|
{ label: 'off', description: 'Keep every Tersio mode inactive' },
|
|
124
129
|
{ label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
125
130
|
{ label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
|
|
@@ -137,9 +142,7 @@ export default function comboToggleExtension(pi) {
|
|
|
137
142
|
if (!ctx?.hasUI)
|
|
138
143
|
syncStatus(ctx);
|
|
139
144
|
await runFirstRunSetup(ctx);
|
|
140
|
-
//
|
|
141
|
-
// state exists — unrelated session entries must not block it, or the
|
|
142
|
-
// combo bar never paints on sessions that already carry other entries.
|
|
145
|
+
// The configured default applies only when no persisted mode state exists.
|
|
143
146
|
const entries = sessionEntries(ctx);
|
|
144
147
|
const hasModeState = entries.some((e) => e?.type === 'custom' && (e.customType === 'combo-level' || e.customType === 'caveman-mode' ||
|
|
145
148
|
e.customType === 'rtk-mode' || e.customType === 'ponytail-mode'));
|
|
@@ -151,10 +154,8 @@ export default function comboToggleExtension(pi) {
|
|
|
151
154
|
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
// ponytail-mode entry. Skipped when it matches the active preset — the
|
|
157
|
-
// preset entry already covers that, no redundant write.
|
|
157
|
+
// No sibling restores a standalone ponytail default, so apply it here; skip
|
|
158
|
+
// it when the active preset already writes the same mode.
|
|
158
159
|
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
159
160
|
const ponytailFallback = readPonytailDefault();
|
|
160
161
|
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
@@ -164,7 +165,7 @@ export default function comboToggleExtension(pi) {
|
|
|
164
165
|
}
|
|
165
166
|
});
|
|
166
167
|
for (const event of ['session_branch', 'session_tree', 'agent_start']) {
|
|
167
|
-
pi
|
|
168
|
+
onHostEvent(pi, event, async (_event, ctx) => {
|
|
168
169
|
track(ctx);
|
|
169
170
|
if (event === 'agent_start')
|
|
170
171
|
await runFirstRunSetup(ctx);
|
|
@@ -173,13 +174,10 @@ export default function comboToggleExtension(pi) {
|
|
|
173
174
|
pi.on('before_agent_start', async (event, ctx) => {
|
|
174
175
|
if (ctx?.hasUI)
|
|
175
176
|
reconcile(ctx);
|
|
176
|
-
if (!isOmpSubagentPrompt(event.systemPrompt))
|
|
177
|
-
return;
|
|
178
177
|
const mode = getSharedComboState().ponytail;
|
|
179
178
|
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE'))
|
|
180
179
|
return;
|
|
181
|
-
|
|
182
|
-
return { systemPrompt: [...base, loadPonytailInstructions(mode)] };
|
|
180
|
+
return injectPromptText(pi, event, await loadPonytailInstructions(mode));
|
|
183
181
|
});
|
|
184
182
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|
|
185
183
|
}
|
|
@@ -1,40 +1,41 @@
|
|
|
1
|
-
// /combo session toggle
|
|
2
|
-
// Modes: off | medium | balanced | max
|
|
1
|
+
// /combo session toggle: off | medium | balanced | max.
|
|
3
2
|
|
|
4
|
-
import os from 'node:os';
|
|
5
3
|
import path from 'node:path';
|
|
6
|
-
import {
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
5
|
import {
|
|
8
6
|
activeModesSummary,
|
|
9
|
-
asPromptArray,
|
|
10
7
|
COMBO_LEVELS,
|
|
11
8
|
getSharedComboState,
|
|
12
9
|
isComboPresetActive,
|
|
13
|
-
isOmpSubagentPrompt,
|
|
14
10
|
normalizeComboLevel,
|
|
15
11
|
paintableCtx,
|
|
16
12
|
reconcileSharedComboEntries,
|
|
17
13
|
sessionEntries,
|
|
18
14
|
setSharedComboLevel,
|
|
15
|
+
statusUi,
|
|
19
16
|
setSharedComboListener,
|
|
20
17
|
setSharedComboMode,
|
|
21
18
|
systemPromptIncludes,
|
|
19
|
+
themeStatus,
|
|
22
20
|
} from '../shared/session-state.ts';
|
|
21
|
+
import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
|
|
23
22
|
import {
|
|
24
23
|
isComboSetupComplete,
|
|
25
24
|
readComboDefault,
|
|
26
25
|
readPonytailDefault,
|
|
27
26
|
saveComboSetup,
|
|
28
27
|
} from '../shared/plugin-settings.ts';
|
|
28
|
+
import { findHoistedPackage } from '../lib/utils.ts';
|
|
29
29
|
import type { ComboState, ExtensionApi, ExtensionCtx, SystemPromptEvent } from '../shared/types.ts';
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
32
32
|
|
|
33
33
|
const PONYTAIL_FALLBACK_INTENSITY: Record<string, string> = {
|
|
34
34
|
lite: 'Prefer the simplest correct solution.',
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// Fallback text when Ponytail cannot be found; exported for its own test.
|
|
38
|
+
export function ponytailFallback(mode: string): string {
|
|
38
39
|
const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
|
|
39
40
|
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.`;
|
|
40
41
|
}
|
|
@@ -43,60 +44,52 @@ function levelSummary(state: ComboState): string {
|
|
|
43
44
|
return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} catch { }
|
|
47
|
+
// Ponytail is hoisted, so one upward walk covers every install layout. The hook
|
|
48
|
+
// is CommonJS, so both namespace shapes are tried.
|
|
49
|
+
async function loadPonytailInstructions(mode: string): Promise<string> {
|
|
50
|
+
const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
|
|
51
|
+
if (installed) {
|
|
52
|
+
try {
|
|
53
|
+
const loaded = await import(pathToFileURL(installed).href) as {
|
|
54
|
+
getPonytailInstructions?: (level: string) => string;
|
|
55
|
+
default?: { getPonytailInstructions?: (level: string) => string };
|
|
56
|
+
};
|
|
57
|
+
const build = loaded.getPonytailInstructions ?? loaded.default?.getPonytailInstructions;
|
|
58
|
+
if (build) return build(mode);
|
|
59
|
+
} catch { /* fall through to the built-in text */ }
|
|
60
|
+
}
|
|
61
61
|
return ponytailFallback(mode);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
65
|
-
pi
|
|
65
|
+
setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
|
|
66
66
|
|
|
67
67
|
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
68
68
|
let setupPrompted = false;
|
|
69
69
|
|
|
70
70
|
function syncStatus(ctx?: ExtensionCtx): void {
|
|
71
71
|
lastCtx = paintableCtx(lastCtx, ctx);
|
|
72
|
-
const
|
|
73
|
-
if (!
|
|
74
|
-
//
|
|
75
|
-
// active. In custom/off, the individual bars come back and combo stays clear.
|
|
72
|
+
const ui = statusUi(lastCtx);
|
|
73
|
+
if (!ui) return;
|
|
74
|
+
// One unified bar replaces the per-extension bars while a preset is active.
|
|
76
75
|
if (!isComboPresetActive()) {
|
|
77
|
-
|
|
76
|
+
ui.setStatus('combo', undefined);
|
|
78
77
|
return;
|
|
79
78
|
}
|
|
80
|
-
// Read the live bridge, not
|
|
81
|
-
// reconcile it from persisted entries before this one runs.
|
|
79
|
+
// Read the live bridge, not a cache: siblings reconcile it first.
|
|
82
80
|
const state = getSharedComboState();
|
|
83
|
-
const theme = c.ui.theme;
|
|
84
81
|
const c0 = state.caveman.toUpperCase();
|
|
85
82
|
const r = state.rtk.toUpperCase();
|
|
86
83
|
const p = state.ponytail.toUpperCase();
|
|
87
84
|
const lvl = state.level.toUpperCase();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
c.ui.setStatus('rtk', undefined);
|
|
94
|
-
c.ui.setStatus('ponytail', undefined);
|
|
85
|
+
ui.setStatus('combo', themeStatus(ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
|
|
86
|
+
// Our bar is canonical for the preset, so clear any sibling bar.
|
|
87
|
+
ui.setStatus('caveman', undefined);
|
|
88
|
+
ui.setStatus('rtk', undefined);
|
|
89
|
+
ui.setStatus('ponytail', undefined);
|
|
95
90
|
}
|
|
96
91
|
|
|
97
|
-
//
|
|
98
|
-
// restore from these entries, so the fallback must write them too or the
|
|
99
|
-
// preset evaporates on resume and ponytail never activates.
|
|
92
|
+
// Siblings restore from these entries, so the fallback must write them too.
|
|
100
93
|
function persistPreset(level: string): void {
|
|
101
94
|
const modes = COMBO_LEVELS[level];
|
|
102
95
|
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
@@ -115,9 +108,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
115
108
|
return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
|
|
116
109
|
}
|
|
117
110
|
function listen(ctx?: ExtensionCtx): void {
|
|
118
|
-
|
|
119
|
-
// calls register once instead of stacking duplicate listeners.
|
|
120
|
-
if (ctx?.hasUI) setSharedComboListener(useState);
|
|
111
|
+
if (ctx?.hasUI) setSharedComboListener('combo', useState);
|
|
121
112
|
}
|
|
122
113
|
|
|
123
114
|
function track(ctx?: ExtensionCtx): void {
|
|
@@ -174,7 +165,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
174
165
|
async function runFirstRunSetup(ctx?: ExtensionCtx): Promise<void> {
|
|
175
166
|
if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete()) return;
|
|
176
167
|
setupPrompted = true;
|
|
177
|
-
const choice = await ctx.ui
|
|
168
|
+
const choice = await hostSelect(pi, ctx.ui, 'Session-start defaults — Combo preset', [
|
|
178
169
|
{ label: 'off', description: 'Keep every Tersio mode inactive' },
|
|
179
170
|
{ label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
180
171
|
{ label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
|
|
@@ -192,9 +183,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
192
183
|
track(ctx);
|
|
193
184
|
if (!ctx?.hasUI) syncStatus(ctx);
|
|
194
185
|
await runFirstRunSetup(ctx);
|
|
195
|
-
//
|
|
196
|
-
// state exists — unrelated session entries must not block it, or the
|
|
197
|
-
// combo bar never paints on sessions that already carry other entries.
|
|
186
|
+
// The configured default applies only when no persisted mode state exists.
|
|
198
187
|
const entries = sessionEntries(ctx);
|
|
199
188
|
const hasModeState = entries.some((e) => e?.type === 'custom' && (
|
|
200
189
|
e.customType === 'combo-level' || e.customType === 'caveman-mode' ||
|
|
@@ -207,10 +196,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
207
196
|
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
208
197
|
}
|
|
209
198
|
}
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// ponytail-mode entry. Skipped when it matches the active preset — the
|
|
213
|
-
// preset entry already covers that, no redundant write.
|
|
199
|
+
// No sibling restores a standalone ponytail default, so apply it here; skip
|
|
200
|
+
// it when the active preset already writes the same mode.
|
|
214
201
|
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
215
202
|
const ponytailFallback = readPonytailDefault();
|
|
216
203
|
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
@@ -221,7 +208,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
221
208
|
});
|
|
222
209
|
|
|
223
210
|
for (const event of ['session_branch', 'session_tree', 'agent_start']) {
|
|
224
|
-
pi
|
|
211
|
+
onHostEvent(pi, event, async (_event, ctx) => {
|
|
225
212
|
track(ctx);
|
|
226
213
|
if (event === 'agent_start') await runFirstRunSetup(ctx);
|
|
227
214
|
});
|
|
@@ -229,12 +216,9 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
229
216
|
|
|
230
217
|
pi.on<SystemPromptEvent>('before_agent_start', async (event, ctx) => {
|
|
231
218
|
if (ctx?.hasUI) reconcile(ctx);
|
|
232
|
-
if (!isOmpSubagentPrompt(event.systemPrompt)) return;
|
|
233
|
-
|
|
234
219
|
const mode = getSharedComboState().ponytail;
|
|
235
220
|
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE')) return;
|
|
236
|
-
|
|
237
|
-
return { systemPrompt: [...base, loadPonytailInstructions(mode)] };
|
|
221
|
+
return injectPromptText(pi, event, await loadPonytailInstructions(mode));
|
|
238
222
|
});
|
|
239
223
|
|
|
240
224
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|
package/extensions/lib/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
import { accessSync, constants, createWriteStream, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
5
5
|
import fs from 'node:fs/promises';
|
|
@@ -7,7 +7,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;
|
|
@@ -42,13 +41,36 @@ export function resolveRtkBinary(managedDir = path.join(os.homedir(), '.bun', 'b
|
|
|
42
41
|
export function rtkPlatformSpec(platform = process.platform, arch = process.arch) {
|
|
43
42
|
return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
|
|
44
43
|
}
|
|
45
|
-
//
|
|
44
|
+
// The 3xx+location redirect rule shared by httpsGet/httpsDownload.
|
|
46
45
|
function redirectNext(res, url) {
|
|
47
46
|
if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
48
47
|
return new URL(res.headers.location, url).href;
|
|
49
48
|
}
|
|
50
49
|
return null;
|
|
51
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
|
+
}
|
|
52
74
|
export function httpsGet(url, opts = {}) {
|
|
53
75
|
const { promise, resolve, reject } = withResolvers();
|
|
54
76
|
const maxRedirects = opts.maxRedirects ?? 5;
|
|
@@ -70,7 +92,7 @@ export function httpsGet(url, opts = {}) {
|
|
|
70
92
|
return;
|
|
71
93
|
}
|
|
72
94
|
let body = '';
|
|
73
|
-
//
|
|
95
|
+
// Streamed accumulation: fine for tens of KB.
|
|
74
96
|
res.setEncoding('utf8');
|
|
75
97
|
res.on('data', (chunk) => { body += chunk; });
|
|
76
98
|
res.on('end', () => resolve(body));
|
|
@@ -113,7 +135,13 @@ export function httpsDownload(url, dest, opts = {}) {
|
|
|
113
135
|
return promise;
|
|
114
136
|
}
|
|
115
137
|
export async function fetchJson(url) {
|
|
116
|
-
|
|
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
|
+
}
|
|
117
145
|
}
|
|
118
146
|
export function sha256Hex(text) {
|
|
119
147
|
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
@@ -166,11 +194,14 @@ export async function readTextIfExists(p) {
|
|
|
166
194
|
return null;
|
|
167
195
|
}
|
|
168
196
|
}
|
|
169
|
-
// Tersio-owned data home: ~/.tersio
|
|
170
|
-
//
|
|
171
|
-
// 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.
|
|
172
199
|
const migratedTersioFiles = new Set();
|
|
173
200
|
export function tersioHome() {
|
|
201
|
+
// Matches the other TERSIO_* overrides so a test never reads the real home.
|
|
202
|
+
const override = process.env.TERSIO_HOME;
|
|
203
|
+
if (override && override.trim() !== '')
|
|
204
|
+
return override;
|
|
174
205
|
return path.join(os.homedir(), '.tersio');
|
|
175
206
|
}
|
|
176
207
|
export function tersioDataPath(name, legacy) {
|
package/extensions/lib/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { accessSync, constants, createWriteStream, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
@@ -14,7 +14,6 @@ export interface HttpOptions {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Promise.withResolvers is Node 22+; the package supports Node 18+.
|
|
17
|
-
// ponytail: swap for the built-in when engines bumps to >=22.
|
|
18
17
|
export function withResolvers<T>(): PromiseWithResolvers<T> {
|
|
19
18
|
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
20
19
|
let reject!: (reason?: unknown) => void;
|
|
@@ -66,7 +65,7 @@ export function rtkPlatformSpec(platform: string = process.platform, arch: strin
|
|
|
66
65
|
return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
//
|
|
68
|
+
// The 3xx+location redirect rule shared by httpsGet/httpsDownload.
|
|
70
69
|
function redirectNext(res: { statusCode?: number; headers: { location?: string } }, url: string): string | null {
|
|
71
70
|
if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
72
71
|
return new URL(res.headers.location, url).href;
|
|
@@ -74,6 +73,30 @@ function redirectNext(res: { statusCode?: number; headers: { location?: string }
|
|
|
74
73
|
return null;
|
|
75
74
|
}
|
|
76
75
|
|
|
76
|
+
// pi sets PI_CODING_AGENT; OMP does not. OMP does read PI_CODING_AGENT_DIR as
|
|
77
|
+
// its own relocation variable, so the override only counts under that marker.
|
|
78
|
+
export function isPiProcess(): boolean {
|
|
79
|
+
return process.env.PI_CODING_AGENT === 'true';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function piAgentDir(): string {
|
|
83
|
+
const override = process.env.PI_CODING_AGENT_DIR;
|
|
84
|
+
return isPiProcess() && override ? override : path.join(os.homedir(), '.pi', 'agent');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Ponytail is a Tersio dependency that npm hoists, so it can sit in Tersio's
|
|
88
|
+
// node_modules, a host's plugin dir, or beside it. Walking up covers all.
|
|
89
|
+
export function findHoistedPackage(pkg: string, fromDir: string, ...relInside: string[]): string | null {
|
|
90
|
+
let dir = fromDir;
|
|
91
|
+
for (;;) {
|
|
92
|
+
const candidate = path.join(dir, 'node_modules', ...pkg.split('/'), ...relInside);
|
|
93
|
+
if (existsSync(candidate)) return candidate;
|
|
94
|
+
const parent = path.dirname(dir);
|
|
95
|
+
if (parent === dir) return null;
|
|
96
|
+
dir = parent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
77
100
|
export function httpsGet(url: string, opts: HttpOptions = {}): Promise<string> {
|
|
78
101
|
const { promise, resolve, reject } = withResolvers<string>();
|
|
79
102
|
const maxRedirects = opts.maxRedirects ?? 5;
|
|
@@ -87,7 +110,7 @@ export function httpsGet(url: string, opts: HttpOptions = {}): Promise<string> {
|
|
|
87
110
|
}
|
|
88
111
|
if (res.statusCode !== 200) { res.resume(); reject(new Error(`HTTP ${res.statusCode} for ${url}`)); return; }
|
|
89
112
|
let body = '';
|
|
90
|
-
//
|
|
113
|
+
// Streamed accumulation: fine for tens of KB.
|
|
91
114
|
res.setEncoding('utf8');
|
|
92
115
|
res.on('data', (chunk) => { body += chunk; });
|
|
93
116
|
res.on('end', () => resolve(body));
|
|
@@ -124,7 +147,12 @@ export function httpsDownload(url: string, dest: string, opts: HttpOptions = {})
|
|
|
124
147
|
}
|
|
125
148
|
|
|
126
149
|
export async function fetchJson<T>(url: string): Promise<T> {
|
|
127
|
-
|
|
150
|
+
const body = await httpsGet(url);
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(body) as T;
|
|
153
|
+
} catch {
|
|
154
|
+
throw new Error(`${url} did not return JSON`);
|
|
155
|
+
}
|
|
128
156
|
}
|
|
129
157
|
|
|
130
158
|
export function sha256Hex(text: string): string {
|
|
@@ -172,15 +200,18 @@ export async function readTextIfExists(p: string): Promise<string | null> {
|
|
|
172
200
|
try { return await fs.readFile(p, 'utf8'); } catch { return null; }
|
|
173
201
|
}
|
|
174
202
|
|
|
175
|
-
// Tersio-owned data home: ~/.tersio
|
|
176
|
-
//
|
|
177
|
-
// installs keep their history.
|
|
203
|
+
// Tersio-owned data home: ~/.tersio. First use moves legacy
|
|
204
|
+
// ~/.omp/plugins/tersio-* files over so existing installs keep their history.
|
|
178
205
|
const migratedTersioFiles = new Set<string>();
|
|
179
206
|
|
|
180
207
|
export function tersioHome(): string {
|
|
208
|
+
// Matches the other TERSIO_* overrides so a test never reads the real home.
|
|
209
|
+
const override = process.env.TERSIO_HOME;
|
|
210
|
+
if (override && override.trim() !== '') return override;
|
|
181
211
|
return path.join(os.homedir(), '.tersio');
|
|
182
212
|
}
|
|
183
213
|
|
|
214
|
+
|
|
184
215
|
export function tersioDataPath(name: string, legacy: string): string {
|
|
185
216
|
const dest = path.join(tersioHome(), name);
|
|
186
217
|
if (!migratedTersioFiles.has(name)) {
|