@krtclcdy/tersio 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -9
- package/extensions/ai-addons-updater/index.js +166 -238
- package/extensions/caveman-session/index.js +48 -67
- package/extensions/combo-toggle/index.js +63 -71
- package/extensions/lib/utils.js +77 -24
- package/extensions/rtk-session/index.js +48 -63
- package/extensions/shared/mode-reinforcement.js +11 -14
- package/extensions/shared/plugin-settings.js +18 -16
- package/extensions/shared/session-state.js +77 -45
- package/package.json +6 -15
- package/tersio.js +689 -703
- package/extensions/amanai-reward/index.js +0 -24
- package/extensions/amanai-reward/pi.js +0 -32
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# Tersio ✂️
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@krtclcdy/tersio)
|
|
4
|
-
[](https://nodejs.org)
|
|
3
|
+
[](https://www.npmjs.com/package/@krtclcdy/tersio)
|
|
4
|
+
[](https://github.com/KurutoDenzeru/tersio/releases)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
7
|
[](https://github.com/KurutoDenzeru/tersio/actions)
|
|
8
|
+
[](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)
|
|
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**),
|
|
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
|
|
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
|
|
7
|
-
import { createHash } from
|
|
8
|
-
import fs from
|
|
9
|
-
import os from
|
|
10
|
-
import path from
|
|
11
|
-
import { httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, } from
|
|
12
|
-
const IS_WINDOWS = process.platform ===
|
|
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 =
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const RTK_BINARY = path.join(HOME,
|
|
18
|
-
const
|
|
19
|
-
const
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 ?
|
|
35
|
-
: localVer === remoteVer ?
|
|
36
|
-
:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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, [
|
|
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
|
|
61
|
-
: normalizeRtkVersion(localVer) === normalizeRtkVersion(latestTag ?? undefined) ?
|
|
62
|
-
:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 =
|
|
76
|
+
const remoteHash = shortHash(remote);
|
|
76
77
|
const local = await readTextIfExists(CAVEMAN_LOCAL);
|
|
77
|
-
const localHash = local ?
|
|
78
|
-
const status = !local ?
|
|
79
|
-
: localHash === remoteHash ?
|
|
80
|
-
:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
92
|
+
return lines.join('\n');
|
|
91
93
|
}
|
|
92
94
|
async function updatePonytail(pi, ctx, dryRun = false) {
|
|
93
|
-
const pluginsDir = path.join(HOME,
|
|
95
|
+
const pluginsDir = path.join(HOME, '.omp', 'plugins');
|
|
94
96
|
if (dryRun) {
|
|
95
|
-
const m = `Ponytail dry-run: would run \`npm install
|
|
96
|
-
|
|
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,
|
|
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
|
-
|
|
106
|
-
|
|
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,
|
|
109
|
-
let out =
|
|
112
|
+
notify(ctx, 'Ponytail: running npm install…', 'info');
|
|
113
|
+
let out = '';
|
|
110
114
|
try {
|
|
111
|
-
const r = await pi.exec(
|
|
112
|
-
out = [r.stdout, r.stderr].filter(Boolean).join(
|
|
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
|
-
|
|
119
|
-
return m;
|
|
122
|
+
return report(ctx, m, 'warning');
|
|
120
123
|
}
|
|
121
|
-
const m = `Ponytail update finished.${out ? `\n${out}` :
|
|
122
|
-
notify(ctx,
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
return m;
|
|
135
|
+
return report(ctx, m, 'warning');
|
|
135
136
|
}
|
|
136
|
-
const tag = release.tag_name ||
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
return m;
|
|
156
|
+
return report(ctx, m, 'info');
|
|
185
157
|
}
|
|
186
|
-
const tmp = await fs.mkdtemp(path.join(os.tmpdir(),
|
|
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,
|
|
160
|
+
const checksPath = path.join(tmp, 'checksums.txt');
|
|
189
161
|
try {
|
|
190
|
-
notify(ctx, `RTK: downloading ${asset.name} (${tag})…`,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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,
|
|
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
|
-
|
|
200
|
-
return m;
|
|
173
|
+
return report(ctx, m, 'warning');
|
|
201
174
|
}
|
|
202
175
|
const archiveBuf = await fs.readFile(archivePath);
|
|
203
|
-
const actual = createHash(
|
|
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
|
-
|
|
207
|
-
return m;
|
|
179
|
+
return report(ctx, m, 'warning');
|
|
208
180
|
}
|
|
209
|
-
notify(ctx,
|
|
181
|
+
notify(ctx, 'RTK: checksum verified.', 'info');
|
|
210
182
|
// Extract by archive format
|
|
211
|
-
const extractDir = path.join(tmp,
|
|
183
|
+
const extractDir = path.join(tmp, 'extracted');
|
|
212
184
|
await fs.mkdir(extractDir, { recursive: true });
|
|
213
|
-
if (asset.name.endsWith(
|
|
185
|
+
if (asset.name.endsWith('.zip')) {
|
|
214
186
|
if (IS_WINDOWS) {
|
|
215
|
-
execFileSync(
|
|
216
|
-
`Expand-Archive -LiteralPath
|
|
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(
|
|
191
|
+
execFileSync('unzip', [archivePath, '-d', extractDir], { encoding: 'utf8', shell: false });
|
|
220
192
|
}
|
|
221
193
|
}
|
|
222
|
-
else if (asset.name.endsWith(
|
|
194
|
+
else if (asset.name.endsWith('.tar.gz') || asset.name.endsWith('.tgz')) {
|
|
223
195
|
try {
|
|
224
|
-
execFileSync(
|
|
196
|
+
execFileSync('tar', ['xzf', archivePath, '-C', extractDir], { encoding: 'utf8', shell: false });
|
|
225
197
|
}
|
|
226
198
|
catch (e) {
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, [
|
|
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 ?
|
|
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 ?
|
|
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 :
|
|
269
|
-
notify(ctx,
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
notify(ctx, m, "warning");
|
|
316
|
-
return m;
|
|
250
|
+
return report(ctx, `Caveman update failed: ${e.message}`, 'warning');
|
|
317
251
|
}
|
|
318
|
-
const remoteHash =
|
|
252
|
+
const remoteHash = shortHash(remote);
|
|
319
253
|
const oldLocal = await readTextIfExists(CAVEMAN_LOCAL);
|
|
320
|
-
const oldHash = oldLocal ?
|
|
254
|
+
const oldHash = oldLocal ? shortHash(oldLocal) : null;
|
|
321
255
|
if (dryRun) {
|
|
322
|
-
const m = `Caveman dry-run: would write ${CAVEMAN_LOCAL}\nold=${oldHash ||
|
|
323
|
-
|
|
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,
|
|
331
|
-
await fs.writeFile(CAVEMAN_LOCAL, remote,
|
|
332
|
-
const written = await fs.readFile(CAVEMAN_LOCAL,
|
|
333
|
-
const writtenHash =
|
|
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,
|
|
337
|
-
throw new Error(`written hash ${writtenHash} did not match remote ${remoteHash}${oldLocal !== null ?
|
|
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 ||
|
|
340
|
-
notify(ctx,
|
|
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
|
-
|
|
346
|
-
return m;
|
|
278
|
+
return report(ctx, m, 'warning');
|
|
347
279
|
}
|
|
348
280
|
}
|
|
349
281
|
export default function aiAddonsUpdaterExtension(pi) {
|
|
350
|
-
pi.setLabel?.(
|
|
351
|
-
pi.registerCommand?.(
|
|
352
|
-
description:
|
|
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 ||
|
|
286
|
+
const arg = String(args || '').trim().toLowerCase();
|
|
355
287
|
const parts = arg.split(/\s+/).filter(Boolean);
|
|
356
|
-
const dryRun = parts.includes(
|
|
357
|
-
const cleanParts = parts.filter((p) => p !==
|
|
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 ===
|
|
291
|
+
if (sub === 'check' || sub === 'status') {
|
|
360
292
|
const summary = await checkAddons(ctx);
|
|
361
|
-
notify(ctx,
|
|
293
|
+
notify(ctx, 'ai-addons check complete.', 'info');
|
|
362
294
|
return summary;
|
|
363
295
|
}
|
|
364
|
-
if (sub ===
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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 ?
|
|
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 =
|
|
387
|
-
|
|
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(
|
|
319
|
+
return results.join('\n\n');
|
|
391
320
|
}
|
|
392
|
-
const m =
|
|
393
|
-
|
|
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
|
}
|