@krtclcdy/tersio 1.0.4 → 2.0.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 +22 -9
- package/extensions/ai-addons-updater/index.js +166 -238
- package/extensions/caveman-session/index.js +48 -67
- package/extensions/combo-toggle/index.js +55 -69
- package/extensions/lib/utils.js +77 -24
- package/extensions/rtk-session/index.js +48 -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 +673 -750
- 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, 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,83 @@ 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);
|
|
120
99
|
// Persisted session state wins; a fresh session falls back to the
|
|
121
100
|
// installer/user-configured default (off unless configured).
|
|
122
|
-
const persisted = resolveMode(entries,
|
|
101
|
+
const persisted = resolveMode(entries, '');
|
|
123
102
|
currentMode = persisted || readCavemanDefault();
|
|
124
103
|
syncStatus(ctx);
|
|
125
104
|
}
|
|
126
|
-
pi.on(
|
|
105
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
127
106
|
restoreMode(ctx);
|
|
128
|
-
ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`,
|
|
107
|
+
ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
|
|
129
108
|
});
|
|
130
|
-
pi.on(
|
|
109
|
+
pi.on('session_branch', async (_event, ctx) => {
|
|
131
110
|
restoreMode(ctx);
|
|
132
111
|
});
|
|
133
|
-
pi.on(
|
|
112
|
+
pi.on('session_tree', async (_event, ctx) => {
|
|
134
113
|
restoreMode(ctx);
|
|
135
114
|
});
|
|
136
|
-
pi.on(
|
|
115
|
+
pi.on('agent_start', async (_event, ctx) => {
|
|
137
116
|
isActive = true;
|
|
138
117
|
syncStatus(ctx);
|
|
139
118
|
});
|
|
140
|
-
pi.on(
|
|
119
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
141
120
|
isActive = false;
|
|
142
121
|
syncStatus(ctx);
|
|
143
122
|
});
|
|
144
|
-
pi.on(
|
|
123
|
+
pi.on('before_agent_start', async (event) => {
|
|
145
124
|
const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
|
|
146
|
-
if (!mode || mode ===
|
|
125
|
+
if (!mode || mode === 'off')
|
|
147
126
|
return;
|
|
148
127
|
const def = INSTRUCTIONS[mode];
|
|
149
|
-
|
|
150
|
-
|
|
128
|
+
if (!def)
|
|
129
|
+
return;
|
|
130
|
+
const instruction = typeof def === 'function' ? def() : def;
|
|
131
|
+
const base = asPromptArray(event.systemPrompt);
|
|
151
132
|
return { systemPrompt: [...base, instruction] };
|
|
152
133
|
});
|
|
153
134
|
}
|
|
@@ -1,41 +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?.(
|
|
31
|
+
pi.setLabel?.('Combo session toggle (all 3 add-ons)');
|
|
39
32
|
let currentState = getSharedComboState();
|
|
40
33
|
let lastCtx = undefined;
|
|
41
34
|
function syncStatus(ctx) {
|
|
@@ -46,8 +39,8 @@ export default function comboToggleExtension(pi) {
|
|
|
46
39
|
return;
|
|
47
40
|
// Single unified bar replaces the three per-extension bars while a preset is
|
|
48
41
|
// active. In custom/off, the individual bars come back and combo stays clear.
|
|
49
|
-
if (
|
|
50
|
-
c.ui.setStatus(
|
|
42
|
+
if (!isComboPresetActive()) {
|
|
43
|
+
c.ui.setStatus('combo', undefined);
|
|
51
44
|
return;
|
|
52
45
|
}
|
|
53
46
|
const theme = c.ui.theme;
|
|
@@ -56,22 +49,22 @@ export default function comboToggleExtension(pi) {
|
|
|
56
49
|
const p = currentState.ponytail.toUpperCase();
|
|
57
50
|
const lvl = currentState.level.toUpperCase();
|
|
58
51
|
const label = `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`;
|
|
59
|
-
c.ui.setStatus(
|
|
52
|
+
c.ui.setStatus('combo', theme?.fg ? `${theme.fg('accent', '🧩')} ${theme.fg('muted', label)}` : `🧩 ${label}`);
|
|
60
53
|
// Clobber any sibling bars another extension may have painted in a race
|
|
61
54
|
// 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(
|
|
55
|
+
c.ui.setStatus('caveman', undefined);
|
|
56
|
+
c.ui.setStatus('rtk', undefined);
|
|
57
|
+
c.ui.setStatus('ponytail', undefined);
|
|
65
58
|
}
|
|
66
59
|
// ponytail: same persistence as /combo — siblings (incl. upstream ponytail)
|
|
67
60
|
// restore from these entries, so the fallback must write them too or the
|
|
68
61
|
// preset evaporates on resume and ponytail never activates.
|
|
69
62
|
function persistPreset(level) {
|
|
70
63
|
const modes = COMBO_LEVELS[level];
|
|
71
|
-
pi.appendEntry?.(
|
|
72
|
-
pi.appendEntry?.(
|
|
73
|
-
pi.appendEntry?.(
|
|
74
|
-
pi.appendEntry?.(
|
|
64
|
+
pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
|
|
65
|
+
pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
|
|
66
|
+
pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
|
|
67
|
+
pi.appendEntry?.('combo-level', { level });
|
|
75
68
|
}
|
|
76
69
|
function useState(state, ctx) {
|
|
77
70
|
currentState = state;
|
|
@@ -81,82 +74,75 @@ export default function comboToggleExtension(pi) {
|
|
|
81
74
|
function reconcile(ctx) {
|
|
82
75
|
if (!ctx?.hasUI)
|
|
83
76
|
return currentState;
|
|
84
|
-
return useState(reconcileSharedComboEntries(
|
|
77
|
+
return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
|
|
85
78
|
}
|
|
86
79
|
function listen(ctx) {
|
|
87
80
|
if (ctx?.hasUI)
|
|
88
81
|
setSharedComboListener((state) => useState(state));
|
|
89
82
|
}
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
function track(ctx) {
|
|
84
|
+
listen(ctx);
|
|
85
|
+
if (ctx?.hasUI)
|
|
86
|
+
reconcile(ctx);
|
|
87
|
+
}
|
|
88
|
+
pi.registerCommand?.('combo', {
|
|
89
|
+
description: 'Toggle all 3 OMP add-ons at once. Usage: /combo <off|medium|balanced|max|status>',
|
|
92
90
|
handler: async (args, ctx) => {
|
|
93
91
|
listen(ctx);
|
|
94
|
-
const arg = String(args ||
|
|
95
|
-
if (!arg || arg ===
|
|
92
|
+
const arg = String(args || '').trim().toLowerCase();
|
|
93
|
+
if (!arg || arg === 'status') {
|
|
96
94
|
const state = reconcile(ctx);
|
|
97
|
-
ctx?.ui?.notify?.(`Combo: ${state.level ===
|
|
95
|
+
ctx?.ui?.notify?.(`Combo: ${state.level === 'custom' ? 'INACTIVE' : state.level.toUpperCase()} (${levelSummary(state)})`, 'info');
|
|
98
96
|
return;
|
|
99
97
|
}
|
|
100
|
-
if (arg ===
|
|
101
|
-
ctx?.ui?.notify?.(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
if (arg === 'help') {
|
|
99
|
+
ctx?.ui?.notify?.('/combo off — disables all 3 (caveman, rtk, ponytail)\n' +
|
|
100
|
+
'/combo medium — light: caveman=lite, rtk=on, ponytail=lite\n' +
|
|
101
|
+
'/combo balanced — middle: caveman=full, rtk=on, ponytail=full\n' +
|
|
102
|
+
'/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra', 'info');
|
|
105
103
|
return;
|
|
106
104
|
}
|
|
107
105
|
const level = normalizeComboLevel(arg);
|
|
108
106
|
if (!level) {
|
|
109
|
-
ctx?.ui?.notify?.(`Unknown combo level: ${arg}. Use: off | medium | balanced | max`,
|
|
107
|
+
ctx?.ui?.notify?.(`Unknown combo level: ${arg}. Use: off | medium | balanced | max`, 'warning');
|
|
110
108
|
return;
|
|
111
109
|
}
|
|
112
110
|
persistPreset(level);
|
|
113
111
|
useState(setSharedComboLevel(level), ctx);
|
|
114
|
-
ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(currentState)}`,
|
|
112
|
+
ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(currentState)}`, 'info');
|
|
115
113
|
if (ctx?.reload)
|
|
116
114
|
await ctx.reload();
|
|
117
115
|
},
|
|
118
116
|
});
|
|
119
|
-
pi.on(
|
|
120
|
-
|
|
121
|
-
if (ctx?.hasUI)
|
|
122
|
-
reconcile(ctx);
|
|
123
|
-
else
|
|
117
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
118
|
+
track(ctx);
|
|
119
|
+
if (!ctx?.hasUI)
|
|
124
120
|
syncStatus(ctx);
|
|
125
121
|
// Installer/user-configured default applies only to a fresh session with
|
|
126
122
|
// no persisted state — real session state always wins.
|
|
127
|
-
if (getSharedComboState().level ===
|
|
123
|
+
if (getSharedComboState().level === 'off' && !sessionEntries(ctx).length) {
|
|
128
124
|
const fallback = readComboDefault();
|
|
129
|
-
if (fallback !==
|
|
125
|
+
if (fallback !== 'off') {
|
|
130
126
|
persistPreset(fallback);
|
|
131
127
|
useState(setSharedComboLevel(fallback), ctx);
|
|
132
|
-
ctx?.ui?.notify?.(`Combo default applied: ${fallback}`,
|
|
128
|
+
ctx?.ui?.notify?.(`Combo default applied: ${fallback}`, 'info');
|
|
133
129
|
}
|
|
134
130
|
}
|
|
135
131
|
});
|
|
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) => {
|
|
132
|
+
for (const event of ['session_branch', 'session_tree', 'agent_start']) {
|
|
133
|
+
pi.on(event, async (_event, ctx) => {
|
|
134
|
+
track(ctx);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
pi.on('before_agent_start', async (event, ctx) => {
|
|
152
138
|
if (ctx?.hasUI)
|
|
153
139
|
reconcile(ctx);
|
|
154
140
|
if (!isOmpSubagentPrompt(event.systemPrompt))
|
|
155
141
|
return;
|
|
156
142
|
const mode = getSharedComboState().ponytail;
|
|
157
|
-
if (mode ===
|
|
143
|
+
if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE'))
|
|
158
144
|
return;
|
|
159
|
-
const base =
|
|
145
|
+
const base = asPromptArray(event.systemPrompt);
|
|
160
146
|
return { systemPrompt: [...base, loadPonytailInstructions(mode)] };
|
|
161
147
|
});
|
|
162
148
|
// 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
|
}
|