@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 CHANGED
@@ -1,12 +1,13 @@
1
1
  # Tersio ✂️
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@krtclcdy%2Ftersio)](https://www.npmjs.com/package/@krtclcdy/tersio)
4
- [![npm downloads](https://img.shields.io/npm/dt/@krtclcdy%2Ftersio)](https://www.npmjs.com/package/@krtclcdy/tersio)
5
- [![license](https://img.shields.io/npm/l/@krtclcdy%2Ftersio)](./LICENSE)
6
- [![node](https://img.shields.io/node/v/@krtclcdy%2Ftersio)](https://nodejs.org)
3
+ [![npm version](https://img.shields.io/npm/v/@krtclcdy%2Ftersio?color=cb0000)](https://www.npmjs.com/package/@krtclcdy/tersio)
4
+ [![release](https://img.shields.io/github/v/release/KurutoDenzeru/tersio?color=7c3aed)](https://github.com/KurutoDenzeru/tersio/releases)
5
+ [![license](https://img.shields.io/badge/license-MIT-6e7681)](./LICENSE)
6
+ [![node](https://img.shields.io/node/v/@krtclcdy%2Ftersio?color=30363d)](https://nodejs.org)
7
7
  [![CI](https://github.com/KurutoDenzeru/tersio/actions/workflows/ci.yml/badge.svg)](https://github.com/KurutoDenzeru/tersio/actions)
8
+ [![issues](https://img.shields.io/github/issues/KurutoDenzeru/tersio?color=d97706)](https://github.com/KurutoDenzeru/tersio/issues)
8
9
 
9
- Terse replies, compact shell output, and minimal code decisions for [Oh My Pi (OMP)](https://github.com/can1357/oh-my-pi) — plus a passive Amanai reward detector.
10
+ Terse replies, compact shell output, and minimal code decisions for [Oh My Pi (OMP)](https://github.com/can1357/oh-my-pi).
10
11
 
11
12
  ## Install
12
13
 
@@ -55,9 +56,21 @@ Defaults apply to fresh sessions only — anything persisted with `/combo`, `/ca
55
56
  | **Ponytail** | Minimal, YAGNI-oriented code decisions |
56
57
  | **Combo** | Toggles all three at once. Presets: `off`, `medium`, `balanced`, `max` |
57
58
  | **Updater** | `/ai-addons` checks and updates Ponytail, RTK, and Caveman in-session, with dry-run and backups |
58
- | **Amanai reward detector** | Local notice when a final response contains a footer-shaped `AMANAI-GACHA-…` key. Never alters output, stores or sends the key, redeems it, or makes requests — redeem keys yourself in the Amanai billing dashboard |
59
59
 
60
- User-level installs also register the package in `~/.omp/plugins` (visible in OMP **Settings → Plugins**), where the detector loads through the plugin manifest instead of a copied extension entry. Active modes reassert after Ponytail's prompt block on every top-level turn, including after history compaction. The package also ships a Pi-native adapter via `pi.extensions` (final settled response only); the OMP installer does not install it.
60
+ User-level installs also register the package in `~/.omp/plugins` (visible in OMP **Settings → Plugins**), directly through the plugin manifest. Active modes reassert after Ponytail's prompt block on every top-level turn, including after history compaction.
61
+
62
+ ## Benchmarks
63
+
64
+ Measured before/after — real `o200k_base` token counts, samples and run protocol in [BENCHMARK.md](./BENCHMARK.md). Overhead is paid once per session; savings accumulate per reply, command, or task.
65
+
66
+ | Mode | Overhead (once) | Before | After | Saving | Pays off after |
67
+ |---|---:|---:|---:|---:|---|
68
+ | `/caveman lite` | 46 tok | 146 tok/reply | 67 | −55% | 1 reply |
69
+ | `/caveman full` | 172 tok | 146 tok/reply | 61 | −58% | ~2 replies |
70
+ | `/caveman ultra` | 68 tok | 146 tok/reply | 34 | −77% | 1 reply |
71
+ | `/rtk on` | 165 tok | 537 tok (grep) | 469 | −13% (−34% on `git status`) | varies |
72
+ | `/ponytail full` | 76 tok | 261–481 tok/task | 108–143 | −59 to −70% | first task |
73
+ | `/combo balanced` | 480 tok | 944 tok mixed turn | 638 | −32%/turn | first turn |
61
74
 
62
75
  ## CLI
63
76
 
@@ -70,7 +83,7 @@ User-level installs also register the package in `~/.omp/plugins` (visible in OM
70
83
  | `tersio uninstall` | Remove extensions and registration (`--remove-rtk`, `--remove-ponytail` for more) |
71
84
  | `tersio version` | Print version |
72
85
 
73
- Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--scope`, `--combo-default`/`--caveman-default`/`--rtk-default`. Legacy `--doctor` / `--uninstall` forms still work.
86
+ Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--scope`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`. Legacy `--doctor` / `--uninstall` forms still work.
74
87
 
75
88
  ## Commands reference
76
89
 
@@ -136,7 +149,7 @@ Covers noisy commands (`git status`, `git diff`, `read`, `grep`, test, `tsc`, li
136
149
 
137
150
  | What | Path |
138
151
  |---|---|
139
- | Caveman / RTK / Updater / Combo / Amanai extensions | `~/.omp/agent/extensions/{caveman-session,rtk-session,ai-addons-updater,combo-toggle,amanai-reward}/` |
152
+ | Caveman / RTK / Updater / Combo extensions | `~/.omp/agent/extensions/{caveman-session,rtk-session,ai-addons-updater,combo-toggle}/` |
140
153
  | Ponytail package | `~/.omp/plugins/node_modules/@dietrichgebert/ponytail/` |
141
154
  | RTK binary | `~/.bun/bin/rtk` (`rtk.exe` on Windows) |
142
155
  | Extension registrations | `~/.omp/agent/config.yml` |
@@ -3,228 +3,200 @@
3
3
  // ponytail: `skipped: none` — semantics match one-liner: fetch + compare + run install.
4
4
  // rtk: `skipped: signature verification` — checksums.txt ships only SHA256 of release assets; add sigchain when upstream publishes a signing key.
5
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";
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 { CAVEMAN_REMOTE_RULE as CAVEMAN_REMOTE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, } from '../lib/utils.js';
12
+ const IS_WINDOWS = process.platform === 'win32';
13
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.";
14
+ const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
15
+ const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
16
+ const PONYTAIL_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
17
+ const RTK_BINARY = path.join(HOME, '.bun', 'bin', IS_WINDOWS ? 'rtk.exe' : 'rtk');
18
+ const CAVEMAN_LOCAL = path.join(HOME, '.omp', 'agent', 'extensions', 'caveman-session', 'rule.md');
19
+ const RELOAD_MSG = 'Reminder: restart OMP (or reload extensions) for updates to take effect.';
21
20
  function notify(ctx, msg, level) {
22
21
  ctx?.ui?.notify?.(String(msg), level);
23
22
  }
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);
23
+ function report(ctx, msg, level) {
24
+ notify(ctx, msg, level);
25
+ return msg;
26
+ }
27
+ function shortHash(text) {
28
+ return sha256Hex(text).slice(0, 16);
29
+ }
30
+ function checkFailed(name, e) {
31
+ return { text: `${name} check failed: ${e.message}`, level: 'warning' };
32
+ }
33
+ // Single error-handling source for the three probes; messages unchanged.
34
+ function runCheck(name, probe) {
35
+ return probe().then((text) => ({ text, level: 'info' }), (e) => checkFailed(name, e));
36
+ }
37
+ // Check: no mutation. Probes run concurrently via checkAddons below.
38
+ function checkPonytail() {
39
+ return runCheck('Ponytail', async () => {
40
+ const remoteJson = await fetchJson(PONYTAIL_REMOTE);
31
41
  const localRaw = await readTextIfExists(PONYTAIL_LOCAL);
32
42
  const localVer = localRaw ? JSON.parse(localRaw).version ?? null : null;
33
43
  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);
44
+ const status = !localVer ? 'not installed'
45
+ : localVer === remoteVer ? 'up to date'
46
+ : 'update available';
47
+ return `Ponytail ${status}: local=${localVer || '—'} latest=${remoteVer}`;
48
+ });
49
+ }
50
+ async function fetchRelease() {
51
+ return fetchJson(RTK_RELEASE_API);
52
+ }
53
+ function checkRtk() {
54
+ return runCheck('RTK', async () => {
55
+ const release = await fetchRelease();
50
56
  const latestTag = release.tag_name || null;
51
57
  let localVer = null;
52
58
  try {
53
- const out = execFileSync(RTK_BINARY, ["--version"], { encoding: "utf8", windowsHide: true, shell: false, timeout: 10000 }) || "";
59
+ const out = execFileSync(RTK_BINARY, ['--version'], { encoding: 'utf8', windowsHide: true, shell: false, timeout: 10000 }) || '';
54
60
  if (out)
55
61
  localVer = out.trim().split(/\r?\n/)[0];
56
62
  }
57
63
  catch {
58
64
  localVer = null;
59
65
  }
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 {
66
+ const status = localVer === null ? 'not installed'
67
+ : normalizeRtkVersion(localVer) === normalizeRtkVersion(latestTag ?? undefined) ? 'up to date'
68
+ : 'update available';
69
+ return `RTK ${status}: local=${localVer || '—'} latest=${latestTag || '—'}`;
70
+ });
71
+ }
72
+ // Caveman (rule.md)
73
+ function checkCaveman() {
74
+ return runCheck('Caveman', async () => {
74
75
  const remote = await httpsGet(CAVEMAN_REMOTE);
75
- const remoteHash = sha256Hex(remote).slice(0, 16);
76
+ const remoteHash = shortHash(remote);
76
77
  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");
78
+ const localHash = local ? shortHash(local) : null;
79
+ const status = !local ? 'rule.md missing'
80
+ : localHash === remoteHash ? 'rule.md up to date'
81
+ : 'rule.md update available';
82
+ return `Caveman ${status}: local=${localHash || '—'} remote=${remoteHash}`;
83
+ });
84
+ }
85
+ async function checkAddons(ctx) {
86
+ const [ponytail, rtk, caveman] = await Promise.all([checkPonytail(), checkRtk(), checkCaveman()]);
87
+ const lines = [];
88
+ for (const status of [ponytail, rtk, caveman]) {
89
+ lines.push(status.text);
90
+ notify(ctx, status.text, status.level);
89
91
  }
90
- return lines.join("\n");
92
+ return lines.join('\n');
91
93
  }
92
94
  async function updatePonytail(pi, ctx, dryRun = false) {
93
- const pluginsDir = path.join(HOME, ".omp", "plugins");
95
+ const pluginsDir = path.join(HOME, '.omp', 'plugins');
94
96
  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;
97
+ const m = `Ponytail dry-run: would run \`npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund\` in ${pluginsDir}.`;
98
+ return report(ctx, m, 'info');
98
99
  }
99
- notify(ctx, "Ponytail: ensuring plugin directory exists…", "info");
100
+ notify(ctx, 'Ponytail: ensuring plugin directory exists…', 'info');
100
101
  try {
101
102
  await fs.mkdir(pluginsDir, { recursive: true });
102
103
  }
103
104
  catch (e) {
104
105
  const m = `Ponytail update failed: failed to create ${pluginsDir}: ${e.message}`;
105
- notify(ctx, m, "warning");
106
- return m;
106
+ return report(ctx, m, 'warning');
107
+ }
108
+ if (!pi.exec) {
109
+ const m = 'Ponytail update failed: extension host does not provide exec';
110
+ return report(ctx, m, 'warning');
107
111
  }
108
- notify(ctx, "Ponytail: running npm install…", "info");
109
- let out = "";
112
+ notify(ctx, 'Ponytail: running npm install…', 'info');
113
+ let out = '';
110
114
  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();
115
+ const r = await pi.exec('npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir });
116
+ out = [r.stdout, r.stderr].filter(Boolean).join('\n').trim();
113
117
  if (r.code !== 0)
114
118
  throw new Error(r.stderr || `npm exited ${r.code}`);
115
119
  }
116
120
  catch (e) {
117
121
  const m = `Ponytail update failed: ${e.message}`;
118
- notify(ctx, m, "warning");
119
- return m;
122
+ return report(ctx, m, 'warning');
120
123
  }
121
- const m = `Ponytail update finished.${out ? `\n${out}` : ""}\n${RELOAD_MSG}`;
122
- notify(ctx, "Ponytail update finished. " + RELOAD_MSG, "info");
124
+ const m = `Ponytail update finished.${out ? `\n${out}` : ''}\n${RELOAD_MSG}`;
125
+ notify(ctx, 'Ponytail update finished. ' + RELOAD_MSG, 'info');
123
126
  return m;
124
127
  }
125
128
  async function updateRtk(ctx, dryRun = false) {
126
129
  let release;
127
130
  try {
128
- const raw = await httpsGet(RTK_RELEASE_API);
129
- release = JSON.parse(raw);
131
+ release = await fetchRelease();
130
132
  }
131
133
  catch (e) {
132
134
  const m = `RTK: cannot fetch release info: ${e.message}`;
133
- notify(ctx, m, "warning");
134
- return m;
135
+ return report(ctx, m, 'warning');
135
136
  }
136
- const tag = release.tag_name || "unknown";
137
+ const tag = release.tag_name || 'unknown';
137
138
  const assets = Array.isArray(release.assets) ? release.assets : [];
138
139
  // Cross-platform asset selection (mirrors installer stepRtk)
139
140
  const PLATFORM = process.platform;
140
141
  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 {
142
+ const spec = rtkPlatformSpec(PLATFORM, ARCH);
143
+ if (!spec) {
170
144
  const m = `RTK: unsupported platform ${PLATFORM}/${ARCH}`;
171
- notify(ctx, m, "warning");
172
- return m;
145
+ return report(ctx, m, 'warning');
173
146
  }
147
+ const { triple: assetTriple, ext: assetExt, binary: binaryName } = spec;
174
148
  const asset = assets.find((a) => a.name === `rtk-${assetTriple}${assetExt}`);
175
- const checksAsset = assets.find((a) => a.name === "checksums.txt");
149
+ const checksAsset = assets.find((a) => a.name === 'checksums.txt');
176
150
  if (!asset || !checksAsset) {
177
151
  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;
152
+ return report(ctx, m, 'warning');
180
153
  }
181
154
  if (dryRun) {
182
155
  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;
156
+ return report(ctx, m, 'info');
185
157
  }
186
- const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "rtk-update-"));
158
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'rtk-update-'));
187
159
  const archivePath = path.join(tmp, asset.name);
188
- const checksPath = path.join(tmp, "checksums.txt");
160
+ const checksPath = path.join(tmp, 'checksums.txt');
189
161
  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);
162
+ notify(ctx, `RTK: downloading ${asset.name} (${tag})…`, 'info');
163
+ notify(ctx, 'RTK: downloading checksums.txt…', 'info');
164
+ await Promise.all([
165
+ httpsDownload(asset.browser_download_url, archivePath),
166
+ httpsDownload(checksAsset.browser_download_url, checksPath),
167
+ ]);
194
168
  // Verify SHA256 against checksums.txt
195
- const checks = await fs.readFile(checksPath, "utf8");
169
+ const checks = await fs.readFile(checksPath, 'utf8');
196
170
  const expected = parseChecksum(checks, asset.name);
197
171
  if (!expected) {
198
172
  const m = `RTK: checksums.txt has no entry for ${asset.name}`;
199
- notify(ctx, m, "warning");
200
- return m;
173
+ return report(ctx, m, 'warning');
201
174
  }
202
175
  const archiveBuf = await fs.readFile(archivePath);
203
- const actual = createHash("sha256").update(archiveBuf).digest("hex").toLowerCase();
176
+ const actual = createHash('sha256').update(archiveBuf).digest('hex').toLowerCase();
204
177
  if (actual !== expected) {
205
178
  const m = `RTK: checksum mismatch! expected=${expected.slice(0, 12)}… actual=${actual.slice(0, 12)}…`;
206
- notify(ctx, m, "warning");
207
- return m;
179
+ return report(ctx, m, 'warning');
208
180
  }
209
- notify(ctx, "RTK: checksum verified.", "info");
181
+ notify(ctx, 'RTK: checksum verified.', 'info');
210
182
  // Extract by archive format
211
- const extractDir = path.join(tmp, "extracted");
183
+ const extractDir = path.join(tmp, 'extracted');
212
184
  await fs.mkdir(extractDir, { recursive: true });
213
- if (asset.name.endsWith(".zip")) {
185
+ if (asset.name.endsWith('.zip')) {
214
186
  if (IS_WINDOWS) {
215
- execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
216
- `Expand-Archive -LiteralPath "${archivePath}" -DestinationPath "${extractDir}" -Force`], { encoding: "utf8", windowsHide: true, shell: false });
187
+ execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
188
+ `Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${extractDir}' -Force`], { encoding: 'utf8', windowsHide: true, shell: false });
217
189
  }
218
190
  else {
219
- execFileSync("unzip", [archivePath, "-d", extractDir], { encoding: "utf8", shell: false });
191
+ execFileSync('unzip', [archivePath, '-d', extractDir], { encoding: 'utf8', shell: false });
220
192
  }
221
193
  }
222
- else if (asset.name.endsWith(".tar.gz") || asset.name.endsWith(".tgz")) {
194
+ else if (asset.name.endsWith('.tar.gz') || asset.name.endsWith('.tgz')) {
223
195
  try {
224
- execFileSync("tar", ["xzf", archivePath, "-C", extractDir], { encoding: "utf8", shell: false });
196
+ execFileSync('tar', ['xzf', archivePath, '-C', extractDir], { encoding: 'utf8', shell: false });
225
197
  }
226
198
  catch (e) {
227
- execFileSync("sh", ["-c", `gunzip < "${archivePath}" | tar xf - -C "${extractDir}"`], { encoding: "utf8", shell: false });
199
+ throw new Error(`tar could not extract ${asset.name}: ${e.message}`);
228
200
  }
229
201
  }
230
202
  else {
@@ -233,165 +205,121 @@ async function updateRtk(ctx, dryRun = false) {
233
205
  const rtkExtracted = await findFile(extractDir, binaryName);
234
206
  if (!rtkExtracted) {
235
207
  const m = `RTK: ${binaryName} not found in extracted archive`;
236
- notify(ctx, m, "warning");
237
- return m;
208
+ return report(ctx, m, 'warning');
238
209
  }
239
210
  await fs.mkdir(path.dirname(RTK_BINARY), { recursive: true });
240
211
  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
- }
212
+ const backedUp = await fs.copyFile(RTK_BINARY, backupPath).then(() => true, () => false);
249
213
  await fs.copyFile(rtkExtracted, RTK_BINARY);
250
214
  // Set executable bit on Unix
251
215
  if (!IS_WINDOWS) {
252
216
  await fs.chmod(RTK_BINARY, 0o755);
253
217
  }
254
- let versionOut = "";
218
+ let versionOut = '';
255
219
  try {
256
- versionOut = execFileSync(RTK_BINARY, ["--version"], { encoding: "utf8", windowsHide: true, shell: false, timeout: 10000 }).trim();
220
+ versionOut = execFileSync(RTK_BINARY, ['--version'], { encoding: 'utf8', windowsHide: true, shell: false, timeout: 10000 }).trim();
257
221
  }
258
222
  catch (e) {
259
223
  if (backedUp)
260
224
  await fs.copyFile(backupPath, RTK_BINARY);
261
- throw new Error(`new ${binaryName} failed --version${backedUp ? "; restored backup" : ""}: ${e.message}`);
225
+ throw new Error(`new ${binaryName} failed --version${backedUp ? '; restored backup' : ''}: ${e.message}`);
262
226
  }
263
227
  if (normalizeRtkVersion(versionOut) !== normalizeRtkVersion(tag)) {
264
228
  if (backedUp)
265
229
  await fs.copyFile(backupPath, RTK_BINARY);
266
- throw new Error(`new ${binaryName} reports ${versionOut}, expected ${tag}${backedUp ? "; restored backup" : ""}`);
230
+ throw new Error(`new ${binaryName} reports ${versionOut}, expected ${tag}${backedUp ? '; restored backup' : ''}`);
267
231
  }
268
- const m = `RTK updated to ${tag} → ${RTK_BINARY}\nbackup=${backedUp ? backupPath : "—"}\n${RELOAD_MSG}`;
269
- notify(ctx, "RTK update finished. " + RELOAD_MSG, "info");
232
+ const m = `RTK updated to ${tag} → ${RTK_BINARY}\nbackup=${backedUp ? backupPath : '—'}\n${RELOAD_MSG}`;
233
+ notify(ctx, 'RTK update finished. ' + RELOAD_MSG, 'info');
270
234
  return m;
271
235
  }
272
236
  catch (e) {
273
237
  const m = `RTK update failed: ${e.message}`;
274
- notify(ctx, m, "warning");
275
- return m;
238
+ return report(ctx, m, 'warning');
276
239
  }
277
240
  finally {
278
241
  fs.rm(tmp, { recursive: true, force: true }).catch(() => { });
279
242
  }
280
243
  }
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
244
  async function updateCaveman(ctx, dryRun = false) {
309
245
  let remote;
310
246
  try {
311
247
  remote = await httpsGet(CAVEMAN_REMOTE);
312
248
  }
313
249
  catch (e) {
314
- const m = `Caveman update failed: ${e.message}`;
315
- notify(ctx, m, "warning");
316
- return m;
250
+ return report(ctx, `Caveman update failed: ${e.message}`, 'warning');
317
251
  }
318
- const remoteHash = sha256Hex(remote).slice(0, 16);
252
+ const remoteHash = shortHash(remote);
319
253
  const oldLocal = await readTextIfExists(CAVEMAN_LOCAL);
320
- const oldHash = oldLocal ? sha256Hex(oldLocal).slice(0, 16) : null;
254
+ const oldHash = oldLocal ? shortHash(oldLocal) : null;
321
255
  if (dryRun) {
322
- const m = `Caveman dry-run: would write ${CAVEMAN_LOCAL}\nold=${oldHash || "—"} new=${remoteHash}.`;
323
- notify(ctx, m, "info");
324
- return m;
256
+ const m = `Caveman dry-run: would write ${CAVEMAN_LOCAL}\nold=${oldHash || '—'} new=${remoteHash}.`;
257
+ return report(ctx, m, 'info');
325
258
  }
326
259
  try {
327
260
  await fs.mkdir(path.dirname(CAVEMAN_LOCAL), { recursive: true });
328
261
  const backupPath = `${CAVEMAN_LOCAL}.bak`;
329
262
  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);
263
+ await fs.writeFile(backupPath, oldLocal, 'utf8');
264
+ await fs.writeFile(CAVEMAN_LOCAL, remote, 'utf8');
265
+ const written = await fs.readFile(CAVEMAN_LOCAL, 'utf8');
266
+ const writtenHash = shortHash(written);
334
267
  if (writtenHash !== remoteHash) {
335
268
  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" : ""}`);
269
+ await fs.writeFile(CAVEMAN_LOCAL, oldLocal, 'utf8');
270
+ throw new Error(`written hash ${writtenHash} did not match remote ${remoteHash}${oldLocal !== null ? '; restored backup' : ''}`);
338
271
  }
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");
272
+ const m = `Caveman rule.md updated → ${CAVEMAN_LOCAL}\nold=${oldHash || '—'} new=${remoteHash}\nbackup=${oldLocal !== null ? backupPath : '—'}\n${RELOAD_MSG}`;
273
+ notify(ctx, 'Caveman rule.md updated. ' + RELOAD_MSG, 'info');
341
274
  return m;
342
275
  }
343
276
  catch (e) {
344
277
  const m = `Caveman update failed: ${e.message}`;
345
- notify(ctx, m, "warning");
346
- return m;
278
+ return report(ctx, m, 'warning');
347
279
  }
348
280
  }
349
281
  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]",
282
+ pi.setLabel?.('AI add-ons updater');
283
+ pi.registerCommand?.('ai-addons', {
284
+ description: 'Check or update AI add-ons (ponytail/rtk/caveman/all). Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]',
353
285
  handler: async (args, ctx) => {
354
- const arg = String(args || "").trim().toLowerCase();
286
+ const arg = String(args || '').trim().toLowerCase();
355
287
  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");
288
+ const dryRun = parts.includes('--dry-run') || parts.includes('dry-run');
289
+ const cleanParts = parts.filter((p) => p !== '--dry-run' && p !== 'dry-run');
358
290
  const sub = cleanParts[0];
359
- if (sub === "check" || sub === "status") {
291
+ if (sub === 'check' || sub === 'status') {
360
292
  const summary = await checkAddons(ctx);
361
- notify(ctx, "ai-addons check complete.", "info");
293
+ notify(ctx, 'ai-addons check complete.', 'info');
362
294
  return summary;
363
295
  }
364
- if (sub === "update" && cleanParts[1]) {
365
- const target = cleanParts.slice(1).join(" ");
296
+ if (sub === 'update' && cleanParts[1]) {
297
+ const target = cleanParts.slice(1).join(' ');
366
298
  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));
299
+ const updaters = {
300
+ ponytail: () => updatePonytail(pi, ctx, dryRun),
301
+ rtk: () => updateRtk(ctx, dryRun),
302
+ caveman: () => updateCaveman(ctx, dryRun),
303
+ };
304
+ if (target === 'all') {
305
+ notify(ctx, `ai-addons update all${dryRun ? ' dry-run' : ''}: starting ponytail → rtk → caveman sequentially…`, 'info');
306
+ for (const name of ['ponytail', 'rtk', 'caveman'])
307
+ results.push(await updaters[name]());
381
308
  if (!dryRun)
382
309
  results.push(RELOAD_MSG);
383
- notify(ctx, `ai-addons update all ${dryRun ? "dry-run " : ""}complete.${dryRun ? "" : ` ${RELOAD_MSG}`}`, "info");
310
+ notify(ctx, `ai-addons update all ${dryRun ? 'dry-run ' : ''}complete.${dryRun ? '' : ` ${RELOAD_MSG}`}`, 'info');
311
+ }
312
+ else if (Object.hasOwn(updaters, target)) {
313
+ results.push(await updaters[target]());
384
314
  }
385
315
  else {
386
- const m = "Usage: /ai-addons update <ponytail|rtk|caveman|all> [--dry-run]";
387
- notify(ctx, m, "warning");
388
- return m;
316
+ const m = 'Usage: /ai-addons update <ponytail|rtk|caveman|all> [--dry-run]';
317
+ return report(ctx, m, 'warning');
389
318
  }
390
- return results.join("\n\n");
319
+ return results.join('\n\n');
391
320
  }
392
- const m = "Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]";
393
- notify(ctx, m, "warning");
394
- return m;
321
+ const m = 'Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]';
322
+ return report(ctx, m, 'warning');
395
323
  },
396
324
  });
397
325
  }