@krtclcdy/tersio 2.22.1 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -46
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/common.js +55 -55
- package/cli/currency.js +8 -12
- package/cli/dashboard.js +76 -49
- package/cli/doctor-fix.js +61 -44
- package/cli/doctor.js +66 -44
- package/cli/hosts.js +118 -0
- package/cli/install.js +187 -143
- package/cli/interactive.js +27 -1
- package/cli/layer.js +24 -0
- package/cli/manifest.js +36 -0
- package/cli/omp-layer.js +51 -0
- package/cli/pi-layer.js +37 -0
- package/cli/pi-wiring.js +92 -0
- package/cli/profile.js +45 -46
- package/cli/reset.js +3 -26
- package/cli/rtk-wiring.js +10 -15
- package/cli/settings.js +1 -1
- package/cli/uninstall.js +135 -44
- package/cli/update.js +24 -30
- package/cli/usage.js +6 -1
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +36 -25
- package/extensions/ai-addons-updater/index.ts +36 -24
- package/extensions/caveman-session/index.js +23 -18
- package/extensions/caveman-session/index.ts +23 -18
- package/extensions/caveman-session/rule.md +90 -0
- package/extensions/combo-toggle/index.js +63 -41
- package/extensions/combo-toggle/index.ts +61 -48
- package/extensions/lib/utils.js +56 -13
- package/extensions/lib/utils.ts +55 -13
- package/extensions/rtk-session/index.js +20 -19
- package/extensions/rtk-session/index.ts +18 -24
- package/extensions/shared/carbon.js +5 -8
- package/extensions/shared/carbon.ts +5 -8
- package/extensions/shared/host.js +68 -0
- package/extensions/shared/host.ts +104 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +63 -16
- package/extensions/shared/plugin-settings.ts +67 -17
- package/extensions/shared/pricing.js +26 -15
- package/extensions/shared/pricing.ts +28 -16
- package/extensions/shared/rtk-gain.js +13 -24
- package/extensions/shared/rtk-gain.ts +14 -25
- package/extensions/shared/session-state.js +26 -28
- package/extensions/shared/session-state.ts +26 -29
- package/extensions/shared/types.ts +15 -3
- package/extensions/shared/usage-ledger.js +136 -38
- package/extensions/shared/usage-ledger.ts +149 -52
- package/extensions/shared/usage-store.js +14 -12
- package/extensions/shared/usage-store.ts +16 -14
- package/extensions/tersio-commands/index.js +2 -1
- package/extensions/tersio-commands/index.ts +2 -1
- package/package.json +15 -7
- package/tersio.js +6 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
- package/extensions/shared/mode-reinforcement.ts +0 -29
package/extensions/lib/utils.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
-
import { createWriteStream, existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
5
|
+
import { accessSync, constants, createWriteStream, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
import https from 'node:https';
|
|
8
8
|
import os from 'node:os';
|
|
@@ -10,10 +10,10 @@ import path from 'node:path';
|
|
|
10
10
|
|
|
11
11
|
export interface HttpOptions {
|
|
12
12
|
maxRedirects?: number;
|
|
13
|
+
signal?: AbortSignal;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
// Promise.withResolvers is Node 22+; the package supports Node 18+.
|
|
16
|
-
// ponytail: swap for the built-in when engines bumps to >=22.
|
|
17
17
|
export function withResolvers<T>(): PromiseWithResolvers<T> {
|
|
18
18
|
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
19
19
|
let reject!: (reason?: unknown) => void;
|
|
@@ -22,7 +22,7 @@ export function withResolvers<T>(): PromiseWithResolvers<T> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
|
|
25
|
-
export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/
|
|
25
|
+
export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/caveman/SKILL.md';
|
|
26
26
|
|
|
27
27
|
export interface RtkPlatformSpec {
|
|
28
28
|
triple: string;
|
|
@@ -47,12 +47,25 @@ const RTK_PLATFORM_SPECS: Record<string, RtkPlatformSpec> = {
|
|
|
47
47
|
'darwin/x64': { triple: 'x86_64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
|
|
48
48
|
'darwin/arm64': { triple: 'aarch64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
|
|
49
49
|
};
|
|
50
|
+
export function resolveRtkBinary(managedDir = path.join(os.homedir(), '.bun', 'bin')): string | null {
|
|
51
|
+
const name = process.platform === 'win32' ? 'rtk.exe' : 'rtk';
|
|
52
|
+
const candidates = (process.env.PATH || '').split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, name));
|
|
53
|
+
if (managedDir) candidates.push(path.join(managedDir, name));
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
try {
|
|
56
|
+
if (!statSync(candidate).isFile()) continue;
|
|
57
|
+
accessSync(candidate, constants.X_OK);
|
|
58
|
+
return candidate;
|
|
59
|
+
} catch { /* keep searching */ }
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
50
63
|
|
|
51
64
|
export function rtkPlatformSpec(platform: string = process.platform, arch: string = process.arch): RtkPlatformSpec | null {
|
|
52
65
|
return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
|
|
53
66
|
}
|
|
54
67
|
|
|
55
|
-
//
|
|
68
|
+
// The 3xx+location redirect rule shared by httpsGet/httpsDownload.
|
|
56
69
|
function redirectNext(res: { statusCode?: number; headers: { location?: string } }, url: string): string | null {
|
|
57
70
|
if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
58
71
|
return new URL(res.headers.location, url).href;
|
|
@@ -60,20 +73,44 @@ function redirectNext(res: { statusCode?: number; headers: { location?: string }
|
|
|
60
73
|
return null;
|
|
61
74
|
}
|
|
62
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
|
+
|
|
63
100
|
export function httpsGet(url: string, opts: HttpOptions = {}): Promise<string> {
|
|
64
101
|
const { promise, resolve, reject } = withResolvers<string>();
|
|
65
102
|
const maxRedirects = opts.maxRedirects ?? 5;
|
|
66
|
-
const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' } }, (res) => {
|
|
103
|
+
const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' }, signal: opts.signal }, (res) => {
|
|
67
104
|
const next = redirectNext(res, url);
|
|
68
105
|
if (next) {
|
|
69
106
|
if (maxRedirects <= 0) { res.resume(); reject(new Error(`Too many redirects fetching ${url}`)); return; }
|
|
70
107
|
res.resume();
|
|
71
|
-
resolve(httpsGet(next, { maxRedirects: maxRedirects - 1 }));
|
|
108
|
+
resolve(httpsGet(next, { maxRedirects: maxRedirects - 1, signal: opts.signal }));
|
|
72
109
|
return;
|
|
73
110
|
}
|
|
74
111
|
if (res.statusCode !== 200) { res.resume(); reject(new Error(`HTTP ${res.statusCode} for ${url}`)); return; }
|
|
75
112
|
let body = '';
|
|
76
|
-
//
|
|
113
|
+
// Streamed accumulation: fine for tens of KB.
|
|
77
114
|
res.setEncoding('utf8');
|
|
78
115
|
res.on('data', (chunk) => { body += chunk; });
|
|
79
116
|
res.on('end', () => resolve(body));
|
|
@@ -110,7 +147,12 @@ export function httpsDownload(url: string, dest: string, opts: HttpOptions = {})
|
|
|
110
147
|
}
|
|
111
148
|
|
|
112
149
|
export async function fetchJson<T>(url: string): Promise<T> {
|
|
113
|
-
|
|
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
|
+
}
|
|
114
156
|
}
|
|
115
157
|
|
|
116
158
|
export function sha256Hex(text: string): string {
|
|
@@ -158,15 +200,15 @@ export async function readTextIfExists(p: string): Promise<string | null> {
|
|
|
158
200
|
try { return await fs.readFile(p, 'utf8'); } catch { return null; }
|
|
159
201
|
}
|
|
160
202
|
|
|
161
|
-
// Tersio-owned data home: ~/.tersio
|
|
162
|
-
//
|
|
163
|
-
// 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.
|
|
164
205
|
const migratedTersioFiles = new Set<string>();
|
|
165
206
|
|
|
166
207
|
export function tersioHome(): string {
|
|
167
208
|
return path.join(os.homedir(), '.tersio');
|
|
168
209
|
}
|
|
169
210
|
|
|
211
|
+
|
|
170
212
|
export function tersioDataPath(name: string, legacy: string): string {
|
|
171
213
|
const dest = path.join(tersioHome(), name);
|
|
172
214
|
if (!migratedTersioFiles.has(name)) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
|
|
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';
|
|
4
3
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
5
4
|
const DEFAULT_ENABLED = false;
|
|
6
5
|
function asBoolean(value) {
|
|
@@ -15,14 +14,14 @@ function asBoolean(value) {
|
|
|
15
14
|
function resolveEnabled(entries) {
|
|
16
15
|
return lastCustomValue(entries, 'rtk-mode', (data) => asBoolean(data?.enabled));
|
|
17
16
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
function setRtkProcessEnabled(enabled) {
|
|
18
|
+
if (enabled)
|
|
19
|
+
delete process.env.RTK_DISABLED;
|
|
20
|
+
else
|
|
21
|
+
process.env.RTK_DISABLED = '1';
|
|
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.`;
|
|
24
24
|
export default function rtkSessionExtension(pi) {
|
|
25
|
-
const { z } = pi.zod;
|
|
26
25
|
let enabled = DEFAULT_ENABLED;
|
|
27
26
|
let isActive = false;
|
|
28
27
|
let lastCtx = undefined;
|
|
@@ -42,15 +41,16 @@ export default function rtkSessionExtension(pi) {
|
|
|
42
41
|
enabled = Boolean(next);
|
|
43
42
|
pi.appendEntry?.('rtk-mode', { enabled });
|
|
44
43
|
setSharedComboMode('rtk', enabled);
|
|
45
|
-
|
|
44
|
+
setRtkProcessEnabled(enabled);
|
|
46
45
|
const active = activeModesSummary(getSharedComboState());
|
|
47
46
|
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
48
47
|
}
|
|
49
|
-
pi
|
|
48
|
+
setExtensionLabel(pi, 'RTK session toggle');
|
|
50
49
|
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
51
50
|
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
52
51
|
function syncFromShared(state) {
|
|
53
52
|
enabled = state.rtk === 'on';
|
|
53
|
+
setRtkProcessEnabled(enabled);
|
|
54
54
|
syncStatus();
|
|
55
55
|
}
|
|
56
56
|
setSharedComboListener(syncFromShared);
|
|
@@ -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) {
|
|
@@ -89,7 +90,7 @@ export default function rtkSessionExtension(pi) {
|
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
92
|
-
const result = await pi.exec(
|
|
93
|
+
const result = await pi.exec('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
93
94
|
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
94
95
|
return {
|
|
95
96
|
isError: result.code !== 0,
|
|
@@ -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);
|
|
@@ -118,13 +119,14 @@ export default function rtkSessionExtension(pi) {
|
|
|
118
119
|
// installer/user-configured default (off unless configured).
|
|
119
120
|
const persisted = resolveEnabled(entries);
|
|
120
121
|
enabled = typeof persisted === 'boolean' ? persisted : readRtkDefault();
|
|
122
|
+
setRtkProcessEnabled(enabled);
|
|
121
123
|
syncStatus(ctx);
|
|
122
124
|
}
|
|
123
125
|
pi.on('session_start', async (_event, ctx) => {
|
|
124
126
|
restoreEnabled(ctx);
|
|
125
127
|
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
126
128
|
});
|
|
127
|
-
pi
|
|
129
|
+
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
128
130
|
restoreEnabled(ctx);
|
|
129
131
|
});
|
|
130
132
|
pi.on('session_tree', async (_event, ctx) => {
|
|
@@ -142,7 +144,6 @@ export default function rtkSessionExtension(pi) {
|
|
|
142
144
|
const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
|
|
143
145
|
if (!active)
|
|
144
146
|
return;
|
|
145
|
-
|
|
146
|
-
return { systemPrompt: [...base, RTK_PROMPT] };
|
|
147
|
+
return injectPromptText(pi, event, RTK_PROMPT);
|
|
147
148
|
});
|
|
148
149
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.ts';
|
|
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';
|
|
4
3
|
import { readRtkDefault } from '../shared/plugin-settings.ts';
|
|
5
4
|
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
6
5
|
|
|
@@ -17,21 +16,14 @@ function resolveEnabled(entries: SessionEntry[] | null | undefined): boolean | n
|
|
|
17
16
|
return lastCustomValue(entries, 'rtk-mode', (data) => asBoolean(data?.enabled));
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const RTK_PROMPT = `RTK mode active for this session.
|
|
25
|
-
Use Rust Token Killer for shell output that would otherwise be noisy. Prefer explicit RTK commands in bash: \`rtk git status\`, \`rtk git diff\`, \`rtk read <file>\`, \`rtk grep <pattern> <path>\`, \`rtk find <glob> <path>\`, \`rtk test <cmd...>\`, \`rtk tsc\`, \`rtk lint\`, or \`rtk <tool> ...\` for supported dev commands.
|
|
26
|
-
Do not use RTK when exact raw output is required, when a specialized OMP tool is required by system policy, or when the command changes state and RTK would hide important confirmation text. Specialized OMP tools still win: read/glob/grep/edit/lsp stay preferred over shell equivalents.`;
|
|
27
|
-
|
|
28
|
-
interface ZodChain {
|
|
29
|
-
min: (n: number) => ZodChain;
|
|
30
|
-
describe: (text: string) => ZodChain;
|
|
19
|
+
function setRtkProcessEnabled(enabled: boolean): void {
|
|
20
|
+
if (enabled) delete process.env.RTK_DISABLED;
|
|
21
|
+
else process.env.RTK_DISABLED = '1';
|
|
31
22
|
}
|
|
32
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.`;
|
|
25
|
+
|
|
33
26
|
export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
34
|
-
const { z } = pi.zod as { z: { object: (shape: Record<string, unknown>) => Record<string, unknown>; array: (el: unknown) => ZodChain; string: () => ZodChain } };
|
|
35
27
|
let enabled = DEFAULT_ENABLED;
|
|
36
28
|
let isActive = false;
|
|
37
29
|
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
@@ -52,17 +44,18 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
52
44
|
enabled = Boolean(next);
|
|
53
45
|
pi.appendEntry?.('rtk-mode', { enabled });
|
|
54
46
|
setSharedComboMode('rtk', enabled);
|
|
55
|
-
|
|
47
|
+
setRtkProcessEnabled(enabled);
|
|
56
48
|
const active = activeModesSummary(getSharedComboState());
|
|
57
49
|
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
58
50
|
}
|
|
59
51
|
|
|
60
|
-
pi
|
|
52
|
+
setExtensionLabel(pi, 'RTK session toggle');
|
|
61
53
|
|
|
62
54
|
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
63
55
|
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
64
56
|
function syncFromShared(state: { rtk: string }): void {
|
|
65
57
|
enabled = state.rtk === 'on';
|
|
58
|
+
setRtkProcessEnabled(enabled);
|
|
66
59
|
syncStatus();
|
|
67
60
|
}
|
|
68
61
|
setSharedComboListener(syncFromShared);
|
|
@@ -90,8 +83,9 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
90
83
|
name: 'rtk_run',
|
|
91
84
|
label: 'RTK Run',
|
|
92
85
|
description: 'Run the installed `rtk` binary for compact command output when RTK mode is enabled.',
|
|
93
|
-
parameters:
|
|
94
|
-
|
|
86
|
+
parameters: stringArrayToolParams(pi, 'args', {
|
|
87
|
+
minItems: 1,
|
|
88
|
+
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
95
89
|
}),
|
|
96
90
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
97
91
|
if (!enabled) {
|
|
@@ -102,7 +96,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
102
96
|
};
|
|
103
97
|
}
|
|
104
98
|
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
105
|
-
const result = await pi.exec!(
|
|
99
|
+
const result = await pi.exec!('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
106
100
|
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
107
101
|
return {
|
|
108
102
|
isError: result.code !== 0,
|
|
@@ -112,7 +106,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
112
106
|
},
|
|
113
107
|
});
|
|
114
108
|
|
|
115
|
-
|
|
109
|
+
onHostEvent<InputEvent>(pi, 'input', async (event) => {
|
|
116
110
|
if (event?.source === 'extension') return;
|
|
117
111
|
const t = normalizeInputCommand(event?.text);
|
|
118
112
|
if (t === 'rtk on' || t === 'use rtk') setEnabled(true);
|
|
@@ -129,6 +123,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
129
123
|
// installer/user-configured default (off unless configured).
|
|
130
124
|
const persisted = resolveEnabled(entries);
|
|
131
125
|
enabled = typeof persisted === 'boolean' ? persisted : readRtkDefault();
|
|
126
|
+
setRtkProcessEnabled(enabled);
|
|
132
127
|
syncStatus(ctx);
|
|
133
128
|
}
|
|
134
129
|
|
|
@@ -137,7 +132,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
137
132
|
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
138
133
|
});
|
|
139
134
|
|
|
140
|
-
pi
|
|
135
|
+
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
141
136
|
restoreEnabled(ctx);
|
|
142
137
|
});
|
|
143
138
|
|
|
@@ -158,7 +153,6 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
158
153
|
pi.on<SystemPromptEvent>('before_agent_start', async (event) => {
|
|
159
154
|
const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
|
|
160
155
|
if (!active) return;
|
|
161
|
-
|
|
162
|
-
return { systemPrompt: [...base, RTK_PROMPT] };
|
|
156
|
+
return injectPromptText(pi, event, RTK_PROMPT);
|
|
163
157
|
});
|
|
164
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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
|
|
13
|
+
import { asPromptArray } from './session-state.ts';
|
|
14
|
+
import type { ExtensionApi, ExtensionCtx, PromptInjection, SystemPromptEvent, ToolParams, UiApi } from './types.ts';
|
|
15
|
+
|
|
16
|
+
/** The zod subset the one tool schema needs. */
|
|
17
|
+
export interface ZodChain {
|
|
18
|
+
min: (n: number) => ZodChain;
|
|
19
|
+
describe: (text: string) => ZodChain;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ZodFactory {
|
|
23
|
+
z: {
|
|
24
|
+
object: (shape: Record<string, unknown>) => ToolParams;
|
|
25
|
+
array: (element: unknown) => ZodChain;
|
|
26
|
+
string: () => ZodChain;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The host fields these adapters read. */
|
|
31
|
+
export interface HostHandle {
|
|
32
|
+
zod?: unknown;
|
|
33
|
+
setLabel?: (...args: string[]) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isPiHost(pi: HostHandle): boolean {
|
|
37
|
+
return !pi.zod;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setExtensionLabel(pi: HostHandle, label: string): void {
|
|
41
|
+
if (isPiHost(pi)) return;
|
|
42
|
+
pi.setLabel?.(label);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function onHostEvent<E>(pi: ExtensionApi, event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void {
|
|
46
|
+
if (event === 'session_branch' && isPiHost(pi)) return;
|
|
47
|
+
pi.on<E>(event, handler);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SelectOption {
|
|
51
|
+
label: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Returns the option's label on both hosts: pi renders plain strings, so the
|
|
56
|
+
// description rides along inside the label and is stripped back out.
|
|
57
|
+
export async function hostSelect(
|
|
58
|
+
pi: HostHandle,
|
|
59
|
+
ui: UiApi | undefined,
|
|
60
|
+
title: string,
|
|
61
|
+
options: SelectOption[],
|
|
62
|
+
dialogOptions?: Record<string, unknown>,
|
|
63
|
+
): Promise<string | undefined> {
|
|
64
|
+
if (!ui?.select) return undefined;
|
|
65
|
+
if (!isPiHost(pi)) return ui.select(title, options, dialogOptions);
|
|
66
|
+
const shown = options.map((option) => (option.description ? `${option.label} — ${option.description}` : option.label));
|
|
67
|
+
const picked = await ui.select(title, shown, dialogOptions);
|
|
68
|
+
if (picked === undefined) return undefined;
|
|
69
|
+
return options[shown.indexOf(picked)]?.label ?? picked;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// What a `before_agent_start` handler hands back; undefined once pi was
|
|
73
|
+
// updated in place.
|
|
74
|
+
export function injectPromptText(pi: HostHandle, event: SystemPromptEvent, text: string): PromptInjection | undefined {
|
|
75
|
+
if (!isPiHost(pi)) return { systemPrompt: [...asPromptArray(event.systemPrompt), text] };
|
|
76
|
+
// Appending keeps pi's sections and the transcript diff; replacing the
|
|
77
|
+
// prompt the OMP way would drop every section.
|
|
78
|
+
const options = event.systemPromptOptions;
|
|
79
|
+
if (options && typeof options.appendSystemPrompt === 'string') {
|
|
80
|
+
options.appendSystemPrompt = [options.appendSystemPrompt, text].filter(Boolean).join('\n\n');
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return { systemPrompt: `${asPromptArray(event.systemPrompt).join('\n\n')}\n\n${text}` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface StringArraySpec {
|
|
87
|
+
minItems?: number;
|
|
88
|
+
description?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// One string-array argument in the active host's dialect. pi only requires an
|
|
92
|
+
// object, so a JSON Schema literal suffices and no `typebox` import is needed.
|
|
93
|
+
export function stringArrayToolParams(pi: ExtensionApi, name: string, spec: StringArraySpec = {}): ToolParams {
|
|
94
|
+
if (!isPiHost(pi)) {
|
|
95
|
+
const { z } = pi.zod as { z: ZodFactory['z'] };
|
|
96
|
+
const array = z.array(z.string());
|
|
97
|
+
const constrained = spec.minItems ? array.min(spec.minItems) : array;
|
|
98
|
+
return z.object({ [name]: spec.description ? constrained.describe(spec.description) : constrained });
|
|
99
|
+
}
|
|
100
|
+
const array: Record<string, unknown> = { type: 'array', items: { type: 'string' } };
|
|
101
|
+
if (spec.minItems) array.minItems = spec.minItems;
|
|
102
|
+
if (spec.description) array.description = spec.description;
|
|
103
|
+
return { type: 'object', properties: { [name]: array }, required: [name] };
|
|
104
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// extensions/omp/omp-prompt.ts — OMP's system-prompt surface.
|
|
2
|
+
// The subagent marker OMP puts in a delegated agent's prompt.
|
|
3
|
+
export const OMP_SUBAGENT_MARKER = 'You are operating on a piece of work assigned to you by the main agent.';
|
|
4
|
+
export function asPromptArray(systemPrompt) {
|
|
5
|
+
return Array.isArray(systemPrompt) ? systemPrompt : [systemPrompt];
|
|
6
|
+
}
|
|
7
|
+
export function systemPromptIncludes(systemPrompt, marker) {
|
|
8
|
+
return asPromptArray(systemPrompt).some((prompt) => typeof prompt === 'string' && prompt.includes(marker));
|
|
9
|
+
}
|
|
10
|
+
export function isOmpSubagentPrompt(systemPrompt) {
|
|
11
|
+
return systemPromptIncludes(systemPrompt, OMP_SUBAGENT_MARKER);
|
|
12
|
+
}
|