@krtclcdy/tersio 2.19.0 → 2.20.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.
Files changed (34) hide show
  1. package/cli/common.js +13 -1
  2. package/cli/doctor.js +7 -7
  3. package/cli/install.js +28 -28
  4. package/extensions/ai-addons-updater/index.ts +377 -0
  5. package/extensions/caveman-session/index.ts +161 -0
  6. package/extensions/combo-toggle/index.ts +217 -0
  7. package/extensions/lib/utils.ts +162 -0
  8. package/extensions/rtk-session/index.ts +164 -0
  9. package/extensions/shared/carbon.ts +123 -0
  10. package/extensions/shared/mode-reinforcement.ts +29 -0
  11. package/extensions/shared/plugin-settings.ts +57 -0
  12. package/extensions/shared/pricing.ts +168 -0
  13. package/extensions/shared/rtk-gain.ts +105 -0
  14. package/extensions/shared/session-state.ts +201 -0
  15. package/extensions/shared/types.ts +85 -0
  16. package/extensions/shared/usage-ledger.ts +406 -0
  17. package/extensions/shared/usage-store.ts +325 -0
  18. package/extensions/tersio-commands/index.ts +110 -0
  19. package/package.json +19 -24
  20. package/extensions/ai-addons-updater/index.js +0 -331
  21. package/extensions/caveman-session/index.js +0 -152
  22. package/extensions/combo-toggle/index.js +0 -165
  23. package/extensions/lib/utils.js +0 -154
  24. package/extensions/rtk-session/index.js +0 -148
  25. package/extensions/shared/carbon.js +0 -88
  26. package/extensions/shared/mode-reinforcement.js +0 -26
  27. package/extensions/shared/plugin-settings.js +0 -51
  28. package/extensions/shared/pricing.js +0 -163
  29. package/extensions/shared/rtk-gain.js +0 -79
  30. package/extensions/shared/session-state.js +0 -177
  31. package/extensions/shared/types.js +0 -3
  32. package/extensions/shared/usage-ledger.js +0 -354
  33. package/extensions/shared/usage-store.js +0 -294
  34. package/extensions/tersio-commands/index.js +0 -102
