@krtclcdy/tersio 2.24.1 → 2.25.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 +27 -21
- package/cli/banner.js +7 -4
- package/cli/common.js +14 -13
- package/cli/currency.js +1 -2
- package/cli/dashboard.js +57 -14
- package/cli/doctor-fix.js +88 -21
- package/cli/doctor.js +32 -24
- package/cli/hosts.js +19 -18
- package/cli/install.js +123 -52
- package/cli/interactive.js +29 -19
- package/cli/manifest.js +7 -0
- package/cli/profile.js +42 -11
- package/cli/rtk-wiring.js +28 -26
- package/cli/settings.js +37 -8
- package/cli/uninstall.js +40 -7
- package/cli/update.js +22 -14
- package/cli/usage.js +39 -14
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +69 -15
- package/extensions/ai-addons-updater/index.ts +60 -17
- package/extensions/caveman-session/index.js +28 -55
- package/extensions/caveman-session/index.ts +28 -55
- package/extensions/combo-toggle/index.js +40 -58
- package/extensions/combo-toggle/index.ts +38 -67
- package/extensions/lib/utils.js +60 -15
- package/extensions/lib/utils.ts +59 -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 +20 -35
- package/extensions/rtk-session/index.ts +19 -34
- package/extensions/shared/carbon.js +1 -2
- package/extensions/shared/carbon.ts +1 -2
- package/extensions/shared/host.js +95 -46
- package/extensions/shared/host.ts +106 -50
- package/extensions/shared/plugin-settings.js +4 -6
- package/extensions/shared/plugin-settings.ts +4 -6
- package/extensions/shared/pricing.js +79 -25
- package/extensions/shared/pricing.ts +80 -26
- package/extensions/shared/rtk-gain.js +3 -5
- package/extensions/shared/rtk-gain.ts +3 -5
- package/extensions/shared/session-state.js +10 -33
- package/extensions/shared/session-state.ts +12 -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 +190 -56
- package/extensions/shared/usage-ledger.ts +194 -58
- package/extensions/shared/usage-store.js +228 -64
- package/extensions/shared/usage-store.ts +235 -67
- package/extensions/tersio-commands/index.js +4 -9
- package/extensions/tersio-commands/index.ts +4 -9
- package/package.json +8 -2
- package/tersio.js +3 -3
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -22
- 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
|
@@ -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 ui = statusUi(lastCtx);
|
|
50
|
-
if (!ui)
|
|
51
|
-
return;
|
|
52
|
-
// One unified bar replaces the per-extension bars while a preset is active.
|
|
53
|
-
if (!isComboPresetActive()) {
|
|
54
|
-
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
|
-
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);
|
|
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,18 +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
|
-
if (ctx?.hasUI)
|
|
88
|
-
|
|
62
|
+
if (!ctx?.hasUI)
|
|
63
|
+
return;
|
|
64
|
+
setSharedComboListener('combo', (state) => { announceStatus(ctx); return state; });
|
|
89
65
|
}
|
|
90
66
|
function track(ctx) {
|
|
91
67
|
listen(ctx);
|
|
@@ -98,8 +74,8 @@ export default function comboToggleExtension(pi) {
|
|
|
98
74
|
listen(ctx);
|
|
99
75
|
const arg = String(args || '').trim().toLowerCase();
|
|
100
76
|
if (!arg || arg === 'status') {
|
|
101
|
-
|
|
102
|
-
ctx
|
|
77
|
+
reconcile(ctx);
|
|
78
|
+
announceStatus(ctx);
|
|
103
79
|
return;
|
|
104
80
|
}
|
|
105
81
|
if (arg === 'help') {
|
|
@@ -115,9 +91,8 @@ export default function comboToggleExtension(pi) {
|
|
|
115
91
|
return;
|
|
116
92
|
}
|
|
117
93
|
persistPreset(level);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
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);
|
|
121
96
|
},
|
|
122
97
|
});
|
|
123
98
|
async function runFirstRunSetup(ctx) {
|
|
@@ -133,40 +108,39 @@ export default function comboToggleExtension(pi) {
|
|
|
133
108
|
const level = choice || 'off';
|
|
134
109
|
if (saveComboSetup(level) && level !== 'off') {
|
|
135
110
|
persistPreset(level);
|
|
136
|
-
|
|
111
|
+
setSharedComboLevel(level);
|
|
137
112
|
ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
|
|
138
113
|
}
|
|
139
114
|
}
|
|
140
115
|
pi.on('session_start', async (_event, ctx) => {
|
|
141
116
|
track(ctx);
|
|
142
|
-
if (!ctx?.hasUI)
|
|
143
|
-
syncStatus(ctx);
|
|
144
117
|
await runFirstRunSetup(ctx);
|
|
145
118
|
// The configured default applies only when no persisted mode state exists.
|
|
146
119
|
const entries = sessionEntries(ctx);
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
if (getSharedComboState().level === 'off' && !hasModeState) {
|
|
120
|
+
const persisted = hasModeState(entries);
|
|
121
|
+
if (getSharedComboState().level === 'off' && !persisted) {
|
|
150
122
|
const fallback = readComboDefault();
|
|
151
123
|
if (fallback !== 'off') {
|
|
152
124
|
persistPreset(fallback);
|
|
153
|
-
|
|
154
|
-
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
125
|
+
setSharedComboLevel(fallback);
|
|
155
126
|
}
|
|
156
127
|
}
|
|
157
|
-
// No sibling restores a standalone ponytail default, so apply it here; skip
|
|
158
|
-
// 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.
|
|
159
129
|
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
160
130
|
const ponytailFallback = readPonytailDefault();
|
|
161
131
|
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
162
132
|
pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
|
|
163
|
-
|
|
133
|
+
setSharedComboMode('ponytail', ponytailFallback);
|
|
164
134
|
}
|
|
165
135
|
}
|
|
136
|
+
// Announced last, so any default above is reflected.
|
|
137
|
+
announceStatus(ctx);
|
|
166
138
|
});
|
|
167
|
-
|
|
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']) {
|
|
168
141
|
onHostEvent(pi, event, async (_event, ctx) => {
|
|
169
142
|
track(ctx);
|
|
143
|
+
announceStatus(ctx);
|
|
170
144
|
if (event === 'agent_start')
|
|
171
145
|
await runFirstRunSetup(ctx);
|
|
172
146
|
});
|
|
@@ -175,9 +149,17 @@ export default function comboToggleExtension(pi) {
|
|
|
175
149
|
if (ctx?.hasUI)
|
|
176
150
|
reconcile(ctx);
|
|
177
151
|
const mode = getSharedComboState().ponytail;
|
|
178
|
-
|
|
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}`))
|
|
179
157
|
return;
|
|
180
|
-
|
|
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);
|
|
181
163
|
});
|
|
182
164
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|
|
183
165
|
}
|
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
// /combo session toggle: off | medium | balanced | max.
|
|
2
2
|
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import {
|
|
6
7
|
activeModesSummary,
|
|
7
8
|
COMBO_LEVELS,
|
|
8
9
|
getSharedComboState,
|
|
9
|
-
|
|
10
|
+
hasModeState,
|
|
10
11
|
normalizeComboLevel,
|
|
11
|
-
paintableCtx,
|
|
12
12
|
reconcileSharedComboEntries,
|
|
13
13
|
sessionEntries,
|
|
14
14
|
setSharedComboLevel,
|
|
15
|
-
statusUi,
|
|
16
15
|
setSharedComboListener,
|
|
17
16
|
setSharedComboMode,
|
|
18
17
|
systemPromptIncludes,
|
|
19
|
-
themeStatus,
|
|
20
18
|
} from '../shared/session-state.ts';
|
|
21
19
|
import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
|
|
20
|
+
import { announceStatus } from '../shared/status.ts';
|
|
22
21
|
import {
|
|
23
22
|
isComboSetupComplete,
|
|
24
23
|
readComboDefault,
|
|
25
24
|
readPonytailDefault,
|
|
26
25
|
saveComboSetup,
|
|
27
26
|
} from '../shared/plugin-settings.ts';
|
|
28
|
-
import { findHoistedPackage } from '../lib/utils.ts';
|
|
27
|
+
import { findHoistedPackage, isPiProcess } from '../lib/utils.ts';
|
|
29
28
|
import type { ComboState, ExtensionApi, ExtensionCtx, SystemPromptEvent } from '../shared/types.ts';
|
|
30
29
|
|
|
31
30
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -40,17 +39,16 @@ export function ponytailFallback(mode: string): string {
|
|
|
40
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.`;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
function levelSummary(state: ComboState): string {
|
|
44
|
-
return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
|
|
45
|
-
}
|
|
46
42
|
|
|
47
|
-
// Ponytail is hoisted, so one upward walk covers every install layout. The hook
|
|
48
|
-
// 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.
|
|
49
44
|
async function loadPonytailInstructions(mode: string): Promise<string> {
|
|
50
45
|
const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
|
|
51
|
-
|
|
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) {
|
|
52
50
|
try {
|
|
53
|
-
const loaded = await import(pathToFileURL(
|
|
51
|
+
const loaded = await import(pathToFileURL(treeLocal).href) as {
|
|
54
52
|
getPonytailInstructions?: (level: string) => string;
|
|
55
53
|
default?: { getPonytailInstructions?: (level: string) => string };
|
|
56
54
|
};
|
|
@@ -64,30 +62,9 @@ async function loadPonytailInstructions(mode: string): Promise<string> {
|
|
|
64
62
|
export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
65
63
|
setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
|
|
66
64
|
|
|
67
|
-
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
68
65
|
let setupPrompted = false;
|
|
66
|
+
let lastInjected: string | undefined = undefined;
|
|
69
67
|
|
|
70
|
-
function syncStatus(ctx?: ExtensionCtx): void {
|
|
71
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
72
|
-
const ui = statusUi(lastCtx);
|
|
73
|
-
if (!ui) return;
|
|
74
|
-
// One unified bar replaces the per-extension bars while a preset is active.
|
|
75
|
-
if (!isComboPresetActive()) {
|
|
76
|
-
ui.setStatus('combo', undefined);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
// Read the live bridge, not a cache: siblings reconcile it first.
|
|
80
|
-
const state = getSharedComboState();
|
|
81
|
-
const c0 = state.caveman.toUpperCase();
|
|
82
|
-
const r = state.rtk.toUpperCase();
|
|
83
|
-
const p = state.ponytail.toUpperCase();
|
|
84
|
-
const lvl = state.level.toUpperCase();
|
|
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);
|
|
90
|
-
}
|
|
91
68
|
|
|
92
69
|
// Siblings restore from these entries, so the fallback must write them too.
|
|
93
70
|
function persistPreset(level: string): void {
|
|
@@ -98,17 +75,13 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
98
75
|
pi.appendEntry?.('combo-level', { level });
|
|
99
76
|
}
|
|
100
77
|
|
|
101
|
-
function useState(state: Readonly<ComboState>, ctx?: ExtensionCtx): Readonly<ComboState> {
|
|
102
|
-
syncStatus(ctx);
|
|
103
|
-
return state;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
78
|
function reconcile(ctx?: ExtensionCtx): Readonly<ComboState> {
|
|
107
79
|
if (!ctx?.hasUI) return getSharedComboState();
|
|
108
|
-
return
|
|
80
|
+
return reconcileSharedComboEntries(sessionEntries(ctx));
|
|
109
81
|
}
|
|
110
82
|
function listen(ctx?: ExtensionCtx): void {
|
|
111
|
-
if (ctx?.hasUI)
|
|
83
|
+
if (!ctx?.hasUI) return;
|
|
84
|
+
setSharedComboListener('combo', (state) => { announceStatus(ctx); return state; });
|
|
112
85
|
}
|
|
113
86
|
|
|
114
87
|
function track(ctx?: ExtensionCtx): void {
|
|
@@ -123,11 +96,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
123
96
|
const arg = String(args || '').trim().toLowerCase();
|
|
124
97
|
|
|
125
98
|
if (!arg || arg === 'status') {
|
|
126
|
-
|
|
127
|
-
ctx
|
|
128
|
-
`Combo: ${state.level === 'custom' ? 'INACTIVE' : state.level.toUpperCase()} (${levelSummary(state)})`,
|
|
129
|
-
'info'
|
|
130
|
-
);
|
|
99
|
+
reconcile(ctx);
|
|
100
|
+
announceStatus(ctx);
|
|
131
101
|
return;
|
|
132
102
|
}
|
|
133
103
|
|
|
@@ -152,13 +122,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
152
122
|
}
|
|
153
123
|
|
|
154
124
|
persistPreset(level);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const active = activeModesSummary(getSharedComboState());
|
|
158
|
-
ctx?.ui?.notify?.(
|
|
159
|
-
level === 'off' ? `Combo off — all tersio modes inactive for this session. Active: ${active}.` : `Combo ${level} on — ${active} active for this session.`,
|
|
160
|
-
'info'
|
|
161
|
-
);
|
|
125
|
+
setSharedComboLevel(level);
|
|
126
|
+
announceStatus(ctx);
|
|
162
127
|
},
|
|
163
128
|
});
|
|
164
129
|
|
|
@@ -174,42 +139,41 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
174
139
|
const level = choice || 'off';
|
|
175
140
|
if (saveComboSetup(level) && level !== 'off') {
|
|
176
141
|
persistPreset(level);
|
|
177
|
-
|
|
142
|
+
setSharedComboLevel(level);
|
|
178
143
|
ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
|
|
179
144
|
}
|
|
180
145
|
}
|
|
181
146
|
|
|
182
147
|
pi.on('session_start', async (_event, ctx) => {
|
|
183
148
|
track(ctx);
|
|
184
|
-
if (!ctx?.hasUI) syncStatus(ctx);
|
|
185
149
|
await runFirstRunSetup(ctx);
|
|
186
150
|
// The configured default applies only when no persisted mode state exists.
|
|
187
151
|
const entries = sessionEntries(ctx);
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
e.customType === 'rtk-mode' || e.customType === 'ponytail-mode'));
|
|
191
|
-
if (getSharedComboState().level === 'off' && !hasModeState) {
|
|
152
|
+
const persisted = hasModeState(entries);
|
|
153
|
+
if (getSharedComboState().level === 'off' && !persisted) {
|
|
192
154
|
const fallback = readComboDefault();
|
|
193
155
|
if (fallback !== 'off') {
|
|
194
156
|
persistPreset(fallback);
|
|
195
|
-
|
|
196
|
-
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
157
|
+
setSharedComboLevel(fallback);
|
|
197
158
|
}
|
|
198
159
|
}
|
|
199
|
-
// No sibling restores a standalone ponytail default, so apply it here; skip
|
|
200
|
-
// 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.
|
|
201
161
|
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
202
162
|
const ponytailFallback = readPonytailDefault();
|
|
203
163
|
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
204
164
|
pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
|
|
205
|
-
|
|
165
|
+
setSharedComboMode('ponytail', ponytailFallback);
|
|
206
166
|
}
|
|
207
167
|
}
|
|
168
|
+
// Announced last, so any default above is reflected.
|
|
169
|
+
announceStatus(ctx);
|
|
208
170
|
});
|
|
209
171
|
|
|
210
|
-
|
|
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']) {
|
|
211
174
|
onHostEvent(pi, event, async (_event, ctx) => {
|
|
212
175
|
track(ctx);
|
|
176
|
+
announceStatus(ctx);
|
|
213
177
|
if (event === 'agent_start') await runFirstRunSetup(ctx);
|
|
214
178
|
});
|
|
215
179
|
}
|
|
@@ -217,8 +181,15 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
217
181
|
pi.on<SystemPromptEvent>('before_agent_start', async (event, ctx) => {
|
|
218
182
|
if (ctx?.hasUI) reconcile(ctx);
|
|
219
183
|
const mode = getSharedComboState().ponytail;
|
|
220
|
-
|
|
221
|
-
|
|
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);
|
|
222
193
|
});
|
|
223
194
|
|
|
224
195
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|
package/extensions/lib/utils.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// Shared helpers for the installer and the AI add-ons updater. Node built-ins
|
|
2
|
-
// only — this module must stay dependency-free.
|
|
1
|
+
// Shared helpers for the installer and the AI add-ons updater. Node built-ins only — this module must stay dependency-free.
|
|
3
2
|
import { createHash } from 'node:crypto';
|
|
4
3
|
import { accessSync, constants, createWriteStream, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
5
4
|
import fs from 'node:fs/promises';
|
|
@@ -13,6 +12,11 @@ export function withResolvers() {
|
|
|
13
12
|
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
14
13
|
return { promise, resolve, reject };
|
|
15
14
|
}
|
|
15
|
+
// os.homedir() can ignore a runtime HOME, so a redirected home would read the real one.
|
|
16
|
+
export function homeDir() {
|
|
17
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
18
|
+
return home && home.trim() !== '' ? home : os.homedir();
|
|
19
|
+
}
|
|
16
20
|
export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
|
|
17
21
|
export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/caveman/SKILL.md';
|
|
18
22
|
const RTK_PLATFORM_SPECS = {
|
|
@@ -22,11 +26,13 @@ const RTK_PLATFORM_SPECS = {
|
|
|
22
26
|
'darwin/x64': { triple: 'x86_64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
|
|
23
27
|
'darwin/arm64': { triple: 'aarch64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
|
|
24
28
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
29
|
+
// First executable `name` on PATH, else the managed Bun bin dir.
|
|
30
|
+
function firstExecutable(name, managedDir) {
|
|
31
|
+
const ext = process.platform === 'win32' ? '.cmd' : '';
|
|
32
|
+
const candidates = (process.env.PATH || '').split(path.delimiter).filter(Boolean)
|
|
33
|
+
.flatMap((dir) => [path.join(dir, name + ext), path.join(dir, name)]);
|
|
28
34
|
if (managedDir)
|
|
29
|
-
candidates.push(path.join(managedDir, name));
|
|
35
|
+
candidates.push(path.join(managedDir, name + ext), path.join(managedDir, name));
|
|
30
36
|
for (const candidate of candidates) {
|
|
31
37
|
try {
|
|
32
38
|
if (!statSync(candidate).isFile())
|
|
@@ -38,6 +44,13 @@ export function resolveRtkBinary(managedDir = path.join(os.homedir(), '.bun', 'b
|
|
|
38
44
|
}
|
|
39
45
|
return null;
|
|
40
46
|
}
|
|
47
|
+
export function resolveRtkBinary(managedDir = path.join(homeDir(), '.bun', 'bin')) {
|
|
48
|
+
return firstExecutable(process.platform === 'win32' ? 'rtk.exe' : 'rtk', managedDir);
|
|
49
|
+
}
|
|
50
|
+
// A host binary to scan for its subagent marker.
|
|
51
|
+
export function resolveHostBinary(name, managedDir = path.join(homeDir(), '.bun', 'bin')) {
|
|
52
|
+
return firstExecutable(name, managedDir);
|
|
53
|
+
}
|
|
41
54
|
export function rtkPlatformSpec(platform = process.platform, arch = process.arch) {
|
|
42
55
|
return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
|
|
43
56
|
}
|
|
@@ -48,23 +61,25 @@ function redirectNext(res, url) {
|
|
|
48
61
|
}
|
|
49
62
|
return null;
|
|
50
63
|
}
|
|
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.
|
|
64
|
+
// pi sets PI_CODING_AGENT; OMP does not. OMP does read PI_CODING_AGENT_DIR as its own relocation variable, so the override only counts under that marker.
|
|
53
65
|
export function isPiProcess() {
|
|
54
66
|
return process.env.PI_CODING_AGENT === 'true';
|
|
55
67
|
}
|
|
56
68
|
export function piAgentDir() {
|
|
57
69
|
const override = process.env.PI_CODING_AGENT_DIR;
|
|
58
|
-
return isPiProcess() && override ? override : path.join(
|
|
70
|
+
return isPiProcess() && override ? override : path.join(homeDir(), '.pi', 'agent');
|
|
59
71
|
}
|
|
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.
|
|
72
|
+
// Ponytail is a Tersio dependency that npm hoists, so it can sit in Tersio's node_modules, a host's plugin dir, or beside it. Walking up covers all.
|
|
62
73
|
export function findHoistedPackage(pkg, fromDir, ...relInside) {
|
|
63
74
|
let dir = fromDir;
|
|
64
75
|
for (;;) {
|
|
65
76
|
const candidate = path.join(dir, 'node_modules', ...pkg.split('/'), ...relInside);
|
|
66
77
|
if (existsSync(candidate))
|
|
67
78
|
return candidate;
|
|
79
|
+
// pi installs npm packages under ~/.pi/agent/npm/node_modules.
|
|
80
|
+
const piCandidate = path.join(dir, 'npm', 'node_modules', ...pkg.split('/'), ...relInside);
|
|
81
|
+
if (existsSync(piCandidate))
|
|
82
|
+
return piCandidate;
|
|
68
83
|
const parent = path.dirname(dir);
|
|
69
84
|
if (parent === dir)
|
|
70
85
|
return null;
|
|
@@ -194,15 +209,45 @@ export async function readTextIfExists(p) {
|
|
|
194
209
|
return null;
|
|
195
210
|
}
|
|
196
211
|
}
|
|
197
|
-
//
|
|
198
|
-
|
|
212
|
+
// Chunked so a 200 MiB host binary is never held in memory. `tail` keeps the last needle.length-1 bytes of the previous chunk, because a match can begin near the end of one chunk and finish inside the next.
|
|
213
|
+
export async function fileContains(filePath, needleText) {
|
|
214
|
+
const needle = Buffer.from(needleText, 'utf8');
|
|
215
|
+
if (needle.length === 0)
|
|
216
|
+
return false;
|
|
217
|
+
const keep = needle.length - 1;
|
|
218
|
+
let handle;
|
|
219
|
+
try {
|
|
220
|
+
handle = await fs.open(filePath, 'r');
|
|
221
|
+
const { size } = await handle.stat();
|
|
222
|
+
const chunk = Buffer.allocUnsafe(1 << 20);
|
|
223
|
+
let tail = Buffer.alloc(0);
|
|
224
|
+
for (let position = 0; position < size;) {
|
|
225
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, position);
|
|
226
|
+
if (bytesRead === 0)
|
|
227
|
+
break;
|
|
228
|
+
const window = Buffer.concat([tail, chunk.subarray(0, bytesRead)]);
|
|
229
|
+
if (window.includes(needle))
|
|
230
|
+
return true;
|
|
231
|
+
tail = window.subarray(Math.max(0, window.length - keep));
|
|
232
|
+
position += bytesRead;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await handle?.close().catch(() => { });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Tersio-owned data home: ~/.tersio. First use moves legacy ~/.omp/plugins/tersio-* files over so existing installs keep their history.
|
|
199
244
|
const migratedTersioFiles = new Set();
|
|
200
245
|
export function tersioHome() {
|
|
201
246
|
// Matches the other TERSIO_* overrides so a test never reads the real home.
|
|
202
247
|
const override = process.env.TERSIO_HOME;
|
|
203
248
|
if (override && override.trim() !== '')
|
|
204
249
|
return override;
|
|
205
|
-
return path.join(
|
|
250
|
+
return path.join(homeDir(), '.tersio');
|
|
206
251
|
}
|
|
207
252
|
export function tersioDataPath(name, legacy) {
|
|
208
253
|
const dest = path.join(tersioHome(), name);
|
|
@@ -210,7 +255,7 @@ export function tersioDataPath(name, legacy) {
|
|
|
210
255
|
migratedTersioFiles.add(name);
|
|
211
256
|
try {
|
|
212
257
|
if (!existsSync(dest)) {
|
|
213
|
-
const src = path.join(
|
|
258
|
+
const src = path.join(homeDir(), '.omp', 'plugins', legacy);
|
|
214
259
|
if (existsSync(src)) {
|
|
215
260
|
mkdirSync(tersioHome(), { recursive: true });
|
|
216
261
|
renameSync(src, dest);
|