@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/tersio.js ADDED
@@ -0,0 +1,1143 @@
1
+ #!/usr/bin/env node
2
+ // tersio.ts — Install Tersio (caveman/rtk/ponytail) add-ons on any OMP device.
3
+ // Usage: node tersio.js [install|update|reinstall|doctor|uninstall|version|help] [options]
4
+ // Requires: node/npm and omp CLI
5
+ import { execFile } from "node:child_process";
6
+ import { promises as fs } from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import readline from "node:readline";
10
+ import { fileURLToPath } from "node:url";
11
+ import { promisify } from "node:util";
12
+ import { createRequire } from "node:module";
13
+ import { httpsGet, httpsDownload, sha256File, parseChecksum, readTextIfExists, } from "./extensions/lib/utils.js";
14
+ const IS_WINDOWS = process.platform === "win32";
15
+ const HOME = process.env.HOME || process.env.USERPROFILE || "";
16
+ const PACKAGE_NAME = "@krtclcdy/tersio";
17
+ const PACKAGE_BIN = "tersio";
18
+ const { version: PACKAGE_VERSION } = createRequire(import.meta.url)("./package.json");
19
+ // Session-start mode defaults for fresh installs.
20
+ const COMBO_DEFAULTS = { off: true, medium: true, balanced: true, max: true };
21
+ const CAVEMAN_DEFAULTS = { off: true, lite: true, full: true, ultra: true, wenyan: true };
22
+ function parseEnum(value, valid, flag) {
23
+ if (value === undefined)
24
+ return undefined;
25
+ const v = value.trim().toLowerCase();
26
+ if (!(v in valid)) {
27
+ console.error(`[fail] Invalid ${flag}: ${value}. Valid: ${Object.keys(valid).join(", ")}`);
28
+ process.exit(1);
29
+ }
30
+ return v;
31
+ }
32
+ function flagValue(name) {
33
+ const i = args.indexOf(name);
34
+ if (i === -1)
35
+ return undefined;
36
+ return args[i + 1];
37
+ }
38
+ // --- CLI flags ---
39
+ const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, version: true, help: true };
40
+ const args = process.argv.slice(2);
41
+ const commandArg = args.find((arg, index) => !arg.startsWith("-") && args[index - 1] !== "--scope");
42
+ const command = commandArg?.toLowerCase() || null;
43
+ const unknownCommand = command !== null && !(command in COMMANDS);
44
+ const install = command === "install";
45
+ const update = command === "update";
46
+ const reinstall = command === "reinstall";
47
+ const showVersion = command === "version" || args.includes("--version") || args.includes("-v");
48
+ const showHelp = command === "help" || args.includes("--help") || args.includes("-h");
49
+ const applyUpdate = args.includes("--apply-update");
50
+ const dryRun = args.includes("--dry-run");
51
+ const yes = args.includes("--yes") || args.includes("-y") || install || update || reinstall || applyUpdate;
52
+ const verbose = args.includes("--verbose");
53
+ const doctor = command === "doctor" || args.includes("--doctor");
54
+ const uninstall = command === "uninstall" || args.includes("--uninstall");
55
+ const removePonytail = args.includes("--remove-ponytail");
56
+ const removeRtk = args.includes("--remove-rtk");
57
+ const scopeFlag = (() => {
58
+ const i = args.indexOf("--scope");
59
+ if (i === -1)
60
+ return null;
61
+ return args[i + 1]?.toLowerCase() || null;
62
+ })();
63
+ // --- Profile selection (session-start mode defaults) ---
64
+ const comboDefaultFlag = parseEnum(flagValue("--combo-default"), COMBO_DEFAULTS, "--combo-default");
65
+ const cavemanDefaultFlag = parseEnum(flagValue("--caveman-default"), CAVEMAN_DEFAULTS, "--caveman-default");
66
+ const rtkDefaultFlag = flagValue("--rtk-default")?.toLowerCase();
67
+ if (rtkDefaultFlag !== undefined && rtkDefaultFlag !== "on" && rtkDefaultFlag !== "off") {
68
+ console.error(`[fail] Invalid --rtk-default: ${rtkDefaultFlag}. Use: on, off`);
69
+ process.exit(1);
70
+ }
71
+ const profileFlagsGiven = comboDefaultFlag !== undefined
72
+ || cavemanDefaultFlag !== undefined || rtkDefaultFlag !== undefined;
73
+ function printHelp() {
74
+ console.log(`Usage: ${PACKAGE_BIN} [command] [options]
75
+
76
+ Commands:
77
+ install Install the add-ons (user scope by default)
78
+ update Run the latest published installer
79
+ reinstall Clean and reinstall the user-scope add-ons
80
+ doctor Check the current installation
81
+ uninstall Remove the managed extensions
82
+ version Print the package version
83
+ help Show this help
84
+
85
+ Options:
86
+ --scope user|project|both
87
+ --combo-default off|medium|balanced|max
88
+ --caveman-default off|lite|full|ultra|wenyan
89
+ --rtk-default on|off
90
+ --yes, -y
91
+ --dry-run
92
+ --verbose
93
+ --version, -v
94
+ --help, -h`);
95
+ }
96
+ function debug(...a) {
97
+ if (verbose)
98
+ console.log(" [debug]", ...a);
99
+ }
100
+ const RL = readline.createInterface({ input: process.stdin, output: process.stdout });
101
+ let rlOpen = true;
102
+ function ask(q) {
103
+ const { promise, resolve } = Promise.withResolvers();
104
+ RL.question(q, resolve);
105
+ return promise;
106
+ }
107
+ function closeRL() { if (rlOpen) {
108
+ RL.close();
109
+ rlOpen = false;
110
+ } }
111
+ // Paths to extension source files (relative to this script)
112
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
113
+ const EXT_DIR = path.join(SCRIPT_DIR, "extensions");
114
+ const SHARED_SESSION_STATE = path.join(EXT_DIR, "shared", "session-state.js");
115
+ const CAVEMAN_INDEX = path.join(EXT_DIR, "caveman-session", "index.js");
116
+ const RTK_SESSION_INDEX = path.join(EXT_DIR, "rtk-session", "index.js");
117
+ const UPDATER_INDEX = path.join(EXT_DIR, "ai-addons-updater", "index.js");
118
+ const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, "combo-toggle", "index.js");
119
+ const AMANAI_REWARD_INDEX = path.join(EXT_DIR, "amanai-reward", "index.js");
120
+ const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, "shared", "mode-reinforcement.js");
121
+ const SHARED_TYPES = path.join(EXT_DIR, "shared", "types.js");
122
+ const LIB_UTILS = path.join(EXT_DIR, "lib", "utils.js");
123
+ const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, "shared", "plugin-settings.js");
124
+ const CAVEMAN_REMOTE_RULE = "https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rules/caveman-activate.md";
125
+ const RTK_RELEASE_API = "https://api.github.com/repos/rtk-ai/rtk/releases/latest";
126
+ // --- Helpers ---
127
+ const execFileP = promisify(execFile);
128
+ async function execP(cmd, args, opts = {}) {
129
+ return execFileP(cmd, args, {
130
+ timeout: opts.timeout || 120000,
131
+ encoding: "utf8",
132
+ ...opts,
133
+ });
134
+ }
135
+ async function writeIfChanged(dest, content, options = {}) {
136
+ const existing = await readTextIfExists(dest);
137
+ if (existing === content) {
138
+ debug(`${dest} already up to date`);
139
+ return false;
140
+ }
141
+ if (options.dryRun) {
142
+ console.log(` [dry-run] would write ${dest}`);
143
+ return true;
144
+ }
145
+ await fs.mkdir(path.dirname(dest), { recursive: true });
146
+ if (existing !== null) {
147
+ await fs.copyFile(dest, `${dest}.bak`);
148
+ debug(`${path.basename(dest)} → ${path.basename(dest)}.bak`);
149
+ }
150
+ await fs.writeFile(dest, content, "utf8");
151
+ console.log(` [write] ${dest}`);
152
+ return true;
153
+ }
154
+ async function ensureExtensionInConfig(configPath, extensionPath, label, options = {}) {
155
+ const normalizedPath = extensionPath.replace(/\\/g, "/");
156
+ const line = ` - ${normalizedPath}`;
157
+ const raw = await readTextIfExists(configPath);
158
+ let lines = (raw || "").split("\n");
159
+ if (lines.some((l) => l.includes(normalizedPath))) {
160
+ debug(`${label} already in config.yml`);
161
+ return false;
162
+ }
163
+ const extLineIdx = lines.findIndex((l) => /^\s*extensions\s*:/i.test(l));
164
+ if (options.dryRun) {
165
+ console.log(` [dry-run] would add ${label} to config.yml: ${normalizedPath}`);
166
+ return true;
167
+ }
168
+ // Handle "extensions: []" (empty YAML array)
169
+ const emptyArrayIdx = lines.findIndex((l) => /^\s*extensions\s*:\s*\[\s*\]\s*$/i.test(l));
170
+ if (emptyArrayIdx !== -1) {
171
+ lines[emptyArrayIdx] = "extensions:";
172
+ lines.splice(emptyArrayIdx + 1, 0, line, "");
173
+ }
174
+ else if (extLineIdx === -1) {
175
+ lines.push("extensions:");
176
+ lines.push(line);
177
+ lines.push("");
178
+ }
179
+ else {
180
+ lines.splice(extLineIdx + 1, 0, line);
181
+ }
182
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
183
+ await fs.writeFile(configPath, lines.join("\n"), "utf8");
184
+ console.log(` [write] Added ${label} to config.yml`);
185
+ return true;
186
+ }
187
+ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterPath, label, options = {}) {
188
+ const normalizedPath = extensionPath.replace(/\\/g, "/");
189
+ const normalizedAfterPath = afterPath.replace(/\\/g, "/");
190
+ const line = ` - ${normalizedPath}`;
191
+ const raw = await readTextIfExists(configPath);
192
+ const lines = (raw || "").split("\n");
193
+ const existingIndex = lines.findIndex((entry) => entry.includes(normalizedPath));
194
+ const afterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
195
+ if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
196
+ return false;
197
+ if (options.dryRun) {
198
+ console.log(` [dry-run] would place ${label} after Ponytail in config.yml: ${normalizedPath}`);
199
+ return true;
200
+ }
201
+ if (existingIndex !== -1)
202
+ lines.splice(existingIndex, 1);
203
+ const refreshedAfterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
204
+ if (refreshedAfterIndex !== -1) {
205
+ lines.splice(refreshedAfterIndex + 1, 0, line);
206
+ }
207
+ else {
208
+ const extensionsIndex = lines.findIndex((entry) => /^\s*extensions\s*:/i.test(entry));
209
+ if (extensionsIndex === -1) {
210
+ lines.push("extensions:", line, "");
211
+ }
212
+ else {
213
+ lines.splice(extensionsIndex + 1, 0, line);
214
+ }
215
+ }
216
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
217
+ await fs.writeFile(configPath, lines.join("\n"), "utf8");
218
+ console.log(` [write] Placed ${label} after Ponytail in config.yml`);
219
+ return true;
220
+ }
221
+ function readPonytailConfig() {
222
+ const dir = process.env.XDG_CONFIG_HOME
223
+ ? path.join(process.env.XDG_CONFIG_HOME, "ponytail")
224
+ : path.join(HOME, ".config", "ponytail");
225
+ return { dir, path: path.join(dir, "config.json") };
226
+ }
227
+ async function ensurePonytailDefaultOff(options = {}) {
228
+ const config = readPonytailConfig();
229
+ if (options.dryRun) {
230
+ console.log(` [dry-run] would set Ponytail defaultMode=off in ${config.path}`);
231
+ return;
232
+ }
233
+ let cfg = {};
234
+ const existing = await readTextIfExists(config.path);
235
+ if (existing) {
236
+ try {
237
+ cfg = JSON.parse(existing.replace(/^\uFEFF/, ""));
238
+ if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) {
239
+ cfg = {};
240
+ }
241
+ }
242
+ catch {
243
+ cfg = {};
244
+ }
245
+ }
246
+ if (cfg.defaultMode === "off") {
247
+ debug("Ponytail defaultMode already off");
248
+ return;
249
+ }
250
+ await fs.mkdir(config.dir, { recursive: true });
251
+ cfg.defaultMode = "off";
252
+ await fs.writeFile(config.path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
253
+ console.log(` [write] Set Ponytail defaultMode=off in ${config.path}`);
254
+ }
255
+ // Sets ~/.config/ponytail/config.json#hideStatus=true so the upstream ponytail
256
+ // status bar doesn't render — the combo extension owns the bar instead.
257
+ async function ensurePonytailHideStatus(options = {}) {
258
+ const config = readPonytailConfig();
259
+ if (options.dryRun) {
260
+ console.log(` [dry-run] would set Ponytail hideStatus=true in ${config.path}`);
261
+ return;
262
+ }
263
+ let cfg = {};
264
+ const existing = await readTextIfExists(config.path);
265
+ if (existing) {
266
+ try {
267
+ cfg = JSON.parse(existing.replace(/^\uFEFF/, ""));
268
+ if (!cfg || typeof cfg !== "object" || Array.isArray(cfg))
269
+ cfg = {};
270
+ }
271
+ catch {
272
+ cfg = {};
273
+ }
274
+ }
275
+ if (cfg.hideStatus === true) {
276
+ debug("Ponytail hideStatus already true");
277
+ return;
278
+ }
279
+ await fs.mkdir(config.dir, { recursive: true });
280
+ cfg.hideStatus = true;
281
+ await fs.writeFile(config.path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
282
+ console.log(` [write] Set Ponytail hideStatus=true in ${config.path}`);
283
+ }
284
+ // --- Steps ---
285
+ async function stepPonytail(pluginsDir, userDir, options) {
286
+ console.log("\n[1/8] Installing Ponytail plugin...");
287
+ await fs.mkdir(pluginsDir, { recursive: true });
288
+ const pkgPath = path.join(pluginsDir, "package.json");
289
+ let pkg = {};
290
+ const existing = await readTextIfExists(pkgPath);
291
+ if (existing)
292
+ pkg = JSON.parse(existing);
293
+ pkg.name = pkg.name || "omp-plugins";
294
+ pkg.private = true;
295
+ pkg.dependencies = pkg.dependencies || {};
296
+ pkg.dependencies["@dietrichgebert/ponytail"] = "github:DietrichGebert/ponytail";
297
+ if (options.dryRun) {
298
+ console.log(` [dry-run] would write ${pkgPath}`);
299
+ }
300
+ else {
301
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
302
+ console.log(` [write] package.json`);
303
+ }
304
+ if (options.dryRun) {
305
+ console.log(" [dry-run] would run: omp plugin install github:DietrichGebert/ponytail");
306
+ if (options.reinstall) {
307
+ console.log(" [dry-run] would run: npm install @dietrichgebert/ponytail@latest --save --no-audit --no-fund");
308
+ }
309
+ const ponytailExtPath = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail", "pi-extension", "index.js");
310
+ const configPath = path.join(userDir, "config.yml");
311
+ await ensureExtensionInConfig(configPath, ponytailExtPath, "ponytail", options);
312
+ await ensurePonytailDefaultOff(options);
313
+ await ensurePonytailHideStatus(options);
314
+ return;
315
+ }
316
+ // Try omp plugin install first
317
+ try {
318
+ await execP(IS_WINDOWS ? "omp.cmd" : "omp", ["plugin", "install", "github:DietrichGebert/ponytail"], { cwd: pluginsDir });
319
+ console.log(" [ok] omp plugin install ran");
320
+ }
321
+ catch (e) {
322
+ console.log(` [warn] omp plugin install failed: ${e.message}`);
323
+ }
324
+ if (options.reinstall) {
325
+ try {
326
+ await execP("npm", [
327
+ "install",
328
+ "@dietrichgebert/ponytail@latest",
329
+ "--save",
330
+ "--no-audit",
331
+ "--no-fund",
332
+ ], { cwd: pluginsDir, timeout: 120000 });
333
+ console.log(" [ok] Ponytail refreshed");
334
+ }
335
+ catch (e) {
336
+ console.log(` [fail] Could not refresh ponytail: ${e.message}`);
337
+ console.log(` [hint] Manual: cd ~/.omp/plugins && npm install @dietrichgebert/ponytail@latest --save --no-audit --no-fund`);
338
+ }
339
+ }
340
+ // Verify the pi-extension/index.js actually exists
341
+ const ponytailExtPath = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail", "pi-extension", "index.js");
342
+ let ponytailExtExists = await readTextIfExists(ponytailExtPath);
343
+ // Fallback: try bun install or npm install
344
+ if (!ponytailExtExists) {
345
+ console.log(" [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...");
346
+ try {
347
+ await execP("npm", ["install"], { cwd: pluginsDir, timeout: 120000 });
348
+ console.log(" [ok] npm install completed");
349
+ }
350
+ catch {
351
+ try {
352
+ await execP("bun", ["install"], { cwd: pluginsDir, timeout: 120000 });
353
+ console.log(" [ok] bun install completed");
354
+ }
355
+ catch (e2) {
356
+ console.log(` [fail] Could not install ponytail: ${e2.message}`);
357
+ console.log(` [hint] Manual: cd ~/.omp/plugins && npm install`);
358
+ }
359
+ }
360
+ ponytailExtExists = await readTextIfExists(ponytailExtPath);
361
+ }
362
+ // Last-resort fallback: git clone the repo into node_modules
363
+ if (!ponytailExtExists) {
364
+ console.log(" [info] npm/bun did not produce pi-extension — trying git clone...");
365
+ try {
366
+ const dest = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail");
367
+ await fs.mkdir(path.dirname(dest), { recursive: true });
368
+ await execP("git", ["clone", "--depth", "1", "https://github.com/DietrichGebert/ponytail.git", dest], { timeout: 180000 });
369
+ console.log(" [ok] git clone completed");
370
+ ponytailExtExists = await readTextIfExists(ponytailExtPath);
371
+ }
372
+ catch (e3) {
373
+ console.log(` [fail] git clone failed: ${e3.message}`);
374
+ console.log(` [hint] Install git or check network: https://github.com/DietrichGebert/ponytail`);
375
+ }
376
+ }
377
+ if (!ponytailExtExists) {
378
+ console.log(" [skip] Ponytail pi-extension/index.js still not found — skill-only mode");
379
+ console.log(" [hint] The /ponytail command won't work, but ponytail skills will still load");
380
+ // Still set Ponytail config defaultMode=off even without the extension command
381
+ await ensurePonytailDefaultOff(options);
382
+ await ensurePonytailHideStatus(options);
383
+ return;
384
+ }
385
+ // Wire extension into config.yml so /ponytail command loads
386
+ console.log(" [ok] Ponytail pi-extension found");
387
+ const configPath = path.join(userDir, "config.yml");
388
+ await ensureExtensionInConfig(configPath, ponytailExtPath, "ponytail", options);
389
+ await ensurePonytailDefaultOff(options);
390
+ await ensurePonytailHideStatus(options);
391
+ }
392
+ // Registers this package in ~/.omp/plugins so OMP lists it on the
393
+ // Settings → Plugins page (OMP enumerates plugins/package.json dependencies).
394
+ // Returns true when the package is verified in plugins/node_modules — the
395
+ // Amanai detector then loads via the plugin's `omp.extensions` manifest, so
396
+ // the caller must skip copying it into agent/extensions to avoid a double load.
397
+ async function stepSelfPlugin(pluginsDir, options) {
398
+ console.log("\n[2/8] Registering tersio as OMP plugin...");
399
+ const pkgPath = path.join(pluginsDir, "package.json");
400
+ let pkg = {};
401
+ const existing = await readTextIfExists(pkgPath);
402
+ if (existing) {
403
+ try {
404
+ pkg = JSON.parse(existing);
405
+ }
406
+ catch {
407
+ pkg = {};
408
+ }
409
+ }
410
+ pkg.name = pkg.name || "omp-plugins";
411
+ pkg.private = true;
412
+ pkg.dependencies = pkg.dependencies || {};
413
+ pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
414
+ for (const legacy of ["oh-my-pi-token-saver", "tersio-omp"]) {
415
+ if (legacy in pkg.dependencies) {
416
+ delete pkg.dependencies[legacy];
417
+ if (!options.dryRun)
418
+ console.log(` [migrate] dropped legacy ${legacy} dependency`);
419
+ }
420
+ }
421
+ if (options.dryRun) {
422
+ console.log(` [dry-run] would add ${PACKAGE_NAME}@^${PACKAGE_VERSION} to ${pkgPath}`);
423
+ console.log(` [dry-run] would run: npm install --no-audit --no-fund (in ${pluginsDir})`);
424
+ return false;
425
+ }
426
+ await fs.mkdir(pluginsDir, { recursive: true });
427
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
428
+ try {
429
+ await execP("npm", ["install", "--no-audit", "--no-fund"], { cwd: pluginsDir, timeout: 180000 });
430
+ }
431
+ catch {
432
+ try {
433
+ await execP("bun", ["install"], { cwd: pluginsDir, timeout: 180000 });
434
+ }
435
+ catch (e) {
436
+ console.log(` [fail] Could not install ${PACKAGE_NAME} into plugins dir: ${e.message}`);
437
+ console.log(` [hint] Manual: cd ~/.omp/plugins && npm install ${PACKAGE_NAME}@^${PACKAGE_VERSION} --save --no-audit --no-fund`);
438
+ return false;
439
+ }
440
+ }
441
+ const installedPkg = path.join(pluginsDir, "node_modules", PACKAGE_NAME, "package.json");
442
+ if ((await readTextIfExists(installedPkg)) === null) {
443
+ console.log(` [warn] ${PACKAGE_NAME} not found in plugins/node_modules after install`);
444
+ return false;
445
+ }
446
+ console.log(" [ok] Listed in OMP Settings → Plugins as tersio");
447
+ return true;
448
+ }
449
+ async function stepRtk(binDir, options) {
450
+ console.log("\n[3/8] Installing RTK binary...");
451
+ try {
452
+ const raw = await httpsGet(RTK_RELEASE_API);
453
+ const release = JSON.parse(raw);
454
+ const tag = release.tag_name;
455
+ // Map (platform, arch) → Rust triple stem.
456
+ const PLATFORM = process.platform;
457
+ const ARCH = process.arch;
458
+ let assetTriple;
459
+ if (PLATFORM === "win32" && ARCH === "x64") {
460
+ assetTriple = "x86_64-pc-windows-msvc";
461
+ }
462
+ else if (PLATFORM === "linux" && ARCH === "x64") {
463
+ assetTriple = "x86_64-unknown-linux-musl";
464
+ }
465
+ else if (PLATFORM === "linux" && ARCH === "arm64") {
466
+ assetTriple = "aarch64-unknown-linux-gnu";
467
+ }
468
+ else if (PLATFORM === "darwin" && ARCH === "x64") {
469
+ assetTriple = "x86_64-apple-darwin";
470
+ }
471
+ else if (PLATFORM === "darwin" && ARCH === "arm64") {
472
+ assetTriple = "aarch64-apple-darwin";
473
+ }
474
+ else {
475
+ console.log(` [fail] Unsupported platform: ${PLATFORM}/${ARCH}`);
476
+ console.log(` [hint] Manual: https://github.com/rtk-ai/rtk/releases`);
477
+ return;
478
+ }
479
+ const asset = (release.assets || []).find((a) => a.name === `rtk-${assetTriple}.zip` || a.name === `rtk-${assetTriple}.tar.gz`);
480
+ if (!asset) {
481
+ console.log(` [fail] No rtk-${assetTriple}.<zip|tar.gz> in release ${tag}`);
482
+ console.log(` [hint] Available: ${(release.assets || []).map((a) => a.name).filter((n) => n.startsWith("rtk-")).join(", ")}`);
483
+ return;
484
+ }
485
+ const binDest = path.join(binDir, IS_WINDOWS ? "rtk.exe" : "rtk");
486
+ if (options.dryRun) {
487
+ console.log(` [dry-run] would download ${asset.name} from release ${tag}`);
488
+ console.log(` [dry-run] would verify checksum against checksums.txt`);
489
+ console.log(` [dry-run] would extract and install to ${binDest}`);
490
+ return;
491
+ }
492
+ // Also download checksums.txt for verification
493
+ const checksumsAsset = (release.assets || []).find((a) => a.name === "checksums.txt");
494
+ let checksumsText = null;
495
+ if (checksumsAsset) {
496
+ try {
497
+ checksumsText = await httpsGet(checksumsAsset.browser_download_url);
498
+ }
499
+ catch (e) {
500
+ debug(`Could not download checksums.txt: ${e.message}`);
501
+ }
502
+ }
503
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-rtk-"));
504
+ const archivePath = path.join(tmpDir, asset.name);
505
+ await httpsDownload(asset.browser_download_url, archivePath);
506
+ // Verify checksum
507
+ if (checksumsText) {
508
+ const expected = parseChecksum(checksumsText, asset.name);
509
+ const actual = await sha256File(archivePath);
510
+ if (!expected) {
511
+ console.log(` [warn] checksums.txt missing entry for ${asset.name} — skipping verification`);
512
+ }
513
+ else if (actual !== expected) {
514
+ console.log(` [fail] Checksum mismatch for ${asset.name}`);
515
+ console.log(` [fail] Expected: ${expected}`);
516
+ console.log(` [fail] Got: ${actual}`);
517
+ await fs.rm(tmpDir, { recursive: true, force: true });
518
+ return;
519
+ }
520
+ else {
521
+ console.log(` [ok] Checksum verified for ${asset.name}`);
522
+ }
523
+ }
524
+ else {
525
+ console.log(` [warn] No checksums.txt available — skipping verification`);
526
+ }
527
+ // Extract by extension (not OS)
528
+ const extractDir = path.join(tmpDir, "extracted");
529
+ await fs.mkdir(extractDir, { recursive: true });
530
+ if (asset.name.endsWith(".zip")) {
531
+ if (IS_WINDOWS) {
532
+ await execP("powershell", ["Expand-Archive", "-Path", archivePath, "-DestinationPath", extractDir, "-Force"], { timeout: 60000 });
533
+ }
534
+ else {
535
+ await execP("unzip", [archivePath, "-d", extractDir], { timeout: 60000 });
536
+ }
537
+ }
538
+ else if (asset.name.endsWith(".tar.gz") || asset.name.endsWith(".tgz")) {
539
+ try {
540
+ await execP("tar", ["xzf", archivePath, "-C", extractDir], { timeout: 60000 });
541
+ debug("tar xzf ok");
542
+ }
543
+ catch (e) {
544
+ debug(`tar xzf failed: ${((e.stderr) || e.message || "").trim().slice(0, 200)}`);
545
+ try {
546
+ await execP("sh", ["-c", `gunzip < "${archivePath}" | tar xf - -C "${extractDir}"`], { timeout: 60000 });
547
+ debug("gunzip|tar fallback ok");
548
+ }
549
+ catch (e2) {
550
+ debug(`gunzip|tar fallback failed: ${((e2.stderr) || e2.message || "").trim().slice(0, 200)}`);
551
+ }
552
+ }
553
+ }
554
+ else {
555
+ console.log(` [fail] Unknown archive format: ${asset.name}`);
556
+ await fs.rm(tmpDir, { recursive: true, force: true });
557
+ return;
558
+ }
559
+ // Find binary
560
+ const binaryName = IS_WINDOWS ? "rtk.exe" : "rtk";
561
+ const entries = await fs.readdir(extractDir, { recursive: true });
562
+ debug(`extracted entries: ${entries.join(", ")}`);
563
+ const found = entries.find((e) => path.basename(e) === binaryName);
564
+ if (!found) {
565
+ console.log(` [fail] Could not find ${binaryName} in extracted archive`);
566
+ await fs.rm(tmpDir, { recursive: true, force: true });
567
+ return;
568
+ }
569
+ await fs.mkdir(path.dirname(binDest), { recursive: true });
570
+ await fs.copyFile(path.join(extractDir, found), binDest);
571
+ console.log(` [write] ${binDest}`);
572
+ // Set executable bit on Unix
573
+ if (!IS_WINDOWS) {
574
+ await fs.chmod(binDest, 0o755);
575
+ debug(`chmod 755 ${binDest}`);
576
+ }
577
+ // Verify
578
+ try {
579
+ const v = (await execP(binDest, ["--version"], { timeout: 10000, shell: false })).stdout.trim();
580
+ console.log(` [ok] ${binDest} → ${v}`);
581
+ }
582
+ catch {
583
+ console.log(` [hint] Verify manually: ${binDest} --version`);
584
+ }
585
+ // Cleanup
586
+ await fs.rm(tmpDir, { recursive: true, force: true });
587
+ }
588
+ catch (e) {
589
+ console.log(` [fail] RTK: ${e.message}`);
590
+ console.log(` [hint] Manual: https://github.com/rtk-ai/rtk/releases`);
591
+ }
592
+ }
593
+ async function stepSharedSessionState(extDir, options) {
594
+ const src = await readTextIfExists(SHARED_SESSION_STATE);
595
+ if (!src) {
596
+ console.log(" [skip] shared/session-state.js not found in repo");
597
+ return;
598
+ }
599
+ await writeIfChanged(path.join(extDir, "shared", "session-state.js"), src, options);
600
+ const typesSrc = await readTextIfExists(SHARED_TYPES);
601
+ if (typesSrc)
602
+ await writeIfChanged(path.join(extDir, "shared", "types.js"), typesSrc, options);
603
+ const utilsSrc = await readTextIfExists(LIB_UTILS);
604
+ if (utilsSrc)
605
+ await writeIfChanged(path.join(extDir, "lib", "utils.js"), utilsSrc, options);
606
+ const settingsSrc = await readTextIfExists(SHARED_PLUGIN_SETTINGS);
607
+ if (settingsSrc)
608
+ await writeIfChanged(path.join(extDir, "shared", "plugin-settings.js"), settingsSrc, options);
609
+ }
610
+ async function stepModeReinforcement(extDir, ponytailExtPath, options) {
611
+ console.log("\n[7/8] Installing mode reinforcement extension...");
612
+ const src = await readTextIfExists(MODE_REINFORCEMENT_INDEX);
613
+ if (!src) {
614
+ console.log(" [skip] shared/mode-reinforcement.js not found in repo");
615
+ return;
616
+ }
617
+ const dest = path.join(extDir, "shared", "mode-reinforcement.js");
618
+ await writeIfChanged(dest, src, options);
619
+ await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), "config.yml"), dest, ponytailExtPath, "mode reinforcement", options);
620
+ }
621
+ async function stepRtkSession(extDir, options) {
622
+ console.log("\n[4/8] Installing RTK session extension...");
623
+ const src = await readTextIfExists(RTK_SESSION_INDEX);
624
+ if (!src) {
625
+ console.log(" [skip] rtk-session/index.js not found in repo");
626
+ return;
627
+ }
628
+ const dest = path.join(extDir, "rtk-session", "index.js");
629
+ await writeIfChanged(dest, src, options);
630
+ }
631
+ async function stepCaveman(extDir, options) {
632
+ console.log("\n[5/8] Installing Caveman session extension...");
633
+ const cavemanDir = path.join(extDir, "caveman-session");
634
+ if (!options.dryRun)
635
+ await fs.mkdir(cavemanDir, { recursive: true });
636
+ // Dry runs stay offline; the bundled rule is enough to preview its destination.
637
+ const rule = options.dryRun ? await readTextIfExists(path.join(path.dirname(CAVEMAN_INDEX), "rule.md")) || "" : await httpsGet(CAVEMAN_REMOTE_RULE);
638
+ await writeIfChanged(path.join(cavemanDir, "rule.md"), rule, options);
639
+ // Write index.js
640
+ const src = await readTextIfExists(CAVEMAN_INDEX);
641
+ if (!src) {
642
+ console.log(" [skip] caveman-session/index.js not found in repo");
643
+ return;
644
+ }
645
+ await writeIfChanged(path.join(cavemanDir, "index.js"), src, options);
646
+ }
647
+ async function stepUpdater(extDir, options) {
648
+ const updaterSrc = await readTextIfExists(UPDATER_INDEX);
649
+ if (!updaterSrc) {
650
+ console.log(" [skip] ai-addons-updater/index.js not found in repo");
651
+ return;
652
+ }
653
+ await writeIfChanged(path.join(extDir, "ai-addons-updater", "index.js"), updaterSrc, options);
654
+ }
655
+ async function stepCombo(extDir, options) {
656
+ console.log("\n[6/8] Installing Combo toggle extension...");
657
+ const src = await readTextIfExists(COMBO_TOGGLE_INDEX);
658
+ if (!src) {
659
+ console.log(" [skip] combo-toggle/index.js not found in repo");
660
+ return;
661
+ }
662
+ const dest = path.join(extDir, "combo-toggle", "index.js");
663
+ await writeIfChanged(dest, src, options);
664
+ // Auto-register combo in config.yml
665
+ const configPath = path.join(path.dirname(extDir), "config.yml");
666
+ await ensureExtensionInConfig(configPath, dest, "combo", options);
667
+ }
668
+ async function stepAmanaiReward(extDir, options, pluginProvided = false) {
669
+ console.log("\n[8/8] Installing Amanai reward detector...");
670
+ const destDir = path.join(extDir, "amanai-reward");
671
+ if (pluginProvided) {
672
+ // The plugin's omp.extensions manifest loads the detector from
673
+ // plugins/node_modules; a copied agent/extensions entry would double-load.
674
+ if ((await readTextIfExists(path.join(destDir, "index.js"))) !== null) {
675
+ if (options.dryRun)
676
+ console.log(` [dry-run] would remove ${destDir} (now provided by the plugin)`);
677
+ else {
678
+ await fs.rm(destDir, { recursive: true, force: true });
679
+ console.log(` [rm] ${destDir} (now provided by the plugin)`);
680
+ }
681
+ }
682
+ console.log(" [ok] detector loads via the tersio plugin manifest");
683
+ return;
684
+ }
685
+ const src = await readTextIfExists(AMANAI_REWARD_INDEX);
686
+ if (!src) {
687
+ console.log(" [skip] amanai-reward/index.js not found in repo");
688
+ return;
689
+ }
690
+ await writeIfChanged(path.join(destDir, "index.js"), src, options);
691
+ }
692
+ // --- Doctor ---
693
+ async function runDoctor() {
694
+ console.log("\n=== Tersio Doctor ===\n");
695
+ // Node
696
+ console.log(` Node: ok v${process.version}`);
697
+ // OMP CLI
698
+ try {
699
+ const v = (await execP(IS_WINDOWS ? "omp.cmd" : "omp", ["--version"])).stdout.trim();
700
+ console.log(` OMP CLI: ok ${v}`);
701
+ }
702
+ catch {
703
+ console.log(" OMP CLI: MISSING");
704
+ }
705
+ // Home
706
+ console.log(` Home: ${HOME}`);
707
+ // Directories
708
+ const agentDir = path.join(HOME, ".omp", "agent");
709
+ const extDir = path.join(agentDir, "extensions");
710
+ const configPath = path.join(agentDir, "config.yml");
711
+ const pluginsDir = path.join(HOME, ".omp", "plugins");
712
+ const rtkBin = path.join(HOME, ".bun", "bin", IS_WINDOWS ? "rtk.exe" : "rtk");
713
+ const agentOk = await readTextIfExists(agentDir) !== null || (await fs.readdir(agentDir).catch(() => null)) !== null;
714
+ console.log(` OMP agent dir: ${agentOk ? "ok" : "MISSING"} ${agentDir}`);
715
+ const extOk = (await fs.readdir(extDir).catch(() => null)) !== null;
716
+ console.log(` OMP extensions dir: ${extOk ? "ok" : "MISSING"} ${extDir}`);
717
+ const sharedState = path.join(extDir, "shared", "session-state.js");
718
+ console.log(` Shared session bridge: ${(await readTextIfExists(sharedState)) !== null ? "installed" : "MISSING"}`);
719
+ const configOk = (await readTextIfExists(configPath)) !== null;
720
+ console.log(` OMP config.yml: ${configOk ? "ok" : "MISSING"} ${configPath}`);
721
+ // Ponytail
722
+ const ponytailPkg = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail", "package.json");
723
+ const ponytailExt = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail", "pi-extension", "index.js");
724
+ const ponytailInstalled = (await readTextIfExists(ponytailPkg)) !== null;
725
+ const ponytailExtInstalled = (await readTextIfExists(ponytailExt)) !== null;
726
+ console.log(` Ponytail package: ${ponytailInstalled ? "installed" : "MISSING"}`);
727
+ console.log(` Ponytail extension: ${ponytailExtInstalled ? "installed" : "MISSING"}`);
728
+ if (configOk) {
729
+ const configText = (await readTextIfExists(configPath));
730
+ const hasPonytailPath = configText.includes("ponytail") && configText.includes("pi-extension");
731
+ console.log(` Ponytail in config.yml: ${hasPonytailPath ? "registered" : "MISSING"}`);
732
+ }
733
+ // Self plugin registration (Settings → Plugins listing)
734
+ const pluginsPkgRaw = await readTextIfExists(path.join(pluginsDir, "package.json"));
735
+ let selfDep = false;
736
+ if (pluginsPkgRaw) {
737
+ try {
738
+ selfDep = PACKAGE_NAME in (JSON.parse(pluginsPkgRaw).dependencies || {});
739
+ }
740
+ catch { /* ignore */ }
741
+ }
742
+ console.log(` Self plugin in plugins/package.json: ${selfDep ? "registered" : "MISSING"}`);
743
+ const selfPkg = path.join(pluginsDir, "node_modules", PACKAGE_NAME, "package.json");
744
+ console.log(` Self plugin package: ${(await readTextIfExists(selfPkg)) !== null ? "installed" : "MISSING"}`);
745
+ // RTK
746
+ const rtkExists = (await readTextIfExists(rtkBin)) !== null;
747
+ console.log(` RTK binary: ${rtkExists ? "installed" : "MISSING"} ${rtkBin}`);
748
+ if (rtkExists) {
749
+ try {
750
+ const v = (await execP(rtkBin, ["--version"], { timeout: 5000 })).stdout.trim();
751
+ console.log(` RTK version: ${v}`);
752
+ }
753
+ catch {
754
+ console.log(" RTK version: unavailable (may not be executable)");
755
+ }
756
+ }
757
+ // Caveman
758
+ const cavemanIndex = path.join(extDir, "caveman-session", "index.js");
759
+ const cavemanRule = path.join(extDir, "caveman-session", "rule.md");
760
+ console.log(` Caveman extension: ${(await readTextIfExists(cavemanIndex)) !== null ? "installed" : "MISSING"}`);
761
+ console.log(` Caveman rule.md: ${(await readTextIfExists(cavemanRule)) !== null ? "installed" : "MISSING"}`);
762
+ // RTK extension
763
+ const rtkIndex = path.join(extDir, "rtk-session", "index.js");
764
+ console.log(` RTK extension: ${(await readTextIfExists(rtkIndex)) !== null ? "installed" : "MISSING"}`);
765
+ // Updater
766
+ const updaterIndex = path.join(extDir, "ai-addons-updater", "index.js");
767
+ console.log(` Updater extension: ${(await readTextIfExists(updaterIndex)) !== null ? "installed" : "MISSING"}`);
768
+ // Combo
769
+ const comboIndex = path.join(extDir, "combo-toggle", "index.js");
770
+ console.log(` Combo extension: ${(await readTextIfExists(comboIndex)) !== null ? "installed" : "MISSING"}`);
771
+ const modeReinforcement = path.join(extDir, "shared", "mode-reinforcement.js");
772
+ console.log(` Mode reinforcement extension: ${(await readTextIfExists(modeReinforcement)) !== null ? "installed" : "MISSING"}`);
773
+ // Amanai reward detector
774
+ const amanaiRewardIndex = path.join(extDir, "amanai-reward", "index.js");
775
+ const amanaiViaPlugin = selfDep && (await readTextIfExists(selfPkg)) !== null;
776
+ const amanaiState = (await readTextIfExists(amanaiRewardIndex)) !== null ? "installed" : amanaiViaPlugin ? "installed (via plugin manifest)" : "MISSING";
777
+ console.log(` Amanai reward detector: ${amanaiState}`);
778
+ if (configOk) {
779
+ const configText = (await readTextIfExists(configPath));
780
+ const hasComboPath = configText.includes("combo-toggle");
781
+ console.log(` Combo in config.yml: ${hasComboPath ? "registered" : "MISSING"}`);
782
+ }
783
+ }
784
+ async function runUninstall(options = {}) {
785
+ const confirmed = options.yes ?? yes;
786
+ const shouldRemovePonytail = options.removePonytail ?? removePonytail;
787
+ const shouldRemoveRtk = options.removeRtk ?? removeRtk;
788
+ const shouldDryRun = options.dryRun ?? dryRun;
789
+ console.log("\n=== Tersio Uninstall ===\n");
790
+ const extDir = path.join(HOME, ".omp", "agent", "extensions");
791
+ const configPath = path.join(HOME, ".omp", "agent", "config.yml");
792
+ const rtkBin = path.join(HOME, ".bun", "bin", IS_WINDOWS ? "rtk.exe" : "rtk");
793
+ const targets = [
794
+ path.join(extDir, "caveman-session"),
795
+ path.join(extDir, "rtk-session"),
796
+ path.join(extDir, "ai-addons-updater"),
797
+ path.join(extDir, "combo-toggle"),
798
+ path.join(extDir, "shared"),
799
+ path.join(extDir, "amanai-reward"),
800
+ // Legacy always-on combo helper; imports shared/session-state.js, so it
801
+ // breaks with a module-not-found warning once the shared dir is removed.
802
+ path.join(extDir, "aaa-combo-boot"),
803
+ ];
804
+ console.log("Will remove:");
805
+ for (const t of targets) {
806
+ console.log(` ${t}`);
807
+ }
808
+ if (shouldRemoveRtk) {
809
+ console.log(` ${rtkBin}`);
810
+ }
811
+ if (!confirmed) {
812
+ const answer = await ask("\nProceed? [y/N]: ");
813
+ if (!answer.toLowerCase().startsWith("y")) {
814
+ console.log("Aborted.");
815
+ closeRL();
816
+ return false;
817
+ }
818
+ }
819
+ // Remove extension directories
820
+ for (const t of targets) {
821
+ try {
822
+ if (shouldDryRun)
823
+ console.log(` [dry-run] would remove ${t}`);
824
+ else {
825
+ await fs.rm(t, { recursive: true, force: true });
826
+ console.log(` [rm] ${t}`);
827
+ }
828
+ }
829
+ catch {
830
+ debug(`Could not remove ${t}`);
831
+ }
832
+ }
833
+ // Remove Combo and mode-reinforcement registrations; Ponytail only when requested.
834
+ const configRaw = await readTextIfExists(configPath);
835
+ if (configRaw) {
836
+ let lines = configRaw.split("\n");
837
+ const before = lines.length;
838
+ lines = lines.filter((l) => {
839
+ if (l.includes("combo-toggle") || l.includes("mode-reinforcement"))
840
+ return false;
841
+ if (shouldRemovePonytail && l.includes("ponytail") && l.includes("pi-extension"))
842
+ return false;
843
+ return true;
844
+ });
845
+ if (lines.length !== before) {
846
+ if (shouldDryRun)
847
+ console.log(` [dry-run] would remove ${before - lines.length} config.yml entries`);
848
+ else {
849
+ await fs.writeFile(configPath, lines.join("\n"), "utf8");
850
+ console.log(` [write] Updated config.yml (removed ${before - lines.length} entries)`);
851
+ }
852
+ }
853
+ }
854
+ // Remove our plugin registration from ~/.omp/plugins
855
+ const pluginsDir = path.join(HOME, ".omp", "plugins");
856
+ const pluginsPkgPath = path.join(pluginsDir, "package.json");
857
+ const pluginsPkgRaw = await readTextIfExists(pluginsPkgPath);
858
+ if (pluginsPkgRaw) {
859
+ try {
860
+ const pkg = JSON.parse(pluginsPkgRaw);
861
+ if (pkg.dependencies && PACKAGE_NAME in pkg.dependencies) {
862
+ if (shouldDryRun)
863
+ console.log(` [dry-run] would remove ${PACKAGE_NAME} from ${pluginsPkgPath}`);
864
+ else {
865
+ delete pkg.dependencies[PACKAGE_NAME];
866
+ await fs.writeFile(pluginsPkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
867
+ console.log(` [write] Removed ${PACKAGE_NAME} from plugins/package.json`);
868
+ }
869
+ }
870
+ }
871
+ catch {
872
+ debug("Could not update plugins/package.json");
873
+ }
874
+ }
875
+ const selfPluginDir = path.join(pluginsDir, "node_modules", PACKAGE_NAME);
876
+ if ((await readTextIfExists(path.join(selfPluginDir, "package.json"))) !== null) {
877
+ try {
878
+ if (shouldDryRun)
879
+ console.log(` [dry-run] would remove ${selfPluginDir}`);
880
+ else {
881
+ await fs.rm(selfPluginDir, { recursive: true, force: true });
882
+ console.log(` [rm] ${selfPluginDir}`);
883
+ }
884
+ }
885
+ catch {
886
+ debug(`Could not remove ${selfPluginDir}`);
887
+ }
888
+ }
889
+ // Remove RTK binary if requested
890
+ if (shouldRemoveRtk) {
891
+ try {
892
+ if (shouldDryRun)
893
+ console.log(` [dry-run] would remove ${rtkBin}`);
894
+ else {
895
+ await fs.unlink(rtkBin);
896
+ console.log(` [rm] ${rtkBin}`);
897
+ }
898
+ }
899
+ catch {
900
+ debug(`Could not remove ${rtkBin}`);
901
+ }
902
+ }
903
+ console.log("\nDone. Restart OMP for changes to take effect.");
904
+ return true;
905
+ }
906
+ async function runLatestUpdate() {
907
+ const updateScope = scopeFlag || "user";
908
+ if (!["user", "project", "both"].includes(updateScope)) {
909
+ console.error(`[fail] Invalid --scope: ${updateScope}. Use: user, project, both`);
910
+ process.exitCode = 1;
911
+ return;
912
+ }
913
+ const forwardedArgs = ["--yes", "--scope", updateScope];
914
+ if (dryRun)
915
+ forwardedArgs.push("--dry-run");
916
+ if (verbose)
917
+ forwardedArgs.push("--verbose");
918
+ const npmArgs = [
919
+ "exec",
920
+ "--yes",
921
+ "--prefer-online",
922
+ `--package=${PACKAGE_NAME}@latest`,
923
+ "--",
924
+ PACKAGE_BIN,
925
+ "--apply-update",
926
+ ...forwardedArgs,
927
+ ];
928
+ const npmCommand = IS_WINDOWS ? process.env.ComSpec || "cmd.exe" : "npm";
929
+ const npmCommandArgs = IS_WINDOWS ? ["/d", "/s", "/c", "npm", ...npmArgs] : npmArgs;
930
+ console.log("=== Updating Tersio ===");
931
+ console.log(` Running the latest ${PACKAGE_NAME} installer...\n`);
932
+ try {
933
+ const result = await execP(npmCommand, npmCommandArgs, {
934
+ timeout: 300000,
935
+ maxBuffer: 10 * 1024 * 1024,
936
+ windowsHide: true,
937
+ shell: false,
938
+ });
939
+ if (result.stdout)
940
+ process.stdout.write(result.stdout);
941
+ if (result.stderr)
942
+ process.stderr.write(result.stderr);
943
+ console.log("\n=== Update complete ===");
944
+ }
945
+ catch (e) {
946
+ const err = e;
947
+ if (err.stdout)
948
+ process.stdout.write(err.stdout);
949
+ if (err.stderr)
950
+ process.stderr.write(err.stderr);
951
+ console.error(`\n[fail] Could not run ${PACKAGE_NAME}@latest: ${err.message}`);
952
+ process.exitCode = 1;
953
+ }
954
+ }
955
+ // --- Install profile (session-start mode defaults) ---
956
+ function defaultProfile() {
957
+ return {
958
+ comboDefault: "off",
959
+ cavemanDefault: "off",
960
+ rtkDefault: false,
961
+ };
962
+ }
963
+ function tty() {
964
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
965
+ }
966
+ async function resolveProfile() {
967
+ const profile = defaultProfile();
968
+ // Interactive prompt: only for a real user at a terminal, only when no
969
+ // default flags were given, and never for --apply-update runs.
970
+ if (tty() && !profileFlagsGiven && !applyUpdate && (install || reinstall)) {
971
+ const comboAnswer = (await ask(" Default Combo preset on session start? [off/medium/balanced/max] (off): ")).trim().toLowerCase();
972
+ if (comboAnswer && comboAnswer in COMBO_DEFAULTS)
973
+ profile.comboDefault = comboAnswer;
974
+ const cavemanAnswer = (await ask(" Default Caveman mode on session start? [off/lite/full/ultra/wenyan] (off): ")).trim().toLowerCase();
975
+ if (cavemanAnswer && cavemanAnswer in CAVEMAN_DEFAULTS)
976
+ profile.cavemanDefault = cavemanAnswer;
977
+ const rtkAnswer = (await ask(" Default RTK state on session start? [on/off] (off): ")).trim().toLowerCase();
978
+ if (rtkAnswer === "on" || rtkAnswer === "off")
979
+ profile.rtkDefault = rtkAnswer === "on";
980
+ }
981
+ // Explicit flags always win over the prompt.
982
+ if (comboDefaultFlag !== undefined)
983
+ profile.comboDefault = comboDefaultFlag;
984
+ if (cavemanDefaultFlag !== undefined)
985
+ profile.cavemanDefault = cavemanDefaultFlag;
986
+ if (rtkDefaultFlag !== undefined)
987
+ profile.rtkDefault = rtkDefaultFlag === "on";
988
+ console.log(` Profile: combo default=${profile.comboDefault} · caveman default=${profile.cavemanDefault} · rtk default=${profile.rtkDefault ? "on" : "off"}`);
989
+ return profile;
990
+ }
991
+ // Persist the profile as omp plugin settings so `omp plugin config get`
992
+ // reflects the choice and the extensions pick it up on session start.
993
+ async function writePluginSettings(profile, options) {
994
+ const pluginsDir = path.join(HOME, ".omp", "plugins");
995
+ const lockPath = path.join(pluginsDir, "omp-plugins.lock.json");
996
+ let config = {};
997
+ const existing = await readTextIfExists(lockPath);
998
+ if (existing) {
999
+ try {
1000
+ config = JSON.parse(existing);
1001
+ }
1002
+ catch {
1003
+ config = {};
1004
+ }
1005
+ }
1006
+ config.plugins = config.plugins || {};
1007
+ config.settings = config.settings || {};
1008
+ const settings = { ...(config.settings[PACKAGE_NAME] || {}) };
1009
+ settings.comboDefault = profile.comboDefault;
1010
+ settings.cavemanDefault = profile.cavemanDefault;
1011
+ settings.rtkDefault = profile.rtkDefault;
1012
+ config.settings[PACKAGE_NAME] = settings;
1013
+ if (options.dryRun) {
1014
+ console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
1015
+ return;
1016
+ }
1017
+ await fs.mkdir(pluginsDir, { recursive: true });
1018
+ await fs.writeFile(lockPath, JSON.stringify(config, null, 2) + "\n", "utf8");
1019
+ console.log(` [write] Plugin settings in ${lockPath}`);
1020
+ }
1021
+ // --- Main ---
1022
+ async function main() {
1023
+ if (showVersion) {
1024
+ console.log(PACKAGE_VERSION);
1025
+ closeRL();
1026
+ return;
1027
+ }
1028
+ if (showHelp) {
1029
+ printHelp();
1030
+ closeRL();
1031
+ return;
1032
+ }
1033
+ if (unknownCommand) {
1034
+ console.error(`Unknown command: ${commandArg}`);
1035
+ printHelp();
1036
+ process.exitCode = 1;
1037
+ closeRL();
1038
+ return;
1039
+ }
1040
+ if (update && !applyUpdate) {
1041
+ await runLatestUpdate();
1042
+ closeRL();
1043
+ return;
1044
+ }
1045
+ if (doctor) {
1046
+ await runDoctor();
1047
+ closeRL();
1048
+ return;
1049
+ }
1050
+ if (uninstall) {
1051
+ await runUninstall();
1052
+ closeRL();
1053
+ return;
1054
+ }
1055
+ if (reinstall) {
1056
+ await runUninstall({ yes: true, removePonytail: false, removeRtk: true });
1057
+ }
1058
+ if (dryRun)
1059
+ console.log("[dry-run] No changes will be written.\n");
1060
+ console.log(`=== Tersio v${PACKAGE_VERSION} ===`);
1061
+ console.log(` Platform: ${process.platform}`);
1062
+ console.log(` Arch: ${process.arch}`);
1063
+ console.log(` Home: ${HOME}`);
1064
+ // Determine install scope
1065
+ let scope;
1066
+ if (reinstall) {
1067
+ scope = "1";
1068
+ console.log(" Scope: user (reinstall)");
1069
+ }
1070
+ else if (scopeFlag) {
1071
+ const map = { user: "1", project: "2", both: "3" };
1072
+ scope = map[scopeFlag];
1073
+ if (!scope) {
1074
+ console.log(` [fail] Invalid --scope: ${scopeFlag}. Use: user, project, both`);
1075
+ closeRL();
1076
+ process.exit(1);
1077
+ }
1078
+ console.log(` Scope: ${scopeFlag}`);
1079
+ }
1080
+ else if (install || yes) {
1081
+ scope = "1";
1082
+ console.log(` Scope: user (${install ? "install default" : "--scope omitted, defaulting to user with --yes"})`);
1083
+ }
1084
+ else {
1085
+ console.log("\nInstall scope:");
1086
+ console.log(" 1) User-level (all OMP sessions)");
1087
+ console.log(" 2) Project-level (this repo only)");
1088
+ console.log(" 3) Both");
1089
+ scope = (await ask("\nChoose [1-3] (default 1): ")).trim() || "1";
1090
+ }
1091
+ // Resolve session defaults: flags > interactive prompt > defaults.
1092
+ const profile = await resolveProfile();
1093
+ const userDir = path.join(HOME, ".omp", "agent");
1094
+ const userExtDir = path.join(userDir, "extensions");
1095
+ const userPluginsDir = path.join(userDir, "..", "plugins");
1096
+ const bunBinDir = path.join(HOME, ".bun", "bin");
1097
+ const projectExtDir = path.join(process.cwd(), ".omp", "extensions");
1098
+ const options = { dryRun, verbose, yes, scope, reinstall };
1099
+ // Check prerequisites
1100
+ console.log("\nPrerequisites:");
1101
+ try {
1102
+ const v = (await execP(IS_WINDOWS ? "omp.cmd" : "omp", ["--version"])).stdout.trim();
1103
+ console.log(` [ok] omp ${v}`);
1104
+ }
1105
+ catch {
1106
+ console.log(" [fail] omp not found — ensure it's installed");
1107
+ }
1108
+ if (scope === "1" || scope === "3") {
1109
+ console.log("\n--- User-level install ---");
1110
+ await stepSharedSessionState(userExtDir, options);
1111
+ await stepPonytail(userPluginsDir, userDir, options);
1112
+ const selfPlugin = await stepSelfPlugin(userPluginsDir, options);
1113
+ const ponytailExtPath = path.join(userPluginsDir, "node_modules", "@dietrichgebert", "ponytail", "pi-extension", "index.js");
1114
+ await stepRtk(bunBinDir, options);
1115
+ await stepRtkSession(userExtDir, options);
1116
+ await stepCaveman(userExtDir, options);
1117
+ await stepCombo(userExtDir, options);
1118
+ await stepModeReinforcement(userExtDir, ponytailExtPath, options);
1119
+ await stepUpdater(userExtDir, options);
1120
+ await stepAmanaiReward(userExtDir, options, selfPlugin);
1121
+ if (selfPlugin)
1122
+ await writePluginSettings(profile, options);
1123
+ }
1124
+ if (scope === "2" || scope === "3") {
1125
+ console.log("\n--- Project-level install ---");
1126
+ await stepSharedSessionState(projectExtDir, options);
1127
+ await stepRtkSession(projectExtDir, options);
1128
+ await stepCaveman(projectExtDir, options);
1129
+ await stepUpdater(projectExtDir, options);
1130
+ await stepAmanaiReward(projectExtDir, options);
1131
+ console.log(" [note] Ponytail, RTK binary, and Combo toggle require user-level (global) install");
1132
+ }
1133
+ console.log("\n=== Installation complete ===");
1134
+ console.log("\nNext steps:");
1135
+ console.log(" 1. Restart OMP");
1136
+ console.log(" 2. /caveman full");
1137
+ console.log(" 3. /rtk on");
1138
+ console.log(" 4. /ponytail full");
1139
+ console.log(" 5. /ai-addons check");
1140
+ console.log(" 6. /combo medium (toggle all 3 at once — off by default)");
1141
+ closeRL();
1142
+ }
1143
+ main().catch((e) => { closeRL(); console.error(e); });