@@ -0,0 +1,201 @@
1
+ import type { ComboLevel, ComboState, ExtensionCtx, SessionEntry, UiApi } from './types.ts';
2
+
3
+ const BRIDGE_KEY = Symbol.for('tersio/combo-session-state');
4
+
5
+ export const OMP_SUBAGENT_MARKER = 'You are operating on a piece of work assigned to you by the main agent.';
6
+
7
+ export const COMBO_LEVELS: Record<string, Readonly<ComboState>> = Object.freeze({
8
+ off: Object.freeze({ level: 'off', caveman: 'off', rtk: 'off', ponytail: 'off' }),
9
+ medium: Object.freeze({ level: 'medium', caveman: 'lite', rtk: 'on', ponytail: 'lite' }),
10
+ balanced: Object.freeze({ level: 'balanced', caveman: 'full', rtk: 'on', ponytail: 'full' }),
11
+ max: Object.freeze({ level: 'max', caveman: 'ultra', rtk: 'on', ponytail: 'ultra' }),
12
+ });
13
+
14
+ const MODE_VALUES: Record<string, Set<string>> = {
15
+ caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan']),
16
+ rtk: new Set(['off', 'on']),
17
+ ponytail: new Set(['off', 'lite', 'full', 'ultra', 'review']),
18
+ };
19
+
20
+ type ModeName = 'caveman' | 'rtk' | 'ponytail';
21
+ type Modes = Record<ModeName, string>;
22
+
23
+ const MODE_ENTRY_TYPES: Record<string, ModeName> = {
24
+ 'caveman-mode': 'caveman',
25
+ 'rtk-mode': 'rtk',
26
+ 'ponytail-mode': 'ponytail',
27
+ };
28
+
29
+ interface Bridge {
30
+ state: Readonly<ComboState>;
31
+ listeners: Set<(state: Readonly<ComboState>) => void>;
32
+ }
33
+
34
+ export function normalizeMode(name: ModeName, value: unknown): string | null {
35
+ if (name === 'rtk' && typeof value === 'boolean') return value ? 'on' : 'off';
36
+ const mode = String(value ?? '').trim().toLowerCase();
37
+ return MODE_VALUES[name]?.has(mode) ? mode : null;
38
+ }
39
+
40
+ export function deriveLevel(modes: Modes | null | undefined): ComboLevel {
41
+ for (const level of Object.keys(COMBO_LEVELS) as ComboLevel[]) {
42
+ const preset = COMBO_LEVELS[level];
43
+ if (preset.caveman === modes?.caveman && preset.rtk === modes?.rtk && preset.ponytail === modes?.ponytail) return level;
44
+ }
45
+ return 'custom';
46
+ }
47
+
48
+ function isPresetLevel(value: string): value is ComboLevel {
49
+ return Object.prototype.hasOwnProperty.call(COMBO_LEVELS, value);
50
+ }
51
+
52
+ function isKnownLevel(value: string): boolean {
53
+ return value === 'custom' || isPresetLevel(value);
54
+ }
55
+
56
+ function normalizedState(modes: Partial<Modes> | null | undefined, level: ComboLevel = deriveLevel(modes as Modes)): Readonly<ComboState> {
57
+ const state: Modes = {
58
+ caveman: normalizeMode('caveman', modes?.caveman) || 'off',
59
+ rtk: normalizeMode('rtk', modes?.rtk) || 'off',
60
+ ponytail: normalizeMode('ponytail', modes?.ponytail) || 'off',
61
+ };
62
+ return Object.freeze({ level: isKnownLevel(level) ? level : deriveLevel(state), ...state });
63
+ }
64
+
65
+ function bridge(): Bridge {
66
+ const existing = (globalThis as Record<symbol, unknown>)[BRIDGE_KEY] as Bridge | undefined;
67
+ if (existing?.state) return existing;
68
+ const initial = normalizedState((existing || COMBO_LEVELS.off) as Partial<Modes>);
69
+ return ((globalThis as Record<symbol, unknown>)[BRIDGE_KEY] = { state: initial, listeners: new Set<(state: Readonly<ComboState>) => void>() });
70
+ }
71
+
72
+ function publish(state: Readonly<ComboState>): Readonly<ComboState> {
73
+ const shared = bridge();
74
+ shared.state = state;
75
+ for (const listener of shared.listeners) listener(state);
76
+ return state;
77
+ }
78
+
79
+ export function normalizeInputCommand(value: unknown): string {
80
+ return String(value || '').trim().toLowerCase().replace(/[.!?\s]+$/, '');
81
+ }
82
+
83
+ export function asPromptArray(systemPrompt: string | string[]): string[] {
84
+ return Array.isArray(systemPrompt) ? systemPrompt : [systemPrompt];
85
+ }
86
+
87
+ export function systemPromptIncludes(systemPrompt: string | string[], marker: string): boolean {
88
+ return asPromptArray(systemPrompt).some((prompt) => typeof prompt === 'string' && prompt.includes(marker));
89
+ }
90
+
91
+ export function isOmpSubagentPrompt(systemPrompt: string | string[]): boolean {
92
+ return systemPromptIncludes(systemPrompt, OMP_SUBAGENT_MARKER);
93
+ }
94
+
95
+ export function normalizeComboLevel(value: unknown): ComboLevel | null {
96
+ const level = String(value || '').trim().toLowerCase();
97
+ return isPresetLevel(level) ? level : null;
98
+ }
99
+
100
+ export function paintStatusBar(ui: UiApi | undefined, key: string, emoji: string, label: string, isActive: boolean): void {
101
+ const theme = ui?.theme;
102
+ const indicator = isActive && theme?.fg ? theme.fg('accent', emoji) : emoji;
103
+ ui?.setStatus?.(key, theme?.fg ? `${indicator} ${theme.fg('muted', label)}` : `${indicator} ${label}`);
104
+ }
105
+
106
+ // Which context a status bar should remember to paint through.
107
+ //
108
+ // A UI-less event — a headless child session_start, or a top-level
109
+ // session_start that fires before the TUI attaches — must never become the
110
+ // remembered context. Two things went wrong when it did: the old
111
+ // `ctx || lastCtx` fallback only triggered for a *falsy* ctx, so a truthy
112
+ // ctx without `ui` returned early anyway, and remembering it destroyed the one
113
+ // paintable context we had. Every later bare syncStatus() then went mute —
114
+ // and the bare call is the bridge-listener path, which is how a mode change
115
+ // made by a sibling extension reaches this bar. The symptom is a bar frozen on
116
+ // a preset the session no longer has: it shows "combo BALANCED" while caveman
117
+ // and rtk are already off.
118
+ export function paintableCtx(remembered: ExtensionCtx | undefined, next: ExtensionCtx | undefined): ExtensionCtx | undefined {
119
+ return next?.ui?.setStatus ? next : remembered;
120
+ }
121
+
122
+ // Last-wins scan for a custom session entry; skips entries whose value fails
123
+ // to parse so a corrupt write never shadows an older valid one.
124
+ export function lastCustomValue<T>(entries: SessionEntry[] | null | undefined, customType: string, pick: (data: SessionEntry['data']) => T | null | undefined): T | null {
125
+ if (!Array.isArray(entries)) return null;
126
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
127
+ const entry = entries[i];
128
+ if (entry?.type !== 'custom' || entry?.customType !== customType) continue;
129
+ const value = pick(entry?.data);
130
+ if (value !== null && value !== undefined) return value;
131
+ }
132
+ return null;
133
+ }
134
+
135
+ export function sessionEntries(ctx: ExtensionCtx | undefined): SessionEntry[] {
136
+ return ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
137
+ }
138
+
139
+ export function getSharedComboState(): Readonly<ComboState> {
140
+ return bridge().state;
141
+ }
142
+
143
+ export function isComboPresetActive(): boolean {
144
+ const level = getSharedComboState().level;
145
+ return level !== 'off' && isPresetLevel(level);
146
+ }
147
+
148
+ export function setSharedComboLevel(value: unknown): Readonly<ComboState> {
149
+ const level = normalizeComboLevel(value) || 'off';
150
+ return publish(normalizedState(COMBO_LEVELS[level] as Partial<Modes>, level));
151
+ }
152
+
153
+ export function setSharedComboMode(name: ModeName, value: unknown): Readonly<ComboState> {
154
+ const mode = normalizeMode(name, value);
155
+ if (!mode) return getSharedComboState();
156
+ const modes = { ...getSharedComboState(), [name]: mode } as Modes;
157
+ // Derive the level from the final triplet so a redundant same-value write
158
+ // (entry replay on resume, upstream ponytail re-affirming its mode) cannot
159
+ // strand a matching preset at 'custom' and drop the combo bar.
160
+ return publish(normalizedState(modes));
161
+ }
162
+
163
+ export function reconcileSharedComboEntries(entries: SessionEntry[] | null | undefined): Readonly<ComboState> {
164
+ let modes: Modes = { ...COMBO_LEVELS.off };
165
+ if (Array.isArray(entries)) {
166
+ for (const entry of entries) {
167
+ if (entry?.type !== 'custom') continue;
168
+ if (entry.customType === 'combo-level') {
169
+ const preset = normalizeComboLevel(entry?.data?.level);
170
+ if (preset) modes = { ...COMBO_LEVELS[preset] };
171
+ continue;
172
+ }
173
+ const name = MODE_ENTRY_TYPES[entry.customType ?? ''] ?? null;
174
+ if (!name) continue;
175
+ const value = name === 'rtk' ? entry?.data?.enabled : entry?.data?.mode;
176
+ const mode = normalizeMode(name, value);
177
+ if (mode) modes[name] = mode;
178
+ }
179
+ }
180
+ // The level always reflects the final mode triplet, never the write order:
181
+ // a preset entry followed by the same values reconciles back to the preset.
182
+ return publish(normalizedState(modes));
183
+ }
184
+
185
+ // One-line session-wide mode summary for change confirmations, e.g.
186
+ // `caveman=ULTRA, rtk=ON, ponytail=ULTRA`.
187
+ export function activeModesSummary(state: { caveman: string; rtk: string; ponytail: string }): string {
188
+ return `caveman=${state.caveman.toUpperCase()}, rtk=${state.rtk.toUpperCase()}, ponytail=${state.ponytail.toUpperCase()}`;
189
+ }
190
+
191
+ export function setSharedComboListener(listener: ((state: Readonly<ComboState>) => void) | null): void {
192
+ // Additive: every sibling extension syncs its local mirror on publish, so
193
+ // a /tersio or /combo switch takes effect next turn with no session reload.
194
+ // A null listener clears all (no current callers; reserved for teardown).
195
+ if (typeof listener === 'function') bridge().listeners.add(listener);
196
+ else bridge().listeners.clear();
197
+ }
198
+
199
+ export function resetSharedComboState(): Readonly<ComboState> {
200
+ return setSharedComboLevel('off');
201
+ }
@@ -0,0 +1,85 @@
1
+ // Shared OMP extension host types. Structural — the real `pi` object comes
2
+ // from the OMP runtime; these describe the surface this package touches.
3
+
4
+ export interface ComboState {
5
+ level: string;
6
+ caveman: string;
7
+ rtk: string;
8
+ ponytail: string;
9
+ }
10
+
11
+ export type ComboLevel = 'off' | 'medium' | 'balanced' | 'max' | 'custom';
12
+
13
+ export type SessionEntry = {
14
+ type: string;
15
+ customType?: string;
16
+ data?: {
17
+ mode?: string;
18
+ enabled?: boolean;
19
+ level?: string;
20
+ [key: string]: unknown;
21
+ };
22
+ };
23
+
24
+ export interface UiApi {
25
+ setStatus?: (name: string, value: string | undefined) => void;
26
+ notify?: (message: string, level?: string) => void;
27
+ theme?: {
28
+ fg?: (role: string, text: string) => string;
29
+ };
30
+ }
31
+
32
+ export interface ExtensionCtx {
33
+ hasUI?: boolean;
34
+ cwd?: string;
35
+ sessionManager?: {
36
+ getBranch?: () => SessionEntry[];
37
+ getEntries?: () => SessionEntry[];
38
+ };
39
+ ui?: UiApi;
40
+ reload?: () => Promise<void>;
41
+ }
42
+
43
+ export interface SystemPromptEvent {
44
+ systemPrompt: string | string[];
45
+ }
46
+
47
+ export interface InputEvent {
48
+ text?: string;
49
+ source?: string;
50
+ }
51
+
52
+ export interface ExtensionApi {
53
+ setLabel?: (label: string) => void;
54
+ registerCommand?: (name: string, config: {
55
+ description: string;
56
+ handler: (args: string, ctx: ExtensionCtx) => Promise<void>;
57
+ }) => void;
58
+ registerTool?: (tool: {
59
+ name: string;
60
+ label: string;
61
+ description: string;
62
+ parameters: unknown;
63
+ execute: (
64
+ toolCallId: string,
65
+ params: { args: string[] },
66
+ signal: AbortSignal,
67
+ onUpdate: ((data: unknown) => void) | undefined,
68
+ ctx: ExtensionCtx | undefined,
69
+ ) => Promise<{
70
+ isError: boolean;
71
+ content: { type: string; text: string }[];
72
+ details: Record<string, unknown>;
73
+ }>;
74
+ }) => void;
75
+ appendEntry?: (customType: string, data: Record<string, unknown>) => void;
76
+ exec?: (cmd: string, args: string[], opts?: { signal?: AbortSignal; cwd?: string }) => Promise<{
77
+ stdout: string;
78
+ stderr: string;
79
+ code: number;
80
+ killed?: boolean;
81
+ }>;
82
+ cwd?: string;
83
+ zod?: { z: unknown };
84
+ on<E>(event: string, handler: (event: E, ctx: ExtensionCtx) => unknown): void;
85
+ }