@krtclcdy/tersio 2.24.1 → 2.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -21
- package/cli/banner.js +7 -4
- package/cli/common.js +14 -13
- package/cli/currency.js +1 -2
- package/cli/dashboard.js +57 -14
- package/cli/doctor-fix.js +88 -21
- package/cli/doctor.js +32 -24
- package/cli/hosts.js +19 -18
- package/cli/install.js +123 -52
- package/cli/interactive.js +29 -19
- package/cli/manifest.js +7 -0
- package/cli/profile.js +42 -11
- package/cli/rtk-wiring.js +28 -26
- package/cli/settings.js +37 -8
- package/cli/uninstall.js +40 -7
- package/cli/update.js +22 -14
- package/cli/usage.js +39 -14
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +69 -15
- package/extensions/ai-addons-updater/index.ts +60 -17
- package/extensions/caveman-session/index.js +28 -55
- package/extensions/caveman-session/index.ts +28 -55
- package/extensions/combo-toggle/index.js +40 -58
- package/extensions/combo-toggle/index.ts +38 -67
- package/extensions/lib/utils.js +60 -15
- package/extensions/lib/utils.ts +59 -15
- package/extensions/opencode/index.js +104 -0
- package/extensions/opencode/server.js +190 -0
- package/extensions/opencode/server.ts +200 -0
- package/extensions/rtk-session/index.js +20 -35
- package/extensions/rtk-session/index.ts +19 -34
- package/extensions/shared/carbon.js +1 -2
- package/extensions/shared/carbon.ts +1 -2
- package/extensions/shared/host.js +95 -46
- package/extensions/shared/host.ts +106 -50
- package/extensions/shared/plugin-settings.js +4 -6
- package/extensions/shared/plugin-settings.ts +4 -6
- package/extensions/shared/pricing.js +79 -25
- package/extensions/shared/pricing.ts +80 -26
- package/extensions/shared/rtk-gain.js +3 -5
- package/extensions/shared/rtk-gain.ts +3 -5
- package/extensions/shared/session-state.js +10 -33
- package/extensions/shared/session-state.ts +12 -38
- package/extensions/shared/status.js +38 -0
- package/extensions/shared/status.ts +45 -0
- package/extensions/shared/types.js +1 -2
- package/extensions/shared/types.ts +2 -2
- package/extensions/shared/usage-ledger.js +190 -56
- package/extensions/shared/usage-ledger.ts +194 -58
- package/extensions/shared/usage-store.js +228 -64
- package/extensions/shared/usage-store.ts +235 -67
- package/extensions/tersio-commands/index.js +4 -9
- package/extensions/tersio-commands/index.ts +4 -9
- package/package.json +8 -2
- package/tersio.js +3 -3
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -22
- package/cli/omp-layer.js +0 -51
- package/cli/pi-layer.js +0 -37
- package/cli/pi-wiring.js +0 -92
- package/extensions/shared/omp-prompt.js +0 -12
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.js';
|
|
2
2
|
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.js';
|
|
3
3
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
4
|
+
import { resolveRtkBinary } from '../lib/utils.js';
|
|
5
|
+
import { announceStatus } from '../shared/status.js';
|
|
4
6
|
const DEFAULT_ENABLED = false;
|
|
5
7
|
function asBoolean(value) {
|
|
6
8
|
if (typeof value === 'boolean')
|
|
@@ -23,38 +25,21 @@ function setRtkProcessEnabled(enabled) {
|
|
|
23
25
|
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
26
|
export default function rtkSessionExtension(pi) {
|
|
25
27
|
let enabled = DEFAULT_ENABLED;
|
|
26
|
-
let isActive = false;
|
|
27
|
-
let lastCtx = undefined;
|
|
28
28
|
function fail(text) {
|
|
29
29
|
return { isError: true, content: [{ type: 'text', text }], details: { enabled } };
|
|
30
30
|
}
|
|
31
|
-
function syncStatus(ctx) {
|
|
32
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
33
|
-
const ui = statusUi(lastCtx);
|
|
34
|
-
if (!ui)
|
|
35
|
-
return;
|
|
36
|
-
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
37
|
-
if (isComboPresetActive() || !enabled) {
|
|
38
|
-
ui.setStatus('rtk', undefined);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
paintStatusBar(ui, 'rtk', '⚡', 'rtk: ON', isActive);
|
|
42
|
-
}
|
|
43
31
|
function setEnabled(next, ctx) {
|
|
44
32
|
enabled = Boolean(next);
|
|
45
33
|
pi.appendEntry?.('rtk-mode', { enabled });
|
|
46
34
|
setSharedComboMode('rtk', enabled);
|
|
47
35
|
setRtkProcessEnabled(enabled);
|
|
48
|
-
|
|
49
|
-
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
36
|
+
announceStatus(ctx);
|
|
50
37
|
}
|
|
51
38
|
setExtensionLabel(pi, 'RTK session toggle');
|
|
52
|
-
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
53
|
-
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
39
|
+
// Live mirror: a /tersio or /combo switch publishes shared state — adopt it at once so the next turn and the rtk_run gate see it, no reload.
|
|
54
40
|
function syncFromShared(state) {
|
|
55
41
|
enabled = state.rtk === 'on';
|
|
56
42
|
setRtkProcessEnabled(enabled);
|
|
57
|
-
syncStatus();
|
|
58
43
|
}
|
|
59
44
|
setSharedComboListener('rtk', syncFromShared);
|
|
60
45
|
pi.registerCommand?.('rtk', {
|
|
@@ -85,13 +70,15 @@ export default function rtkSessionExtension(pi) {
|
|
|
85
70
|
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
86
71
|
}),
|
|
87
72
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
88
|
-
|
|
73
|
+
// A subagent's own flag sits at its start-up default, so honour shared state too.
|
|
74
|
+
if (!(enabled || getSharedComboState().rtk === 'on'))
|
|
89
75
|
return fail('RTK mode is off. Run /rtk on for this session, or use bash explicitly.');
|
|
90
76
|
if (typeof pi.exec !== 'function')
|
|
91
77
|
return fail('This host cannot run rtk.');
|
|
92
78
|
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
93
79
|
try {
|
|
94
|
-
|
|
80
|
+
// A GUI-launched host may lack ~/.bun/bin on PATH, so prefer the resolved path.
|
|
81
|
+
const result = await pi.exec(resolveRtkBinary() ?? 'rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
95
82
|
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
96
83
|
return {
|
|
97
84
|
isError: result.code !== 0,
|
|
@@ -106,18 +93,18 @@ export default function rtkSessionExtension(pi) {
|
|
|
106
93
|
});
|
|
107
94
|
function restoreEnabled(ctx) {
|
|
108
95
|
const entries = sessionEntries(ctx);
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// installer/user-configured default (off unless configured).
|
|
96
|
+
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
97
|
+
if (hasModeState(entries))
|
|
98
|
+
reconcileSharedComboEntries(entries);
|
|
99
|
+
// Persisted session state wins; a fresh session falls back to the installer/user-configured default (off unless configured).
|
|
113
100
|
const persisted = resolveEnabled(entries);
|
|
114
|
-
enabled = typeof persisted === 'boolean' ? persisted : readRtkDefault();
|
|
101
|
+
enabled = typeof persisted === 'boolean' ? persisted : enabled || readRtkDefault();
|
|
115
102
|
setRtkProcessEnabled(enabled);
|
|
116
|
-
|
|
103
|
+
announceStatus(ctx);
|
|
117
104
|
}
|
|
105
|
+
// Start, resume, and branch all land here, so every session type shows it.
|
|
118
106
|
pi.on('session_start', async (_event, ctx) => {
|
|
119
107
|
restoreEnabled(ctx);
|
|
120
|
-
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
121
108
|
});
|
|
122
109
|
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
123
110
|
restoreEnabled(ctx);
|
|
@@ -125,13 +112,11 @@ export default function rtkSessionExtension(pi) {
|
|
|
125
112
|
pi.on('session_tree', async (_event, ctx) => {
|
|
126
113
|
restoreEnabled(ctx);
|
|
127
114
|
});
|
|
128
|
-
pi
|
|
129
|
-
|
|
130
|
-
syncStatus(ctx);
|
|
115
|
+
onHostEvent(pi, 'session_switch', async (_event, ctx) => {
|
|
116
|
+
restoreEnabled(ctx);
|
|
131
117
|
});
|
|
132
|
-
pi
|
|
133
|
-
|
|
134
|
-
syncStatus(ctx);
|
|
118
|
+
onHostEvent(pi, 'session_compact', async (_event, ctx) => {
|
|
119
|
+
restoreEnabled(ctx);
|
|
135
120
|
});
|
|
136
121
|
pi.on('before_agent_start', async (event) => {
|
|
137
122
|
const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.ts';
|
|
2
2
|
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.ts';
|
|
3
3
|
import { readRtkDefault } from '../shared/plugin-settings.ts';
|
|
4
|
+
import { resolveRtkBinary } from '../lib/utils.ts';
|
|
5
|
+
import { announceStatus } from '../shared/status.ts';
|
|
4
6
|
import type { ExtensionApi, ExtensionCtx, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
5
7
|
|
|
6
8
|
const DEFAULT_ENABLED = false;
|
|
@@ -25,42 +27,26 @@ const RTK_PROMPT = `RTK guidance active. RTK automatically rewrites eligible Bas
|
|
|
25
27
|
|
|
26
28
|
export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
27
29
|
let enabled = DEFAULT_ENABLED;
|
|
28
|
-
let isActive = false;
|
|
29
|
-
let lastCtx: ExtensionCtx | undefined = undefined;
|
|
30
30
|
|
|
31
31
|
function fail(text: string) {
|
|
32
32
|
return { isError: true as const, content: [{ type: 'text' as const, text }], details: { enabled } };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function syncStatus(ctx?: ExtensionCtx): void {
|
|
36
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
37
|
-
const ui = statusUi(lastCtx);
|
|
38
|
-
if (!ui) return;
|
|
39
|
-
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
40
|
-
if (isComboPresetActive() || !enabled) {
|
|
41
|
-
ui.setStatus('rtk', undefined);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
paintStatusBar(ui, 'rtk', '⚡', 'rtk: ON', isActive);
|
|
45
|
-
}
|
|
46
35
|
|
|
47
36
|
function setEnabled(next: unknown, ctx?: ExtensionCtx): void {
|
|
48
37
|
enabled = Boolean(next);
|
|
49
38
|
pi.appendEntry?.('rtk-mode', { enabled });
|
|
50
39
|
setSharedComboMode('rtk', enabled);
|
|
51
40
|
setRtkProcessEnabled(enabled);
|
|
52
|
-
|
|
53
|
-
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
41
|
+
announceStatus(ctx);
|
|
54
42
|
}
|
|
55
43
|
|
|
56
44
|
setExtensionLabel(pi, 'RTK session toggle');
|
|
57
45
|
|
|
58
|
-
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
59
|
-
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
46
|
+
// Live mirror: a /tersio or /combo switch publishes shared state — adopt it at once so the next turn and the rtk_run gate see it, no reload.
|
|
60
47
|
function syncFromShared(state: { rtk: string }): void {
|
|
61
48
|
enabled = state.rtk === 'on';
|
|
62
49
|
setRtkProcessEnabled(enabled);
|
|
63
|
-
syncStatus();
|
|
64
50
|
}
|
|
65
51
|
setSharedComboListener('rtk', syncFromShared);
|
|
66
52
|
pi.registerCommand?.('rtk', {
|
|
@@ -92,11 +78,13 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
92
78
|
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
93
79
|
}),
|
|
94
80
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
95
|
-
|
|
81
|
+
// A subagent's own flag sits at its start-up default, so honour shared state too.
|
|
82
|
+
if (!(enabled || getSharedComboState().rtk === 'on')) return fail('RTK mode is off. Run /rtk on for this session, or use bash explicitly.');
|
|
96
83
|
if (typeof pi.exec !== 'function') return fail('This host cannot run rtk.');
|
|
97
84
|
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
98
85
|
try {
|
|
99
|
-
|
|
86
|
+
// A GUI-launched host may lack ~/.bun/bin on PATH, so prefer the resolved path.
|
|
87
|
+
const result = await pi.exec(resolveRtkBinary() ?? 'rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
100
88
|
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
101
89
|
return {
|
|
102
90
|
isError: result.code !== 0,
|
|
@@ -111,19 +99,18 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
111
99
|
|
|
112
100
|
function restoreEnabled(ctx?: ExtensionCtx): void {
|
|
113
101
|
const entries = sessionEntries(ctx);
|
|
114
|
-
//
|
|
115
|
-
reconcileSharedComboEntries(entries);
|
|
116
|
-
// Persisted session state wins; a fresh session falls back to the
|
|
117
|
-
// installer/user-configured default (off unless configured).
|
|
102
|
+
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
103
|
+
if (hasModeState(entries)) reconcileSharedComboEntries(entries);
|
|
104
|
+
// Persisted session state wins; a fresh session falls back to the installer/user-configured default (off unless configured).
|
|
118
105
|
const persisted = resolveEnabled(entries);
|
|
119
|
-
enabled = typeof persisted === 'boolean' ? persisted : readRtkDefault();
|
|
106
|
+
enabled = typeof persisted === 'boolean' ? persisted : enabled || readRtkDefault();
|
|
120
107
|
setRtkProcessEnabled(enabled);
|
|
121
|
-
|
|
108
|
+
announceStatus(ctx);
|
|
122
109
|
}
|
|
123
110
|
|
|
111
|
+
// Start, resume, and branch all land here, so every session type shows it.
|
|
124
112
|
pi.on('session_start', async (_event, ctx) => {
|
|
125
113
|
restoreEnabled(ctx);
|
|
126
|
-
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
127
114
|
});
|
|
128
115
|
|
|
129
116
|
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
@@ -134,14 +121,12 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
134
121
|
restoreEnabled(ctx);
|
|
135
122
|
});
|
|
136
123
|
|
|
137
|
-
pi
|
|
138
|
-
|
|
139
|
-
syncStatus(ctx);
|
|
124
|
+
onHostEvent(pi, 'session_switch', async (_event, ctx) => {
|
|
125
|
+
restoreEnabled(ctx);
|
|
140
126
|
});
|
|
141
127
|
|
|
142
|
-
pi
|
|
143
|
-
|
|
144
|
-
syncStatus(ctx);
|
|
128
|
+
onHostEvent(pi, 'session_compact', async (_event, ctx) => {
|
|
129
|
+
restoreEnabled(ctx);
|
|
145
130
|
});
|
|
146
131
|
|
|
147
132
|
pi.on<SystemPromptEvent>('before_agent_start', async (event) => {
|
|
@@ -23,8 +23,7 @@ const GRIDS = {
|
|
|
23
23
|
export const SERVING_CONCURRENCY = 32;
|
|
24
24
|
/** EcoLogits release this port tracks, so a UI can name it instead of guessing. */
|
|
25
25
|
export const ECOLOGITS_VERSION = '0.8.2';
|
|
26
|
-
// Registry values verbatim from EcoLogits' model_repository; borrows cite
|
|
27
|
-
// the public param disclosure they stand in for (MoE active/total).
|
|
26
|
+
// Registry values verbatim from EcoLogits' model_repository; borrows cite the public param disclosure they stand in for (MoE active/total).
|
|
28
27
|
const TABLE = [
|
|
29
28
|
{ match: 'opus', active: 625.0, total: 2000, provider: 'anthropic', source: 'registry' },
|
|
30
29
|
{ match: 'sonnet', active: 137.5, total: 440, provider: 'anthropic', source: 'registry' },
|
|
@@ -44,8 +44,7 @@ interface RegistryEntry {
|
|
|
44
44
|
source: 'registry' | 'borrowed';
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// Registry values verbatim from EcoLogits' model_repository; borrows cite
|
|
48
|
-
// the public param disclosure they stand in for (MoE active/total).
|
|
47
|
+
// Registry values verbatim from EcoLogits' model_repository; borrows cite the public param disclosure they stand in for (MoE active/total).
|
|
49
48
|
const TABLE: RegistryEntry[] = [
|
|
50
49
|
{ match: 'opus', active: 625.0, total: 2000, provider: 'anthropic', source: 'registry' },
|
|
51
50
|
{ match: 'sonnet', active: 137.5, total: 440, provider: 'anthropic', source: 'registry' },
|
|
@@ -1,58 +1,107 @@
|
|
|
1
|
-
// One source for both hosts;
|
|
1
|
+
// One source for both hosts; each host-specific behaviour lives in one adapter row.
|
|
2
2
|
import { asPromptArray } from './session-state.js';
|
|
3
|
+
const piAdapter = {
|
|
4
|
+
label() { },
|
|
5
|
+
on(pi, event, handler) {
|
|
6
|
+
if (event === 'session_branch')
|
|
7
|
+
return;
|
|
8
|
+
pi.on(event, handler);
|
|
9
|
+
},
|
|
10
|
+
async select(ui, title, options, dialogOptions) {
|
|
11
|
+
if (!ui?.select)
|
|
12
|
+
return undefined;
|
|
13
|
+
const shown = options.map((o) => (o.description ? `${o.label} — ${o.description}` : o.label));
|
|
14
|
+
const picked = await ui.select(title, shown, dialogOptions);
|
|
15
|
+
if (picked === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
return options[shown.indexOf(picked)]?.label ?? picked;
|
|
18
|
+
},
|
|
19
|
+
inject(event, text, staleMarker) {
|
|
20
|
+
const marker = staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
21
|
+
const dropStale = (parts) => (marker ? parts.filter((part) => !part.includes(marker)) : parts);
|
|
22
|
+
const options = event.systemPromptOptions;
|
|
23
|
+
if (options && typeof options.appendSystemPrompt === 'string') {
|
|
24
|
+
options.appendSystemPrompt = dropStale([options.appendSystemPrompt]).concat(text).filter(Boolean).join('\n\n');
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return { systemPrompt: `${dropStale(asPromptArray(event.systemPrompt)).join('\n\n')}\n\n${text}` };
|
|
28
|
+
},
|
|
29
|
+
stringArray(_pi, name, spec) {
|
|
30
|
+
const array = { type: 'array', items: { type: 'string' } };
|
|
31
|
+
if (spec.minItems)
|
|
32
|
+
array.minItems = spec.minItems;
|
|
33
|
+
if (spec.description)
|
|
34
|
+
array.description = spec.description;
|
|
35
|
+
return { type: 'object', properties: { [name]: array }, required: [name] };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const ompAdapter = {
|
|
39
|
+
label(pi, text) { pi.setLabel?.(text); },
|
|
40
|
+
on(pi, event, handler) { pi.on(event, handler); },
|
|
41
|
+
async select(ui, title, options, dialogOptions) {
|
|
42
|
+
if (!ui?.select)
|
|
43
|
+
return undefined;
|
|
44
|
+
return ui.select(title, options, dialogOptions);
|
|
45
|
+
},
|
|
46
|
+
inject(event, text, staleMarker) {
|
|
47
|
+
const marker = staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
48
|
+
const dropStale = (parts) => (marker ? parts.filter((part) => !part.includes(marker)) : parts);
|
|
49
|
+
return { systemPrompt: [...dropStale(asPromptArray(event.systemPrompt)), text] };
|
|
50
|
+
},
|
|
51
|
+
stringArray(pi, name, spec) {
|
|
52
|
+
const { z } = pi.zod;
|
|
53
|
+
const array = z.array(z.string());
|
|
54
|
+
const constrained = spec.minItems ? array.min(spec.minItems) : array;
|
|
55
|
+
return z.object({ [name]: spec.description ? constrained.describe(spec.description) : constrained });
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const opencodeAdapter = {
|
|
59
|
+
label() { },
|
|
60
|
+
on(pi, event, handler) { pi.on(event, handler); },
|
|
61
|
+
async select(ui, title, options, dialogOptions) {
|
|
62
|
+
if (!ui?.select)
|
|
63
|
+
return undefined;
|
|
64
|
+
return ui.select(title, options, dialogOptions);
|
|
65
|
+
},
|
|
66
|
+
inject(event, text, staleMarker) {
|
|
67
|
+
const marker = staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
68
|
+
const dropStale = (parts) => (marker ? parts.filter((part) => !part.includes(marker)) : parts);
|
|
69
|
+
return { systemPrompt: [...dropStale(asPromptArray(event.systemPrompt)), text] };
|
|
70
|
+
},
|
|
71
|
+
stringArray(_pi, name, spec) {
|
|
72
|
+
const array = { type: 'array', items: { type: 'string' } };
|
|
73
|
+
if (spec.minItems)
|
|
74
|
+
array.minItems = spec.minItems;
|
|
75
|
+
if (spec.description)
|
|
76
|
+
array.description = spec.description;
|
|
77
|
+
return { type: 'object', properties: { [name]: array }, required: [name] };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
// Explicit hostId wins; the legacy zod sniff keeps old callers working.
|
|
81
|
+
function adapterFor(pi) {
|
|
82
|
+
if (pi.hostId === 'opencode')
|
|
83
|
+
return opencodeAdapter;
|
|
84
|
+
if (pi.hostId === 'omp')
|
|
85
|
+
return ompAdapter;
|
|
86
|
+
if (pi.hostId === 'pi')
|
|
87
|
+
return piAdapter;
|
|
88
|
+
return pi.zod ? ompAdapter : piAdapter;
|
|
89
|
+
}
|
|
3
90
|
export function isPiHost(pi) {
|
|
4
91
|
return !pi.zod;
|
|
5
92
|
}
|
|
6
|
-
export function setExtensionLabel(pi,
|
|
7
|
-
|
|
8
|
-
return;
|
|
9
|
-
pi.setLabel?.(label);
|
|
93
|
+
export function setExtensionLabel(pi, text) {
|
|
94
|
+
adapterFor(pi).label(pi, text);
|
|
10
95
|
}
|
|
11
96
|
export function onHostEvent(pi, event, handler) {
|
|
12
|
-
|
|
13
|
-
return;
|
|
14
|
-
pi.on(event, handler);
|
|
97
|
+
adapterFor(pi).on(pi, event, handler);
|
|
15
98
|
}
|
|
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
99
|
export async function hostSelect(pi, ui, title, options, dialogOptions) {
|
|
19
|
-
|
|
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;
|
|
100
|
+
return adapterFor(pi).select(ui, title, options, dialogOptions);
|
|
28
101
|
}
|
|
29
|
-
|
|
30
|
-
|
|
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}` };
|
|
102
|
+
export function injectPromptText(pi, event, text, staleMarker) {
|
|
103
|
+
return adapterFor(pi).inject(event, text, staleMarker);
|
|
42
104
|
}
|
|
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
105
|
export function stringArrayToolParams(pi, name, spec = {}) {
|
|
46
|
-
|
|
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] };
|
|
106
|
+
return adapterFor(pi).stringArray(pi, name, spec);
|
|
58
107
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// One source for both hosts;
|
|
1
|
+
// One source for both hosts; each host-specific behaviour lives in one adapter row.
|
|
2
2
|
|
|
3
3
|
import { asPromptArray } from './session-state.ts';
|
|
4
4
|
import type { ExtensionApi, ExtensionCtx, PromptInjection, SystemPromptEvent, ToolParams, UiApi } from './types.ts';
|
|
5
5
|
|
|
6
|
-
/** The zod subset the one tool schema needs. */
|
|
7
6
|
export interface ZodChain {
|
|
8
7
|
min: (n: number) => ZodChain;
|
|
9
8
|
describe: (text: string) => ZodChain;
|
|
@@ -17,33 +16,121 @@ export interface ZodFactory {
|
|
|
17
16
|
};
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
/** The host fields these adapters read. */
|
|
21
19
|
export interface HostHandle {
|
|
22
20
|
zod?: unknown;
|
|
23
21
|
setLabel?: (...args: string[]) => void;
|
|
22
|
+
hostId?: 'pi' | 'omp' | 'opencode';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SelectOption {
|
|
26
|
+
label: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StringArraySpec {
|
|
31
|
+
minItems?: number;
|
|
32
|
+
description?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface HostAdapter {
|
|
36
|
+
label(pi: HostHandle, text: string): void;
|
|
37
|
+
on<E>(pi: ExtensionApi, event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void;
|
|
38
|
+
select(ui: UiApi | undefined, title: string, options: SelectOption[], dialogOptions?: Record<string, unknown>): Promise<string | undefined>;
|
|
39
|
+
inject(event: SystemPromptEvent, text: string, staleMarker?: string): PromptInjection | undefined;
|
|
40
|
+
stringArray(pi: ExtensionApi, name: string, spec: StringArraySpec): ToolParams;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const piAdapter: HostAdapter = {
|
|
44
|
+
label() { /* no label surface */ },
|
|
45
|
+
on(pi, event, handler) {
|
|
46
|
+
if (event === 'session_branch') return;
|
|
47
|
+
pi.on(event, handler);
|
|
48
|
+
},
|
|
49
|
+
async select(ui, title, options, dialogOptions) {
|
|
50
|
+
if (!ui?.select) return undefined;
|
|
51
|
+
const shown = options.map((o) => (o.description ? `${o.label} — ${o.description}` : o.label));
|
|
52
|
+
const picked = await ui.select(title, shown, dialogOptions);
|
|
53
|
+
if (picked === undefined) return undefined;
|
|
54
|
+
return options[shown.indexOf(picked)]?.label ?? picked;
|
|
55
|
+
},
|
|
56
|
+
inject(event, text, staleMarker) {
|
|
57
|
+
const marker = staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
58
|
+
const dropStale = (parts: string[]): string[] => (marker ? parts.filter((part) => !part.includes(marker)) : parts);
|
|
59
|
+
const options = event.systemPromptOptions;
|
|
60
|
+
if (options && typeof options.appendSystemPrompt === 'string') {
|
|
61
|
+
options.appendSystemPrompt = dropStale([options.appendSystemPrompt]).concat(text).filter(Boolean).join('\n\n');
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return { systemPrompt: `${dropStale(asPromptArray(event.systemPrompt)).join('\n\n')}\n\n${text}` };
|
|
65
|
+
},
|
|
66
|
+
stringArray(_pi, name, spec) {
|
|
67
|
+
const array: Record<string, unknown> = { type: 'array', items: { type: 'string' } };
|
|
68
|
+
if (spec.minItems) array.minItems = spec.minItems;
|
|
69
|
+
if (spec.description) array.description = spec.description;
|
|
70
|
+
return { type: 'object', properties: { [name]: array }, required: [name] };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const ompAdapter: HostAdapter = {
|
|
75
|
+
label(pi, text) { pi.setLabel?.(text); },
|
|
76
|
+
on(pi, event, handler) { pi.on(event, handler); },
|
|
77
|
+
async select(ui, title, options, dialogOptions) {
|
|
78
|
+
if (!ui?.select) return undefined;
|
|
79
|
+
return ui.select(title, options, dialogOptions);
|
|
80
|
+
},
|
|
81
|
+
inject(event, text, staleMarker) {
|
|
82
|
+
const marker = staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
83
|
+
const dropStale = (parts: string[]): string[] => (marker ? parts.filter((part) => !part.includes(marker)) : parts);
|
|
84
|
+
return { systemPrompt: [...dropStale(asPromptArray(event.systemPrompt)), text] };
|
|
85
|
+
},
|
|
86
|
+
stringArray(pi, name, spec) {
|
|
87
|
+
const { z } = pi.zod as { z: ZodFactory['z'] };
|
|
88
|
+
const array = z.array(z.string());
|
|
89
|
+
const constrained = spec.minItems ? array.min(spec.minItems) : array;
|
|
90
|
+
return z.object({ [name]: spec.description ? constrained.describe(spec.description) : constrained });
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const opencodeAdapter: HostAdapter = {
|
|
95
|
+
label() { /* no label surface */ },
|
|
96
|
+
on(pi, event, handler) { pi.on(event, handler); },
|
|
97
|
+
async select(ui, title, options, dialogOptions) {
|
|
98
|
+
if (!ui?.select) return undefined;
|
|
99
|
+
return ui.select(title, options, dialogOptions);
|
|
100
|
+
},
|
|
101
|
+
inject(event, text, staleMarker) {
|
|
102
|
+
const marker = staleMarker?.replace(/^[^\w]+/, '').trim();
|
|
103
|
+
const dropStale = (parts: string[]): string[] => (marker ? parts.filter((part) => !part.includes(marker)) : parts);
|
|
104
|
+
return { systemPrompt: [...dropStale(asPromptArray(event.systemPrompt)), text] };
|
|
105
|
+
},
|
|
106
|
+
stringArray(_pi, name, spec) {
|
|
107
|
+
const array: Record<string, unknown> = { type: 'array', items: { type: 'string' } };
|
|
108
|
+
if (spec.minItems) array.minItems = spec.minItems;
|
|
109
|
+
if (spec.description) array.description = spec.description;
|
|
110
|
+
return { type: 'object', properties: { [name]: array }, required: [name] };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Explicit hostId wins; the legacy zod sniff keeps old callers working.
|
|
115
|
+
function adapterFor(pi: HostHandle): HostAdapter {
|
|
116
|
+
if (pi.hostId === 'opencode') return opencodeAdapter;
|
|
117
|
+
if (pi.hostId === 'omp') return ompAdapter;
|
|
118
|
+
if (pi.hostId === 'pi') return piAdapter;
|
|
119
|
+
return pi.zod ? ompAdapter : piAdapter;
|
|
24
120
|
}
|
|
25
121
|
|
|
26
122
|
export function isPiHost(pi: HostHandle): boolean {
|
|
27
123
|
return !pi.zod;
|
|
28
124
|
}
|
|
29
125
|
|
|
30
|
-
export function setExtensionLabel(pi: HostHandle,
|
|
31
|
-
|
|
32
|
-
pi.setLabel?.(label);
|
|
126
|
+
export function setExtensionLabel(pi: HostHandle, text: string): void {
|
|
127
|
+
adapterFor(pi).label(pi, text);
|
|
33
128
|
}
|
|
34
129
|
|
|
35
130
|
export function onHostEvent<E>(pi: ExtensionApi, event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void {
|
|
36
|
-
|
|
37
|
-
pi.on<E>(event, handler);
|
|
131
|
+
adapterFor(pi).on<E>(pi, event, handler);
|
|
38
132
|
}
|
|
39
133
|
|
|
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
134
|
export async function hostSelect(
|
|
48
135
|
pi: HostHandle,
|
|
49
136
|
ui: UiApi | undefined,
|
|
@@ -51,44 +138,13 @@ export async function hostSelect(
|
|
|
51
138
|
options: SelectOption[],
|
|
52
139
|
dialogOptions?: Record<string, unknown>,
|
|
53
140
|
): Promise<string | undefined> {
|
|
54
|
-
|
|
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;
|
|
141
|
+
return adapterFor(pi).select(ui, title, options, dialogOptions);
|
|
60
142
|
}
|
|
61
143
|
|
|
62
|
-
|
|
63
|
-
|
|
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}` };
|
|
144
|
+
export function injectPromptText(pi: HostHandle, event: SystemPromptEvent, text: string, staleMarker?: string): PromptInjection | undefined {
|
|
145
|
+
return adapterFor(pi).inject(event, text, staleMarker);
|
|
74
146
|
}
|
|
75
147
|
|
|
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
148
|
export function stringArrayToolParams(pi: ExtensionApi, name: string, spec: StringArraySpec = {}): ToolParams {
|
|
84
|
-
|
|
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] };
|
|
149
|
+
return adapterFor(pi).stringArray(pi, name, spec);
|
|
94
150
|
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
// One settings file for every host; parse failure yields {} with per-caller defaults.
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
3
|
import path from 'node:path';
|
|
5
|
-
import { tersioHome } from '../lib/utils.js';
|
|
4
|
+
import { homeDir, tersioHome } from '../lib/utils.js';
|
|
6
5
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
7
6
|
// The one store both hosts read; this module owns its path.
|
|
8
7
|
export function tersioSettingsFile() {
|
|
9
8
|
return path.join(tersioHome(), 'settings.json');
|
|
10
9
|
}
|
|
11
|
-
// Older installs kept these as OMP plugin settings; still read so switching
|
|
12
|
-
// the store does not reset a user's defaults.
|
|
10
|
+
// Older installs kept these as OMP plugin settings; still read so switching the store does not reset a user's defaults.
|
|
13
11
|
function legacyOmpSettings() {
|
|
14
|
-
const configHome = process.env.XDG_CONFIG_HOME || path.join(
|
|
12
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(homeDir(), '.config');
|
|
15
13
|
for (const p of [
|
|
16
|
-
path.join(
|
|
14
|
+
path.join(homeDir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
|
|
17
15
|
path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
|
|
18
16
|
]) {
|
|
19
17
|
if (!existsSync(p))
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// One settings file for every host; parse failure yields {} with per-caller defaults.
|
|
2
2
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
4
|
import path from 'node:path';
|
|
6
|
-
import { tersioHome } from '../lib/utils.ts';
|
|
5
|
+
import { homeDir, tersioHome } from '../lib/utils.ts';
|
|
7
6
|
|
|
8
7
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
9
8
|
|
|
@@ -12,12 +11,11 @@ export function tersioSettingsFile(): string {
|
|
|
12
11
|
return path.join(tersioHome(), 'settings.json');
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
// Older installs kept these as OMP plugin settings; still read so switching
|
|
16
|
-
// the store does not reset a user's defaults.
|
|
14
|
+
// Older installs kept these as OMP plugin settings; still read so switching the store does not reset a user's defaults.
|
|
17
15
|
function legacyOmpSettings(): Record<string, unknown> | null {
|
|
18
|
-
const configHome = process.env.XDG_CONFIG_HOME || path.join(
|
|
16
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(homeDir(), '.config');
|
|
19
17
|
for (const p of [
|
|
20
|
-
path.join(
|
|
18
|
+
path.join(homeDir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
|
|
21
19
|
path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
|
|
22
20
|
]) {
|
|
23
21
|
if (!existsSync(p)) continue;
|