@krtclcdy/tersio 2.23.0 → 2.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -45
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/banner.js +2 -3
- package/cli/common.js +16 -20
- package/cli/currency.js +11 -19
- package/cli/dashboard.js +216 -40
- package/cli/doctor-fix.js +40 -26
- package/cli/doctor.js +41 -41
- package/cli/hosts.js +114 -0
- package/cli/install.js +284 -179
- package/cli/interactive.js +62 -2
- package/cli/layer.js +22 -0
- package/cli/manifest.js +33 -0
- package/cli/omp-layer.js +51 -0
- package/cli/pi-layer.js +37 -0
- package/cli/pi-wiring.js +92 -0
- package/cli/profile.js +48 -48
- package/cli/reset.js +5 -32
- package/cli/rtk-wiring.js +7 -17
- package/cli/settings.js +3 -8
- package/cli/uninstall.js +162 -52
- package/cli/update.js +19 -27
- package/cli/usage.js +15 -5
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +37 -29
- package/extensions/ai-addons-updater/index.ts +39 -38
- package/extensions/caveman-session/index.js +19 -34
- package/extensions/caveman-session/index.ts +20 -35
- package/extensions/combo-toggle/index.js +47 -49
- package/extensions/combo-toggle/index.ts +42 -58
- package/extensions/lib/utils.js +40 -9
- package/extensions/lib/utils.ts +40 -9
- package/extensions/rtk-session/index.js +34 -38
- package/extensions/rtk-session/index.ts +32 -41
- package/extensions/shared/carbon.js +3 -11
- package/extensions/shared/carbon.ts +4 -11
- package/extensions/shared/host.js +58 -0
- package/extensions/shared/host.ts +94 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +33 -30
- package/extensions/shared/plugin-settings.ts +35 -26
- package/extensions/shared/pricing.js +25 -16
- package/extensions/shared/pricing.ts +27 -17
- package/extensions/shared/rtk-gain.js +6 -22
- package/extensions/shared/rtk-gain.ts +6 -22
- package/extensions/shared/session-state.js +44 -36
- package/extensions/shared/session-state.ts +48 -35
- package/extensions/shared/types.ts +10 -3
- package/extensions/shared/usage-ledger.js +160 -51
- package/extensions/shared/usage-ledger.ts +170 -67
- package/extensions/shared/usage-store.js +178 -24
- package/extensions/shared/usage-store.ts +176 -25
- package/extensions/tersio-commands/index.js +3 -6
- package/extensions/tersio-commands/index.ts +3 -6
- package/package.json +6 -5
- package/tersio.js +4 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { activeModesSummary,
|
|
1
|
+
import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, statusUi, systemPromptIncludes } 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,21 +22,23 @@ 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;
|
|
28
|
+
function fail(text) {
|
|
29
|
+
return { isError: true, content: [{ type: 'text', text }], details: { enabled } };
|
|
30
|
+
}
|
|
28
31
|
function syncStatus(ctx) {
|
|
29
32
|
lastCtx = paintableCtx(lastCtx, ctx);
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
33
|
+
const ui = statusUi(lastCtx);
|
|
34
|
+
if (!ui)
|
|
32
35
|
return;
|
|
33
36
|
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
34
37
|
if (isComboPresetActive() || !enabled) {
|
|
35
|
-
|
|
38
|
+
ui.setStatus('rtk', undefined);
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
38
|
-
paintStatusBar(
|
|
41
|
+
paintStatusBar(ui, 'rtk', '⚡', 'rtk: ON', isActive);
|
|
39
42
|
}
|
|
40
43
|
function setEnabled(next, ctx) {
|
|
41
44
|
enabled = Boolean(next);
|
|
@@ -45,7 +48,7 @@ export default function rtkSessionExtension(pi) {
|
|
|
45
48
|
const active = activeModesSummary(getSharedComboState());
|
|
46
49
|
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
47
50
|
}
|
|
48
|
-
pi
|
|
51
|
+
setExtensionLabel(pi, 'RTK session toggle');
|
|
49
52
|
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
50
53
|
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
51
54
|
function syncFromShared(state) {
|
|
@@ -53,7 +56,7 @@ export default function rtkSessionExtension(pi) {
|
|
|
53
56
|
setRtkProcessEnabled(enabled);
|
|
54
57
|
syncStatus();
|
|
55
58
|
}
|
|
56
|
-
setSharedComboListener(syncFromShared);
|
|
59
|
+
setSharedComboListener('rtk', syncFromShared);
|
|
57
60
|
pi.registerCommand?.('rtk', {
|
|
58
61
|
description: 'Toggle RTK compact shell-output guidance for this session',
|
|
59
62
|
handler: async (args, ctx) => {
|
|
@@ -77,42 +80,33 @@ export default function rtkSessionExtension(pi) {
|
|
|
77
80
|
name: 'rtk_run',
|
|
78
81
|
label: 'RTK Run',
|
|
79
82
|
description: 'Run the installed `rtk` binary for compact command output when RTK mode is enabled.',
|
|
80
|
-
parameters:
|
|
81
|
-
|
|
83
|
+
parameters: stringArrayToolParams(pi, 'args', {
|
|
84
|
+
minItems: 1,
|
|
85
|
+
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
82
86
|
}),
|
|
83
87
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
84
|
-
if (!enabled)
|
|
88
|
+
if (!enabled)
|
|
89
|
+
return fail('RTK mode is off. Run /rtk on for this session, or use bash explicitly.');
|
|
90
|
+
if (typeof pi.exec !== 'function')
|
|
91
|
+
return fail('This host cannot run rtk.');
|
|
92
|
+
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
93
|
+
try {
|
|
94
|
+
const result = await pi.exec('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
95
|
+
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
85
96
|
return {
|
|
86
|
-
isError:
|
|
87
|
-
content: [{ type: 'text', text:
|
|
88
|
-
details: { enabled },
|
|
97
|
+
isError: result.code !== 0,
|
|
98
|
+
content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
|
|
99
|
+
details: { code: result.code, killed: result.killed, enabled },
|
|
89
100
|
};
|
|
90
101
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
isError: result.code !== 0,
|
|
96
|
-
content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
|
|
97
|
-
details: { code: result.code, killed: result.killed, enabled },
|
|
98
|
-
};
|
|
102
|
+
catch (e) {
|
|
103
|
+
return fail(`rtk could not run: ${e instanceof Error ? e.message : String(e)}`);
|
|
104
|
+
}
|
|
99
105
|
},
|
|
100
106
|
});
|
|
101
|
-
pi.on('input', async (event) => {
|
|
102
|
-
if (event?.source === 'extension')
|
|
103
|
-
return;
|
|
104
|
-
const t = normalizeInputCommand(event?.text);
|
|
105
|
-
if (t === 'rtk on' || t === 'use rtk')
|
|
106
|
-
setEnabled(true);
|
|
107
|
-
if (t === 'rtk off' || t === 'stop rtk')
|
|
108
|
-
setEnabled(false);
|
|
109
|
-
});
|
|
110
107
|
function restoreEnabled(ctx) {
|
|
111
108
|
const entries = sessionEntries(ctx);
|
|
112
|
-
//
|
|
113
|
-
// bar suppression reads the in-process bridge, which is empty in a fresh
|
|
114
|
-
// host until the combo extension reconciles — and its reconcile is
|
|
115
|
-
// UI-gated. Deriving it here makes suppression independent of load order.
|
|
109
|
+
// Derive the bridge here so bar suppression does not depend on load order.
|
|
116
110
|
reconcileSharedComboEntries(entries);
|
|
117
111
|
// Persisted session state wins; a fresh session falls back to the
|
|
118
112
|
// installer/user-configured default (off unless configured).
|
|
@@ -125,7 +119,7 @@ export default function rtkSessionExtension(pi) {
|
|
|
125
119
|
restoreEnabled(ctx);
|
|
126
120
|
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
127
121
|
});
|
|
128
|
-
pi
|
|
122
|
+
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
129
123
|
restoreEnabled(ctx);
|
|
130
124
|
});
|
|
131
125
|
pi.on('session_tree', async (_event, ctx) => {
|
|
@@ -143,7 +137,9 @@ export default function rtkSessionExtension(pi) {
|
|
|
143
137
|
const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
|
|
144
138
|
if (!active)
|
|
145
139
|
return;
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
// Skip when already present, so a host that re-presents the prompt cannot stack it.
|
|
141
|
+
if (systemPromptIncludes(event.systemPrompt, RTK_PROMPT))
|
|
142
|
+
return;
|
|
143
|
+
return injectPromptText(pi, event, RTK_PROMPT);
|
|
148
144
|
});
|
|
149
145
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { activeModesSummary,
|
|
1
|
+
import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, statusUi, systemPromptIncludes } 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
|
-
import type { ExtensionApi, ExtensionCtx,
|
|
4
|
+
import type { ExtensionApi, ExtensionCtx, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
4
5
|
|
|
5
6
|
const DEFAULT_ENABLED = false;
|
|
6
7
|
|
|
@@ -22,27 +23,25 @@ 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;
|
|
35
30
|
|
|
31
|
+
function fail(text: string) {
|
|
32
|
+
return { isError: true as const, content: [{ type: 'text' as const, text }], details: { enabled } };
|
|
33
|
+
}
|
|
34
|
+
|
|
36
35
|
function syncStatus(ctx?: ExtensionCtx): void {
|
|
37
36
|
lastCtx = paintableCtx(lastCtx, ctx);
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
37
|
+
const ui = statusUi(lastCtx);
|
|
38
|
+
if (!ui) return;
|
|
40
39
|
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
41
40
|
if (isComboPresetActive() || !enabled) {
|
|
42
|
-
|
|
41
|
+
ui.setStatus('rtk', undefined);
|
|
43
42
|
return;
|
|
44
43
|
}
|
|
45
|
-
paintStatusBar(
|
|
44
|
+
paintStatusBar(ui, 'rtk', '⚡', 'rtk: ON', isActive);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
function setEnabled(next: unknown, ctx?: ExtensionCtx): void {
|
|
@@ -54,7 +53,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
54
53
|
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
pi
|
|
56
|
+
setExtensionLabel(pi, 'RTK session toggle');
|
|
58
57
|
|
|
59
58
|
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
60
59
|
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
@@ -63,7 +62,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
63
62
|
setRtkProcessEnabled(enabled);
|
|
64
63
|
syncStatus();
|
|
65
64
|
}
|
|
66
|
-
setSharedComboListener(syncFromShared);
|
|
65
|
+
setSharedComboListener('rtk', syncFromShared);
|
|
67
66
|
pi.registerCommand?.('rtk', {
|
|
68
67
|
description: 'Toggle RTK compact shell-output guidance for this session',
|
|
69
68
|
handler: async (args, ctx) => {
|
|
@@ -88,40 +87,31 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
88
87
|
name: 'rtk_run',
|
|
89
88
|
label: 'RTK Run',
|
|
90
89
|
description: 'Run the installed `rtk` binary for compact command output when RTK mode is enabled.',
|
|
91
|
-
parameters:
|
|
92
|
-
|
|
90
|
+
parameters: stringArrayToolParams(pi, 'args', {
|
|
91
|
+
minItems: 1,
|
|
92
|
+
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
93
93
|
}),
|
|
94
94
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
95
|
-
if (!enabled)
|
|
95
|
+
if (!enabled) return fail('RTK mode is off. Run /rtk on for this session, or use bash explicitly.');
|
|
96
|
+
if (typeof pi.exec !== 'function') return fail('This host cannot run rtk.');
|
|
97
|
+
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
98
|
+
try {
|
|
99
|
+
const result = await pi.exec('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
100
|
+
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
96
101
|
return {
|
|
97
|
-
isError:
|
|
98
|
-
content: [{ type: 'text', text:
|
|
99
|
-
details: { enabled },
|
|
102
|
+
isError: result.code !== 0,
|
|
103
|
+
content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
|
|
104
|
+
details: { code: result.code, killed: result.killed, enabled },
|
|
100
105
|
};
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return fail(`rtk could not run: ${e instanceof Error ? e.message : String(e)}`);
|
|
101
108
|
}
|
|
102
|
-
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
103
|
-
const result = await pi.exec!('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
104
|
-
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
105
|
-
return {
|
|
106
|
-
isError: result.code !== 0,
|
|
107
|
-
content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
|
|
108
|
-
details: { code: result.code, killed: result.killed, enabled },
|
|
109
|
-
};
|
|
110
109
|
},
|
|
111
110
|
});
|
|
112
111
|
|
|
113
|
-
pi.on<InputEvent>('input', async (event) => {
|
|
114
|
-
if (event?.source === 'extension') return;
|
|
115
|
-
const t = normalizeInputCommand(event?.text);
|
|
116
|
-
if (t === 'rtk on' || t === 'use rtk') setEnabled(true);
|
|
117
|
-
if (t === 'rtk off' || t === 'stop rtk') setEnabled(false);
|
|
118
|
-
});
|
|
119
112
|
function restoreEnabled(ctx?: ExtensionCtx): void {
|
|
120
113
|
const entries = sessionEntries(ctx);
|
|
121
|
-
//
|
|
122
|
-
// bar suppression reads the in-process bridge, which is empty in a fresh
|
|
123
|
-
// host until the combo extension reconciles — and its reconcile is
|
|
124
|
-
// UI-gated. Deriving it here makes suppression independent of load order.
|
|
114
|
+
// Derive the bridge here so bar suppression does not depend on load order.
|
|
125
115
|
reconcileSharedComboEntries(entries);
|
|
126
116
|
// Persisted session state wins; a fresh session falls back to the
|
|
127
117
|
// installer/user-configured default (off unless configured).
|
|
@@ -136,7 +126,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
136
126
|
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
137
127
|
});
|
|
138
128
|
|
|
139
|
-
pi
|
|
129
|
+
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
140
130
|
restoreEnabled(ctx);
|
|
141
131
|
});
|
|
142
132
|
|
|
@@ -157,7 +147,8 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
157
147
|
pi.on<SystemPromptEvent>('before_agent_start', async (event) => {
|
|
158
148
|
const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
|
|
159
149
|
if (!active) return;
|
|
160
|
-
|
|
161
|
-
|
|
150
|
+
// Skip when already present, so a host that re-presents the prompt cannot stack it.
|
|
151
|
+
if (systemPromptIncludes(event.systemPrompt, RTK_PROMPT)) return;
|
|
152
|
+
return injectPromptText(pi, event, RTK_PROMPT);
|
|
162
153
|
});
|
|
163
154
|
}
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Port of the EcoLogits v0.8.2 LLM impact model (genai-impact/ecologits,
|
|
4
|
-
// MPL-2.0 — see ECO_NOTICE.md), following the same served-estimate approach
|
|
5
|
-
// as honey-for-devs' hooks/eco.js: the faithful single-stream (batch-1)
|
|
6
|
-
// ceiling divided by serving_concurrency for continuous-batching amortization.
|
|
7
|
-
//
|
|
8
|
-
// Model params come from EcoLogits' registry where it covers the model;
|
|
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.
|
|
1
|
+
// EcoLogits v0.8.2 port; always render as ~est, never merged with measured.
|
|
12
2
|
// EcoLogits 0.8.2 constants — kWh, kgCO2eq, seconds, GB.
|
|
13
3
|
const Q_BITS = 4;
|
|
14
4
|
const E_ALPHA = 8.91e-8;
|
|
@@ -31,6 +21,8 @@ const GRIDS = {
|
|
|
31
21
|
};
|
|
32
22
|
// Serving amortization: single-stream ceiling ÷ concurrency ≈ served impact.
|
|
33
23
|
export const SERVING_CONCURRENCY = 32;
|
|
24
|
+
/** EcoLogits release this port tracks, so a UI can name it instead of guessing. */
|
|
25
|
+
export const ECOLOGITS_VERSION = '0.8.2';
|
|
34
26
|
// Registry values verbatim from EcoLogits' model_repository; borrows cite
|
|
35
27
|
// the public param disclosure they stand in for (MoE active/total).
|
|
36
28
|
const TABLE = [
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Port of the EcoLogits v0.8.2 LLM impact model (genai-impact/ecologits,
|
|
4
|
-
// MPL-2.0 — see ECO_NOTICE.md), following the same served-estimate approach
|
|
5
|
-
// as honey-for-devs' hooks/eco.js: the faithful single-stream (batch-1)
|
|
6
|
-
// ceiling divided by serving_concurrency for continuous-batching amortization.
|
|
7
|
-
//
|
|
8
|
-
// Model params come from EcoLogits' registry where it covers the model;
|
|
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.
|
|
1
|
+
// EcoLogits v0.8.2 port; always render as ~est, never merged with measured.
|
|
12
2
|
|
|
13
3
|
export interface ModelCarbon {
|
|
14
4
|
activeB: number;
|
|
@@ -42,6 +32,9 @@ const GRIDS: Record<string, number> = {
|
|
|
42
32
|
// Serving amortization: single-stream ceiling ÷ concurrency ≈ served impact.
|
|
43
33
|
export const SERVING_CONCURRENCY = 32;
|
|
44
34
|
|
|
35
|
+
/** EcoLogits release this port tracks, so a UI can name it instead of guessing. */
|
|
36
|
+
export const ECOLOGITS_VERSION = '0.8.2';
|
|
37
|
+
|
|
45
38
|
interface RegistryEntry {
|
|
46
39
|
match?: string;
|
|
47
40
|
exact?: string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// One source for both hosts; OMP differs in six APIs, detected by shape not name.
|
|
2
|
+
import { asPromptArray } from './session-state.js';
|
|
3
|
+
export function isPiHost(pi) {
|
|
4
|
+
return !pi.zod;
|
|
5
|
+
}
|
|
6
|
+
export function setExtensionLabel(pi, label) {
|
|
7
|
+
if (isPiHost(pi))
|
|
8
|
+
return;
|
|
9
|
+
pi.setLabel?.(label);
|
|
10
|
+
}
|
|
11
|
+
export function onHostEvent(pi, event, handler) {
|
|
12
|
+
if (event === 'session_branch' && isPiHost(pi))
|
|
13
|
+
return;
|
|
14
|
+
pi.on(event, handler);
|
|
15
|
+
}
|
|
16
|
+
// Returns the option's label on both hosts: pi renders plain strings, so the
|
|
17
|
+
// description rides along inside the label and is stripped back out.
|
|
18
|
+
export async function hostSelect(pi, ui, title, options, dialogOptions) {
|
|
19
|
+
if (!ui?.select)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!isPiHost(pi))
|
|
22
|
+
return ui.select(title, options, dialogOptions);
|
|
23
|
+
const shown = options.map((option) => (option.description ? `${option.label} — ${option.description}` : option.label));
|
|
24
|
+
const picked = await ui.select(title, shown, dialogOptions);
|
|
25
|
+
if (picked === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
return options[shown.indexOf(picked)]?.label ?? picked;
|
|
28
|
+
}
|
|
29
|
+
// What a `before_agent_start` handler hands back; undefined once pi was
|
|
30
|
+
// updated in place.
|
|
31
|
+
export function injectPromptText(pi, event, text) {
|
|
32
|
+
if (!isPiHost(pi))
|
|
33
|
+
return { systemPrompt: [...asPromptArray(event.systemPrompt), text] };
|
|
34
|
+
// Appending keeps pi's sections and the transcript diff; replacing the
|
|
35
|
+
// prompt the OMP way would drop every section.
|
|
36
|
+
const options = event.systemPromptOptions;
|
|
37
|
+
if (options && typeof options.appendSystemPrompt === 'string') {
|
|
38
|
+
options.appendSystemPrompt = [options.appendSystemPrompt, text].filter(Boolean).join('\n\n');
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return { systemPrompt: `${asPromptArray(event.systemPrompt).join('\n\n')}\n\n${text}` };
|
|
42
|
+
}
|
|
43
|
+
// One string-array argument in the active host's dialect. pi only requires an
|
|
44
|
+
// object, so a JSON Schema literal suffices and no `typebox` import is needed.
|
|
45
|
+
export function stringArrayToolParams(pi, name, spec = {}) {
|
|
46
|
+
if (!isPiHost(pi)) {
|
|
47
|
+
const { z } = pi.zod;
|
|
48
|
+
const array = z.array(z.string());
|
|
49
|
+
const constrained = spec.minItems ? array.min(spec.minItems) : array;
|
|
50
|
+
return z.object({ [name]: spec.description ? constrained.describe(spec.description) : constrained });
|
|
51
|
+
}
|
|
52
|
+
const array = { type: 'array', items: { type: 'string' } };
|
|
53
|
+
if (spec.minItems)
|
|
54
|
+
array.minItems = spec.minItems;
|
|
55
|
+
if (spec.description)
|
|
56
|
+
array.description = spec.description;
|
|
57
|
+
return { type: 'object', properties: { [name]: array }, required: [name] };
|
|
58
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// One source for both hosts; OMP differs in six APIs, detected by shape not name.
|
|
2
|
+
|
|
3
|
+
import { asPromptArray } from './session-state.ts';
|
|
4
|
+
import type { ExtensionApi, ExtensionCtx, PromptInjection, SystemPromptEvent, ToolParams, UiApi } from './types.ts';
|
|
5
|
+
|
|
6
|
+
/** The zod subset the one tool schema needs. */
|
|
7
|
+
export interface ZodChain {
|
|
8
|
+
min: (n: number) => ZodChain;
|
|
9
|
+
describe: (text: string) => ZodChain;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ZodFactory {
|
|
13
|
+
z: {
|
|
14
|
+
object: (shape: Record<string, unknown>) => ToolParams;
|
|
15
|
+
array: (element: unknown) => ZodChain;
|
|
16
|
+
string: () => ZodChain;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The host fields these adapters read. */
|
|
21
|
+
export interface HostHandle {
|
|
22
|
+
zod?: unknown;
|
|
23
|
+
setLabel?: (...args: string[]) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isPiHost(pi: HostHandle): boolean {
|
|
27
|
+
return !pi.zod;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setExtensionLabel(pi: HostHandle, label: string): void {
|
|
31
|
+
if (isPiHost(pi)) return;
|
|
32
|
+
pi.setLabel?.(label);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function onHostEvent<E>(pi: ExtensionApi, event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void {
|
|
36
|
+
if (event === 'session_branch' && isPiHost(pi)) return;
|
|
37
|
+
pi.on<E>(event, handler);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SelectOption {
|
|
41
|
+
label: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Returns the option's label on both hosts: pi renders plain strings, so the
|
|
46
|
+
// description rides along inside the label and is stripped back out.
|
|
47
|
+
export async function hostSelect(
|
|
48
|
+
pi: HostHandle,
|
|
49
|
+
ui: UiApi | undefined,
|
|
50
|
+
title: string,
|
|
51
|
+
options: SelectOption[],
|
|
52
|
+
dialogOptions?: Record<string, unknown>,
|
|
53
|
+
): Promise<string | undefined> {
|
|
54
|
+
if (!ui?.select) return undefined;
|
|
55
|
+
if (!isPiHost(pi)) return ui.select(title, options, dialogOptions);
|
|
56
|
+
const shown = options.map((option) => (option.description ? `${option.label} — ${option.description}` : option.label));
|
|
57
|
+
const picked = await ui.select(title, shown, dialogOptions);
|
|
58
|
+
if (picked === undefined) return undefined;
|
|
59
|
+
return options[shown.indexOf(picked)]?.label ?? picked;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// What a `before_agent_start` handler hands back; undefined once pi was
|
|
63
|
+
// updated in place.
|
|
64
|
+
export function injectPromptText(pi: HostHandle, event: SystemPromptEvent, text: string): PromptInjection | undefined {
|
|
65
|
+
if (!isPiHost(pi)) return { systemPrompt: [...asPromptArray(event.systemPrompt), text] };
|
|
66
|
+
// Appending keeps pi's sections and the transcript diff; replacing the
|
|
67
|
+
// prompt the OMP way would drop every section.
|
|
68
|
+
const options = event.systemPromptOptions;
|
|
69
|
+
if (options && typeof options.appendSystemPrompt === 'string') {
|
|
70
|
+
options.appendSystemPrompt = [options.appendSystemPrompt, text].filter(Boolean).join('\n\n');
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return { systemPrompt: `${asPromptArray(event.systemPrompt).join('\n\n')}\n\n${text}` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface StringArraySpec {
|
|
77
|
+
minItems?: number;
|
|
78
|
+
description?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// One string-array argument in the active host's dialect. pi only requires an
|
|
82
|
+
// object, so a JSON Schema literal suffices and no `typebox` import is needed.
|
|
83
|
+
export function stringArrayToolParams(pi: ExtensionApi, name: string, spec: StringArraySpec = {}): ToolParams {
|
|
84
|
+
if (!isPiHost(pi)) {
|
|
85
|
+
const { z } = pi.zod as { z: ZodFactory['z'] };
|
|
86
|
+
const array = z.array(z.string());
|
|
87
|
+
const constrained = spec.minItems ? array.min(spec.minItems) : array;
|
|
88
|
+
return z.object({ [name]: spec.description ? constrained.describe(spec.description) : constrained });
|
|
89
|
+
}
|
|
90
|
+
const array: Record<string, unknown> = { type: 'array', items: { type: 'string' } };
|
|
91
|
+
if (spec.minItems) array.minItems = spec.minItems;
|
|
92
|
+
if (spec.description) array.description = spec.description;
|
|
93
|
+
return { type: 'object', properties: { [name]: array }, required: [name] };
|
|
94
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Reads omp's persisted plugin state (~/.omp/plugins/omp-plugins.lock.json),
|
|
3
|
-
// written by `omp plugin config set @krtclcdy/tersio <key> <value>` and by
|
|
4
|
-
// the installer profile step. Tolerant: any parse failure yields {} so every
|
|
5
|
-
// caller falls back to its own default.
|
|
1
|
+
// One settings file for every host; parse failure yields {} with per-caller defaults.
|
|
6
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
3
|
import os from 'node:os';
|
|
8
4
|
import path from 'node:path';
|
|
5
|
+
import { tersioHome } from '../lib/utils.js';
|
|
9
6
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
10
|
-
|
|
7
|
+
// The one store both hosts read; this module owns its path.
|
|
8
|
+
export function tersioSettingsFile() {
|
|
9
|
+
return path.join(tersioHome(), 'settings.json');
|
|
10
|
+
}
|
|
11
|
+
// Older installs kept these as OMP plugin settings; still read so switching
|
|
12
|
+
// the store does not reset a user's defaults.
|
|
13
|
+
function legacyOmpSettings() {
|
|
11
14
|
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
12
|
-
|
|
15
|
+
for (const p of [
|
|
13
16
|
path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
|
|
14
17
|
path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
|
|
15
|
-
]
|
|
16
|
-
}
|
|
17
|
-
// `settings[PLUGIN_NAME]` in the lock file; empty object when missing.
|
|
18
|
-
export function readPluginSettings() {
|
|
19
|
-
for (const p of lockPaths()) {
|
|
18
|
+
]) {
|
|
20
19
|
if (!existsSync(p))
|
|
21
20
|
continue;
|
|
22
21
|
try {
|
|
@@ -27,7 +26,23 @@ export function readPluginSettings() {
|
|
|
27
26
|
}
|
|
28
27
|
catch { /* tolerate corrupt file */ }
|
|
29
28
|
}
|
|
30
|
-
return
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function readSettingsFile() {
|
|
32
|
+
const p = tersioSettingsFile();
|
|
33
|
+
if (!existsSync(p))
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(p, 'utf8'));
|
|
37
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// The stored settings; empty when the file is missing, corrupt, or empty.
|
|
44
|
+
export function readPluginSettings() {
|
|
45
|
+
return readSettingsFile() ?? legacyOmpSettings() ?? {};
|
|
31
46
|
}
|
|
32
47
|
const COMBO_LEVELS = new Set(['off', 'medium', 'balanced', 'max']);
|
|
33
48
|
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
|
|
@@ -61,25 +76,13 @@ const COMBO_MODE_DEFAULTS = {
|
|
|
61
76
|
export function saveComboSetup(level) {
|
|
62
77
|
if (!(level in COMBO_MODE_DEFAULTS))
|
|
63
78
|
return false;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
lock = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
lock = {};
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
lock.plugins ||= {};
|
|
75
|
-
lock.settings ||= {};
|
|
76
|
-
lock.settings[PLUGIN_NAME] = {
|
|
77
|
-
...lock.settings[PLUGIN_NAME],
|
|
79
|
+
const file = tersioSettingsFile();
|
|
80
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
81
|
+
writeFileSync(file, `${JSON.stringify({
|
|
82
|
+
...readPluginSettings(),
|
|
78
83
|
comboDefault: level,
|
|
79
84
|
...COMBO_MODE_DEFAULTS[level],
|
|
80
85
|
comboSetupComplete: true,
|
|
81
|
-
};
|
|
82
|
-
mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
83
|
-
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf8');
|
|
86
|
+
}, null, 2)}\n`, 'utf8');
|
|
84
87
|
return true;
|
|
85
88
|
}
|