@krtclcdy/tersio 2.25.7 → 2.25.8
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 +17 -10
- package/dashboard/dist/index.html +1 -1
- package/dist/cli/common.js +25 -3
- package/dist/cli/dashboard.js +7 -2
- package/dist/cli/doctor-fix.js +12 -7
- package/dist/cli/doctor.js +16 -3
- package/dist/cli/install.js +5 -5
- package/dist/cli/profile.js +28 -3
- package/dist/cli/rtk-wiring.js +13 -10
- package/dist/cli/usage.js +5 -0
- package/dist/extensions/caveman/index.js +3 -6
- package/dist/extensions/combo/index.js +1 -6
- package/dist/extensions/commands/index.js +7 -2
- package/dist/extensions/pstack/index.js +2 -1
- package/dist/extensions/rtk/filter.js +45 -4
- package/dist/extensions/rtk/session.js +4 -2
- package/dist/extensions/server.js +44 -29
- package/dist/extensions/shared/agent-stats.js +21 -13
- package/dist/extensions/shared/pricing.js +5 -4
- package/dist/extensions/shared/rtk-eligible.js +7 -11
- package/dist/extensions/shared/rtk-gain.js +2 -6
- package/dist/extensions/shared/sqlite.js +1 -6
- package/dist/extensions/shared/usage-ledger.js +97 -12
- package/dist/extensions/shared/utils.js +33 -0
- package/dist/extensions/updater/index.js +15 -7
- package/extensions/caveman/index.ts +3 -6
- package/extensions/combo/index.ts +1 -6
- package/extensions/commands/index.ts +7 -2
- package/extensions/pstack/index.ts +2 -1
- package/extensions/pstack/playbooks/autopilot-full.md +1 -1
- package/extensions/pstack/playbooks/babysit.md +1 -1
- package/extensions/pstack/playbooks/eval.md +1 -1
- package/extensions/pstack/playbooks/multi-phase-plan.md +2 -2
- package/extensions/pstack/playbooks/orchestrate.md +3 -3
- package/extensions/pstack/playbooks/session-pickup.md +1 -1
- package/extensions/pstack/playbooks/shipping.md +2 -2
- package/extensions/pstack/playbooks/worktree-cleanup.md +1 -1
- package/extensions/pstack/skills/reflect/SKILL.md +2 -2
- package/extensions/pstack/skills/show-me-your-work/SKILL.md +2 -2
- package/extensions/rtk/filter.ts +51 -4
- package/extensions/rtk/session.ts +4 -2
- package/extensions/server.ts +31 -23
- package/extensions/shared/agent-stats.ts +21 -12
- package/extensions/shared/pricing.ts +7 -5
- package/extensions/shared/rtk-eligible.ts +7 -11
- package/extensions/shared/rtk-gain.ts +2 -6
- package/extensions/shared/sqlite.ts +1 -6
- package/extensions/shared/usage-ledger.ts +102 -14
- package/extensions/shared/utils.ts +29 -0
- package/extensions/updater/index.ts +18 -7
- package/package.json +3 -5
- package/dist/bench/code-out/pfull-0/debounce.js +0 -13
- package/dist/bench/code-out/pfull-0/deepClone.js +0 -18
- package/dist/bench/code-out/pfull-0/groupBy.js +0 -15
- package/dist/bench/code-out/pfull-1/debounce.js +0 -13
- package/dist/bench/code-out/pfull-1/deepClone.js +0 -17
- package/dist/bench/code-out/pfull-1/groupBy.js +0 -14
- package/dist/bench/code-out/pfull-2/debounce.js +0 -13
- package/dist/bench/code-out/pfull-2/deepClone.js +0 -18
- package/dist/bench/code-out/pfull-2/groupBy.js +0 -15
- package/dist/bench/code-out/pfull-3/debounce.js +0 -12
- package/dist/bench/code-out/pfull-3/deepClone.js +0 -17
- package/dist/bench/code-out/pfull-3/groupBy.js +0 -7
- package/dist/bench/code-out/pfull-4/debounce.js +0 -10
- package/dist/bench/code-out/pfull-4/deepClone.js +0 -17
- package/dist/bench/code-out/pfull-4/groupBy.js +0 -14
- package/dist/bench/code-out/plite-0/debounce.js +0 -13
- package/dist/bench/code-out/plite-0/deepClone.js +0 -18
- package/dist/bench/code-out/plite-0/groupBy.js +0 -15
- package/dist/bench/code-out/plite-1/debounce.js +0 -13
- package/dist/bench/code-out/plite-1/deepClone.js +0 -17
- package/dist/bench/code-out/plite-1/groupBy.js +0 -11
- package/dist/bench/code-out/plite-2/debounce.js +0 -12
- package/dist/bench/code-out/plite-2/deepClone.js +0 -17
- package/dist/bench/code-out/plite-2/groupBy.js +0 -14
- package/dist/bench/code-out/plite-3/debounce.js +0 -12
- package/dist/bench/code-out/plite-3/deepClone.js +0 -17
- package/dist/bench/code-out/plite-3/groupBy.js +0 -14
- package/dist/bench/code-out/plite-4/debounce.js +0 -12
- package/dist/bench/code-out/plite-4/deepClone.js +0 -17
- package/dist/bench/code-out/plite-4/groupBy.js +0 -14
- package/dist/bench/code-out/pultra-0/debounce.js +0 -12
- package/dist/bench/code-out/pultra-0/deepClone.js +0 -20
- package/dist/bench/code-out/pultra-0/groupBy.js +0 -14
- package/dist/bench/code-out/pultra-1/debounce.js +0 -10
- package/dist/bench/code-out/pultra-1/deepClone.js +0 -17
- package/dist/bench/code-out/pultra-1/groupBy.js +0 -7
- package/dist/bench/code-out/pultra-2/debounce.js +0 -13
- package/dist/bench/code-out/pultra-2/deepClone.js +0 -18
- package/dist/bench/code-out/pultra-2/groupBy.js +0 -15
- package/dist/bench/code-out/pultra-3/debounce.js +0 -13
- package/dist/bench/code-out/pultra-3/deepClone.js +0 -18
- package/dist/bench/code-out/pultra-3/groupBy.js +0 -15
- package/dist/bench/code-out/pultra-4/debounce.js +0 -13
- package/dist/bench/code-out/pultra-4/deepClone.js +0 -18
- package/dist/bench/code-out/pultra-4/groupBy.js +0 -15
- package/dist/bench/code-out/run-0/debounce.js +0 -12
- package/dist/bench/code-out/run-0/deepClone.js +0 -17
- package/dist/bench/code-out/run-0/groupBy.js +0 -14
- package/dist/bench/code-out/run-1/debounce.js +0 -13
- package/dist/bench/code-out/run-1/deepClone.js +0 -15
- package/dist/bench/code-out/run-1/groupBy.js +0 -13
- package/dist/bench/code-out/run-2/debounce.js +0 -12
- package/dist/bench/code-out/run-2/deepClone.js +0 -18
- package/dist/bench/code-out/run-2/groupBy.js +0 -13
- package/dist/bench/code-out/run-3/debounce.js +0 -12
- package/dist/bench/code-out/run-3/deepClone.js +0 -18
- package/dist/bench/code-out/run-3/groupBy.js +0 -14
- package/dist/bench/code-out/run-4/debounce.js +0 -12
- package/dist/bench/code-out/run-4/deepClone.js +0 -16
- package/dist/bench/code-out/run-4/groupBy.js +0 -13
- package/dist/extensions/caveman/rule-lite.md +0 -77
- package/dist/extensions/caveman/rule-megacave-lite.md +0 -59
- package/dist/extensions/caveman/rule-megacave-ultra.md +0 -59
- package/dist/extensions/caveman/rule-megacave.md +0 -65
- package/dist/extensions/caveman/rule-ultra.md +0 -63
- package/dist/extensions/caveman/rule.md +0 -89
- package/dist/extensions/pstack/rule.md +0 -141
- package/dist/probe-catalog.js +0 -48
- package/dist/probe-catalog188.js +0 -44
package/dist/cli/common.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cli/common.ts — shared foundation: CLI flags, paths, exec, file/config helpers.
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
@@ -14,6 +14,10 @@ const OMP_AGENT_DIR = path.join(HOME, '.omp', 'agent');
|
|
|
14
14
|
const OMP_PLUGINS_DIR = path.join(HOME, '.omp', 'plugins');
|
|
15
15
|
const BUN_BIN_DIR = path.join(HOME, '.bun', 'bin');
|
|
16
16
|
const OMP_BIN = IS_WINDOWS ? 'omp.cmd' : 'omp';
|
|
17
|
+
// npm and bun are `.cmd` shims on Windows, and CreateProcess spawns neither a bare name nor a shim,
|
|
18
|
+
// so the spawn name itself carries the extension the way `omp.cmd` does.
|
|
19
|
+
const NPM_BIN = IS_WINDOWS ? 'npm.cmd' : 'npm';
|
|
20
|
+
const BUN_BIN = IS_WINDOWS ? 'bun.cmd' : 'bun';
|
|
17
21
|
const PACKAGE_NAME = '@krtclcdy/tersio';
|
|
18
22
|
const PACKAGE_BIN = 'tersio';
|
|
19
23
|
const { version: PACKAGE_VERSION } = createRequire(import.meta.url)(path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'package.json'));
|
|
@@ -112,8 +116,26 @@ function debug(...a) {
|
|
|
112
116
|
console.log(' [debug]', ...a);
|
|
113
117
|
}
|
|
114
118
|
const execFileP = promisify(execFile);
|
|
119
|
+
/** Windows has no real `omp`, `npm`, or `pgrep`: `.cmd` shims throw EINVAL when spawned, so a bare name wraps through cmd.exe only when a `<name>.cmd` really sits on PATH. */
|
|
120
|
+
export function shimCommand(cmd, args) {
|
|
121
|
+
if (!IS_WINDOWS)
|
|
122
|
+
return { cmd, args };
|
|
123
|
+
if (/\.(?:cmd|bat)$/i.test(cmd)) {
|
|
124
|
+
return { cmd: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', cmd, ...args] };
|
|
125
|
+
}
|
|
126
|
+
if (hasCmdShim(cmd)) {
|
|
127
|
+
return { cmd: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', cmd, ...args] };
|
|
128
|
+
}
|
|
129
|
+
return { cmd, args };
|
|
130
|
+
}
|
|
131
|
+
function hasCmdShim(cmd) {
|
|
132
|
+
if (cmd.includes('/') || cmd.includes('\\'))
|
|
133
|
+
return false;
|
|
134
|
+
return (process.env.PATH || '').split(path.delimiter).some((dir) => dir && existsSync(path.join(dir, `${cmd}.cmd`)));
|
|
135
|
+
}
|
|
115
136
|
async function execP(cmd, args, opts = {}) {
|
|
116
|
-
|
|
137
|
+
const shim = shimCommand(cmd, args);
|
|
138
|
+
return execFileP(shim.cmd, shim.args, {
|
|
117
139
|
timeout: opts.timeout || 120000,
|
|
118
140
|
encoding: 'utf8',
|
|
119
141
|
...opts,
|
|
@@ -316,4 +338,4 @@ function relTime(ageMs) {
|
|
|
316
338
|
const weeks = Math.floor(days / 7);
|
|
317
339
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
318
340
|
}
|
|
319
|
-
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, reinstall, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, pstackDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, bundledPonytailSpec, ponytailOverride, };
|
|
341
|
+
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, NPM_BIN, BUN_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, reinstall, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, pstackDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, bundledPonytailSpec, ponytailOverride, };
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -457,8 +457,13 @@ function readBody(req) {
|
|
|
457
457
|
});
|
|
458
458
|
}
|
|
459
459
|
function openBrowser(url) {
|
|
460
|
-
|
|
461
|
-
|
|
460
|
+
// `start` is a cmd builtin, not an executable: spawning it by name fails with ENOENT.
|
|
461
|
+
const [cmd, args] = process.platform === 'darwin'
|
|
462
|
+
? ['open', [url]]
|
|
463
|
+
: process.platform === 'win32'
|
|
464
|
+
? [process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'start', '', url]]
|
|
465
|
+
: ['xdg-open', [url]];
|
|
466
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
|
|
462
467
|
}
|
|
463
468
|
// Replacers throughout: session data holds `$'` (shell quoting) and `</script>`, which String.replace would mangle.
|
|
464
469
|
function escapeInline(json) {
|
package/dist/cli/doctor-fix.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, promises as fs } from 'node:fs';
|
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, bundledPonytailSpec, execP, ponytailOverride, readPluginsPackage, } from './common.js';
|
|
5
|
+
import { BUN_BIN_DIR, HOME, NPM_BIN, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, bundledPonytailSpec, execP, ponytailOverride, readPluginsPackage, shimCommand, } from './common.js';
|
|
6
6
|
import { execNetwork, sayTagged } from './interactive.js';
|
|
7
7
|
import { wireRtkOmp, wireRtkOpencode, wireRtkPi, ensureRtkInConfig } from './rtk-wiring.js';
|
|
8
8
|
import { RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, resolveHostBinary, rtkPlatformSpec, sha256File, } from '../extensions/shared/utils.js';
|
|
@@ -14,18 +14,20 @@ export function serviceOrphanPids() {
|
|
|
14
14
|
const bin = resolveHostBinary('opencode', path.join(HOME, '.opencode', 'bin'));
|
|
15
15
|
if (!bin)
|
|
16
16
|
return [];
|
|
17
|
+
const shim = shimCommand(bin, ['service', 'status']);
|
|
17
18
|
let status = '';
|
|
18
19
|
try {
|
|
19
|
-
status = spawnSync(
|
|
20
|
+
status = spawnSync(shim.cmd, shim.args, { encoding: 'utf8' }).stdout.trim().toLowerCase();
|
|
20
21
|
}
|
|
21
22
|
catch {
|
|
22
23
|
return [];
|
|
23
24
|
}
|
|
24
25
|
if (serviceRunning(status))
|
|
25
26
|
return [];
|
|
27
|
+
const grepShim = shimCommand('pgrep', ['-f', 'opencode serve --service']);
|
|
26
28
|
let out = '';
|
|
27
29
|
try {
|
|
28
|
-
out = spawnSync(
|
|
30
|
+
out = spawnSync(grepShim.cmd, grepShim.args, { encoding: 'utf8' }).stdout ?? '';
|
|
29
31
|
}
|
|
30
32
|
catch {
|
|
31
33
|
return [];
|
|
@@ -38,9 +40,10 @@ export function serviceState() {
|
|
|
38
40
|
const bin = resolveHostBinary('opencode', path.join(HOME, '.opencode', 'bin'));
|
|
39
41
|
if (!bin)
|
|
40
42
|
return null;
|
|
43
|
+
const shim = shimCommand(bin, ['service', 'status']);
|
|
41
44
|
let status = '';
|
|
42
45
|
try {
|
|
43
|
-
status = spawnSync(
|
|
46
|
+
status = spawnSync(shim.cmd, shim.args, { encoding: 'utf8' }).stdout.trim().toLowerCase();
|
|
44
47
|
}
|
|
45
48
|
catch {
|
|
46
49
|
return null;
|
|
@@ -49,7 +52,8 @@ export function serviceState() {
|
|
|
49
52
|
return null;
|
|
50
53
|
let live = false;
|
|
51
54
|
try {
|
|
52
|
-
const
|
|
55
|
+
const grepShim = shimCommand('pgrep', ['-f', 'opencode serve --service']);
|
|
56
|
+
const out = spawnSync(grepShim.cmd, grepShim.args, { encoding: 'utf8' }).stdout ?? '';
|
|
53
57
|
live = out.split(/[\s,]+/).some((line) => /^\d+$/.test(line.trim()));
|
|
54
58
|
}
|
|
55
59
|
catch {
|
|
@@ -365,7 +369,7 @@ async function fixPonytail(pluginsDir) {
|
|
|
365
369
|
return;
|
|
366
370
|
}
|
|
367
371
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
368
|
-
await execNetwork('Restoring bundled Ponytail',
|
|
372
|
+
await execNetwork('Restoring bundled Ponytail', NPM_BIN, ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 }).catch((e) => {
|
|
369
373
|
throw new Error(`bundled ponytail restore failed: ${e.message}`);
|
|
370
374
|
});
|
|
371
375
|
}
|
|
@@ -405,7 +409,8 @@ async function fixOpencodeService() {
|
|
|
405
409
|
sayTagged(' [warn] opencode not found on PATH; start it with `opencode service start`');
|
|
406
410
|
return;
|
|
407
411
|
}
|
|
408
|
-
|
|
412
|
+
const shim = shimCommand(bin, ['service', 'start']);
|
|
413
|
+
await execNetwork('Starting the OpenCode service', shim.cmd, shim.args, { timeout: 60000 }).catch((e) => {
|
|
409
414
|
throw new Error(`service start failed: ${shortError(e)}`);
|
|
410
415
|
});
|
|
411
416
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, statSync, promises as fs } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, fix, yes, BUN_BIN_DIR, OMP_BIN, execP, parseJsonObject, relTime, } from './common.js';
|
|
5
|
+
import { HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, fix, yes, BUN_BIN_DIR, OMP_BIN, execP, parseJsonObject, relTime, shimCommand, } from './common.js';
|
|
6
6
|
import { detectHosts, hostExtensionsDir, ompPackageDir, piPonytailPackageDir } from './hosts.js';
|
|
7
7
|
import { GENERATED_TREE_FILES } from './manifest.js';
|
|
8
8
|
import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
|
|
9
9
|
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
10
10
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
11
|
-
import { findHoistedPackage, fetchJson, piAgentDir, readTextIfExists, resolveHostBinary, resolveRtkBinary, ruleBodyProblem } from '../extensions/shared/utils.js';
|
|
11
|
+
import { findHoistedPackage, fetchJson, piAgentDir, readTextIfExists, resolveHostBinary, resolveRtkBinary, rtkPlatformSpec, ruleBodyProblem, tersioHome } from '../extensions/shared/utils.js';
|
|
12
12
|
import { sqliteScript, statsDbPath } from '../extensions/shared/agent-stats.js';
|
|
13
13
|
import { OMP_SUBAGENT_MARKER } from '../extensions/shared/session-state.js';
|
|
14
14
|
import { PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, TREE_FILES, TREE_PSTACK_FILES, isObsoleteTreeFile, treeFileText, treeFilesFor } from './manifest.js';
|
|
@@ -201,6 +201,18 @@ async function runDoctor(recheck = false) {
|
|
|
201
201
|
function unused(label, detail) {
|
|
202
202
|
console.log(` — ${label}: not installed ${detail}`);
|
|
203
203
|
}
|
|
204
|
+
// A Windows failure names its cause here: several rows below silently degrade when the
|
|
205
|
+
// platform has no rtk build, and a wrong home would move every one of them.
|
|
206
|
+
section('Platform');
|
|
207
|
+
const rtkSpec = rtkPlatformSpec();
|
|
208
|
+
check('Platform', rtkSpec !== null, `${process.platform}/${process.arch}${rtkSpec ? ` · node ${process.version} · rtk ${rtkSpec.triple}` : ''}`);
|
|
209
|
+
if (rtkSpec === null) {
|
|
210
|
+
// Windows on ARM today: rtk publishes no build, so the RTK rows read as not installed.
|
|
211
|
+
console.log(` rtk publishes no build for ${process.platform}/${process.arch}; every RTK row below reads as missing`);
|
|
212
|
+
}
|
|
213
|
+
// Windows resolves data through the profile, so name the resolved home and data dir.
|
|
214
|
+
check('Home', HOME !== '', HOME);
|
|
215
|
+
check('Data', true, path.join(tersioHome(), 'usage.db'));
|
|
204
216
|
section('Hosts');
|
|
205
217
|
for (const host of hosts) {
|
|
206
218
|
// pi can hold a working tree while reading as not installed; use the tree as ground truth.
|
|
@@ -420,7 +432,8 @@ function subagentMarkerStatus(ompBin) {
|
|
|
420
432
|
return null;
|
|
421
433
|
let haystack;
|
|
422
434
|
try {
|
|
423
|
-
|
|
435
|
+
const shim = shimCommand(ompBin, ['--version']);
|
|
436
|
+
haystack = execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
424
437
|
+ readFileSync(ompBin, 'utf8');
|
|
425
438
|
}
|
|
426
439
|
catch {
|
package/dist/cli/install.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, promises as fs } from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { BUN_BIN_DIR, COMBO_PRESET_MODES, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, bundledPonytailSpec, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, pstackDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, currency, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
6
|
+
import { BUN_BIN_DIR, BUN_BIN, COMBO_PRESET_MODES, HOME, IS_WINDOWS, NPM_BIN, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, bundledPonytailSpec, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, pstackDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, currency, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
7
7
|
import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, withInteractiveTask, sayTagged } from './interactive.js';
|
|
8
8
|
import { printWelcome } from './banner.js';
|
|
9
9
|
import { checkForUpdate, runLatestUpdate } from './update.js';
|
|
@@ -80,11 +80,11 @@ async function stepPonytail(pluginsDir, options) {
|
|
|
80
80
|
else {
|
|
81
81
|
// Reinstall only when the self-plugin step did not materialize it.
|
|
82
82
|
try {
|
|
83
|
-
await execNetwork('Installing bundled Ponytail',
|
|
83
|
+
await execNetwork('Installing bundled Ponytail', NPM_BIN, ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
|
|
84
84
|
}
|
|
85
85
|
catch {
|
|
86
86
|
try {
|
|
87
|
-
await execNetwork('Installing bundled Ponytail',
|
|
87
|
+
await execNetwork('Installing bundled Ponytail', BUN_BIN, ['install'], { cwd: pluginsDir, timeout: 180000 });
|
|
88
88
|
}
|
|
89
89
|
catch (e) {
|
|
90
90
|
sayTagged(` [fail] Could not install bundled ponytail: ${e.message}`);
|
|
@@ -136,11 +136,11 @@ async function stepSelfPlugin(pluginsDir, options) {
|
|
|
136
136
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
137
137
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
138
138
|
try {
|
|
139
|
-
await execNetwork('Installing Tersio plugin dependencies',
|
|
139
|
+
await execNetwork('Installing Tersio plugin dependencies', NPM_BIN, ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
|
|
140
140
|
}
|
|
141
141
|
catch {
|
|
142
142
|
try {
|
|
143
|
-
await execNetwork('Installing Tersio plugin dependencies',
|
|
143
|
+
await execNetwork('Installing Tersio plugin dependencies', BUN_BIN, ['install'], { cwd: pluginsDir, timeout: 180000 });
|
|
144
144
|
}
|
|
145
145
|
catch (e) {
|
|
146
146
|
sayTagged(` [fail] Could not install ${PACKAGE_NAME} into plugins dir: ${e.message}`);
|
package/dist/cli/profile.js
CHANGED
|
@@ -62,11 +62,12 @@ async function storedProfile() {
|
|
|
62
62
|
return storedProfileSync();
|
|
63
63
|
}
|
|
64
64
|
function legacyOmpLockPath() {
|
|
65
|
-
const
|
|
65
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
66
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
66
67
|
return [
|
|
67
|
-
path.join(
|
|
68
|
+
path.join(home, '.omp', 'plugins', 'omp-plugins.lock.json'),
|
|
68
69
|
path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
|
|
69
|
-
].find((p) => existsSync(p)) ?? path.join(
|
|
70
|
+
].find((p) => existsSync(p)) ?? path.join(home, '.omp', 'plugins', 'omp-plugins.lock.json');
|
|
70
71
|
}
|
|
71
72
|
function parseStored(raw) {
|
|
72
73
|
if (!raw)
|
|
@@ -83,6 +84,29 @@ function parseStored(raw) {
|
|
|
83
84
|
return null;
|
|
84
85
|
}
|
|
85
86
|
}
|
|
87
|
+
// Upstream Ponytail's pi extension reads its own config for the session default, so the saved default
|
|
88
|
+
// is mirrored there. Without it a headless pi session starts at upstream's default and never injects.
|
|
89
|
+
function ponytailConfigFile() {
|
|
90
|
+
const base = (process.env.XDG_CONFIG_HOME || '').trim() || path.join(os.homedir(), '.config');
|
|
91
|
+
return path.join(base, 'ponytail', 'config.json');
|
|
92
|
+
}
|
|
93
|
+
async function mirrorPonytailDefault(mode, options) {
|
|
94
|
+
const file = ponytailConfigFile();
|
|
95
|
+
if (options.dryRun) {
|
|
96
|
+
if (verbose && !options.quiet)
|
|
97
|
+
console.log(` [dry-run] would write the Ponytail default to ${file}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const existing = JSON.parse(readTextIfExistsSync(file) ?? '{}');
|
|
102
|
+
if (existing.defaultMode === mode)
|
|
103
|
+
return;
|
|
104
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
105
|
+
await fs.writeFile(file, `${JSON.stringify({ ...existing, defaultMode: mode }, null, 2)}\n`, 'utf8');
|
|
106
|
+
console.log(` [write] Ponytail default in ${file}`);
|
|
107
|
+
}
|
|
108
|
+
catch { /* upstream's config is best-effort; its own default stands */ }
|
|
109
|
+
}
|
|
86
110
|
// Persist profile where every host reads it. `omp plugin config get` no longer reports it; `tersio settings` does.
|
|
87
111
|
async function writePluginSettings(profile, options) {
|
|
88
112
|
const file = tersioSettingsFile();
|
|
@@ -106,6 +130,7 @@ async function writePluginSettings(profile, options) {
|
|
|
106
130
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
107
131
|
await fs.writeFile(file, `${JSON.stringify(values, null, 2)}\n`, 'utf8');
|
|
108
132
|
console.log(` [write] Session defaults in ${file}`);
|
|
133
|
+
await mirrorPonytailDefault(profile.ponytailDefault, options);
|
|
109
134
|
}
|
|
110
135
|
function formatProfile(profile) {
|
|
111
136
|
return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault} · pstack=${profile.pstackDefault}) · currency=${profile.currency}`;
|
package/dist/cli/rtk-wiring.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, promises as fs } from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
|
+
import { shimCommand } from '../extensions/shared/utils.js';
|
|
6
7
|
function debugWire(options, msg) {
|
|
7
8
|
if (!options.quiet)
|
|
8
9
|
console.log(` ${msg}`);
|
|
@@ -14,17 +15,19 @@ function shortWiringError(e) {
|
|
|
14
15
|
return why.slice(0, 200);
|
|
15
16
|
}
|
|
16
17
|
function execFileP(cmd, args, timeout) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
19
|
+
// A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
|
|
20
|
+
const shim = shimCommand(cmd, args);
|
|
21
|
+
execFile(shim.cmd, shim.args, { timeout, encoding: 'utf8' }, (err, stdout, stderr) => {
|
|
22
|
+
if (err) {
|
|
23
|
+
err.stderr = String(stderr || '');
|
|
24
|
+
reject(err);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
28
|
+
}
|
|
27
29
|
});
|
|
30
|
+
return promise;
|
|
28
31
|
}
|
|
29
32
|
async function runWire(rtkBin, spec, options) {
|
|
30
33
|
if (!options.dryRun)
|
package/dist/cli/usage.js
CHANGED
|
@@ -215,6 +215,11 @@ function printReport(report) {
|
|
|
215
215
|
['Tokens', `${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`],
|
|
216
216
|
['Cost', `${formatCurrency(report.usd, report.currency)}${report.priced ? '' : ' (includes default pricing)'} · ~${formatCurrency(report.savedUsd, report.currency)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)${costNote}`],
|
|
217
217
|
['RTK adoption', `${fmt(a.rtkCalls)}/${fmt(a.eligibleCalls)} eligible Bash calls use RTK (${a.adoptionPct.toFixed(1)}%) · ${fmt(a.missedCalls)} missed · recall ${report.rtkRecall.available ? `${report.rtkRecall.mode} (${report.rtkRecall.entries})` : 'unavailable'}`],
|
|
218
|
+
// A blended number hides a quiet host, so each host reporting bash calls gets its own row.
|
|
219
|
+
...Object.entries(a.byHost)
|
|
220
|
+
.filter(([, c]) => c.bashCalls > 0)
|
|
221
|
+
.sort((x, y) => y[1].eligibleCalls - x[1].eligibleCalls)
|
|
222
|
+
.map(([name, c]) => [`RTK — ${name}`, `${fmt(c.rtkCalls)}/${fmt(c.eligibleCalls)} eligible calls (${c.adoptionPct.toFixed(1)}%) · ${fmt(c.missedCalls)} missed · ${fmt(c.bashCalls)} bash calls`]),
|
|
218
223
|
], [false, false], 118))
|
|
219
224
|
console.log(l);
|
|
220
225
|
if (measuredCount > 0) {
|
|
@@ -5,12 +5,9 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
|
|
6
6
|
import { announceStatus, formatCavemanStatus } from '../shared/status.js';
|
|
7
7
|
import { readCavemanDefault } from '../shared/plugin-settings.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
// `lite`, `megacave-lite`, and `megacave-ultra` are our forks of upstream's rows at 84cc3c1, so they are
|
|
12
|
-
// bundled rather than fetched. rule-ultra.md and rule-megacave.md are never edited here: the in-session
|
|
13
|
-
// updater rewrites them from upstream and restores the backup when the write does not match.
|
|
8
|
+
import { ruleDir } from '../shared/utils.js';
|
|
9
|
+
const CAVERN_DIR = ruleDir('caveman', dirname(fileURLToPath(import.meta.url)));
|
|
10
|
+
// `rule.md`, `rule-ultra.md`, and `rule-megacave.md` track upstream and are refetched; `lite`, `megacave-lite`, and `megacave-ultra` are our forks and ship bundled.
|
|
14
11
|
const RULE_FILES = { lite: 'rule-lite.md', full: 'rule.md', ultra: 'rule-ultra.md', 'megacave-lite': 'rule-megacave-lite.md', 'megacave-full': 'rule-megacave.md', 'megacave-ultra': 'rule-megacave-ultra.md' };
|
|
15
12
|
const DEFAULT_MODE = 'off';
|
|
16
13
|
// A rule file is immutable for the process, so read each level once; reinstalling needs a restart.
|
|
@@ -191,12 +191,7 @@ export default function comboToggleExtension(pi) {
|
|
|
191
191
|
if (!hasModeState(entries) && (ctx?.hasUI || OPENCODE))
|
|
192
192
|
restoreConfiguredDefaults();
|
|
193
193
|
else if (!hasModeState(entries)) {
|
|
194
|
-
// A headless root session
|
|
195
|
-
// the bridge, so it never loaded: a benchmark on omp measured Block 0 for every Ponytail level and
|
|
196
|
-
// for the Ponytail part of every Combo. Publish the saved modes only while the bridge is clear:
|
|
197
|
-
// once a parent or a toggle has published, the bridge carries that session's own choice and a
|
|
198
|
-
// subagent branch must keep inheriting it. All four go together, because Caveman, RTK, and pstack
|
|
199
|
-
// already restored their own levels and a disagreeing bridge resets them.
|
|
194
|
+
// A headless root session has no UI to publish the bridge, so Ponytail never loaded; publish the saved modes only while the bridge is clear, or a subagent branch stops inheriting.
|
|
200
195
|
const shared = getSharedComboState();
|
|
201
196
|
if (shared.caveman === 'off' && shared.rtk === 'off' && shared.ponytail === 'off' && shared.pstack === 'off') {
|
|
202
197
|
setSharedComboModes(savedModes());
|
|
@@ -32,8 +32,13 @@ async function openDashboard(pi, ctx) {
|
|
|
32
32
|
const exported = await pi.exec?.('tersio', ['dashboard', '--export', file], { cwd: ctx?.cwd });
|
|
33
33
|
if (!exported || exported.code !== 0)
|
|
34
34
|
throw new Error((exported?.stderr || 'export failed').trim());
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// `start` is a cmd builtin, not an executable: spawning it by name fails with ENOENT.
|
|
36
|
+
const [openCmd, openArgs] = process.platform === 'darwin'
|
|
37
|
+
? ['open', [file]]
|
|
38
|
+
: process.platform === 'win32'
|
|
39
|
+
? [process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'start', '', file]]
|
|
40
|
+
: ['xdg-open', [file]];
|
|
41
|
+
await pi.exec?.(openCmd, openArgs, { cwd: ctx?.cwd });
|
|
37
42
|
ctx?.ui?.notify?.(`tersio dashboard: opened in your browser.`, 'info');
|
|
38
43
|
}
|
|
39
44
|
catch (e) {
|
|
@@ -6,7 +6,8 @@ import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue
|
|
|
6
6
|
import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
|
|
7
7
|
import { announceStatus } from '../shared/status.js';
|
|
8
8
|
import { readPstackDefault } from '../shared/plugin-settings.js';
|
|
9
|
-
|
|
9
|
+
import { ruleDir } from '../shared/utils.js';
|
|
10
|
+
const PSTACK_DIR = ruleDir('pstack', dirname(fileURLToPath(import.meta.url)));
|
|
10
11
|
const RULE_FILE = 'rule.md';
|
|
11
12
|
const DEFAULT_MODE = 'off';
|
|
12
13
|
// The body is immutable for the process, so read it once; reinstalling needs a restart.
|
|
@@ -5,6 +5,8 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { resolveRtkBinary } from '../shared/utils.js';
|
|
6
6
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
7
7
|
import { getSharedComboState, lastCustomValue, sessionEntries } from '../shared/session-state.js';
|
|
8
|
+
import { appendUsage } from '../shared/usage-ledger.js';
|
|
9
|
+
import { isPiHost } from '../shared/host.js';
|
|
8
10
|
/** Tool name to `rtk pipe --filter` name. Exported so the OpenCode host reuses one map. */
|
|
9
11
|
export const FILTER_BY_TOOL = new Map([
|
|
10
12
|
['grep', 'grep'],
|
|
@@ -14,8 +16,13 @@ export const FILTER_BY_TOOL = new Map([
|
|
|
14
16
|
/** Below this the rtk spawn costs more than the filter saves. */
|
|
15
17
|
export const MIN_FILTER_BYTES = 1200;
|
|
16
18
|
const FILTER_TIMEOUT_MS = 5000;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
// A shell that understands `<`, resolved per platform; separate flags, because Node quotes each argv element and cmd.exe rejects `"/d /s /c"`.
|
|
20
|
+
/** The pipe's shell, and whether the whole command needs an extra quote pair: `cmd.exe /s` strips the outer one, so a quoted binary and redirect path must be wrapped. */
|
|
21
|
+
export function filterShell() {
|
|
22
|
+
return process.platform === 'win32'
|
|
23
|
+
? { shell: process.env.ComSpec || 'cmd.exe', flags: ['/d', '/s', '/c'], wrap: true }
|
|
24
|
+
: { shell: '/bin/sh', flags: ['-c'], wrap: false };
|
|
25
|
+
}
|
|
19
26
|
function modeOf(value) {
|
|
20
27
|
if (value === true)
|
|
21
28
|
return true;
|
|
@@ -46,8 +53,10 @@ export async function filterThroughRtk(exec, request) {
|
|
|
46
53
|
dir = mkdtempSync(join(tmpdir(), 'tersio-rtk-'));
|
|
47
54
|
const file = join(dir, 'in.txt');
|
|
48
55
|
writeFileSync(file, text, 'utf8');
|
|
49
|
-
// `rtk pipe` reads stdin only and execFile has no stdin, so one redirect.
|
|
50
|
-
const
|
|
56
|
+
// `rtk pipe` reads stdin only and execFile has no stdin, so one redirect through the platform shell.
|
|
57
|
+
const { shell, flags, wrap } = filterShell();
|
|
58
|
+
const line = `"${bin}" pipe -f ${filter} < "${file}"`;
|
|
59
|
+
const result = await exec(shell, [...flags, wrap ? `"${line}"` : line], { signal, timeout: FILTER_TIMEOUT_MS });
|
|
51
60
|
if (result.code !== 0)
|
|
52
61
|
return null;
|
|
53
62
|
const out = result.stdout;
|
|
@@ -63,11 +72,43 @@ export async function filterThroughRtk(exec, request) {
|
|
|
63
72
|
rmSync(dir, { recursive: true, force: true });
|
|
64
73
|
}
|
|
65
74
|
}
|
|
75
|
+
/** Ask rtk for the rewritten command; null on any failure, so the original runs. */
|
|
76
|
+
async function rewriteCommand(exec, bin, command, signal) {
|
|
77
|
+
try {
|
|
78
|
+
const result = await exec(bin, ['rewrite', command], { timeout: FILTER_TIMEOUT_MS, signal });
|
|
79
|
+
const out = result.stdout.trim();
|
|
80
|
+
return out || null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
66
86
|
export default function rtkFilterExtension(pi) {
|
|
67
87
|
const bin = resolveRtkBinary();
|
|
68
88
|
const exec = pi.exec;
|
|
69
89
|
if (!bin || !exec)
|
|
70
90
|
return;
|
|
91
|
+
// omp records the post-hook command in its transcript, so only pi leaves an audit row here:
|
|
92
|
+
// opencode rewrites in its own plugin hook, and an omp row would double-count the transcript.
|
|
93
|
+
const auditHost = isPiHost(pi) ? 'pi' : null;
|
|
94
|
+
// Registered synchronously, unlike the rtk-init hook, whose async version probe can miss the first call of a fast session.
|
|
95
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
96
|
+
const call = (event ?? {});
|
|
97
|
+
const input = call.input;
|
|
98
|
+
if (call.toolName !== 'bash' || !input)
|
|
99
|
+
return;
|
|
100
|
+
const command = input.command;
|
|
101
|
+
if (typeof command !== 'string' || !command.trim() || command.startsWith('rtk '))
|
|
102
|
+
return;
|
|
103
|
+
if (!rtkActive(sessionEntries(ctx)))
|
|
104
|
+
return;
|
|
105
|
+
const rewritten = await rewriteCommand(exec, bin, command, ctx?.signal);
|
|
106
|
+
if (!rewritten || rewritten === command)
|
|
107
|
+
return;
|
|
108
|
+
input.command = rewritten;
|
|
109
|
+
if (auditHost)
|
|
110
|
+
appendUsage('rtk-audit', `${auditHost} ${command}`);
|
|
111
|
+
});
|
|
71
112
|
pi.on('tool_result', async (event, ctx) => {
|
|
72
113
|
const filter = FILTER_BY_TOOL.get(String(event?.toolName ?? '').toLowerCase());
|
|
73
114
|
if (!filter || event?.isError)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.js';
|
|
2
2
|
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.js';
|
|
3
3
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
4
|
-
import { resolveRtkBinary } from '../shared/utils.js';
|
|
4
|
+
import { resolveRtkBinary, shimCommand } from '../shared/utils.js';
|
|
5
5
|
import { announceStatus } from '../shared/status.js';
|
|
6
6
|
const DEFAULT_ENABLED = false;
|
|
7
7
|
function asBoolean(value) {
|
|
@@ -87,7 +87,9 @@ export default function rtkSessionExtension(pi) {
|
|
|
87
87
|
onUpdate?.({ content: [{ type: 'text', text: `${bin === null ? '' : 'rtk '}${argv.join(' ')}` }], details: { phase: 'start' } });
|
|
88
88
|
try {
|
|
89
89
|
// A GUI-launched host may lack ~/.bun/bin on PATH, so prefer the resolved path.
|
|
90
|
-
|
|
90
|
+
// A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
|
|
91
|
+
const shim = shimCommand(bin ?? argv[0], argv);
|
|
92
|
+
const result = await pi.exec(shim.cmd, shim.args, { signal, cwd });
|
|
91
93
|
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
92
94
|
return {
|
|
93
95
|
isError: result.code !== 0,
|
|
@@ -11,7 +11,8 @@ import comboToggleExtension from './combo/index.js';
|
|
|
11
11
|
import tersioCommandsExtension from './commands/index.js';
|
|
12
12
|
import { getSharedComboState } from './shared/session-state.js';
|
|
13
13
|
import { formatStatus } from './shared/status.js';
|
|
14
|
-
import { resolveRtkBinary } from './shared/utils.js';
|
|
14
|
+
import { resolveRtkBinary, shimCommand } from './shared/utils.js';
|
|
15
|
+
import { appendUsage } from './shared/usage-ledger.js';
|
|
15
16
|
// The npm name, so the plugin list reads the same as the OMP and pi installs.
|
|
16
17
|
const plugin = {
|
|
17
18
|
id: '@krtclcdy/tersio',
|
|
@@ -217,8 +218,12 @@ const plugin = {
|
|
|
217
218
|
if (!rtkActive(sid === undefined ? undefined : sessionEntriesOf(sid)))
|
|
218
219
|
return;
|
|
219
220
|
const rewritten = await rewriteCommand(input.command);
|
|
220
|
-
if (rewritten)
|
|
221
|
-
|
|
221
|
+
if (!rewritten || rewritten === input.command)
|
|
222
|
+
return;
|
|
223
|
+
const original = input.command;
|
|
224
|
+
input.command = rewritten;
|
|
225
|
+
// The transcript stores the pre-hook command, so the rewrite leaves its own row.
|
|
226
|
+
appendUsage('rtk-audit', `opencode ${original}`);
|
|
222
227
|
});
|
|
223
228
|
// Built-in grep and glob output never reaches the bash hook; mutate result.content in place.
|
|
224
229
|
try {
|
|
@@ -253,16 +258,20 @@ const plugin = {
|
|
|
253
258
|
};
|
|
254
259
|
/** execFile with the promise shape `filterThroughRtk` expects. */
|
|
255
260
|
function execRtk(cmd, args, opts) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
261
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
262
|
+
// cmd.exe parses its own quoting, so the command string must reach it verbatim: Node's default escaping turns the inner quotes into `\"` and the redirect then fails.
|
|
263
|
+
const shim = shimCommand(cmd, args);
|
|
264
|
+
execFile(shim.cmd, shim.args, { signal: opts?.signal, timeout: opts?.timeout, windowsVerbatimArguments: process.platform === 'win32' }, (error, stdout) => {
|
|
265
|
+
const out = stdout ?? '';
|
|
266
|
+
if (!error) {
|
|
267
|
+
resolve({ stdout: out, code: 0 });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// A non-numeric code is a spawn failure (ENOENT, EACCES, killed) and must not read as success.
|
|
271
|
+
const raw = error.code;
|
|
272
|
+
resolve({ stdout: out, code: typeof raw === 'number' ? raw : 1 });
|
|
265
273
|
});
|
|
274
|
+
return promise;
|
|
266
275
|
}
|
|
267
276
|
/** `rtk rewrite` exit codes: 0 rewrite found, 1 no equivalent, 3 advisory; the code is the contract. */
|
|
268
277
|
const RTK_REWRITE_FOUND = 0;
|
|
@@ -326,23 +335,29 @@ function rewriteCommand(command) {
|
|
|
326
335
|
const bin = resolveRtkBinary();
|
|
327
336
|
if (!bin)
|
|
328
337
|
return Promise.resolve(null);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
338
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
339
|
+
// A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
|
|
340
|
+
const shim = shimCommand(bin, ['rewrite', command]);
|
|
341
|
+
execFile(shim.cmd, shim.args, { timeout: 5000 }, (error, stdout) => {
|
|
342
|
+
// A kill or timeout means untrustworthy output; pass the command through.
|
|
343
|
+
const err = error;
|
|
344
|
+
if (err && (err.killed || err.signal)) {
|
|
345
|
+
resolve(null);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const code = typeof err?.code === 'number' ? err.code : RTK_REWRITE_FOUND;
|
|
349
|
+
// Exit 1 is the only "no equivalent" answer, so anything else with output rewrites.
|
|
350
|
+
if (code === 1) {
|
|
351
|
+
resolve(null);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const out = String(stdout).trim();
|
|
355
|
+
if (!out || out === command || (code !== RTK_REWRITE_FOUND && code !== RTK_REWRITE_ADVISORY)) {
|
|
356
|
+
resolve(null);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
resolve(out);
|
|
346
360
|
});
|
|
361
|
+
return promise;
|
|
347
362
|
}
|
|
348
363
|
export default plugin;
|