@krtclcdy/tersio 2.24.0 → 2.25.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 +28 -24
- package/cli/banner.js +7 -5
- package/cli/common.js +14 -13
- package/cli/currency.js +2 -7
- package/cli/dashboard.js +220 -13
- package/cli/doctor-fix.js +99 -29
- package/cli/doctor.js +32 -35
- package/cli/hosts.js +23 -26
- package/cli/install.js +228 -115
- package/cli/interactive.js +65 -21
- package/cli/manifest.js +8 -4
- package/cli/profile.js +49 -16
- package/cli/reset.js +3 -7
- package/cli/rtk-wiring.js +29 -32
- package/cli/settings.js +40 -16
- package/cli/uninstall.js +79 -29
- package/cli/update.js +27 -20
- package/cli/usage.js +54 -19
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +75 -20
- package/extensions/ai-addons-updater/index.ts +67 -23
- package/extensions/caveman-session/index.js +33 -75
- package/extensions/caveman-session/index.ts +35 -77
- package/extensions/combo-toggle/index.js +40 -62
- package/extensions/combo-toggle/index.ts +38 -72
- package/extensions/lib/utils.js +64 -15
- package/extensions/lib/utils.ts +62 -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 +38 -57
- package/extensions/rtk-session/index.ts +36 -55
- package/extensions/shared/carbon.js +4 -10
- package/extensions/shared/carbon.ts +5 -10
- package/extensions/shared/host.js +95 -56
- package/extensions/shared/host.ts +106 -60
- package/extensions/shared/plugin-settings.js +6 -18
- package/extensions/shared/plugin-settings.ts +6 -18
- package/extensions/shared/pricing.js +79 -27
- package/extensions/shared/pricing.ts +80 -28
- package/extensions/shared/rtk-gain.js +4 -11
- package/extensions/shared/rtk-gain.ts +4 -11
- package/extensions/shared/session-state.js +28 -39
- package/extensions/shared/session-state.ts +29 -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 +312 -57
- package/extensions/shared/usage-ledger.ts +321 -61
- package/extensions/shared/usage-store.js +354 -38
- package/extensions/shared/usage-store.ts +360 -43
- package/extensions/tersio-commands/index.js +5 -14
- package/extensions/tersio-commands/index.ts +5 -14
- package/package.json +12 -7
- package/tersio.js +4 -6
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -24
- 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,68 +1,107 @@
|
|
|
1
|
-
//
|
|
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.
|
|
1
|
+
// One source for both hosts; each host-specific behaviour lives in one adapter row.
|
|
12
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
|
+
}
|
|
13
90
|
export function isPiHost(pi) {
|
|
14
91
|
return !pi.zod;
|
|
15
92
|
}
|
|
16
|
-
export function setExtensionLabel(pi,
|
|
17
|
-
|
|
18
|
-
return;
|
|
19
|
-
pi.setLabel?.(label);
|
|
93
|
+
export function setExtensionLabel(pi, text) {
|
|
94
|
+
adapterFor(pi).label(pi, text);
|
|
20
95
|
}
|
|
21
96
|
export function onHostEvent(pi, event, handler) {
|
|
22
|
-
|
|
23
|
-
return;
|
|
24
|
-
pi.on(event, handler);
|
|
97
|
+
adapterFor(pi).on(pi, event, handler);
|
|
25
98
|
}
|
|
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
99
|
export async function hostSelect(pi, ui, title, options, dialogOptions) {
|
|
29
|
-
|
|
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;
|
|
100
|
+
return adapterFor(pi).select(ui, title, options, dialogOptions);
|
|
38
101
|
}
|
|
39
|
-
|
|
40
|
-
|
|
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}` };
|
|
102
|
+
export function injectPromptText(pi, event, text, staleMarker) {
|
|
103
|
+
return adapterFor(pi).inject(event, text, staleMarker);
|
|
52
104
|
}
|
|
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
105
|
export function stringArrayToolParams(pi, name, spec = {}) {
|
|
56
|
-
|
|
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] };
|
|
106
|
+
return adapterFor(pi).stringArray(pi, name, spec);
|
|
68
107
|
}
|
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
//
|
|
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.
|
|
1
|
+
// One source for both hosts; each host-specific behaviour lives in one adapter row.
|
|
12
2
|
|
|
13
3
|
import { asPromptArray } from './session-state.ts';
|
|
14
4
|
import type { ExtensionApi, ExtensionCtx, PromptInjection, SystemPromptEvent, ToolParams, UiApi } from './types.ts';
|
|
15
5
|
|
|
16
|
-
/** The zod subset the one tool schema needs. */
|
|
17
6
|
export interface ZodChain {
|
|
18
7
|
min: (n: number) => ZodChain;
|
|
19
8
|
describe: (text: string) => ZodChain;
|
|
@@ -27,33 +16,121 @@ export interface ZodFactory {
|
|
|
27
16
|
};
|
|
28
17
|
}
|
|
29
18
|
|
|
30
|
-
/** The host fields these adapters read. */
|
|
31
19
|
export interface HostHandle {
|
|
32
20
|
zod?: unknown;
|
|
33
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;
|
|
34
120
|
}
|
|
35
121
|
|
|
36
122
|
export function isPiHost(pi: HostHandle): boolean {
|
|
37
123
|
return !pi.zod;
|
|
38
124
|
}
|
|
39
125
|
|
|
40
|
-
export function setExtensionLabel(pi: HostHandle,
|
|
41
|
-
|
|
42
|
-
pi.setLabel?.(label);
|
|
126
|
+
export function setExtensionLabel(pi: HostHandle, text: string): void {
|
|
127
|
+
adapterFor(pi).label(pi, text);
|
|
43
128
|
}
|
|
44
129
|
|
|
45
130
|
export function onHostEvent<E>(pi: ExtensionApi, event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void {
|
|
46
|
-
|
|
47
|
-
pi.on<E>(event, handler);
|
|
131
|
+
adapterFor(pi).on<E>(pi, event, handler);
|
|
48
132
|
}
|
|
49
133
|
|
|
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
134
|
export async function hostSelect(
|
|
58
135
|
pi: HostHandle,
|
|
59
136
|
ui: UiApi | undefined,
|
|
@@ -61,44 +138,13 @@ export async function hostSelect(
|
|
|
61
138
|
options: SelectOption[],
|
|
62
139
|
dialogOptions?: Record<string, unknown>,
|
|
63
140
|
): Promise<string | undefined> {
|
|
64
|
-
|
|
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;
|
|
141
|
+
return adapterFor(pi).select(ui, title, options, dialogOptions);
|
|
70
142
|
}
|
|
71
143
|
|
|
72
|
-
|
|
73
|
-
|
|
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}` };
|
|
144
|
+
export function injectPromptText(pi: HostHandle, event: SystemPromptEvent, text: string, staleMarker?: string): PromptInjection | undefined {
|
|
145
|
+
return adapterFor(pi).inject(event, text, staleMarker);
|
|
84
146
|
}
|
|
85
147
|
|
|
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
148
|
export function stringArrayToolParams(pi: ExtensionApi, name: string, spec: StringArraySpec = {}): ToolParams {
|
|
94
|
-
|
|
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] };
|
|
149
|
+
return adapterFor(pi).stringArray(pi, name, spec);
|
|
104
150
|
}
|
|
@@ -1,29 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// One file, under Tersio's own home (~/.tersio/settings.json), serves every
|
|
4
|
-
// host: OMP has no reason to keep its own copy and pi never had a store. The
|
|
5
|
-
// profile CLI writes the same file, so the extensions, `tersio settings`, and
|
|
6
|
-
// both hosts always agree.
|
|
7
|
-
//
|
|
8
|
-
// Tolerant: any parse failure yields {} so every caller falls back to its own
|
|
9
|
-
// default.
|
|
1
|
+
// One settings file for every host; parse failure yields {} with per-caller defaults.
|
|
10
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
-
import os from 'node:os';
|
|
12
3
|
import path from 'node:path';
|
|
13
|
-
import { tersioHome } from '../lib/utils.js';
|
|
4
|
+
import { homeDir, tersioHome } from '../lib/utils.js';
|
|
14
5
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
15
|
-
//
|
|
16
|
-
// home, so OMP and pi read the same values. This module owns the path.
|
|
6
|
+
// The one store both hosts read; this module owns its path.
|
|
17
7
|
export function tersioSettingsFile() {
|
|
18
8
|
return path.join(tersioHome(), 'settings.json');
|
|
19
9
|
}
|
|
20
|
-
// Older installs
|
|
21
|
-
// that predates ~/.tersio still has them there. Read once and keep them, so
|
|
22
|
-
// switching 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.
|
|
23
11
|
function legacyOmpSettings() {
|
|
24
|
-
const configHome = process.env.XDG_CONFIG_HOME || path.join(
|
|
12
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(homeDir(), '.config');
|
|
25
13
|
for (const p of [
|
|
26
|
-
path.join(
|
|
14
|
+
path.join(homeDir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
|
|
27
15
|
path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
|
|
28
16
|
]) {
|
|
29
17
|
if (!existsSync(p))
|
|
@@ -1,33 +1,21 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// One file, under Tersio's own home (~/.tersio/settings.json), serves every
|
|
4
|
-
// host: OMP has no reason to keep its own copy and pi never had a store. The
|
|
5
|
-
// profile CLI writes the same file, so the extensions, `tersio settings`, and
|
|
6
|
-
// both hosts always agree.
|
|
7
|
-
//
|
|
8
|
-
// Tolerant: any parse failure yields {} so every caller falls back to its own
|
|
9
|
-
// default.
|
|
1
|
+
// One settings file for every host; parse failure yields {} with per-caller defaults.
|
|
10
2
|
|
|
11
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
-
import os from 'node:os';
|
|
13
4
|
import path from 'node:path';
|
|
14
|
-
import { tersioHome } from '../lib/utils.ts';
|
|
5
|
+
import { homeDir, tersioHome } from '../lib/utils.ts';
|
|
15
6
|
|
|
16
7
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
17
8
|
|
|
18
|
-
//
|
|
19
|
-
// home, so OMP and pi read the same values. This module owns the path.
|
|
9
|
+
// The one store both hosts read; this module owns its path.
|
|
20
10
|
export function tersioSettingsFile(): string {
|
|
21
11
|
return path.join(tersioHome(), 'settings.json');
|
|
22
12
|
}
|
|
23
13
|
|
|
24
|
-
// Older installs
|
|
25
|
-
// that predates ~/.tersio still has them there. Read once and keep them, so
|
|
26
|
-
// switching 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.
|
|
27
15
|
function legacyOmpSettings(): Record<string, unknown> | null {
|
|
28
|
-
const configHome = process.env.XDG_CONFIG_HOME || path.join(
|
|
16
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(homeDir(), '.config');
|
|
29
17
|
for (const p of [
|
|
30
|
-
path.join(
|
|
18
|
+
path.join(homeDir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
|
|
31
19
|
path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
|
|
32
20
|
]) {
|
|
33
21
|
if (!existsSync(p)) continue;
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM.
|
|
2
|
-
// Exact id → provider-prefix strip → default, which reports with known:false so
|
|
3
|
-
// the Dashboard labels an unpriced model honestly. No static table: prices rot.
|
|
1
|
+
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM. Exact id → provider-prefix strip → default, reported with known:false when unpriced.
|
|
4
2
|
import fs from 'node:fs';
|
|
5
3
|
import path from 'node:path';
|
|
6
4
|
import { tersioDataPath } from '../lib/utils.js';
|
|
7
5
|
// Sonnet-class default for unpriced models; always paired with known:false.
|
|
8
6
|
export const DEFAULT_PRICE = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
|
|
9
7
|
const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
10
|
-
// Readers use the file even when stale, and only one refresh runs per process;
|
|
11
|
-
// `tersio update` still forces a fresh fetch.
|
|
8
|
+
// Readers use the file even when stale, and only one refresh runs per process; `tersio update` still forces a fresh fetch.
|
|
12
9
|
const CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
|
|
13
10
|
let refreshInflight = null;
|
|
14
11
|
export function pricesCachePath() {
|
|
@@ -23,8 +20,7 @@ export function pricesUrl() {
|
|
|
23
20
|
function toPrice(t) {
|
|
24
21
|
return { input: t[0], output: t[1], cacheRead: t[2], cacheWrite: t[3] };
|
|
25
22
|
}
|
|
26
|
-
// Tolerate both cache shapes (compact tuples and ModelPrice objects) so a
|
|
27
|
-
// version bump never orphans the cache.
|
|
23
|
+
// Tolerate both cache shapes (compact tuples and ModelPrice objects) so a version bump never orphans the cache.
|
|
28
24
|
function asPrice(v) {
|
|
29
25
|
if (Array.isArray(v) && v.length === 4 && v.every((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0)) {
|
|
30
26
|
return toPrice(v);
|
|
@@ -32,14 +28,30 @@ function asPrice(v) {
|
|
|
32
28
|
if (v && typeof v === 'object') {
|
|
33
29
|
const o = v;
|
|
34
30
|
if (['input', 'output', 'cacheRead', 'cacheWrite'].every((k) => typeof o[k] === 'number' && Number.isFinite(o[k]) && o[k] >= 0)) {
|
|
35
|
-
// SAFETY: the line above verified all four keys are finite numbers >= 0,
|
|
36
|
-
// which is the whole of ModelPrice.
|
|
31
|
+
// SAFETY: the line above verified all four keys are finite numbers >= 0, which is the whole of ModelPrice.
|
|
37
32
|
return o;
|
|
38
33
|
}
|
|
39
34
|
}
|
|
40
35
|
return null;
|
|
41
36
|
}
|
|
37
|
+
let liveCache = null;
|
|
42
38
|
export function loadLivePrices() {
|
|
39
|
+
// One stat per call beats one full parse: the recent-rows map prices thousands of rows.
|
|
40
|
+
try {
|
|
41
|
+
const file = pricesCachePath();
|
|
42
|
+
const mtime = fs.statSync(file).mtimeMs;
|
|
43
|
+
if (liveCache && liveCache.path === file && liveCache.mtime === mtime)
|
|
44
|
+
return liveCache.prices;
|
|
45
|
+
const prices = readLivePrices();
|
|
46
|
+
if (prices)
|
|
47
|
+
liveCache = { path: file, mtime, prices };
|
|
48
|
+
return prices;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function readLivePrices() {
|
|
43
55
|
let raw;
|
|
44
56
|
try {
|
|
45
57
|
raw = fs.readFileSync(pricesCachePath(), 'utf8');
|
|
@@ -70,37 +82,78 @@ export function loadLivePrices() {
|
|
|
70
82
|
function num(v) {
|
|
71
83
|
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
|
|
72
84
|
}
|
|
73
|
-
//
|
|
74
|
-
// `openrouter/stealth/space-bunny-alpha` at zero. Aliases fold to the first, so
|
|
75
|
-
// a free stealth model never reports the default price.
|
|
85
|
+
// Transcripts and the feed spell one model differently; aliases stop a free model reporting the default price.
|
|
76
86
|
export const MODEL_ALIASES = [
|
|
77
87
|
{ id: 'space-bunny', feed: 'stealth/space-bunny-alpha' },
|
|
78
88
|
];
|
|
79
89
|
export function canonicalPriceId(model) {
|
|
80
90
|
const key = model.toLowerCase();
|
|
81
|
-
|
|
91
|
+
// Suffix match: gateways prefix the id (opencode/kilo/stealth/space-bunny-alpha).
|
|
92
|
+
return MODEL_ALIASES.find((a) => a.id === key || a.feed === key || key.endsWith(`/${a.id}`) || key.endsWith(`/${a.feed}`))?.id ?? key;
|
|
82
93
|
}
|
|
83
94
|
export function priceFor(model, live) {
|
|
84
95
|
const table = live === undefined ? loadLivePrices() : live;
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
if (live === undefined) {
|
|
97
|
+
const memo = priceMemo(table);
|
|
98
|
+
const hit = memo.get(model);
|
|
99
|
+
if (hit)
|
|
100
|
+
return hit;
|
|
101
|
+
const resolved = resolvePrice(model, table);
|
|
102
|
+
memo.set(model, resolved);
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
return resolvePrice(model, table);
|
|
106
|
+
}
|
|
107
|
+
// Recent rows repeat a handful of models; the memo turns thousands of lookups into one per model. Cleared whenever the table identity changes.
|
|
108
|
+
let memoTable;
|
|
109
|
+
let memo = new Map();
|
|
110
|
+
function priceMemo(table) {
|
|
111
|
+
if (memoTable !== table) {
|
|
112
|
+
memoTable = table;
|
|
113
|
+
memo = new Map();
|
|
114
|
+
}
|
|
115
|
+
return memo;
|
|
116
|
+
}
|
|
117
|
+
// Validated once per table instead of once per lookup.
|
|
118
|
+
let compiledTable = null;
|
|
119
|
+
let compiledEntries = [];
|
|
120
|
+
function priceEntries(table) {
|
|
121
|
+
if (compiledTable === table)
|
|
122
|
+
return compiledEntries;
|
|
123
|
+
const entries = [];
|
|
124
|
+
for (const [k, v] of Object.entries(table.exact)) {
|
|
125
|
+
const price = asPrice(v);
|
|
126
|
+
if (price)
|
|
127
|
+
entries.push([k, price]);
|
|
128
|
+
}
|
|
129
|
+
compiledTable = table;
|
|
130
|
+
compiledEntries = entries;
|
|
131
|
+
return entries;
|
|
132
|
+
}
|
|
133
|
+
function resolvePrice(model, table) {
|
|
134
|
+
// An alias goes through the same exact-then-suffix chain as any other id, so it still resolves when the feed keys it under a provider prefix.
|
|
87
135
|
const id = MODEL_ALIASES.find((a) => a.id === model.toLowerCase())?.feed ?? model;
|
|
88
136
|
if (table) {
|
|
89
|
-
const
|
|
137
|
+
const entries = priceEntries(table);
|
|
138
|
+
const lower = id.toLowerCase();
|
|
139
|
+
const hit = entries.find(([k]) => k === id) ?? entries.find(([k]) => k === lower);
|
|
90
140
|
if (hit)
|
|
91
|
-
return { price: hit, known: true, live: true };
|
|
141
|
+
return { price: hit[1], known: true, live: true };
|
|
92
142
|
// Provider-prefixed ids ("azure/gpt-4o", "dashscope/qwen-max"): match the
|
|
93
|
-
// first cached
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
143
|
+
// first cached key that ends with the full id, then with the bare tail
|
|
144
|
+
// segment, so "codex/openai/gpt-6-luna" still finds "gpt-6-luna".
|
|
145
|
+
const name = lower;
|
|
146
|
+
const tail = name.split('/').pop() ?? name;
|
|
147
|
+
const key = entries.find(([k]) => {
|
|
148
|
+
const lk = k.toLowerCase();
|
|
149
|
+
return lk === name || lk.endsWith(`/${name}`) || lk === tail || lk.endsWith(`/${tail}`);
|
|
150
|
+
});
|
|
151
|
+
if (key)
|
|
152
|
+
return { price: key[1], known: true, live: true };
|
|
99
153
|
}
|
|
100
154
|
return { price: DEFAULT_PRICE, known: false, live: false };
|
|
101
155
|
}
|
|
102
|
-
// Fetch LiteLLM pricing and write the cache. False on any failure — callers
|
|
103
|
-
// fall back silently.
|
|
156
|
+
// Fetch LiteLLM pricing and write the cache. False on any failure — callers fall back silently.
|
|
104
157
|
export async function refreshPrices() {
|
|
105
158
|
if (refreshInflight)
|
|
106
159
|
return refreshInflight;
|
|
@@ -164,8 +217,7 @@ export async function refreshPrices() {
|
|
|
164
217
|
refreshInflight = null;
|
|
165
218
|
}
|
|
166
219
|
}
|
|
167
|
-
// Fire-and-forget refresh when the cache is stale; readers keep the stale
|
|
168
|
-
// file, so pricing never blocks.
|
|
220
|
+
// Fire-and-forget refresh when the cache is stale; readers keep the stale file, so pricing never blocks.
|
|
169
221
|
export function refreshPricesIfStale() {
|
|
170
222
|
const live = loadLivePrices();
|
|
171
223
|
if (live && Date.now() - live.fetchedAt <= CACHE_TTL_MS)
|