@krtclcdy/tersio 2.25.6 → 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 +43 -13
- package/dashboard/dist/index.html +1 -1
- package/dist/cli/banner.js +1 -2
- package/dist/cli/common.js +26 -4
- package/dist/cli/dashboard.js +7 -2
- package/dist/cli/doctor-fix.js +12 -7
- package/dist/cli/doctor.js +64 -15
- package/dist/cli/hosts.js +1 -2
- package/dist/cli/install.js +12 -7
- package/dist/cli/manifest.js +25 -5
- package/dist/cli/profile.js +28 -3
- package/dist/cli/rtk-wiring.js +13 -10
- package/dist/cli/uninstall.js +1 -1
- package/dist/cli/usage.js +5 -0
- package/dist/extensions/caveman/index.js +4 -3
- package/dist/extensions/combo/index.js +21 -7
- package/dist/extensions/commands/index.js +7 -2
- package/dist/extensions/pstack/index.js +5 -2
- package/dist/extensions/rtk/filter.js +45 -4
- package/dist/extensions/rtk/session.js +21 -11
- 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 +80 -0
- package/dist/extensions/shared/rtk-gain.js +3 -7
- package/dist/extensions/shared/sqlite.js +2 -13
- package/dist/extensions/shared/status.js +1 -2
- package/dist/extensions/shared/usage-ledger.js +196 -29
- package/dist/extensions/shared/utils.js +33 -0
- package/dist/extensions/updater/index.js +15 -7
- package/extensions/caveman/index.ts +4 -3
- package/extensions/caveman/rule-lite.md +77 -0
- package/extensions/caveman/rule-megacave-lite.md +59 -0
- package/extensions/caveman/rule-megacave-ultra.md +59 -0
- package/extensions/caveman/rule.md +1 -1
- package/extensions/combo/index.ts +22 -7
- package/extensions/commands/index.ts +7 -2
- package/extensions/pstack/index.ts +5 -2
- package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
- package/extensions/pstack/playbooks/autonomous-run.md +13 -0
- package/extensions/pstack/playbooks/autopilot-full.md +13 -0
- package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
- package/extensions/pstack/playbooks/babysit.md +23 -0
- package/extensions/pstack/playbooks/bug-fix.md +15 -0
- package/extensions/pstack/playbooks/eval.md +25 -0
- package/extensions/pstack/playbooks/feature.md +21 -0
- package/extensions/pstack/playbooks/hillclimb.md +21 -0
- package/extensions/pstack/playbooks/investigation.md +14 -0
- package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
- package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
- package/extensions/pstack/playbooks/orchestrate.md +111 -0
- package/extensions/pstack/playbooks/pause-safely.md +10 -0
- package/extensions/pstack/playbooks/perf-issue.md +25 -0
- package/extensions/pstack/playbooks/prototype.md +14 -0
- package/extensions/pstack/playbooks/refactoring.md +16 -0
- package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
- package/extensions/pstack/playbooks/session-pickup.md +11 -0
- package/extensions/pstack/playbooks/shipping.md +17 -0
- package/extensions/pstack/playbooks/trace-forensics.md +14 -0
- package/extensions/pstack/playbooks/visual-parity.md +11 -0
- package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
- package/extensions/pstack/principles/attack-the-premise.md +22 -0
- package/extensions/pstack/principles/boundary-discipline.md +33 -0
- package/extensions/pstack/principles/build-the-lever.md +22 -0
- package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
- package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
- package/extensions/pstack/principles/experience-first.md +18 -0
- package/extensions/pstack/principles/explain-the-number.md +16 -0
- package/extensions/pstack/principles/fix-root-causes.md +23 -0
- package/extensions/pstack/principles/foundational-thinking.md +20 -0
- package/extensions/pstack/principles/guard-the-context-window.md +11 -0
- package/extensions/pstack/principles/laziness-protocol.md +9 -0
- package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
- package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
- package/extensions/pstack/principles/minimize-reader-load.md +17 -0
- package/extensions/pstack/principles/model-the-domain.md +23 -0
- package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
- package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
- package/extensions/pstack/principles/prove-it-works.md +21 -0
- package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
- package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
- package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
- package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
- package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
- package/extensions/pstack/principles/type-system-discipline.md +17 -0
- package/extensions/pstack/references/bugbot-triage.md +142 -0
- package/extensions/pstack/rule.md +88 -12
- package/extensions/pstack/skills/architect/SKILL.md +78 -0
- package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
- package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
- package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
- package/extensions/pstack/skills/arena/SKILL.md +47 -0
- package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
- package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
- package/extensions/pstack/skills/bro/SKILL.md +6 -0
- package/extensions/pstack/skills/correct/SKILL.md +31 -0
- package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
- package/extensions/pstack/skills/how/SKILL.md +35 -0
- package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
- package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
- package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
- package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
- package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
- package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
- package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
- package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
- package/extensions/pstack/skills/reflect/SKILL.md +66 -0
- package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
- package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
- package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
- package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
- package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
- package/extensions/pstack/skills/swarm/SKILL.md +45 -0
- package/extensions/pstack/skills/tdd/SKILL.md +41 -0
- package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
- package/extensions/pstack/skills/unslop/SKILL.md +61 -0
- package/extensions/pstack/skills/why/SKILL.md +97 -0
- package/extensions/pstack/skills/why/references/epistemics.md +71 -0
- package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
- package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
- package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
- package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
- package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
- package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
- package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
- package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
- package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
- package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
- package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
- package/extensions/rtk/filter.ts +51 -4
- package/extensions/rtk/session.ts +20 -10
- 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 +79 -0
- package/extensions/shared/rtk-gain.ts +3 -7
- package/extensions/shared/sqlite.ts +2 -13
- package/extensions/shared/status.ts +1 -2
- package/extensions/shared/usage-ledger.ts +198 -32
- package/extensions/shared/utils.ts +29 -0
- package/extensions/timing/index.ts +1 -2
- package/extensions/updater/index.ts +18 -7
- package/package.json +8 -7
- 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 -65
- package/dist/probe-catalog.js +0 -48
- package/dist/probe-catalog188.js +0 -44
|
@@ -33,8 +33,11 @@ function piHasUpstreamPonytail() {
|
|
|
33
33
|
return false;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
// Each level's own rule, quoted from the installed Ponytail so a host that cannot resolve the package still gets the right register: `lite` builds what was asked and offers the smaller option, and only `ultra` questions the request.
|
|
36
37
|
const PONYTAIL_FALLBACK_INTENSITY = {
|
|
37
|
-
lite: '
|
|
38
|
+
lite: 'Build what was asked. Name the smaller option in one line and let the user pick.',
|
|
39
|
+
full: 'The rules above. Default.',
|
|
40
|
+
ultra: 'Also question the request: before building, push back on any part the need does not justify.',
|
|
38
41
|
};
|
|
39
42
|
// Fallback text when Ponytail cannot be found; exported for its own test.
|
|
40
43
|
export function ponytailFallback(mode) {
|
|
@@ -58,6 +61,14 @@ async function loadPonytailInstructions(mode) {
|
|
|
58
61
|
}
|
|
59
62
|
return ponytailFallback(mode);
|
|
60
63
|
}
|
|
64
|
+
// What a fresh session starts from: a stored per-mode default wins, and with none the Combo preset supplies all four. pstackDefault counts as its own default, so a session that stores only it still honors it instead of losing it to the preset's `off`.
|
|
65
|
+
function savedModes() {
|
|
66
|
+
const hasModeDefaults = ['cavemanDefault', 'rtkDefault', 'ponytailDefault', 'pstackDefault'].some((key) => key in readPluginSettings());
|
|
67
|
+
const preset = COMBO_LEVELS[readComboDefault()];
|
|
68
|
+
return hasModeDefaults
|
|
69
|
+
? { caveman: readCavemanDefault(), rtk: readRtkDefault() ? 'on' : 'off', ponytail: readPonytailDefault(), pstack: readPstackDefault() }
|
|
70
|
+
: { caveman: preset.caveman, rtk: preset.rtk, ponytail: preset.ponytail, pstack: preset.pstack };
|
|
71
|
+
}
|
|
61
72
|
export default function comboToggleExtension(pi) {
|
|
62
73
|
setExtensionLabel(pi, 'Combo session toggle (all four add-ons)');
|
|
63
74
|
let setupPrompted = false;
|
|
@@ -74,14 +85,9 @@ export default function comboToggleExtension(pi) {
|
|
|
74
85
|
pi.appendEntry?.('combo-level', { level });
|
|
75
86
|
}
|
|
76
87
|
function restoreConfiguredDefaults() {
|
|
77
|
-
const settings = readPluginSettings();
|
|
78
|
-
const hasModeDefaults = ['cavemanDefault', 'rtkDefault', 'ponytailDefault'].some((key) => key in settings);
|
|
79
88
|
const combo = readComboDefault();
|
|
80
89
|
const preset = COMBO_LEVELS[combo];
|
|
81
|
-
|
|
82
|
-
const modes = hasModeDefaults
|
|
83
|
-
? { caveman: readCavemanDefault(), rtk: readRtkDefault() ? 'on' : 'off', ponytail: readPonytailDefault(), pstack: readPstackDefault() }
|
|
84
|
-
: { caveman: preset.caveman, rtk: preset.rtk, ponytail: preset.ponytail, pstack: preset.pstack };
|
|
90
|
+
const modes = savedModes();
|
|
85
91
|
setSharedComboModes(modes);
|
|
86
92
|
if (modes.caveman === 'off' && modes.rtk === 'off' && modes.ponytail === 'off' && modes.pstack === 'off')
|
|
87
93
|
return;
|
|
@@ -184,6 +190,13 @@ export default function comboToggleExtension(pi) {
|
|
|
184
190
|
const entries = sessionEntries(ctx);
|
|
185
191
|
if (!hasModeState(entries) && (ctx?.hasUI || OPENCODE))
|
|
186
192
|
restoreConfiguredDefaults();
|
|
193
|
+
else if (!hasModeState(entries)) {
|
|
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.
|
|
195
|
+
const shared = getSharedComboState();
|
|
196
|
+
if (shared.caveman === 'off' && shared.rtk === 'off' && shared.ponytail === 'off' && shared.pstack === 'off') {
|
|
197
|
+
setSharedComboModes(savedModes());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
187
200
|
// Announced last, so any default above is reflected.
|
|
188
201
|
announceStatus(ctx);
|
|
189
202
|
});
|
|
@@ -199,6 +212,7 @@ export default function comboToggleExtension(pi) {
|
|
|
199
212
|
pi.on('before_agent_start', async (event, ctx) => {
|
|
200
213
|
if (ctx?.hasUI)
|
|
201
214
|
reconcile(ctx);
|
|
215
|
+
// Shared state, with no marker check: the marker is a sentence copied out of the host binary, so matching on it would drop Ponytail after a host reworded the line.
|
|
202
216
|
const mode = getSharedComboState().ponytail;
|
|
203
217
|
// On pi, an installed upstream pi-extension is the sole injector, so Tersio only falls back with none.
|
|
204
218
|
if (isPiProcess() && piHasUpstreamPonytail())
|
|
@@ -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.
|
|
@@ -22,7 +23,9 @@ function buildInstruction() {
|
|
|
22
23
|
catch {
|
|
23
24
|
return `pstack mode requested, but ${RULE_FILE} is missing. Run: tersio doctor --fix extensions`;
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
// The rule body names the files by relative path, so one line gives the directory they sit in.
|
|
27
|
+
const home = `Leaves, playbooks, and skills live at: ${PSTACK_DIR} — \`principles/<title>.md\`, \`playbooks/<title>.md\`, \`skills/<name>/SKILL.md\`, \`references/<title>.md\`.`;
|
|
28
|
+
cachedInstruction = `pstack mode active for this session.\n${body}\n${home}`;
|
|
26
29
|
return cachedInstruction;
|
|
27
30
|
}
|
|
28
31
|
function resolveMode(entries, fallback = DEFAULT_MODE) {
|
|
@@ -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) {
|
|
@@ -22,8 +22,11 @@ function setRtkProcessEnabled(enabled) {
|
|
|
22
22
|
else
|
|
23
23
|
process.env.RTK_DISABLED = '1';
|
|
24
24
|
}
|
|
25
|
-
// rtk's contract: name the rewritten surface
|
|
26
|
-
|
|
25
|
+
// rtk's contract: name the rewritten surface first and the binary second — a hand-typed `rtk` in front of a
|
|
26
|
+
// rewritten command fights the rewrite, and the direct calls cover only what the rewrite cannot express.
|
|
27
|
+
const RTK_PROMPT = `RTK guidance active. What you run is condensed automatically, in the host's own rewritten form: searches (grep, rg, ast-grep), listings (ls, find, tree), reads (cat), package and test runs (bun, npm, pnpm, npx, cargo, go, pytest, vitest, jest, ctest, tsc), version control and forges (git, gh, glab), containers and clusters (docker, kubectl), cloud and platform CLIs (aws, oc, dotnet), PHP tooling (phpunit, pest, phpstan, pint, ecs), lint and format (eslint, ruff, mypy, prettier, sqlfluff, playwright, prisma, next), and data tools (jq, curl, wget, psql, wc, make, tail, docker logs). The runner's own subcommand, such as \`rtk bun run test\`, keeps the failing test name, the assertion, and the file and line. Every signal kept, costly noise dropped. Treat a condensed result as complete. Re-run something unfiltered only when its result is empty, garbled, or contradicts its exit code.
|
|
28
|
+
|
|
29
|
+
Call rtk directly for what the rewrite cannot express: \`rtk read <file> --level aggressive\` for an overview, \`--max-lines N\` for a preview, \`rtk smart <file>\` for a two-line summary, \`rtk summary <cmd>\` for a heuristic one, \`rtk err <cmd>\` for errors only, \`rtk log <file>\` to dedupe logs, \`rtk json <file>\` with \`--keys-only\` for shape, plus \`rtk deps\`, \`rtk diff\`, \`rtk env\`, \`rtk ast-grep\`, and \`rtk test <cmd>\` for a runner without its own form. A command with no condensed form passes through unchanged, so wrap it: \`rtk err node build.js\`, and \`rtk find -name '*.ts'\` in place of fd. Never type \`rtk\` in front of a command listed above.`;
|
|
27
30
|
export default function rtkSessionExtension(pi) {
|
|
28
31
|
let enabled = DEFAULT_ENABLED;
|
|
29
32
|
function fail(text) {
|
|
@@ -65,30 +68,37 @@ export default function rtkSessionExtension(pi) {
|
|
|
65
68
|
pi.registerTool?.({
|
|
66
69
|
name: 'rtk_run',
|
|
67
70
|
label: 'RTK Run',
|
|
68
|
-
description: 'Run the installed `rtk` binary
|
|
71
|
+
description: 'Run a shell command through the installed `rtk` binary when RTK mode is on, and run it plainly when RTK mode is off.',
|
|
69
72
|
parameters: stringArrayToolParams(pi, 'args', {
|
|
70
73
|
minItems: 1,
|
|
71
74
|
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
72
75
|
}),
|
|
73
76
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
74
77
|
// A subagent's own flag sits at its start-up default, so honour shared state too.
|
|
75
|
-
|
|
76
|
-
return fail('RTK mode is off. Run /rtk on for this session, or use bash explicitly.');
|
|
78
|
+
const rtkOn = enabled || getSharedComboState().rtk === 'on';
|
|
77
79
|
if (typeof pi.exec !== 'function')
|
|
78
80
|
return fail('This host cannot run rtk.');
|
|
79
|
-
|
|
81
|
+
// RTK off: run the command as given rather than answering "mode is off". The tool is the only one left under --no-tools, and a refusal there made the model spend turns re-calling it. rtk is the condenser, never the command.
|
|
82
|
+
const cwd = ctx?.cwd || pi.cwd;
|
|
83
|
+
const bin = rtkOn ? resolveRtkBinary() : null;
|
|
84
|
+
if (rtkOn && bin === null)
|
|
85
|
+
return fail('RTK mode is on but the rtk binary is not installed. Run: tersio update');
|
|
86
|
+
const argv = params.args;
|
|
87
|
+
onUpdate?.({ content: [{ type: 'text', text: `${bin === null ? '' : 'rtk '}${argv.join(' ')}` }], details: { phase: 'start' } });
|
|
80
88
|
try {
|
|
81
89
|
// A GUI-launched host may lack ~/.bun/bin on PATH, so prefer the resolved path.
|
|
82
|
-
|
|
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 });
|
|
83
93
|
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
84
94
|
return {
|
|
85
95
|
isError: result.code !== 0,
|
|
86
|
-
content: [{ type: 'text', text: text || `
|
|
87
|
-
details: { code: result.code, killed: result.killed, enabled },
|
|
96
|
+
content: [{ type: 'text', text: text || `exited ${result.code}` }],
|
|
97
|
+
details: { code: result.code, killed: result.killed, enabled, condensed: bin !== null },
|
|
88
98
|
};
|
|
89
99
|
}
|
|
90
100
|
catch (e) {
|
|
91
|
-
return fail(
|
|
101
|
+
return fail(`${bin === null ? argv[0] : 'rtk'} could not run: ${e instanceof Error ? e.message : String(e)}`);
|
|
92
102
|
}
|
|
93
103
|
},
|
|
94
104
|
});
|
|
@@ -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;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Read-only aggregate over the usage store; missing SQLite, table, or metric reads as zeros.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import { sqliteRead } from './sqlite.js';
|
|
5
|
+
import { homeDir } from './utils.js';
|
|
6
6
|
import { usageDbPath } from './usage-store.js';
|
|
7
7
|
export const RANGES = ['1h', '24h', '7d', '30d', '90d', 'all'];
|
|
8
8
|
export function isRangeKey(v) {
|
|
@@ -65,7 +65,7 @@ export function statsDbPath() {
|
|
|
65
65
|
}
|
|
66
66
|
/** Quota snapshots. Only omp records these, so this stays omp's own database. */
|
|
67
67
|
export function agentDbPath() {
|
|
68
|
-
return process.env.TERSIO_AGENT_SESSIONS_DB ?? path.join(
|
|
68
|
+
return process.env.TERSIO_AGENT_SESSIONS_DB ?? path.join(homeDir(), '.omp', 'agent', 'agent.db');
|
|
69
69
|
}
|
|
70
70
|
/** The row shape every query groups by: a message joined to its file, since path and facts live apart. */
|
|
71
71
|
const SOURCE = 'messages m JOIN files f ON f.id = m.file_id';
|
|
@@ -94,14 +94,19 @@ function providerExpr(model) {
|
|
|
94
94
|
.join(' ');
|
|
95
95
|
return `CASE WHEN instr(${model}, '/') = 0 THEN '' ELSE CASE ${named} ${cases} ELSE ${named} END END`;
|
|
96
96
|
}
|
|
97
|
-
/** SQL: the project a transcript
|
|
97
|
+
/** SQL: the project from a transcript's session folder; the separator follows the stored path, since a Windows run stores `\`. */
|
|
98
98
|
function projectExpr(file) {
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
99
|
+
// Position of the session marker under either separator; 0 when the path has none.
|
|
100
|
+
const posAt = `instr(${file}, '/sessions/')`;
|
|
101
|
+
const posWin = `instr(${file}, '\\sessions\\')`;
|
|
102
|
+
const at = `CASE WHEN ${posAt} > 0 THEN ${posAt} ELSE ${posWin} END`;
|
|
103
|
+
// Which separator to slice on: the one the matched marker used, else the one the path uses.
|
|
104
|
+
const sep = `CASE WHEN ${posAt} > 0 THEN '/' WHEN ${posWin} > 0 THEN '\\' WHEN instr(${file}, '\\') > 0 THEN '\\' ELSE '/' END`;
|
|
105
|
+
const seg = `substr(${file}, ${at} + 10, instr(substr(${file}, ${at} + 10) || ${sep}, ${sep}) - 1)`;
|
|
106
|
+
const base = `replace(${file}, rtrim(${file}, replace(${file}, ${sep}, '')), '')`;
|
|
102
107
|
const dir = `substr(${file}, 1, length(${file}) - length(${base}) - 1)`;
|
|
103
|
-
const dirBase = `replace(${dir}, rtrim(${dir}, replace(${dir},
|
|
104
|
-
return `CASE WHEN ${
|
|
108
|
+
const dirBase = `replace(${dir}, rtrim(${dir}, replace(${dir}, ${sep}, '')), '')`;
|
|
109
|
+
return `CASE WHEN ${at} > 0 THEN ${seg} ELSE ${dirBase} END`;
|
|
105
110
|
}
|
|
106
111
|
/** Every column a query reads, mapped to the store's schema in one place. */
|
|
107
112
|
const COL = {
|
|
@@ -436,18 +441,20 @@ function foldTraces(rows) {
|
|
|
436
441
|
.map((row) => ({ ...row, models: row.models.toSorted() }))
|
|
437
442
|
.toSorted((a, b) => b.lastTs - a.lastTs);
|
|
438
443
|
}
|
|
439
|
-
/** The root session file for a transcript, which is itself when it is already a root.
|
|
444
|
+
/** The root session file for a transcript, which is itself when it is already a root.
|
|
445
|
+
* Splits on whichever separator the stored path uses: a Windows run writes `\`. */
|
|
440
446
|
export function rootSessionFile(sessionFile) {
|
|
441
|
-
const marker = '/sessions/';
|
|
447
|
+
const marker = sessionFile.includes('/sessions/') ? '/sessions/' : '\\sessions\\';
|
|
442
448
|
const at = sessionFile.indexOf(marker);
|
|
443
449
|
if (at < 0)
|
|
444
450
|
return sessionFile;
|
|
445
451
|
const base = sessionFile.slice(0, at + marker.length);
|
|
446
|
-
const parts = sessionFile.slice(at + marker.length).split(
|
|
452
|
+
const parts = sessionFile.slice(at + marker.length).split(marker[0]);
|
|
447
453
|
// `<project>/<root>.jsonl` is a root; `<project>/<root>/<child>.jsonl` belongs to that root.
|
|
448
454
|
if (parts.length < 3)
|
|
449
455
|
return sessionFile;
|
|
450
|
-
|
|
456
|
+
// Rejoined with the same separator, so a child's root matches its root file exactly.
|
|
457
|
+
return `${base}${parts[0]}${marker[0]}${parts[1]}.jsonl`;
|
|
451
458
|
}
|
|
452
459
|
/** Same normalization as the reference: ids and counters collapse, an HTTP status does not. */
|
|
453
460
|
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
@@ -900,7 +907,8 @@ function stopState(stopReason) {
|
|
|
900
907
|
const AGENT_TOOLS = new Set(['task', 'subagent']);
|
|
901
908
|
/** One transcript as spans, tracks and markers: a model span runs `ts - duration` to `ts`. */
|
|
902
909
|
export function readSessionTrace(sessionFile, limit = TRACE_SPAN_LIMIT) {
|
|
903
|
-
|
|
910
|
+
// Either separator: a Windows host writes `<drive>\...\.omp\agent\sessions\<project>\<file>`.
|
|
911
|
+
const project = sessionFile.split(/[/\\]sessions[/\\]/)[1]?.split(/[/\\]/)[0] ?? '';
|
|
904
912
|
const empty = {
|
|
905
913
|
sessionFile, project, title: '', cwd: project, startedAt: 0, endedAt: 0,
|
|
906
914
|
tracks: [], markers: [], summary: emptyTraceSummary(), toolStats: [], truncated: false,
|
|
@@ -80,15 +80,16 @@ function asPrice(v) {
|
|
|
80
80
|
}
|
|
81
81
|
let liveCache = null;
|
|
82
82
|
export function loadLivePrices() {
|
|
83
|
-
// One stat per call beats
|
|
83
|
+
// One stat per call beats a full parse; the key carries size and mtime, because Windows rounds a rapid rewrite's mtime to one value.
|
|
84
84
|
try {
|
|
85
85
|
const file = pricesCachePath();
|
|
86
|
-
const
|
|
87
|
-
if (liveCache && liveCache.path === file && liveCache.mtime ===
|
|
86
|
+
const stat = fs.statSync(file);
|
|
87
|
+
if (liveCache && liveCache.path === file && liveCache.mtime === stat.mtimeMs && liveCache.size === stat.size) {
|
|
88
88
|
return liveCache.prices;
|
|
89
|
+
}
|
|
89
90
|
const prices = readLivePrices();
|
|
90
91
|
if (prices)
|
|
91
|
-
liveCache = { path: file, mtime, prices };
|
|
92
|
+
liveCache = { path: file, mtime: stat.mtimeMs, size: stat.size, prices };
|
|
92
93
|
return prices;
|
|
93
94
|
}
|
|
94
95
|
catch {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Eligible heads come from rtk's own contract: `rtk rewrite <probe>` exits 0 or 3 when rtk handles the command, 1 when it has no equivalent.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { normalizeRtkVersion, shimCommand, tersioDataPath } from './utils.js';
|
|
6
|
+
/** Verified against rtk 1.0.1; the drift test fails when rtk's set moves. */
|
|
7
|
+
export const FALLBACK_ELIGIBLE_HEADS = [
|
|
8
|
+
'rtk', 'git', 'gh', 'glab', 'docker', 'kubectl', 'psql', 'aws', 'bunx', 'npx',
|
|
9
|
+
'pytest', 'mypy', 'cat', 'grep', 'rg', 'find', 'tree', 'diff', 'ls', 'ast-grep',
|
|
10
|
+
'wget', 'prettier', 'playwright', 'prisma', 'jest', 'vitest', 'ctest', 'lint',
|
|
11
|
+
'wc', 'tsc', 'curl', 'cargo', 'npm', 'pnpm', 'bun', 'go', 'ruff', 'next',
|
|
12
|
+
'dotnet', 'oc', 'jq', 'make',
|
|
13
|
+
];
|
|
14
|
+
const PROBES = {
|
|
15
|
+
git: 'git log', gh: 'gh pr list', glab: 'glab mr list', docker: 'docker ps',
|
|
16
|
+
kubectl: 'kubectl get pods', psql: 'psql --version', aws: 'aws --version',
|
|
17
|
+
bunx: 'bunx --version', npx: 'npx --version', pytest: 'pytest --version',
|
|
18
|
+
mypy: 'mypy --version', cat: 'cat README.md', grep: 'grep rtk README.md',
|
|
19
|
+
rg: 'rg rtk README.md', find: 'find . -name x', tree: 'tree .', diff: 'diff a b',
|
|
20
|
+
ls: 'ls -la', 'ast-grep': 'ast-grep --version', wget: 'wget --version',
|
|
21
|
+
prettier: 'prettier --version', playwright: 'playwright --version', prisma: 'prisma --version',
|
|
22
|
+
jest: 'jest --version', vitest: 'vitest --version', ctest: 'ctest --version',
|
|
23
|
+
lint: 'lint .', wc: 'wc -l x', tsc: 'tsc --version', curl: 'curl --version',
|
|
24
|
+
cargo: 'cargo test', npm: 'npm run build', pnpm: 'pnpm run build', bun: 'bun run test',
|
|
25
|
+
go: 'go test ./...', ruff: 'ruff check .', next: 'next build', dotnet: 'dotnet build',
|
|
26
|
+
oc: 'oc get pods', jq: 'jq . x', make: 'make',
|
|
27
|
+
};
|
|
28
|
+
function probe(bin, command) {
|
|
29
|
+
try {
|
|
30
|
+
const shim = shimCommand(bin, ['rewrite', command]);
|
|
31
|
+
const out = execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
32
|
+
return out.trim().length > 0;
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
// Non-zero exit throws; exit 3 still prints a rewrite, exits 1 and 2 do not.
|
|
36
|
+
const err = e;
|
|
37
|
+
if (err.status !== 0 && err.status !== 3)
|
|
38
|
+
return false;
|
|
39
|
+
return String(err.stdout ?? '').trim().length > 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** One set of heads for this rtk build, read straight from its rewrite contract. */
|
|
43
|
+
export function deriveEligibleHeads(bin) {
|
|
44
|
+
const heads = new Set(['rtk']);
|
|
45
|
+
for (const [head, command] of Object.entries(PROBES)) {
|
|
46
|
+
if (probe(bin, command))
|
|
47
|
+
heads.add(head);
|
|
48
|
+
}
|
|
49
|
+
return heads;
|
|
50
|
+
}
|
|
51
|
+
function rtkVersion(bin) {
|
|
52
|
+
try {
|
|
53
|
+
const shim = shimCommand(bin, ['--version']);
|
|
54
|
+
return normalizeRtkVersion(execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true }));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Heads rtk rewrites, cached per binary version; without a binary, or on any read failure, the verified fallback stands. */
|
|
61
|
+
export function eligibleHeads(bin) {
|
|
62
|
+
if (!bin)
|
|
63
|
+
return new Set(FALLBACK_ELIGIBLE_HEADS);
|
|
64
|
+
const version = rtkVersion(bin);
|
|
65
|
+
const file = tersioDataPath('rtk-eligible.json', 'rtk-eligible.json');
|
|
66
|
+
try {
|
|
67
|
+
const cached = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
68
|
+
if (cached.version === version && Array.isArray(cached.heads) && cached.heads.every((h) => typeof h === 'string')) {
|
|
69
|
+
return new Set(cached.heads);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch { /* derive below */ }
|
|
73
|
+
const heads = deriveEligibleHeads(bin);
|
|
74
|
+
try {
|
|
75
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
76
|
+
fs.writeFileSync(file, `${JSON.stringify({ version, heads: [...heads] })}\n`, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
catch { /* the cache is best-effort */ }
|
|
79
|
+
return heads;
|
|
80
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Savings read from rtk history, never estimated; modes bench in docs/
|
|
1
|
+
// Savings read from rtk history, never estimated; modes bench in docs/BENCHMARK_GUIDE.md.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
3
|
import path from 'node:path';
|
|
4
|
+
import { rtkDirs } from './utils.js';
|
|
5
5
|
import { sqliteRead } from './sqlite.js';
|
|
6
6
|
const EMPTY = { commands: 0, saved: 0, input: 0, avgPct: 0, totalMs: 0, byCommand: [] };
|
|
7
7
|
// The table shows one clipped cell, so bound the command before it reaches /data.json.
|
|
@@ -12,11 +12,7 @@ export function rtkDbPath() {
|
|
|
12
12
|
const override = process.env.TERSIO_RTK_DB;
|
|
13
13
|
if (override)
|
|
14
14
|
return override;
|
|
15
|
-
|
|
16
|
-
return path.join(os.homedir(), 'Library', 'Application Support', 'rtk', 'history.db');
|
|
17
|
-
}
|
|
18
|
-
const xdg = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
|
19
|
-
return path.join(xdg, 'rtk', 'history.db');
|
|
15
|
+
return path.join(rtkDirs().data, 'history.db');
|
|
20
16
|
}
|
|
21
17
|
function query(db, sql) {
|
|
22
18
|
return sqliteRead(db, sql)
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
// One place that runs SQL
|
|
2
|
-
//
|
|
3
|
-
// Bun and Node 22.5+ carry SQLite in process, so the `sqlite3` binary is a fallback rather than a
|
|
4
|
-
// requirement: hosts without it kept silently reading nothing. Reads come back exactly as the sqlite3
|
|
5
|
-
// CLI printed them -- rows on their own lines, cells joined by tabs, NULL an empty cell, one trailing
|
|
6
|
-
// newline -- because every caller already parses that shape. The engine rungs take one statement per
|
|
7
|
-
// read, so a caller with several queries calls once per query.
|
|
8
|
-
//
|
|
9
|
-
// The write path leaves the journal mode alone. The store's own schema sets WAL so a reader (the
|
|
10
|
-
// dashboard) never blocks a writer, and changing the mode needs exclusive access: with a reader holding
|
|
11
|
-
// the file the pragma fails, which is a failure reported after the caller's rows have already committed.
|
|
1
|
+
// One place that runs SQL: Bun or Node 22.5+ in process, the `sqlite3` binary as the fallback, and rows in its CLI shape for every caller. The write path leaves the journal mode alone (see docs/ERRORS.md).
|
|
12
2
|
import { execFileSync } from 'node:child_process';
|
|
13
3
|
import fs from 'node:fs';
|
|
14
4
|
import { createRequire } from 'node:module';
|
|
@@ -51,8 +41,7 @@ function engine() {
|
|
|
51
41
|
catch { /* not Bun, or no SQLite in this build */ }
|
|
52
42
|
try {
|
|
53
43
|
const { DatabaseSync } = requireOptional('node:sqlite');
|
|
54
|
-
// Rows arrive keyed by column name unless the statement returns arrays
|
|
55
|
-
// its key and shifts every later cell, so a Node that cannot return arrays is no engine at all.
|
|
44
|
+
// Rows arrive keyed by column name unless the statement returns arrays: a duplicate name collapses its key and shifts every later cell, so a Node that cannot return arrays is no engine.
|
|
56
45
|
const probe = new DatabaseSync(':memory:');
|
|
57
46
|
try {
|
|
58
47
|
if (typeof probe.prepare('SELECT 1').setReturnArrays !== 'function')
|
|
@@ -26,8 +26,7 @@ function statusUi(ctx) {
|
|
|
26
26
|
let remembered;
|
|
27
27
|
let lastText;
|
|
28
28
|
let lastUi;
|
|
29
|
-
// Paint the host footer bar; same text on the same UI is skipped, a
|
|
30
|
-
// One key only: the combo line already names the caveman mode, and a second key renders as a second footer line.
|
|
29
|
+
// Paint the host footer bar; same text on the same UI is skipped. One key only: the combo line already names the caveman mode, and a second key renders as a second footer line.
|
|
31
30
|
export function announceStatus(ctx) {
|
|
32
31
|
const state = getSharedComboState();
|
|
33
32
|
const text = formatStatus(state);
|