@krtclcdy/tersio 2.23.0 → 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 +58 -45
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/common.js +16 -20
- package/cli/currency.js +8 -12
- package/cli/dashboard.js +49 -37
- package/cli/doctor-fix.js +28 -17
- package/cli/doctor.js +52 -41
- package/cli/hosts.js +118 -0
- package/cli/install.js +180 -117
- 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 +44 -46
- package/cli/reset.js +3 -26
- package/cli/rtk-wiring.js +10 -15
- package/cli/uninstall.js +135 -44
- package/cli/update.js +12 -19
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +31 -24
- package/extensions/ai-addons-updater/index.ts +32 -32
- package/extensions/caveman-session/index.js +6 -6
- package/extensions/caveman-session/index.ts +6 -6
- package/extensions/combo-toggle/index.js +42 -40
- package/extensions/combo-toggle/index.ts +36 -47
- package/extensions/lib/utils.js +36 -9
- package/extensions/lib/utils.ts +37 -9
- package/extensions/rtk-session/index.js +9 -9
- package/extensions/rtk-session/index.ts +9 -14
- 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 +43 -30
- package/extensions/shared/plugin-settings.ts +45 -26
- package/extensions/shared/pricing.js +26 -15
- package/extensions/shared/pricing.ts +28 -16
- package/extensions/shared/rtk-gain.js +11 -22
- package/extensions/shared/rtk-gain.ts +11 -22
- package/extensions/shared/session-state.js +21 -25
- package/extensions/shared/session-state.ts +22 -26
- package/extensions/shared/types.ts +10 -3
- package/extensions/shared/usage-ledger.js +24 -36
- package/extensions/shared/usage-ledger.ts +29 -50
- 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 +4 -2
- package/tersio.js +4 -4
- package/extensions/shared/mode-reinforcement.js +0 -26
|
@@ -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,22 +17,26 @@ import {
|
|
|
19
17
|
setSharedComboListener,
|
|
20
18
|
setSharedComboMode,
|
|
21
19
|
systemPromptIncludes,
|
|
20
|
+
themeStatus,
|
|
22
21
|
} from '../shared/session-state.ts';
|
|
22
|
+
import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
|
|
23
23
|
import {
|
|
24
24
|
isComboSetupComplete,
|
|
25
25
|
readComboDefault,
|
|
26
26
|
readPonytailDefault,
|
|
27
27
|
saveComboSetup,
|
|
28
28
|
} from '../shared/plugin-settings.ts';
|
|
29
|
+
import { findHoistedPackage } from '../lib/utils.ts';
|
|
29
30
|
import type { ComboState, ExtensionApi, ExtensionCtx, SystemPromptEvent } from '../shared/types.ts';
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
+
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
32
33
|
|
|
33
34
|
const PONYTAIL_FALLBACK_INTENSITY: Record<string, string> = {
|
|
34
35
|
lite: 'Prefer the simplest correct solution.',
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
// Fallback text when Ponytail cannot be found; exported for its own test.
|
|
39
|
+
export function ponytailFallback(mode: string): string {
|
|
38
40
|
const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
|
|
39
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.`;
|
|
40
42
|
}
|
|
@@ -43,26 +45,25 @@ function levelSummary(state: ComboState): string {
|
|
|
43
45
|
return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} 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
|
+
}
|
|
61
62
|
return ponytailFallback(mode);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
65
|
-
pi
|
|
66
|
+
setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
|
|
66
67
|
|
|
67
68
|
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
68
69
|
let setupPrompted = false;
|
|
@@ -71,32 +72,25 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
71
72
|
lastCtx = paintableCtx(lastCtx, ctx);
|
|
72
73
|
const c = lastCtx;
|
|
73
74
|
if (!c?.ui?.setStatus) return;
|
|
74
|
-
//
|
|
75
|
-
// 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.
|
|
76
76
|
if (!isComboPresetActive()) {
|
|
77
77
|
c.ui.setStatus('combo', undefined);
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
-
// Read the live bridge, not
|
|
81
|
-
// reconcile it from persisted entries before this one runs.
|
|
80
|
+
// Read the live bridge, not a cache: siblings reconcile it first.
|
|
82
81
|
const state = getSharedComboState();
|
|
83
|
-
const theme = c.ui.theme;
|
|
84
82
|
const c0 = state.caveman.toUpperCase();
|
|
85
83
|
const r = state.rtk.toUpperCase();
|
|
86
84
|
const p = state.ponytail.toUpperCase();
|
|
87
85
|
const lvl = state.level.toUpperCase();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// Clobber any sibling bars another extension may have painted in a race
|
|
91
|
-
// 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.
|
|
92
88
|
c.ui.setStatus('caveman', undefined);
|
|
93
89
|
c.ui.setStatus('rtk', undefined);
|
|
94
90
|
c.ui.setStatus('ponytail', undefined);
|
|
95
91
|
}
|
|
96
92
|
|
|
97
|
-
//
|
|
98
|
-
// restore from these entries, so the fallback must write them too or the
|
|
99
|
-
// preset evaporates on resume and ponytail never activates.
|
|
93
|
+
// Siblings restore from these entries, so the fallback must write them too.
|
|
100
94
|
function persistPreset(level: string): void {
|
|
101
95
|
const modes = COMBO_LEVELS[level];
|
|
102
96
|
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
@@ -115,8 +109,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
115
109
|
return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
|
|
116
110
|
}
|
|
117
111
|
function listen(ctx?: ExtensionCtx): void {
|
|
118
|
-
// Stable identity: the bridge set dedupes, so
|
|
119
|
-
// calls register once instead of stacking duplicate listeners.
|
|
112
|
+
// Stable identity: the bridge set dedupes, so repeat calls register once.
|
|
120
113
|
if (ctx?.hasUI) setSharedComboListener(useState);
|
|
121
114
|
}
|
|
122
115
|
|
|
@@ -174,7 +167,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
174
167
|
async function runFirstRunSetup(ctx?: ExtensionCtx): Promise<void> {
|
|
175
168
|
if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete()) return;
|
|
176
169
|
setupPrompted = true;
|
|
177
|
-
const choice = await ctx.ui
|
|
170
|
+
const choice = await hostSelect(pi, ctx.ui, 'Session-start defaults — Combo preset', [
|
|
178
171
|
{ label: 'off', description: 'Keep every Tersio mode inactive' },
|
|
179
172
|
{ label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
180
173
|
{ label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
|
|
@@ -192,9 +185,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
192
185
|
track(ctx);
|
|
193
186
|
if (!ctx?.hasUI) syncStatus(ctx);
|
|
194
187
|
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.
|
|
188
|
+
// The configured default applies only when no persisted mode state exists.
|
|
198
189
|
const entries = sessionEntries(ctx);
|
|
199
190
|
const hasModeState = entries.some((e) => e?.type === 'custom' && (
|
|
200
191
|
e.customType === 'combo-level' || e.customType === 'caveman-mode' ||
|
|
@@ -207,10 +198,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
207
198
|
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
208
199
|
}
|
|
209
200
|
}
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// ponytail-mode entry. Skipped when it matches the active preset — the
|
|
213
|
-
// 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.
|
|
214
203
|
if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
|
|
215
204
|
const ponytailFallback = readPonytailDefault();
|
|
216
205
|
if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
|
|
@@ -221,7 +210,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
221
210
|
});
|
|
222
211
|
|
|
223
212
|
for (const event of ['session_branch', 'session_tree', 'agent_start']) {
|
|
224
|
-
pi
|
|
213
|
+
onHostEvent(pi, event, async (_event, ctx) => {
|
|
225
214
|
track(ctx);
|
|
226
215
|
if (event === 'agent_start') await runFirstRunSetup(ctx);
|
|
227
216
|
});
|
|
@@ -233,8 +222,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
|
|
|
233
222
|
|
|
234
223
|
const mode = getSharedComboState().ponytail;
|
|
235
224
|
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE')) return;
|
|
236
|
-
const
|
|
237
|
-
return
|
|
225
|
+
const instructions = await loadPonytailInstructions(mode);
|
|
226
|
+
return injectPromptText(pi, event, instructions);
|
|
238
227
|
});
|
|
239
228
|
|
|
240
229
|
// 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,9 +194,8 @@ 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() {
|
|
174
201
|
return path.join(os.homedir(), '.tersio');
|
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,15 @@ 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 {
|
|
181
208
|
return path.join(os.homedir(), '.tersio');
|
|
182
209
|
}
|
|
183
210
|
|
|
211
|
+
|
|
184
212
|
export function tersioDataPath(name: string, legacy: string): string {
|
|
185
213
|
const dest = path.join(tersioHome(), name);
|
|
186
214
|
if (!migratedTersioFiles.has(name)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { activeModesSummary,
|
|
1
|
+
import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
|
|
2
|
+
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.js';
|
|
2
3
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
3
4
|
const DEFAULT_ENABLED = false;
|
|
4
5
|
function asBoolean(value) {
|
|
@@ -21,7 +22,6 @@ function setRtkProcessEnabled(enabled) {
|
|
|
21
22
|
}
|
|
22
23
|
const RTK_PROMPT = `RTK guidance active. RTK automatically rewrites eligible Bash calls through the installed rtk hook. Prefer rtk for noisy shell output, but use exact raw output for state changes, checksums, patches, and diagnostics that need full bytes.`;
|
|
23
24
|
export default function rtkSessionExtension(pi) {
|
|
24
|
-
const { z } = pi.zod;
|
|
25
25
|
let enabled = DEFAULT_ENABLED;
|
|
26
26
|
let isActive = false;
|
|
27
27
|
let lastCtx = undefined;
|
|
@@ -45,7 +45,7 @@ export default function rtkSessionExtension(pi) {
|
|
|
45
45
|
const active = activeModesSummary(getSharedComboState());
|
|
46
46
|
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
47
47
|
}
|
|
48
|
-
pi
|
|
48
|
+
setExtensionLabel(pi, 'RTK session toggle');
|
|
49
49
|
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
50
50
|
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
51
51
|
function syncFromShared(state) {
|
|
@@ -77,8 +77,9 @@ export default function rtkSessionExtension(pi) {
|
|
|
77
77
|
name: 'rtk_run',
|
|
78
78
|
label: 'RTK Run',
|
|
79
79
|
description: 'Run the installed `rtk` binary for compact command output when RTK mode is enabled.',
|
|
80
|
-
parameters:
|
|
81
|
-
|
|
80
|
+
parameters: stringArrayToolParams(pi, 'args', {
|
|
81
|
+
minItems: 1,
|
|
82
|
+
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
82
83
|
}),
|
|
83
84
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
84
85
|
if (!enabled) {
|
|
@@ -98,7 +99,7 @@ export default function rtkSessionExtension(pi) {
|
|
|
98
99
|
};
|
|
99
100
|
},
|
|
100
101
|
});
|
|
101
|
-
pi
|
|
102
|
+
onHostEvent(pi, 'input', async (event) => {
|
|
102
103
|
if (event?.source === 'extension')
|
|
103
104
|
return;
|
|
104
105
|
const t = normalizeInputCommand(event?.text);
|
|
@@ -125,7 +126,7 @@ export default function rtkSessionExtension(pi) {
|
|
|
125
126
|
restoreEnabled(ctx);
|
|
126
127
|
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
127
128
|
});
|
|
128
|
-
pi
|
|
129
|
+
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
129
130
|
restoreEnabled(ctx);
|
|
130
131
|
});
|
|
131
132
|
pi.on('session_tree', async (_event, ctx) => {
|
|
@@ -143,7 +144,6 @@ export default function rtkSessionExtension(pi) {
|
|
|
143
144
|
const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
|
|
144
145
|
if (!active)
|
|
145
146
|
return;
|
|
146
|
-
|
|
147
|
-
return { systemPrompt: [...base, RTK_PROMPT] };
|
|
147
|
+
return injectPromptText(pi, event, RTK_PROMPT);
|
|
148
148
|
});
|
|
149
149
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { activeModesSummary,
|
|
1
|
+
import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.ts';
|
|
2
|
+
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.ts';
|
|
2
3
|
import { readRtkDefault } from '../shared/plugin-settings.ts';
|
|
3
4
|
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
4
5
|
|
|
@@ -22,13 +23,7 @@ function setRtkProcessEnabled(enabled: boolean): void {
|
|
|
22
23
|
|
|
23
24
|
const RTK_PROMPT = `RTK guidance active. RTK automatically rewrites eligible Bash calls through the installed rtk hook. Prefer rtk for noisy shell output, but use exact raw output for state changes, checksums, patches, and diagnostics that need full bytes.`;
|
|
24
25
|
|
|
25
|
-
interface ZodChain {
|
|
26
|
-
min: (n: number) => ZodChain;
|
|
27
|
-
describe: (text: string) => ZodChain;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
26
|
export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
31
|
-
const { z } = pi.zod as { z: { object: (shape: Record<string, unknown>) => Record<string, unknown>; array: (el: unknown) => ZodChain; string: () => ZodChain } };
|
|
32
27
|
let enabled = DEFAULT_ENABLED;
|
|
33
28
|
let isActive = false;
|
|
34
29
|
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
@@ -54,7 +49,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
54
49
|
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
pi
|
|
52
|
+
setExtensionLabel(pi, 'RTK session toggle');
|
|
58
53
|
|
|
59
54
|
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
60
55
|
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
@@ -88,8 +83,9 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
88
83
|
name: 'rtk_run',
|
|
89
84
|
label: 'RTK Run',
|
|
90
85
|
description: 'Run the installed `rtk` binary for compact command output when RTK mode is enabled.',
|
|
91
|
-
parameters:
|
|
92
|
-
|
|
86
|
+
parameters: stringArrayToolParams(pi, 'args', {
|
|
87
|
+
minItems: 1,
|
|
88
|
+
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
93
89
|
}),
|
|
94
90
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
95
91
|
if (!enabled) {
|
|
@@ -110,7 +106,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
110
106
|
},
|
|
111
107
|
});
|
|
112
108
|
|
|
113
|
-
|
|
109
|
+
onHostEvent<InputEvent>(pi, 'input', async (event) => {
|
|
114
110
|
if (event?.source === 'extension') return;
|
|
115
111
|
const t = normalizeInputCommand(event?.text);
|
|
116
112
|
if (t === 'rtk on' || t === 'use rtk') setEnabled(true);
|
|
@@ -136,7 +132,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
136
132
|
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
137
133
|
});
|
|
138
134
|
|
|
139
|
-
pi
|
|
135
|
+
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
140
136
|
restoreEnabled(ctx);
|
|
141
137
|
});
|
|
142
138
|
|
|
@@ -157,7 +153,6 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
157
153
|
pi.on<SystemPromptEvent>('before_agent_start', async (event) => {
|
|
158
154
|
const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
|
|
159
155
|
if (!active) return;
|
|
160
|
-
|
|
161
|
-
return { systemPrompt: [...base, RTK_PROMPT] };
|
|
156
|
+
return injectPromptText(pi, event, RTK_PROMPT);
|
|
162
157
|
});
|
|
163
158
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
// extensions/shared/carbon.ts — CO2/energy footprints, EcoLogits-style.
|
|
2
2
|
//
|
|
3
3
|
// Port of the EcoLogits v0.8.2 LLM impact model (genai-impact/ecologits,
|
|
4
|
-
// MPL-2.0 — see ECO_NOTICE.md)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// otherwise a documented class borrow (never a silent guess — every borrow
|
|
10
|
-
// carries its source). Unknown ids fall to the gpt-4o default. Everything
|
|
11
|
-
// rendered from here must carry ~est. and never merge with measured figures.
|
|
4
|
+
// MPL-2.0 — see ECO_NOTICE.md): the single-stream (batch-1) ceiling divided by
|
|
5
|
+
// serving_concurrency for continuous-batching amortization. Model params come
|
|
6
|
+
// from EcoLogits' registry, else a documented class borrow; unknown ids fall
|
|
7
|
+
// to the gpt-4o default. Render everything from here as ~est., never merged
|
|
8
|
+
// with measured figures.
|
|
12
9
|
// EcoLogits 0.8.2 constants — kWh, kgCO2eq, seconds, GB.
|
|
13
10
|
const Q_BITS = 4;
|
|
14
11
|
const E_ALPHA = 8.91e-8;
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
// extensions/shared/carbon.ts — CO2/energy footprints, EcoLogits-style.
|
|
2
2
|
//
|
|
3
3
|
// Port of the EcoLogits v0.8.2 LLM impact model (genai-impact/ecologits,
|
|
4
|
-
// MPL-2.0 — see ECO_NOTICE.md)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// otherwise a documented class borrow (never a silent guess — every borrow
|
|
10
|
-
// carries its source). Unknown ids fall to the gpt-4o default. Everything
|
|
11
|
-
// rendered from here must carry ~est. and never merge with measured figures.
|
|
4
|
+
// MPL-2.0 — see ECO_NOTICE.md): the single-stream (batch-1) ceiling divided by
|
|
5
|
+
// serving_concurrency for continuous-batching amortization. Model params come
|
|
6
|
+
// from EcoLogits' registry, else a documented class borrow; unknown ids fall
|
|
7
|
+
// to the gpt-4o default. Render everything from here as ~est., never merged
|
|
8
|
+
// with measured figures.
|
|
12
9
|
|
|
13
10
|
export interface ModelCarbon {
|
|
14
11
|
activeB: number;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// extensions/shared/host.ts — one extension source, two agent hosts.
|
|
2
|
+
//
|
|
3
|
+
// OMP is a fork of pi, and six surfaces differ: tool schemas (OMP injects
|
|
4
|
+
// `pi.zod`, pi takes any object it forwards as JSON Schema), `setLabel`
|
|
5
|
+
// (pi's takes an entryId, so an extension name must never reach it),
|
|
6
|
+
// `session_branch` (pi reports that switch as `session_tree`), select options
|
|
7
|
+
// (pi takes plain strings), the status-bar theme (pi has no ctx.ui.theme), and
|
|
8
|
+
// prompt injection (pi appends to its sections rather than replacing them).
|
|
9
|
+
//
|
|
10
|
+
// The host is read from the API shape, never a name or an env marker: OMP is
|
|
11
|
+
// the fork that injects zod, and pi does not, including through the SDK.
|
|
12
|
+
import { asPromptArray } from './session-state.js';
|
|
13
|
+
export function isPiHost(pi) {
|
|
14
|
+
return !pi.zod;
|
|
15
|
+
}
|
|
16
|
+
export function setExtensionLabel(pi, label) {
|
|
17
|
+
if (isPiHost(pi))
|
|
18
|
+
return;
|
|
19
|
+
pi.setLabel?.(label);
|
|
20
|
+
}
|
|
21
|
+
export function onHostEvent(pi, event, handler) {
|
|
22
|
+
if (event === 'session_branch' && isPiHost(pi))
|
|
23
|
+
return;
|
|
24
|
+
pi.on(event, handler);
|
|
25
|
+
}
|
|
26
|
+
// Returns the option's label on both hosts: pi renders plain strings, so the
|
|
27
|
+
// description rides along inside the label and is stripped back out.
|
|
28
|
+
export async function hostSelect(pi, ui, title, options, dialogOptions) {
|
|
29
|
+
if (!ui?.select)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!isPiHost(pi))
|
|
32
|
+
return ui.select(title, options, dialogOptions);
|
|
33
|
+
const shown = options.map((option) => (option.description ? `${option.label} — ${option.description}` : option.label));
|
|
34
|
+
const picked = await ui.select(title, shown, dialogOptions);
|
|
35
|
+
if (picked === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
return options[shown.indexOf(picked)]?.label ?? picked;
|
|
38
|
+
}
|
|
39
|
+
// What a `before_agent_start` handler hands back; undefined once pi was
|
|
40
|
+
// updated in place.
|
|
41
|
+
export function injectPromptText(pi, event, text) {
|
|
42
|
+
if (!isPiHost(pi))
|
|
43
|
+
return { systemPrompt: [...asPromptArray(event.systemPrompt), text] };
|
|
44
|
+
// Appending keeps pi's sections and the transcript diff; replacing the
|
|
45
|
+
// prompt the OMP way would drop every section.
|
|
46
|
+
const options = event.systemPromptOptions;
|
|
47
|
+
if (options && typeof options.appendSystemPrompt === 'string') {
|
|
48
|
+
options.appendSystemPrompt = [options.appendSystemPrompt, text].filter(Boolean).join('\n\n');
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return { systemPrompt: `${asPromptArray(event.systemPrompt).join('\n\n')}\n\n${text}` };
|
|
52
|
+
}
|
|
53
|
+
// One string-array argument in the active host's dialect. pi only requires an
|
|
54
|
+
// object, so a JSON Schema literal suffices and no `typebox` import is needed.
|
|
55
|
+
export function stringArrayToolParams(pi, name, spec = {}) {
|
|
56
|
+
if (!isPiHost(pi)) {
|
|
57
|
+
const { z } = pi.zod;
|
|
58
|
+
const array = z.array(z.string());
|
|
59
|
+
const constrained = spec.minItems ? array.min(spec.minItems) : array;
|
|
60
|
+
return z.object({ [name]: spec.description ? constrained.describe(spec.description) : constrained });
|
|
61
|
+
}
|
|
62
|
+
const array = { type: 'array', items: { type: 'string' } };
|
|
63
|
+
if (spec.minItems)
|
|
64
|
+
array.minItems = spec.minItems;
|
|
65
|
+
if (spec.description)
|
|
66
|
+
array.description = spec.description;
|
|
67
|
+
return { type: 'object', properties: { [name]: array }, required: [name] };
|
|
68
|
+
}
|