@krtclcdy/tersio 1.0.4 → 2.0.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 +25 -10
- package/extensions/ai-addons-updater/index.js +166 -238
- package/extensions/caveman-session/index.js +53 -67
- package/extensions/combo-toggle/index.js +63 -76
- package/extensions/lib/utils.js +77 -24
- package/extensions/rtk-session/index.js +53 -63
- package/extensions/shared/mode-reinforcement.js +11 -14
- package/extensions/shared/plugin-settings.js +18 -16
- package/extensions/shared/session-state.js +77 -45
- package/package.json +6 -15
- package/tersio.js +678 -752
- package/extensions/amanai-reward/index.js +0 -24
- package/extensions/amanai-reward/pi.js +0 -32
|
@@ -1,31 +1,30 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
2
|
-
import { fileURLToPath } from
|
|
3
|
-
import { dirname, join } from
|
|
4
|
-
import {
|
|
5
|
-
import { readCavemanDefault } from
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
|
|
5
|
+
import { readCavemanDefault } from '../shared/plugin-settings.js';
|
|
6
6
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const RULE_PATH = join(CAVERN_DIR,
|
|
7
|
+
const RULE_PATH = join(CAVERN_DIR, 'rule.md');
|
|
8
8
|
const FALLBACK_FULL_RULE = `Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
|
9
9
|
|
|
10
10
|
Rules:
|
|
11
11
|
- Drop articles (a/an/the), filler (just/really/basically), pleasantries, hedging.
|
|
12
12
|
- Fragments OK. Short synonyms. Technical terms exact. Code unchanged.
|
|
13
13
|
- Pattern: [thing] [action] [reason]. [next step].
|
|
14
|
-
- Not:
|
|
15
|
-
- Yes:
|
|
14
|
+
- Not: 'Sure! I'd be happy to help you with that.'
|
|
15
|
+
- Yes: 'Bug in auth middleware. Fix:'
|
|
16
16
|
|
|
17
17
|
Auto-clarity: drop caveman for security warnings, irreversible actions, or when user seems confused. Resume after.`;
|
|
18
18
|
// ponytail: synchronous read each full-mode injection; ceiling = small file, cold session start. Upgrade path: cache file contents + mtime, invalidate on change.
|
|
19
19
|
function readFullRule() {
|
|
20
20
|
try {
|
|
21
|
-
return readFileSync(RULE_PATH,
|
|
21
|
+
return readFileSync(RULE_PATH, 'utf8');
|
|
22
22
|
}
|
|
23
23
|
catch {
|
|
24
24
|
return FALLBACK_FULL_RULE;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
const DEFAULT_MODE =
|
|
28
|
-
const MODES = new Set(["off", "lite", "full", "ultra", "wenyan"]);
|
|
27
|
+
const DEFAULT_MODE = 'off';
|
|
29
28
|
const INSTRUCTIONS = {
|
|
30
29
|
lite: `Caveman lite active for this session.
|
|
31
30
|
Respond concise. Drop pleasantries, filler, and hedging. Keep complete technical substance. Code, commands, paths, errors, commits, and PR text stay normal/exact.`,
|
|
@@ -35,26 +34,12 @@ Maximum terse prose. Fragments preferred. No pleasantries, no tour, no recap unl
|
|
|
35
34
|
wenyan: `Caveman wenyan active for this session.
|
|
36
35
|
Use ultra-terse classical-Chinese-style prose only where it preserves clarity for the user. Keep technical terms, code, commands, commits, PR text, paths, and errors exact. If clarity would suffer, use caveman full instead.`,
|
|
37
36
|
};
|
|
38
|
-
function normalizeMode(value) {
|
|
39
|
-
const mode = String(value || "").trim().toLowerCase();
|
|
40
|
-
return MODES.has(mode) ? mode : null;
|
|
41
|
-
}
|
|
42
37
|
function resolveMode(entries, fallback = DEFAULT_MODE) {
|
|
43
|
-
|
|
44
|
-
return fallback;
|
|
45
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
46
|
-
const entry = entries[i];
|
|
47
|
-
if (entry?.type !== "custom" || entry?.customType !== "caveman-mode")
|
|
48
|
-
continue;
|
|
49
|
-
const mode = normalizeMode(entry?.data?.mode);
|
|
50
|
-
if (mode)
|
|
51
|
-
return mode;
|
|
52
|
-
}
|
|
53
|
-
return fallback;
|
|
38
|
+
return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
|
|
54
39
|
}
|
|
55
40
|
function isOffCommand(text) {
|
|
56
|
-
const t =
|
|
57
|
-
return t ===
|
|
41
|
+
const t = normalizeInputCommand(text);
|
|
42
|
+
return t === 'stop caveman' || t === 'normal mode' || t === 'caveman off';
|
|
58
43
|
}
|
|
59
44
|
export default function cavemanSessionExtension(pi) {
|
|
60
45
|
let currentMode = DEFAULT_MODE;
|
|
@@ -67,87 +52,88 @@ export default function cavemanSessionExtension(pi) {
|
|
|
67
52
|
if (!c?.ui?.setStatus)
|
|
68
53
|
return;
|
|
69
54
|
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
70
|
-
if (
|
|
71
|
-
c.ui.setStatus(
|
|
55
|
+
if (isComboPresetActive() || currentMode === 'off') {
|
|
56
|
+
c.ui.setStatus('caveman', undefined);
|
|
72
57
|
return;
|
|
73
58
|
}
|
|
74
|
-
|
|
75
|
-
c.ui.setStatus("caveman", undefined);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const theme = c.ui.theme;
|
|
79
|
-
const indicator = isActive && theme?.fg ? theme.fg("accent", "🪨") : "🪨";
|
|
80
|
-
const label = `caveman: ${currentMode.toUpperCase()}`;
|
|
81
|
-
c.ui.setStatus("caveman", theme?.fg ? `${indicator} ${theme.fg("muted", label)}` : `${indicator} ${label}`);
|
|
59
|
+
paintStatusBar(c.ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
|
|
82
60
|
}
|
|
83
61
|
function setMode(mode, ctx) {
|
|
84
|
-
const normalized = normalizeMode(mode);
|
|
62
|
+
const normalized = normalizeMode('caveman', mode);
|
|
85
63
|
if (!normalized)
|
|
86
64
|
return false;
|
|
87
65
|
currentMode = normalized;
|
|
88
|
-
pi.appendEntry?.(
|
|
89
|
-
setSharedComboMode(
|
|
66
|
+
pi.appendEntry?.('caveman-mode', { mode: normalized });
|
|
67
|
+
setSharedComboMode('caveman', normalized);
|
|
90
68
|
syncStatus(ctx);
|
|
91
|
-
|
|
69
|
+
const msg = normalized === 'off' ? 'Caveman mode off.' : `Caveman mode set to ${normalized}.`;
|
|
70
|
+
ctx?.ui?.notify?.(msg, 'info');
|
|
92
71
|
return true;
|
|
93
72
|
}
|
|
94
|
-
pi.setLabel?.(
|
|
95
|
-
pi.registerCommand?.(
|
|
96
|
-
description:
|
|
73
|
+
pi.setLabel?.('Caveman session toggle');
|
|
74
|
+
pi.registerCommand?.('caveman', {
|
|
75
|
+
description: 'Toggle terse caveman replies for this session',
|
|
97
76
|
handler: async (args, ctx) => {
|
|
98
|
-
const arg = String(args ||
|
|
99
|
-
if (!arg || arg ===
|
|
100
|
-
setMode(
|
|
77
|
+
const arg = String(args || '').trim().toLowerCase();
|
|
78
|
+
if (!arg || arg === 'on') {
|
|
79
|
+
setMode('full', ctx);
|
|
101
80
|
return;
|
|
102
81
|
}
|
|
103
|
-
if (arg ===
|
|
104
|
-
ctx?.ui?.notify?.(`Caveman: ${currentMode}`,
|
|
82
|
+
if (arg === 'status') {
|
|
83
|
+
ctx?.ui?.notify?.(`Caveman: ${currentMode}`, 'info');
|
|
105
84
|
return;
|
|
106
85
|
}
|
|
107
86
|
if (!setMode(arg, ctx)) {
|
|
108
|
-
ctx?.ui?.notify?.(
|
|
87
|
+
ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|wenyan|off|status]', 'warning');
|
|
109
88
|
}
|
|
110
89
|
},
|
|
111
90
|
});
|
|
112
|
-
pi.on(
|
|
113
|
-
if (event?.source ===
|
|
91
|
+
pi.on('input', async (event) => {
|
|
92
|
+
if (event?.source === 'extension')
|
|
114
93
|
return;
|
|
115
|
-
if (currentMode !==
|
|
116
|
-
setMode(
|
|
94
|
+
if (currentMode !== 'off' && isOffCommand(event?.text))
|
|
95
|
+
setMode('off');
|
|
117
96
|
});
|
|
118
97
|
function restoreMode(ctx) {
|
|
119
|
-
const entries =
|
|
98
|
+
const entries = sessionEntries(ctx);
|
|
99
|
+
// Publish the persisted combo state (incl. combo-level) before painting:
|
|
100
|
+
// bar suppression reads the in-process bridge, which is empty in a fresh
|
|
101
|
+
// host until the combo extension reconciles — and its reconcile is
|
|
102
|
+
// UI-gated. Deriving it here makes suppression independent of load order.
|
|
103
|
+
reconcileSharedComboEntries(entries);
|
|
120
104
|
// Persisted session state wins; a fresh session falls back to the
|
|
121
105
|
// installer/user-configured default (off unless configured).
|
|
122
|
-
const persisted = resolveMode(entries,
|
|
106
|
+
const persisted = resolveMode(entries, '');
|
|
123
107
|
currentMode = persisted || readCavemanDefault();
|
|
124
108
|
syncStatus(ctx);
|
|
125
109
|
}
|
|
126
|
-
pi.on(
|
|
110
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
127
111
|
restoreMode(ctx);
|
|
128
|
-
ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`,
|
|
112
|
+
ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
|
|
129
113
|
});
|
|
130
|
-
pi.on(
|
|
114
|
+
pi.on('session_branch', async (_event, ctx) => {
|
|
131
115
|
restoreMode(ctx);
|
|
132
116
|
});
|
|
133
|
-
pi.on(
|
|
117
|
+
pi.on('session_tree', async (_event, ctx) => {
|
|
134
118
|
restoreMode(ctx);
|
|
135
119
|
});
|
|
136
|
-
pi.on(
|
|
120
|
+
pi.on('agent_start', async (_event, ctx) => {
|
|
137
121
|
isActive = true;
|
|
138
122
|
syncStatus(ctx);
|
|
139
123
|
});
|
|
140
|
-
pi.on(
|
|
124
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
141
125
|
isActive = false;
|
|
142
126
|
syncStatus(ctx);
|
|
143
127
|
});
|
|
144
|
-
pi.on(
|
|
128
|
+
pi.on('before_agent_start', async (event) => {
|
|
145
129
|
const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
|
|
146
|
-
if (!mode || mode ===
|
|
130
|
+
if (!mode || mode === 'off')
|
|
147
131
|
return;
|
|
148
132
|
const def = INSTRUCTIONS[mode];
|
|
149
|
-
|
|
150
|
-
|
|
133
|
+
if (!def)
|
|
134
|
+
return;
|
|
135
|
+
const instruction = typeof def === 'function' ? def() : def;
|
|
136
|
+
const base = asPromptArray(event.systemPrompt);
|
|
151
137
|
return { systemPrompt: [...base, instruction] };
|
|
152
138
|
});
|
|
153
139
|
}
|
|
@@ -1,42 +1,34 @@
|
|
|
1
1
|
// /combo session toggle — set all three (caveman, rtk, ponytail) at once.
|
|
2
2
|
// Modes: off | medium | balanced | max
|
|
3
|
-
import os from
|
|
4
|
-
import path from
|
|
5
|
-
import { createRequire } from
|
|
6
|
-
import { COMBO_LEVELS, getSharedComboState, isOmpSubagentPrompt, normalizeComboLevel, reconcileSharedComboEntries, setSharedComboLevel, setSharedComboListener, } from
|
|
7
|
-
import { readComboDefault } from
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { asPromptArray, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, systemPromptIncludes, } from '../shared/session-state.js';
|
|
7
|
+
import { readComboDefault } from '../shared/plugin-settings.js';
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
|
+
const PONYTAIL_FALLBACK_INTENSITY = {
|
|
10
|
+
lite: 'Prefer the simplest correct solution.',
|
|
11
|
+
review: 'Review only for avoidable complexity; recommend the smallest correct replacement.',
|
|
12
|
+
};
|
|
9
13
|
function ponytailFallback(mode) {
|
|
10
|
-
const intensity = mode
|
|
11
|
-
? "Prefer the simplest correct solution."
|
|
12
|
-
: mode === "review"
|
|
13
|
-
? "Review only for avoidable complexity; recommend the smallest correct replacement."
|
|
14
|
-
: "Use the minimum correct solution. Delete or reuse before adding.";
|
|
14
|
+
const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
|
|
15
15
|
return `🦥 PONYTAIL MODE ACTIVE — level: ${mode}\n${intensity} Understand the path first and fix root causes, not symptoms. Prefer the standard library and YAGNI. Avoid speculative abstractions and dependencies. Preserve correctness. Verify changed behavior.`;
|
|
16
16
|
}
|
|
17
|
-
function entriesFrom(ctx) {
|
|
18
|
-
return ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
|
|
19
|
-
}
|
|
20
17
|
function levelSummary(state) {
|
|
21
18
|
return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
|
|
22
19
|
}
|
|
23
|
-
function hasPonytailInstructions(systemPrompt) {
|
|
24
|
-
const prompts = Array.isArray(systemPrompt) ? systemPrompt : [systemPrompt];
|
|
25
|
-
return prompts.some((prompt) => typeof prompt === "string" && prompt.includes("PONYTAIL MODE ACTIVE"));
|
|
26
|
-
}
|
|
27
20
|
function loadPonytailInstructions(mode) {
|
|
28
21
|
try {
|
|
29
|
-
const installed = path.join(os.homedir(),
|
|
22
|
+
const installed = path.join(os.homedir(), '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'hooks', 'ponytail-instructions.js');
|
|
30
23
|
const { getPonytailInstructions } = require(installed);
|
|
31
|
-
if (typeof getPonytailInstructions ===
|
|
24
|
+
if (typeof getPonytailInstructions === 'function')
|
|
32
25
|
return getPonytailInstructions(mode);
|
|
33
26
|
}
|
|
34
27
|
catch { }
|
|
35
28
|
return ponytailFallback(mode);
|
|
36
29
|
}
|
|
37
30
|
export default function comboToggleExtension(pi) {
|
|
38
|
-
pi.setLabel?.(
|
|
39
|
-
let currentState = getSharedComboState();
|
|
31
|
+
pi.setLabel?.('Combo session toggle (all 3 add-ons)');
|
|
40
32
|
let lastCtx = undefined;
|
|
41
33
|
function syncStatus(ctx) {
|
|
42
34
|
if (ctx)
|
|
@@ -46,117 +38,112 @@ export default function comboToggleExtension(pi) {
|
|
|
46
38
|
return;
|
|
47
39
|
// Single unified bar replaces the three per-extension bars while a preset is
|
|
48
40
|
// active. In custom/off, the individual bars come back and combo stays clear.
|
|
49
|
-
if (
|
|
50
|
-
c.ui.setStatus(
|
|
41
|
+
if (!isComboPresetActive()) {
|
|
42
|
+
c.ui.setStatus('combo', undefined);
|
|
51
43
|
return;
|
|
52
44
|
}
|
|
45
|
+
// Read the live bridge, not cached extension state: sibling extensions
|
|
46
|
+
// reconcile it from persisted entries before this one runs.
|
|
47
|
+
const state = getSharedComboState();
|
|
53
48
|
const theme = c.ui.theme;
|
|
54
|
-
const c0 =
|
|
55
|
-
const r =
|
|
56
|
-
const p =
|
|
57
|
-
const lvl =
|
|
49
|
+
const c0 = state.caveman.toUpperCase();
|
|
50
|
+
const r = state.rtk.toUpperCase();
|
|
51
|
+
const p = state.ponytail.toUpperCase();
|
|
52
|
+
const lvl = state.level.toUpperCase();
|
|
58
53
|
const label = `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`;
|
|
59
|
-
c.ui.setStatus(
|
|
54
|
+
c.ui.setStatus('combo', theme?.fg ? `${theme.fg('accent', '🧩')} ${theme.fg('muted', label)}` : `🧩 ${label}`);
|
|
60
55
|
// Clobber any sibling bars another extension may have painted in a race
|
|
61
56
|
// during session_start — our bar is canonical for the duration of the preset.
|
|
62
|
-
c.ui.setStatus(
|
|
63
|
-
c.ui.setStatus(
|
|
64
|
-
c.ui.setStatus(
|
|
57
|
+
c.ui.setStatus('caveman', undefined);
|
|
58
|
+
c.ui.setStatus('rtk', undefined);
|
|
59
|
+
c.ui.setStatus('ponytail', undefined);
|
|
65
60
|
}
|
|
66
61
|
// ponytail: same persistence as /combo — siblings (incl. upstream ponytail)
|
|
67
62
|
// restore from these entries, so the fallback must write them too or the
|
|
68
63
|
// preset evaporates on resume and ponytail never activates.
|
|
69
64
|
function persistPreset(level) {
|
|
70
65
|
const modes = COMBO_LEVELS[level];
|
|
71
|
-
pi.appendEntry?.(
|
|
72
|
-
pi.appendEntry?.(
|
|
73
|
-
pi.appendEntry?.(
|
|
74
|
-
pi.appendEntry?.(
|
|
66
|
+
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
67
|
+
pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
|
|
68
|
+
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
69
|
+
pi.appendEntry?.('combo-level', { level });
|
|
75
70
|
}
|
|
76
71
|
function useState(state, ctx) {
|
|
77
|
-
currentState = state;
|
|
78
72
|
syncStatus(ctx);
|
|
79
73
|
return state;
|
|
80
74
|
}
|
|
81
75
|
function reconcile(ctx) {
|
|
82
76
|
if (!ctx?.hasUI)
|
|
83
|
-
return
|
|
84
|
-
return useState(reconcileSharedComboEntries(
|
|
77
|
+
return getSharedComboState();
|
|
78
|
+
return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
|
|
85
79
|
}
|
|
86
80
|
function listen(ctx) {
|
|
87
81
|
if (ctx?.hasUI)
|
|
88
82
|
setSharedComboListener((state) => useState(state));
|
|
89
83
|
}
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
function track(ctx) {
|
|
85
|
+
listen(ctx);
|
|
86
|
+
if (ctx?.hasUI)
|
|
87
|
+
reconcile(ctx);
|
|
88
|
+
}
|
|
89
|
+
pi.registerCommand?.('combo', {
|
|
90
|
+
description: 'Toggle all 3 OMP add-ons at once. Usage: /combo <off|medium|balanced|max|status>',
|
|
92
91
|
handler: async (args, ctx) => {
|
|
93
92
|
listen(ctx);
|
|
94
|
-
const arg = String(args ||
|
|
95
|
-
if (!arg || arg ===
|
|
93
|
+
const arg = String(args || '').trim().toLowerCase();
|
|
94
|
+
if (!arg || arg === 'status') {
|
|
96
95
|
const state = reconcile(ctx);
|
|
97
|
-
ctx?.ui?.notify?.(`Combo: ${state.level ===
|
|
96
|
+
ctx?.ui?.notify?.(`Combo: ${state.level === 'custom' ? 'INACTIVE' : state.level.toUpperCase()} (${levelSummary(state)})`, 'info');
|
|
98
97
|
return;
|
|
99
98
|
}
|
|
100
|
-
if (arg ===
|
|
101
|
-
ctx?.ui?.notify?.(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
if (arg === 'help') {
|
|
100
|
+
ctx?.ui?.notify?.('/combo off — disables all 3 (caveman, rtk, ponytail)\n' +
|
|
101
|
+
'/combo medium — light: caveman=lite, rtk=on, ponytail=lite\n' +
|
|
102
|
+
'/combo balanced — middle: caveman=full, rtk=on, ponytail=full\n' +
|
|
103
|
+
'/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra', 'info');
|
|
105
104
|
return;
|
|
106
105
|
}
|
|
107
106
|
const level = normalizeComboLevel(arg);
|
|
108
107
|
if (!level) {
|
|
109
|
-
ctx?.ui?.notify?.(`Unknown combo level: ${arg}. Use: off | medium | balanced | max`,
|
|
108
|
+
ctx?.ui?.notify?.(`Unknown combo level: ${arg}. Use: off | medium | balanced | max`, 'warning');
|
|
110
109
|
return;
|
|
111
110
|
}
|
|
112
111
|
persistPreset(level);
|
|
113
112
|
useState(setSharedComboLevel(level), ctx);
|
|
114
|
-
ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(
|
|
113
|
+
ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(getSharedComboState())}`, 'info');
|
|
115
114
|
if (ctx?.reload)
|
|
116
115
|
await ctx.reload();
|
|
117
116
|
},
|
|
118
117
|
});
|
|
119
|
-
pi.on(
|
|
120
|
-
|
|
121
|
-
if (ctx?.hasUI)
|
|
122
|
-
reconcile(ctx);
|
|
123
|
-
else
|
|
118
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
119
|
+
track(ctx);
|
|
120
|
+
if (!ctx?.hasUI)
|
|
124
121
|
syncStatus(ctx);
|
|
125
122
|
// Installer/user-configured default applies only to a fresh session with
|
|
126
123
|
// no persisted state — real session state always wins.
|
|
127
|
-
if (getSharedComboState().level ===
|
|
124
|
+
if (getSharedComboState().level === 'off' && !sessionEntries(ctx).length) {
|
|
128
125
|
const fallback = readComboDefault();
|
|
129
|
-
if (fallback !==
|
|
126
|
+
if (fallback !== 'off') {
|
|
130
127
|
persistPreset(fallback);
|
|
131
128
|
useState(setSharedComboLevel(fallback), ctx);
|
|
132
|
-
ctx?.ui?.notify?.(`Combo default applied: ${fallback}`,
|
|
129
|
+
ctx?.ui?.notify?.(`Combo default applied: ${fallback}`, 'info');
|
|
133
130
|
}
|
|
134
131
|
}
|
|
135
132
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
pi.on(
|
|
142
|
-
listen(ctx);
|
|
143
|
-
if (ctx?.hasUI)
|
|
144
|
-
reconcile(ctx);
|
|
145
|
-
});
|
|
146
|
-
pi.on("agent_start", async (_event, ctx) => {
|
|
147
|
-
listen(ctx);
|
|
148
|
-
if (ctx?.hasUI)
|
|
149
|
-
reconcile(ctx);
|
|
150
|
-
});
|
|
151
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
133
|
+
for (const event of ['session_branch', 'session_tree', 'agent_start']) {
|
|
134
|
+
pi.on(event, async (_event, ctx) => {
|
|
135
|
+
track(ctx);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
pi.on('before_agent_start', async (event, ctx) => {
|
|
152
139
|
if (ctx?.hasUI)
|
|
153
140
|
reconcile(ctx);
|
|
154
141
|
if (!isOmpSubagentPrompt(event.systemPrompt))
|
|
155
142
|
return;
|
|
156
143
|
const mode = getSharedComboState().ponytail;
|
|
157
|
-
if (mode ===
|
|
144
|
+
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE'))
|
|
158
145
|
return;
|
|
159
|
-
const base =
|
|
146
|
+
const base = asPromptArray(event.systemPrompt);
|
|
160
147
|
return { systemPrompt: [...base, loadPonytailInstructions(mode)] };
|
|
161
148
|
});
|
|
162
149
|
// Slash commands only; natural-language input caused accidental toggles and has no reload context.
|
package/extensions/lib/utils.js
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
1
|
// Shared helpers for the installer and the AI add-ons updater.
|
|
2
2
|
// Node built-ins only — this module must stay dependency-free.
|
|
3
|
-
import { createHash } from
|
|
4
|
-
import { createWriteStream } from
|
|
5
|
-
import fs from
|
|
6
|
-
import https from
|
|
7
|
-
import path from
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { createWriteStream } from 'node:fs';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import https from 'node:https';
|
|
7
|
+
import path from 'node:path';
|
|
8
8
|
// Promise.withResolvers is Node 22+; the package supports Node 18+.
|
|
9
9
|
// ponytail: swap for the built-in when engines bumps to >=22.
|
|
10
|
-
function withResolvers() {
|
|
10
|
+
export function withResolvers() {
|
|
11
11
|
let resolve;
|
|
12
12
|
let reject;
|
|
13
13
|
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
14
14
|
return { promise, resolve, reject };
|
|
15
15
|
}
|
|
16
|
+
export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
|
|
17
|
+
export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rules/caveman-activate.md';
|
|
18
|
+
const RTK_PLATFORM_SPECS = {
|
|
19
|
+
'win32/x64': { triple: 'x86_64-pc-windows-msvc', ext: '.zip', binary: 'rtk.exe' },
|
|
20
|
+
'linux/x64': { triple: 'x86_64-unknown-linux-musl', ext: '.tar.gz', binary: 'rtk' },
|
|
21
|
+
'linux/arm64': { triple: 'aarch64-unknown-linux-gnu', ext: '.tar.gz', binary: 'rtk' },
|
|
22
|
+
'darwin/x64': { triple: 'x86_64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
|
|
23
|
+
'darwin/arm64': { triple: 'aarch64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
|
|
24
|
+
};
|
|
25
|
+
export function rtkPlatformSpec(platform = process.platform, arch = process.arch) {
|
|
26
|
+
return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
|
|
27
|
+
}
|
|
28
|
+
// Single source for the 3xx+location redirect rule shared by httpsGet/httpsDownload.
|
|
29
|
+
function redirectNext(res, url) {
|
|
30
|
+
if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
31
|
+
return new URL(res.headers.location, url).href;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
16
35
|
export function httpsGet(url, opts = {}) {
|
|
17
36
|
const { promise, resolve, reject } = withResolvers();
|
|
18
37
|
const maxRedirects = opts.maxRedirects ?? 5;
|
|
19
|
-
const req = https.get(url, { headers: {
|
|
20
|
-
|
|
38
|
+
const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' } }, (res) => {
|
|
39
|
+
const next = redirectNext(res, url);
|
|
40
|
+
if (next) {
|
|
21
41
|
if (maxRedirects <= 0) {
|
|
22
42
|
res.resume();
|
|
23
43
|
reject(new Error(`Too many redirects fetching ${url}`));
|
|
24
44
|
return;
|
|
25
45
|
}
|
|
26
46
|
res.resume();
|
|
27
|
-
const next = new URL(res.headers.location, url).href;
|
|
28
47
|
resolve(httpsGet(next, { maxRedirects: maxRedirects - 1 }));
|
|
29
48
|
return;
|
|
30
49
|
}
|
|
@@ -33,28 +52,28 @@ export function httpsGet(url, opts = {}) {
|
|
|
33
52
|
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
34
53
|
return;
|
|
35
54
|
}
|
|
36
|
-
let body =
|
|
55
|
+
let body = '';
|
|
37
56
|
// ponytail: streamed accumulation — fine for tens of KB; stream-pipe if assets ever exceed a few MB.
|
|
38
|
-
res.setEncoding(
|
|
39
|
-
res.on(
|
|
40
|
-
res.on(
|
|
57
|
+
res.setEncoding('utf8');
|
|
58
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
59
|
+
res.on('end', () => resolve(body));
|
|
41
60
|
});
|
|
42
|
-
req.on(
|
|
61
|
+
req.on('error', reject);
|
|
43
62
|
req.setTimeout(30000, () => req.destroy(new Error(`Timeout fetching ${url}`)));
|
|
44
63
|
return promise;
|
|
45
64
|
}
|
|
46
65
|
export function httpsDownload(url, dest, opts = {}) {
|
|
47
66
|
const { promise, resolve, reject } = withResolvers();
|
|
48
67
|
const maxRedirects = opts.maxRedirects ?? 5;
|
|
49
|
-
const req = https.get(url, { headers: {
|
|
50
|
-
|
|
68
|
+
const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: '*/*' } }, (res) => {
|
|
69
|
+
const next = redirectNext(res, url);
|
|
70
|
+
if (next) {
|
|
51
71
|
if (maxRedirects <= 0) {
|
|
52
72
|
res.resume();
|
|
53
73
|
reject(new Error(`Too many redirects downloading ${url}`));
|
|
54
74
|
return;
|
|
55
75
|
}
|
|
56
76
|
res.resume();
|
|
57
|
-
const next = new URL(res.headers.location, url).href;
|
|
58
77
|
resolve(httpsDownload(next, dest, { maxRedirects: maxRedirects - 1 }));
|
|
59
78
|
return;
|
|
60
79
|
}
|
|
@@ -65,19 +84,26 @@ export function httpsDownload(url, dest, opts = {}) {
|
|
|
65
84
|
}
|
|
66
85
|
const file = createWriteStream(dest);
|
|
67
86
|
res.pipe(file);
|
|
68
|
-
file.on(
|
|
69
|
-
file.on(
|
|
87
|
+
file.on('finish', () => file.close(() => resolve()));
|
|
88
|
+
file.on('error', (err) => {
|
|
89
|
+
req.destroy();
|
|
90
|
+
res.resume();
|
|
91
|
+
reject(err);
|
|
92
|
+
});
|
|
70
93
|
});
|
|
71
|
-
req.on(
|
|
94
|
+
req.on('error', reject);
|
|
72
95
|
req.setTimeout(120000, () => req.destroy(new Error(`Timeout downloading ${url}`)));
|
|
73
96
|
return promise;
|
|
74
97
|
}
|
|
98
|
+
export async function fetchJson(url) {
|
|
99
|
+
return JSON.parse(await httpsGet(url));
|
|
100
|
+
}
|
|
75
101
|
export function sha256Hex(text) {
|
|
76
|
-
return createHash(
|
|
102
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
77
103
|
}
|
|
78
104
|
export async function sha256File(filePath) {
|
|
79
105
|
const buf = await fs.readFile(filePath);
|
|
80
|
-
return createHash(
|
|
106
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
81
107
|
}
|
|
82
108
|
export function parseChecksum(checksumsText, assetName) {
|
|
83
109
|
const target = path.basename(assetName);
|
|
@@ -88,14 +114,41 @@ export function parseChecksum(checksumsText, assetName) {
|
|
|
88
114
|
}
|
|
89
115
|
return null;
|
|
90
116
|
}
|
|
117
|
+
export async function findFile(dir, name) {
|
|
118
|
+
try {
|
|
119
|
+
const ents = await fs.readdir(dir, { withFileTypes: true });
|
|
120
|
+
for (const e of ents) {
|
|
121
|
+
const full = path.join(dir, e.name);
|
|
122
|
+
if (e.isDirectory()) {
|
|
123
|
+
const found = await findFile(full, name);
|
|
124
|
+
if (found)
|
|
125
|
+
return found;
|
|
126
|
+
}
|
|
127
|
+
else if (e.isFile()) {
|
|
128
|
+
if (e.name === name)
|
|
129
|
+
return full;
|
|
130
|
+
// Match rtk-* asset names (e.g. rtk-x86_64-unknown-linux-musl).
|
|
131
|
+
if (name === 'rtk' || name === 'rtk.exe') {
|
|
132
|
+
const base = e.name.toLowerCase();
|
|
133
|
+
if (!/\.(txt|md|json|sha256|sig|asc|pem|crt|license)$/i.test(base)) {
|
|
134
|
+
if (base === 'rtk' || base === 'rtk.exe' || /^rtk[-_.]/.test(base))
|
|
135
|
+
return full;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch { /* ignore */ }
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
91
144
|
export async function readTextIfExists(p) {
|
|
92
145
|
try {
|
|
93
|
-
return await fs.readFile(p,
|
|
146
|
+
return await fs.readFile(p, 'utf8');
|
|
94
147
|
}
|
|
95
148
|
catch {
|
|
96
149
|
return null;
|
|
97
150
|
}
|
|
98
151
|
}
|
|
99
152
|
export function normalizeRtkVersion(value) {
|
|
100
|
-
return String(value ||
|
|
153
|
+
return String(value || '').replace(/^rtk\s+/i, '').replace(/^v/i, '').trim();
|
|
101
154
|
}
|