@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
|
@@ -12,15 +12,15 @@ Three rules carry the rest.
|
|
|
12
12
|
|
|
13
13
|
#### Roles and placement
|
|
14
14
|
|
|
15
|
-
- **Coordinator (this chat).** Local. Frames, authors briefs, drains the inbox, owns the human report, makes judgment calls. It never authors or edits code. Conflicted merges, restacks, and code changes are always tasks. Mechanically landing a verified unit (fast-forward or clean cherry-pick of a worker's commit, then push) is bookkeeping the coordinator may do itself on repos where local git is cheap. Queueing finished work behind an idle stacker is how a deadline harvests nothing. The loop is agentic end to end. Agents are spawned, resumed, and drained only through the Task tool. State reads and writes go through
|
|
15
|
+
- **Coordinator (this chat).** Local. Frames, authors briefs, drains the inbox, owns the human report, makes judgment calls. It never authors or edits code. Conflicted merges, restacks, and code changes are always tasks. Mechanically landing a verified unit (fast-forward or clean cherry-pick of a worker's commit, then push) is bookkeeping the coordinator may do itself on repos where local git is cheap. Queueing finished work behind an idle stacker is how a deadline harvests nothing. The loop is agentic end to end. Agents are spawned, resumed, and drained only through the Task tool. State reads and writes go through the state store at drain points, one command in and one line out. The store never spawns, waits, or wakes anything.
|
|
16
16
|
- **Sub-coordinator.** Always local, durable, one per track, and only when the program exceeds what one coordinator's drains can manage. A track the coordinator can drain itself needs no middle layer. Each nested layer re-pays a full orientation preamble, and a blocking sub-coordinator hides its children while the parent idles. Owns its track's units and boards, authors its workers' briefs, spawns its own workers and verifiers (nesting works to depth 3, and a nested spawn has the full Task schema including `environment`). Rolls up aggregates at wave boundaries. Never forwards raw child reports. Cap in-flight children at what one drain can process, roughly ten, as a rolling window. Never as blocking batches, which cost the slowest child of every batch.
|
|
17
|
-
- **Worker / verifier.** Always unless the task needs this machine:
|
|
17
|
+
- **Worker / verifier.** Always unless the task needs this machine: runtime verification on the real surface. Reading local transcripts under the workspace's transcript root. Simulators and local IDE state. Auth that exists only here. Cloud agents cannot read the local store, so their briefs inline what they need or point at repo paths. Prefer fewer, broader workers. One writer per worktree or branch (principle-separate-before-serializing-shared-state). Run a unit's verifier on a different model family from its worker.
|
|
18
18
|
|
|
19
19
|
Depth stays at coordinator, track, worker. Author the track decomposition per project (build, landing, and verification are common cuts, not a required shape). Hard-coded swarm trees were tried and parked as too rigid.
|
|
20
20
|
|
|
21
21
|
#### Store layout
|
|
22
22
|
|
|
23
|
-
Create `orchestrate/<project-slug>/` in the current agent's store (path in the system prompt). Every file has exactly one writer. Owners publish facts, readers aggregate at read time.
|
|
23
|
+
Create `orchestrate/<project-slug>/` in the current agent's store (path in the system prompt). Every file has exactly one writer. Owners publish facts, readers aggregate at read time. Keep the bookkeeping in plain TSV and JSON, readable without any CLI.
|
|
24
24
|
|
|
25
25
|
- `preferences.md` is the standing-orders register: numbered lines, one constraint each (model policy, stack shape and count, verification bar, forbidden paths, escalation policy). Paste it verbatim into every spawn and every resume. Directives decay across resumes, and each dropped one costs a human turn. When you catch yourself restating an instruction, append the line before you act (principle-encode-lessons-in-structure).
|
|
26
26
|
- `overview.md` is the durable PR and issue DB. Append. Never rewrite wholesale per event.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**You own the resume point. Read the prior trail, don't redo it.**
|
|
4
4
|
|
|
5
|
-
1. Locate the prior trail. A local transcript under the
|
|
5
|
+
1. Locate the prior trail. A local transcript under the workspace's transcript root (the system prompt names the path. Do not glob across every workspace's transcript root, that crosses workspace boundaries and reads private chats from unrelated projects), a cloud-agent URL, or a pushed branch. Read the metadata overview and last messages first, then scan back for the decision points. Parse a long transcript in a subagent and keep the reduced timeline in the main thread (the **principle-guard-the-context-window** skill).
|
|
6
6
|
2. Reconstruct operational state. The branch and worktree, what already landed (`git log`, `git diff` against the base), the open todos, the decisions made. The prior trail is authoritative input. Resist the bias to re-derive it.
|
|
7
7
|
3. Diff done vs pending. Compare what shipped against what was planned, name the resume point, do not re-run the prior repro or redo completed work. A "let me verify from scratch" pass means you're treating the trail as untrustworthy when it's authoritative.
|
|
8
8
|
4. Route the remaining work to the matching playbook and pick the verdict: continue the execution, ship a finished recommendation, ratify or override a prior conclusion, or postmortem a failed run. The pickup playbook ends here. The routed playbook owns the rest.
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
This is the half after `playbooks/babysit.md`.
|
|
6
6
|
|
|
7
|
-
1. **Resolve the forge, then verify every PR independently.** GitHub CLI (`gh`) is the default. If `command -v origin` succeeds and Origin can resolve the repository, use `origin pr ...` for view, watch, edit, and merge operations. Otherwise stay on `gh` and record the fallback. Never require Graphite (`gt`). One subagent per PR, not batched, each exercising the real surface
|
|
7
|
+
1. **Resolve the forge, then verify every PR independently.** GitHub CLI (`gh`) is the default. If `command -v origin` succeeds and Origin can resolve the repository, use `origin pr ...` for view, watch, edit, and merge operations. Otherwise stay on `gh` and record the fallback. Never require Graphite (`gt`). One subagent per PR, not batched, each exercising the real surface through the repo's own control skill where one exists against parent versus head. Each returns `PASS`, `PASS+NOTES` or `FAIL` and posts that verdict on its own PR. Safe means a verdict from an agent that did not write the code. CI green is not a verdict, and an approving bot review is not a verdict.
|
|
8
8
|
2. **Land only the contiguous verified run rooted at the bottom.** Walk up from the lowest unmerged PR and stop at the first one without a passing verdict, where both `PASS` and `PASS+NOTES` pass. A verified PR sitting above an unverified one is not landable. Report the ceiling as a PR number and say what breaks the chain.
|
|
9
9
|
3. **Re-check that each verdict still describes the patch.** Record the verdict head SHA, base SHA, and stable `git patch-id` of that PR's base-to-head diff. A rebase or base retarget rewrites SHAs and can silently invalidate a verdict without touching a check. Before landing a PR, compare the recorded patch-id with its current base-to-head patch-id. When the two patches differ only in tests, docs, or lint config, build what each lane ran. Build it twice at the verdict SHA and once at the current head. A difference is noise if the two builds at the verdict SHA also show it, or if it is an embedded commit SHA. Judge each difference, not each file, and report each kind of noise with its files. If only noise differs, that lane's result stays valid, and checks and a review of the change run fresh. Do not reuse a lane result from a dev server or from anything else with no build output. Rerun that lane. Re-verify anything else when the patch changed. When it did not, keep the code verdict but re-run mergeability and CI at the current head. Never use matching commit messages or a green check from an older SHA as a substitute.
|
|
10
10
|
4. **Prepare only the bottom PR.** Fetch current trunk. Rebase the lowest verified branch onto the exact trunk tip when needed, push it, and retarget only that PR to trunk with `origin pr edit <pr> --base <trunk>` or `gh pr edit <pr> --base <trunk>`. Re-run step 3 after the push. Do not retarget, arm, or merge descendants yet.
|
|
11
11
|
5. **Land one PR at a time.** If the bottom PR is mergeable now, squash it with `origin pr merge <pr> --squash` or `gh pr merge <pr> --squash`. If requirements are still running and the user asked for merge-when-ready, arm only that PR with `origin pr merge <pr> --squash --auto` or `gh pr merge <pr> --squash --auto`. Origin's `--auto` is Origin merge-when-ready. GitHub's `--auto` is GitHub auto-merge. Wait for that PR to merge before preparing the next one.
|
|
12
12
|
6. **Do not read GitHub `autoMergeRequest` as stack readiness.** At most it says GitHub auto-merge was requested for one GitHub PR. It does not prove Origin merge-when-ready is armed, that a descendant is queued, that a patch verdict is current, or that the contiguous stack is safe. Confirm the active forge's state for the current bottom PR, and say that the state is unknown if the active forge cannot report it.
|
|
13
13
|
7. **Recompute after every merge.** Fetch trunk, confirm the merged SHA is present, drop the merged PR from the frozen bottom-to-top list, and inspect the new bottom PR's base, head, checks, and patch-id. A host may retarget a child automatically, but do not assume it did. Repeat steps 3 through 6 for that one PR. Independent work stays outside this chain and ships on its own.
|
|
14
|
-
8. **Watch the current frontier until it merges or fails. Do not mutate the queue around it.** With Origin, use `origin pr view <pr> --checks --comments` and `origin pr checks <pr> --watch`, then re-read the PR until it reports merged or blocked. With GitHub, use
|
|
14
|
+
8. **Watch the current frontier until it merges or fails. Do not mutate the queue around it.** With Origin, use `origin pr view <pr> --checks --comments` and `origin pr checks <pr> --watch`, then re-read the PR until it reports merged or blocked. With GitHub, use an event wake if the repo has one, and poll `gh pr view <pr> --json state,mergedAt,mergeStateStatus,statusCheckRollup,autoMergeRequest` after each wake, ignoring `READY` until `mergedAt` is non-null or `state` is `MERGED`. Only then run step 7. Hard-fail only when `state` is `CLOSED` with no `mergedAt`, a required check concludes `FAILURE` or `CANCELLED` and blocks merge after auto-merge is no longer pending, or `mergeStateStatus` is `UNSTABLE` or `DIRTY` with no auto-merge pending. `BLOCKED` while checks are pending or auto-merge is armed is not failure. Do not use Babysit's queued `WAITING`/`merge-queue` stop condition here. Hold the watch under a repeating loop in dynamic mode. Report each merge and the new ceiling. If the queue stalls, diagnose before mutating.
|
|
15
15
|
9. **Stop at the ceiling.** When the verified run is merged, report what landed, what the next unverified PR is, and what verifying it would take. Extending the run is a new pass through step 1.
|
|
16
16
|
|
|
17
17
|
**Reply:** the verified run and its ceiling, each PR's verdict and who produced it, what you armed and how you confirmed it, what landed, and what the next gap needs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
### Worktree and simulator cleanup
|
|
2
2
|
|
|
3
|
-
1. Snapshot and audit. Record `df -h /`, then
|
|
3
|
+
1. Snapshot and audit. Record `df -h /`, then read paths from `git worktree list` and classify each worktree by size, age, merge state, uncommitted work, PR state, and the newest chat that touched it, then suggest a bucket. The transcript scan is slow, so background it.
|
|
4
4
|
2. The bucket is advice, not permission. The pinned and active chats are the real artifact (principle-prove-it-works). Get that set from the user or sidebar and cross-check every candidate. The lever has marked `safe` a worktree the user had pinned, so the pinned set wins.
|
|
5
5
|
3. Verify usage before deleting. For every `verify-recent-chat` row, or anything you doubt, fan subagents out to read the transcripts and report whether the chat is pinned or ongoing and which worktrees it touches (principle-guard-the-context-window, transcripts are bulk). A pinned chat spawns arena and repro trees into sibling worktrees via background subagents, and those are in use even when their names never hit the sidebar.
|
|
6
6
|
4. Pause on irreversible loss. `wip:N` is N tracked uncommitted edits. Show the diff and get a decision first, since removing a clean worktree is recoverable from its branch but uncommitted work is gone. `scratch:N` is untracked throwaway, safe to drop, but name the files. Per Autonomy, clean and merged and not-in-use proceeds. `wip` and in-use pause.
|
|
@@ -11,10 +11,10 @@ Invoke when the user says "reflect" or "the reflect skill". Skip when the conver
|
|
|
11
11
|
|
|
12
12
|
## 1. Locate the active transcript
|
|
13
13
|
|
|
14
|
-
Before fanning out, find this conversation's transcript in the
|
|
14
|
+
Before fanning out, find this conversation's transcript in the transcript root the system prompt names. Do not glob across every workspace's transcript root. That crosses workspace boundaries and reads private chats from unrelated projects.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
ls -t <
|
|
17
|
+
ls -t <transcript-root>/*.jsonl <transcript-root>/*/*.jsonl <transcript-root>/*/subagents/*.jsonl 2>/dev/null | head -10
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
That covers the legacy flat (`<id>.jsonl`), current nested (`<id>/<id>.jsonl`), and subagent (`<parent>/subagents/<child>.jsonl`) layouts. For each candidate, read the first JSONL line. Take the file whose `message.content[0].text` contains the conversation's opening user prompt. If none matches, pass a tight digest of the session instead.
|
|
@@ -34,7 +34,7 @@ ts phase decision why evidence result
|
|
|
34
34
|
|
|
35
35
|
Write each entry the way you'd tell a teammate what you did. Plain words, concrete actions, no AI speak or abstract jargon (the **unslop** skill applies to log text too).
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Append a row with `printf`, minding those same bytes if cells come from generated or user-supplied text. Prefix any cell starting with `=`, `+`, `-`, or `@` with a single quote.
|
|
38
38
|
|
|
39
39
|
Log decision points and checkpoints, not every action: a fork chosen, a unit completed with its verification result, a pivot or revert with its trigger, a blocker surfaced, a gate fixed. For loop runs, one row per iteration. Skip the trivial and self-evident.
|
|
40
40
|
|
|
@@ -53,7 +53,7 @@ Commit it only when the work is ambitious enough that a reviewer needs the trail
|
|
|
53
53
|
|
|
54
54
|
## Audit the log against the transcript
|
|
55
55
|
|
|
56
|
-
At the end of the run, before handing back, check the log told the truth. Read this run's transcript under the
|
|
56
|
+
At the end of the run, before handing back, check the log told the truth. Read this run's transcript under the workspace's transcript root (the system prompt names the path). Don't glob across every workspace's transcript root. That reads unrelated private chats. Walk this run's rows against what actually happened. Each stretch of them begins at one of this run's `start` rows, or at the first row if this run created the log, and ends at the next `start` row of another run:
|
|
57
57
|
|
|
58
58
|
- Check that every row maps to a real decision or action.
|
|
59
59
|
- Check that each row's evidence resolves and shows what the row claims.
|
package/extensions/rtk/filter.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { resolveRtkBinary } from '../shared/utils.ts';
|
|
6
6
|
import { readRtkDefault } from '../shared/plugin-settings.ts';
|
|
7
7
|
import { getSharedComboState, lastCustomValue, sessionEntries } from '../shared/session-state.ts';
|
|
8
|
+
import { appendUsage } from '../shared/usage-ledger.ts';
|
|
9
|
+
import { isPiHost } from '../shared/host.ts';
|
|
8
10
|
import type { ExtensionApi, ExtensionCtx, SessionEntry, ToolResultEvent } from '../shared/types.ts';
|
|
9
11
|
|
|
10
12
|
/** Tool name to `rtk pipe --filter` name. Exported so the OpenCode host reuses one map. */
|
|
@@ -18,8 +20,13 @@ export const FILTER_BY_TOOL = new Map([
|
|
|
18
20
|
export const MIN_FILTER_BYTES = 1200;
|
|
19
21
|
const FILTER_TIMEOUT_MS = 5000;
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
// A shell that understands `<`, resolved per platform; separate flags, because Node quotes each argv element and cmd.exe rejects `"/d /s /c"`.
|
|
24
|
+
/** 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. */
|
|
25
|
+
export function filterShell(): { shell: string; flags: string[]; wrap: boolean } {
|
|
26
|
+
return process.platform === 'win32'
|
|
27
|
+
? { shell: process.env.ComSpec || 'cmd.exe', flags: ['/d', '/s', '/c'], wrap: true }
|
|
28
|
+
: { shell: '/bin/sh', flags: ['-c'], wrap: false };
|
|
29
|
+
}
|
|
23
30
|
|
|
24
31
|
export type FilterExec = (
|
|
25
32
|
cmd: string,
|
|
@@ -58,8 +65,10 @@ export async function filterThroughRtk(
|
|
|
58
65
|
dir = mkdtempSync(join(tmpdir(), 'tersio-rtk-'));
|
|
59
66
|
const file = join(dir, 'in.txt');
|
|
60
67
|
writeFileSync(file, text, 'utf8');
|
|
61
|
-
// `rtk pipe` reads stdin only and execFile has no stdin, so one redirect.
|
|
62
|
-
const
|
|
68
|
+
// `rtk pipe` reads stdin only and execFile has no stdin, so one redirect through the platform shell.
|
|
69
|
+
const { shell, flags, wrap } = filterShell();
|
|
70
|
+
const line = `"${bin}" pipe -f ${filter} < "${file}"`;
|
|
71
|
+
const result = await exec(shell, [...flags, wrap ? `"${line}"` : line], { signal, timeout: FILTER_TIMEOUT_MS });
|
|
63
72
|
if (result.code !== 0) return null;
|
|
64
73
|
const out = result.stdout;
|
|
65
74
|
if (!out.trim() || out.length >= text.length) return null;
|
|
@@ -71,10 +80,48 @@ export async function filterThroughRtk(
|
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
|
|
83
|
+
/** A bash tool call, before it runs. pi and omp hand the extension the mutable input. */
|
|
84
|
+
interface BashCallEvent {
|
|
85
|
+
toolName?: unknown;
|
|
86
|
+
input?: { command?: unknown };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Ask rtk for the rewritten command; null on any failure, so the original runs. */
|
|
90
|
+
async function rewriteCommand(
|
|
91
|
+
exec: FilterExec,
|
|
92
|
+
bin: string,
|
|
93
|
+
command: string,
|
|
94
|
+
signal?: AbortSignal,
|
|
95
|
+
): Promise<string | null> {
|
|
96
|
+
try {
|
|
97
|
+
const result = await exec(bin, ['rewrite', command], { timeout: FILTER_TIMEOUT_MS, signal });
|
|
98
|
+
const out = result.stdout.trim();
|
|
99
|
+
return out || null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
export default function rtkFilterExtension(pi: ExtensionApi): void {
|
|
75
106
|
const bin = resolveRtkBinary();
|
|
76
107
|
const exec = pi.exec;
|
|
77
108
|
if (!bin || !exec) return;
|
|
109
|
+
// omp records the post-hook command in its transcript, so only pi leaves an audit row here:
|
|
110
|
+
// opencode rewrites in its own plugin hook, and an omp row would double-count the transcript.
|
|
111
|
+
const auditHost = isPiHost(pi) ? 'pi' : null;
|
|
112
|
+
// Registered synchronously, unlike the rtk-init hook, whose async version probe can miss the first call of a fast session.
|
|
113
|
+
pi.on('tool_call', async (event, ctx: ExtensionCtx) => {
|
|
114
|
+
const call = (event ?? {}) as BashCallEvent;
|
|
115
|
+
const input = call.input;
|
|
116
|
+
if (call.toolName !== 'bash' || !input) return;
|
|
117
|
+
const command = input.command;
|
|
118
|
+
if (typeof command !== 'string' || !command.trim() || command.startsWith('rtk ')) return;
|
|
119
|
+
if (!rtkActive(sessionEntries(ctx))) return;
|
|
120
|
+
const rewritten = await rewriteCommand(exec, bin, command, ctx?.signal);
|
|
121
|
+
if (!rewritten || rewritten === command) return;
|
|
122
|
+
input.command = rewritten;
|
|
123
|
+
if (auditHost) appendUsage('rtk-audit', `${auditHost} ${command}`);
|
|
124
|
+
});
|
|
78
125
|
pi.on<ToolResultEvent>('tool_result', async (event, ctx: ExtensionCtx) => {
|
|
79
126
|
const filter = FILTER_BY_TOOL.get(String(event?.toolName ?? '').toLowerCase());
|
|
80
127
|
if (!filter || event?.isError) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.ts';
|
|
2
2
|
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.ts';
|
|
3
3
|
import { readRtkDefault } from '../shared/plugin-settings.ts';
|
|
4
|
-
import { resolveRtkBinary } from '../shared/utils.ts';
|
|
4
|
+
import { resolveRtkBinary, shimCommand } from '../shared/utils.ts';
|
|
5
5
|
import { announceStatus } from '../shared/status.ts';
|
|
6
6
|
import type { ExtensionApi, ExtensionCtx, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
7
7
|
|
|
@@ -93,7 +93,9 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
93
93
|
onUpdate?.({ content: [{ type: 'text', text: `${bin === null ? '' : 'rtk '}${argv.join(' ')}` }], details: { phase: 'start' } });
|
|
94
94
|
try {
|
|
95
95
|
// A GUI-launched host may lack ~/.bun/bin on PATH, so prefer the resolved path.
|
|
96
|
-
|
|
96
|
+
// A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
|
|
97
|
+
const shim = shimCommand(bin ?? argv[0], argv);
|
|
98
|
+
const result = await pi.exec(shim.cmd, shim.args, { signal, cwd });
|
|
97
99
|
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
98
100
|
return {
|
|
99
101
|
isError: result.code !== 0,
|
package/extensions/server.ts
CHANGED
|
@@ -12,7 +12,8 @@ import comboToggleExtension from './combo/index.ts';
|
|
|
12
12
|
import tersioCommandsExtension from './commands/index.ts';
|
|
13
13
|
import { getSharedComboState } from './shared/session-state.ts';
|
|
14
14
|
import { formatStatus } from './shared/status.ts';
|
|
15
|
-
import { resolveRtkBinary } from './shared/utils.ts';
|
|
15
|
+
import { resolveRtkBinary, shimCommand } from './shared/utils.ts';
|
|
16
|
+
import { appendUsage } from './shared/usage-ledger.ts';
|
|
16
17
|
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from './shared/types.ts';
|
|
17
18
|
|
|
18
19
|
interface CommandConfig {
|
|
@@ -221,7 +222,11 @@ const plugin: Plugin.Plugin = {
|
|
|
221
222
|
if (sid !== undefined) await ensureSession(sid);
|
|
222
223
|
if (!rtkActive(sid === undefined ? undefined : sessionEntriesOf(sid))) return;
|
|
223
224
|
const rewritten = await rewriteCommand(input.command);
|
|
224
|
-
if (rewritten
|
|
225
|
+
if (!rewritten || rewritten === input.command) return;
|
|
226
|
+
const original = input.command;
|
|
227
|
+
input.command = rewritten;
|
|
228
|
+
// The transcript stores the pre-hook command, so the rewrite leaves its own row.
|
|
229
|
+
appendUsage('rtk-audit', `opencode ${original}`);
|
|
225
230
|
});
|
|
226
231
|
|
|
227
232
|
// Built-in grep and glob output never reaches the bash hook; mutate result.content in place.
|
|
@@ -250,15 +255,17 @@ const plugin: Plugin.Plugin = {
|
|
|
250
255
|
|
|
251
256
|
/** execFile with the promise shape `filterThroughRtk` expects. */
|
|
252
257
|
function execRtk(cmd: string, args: string[], opts?: { signal?: AbortSignal; timeout?: number }): Promise<{ stdout: string; code: number }> {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
258
|
+
const { promise, resolve } = Promise.withResolvers<{ stdout: string; code: number }>();
|
|
259
|
+
// 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.
|
|
260
|
+
const shim = shimCommand(cmd, args);
|
|
261
|
+
execFile(shim.cmd, shim.args, { signal: opts?.signal, timeout: opts?.timeout, windowsVerbatimArguments: process.platform === 'win32' }, (error, stdout) => {
|
|
262
|
+
const out = stdout ?? '';
|
|
263
|
+
if (!error) { resolve({ stdout: out, code: 0 }); return; }
|
|
264
|
+
// A non-numeric code is a spawn failure (ENOENT, EACCES, killed) and must not read as success.
|
|
265
|
+
const raw = (error as { code?: unknown }).code;
|
|
266
|
+
resolve({ stdout: out, code: typeof raw === 'number' ? raw : 1 });
|
|
261
267
|
});
|
|
268
|
+
return promise;
|
|
262
269
|
}
|
|
263
270
|
|
|
264
271
|
/** `rtk rewrite` exit codes: 0 rewrite found, 1 no equivalent, 3 advisory; the code is the contract. */
|
|
@@ -309,20 +316,21 @@ function bundledSkills(): ReturnType<typeof parseSkillFile>[] {
|
|
|
309
316
|
function rewriteCommand(command: string): Promise<string | null> {
|
|
310
317
|
const bin = resolveRtkBinary();
|
|
311
318
|
if (!bin) return Promise.resolve(null);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
319
|
+
const { promise, resolve } = Promise.withResolvers<string | null>();
|
|
320
|
+
// A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
|
|
321
|
+
const shim = shimCommand(bin, ['rewrite', command]);
|
|
322
|
+
execFile(shim.cmd, shim.args, { timeout: 5000 }, (error, stdout) => {
|
|
323
|
+
// A kill or timeout means untrustworthy output; pass the command through.
|
|
324
|
+
const err = error as { killed?: boolean; signal?: unknown; code?: unknown } | null;
|
|
325
|
+
if (err && (err.killed || err.signal)) { resolve(null); return; }
|
|
326
|
+
const code = typeof err?.code === 'number' ? err.code : RTK_REWRITE_FOUND;
|
|
327
|
+
// Exit 1 is the only "no equivalent" answer, so anything else with output rewrites.
|
|
328
|
+
if (code === 1) { resolve(null); return; }
|
|
329
|
+
const out = String(stdout).trim();
|
|
330
|
+
if (!out || out === command || (code !== RTK_REWRITE_FOUND && code !== RTK_REWRITE_ADVISORY)) { resolve(null); return; }
|
|
331
|
+
resolve(out);
|
|
325
332
|
});
|
|
333
|
+
return promise;
|
|
326
334
|
}
|
|
327
335
|
|
|
328
336
|
export default plugin;
|
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { sqliteRead } from './sqlite.ts';
|
|
6
|
+
import { homeDir } from './utils.ts';
|
|
6
7
|
import { usageDbPath } from './usage-store.ts';
|
|
7
8
|
|
|
8
9
|
export type RangeKey = '1h' | '24h' | '7d' | '30d' | '90d' | 'all';
|
|
@@ -83,7 +84,7 @@ export function statsDbPath(): string {
|
|
|
83
84
|
|
|
84
85
|
/** Quota snapshots. Only omp records these, so this stays omp's own database. */
|
|
85
86
|
export function agentDbPath(): string {
|
|
86
|
-
return process.env.TERSIO_AGENT_SESSIONS_DB ?? path.join(
|
|
87
|
+
return process.env.TERSIO_AGENT_SESSIONS_DB ?? path.join(homeDir(), '.omp', 'agent', 'agent.db');
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
/** The row shape every query groups by: a message joined to its file, since path and facts live apart. */
|
|
@@ -117,14 +118,19 @@ function providerExpr(model: string): string {
|
|
|
117
118
|
return `CASE WHEN instr(${model}, '/') = 0 THEN '' ELSE CASE ${named} ${cases} ELSE ${named} END END`;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
/** SQL: the project a transcript
|
|
121
|
+
/** SQL: the project from a transcript's session folder; the separator follows the stored path, since a Windows run stores `\`. */
|
|
121
122
|
function projectExpr(file: string): string {
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
const
|
|
123
|
+
// Position of the session marker under either separator; 0 when the path has none.
|
|
124
|
+
const posAt = `instr(${file}, '/sessions/')`;
|
|
125
|
+
const posWin = `instr(${file}, '\\sessions\\')`;
|
|
126
|
+
const at = `CASE WHEN ${posAt} > 0 THEN ${posAt} ELSE ${posWin} END`;
|
|
127
|
+
// Which separator to slice on: the one the matched marker used, else the one the path uses.
|
|
128
|
+
const sep = `CASE WHEN ${posAt} > 0 THEN '/' WHEN ${posWin} > 0 THEN '\\' WHEN instr(${file}, '\\') > 0 THEN '\\' ELSE '/' END`;
|
|
129
|
+
const seg = `substr(${file}, ${at} + 10, instr(substr(${file}, ${at} + 10) || ${sep}, ${sep}) - 1)`;
|
|
130
|
+
const base = `replace(${file}, rtrim(${file}, replace(${file}, ${sep}, '')), '')`;
|
|
125
131
|
const dir = `substr(${file}, 1, length(${file}) - length(${base}) - 1)`;
|
|
126
|
-
const dirBase = `replace(${dir}, rtrim(${dir}, replace(${dir},
|
|
127
|
-
return `CASE WHEN ${
|
|
132
|
+
const dirBase = `replace(${dir}, rtrim(${dir}, replace(${dir}, ${sep}, '')), '')`;
|
|
133
|
+
return `CASE WHEN ${at} > 0 THEN ${seg} ELSE ${dirBase} END`;
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
/** Every column a query reads, mapped to the store's schema in one place. */
|
|
@@ -825,16 +831,18 @@ function foldTraces(rows: AgentTraceRow[]): AgentTraceRow[] {
|
|
|
825
831
|
.toSorted((a, b) => b.lastTs - a.lastTs);
|
|
826
832
|
}
|
|
827
833
|
|
|
828
|
-
/** The root session file for a transcript, which is itself when it is already a root.
|
|
834
|
+
/** The root session file for a transcript, which is itself when it is already a root.
|
|
835
|
+
* Splits on whichever separator the stored path uses: a Windows run writes `\`. */
|
|
829
836
|
export function rootSessionFile(sessionFile: string): string {
|
|
830
|
-
const marker = '/sessions/';
|
|
837
|
+
const marker = sessionFile.includes('/sessions/') ? '/sessions/' : '\\sessions\\';
|
|
831
838
|
const at = sessionFile.indexOf(marker);
|
|
832
839
|
if (at < 0) return sessionFile;
|
|
833
840
|
const base = sessionFile.slice(0, at + marker.length);
|
|
834
|
-
const parts = sessionFile.slice(at + marker.length).split(
|
|
841
|
+
const parts = sessionFile.slice(at + marker.length).split(marker[0]);
|
|
835
842
|
// `<project>/<root>.jsonl` is a root; `<project>/<root>/<child>.jsonl` belongs to that root.
|
|
836
843
|
if (parts.length < 3) return sessionFile;
|
|
837
|
-
|
|
844
|
+
// Rejoined with the same separator, so a child's root matches its root file exactly.
|
|
845
|
+
return `${base}${parts[0]}${marker[0]}${parts[1]}.jsonl`;
|
|
838
846
|
}
|
|
839
847
|
|
|
840
848
|
/** Same normalization as the reference: ids and counters collapse, an HTTP status does not. */
|
|
@@ -1304,7 +1312,8 @@ const AGENT_TOOLS = new Set(['task', 'subagent']);
|
|
|
1304
1312
|
|
|
1305
1313
|
/** One transcript as spans, tracks and markers: a model span runs `ts - duration` to `ts`. */
|
|
1306
1314
|
export function readSessionTrace(sessionFile: string, limit = TRACE_SPAN_LIMIT): AgentSessionTrace {
|
|
1307
|
-
|
|
1315
|
+
// Either separator: a Windows host writes `<drive>\...\.omp\agent\sessions\<project>\<file>`.
|
|
1316
|
+
const project = sessionFile.split(/[/\\]sessions[/\\]/)[1]?.split(/[/\\]/)[0] ?? '';
|
|
1308
1317
|
const empty: AgentSessionTrace = {
|
|
1309
1318
|
sessionFile, project, title: '', cwd: project, startedAt: 0, endedAt: 0,
|
|
1310
1319
|
tracks: [], markers: [], summary: emptyTraceSummary(), toolStats: [], truncated: false,
|
|
@@ -105,16 +105,18 @@ function asPrice(v: unknown): ModelPrice | null {
|
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
let liveCache: { path: string; mtime: number; prices: LivePrices } | null = null;
|
|
108
|
+
let liveCache: { path: string; mtime: number; size: number; prices: LivePrices } | null = null;
|
|
109
109
|
|
|
110
110
|
export function loadLivePrices(): LivePrices | null {
|
|
111
|
-
// One stat per call beats
|
|
111
|
+
// 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.
|
|
112
112
|
try {
|
|
113
113
|
const file = pricesCachePath();
|
|
114
|
-
const
|
|
115
|
-
if (liveCache && liveCache.path === file && liveCache.mtime ===
|
|
114
|
+
const stat = fs.statSync(file);
|
|
115
|
+
if (liveCache && liveCache.path === file && liveCache.mtime === stat.mtimeMs && liveCache.size === stat.size) {
|
|
116
|
+
return liveCache.prices;
|
|
117
|
+
}
|
|
116
118
|
const prices = readLivePrices();
|
|
117
|
-
if (prices) liveCache = { path: file, mtime, prices };
|
|
119
|
+
if (prices) liveCache = { path: file, mtime: stat.mtimeMs, size: stat.size, prices };
|
|
118
120
|
return prices;
|
|
119
121
|
} catch {
|
|
120
122
|
return null;
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
// Eligible
|
|
2
|
-
// `rtk rewrite <probe>` prints a rewrite and exits 0 or 3 when rtk handles the command,
|
|
3
|
-
// and exits 1 when no equivalent exists. Each probe below is a representative workload
|
|
4
|
-
// command, because subcommand-scoped tools such as `gh` rewrite `gh pr list` but not `gh`.
|
|
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.
|
|
5
2
|
import { execFileSync } from 'node:child_process';
|
|
6
3
|
import fs from 'node:fs';
|
|
7
4
|
import path from 'node:path';
|
|
8
|
-
import { normalizeRtkVersion, tersioDataPath } from './utils.ts';
|
|
5
|
+
import { normalizeRtkVersion, shimCommand, tersioDataPath } from './utils.ts';
|
|
9
6
|
|
|
10
7
|
/** Verified against rtk 1.0.1; the drift test fails when rtk's set moves. */
|
|
11
8
|
export const FALLBACK_ELIGIBLE_HEADS: readonly string[] = [
|
|
@@ -33,7 +30,8 @@ const PROBES: Record<string, string> = {
|
|
|
33
30
|
|
|
34
31
|
function probe(bin: string, command: string): boolean {
|
|
35
32
|
try {
|
|
36
|
-
const
|
|
33
|
+
const shim = shimCommand(bin, ['rewrite', command]);
|
|
34
|
+
const out = execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
37
35
|
return out.trim().length > 0;
|
|
38
36
|
} catch (e) {
|
|
39
37
|
// Non-zero exit throws; exit 3 still prints a rewrite, exits 1 and 2 do not.
|
|
@@ -54,16 +52,14 @@ export function deriveEligibleHeads(bin: string): Set<string> {
|
|
|
54
52
|
|
|
55
53
|
function rtkVersion(bin: string): string {
|
|
56
54
|
try {
|
|
57
|
-
|
|
55
|
+
const shim = shimCommand(bin, ['--version']);
|
|
56
|
+
return normalizeRtkVersion(execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true }));
|
|
58
57
|
} catch {
|
|
59
58
|
return '';
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
/**
|
|
64
|
-
* Heads rtk rewrites, cached per binary version so an upgrade re-derives once.
|
|
65
|
-
* Without a binary, or on any read failure, the verified fallback stands.
|
|
66
|
-
*/
|
|
62
|
+
/** Heads rtk rewrites, cached per binary version; without a binary, or on any read failure, the verified fallback stands. */
|
|
67
63
|
export function eligibleHeads(bin: string | null): ReadonlySet<string> {
|
|
68
64
|
if (!bin) return new Set(FALLBACK_ELIGIBLE_HEADS);
|
|
69
65
|
const version = rtkVersion(bin);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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.ts';
|
|
5
5
|
import { sqliteRead } from './sqlite.ts';
|
|
6
6
|
|
|
7
7
|
export interface RtkCommandRow {
|
|
@@ -32,11 +32,7 @@ const CMD_ONE_LINE = `replace(replace(replace(rtk_cmd, char(10), ' '), char(13),
|
|
|
32
32
|
export function rtkDbPath(): string {
|
|
33
33
|
const override = process.env.TERSIO_RTK_DB;
|
|
34
34
|
if (override) return override;
|
|
35
|
-
|
|
36
|
-
return path.join(os.homedir(), 'Library', 'Application Support', 'rtk', 'history.db');
|
|
37
|
-
}
|
|
38
|
-
const xdg = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
|
39
|
-
return path.join(xdg, 'rtk', 'history.db');
|
|
35
|
+
return path.join(rtkDirs().data, 'history.db');
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
function query(db: string, sql: string): string[][] {
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
// One place that runs SQL
|
|
2
|
-
// `sqlite3` binary is a fallback: hosts without it kept silently reading nothing. Reads return the sqlite3
|
|
3
|
-
// CLI shape (one row per line, cells tab-joined, NULL an empty cell) because every caller parses that.
|
|
4
|
-
// The write path leaves the journal mode alone: the schema sets WAL so a reader never blocks a writer,
|
|
5
|
-
// and the pragma needs exclusive access, so with a reader holding the file it fails after the caller's
|
|
6
|
-
// rows have already committed (see docs/ERRORS.md).
|
|
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).
|
|
7
2
|
import { execFileSync } from 'node:child_process';
|
|
8
3
|
import fs from 'node:fs';
|
|
9
4
|
import { createRequire } from 'node:module';
|