@krtclcdy/tersio 1.0.1 → 1.0.2
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/extensions/ai-addons-updater/index.js +398 -0
- package/extensions/amanai-reward/index.js +24 -0
- package/extensions/amanai-reward/pi.js +32 -0
- package/extensions/caveman-session/index.js +153 -0
- package/extensions/combo-toggle/index.js +157 -0
- package/extensions/lib/utils.js +101 -0
- package/extensions/rtk-session/index.js +151 -0
- package/package.json +2 -1
- package/tersio.js +1143 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// OMP extension: /ai-addons manual updater for Ponytail, RTK, Caveman.
|
|
2
|
+
// Built-in Node modules only. Default off; registers a single slash command.
|
|
3
|
+
// ponytail: `skipped: none` — semantics match one-liner: fetch + compare + run install.
|
|
4
|
+
// rtk: `skipped: signature verification` — checksums.txt ships only SHA256 of release assets; add sigchain when upstream publishes a signing key.
|
|
5
|
+
// caveman: `skipped: none` — exactly the ask: write rule.md, report old/new hash.
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, } from "../lib/utils.js";
|
|
12
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
13
|
+
const HOME = os.homedir();
|
|
14
|
+
const PONYTAIL_REMOTE = "https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json";
|
|
15
|
+
const PONYTAIL_LOCAL = path.join(HOME, ".omp", "plugins", "node_modules", "@dietrichgebert", "ponytail", "package.json");
|
|
16
|
+
const RTK_RELEASE_API = "https://api.github.com/repos/rtk-ai/rtk/releases/latest";
|
|
17
|
+
const RTK_BINARY = path.join(HOME, ".bun", "bin", IS_WINDOWS ? "rtk.exe" : "rtk");
|
|
18
|
+
const CAVEMAN_REMOTE = "https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rules/caveman-activate.md";
|
|
19
|
+
const CAVEMAN_LOCAL = path.join(HOME, ".omp", "agent", "extensions", "caveman-session", "rule.md");
|
|
20
|
+
const RELOAD_MSG = "Reminder: restart OMP (or reload extensions) for updates to take effect.";
|
|
21
|
+
function notify(ctx, msg, level) {
|
|
22
|
+
ctx?.ui?.notify?.(String(msg), level);
|
|
23
|
+
}
|
|
24
|
+
// Check: no mutation.
|
|
25
|
+
async function checkAddons(ctx) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
// Ponytail
|
|
28
|
+
try {
|
|
29
|
+
const remoteRaw = await httpsGet(PONYTAIL_REMOTE);
|
|
30
|
+
const remoteJson = JSON.parse(remoteRaw);
|
|
31
|
+
const localRaw = await readTextIfExists(PONYTAIL_LOCAL);
|
|
32
|
+
const localVer = localRaw ? JSON.parse(localRaw).version ?? null : null;
|
|
33
|
+
const remoteVer = remoteJson.version;
|
|
34
|
+
const status = !localVer ? "not installed"
|
|
35
|
+
: localVer === remoteVer ? "up to date"
|
|
36
|
+
: "update available";
|
|
37
|
+
const m = `Ponytail ${status}: local=${localVer || "—"} latest=${remoteVer}`;
|
|
38
|
+
lines.push(m);
|
|
39
|
+
notify(ctx, m, "info");
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
const m = `Ponytail check failed: ${e.message}`;
|
|
43
|
+
lines.push(m);
|
|
44
|
+
notify(ctx, m, "warning");
|
|
45
|
+
}
|
|
46
|
+
// RTK
|
|
47
|
+
try {
|
|
48
|
+
const releaseRaw = await httpsGet(RTK_RELEASE_API);
|
|
49
|
+
const release = JSON.parse(releaseRaw);
|
|
50
|
+
const latestTag = release.tag_name || null;
|
|
51
|
+
let localVer = null;
|
|
52
|
+
try {
|
|
53
|
+
const out = execFileSync(RTK_BINARY, ["--version"], { encoding: "utf8", windowsHide: true, shell: false, timeout: 10000 }) || "";
|
|
54
|
+
if (out)
|
|
55
|
+
localVer = out.trim().split(/\r?\n/)[0];
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
localVer = null;
|
|
59
|
+
}
|
|
60
|
+
const status = localVer == null ? "not installed"
|
|
61
|
+
: normalizeRtkVersion(localVer) === normalizeRtkVersion(latestTag ?? undefined) ? "up to date"
|
|
62
|
+
: "update available";
|
|
63
|
+
const m = `RTK ${status}: local=${localVer || "—"} latest=${latestTag || "—"}`;
|
|
64
|
+
lines.push(m);
|
|
65
|
+
notify(ctx, m, "info");
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
const m = `RTK check failed: ${e.message}`;
|
|
69
|
+
lines.push(m);
|
|
70
|
+
notify(ctx, m, "warning");
|
|
71
|
+
}
|
|
72
|
+
// Caveman (rule.md)
|
|
73
|
+
try {
|
|
74
|
+
const remote = await httpsGet(CAVEMAN_REMOTE);
|
|
75
|
+
const remoteHash = sha256Hex(remote).slice(0, 16);
|
|
76
|
+
const local = await readTextIfExists(CAVEMAN_LOCAL);
|
|
77
|
+
const localHash = local ? sha256Hex(local).slice(0, 16) : null;
|
|
78
|
+
const status = !local ? "rule.md missing"
|
|
79
|
+
: localHash === remoteHash ? "rule.md up to date"
|
|
80
|
+
: "rule.md update available";
|
|
81
|
+
const m = `Caveman ${status}: local=${localHash || "—"} remote=${remoteHash}`;
|
|
82
|
+
lines.push(m);
|
|
83
|
+
notify(ctx, m, "info");
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
const m = `Caveman check failed: ${e.message}`;
|
|
87
|
+
lines.push(m);
|
|
88
|
+
notify(ctx, m, "warning");
|
|
89
|
+
}
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
async function updatePonytail(pi, ctx, dryRun = false) {
|
|
93
|
+
const pluginsDir = path.join(HOME, ".omp", "plugins");
|
|
94
|
+
if (dryRun) {
|
|
95
|
+
const m = `Ponytail dry-run: would run \`npm install @dietrichgebert/ponytail@latest --save --no-audit --no-fund\` in ${pluginsDir}.`;
|
|
96
|
+
notify(ctx, m, "info");
|
|
97
|
+
return m;
|
|
98
|
+
}
|
|
99
|
+
notify(ctx, "Ponytail: ensuring plugin directory exists…", "info");
|
|
100
|
+
try {
|
|
101
|
+
await fs.mkdir(pluginsDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
const m = `Ponytail update failed: failed to create ${pluginsDir}: ${e.message}`;
|
|
105
|
+
notify(ctx, m, "warning");
|
|
106
|
+
return m;
|
|
107
|
+
}
|
|
108
|
+
notify(ctx, "Ponytail: running npm install…", "info");
|
|
109
|
+
let out = "";
|
|
110
|
+
try {
|
|
111
|
+
const r = await pi.exec("npm", ["install", "@dietrichgebert/ponytail@latest", "--save", "--no-audit", "--no-fund"], { cwd: pluginsDir });
|
|
112
|
+
out = [r.stdout, r.stderr].filter(Boolean).join("\n").trim();
|
|
113
|
+
if (r.code !== 0)
|
|
114
|
+
throw new Error(r.stderr || `npm exited ${r.code}`);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
const m = `Ponytail update failed: ${e.message}`;
|
|
118
|
+
notify(ctx, m, "warning");
|
|
119
|
+
return m;
|
|
120
|
+
}
|
|
121
|
+
const m = `Ponytail update finished.${out ? `\n${out}` : ""}\n${RELOAD_MSG}`;
|
|
122
|
+
notify(ctx, "Ponytail update finished. " + RELOAD_MSG, "info");
|
|
123
|
+
return m;
|
|
124
|
+
}
|
|
125
|
+
async function updateRtk(ctx, dryRun = false) {
|
|
126
|
+
let release;
|
|
127
|
+
try {
|
|
128
|
+
const raw = await httpsGet(RTK_RELEASE_API);
|
|
129
|
+
release = JSON.parse(raw);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
const m = `RTK: cannot fetch release info: ${e.message}`;
|
|
133
|
+
notify(ctx, m, "warning");
|
|
134
|
+
return m;
|
|
135
|
+
}
|
|
136
|
+
const tag = release.tag_name || "unknown";
|
|
137
|
+
const assets = Array.isArray(release.assets) ? release.assets : [];
|
|
138
|
+
// Cross-platform asset selection (mirrors installer stepRtk)
|
|
139
|
+
const PLATFORM = process.platform;
|
|
140
|
+
const ARCH = process.arch;
|
|
141
|
+
let assetTriple;
|
|
142
|
+
let assetExt;
|
|
143
|
+
let binaryName;
|
|
144
|
+
if (PLATFORM === "win32" && ARCH === "x64") {
|
|
145
|
+
assetTriple = "x86_64-pc-windows-msvc";
|
|
146
|
+
assetExt = ".zip";
|
|
147
|
+
binaryName = "rtk.exe";
|
|
148
|
+
}
|
|
149
|
+
else if (PLATFORM === "linux" && ARCH === "x64") {
|
|
150
|
+
assetTriple = "x86_64-unknown-linux-musl";
|
|
151
|
+
assetExt = ".tar.gz";
|
|
152
|
+
binaryName = "rtk";
|
|
153
|
+
}
|
|
154
|
+
else if (PLATFORM === "linux" && ARCH === "arm64") {
|
|
155
|
+
assetTriple = "aarch64-unknown-linux-gnu";
|
|
156
|
+
assetExt = ".tar.gz";
|
|
157
|
+
binaryName = "rtk";
|
|
158
|
+
}
|
|
159
|
+
else if (PLATFORM === "darwin" && ARCH === "x64") {
|
|
160
|
+
assetTriple = "x86_64-apple-darwin";
|
|
161
|
+
assetExt = ".tar.gz";
|
|
162
|
+
binaryName = "rtk";
|
|
163
|
+
}
|
|
164
|
+
else if (PLATFORM === "darwin" && ARCH === "arm64") {
|
|
165
|
+
assetTriple = "aarch64-apple-darwin";
|
|
166
|
+
assetExt = ".tar.gz";
|
|
167
|
+
binaryName = "rtk";
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const m = `RTK: unsupported platform ${PLATFORM}/${ARCH}`;
|
|
171
|
+
notify(ctx, m, "warning");
|
|
172
|
+
return m;
|
|
173
|
+
}
|
|
174
|
+
const asset = assets.find((a) => a.name === `rtk-${assetTriple}${assetExt}`);
|
|
175
|
+
const checksAsset = assets.find((a) => a.name === "checksums.txt");
|
|
176
|
+
if (!asset || !checksAsset) {
|
|
177
|
+
const m = `RTK: required assets not found in release ${tag} (need rtk-${assetTriple}${assetExt} and checksums.txt)`;
|
|
178
|
+
notify(ctx, m, "warning");
|
|
179
|
+
return m;
|
|
180
|
+
}
|
|
181
|
+
if (dryRun) {
|
|
182
|
+
const m = `RTK dry-run: would download ${asset.name} (${tag}), verify checksums.txt, and replace ${RTK_BINARY}.`;
|
|
183
|
+
notify(ctx, m, "info");
|
|
184
|
+
return m;
|
|
185
|
+
}
|
|
186
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "rtk-update-"));
|
|
187
|
+
const archivePath = path.join(tmp, asset.name);
|
|
188
|
+
const checksPath = path.join(tmp, "checksums.txt");
|
|
189
|
+
try {
|
|
190
|
+
notify(ctx, `RTK: downloading ${asset.name} (${tag})…`, "info");
|
|
191
|
+
await httpsDownload(asset.browser_download_url, archivePath);
|
|
192
|
+
notify(ctx, "RTK: downloading checksums.txt…", "info");
|
|
193
|
+
await httpsDownload(checksAsset.browser_download_url, checksPath);
|
|
194
|
+
// Verify SHA256 against checksums.txt
|
|
195
|
+
const checks = await fs.readFile(checksPath, "utf8");
|
|
196
|
+
const expected = parseChecksum(checks, asset.name);
|
|
197
|
+
if (!expected) {
|
|
198
|
+
const m = `RTK: checksums.txt has no entry for ${asset.name}`;
|
|
199
|
+
notify(ctx, m, "warning");
|
|
200
|
+
return m;
|
|
201
|
+
}
|
|
202
|
+
const archiveBuf = await fs.readFile(archivePath);
|
|
203
|
+
const actual = createHash("sha256").update(archiveBuf).digest("hex").toLowerCase();
|
|
204
|
+
if (actual !== expected) {
|
|
205
|
+
const m = `RTK: checksum mismatch! expected=${expected.slice(0, 12)}… actual=${actual.slice(0, 12)}…`;
|
|
206
|
+
notify(ctx, m, "warning");
|
|
207
|
+
return m;
|
|
208
|
+
}
|
|
209
|
+
notify(ctx, "RTK: checksum verified.", "info");
|
|
210
|
+
// Extract by archive format
|
|
211
|
+
const extractDir = path.join(tmp, "extracted");
|
|
212
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
213
|
+
if (asset.name.endsWith(".zip")) {
|
|
214
|
+
if (IS_WINDOWS) {
|
|
215
|
+
execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
|
216
|
+
`Expand-Archive -LiteralPath "${archivePath}" -DestinationPath "${extractDir}" -Force`], { encoding: "utf8", windowsHide: true, shell: false });
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
execFileSync("unzip", [archivePath, "-d", extractDir], { encoding: "utf8", shell: false });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (asset.name.endsWith(".tar.gz") || asset.name.endsWith(".tgz")) {
|
|
223
|
+
try {
|
|
224
|
+
execFileSync("tar", ["xzf", archivePath, "-C", extractDir], { encoding: "utf8", shell: false });
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
execFileSync("sh", ["-c", `gunzip < "${archivePath}" | tar xf - -C "${extractDir}"`], { encoding: "utf8", shell: false });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
throw new Error(`Unknown archive format: ${asset.name}`);
|
|
232
|
+
}
|
|
233
|
+
const rtkExtracted = await findFile(extractDir, binaryName);
|
|
234
|
+
if (!rtkExtracted) {
|
|
235
|
+
const m = `RTK: ${binaryName} not found in extracted archive`;
|
|
236
|
+
notify(ctx, m, "warning");
|
|
237
|
+
return m;
|
|
238
|
+
}
|
|
239
|
+
await fs.mkdir(path.dirname(RTK_BINARY), { recursive: true });
|
|
240
|
+
const backupPath = `${RTK_BINARY}.bak`;
|
|
241
|
+
let backedUp = false;
|
|
242
|
+
try {
|
|
243
|
+
await fs.copyFile(RTK_BINARY, backupPath);
|
|
244
|
+
backedUp = true;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
backedUp = false;
|
|
248
|
+
}
|
|
249
|
+
await fs.copyFile(rtkExtracted, RTK_BINARY);
|
|
250
|
+
// Set executable bit on Unix
|
|
251
|
+
if (!IS_WINDOWS) {
|
|
252
|
+
await fs.chmod(RTK_BINARY, 0o755);
|
|
253
|
+
}
|
|
254
|
+
let versionOut = "";
|
|
255
|
+
try {
|
|
256
|
+
versionOut = execFileSync(RTK_BINARY, ["--version"], { encoding: "utf8", windowsHide: true, shell: false, timeout: 10000 }).trim();
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
if (backedUp)
|
|
260
|
+
await fs.copyFile(backupPath, RTK_BINARY);
|
|
261
|
+
throw new Error(`new ${binaryName} failed --version${backedUp ? "; restored backup" : ""}: ${e.message}`);
|
|
262
|
+
}
|
|
263
|
+
if (normalizeRtkVersion(versionOut) !== normalizeRtkVersion(tag)) {
|
|
264
|
+
if (backedUp)
|
|
265
|
+
await fs.copyFile(backupPath, RTK_BINARY);
|
|
266
|
+
throw new Error(`new ${binaryName} reports ${versionOut}, expected ${tag}${backedUp ? "; restored backup" : ""}`);
|
|
267
|
+
}
|
|
268
|
+
const m = `RTK updated to ${tag} → ${RTK_BINARY}\nbackup=${backedUp ? backupPath : "—"}\n${RELOAD_MSG}`;
|
|
269
|
+
notify(ctx, "RTK update finished. " + RELOAD_MSG, "info");
|
|
270
|
+
return m;
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
const m = `RTK update failed: ${e.message}`;
|
|
274
|
+
notify(ctx, m, "warning");
|
|
275
|
+
return m;
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
fs.rm(tmp, { recursive: true, force: true }).catch(() => { });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function findFile(dir, name) {
|
|
282
|
+
try {
|
|
283
|
+
const ents = await fs.readdir(dir, { withFileTypes: true });
|
|
284
|
+
for (const e of ents) {
|
|
285
|
+
const full = path.join(dir, e.name);
|
|
286
|
+
if (e.isDirectory()) {
|
|
287
|
+
const r = await findFile(full, name);
|
|
288
|
+
if (r)
|
|
289
|
+
return r;
|
|
290
|
+
}
|
|
291
|
+
else if (e.isFile()) {
|
|
292
|
+
if (e.name === name)
|
|
293
|
+
return full;
|
|
294
|
+
// Match rtk-* asset names (e.g. rtk-x86_64-unknown-linux-musl)
|
|
295
|
+
if (name === "rtk" || name === "rtk.exe") {
|
|
296
|
+
const base = e.name.toLowerCase();
|
|
297
|
+
if (!/\.(txt|md|json|sha256|sig|asc|pem|crt|license)$/i.test(base)) {
|
|
298
|
+
if (base === "rtk" || base === "rtk.exe" || /^rtk[-_.]/.test(base))
|
|
299
|
+
return full;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch { /* ignore */ }
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
async function updateCaveman(ctx, dryRun = false) {
|
|
309
|
+
let remote;
|
|
310
|
+
try {
|
|
311
|
+
remote = await httpsGet(CAVEMAN_REMOTE);
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
const m = `Caveman update failed: ${e.message}`;
|
|
315
|
+
notify(ctx, m, "warning");
|
|
316
|
+
return m;
|
|
317
|
+
}
|
|
318
|
+
const remoteHash = sha256Hex(remote).slice(0, 16);
|
|
319
|
+
const oldLocal = await readTextIfExists(CAVEMAN_LOCAL);
|
|
320
|
+
const oldHash = oldLocal ? sha256Hex(oldLocal).slice(0, 16) : null;
|
|
321
|
+
if (dryRun) {
|
|
322
|
+
const m = `Caveman dry-run: would write ${CAVEMAN_LOCAL}\nold=${oldHash || "—"} new=${remoteHash}.`;
|
|
323
|
+
notify(ctx, m, "info");
|
|
324
|
+
return m;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
await fs.mkdir(path.dirname(CAVEMAN_LOCAL), { recursive: true });
|
|
328
|
+
const backupPath = `${CAVEMAN_LOCAL}.bak`;
|
|
329
|
+
if (oldLocal !== null)
|
|
330
|
+
await fs.writeFile(backupPath, oldLocal, "utf8");
|
|
331
|
+
await fs.writeFile(CAVEMAN_LOCAL, remote, "utf8");
|
|
332
|
+
const written = await fs.readFile(CAVEMAN_LOCAL, "utf8");
|
|
333
|
+
const writtenHash = sha256Hex(written).slice(0, 16);
|
|
334
|
+
if (writtenHash !== remoteHash) {
|
|
335
|
+
if (oldLocal !== null)
|
|
336
|
+
await fs.writeFile(CAVEMAN_LOCAL, oldLocal, "utf8");
|
|
337
|
+
throw new Error(`written hash ${writtenHash} did not match remote ${remoteHash}${oldLocal !== null ? "; restored backup" : ""}`);
|
|
338
|
+
}
|
|
339
|
+
const m = `Caveman rule.md updated → ${CAVEMAN_LOCAL}\nold=${oldHash || "—"} new=${remoteHash}\nbackup=${oldLocal !== null ? backupPath : "—"}\n${RELOAD_MSG}`;
|
|
340
|
+
notify(ctx, "Caveman rule.md updated. " + RELOAD_MSG, "info");
|
|
341
|
+
return m;
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
const m = `Caveman update failed: ${e.message}`;
|
|
345
|
+
notify(ctx, m, "warning");
|
|
346
|
+
return m;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
export default function aiAddonsUpdaterExtension(pi) {
|
|
350
|
+
pi.setLabel?.("AI add-ons updater");
|
|
351
|
+
pi.registerCommand?.("ai-addons", {
|
|
352
|
+
description: "Check or update AI add-ons (ponytail/rtk/caveman/all). Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]",
|
|
353
|
+
handler: async (args, ctx) => {
|
|
354
|
+
const arg = String(args || "").trim().toLowerCase();
|
|
355
|
+
const parts = arg.split(/\s+/).filter(Boolean);
|
|
356
|
+
const dryRun = parts.includes("--dry-run") || parts.includes("dry-run");
|
|
357
|
+
const cleanParts = parts.filter((p) => p !== "--dry-run" && p !== "dry-run");
|
|
358
|
+
const sub = cleanParts[0];
|
|
359
|
+
if (sub === "check" || sub === "status") {
|
|
360
|
+
const summary = await checkAddons(ctx);
|
|
361
|
+
notify(ctx, "ai-addons check complete.", "info");
|
|
362
|
+
return summary;
|
|
363
|
+
}
|
|
364
|
+
if (sub === "update" && cleanParts[1]) {
|
|
365
|
+
const target = cleanParts.slice(1).join(" ");
|
|
366
|
+
const results = [];
|
|
367
|
+
if (target === "ponytail") {
|
|
368
|
+
results.push(await updatePonytail(pi, ctx, dryRun));
|
|
369
|
+
}
|
|
370
|
+
else if (target === "rtk") {
|
|
371
|
+
results.push(await updateRtk(ctx, dryRun));
|
|
372
|
+
}
|
|
373
|
+
else if (target === "caveman") {
|
|
374
|
+
results.push(await updateCaveman(ctx, dryRun));
|
|
375
|
+
}
|
|
376
|
+
else if (target === "all") {
|
|
377
|
+
notify(ctx, `ai-addons update all${dryRun ? " dry-run" : ""}: starting ponytail → rtk → caveman sequentially…`, "info");
|
|
378
|
+
results.push(await updatePonytail(pi, ctx, dryRun));
|
|
379
|
+
results.push(await updateRtk(ctx, dryRun));
|
|
380
|
+
results.push(await updateCaveman(ctx, dryRun));
|
|
381
|
+
if (!dryRun)
|
|
382
|
+
results.push(RELOAD_MSG);
|
|
383
|
+
notify(ctx, `ai-addons update all ${dryRun ? "dry-run " : ""}complete.${dryRun ? "" : ` ${RELOAD_MSG}`}`, "info");
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const m = "Usage: /ai-addons update <ponytail|rtk|caveman|all> [--dry-run]";
|
|
387
|
+
notify(ctx, m, "warning");
|
|
388
|
+
return m;
|
|
389
|
+
}
|
|
390
|
+
return results.join("\n\n");
|
|
391
|
+
}
|
|
392
|
+
const m = "Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]";
|
|
393
|
+
notify(ctx, m, "warning");
|
|
394
|
+
return m;
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
export { parseChecksum };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const REWARD_KEY = /(?<![A-Za-z0-9_-])AMANAI-GACHA-[A-Za-z0-9]+-[A-Za-z0-9]+(?![A-Za-z0-9_-])/;
|
|
2
|
+
const NOTIFICATION = "Amanai reward key detected in the final response. Redeem it manually in the Amanai billing dashboard.";
|
|
3
|
+
export default function amanaiRewardExtension(pi) {
|
|
4
|
+
pi.on("agent_end", (event, ctx) => {
|
|
5
|
+
if (event.willContinue)
|
|
6
|
+
return;
|
|
7
|
+
const messages = Array.isArray(event.messages) ? event.messages : [];
|
|
8
|
+
let message;
|
|
9
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
10
|
+
if (messages[index]?.role === "assistant") {
|
|
11
|
+
message = messages[index];
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (!message || message.stopReason !== "stop" || !Array.isArray(message.content))
|
|
16
|
+
return;
|
|
17
|
+
for (const block of message.content) {
|
|
18
|
+
if (block?.type === "text" && typeof block.text === "string" && REWARD_KEY.test(block.text)) {
|
|
19
|
+
ctx?.ui?.notify?.(NOTIFICATION, "info");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const REWARD_KEY = /(?<![A-Za-z0-9_-])AMANAI-GACHA-[A-Za-z0-9]+-[A-Za-z0-9]+(?![A-Za-z0-9_-])/;
|
|
2
|
+
const NOTIFICATION = "Amanai reward key detected in the final response. Redeem it manually in the Amanai billing dashboard.";
|
|
3
|
+
export default function amanaiRewardExtension(pi) {
|
|
4
|
+
let candidate = false;
|
|
5
|
+
pi.on("agent_start", () => {
|
|
6
|
+
candidate = false;
|
|
7
|
+
});
|
|
8
|
+
pi.on("agent_end", (event) => {
|
|
9
|
+
candidate = false;
|
|
10
|
+
const messages = Array.isArray(event?.messages) ? event.messages : [];
|
|
11
|
+
let message;
|
|
12
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
13
|
+
if (messages[index]?.role === "assistant") {
|
|
14
|
+
message = messages[index];
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (message?.stopReason !== "stop" || !Array.isArray(message.content))
|
|
19
|
+
return;
|
|
20
|
+
candidate = message.content.some((block) => block?.type === "text" && typeof block.text === "string" && REWARD_KEY.test(block.text));
|
|
21
|
+
});
|
|
22
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
23
|
+
const c = ctx;
|
|
24
|
+
try {
|
|
25
|
+
if (candidate && c?.hasUI && c.ui)
|
|
26
|
+
c.ui.notify(NOTIFICATION, "info");
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
candidate = false;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { COMBO_LEVELS, getSharedComboState, isOmpSubagentPrompt, setSharedComboMode } from "../shared/session-state.js";
|
|
5
|
+
import { readCavemanDefault } from "../shared/plugin-settings.js";
|
|
6
|
+
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const RULE_PATH = join(CAVERN_DIR, "rule.md");
|
|
8
|
+
const FALLBACK_FULL_RULE = `Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- Drop articles (a/an/the), filler (just/really/basically), pleasantries, hedging.
|
|
12
|
+
- Fragments OK. Short synonyms. Technical terms exact. Code unchanged.
|
|
13
|
+
- Pattern: [thing] [action] [reason]. [next step].
|
|
14
|
+
- Not: "Sure! I'd be happy to help you with that."
|
|
15
|
+
- Yes: "Bug in auth middleware. Fix:"
|
|
16
|
+
|
|
17
|
+
Auto-clarity: drop caveman for security warnings, irreversible actions, or when user seems confused. Resume after.`;
|
|
18
|
+
// ponytail: synchronous read each full-mode injection; ceiling = small file, cold session start. Upgrade path: cache file contents + mtime, invalidate on change.
|
|
19
|
+
function readFullRule() {
|
|
20
|
+
try {
|
|
21
|
+
return readFileSync(RULE_PATH, "utf8");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return FALLBACK_FULL_RULE;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const DEFAULT_MODE = "off";
|
|
28
|
+
const MODES = new Set(["off", "lite", "full", "ultra", "wenyan"]);
|
|
29
|
+
const INSTRUCTIONS = {
|
|
30
|
+
lite: `Caveman lite active for this session.
|
|
31
|
+
Respond concise. Drop pleasantries, filler, and hedging. Keep complete technical substance. Code, commands, paths, errors, commits, and PR text stay normal/exact.`,
|
|
32
|
+
full: () => `Caveman full active for this session.\n${readFullRule()}`,
|
|
33
|
+
ultra: `Caveman ultra active for this session.
|
|
34
|
+
Maximum terse prose. Fragments preferred. No pleasantries, no tour, no recap unless needed. Keep all technical substance exact. Code, commands, commits, PR text, paths, and errors stay normal/exact. Drop caveman for security warnings, irreversible actions, or user confusion.`,
|
|
35
|
+
wenyan: `Caveman wenyan active for this session.
|
|
36
|
+
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
|
+
};
|
|
38
|
+
function normalizeMode(value) {
|
|
39
|
+
const mode = String(value || "").trim().toLowerCase();
|
|
40
|
+
return MODES.has(mode) ? mode : null;
|
|
41
|
+
}
|
|
42
|
+
function resolveMode(entries, fallback = DEFAULT_MODE) {
|
|
43
|
+
if (!Array.isArray(entries))
|
|
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;
|
|
54
|
+
}
|
|
55
|
+
function isOffCommand(text) {
|
|
56
|
+
const t = String(text || "").trim().toLowerCase().replace(/[.!?\s]+$/, "");
|
|
57
|
+
return t === "stop caveman" || t === "normal mode" || t === "caveman off";
|
|
58
|
+
}
|
|
59
|
+
export default function cavemanSessionExtension(pi) {
|
|
60
|
+
let currentMode = DEFAULT_MODE;
|
|
61
|
+
let isActive = false;
|
|
62
|
+
let lastCtx = undefined;
|
|
63
|
+
function syncStatus(ctx) {
|
|
64
|
+
if (ctx)
|
|
65
|
+
lastCtx = ctx;
|
|
66
|
+
const c = ctx || lastCtx;
|
|
67
|
+
if (!c?.ui?.setStatus)
|
|
68
|
+
return;
|
|
69
|
+
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
70
|
+
if (getSharedComboState().level in COMBO_LEVELS && getSharedComboState().level !== "off") {
|
|
71
|
+
c.ui.setStatus("caveman", undefined);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (currentMode === "off") {
|
|
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}`);
|
|
82
|
+
}
|
|
83
|
+
function setMode(mode, ctx) {
|
|
84
|
+
const normalized = normalizeMode(mode);
|
|
85
|
+
if (!normalized)
|
|
86
|
+
return false;
|
|
87
|
+
currentMode = normalized;
|
|
88
|
+
pi.appendEntry?.("caveman-mode", { mode: normalized });
|
|
89
|
+
setSharedComboMode("caveman", normalized);
|
|
90
|
+
syncStatus(ctx);
|
|
91
|
+
ctx?.ui?.notify?.(`Caveman mode ${normalized === "off" ? "off" : `set to ${normalized}`}.`, "info");
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
pi.setLabel?.("Caveman session toggle");
|
|
95
|
+
pi.registerCommand?.("caveman", {
|
|
96
|
+
description: "Toggle terse caveman replies for this session",
|
|
97
|
+
handler: async (args, ctx) => {
|
|
98
|
+
const arg = String(args || "").trim().toLowerCase();
|
|
99
|
+
if (!arg || arg === "on") {
|
|
100
|
+
setMode("full", ctx);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (arg === "status") {
|
|
104
|
+
ctx?.ui?.notify?.(`Caveman: ${currentMode}`, "info");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!setMode(arg, ctx)) {
|
|
108
|
+
ctx?.ui?.notify?.("Usage: /caveman [lite|full|ultra|wenyan|off|status]", "warning");
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
pi.on("input", async (event) => {
|
|
113
|
+
if (event?.source === "extension")
|
|
114
|
+
return;
|
|
115
|
+
if (currentMode !== "off" && isOffCommand(event?.text))
|
|
116
|
+
setMode("off");
|
|
117
|
+
});
|
|
118
|
+
function restoreMode(ctx) {
|
|
119
|
+
const entries = ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
|
|
120
|
+
// Persisted session state wins; a fresh session falls back to the
|
|
121
|
+
// installer/user-configured default (off unless configured).
|
|
122
|
+
const persisted = resolveMode(entries, "");
|
|
123
|
+
currentMode = persisted || readCavemanDefault();
|
|
124
|
+
syncStatus(ctx);
|
|
125
|
+
}
|
|
126
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
127
|
+
restoreMode(ctx);
|
|
128
|
+
ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, "info");
|
|
129
|
+
});
|
|
130
|
+
pi.on("session_branch", async (_event, ctx) => {
|
|
131
|
+
restoreMode(ctx);
|
|
132
|
+
});
|
|
133
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
134
|
+
restoreMode(ctx);
|
|
135
|
+
});
|
|
136
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
137
|
+
isActive = true;
|
|
138
|
+
syncStatus(ctx);
|
|
139
|
+
});
|
|
140
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
141
|
+
isActive = false;
|
|
142
|
+
syncStatus(ctx);
|
|
143
|
+
});
|
|
144
|
+
pi.on("before_agent_start", async (event) => {
|
|
145
|
+
const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
|
|
146
|
+
if (!mode || mode === "off")
|
|
147
|
+
return;
|
|
148
|
+
const def = INSTRUCTIONS[mode];
|
|
149
|
+
const instruction = typeof def === "function" ? def() : def;
|
|
150
|
+
const base = Array.isArray(event.systemPrompt) ? event.systemPrompt : [event.systemPrompt];
|
|
151
|
+
return { systemPrompt: [...base, instruction] };
|
|
152
|
+
});
|
|
153
|
+
}
|