@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from './utils.ts';
|
|
5
|
+
import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, rtkDirs, shimCommand, tersioDataPath } from './utils.ts';
|
|
6
6
|
import { eligibleHeads } from './rtk-eligible.ts';
|
|
7
7
|
import { sqliteRead } from './sqlite.ts';
|
|
8
8
|
|
|
@@ -105,6 +105,14 @@ export interface SessionTokens {
|
|
|
105
105
|
recent: RecentRequest[];
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export interface RtkHostAdoption {
|
|
109
|
+
bashCalls: number;
|
|
110
|
+
eligibleCalls: number;
|
|
111
|
+
rtkCalls: number;
|
|
112
|
+
missedCalls: number;
|
|
113
|
+
adoptionPct: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
108
116
|
export interface RtkAdoption {
|
|
109
117
|
sessions: number;
|
|
110
118
|
bashCalls: number;
|
|
@@ -112,6 +120,8 @@ export interface RtkAdoption {
|
|
|
112
120
|
rtkCalls: number;
|
|
113
121
|
missedCalls: number;
|
|
114
122
|
adoptionPct: number;
|
|
123
|
+
/** One entry per host that recorded bash calls, so a blended number cannot hide a host. */
|
|
124
|
+
byHost: Record<string, RtkHostAdoption>;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
export interface RtkRecallDiagnostics {
|
|
@@ -708,6 +718,13 @@ function countBashCommands(commands: readonly string[], eligible: ReadonlySet<st
|
|
|
708
718
|
}
|
|
709
719
|
}
|
|
710
720
|
|
|
721
|
+
interface PiToolCall {
|
|
722
|
+
type?: unknown;
|
|
723
|
+
name?: unknown;
|
|
724
|
+
id?: unknown;
|
|
725
|
+
arguments?: unknown;
|
|
726
|
+
}
|
|
727
|
+
|
|
711
728
|
function parseAdoptionFile(file: string, eligible: ReadonlySet<string>): { counts: BashCounts; sessions: number } {
|
|
712
729
|
const counts: BashCounts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
|
|
713
730
|
let text: string;
|
|
@@ -716,20 +733,45 @@ function parseAdoptionFile(file: string, eligible: ReadonlySet<string>): { count
|
|
|
716
733
|
} catch {
|
|
717
734
|
return { counts, sessions: 0 };
|
|
718
735
|
}
|
|
736
|
+
// omp emits a custom row per tool start; pi records the same call inside the assistant message.
|
|
737
|
+
// A host that writes both shapes would double every call, so the custom ids gate the message pass.
|
|
719
738
|
const commands: string[] = [];
|
|
739
|
+
const customIds = new Set<string>();
|
|
740
|
+
const messageCalls: Array<{ id: string | undefined; command: string }> = [];
|
|
720
741
|
for (const line of text.split('\n')) {
|
|
721
|
-
if (!line.includes('tool_execution_start')) continue;
|
|
742
|
+
if (!line.includes('tool_execution_start') && !line.includes('"toolCall"')) continue;
|
|
722
743
|
try {
|
|
723
744
|
const row = JSON.parse(line) as {
|
|
724
|
-
type?:
|
|
725
|
-
customType?:
|
|
726
|
-
data?: { toolName?: unknown; args?: { command?: unknown } };
|
|
745
|
+
type?: unknown;
|
|
746
|
+
customType?: unknown;
|
|
747
|
+
data?: { toolCallId?: unknown; toolName?: unknown; args?: { command?: unknown } };
|
|
748
|
+
message?: { content?: unknown };
|
|
727
749
|
};
|
|
728
|
-
if (row.type
|
|
729
|
-
|
|
730
|
-
|
|
750
|
+
if (row.type === 'custom' && row.customType === 'tool_execution_start' && row.data?.toolName === 'bash') {
|
|
751
|
+
const command = row.data.args?.command;
|
|
752
|
+
if (typeof command !== 'string') continue;
|
|
753
|
+
if (typeof row.data.toolCallId === 'string') customIds.add(row.data.toolCallId);
|
|
754
|
+
commands.push(command);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const content = row.message?.content;
|
|
758
|
+
if (row.type !== 'message' || !Array.isArray(content)) continue;
|
|
759
|
+
for (const part of content) {
|
|
760
|
+
if (typeof part !== 'object' || part === null) continue;
|
|
761
|
+
const call: PiToolCall = part;
|
|
762
|
+
if (call.type !== 'toolCall' || call.name !== 'bash') continue;
|
|
763
|
+
const args = call.arguments;
|
|
764
|
+
if (typeof args !== 'object' || args === null || !('command' in args)) continue;
|
|
765
|
+
const command = args.command;
|
|
766
|
+
if (typeof command !== 'string') continue;
|
|
767
|
+
messageCalls.push({ id: typeof call.id === 'string' ? call.id : undefined, command });
|
|
768
|
+
}
|
|
731
769
|
} catch { /* skip corrupt line */ }
|
|
732
770
|
}
|
|
771
|
+
for (const call of messageCalls) {
|
|
772
|
+
if (call.id !== undefined && customIds.has(call.id)) continue;
|
|
773
|
+
commands.push(call.command);
|
|
774
|
+
}
|
|
733
775
|
countBashCommands(commands, eligible, counts);
|
|
734
776
|
return { counts, sessions: commands.length > 0 ? 1 : 0 };
|
|
735
777
|
}
|
|
@@ -738,6 +780,24 @@ export function clearRtkAdoptionCache(): void {
|
|
|
738
780
|
adoptionFileCache.clear();
|
|
739
781
|
}
|
|
740
782
|
|
|
783
|
+
/** Rewrites the hook performed on hosts whose transcript records the pre-hook command, by host. */
|
|
784
|
+
function countRtkAuditRows(): Record<string, number> {
|
|
785
|
+
const byHost: Record<string, number> = {};
|
|
786
|
+
let text: string;
|
|
787
|
+
try { text = fs.readFileSync(ledgerPath(), 'utf8'); } catch { return byHost; }
|
|
788
|
+
for (const line of text.split('\n')) {
|
|
789
|
+
if (!line.includes('rtk-audit')) continue;
|
|
790
|
+
try {
|
|
791
|
+
const row = JSON.parse(line) as { kind?: unknown; detail?: unknown };
|
|
792
|
+
if (row.kind !== 'rtk-audit' || typeof row.detail !== 'string') continue;
|
|
793
|
+
const host = row.detail.split(' ')[0];
|
|
794
|
+
if (!host) continue;
|
|
795
|
+
byHost[host] = (byHost[host] ?? 0) + 1;
|
|
796
|
+
} catch { /* skip corrupt line */ }
|
|
797
|
+
}
|
|
798
|
+
return byHost;
|
|
799
|
+
}
|
|
800
|
+
|
|
741
801
|
export function readRtkAdoption(): RtkAdoption {
|
|
742
802
|
// The eligible set comes from rtk's rewrite contract, cached per binary version.
|
|
743
803
|
const eligible = eligibleHeads(resolveRtkBinary());
|
|
@@ -753,6 +813,14 @@ export function readRtkAdoption(): RtkAdoption {
|
|
|
753
813
|
let eligibleCalls = 0;
|
|
754
814
|
let rtkCalls = 0;
|
|
755
815
|
let sessions = 0;
|
|
816
|
+
const byHost: Record<string, BashCounts> = {};
|
|
817
|
+
const hostCounts = (host: string, counts: BashCounts): void => {
|
|
818
|
+
const into = byHost[host] ?? { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
|
|
819
|
+
into.bashCalls += counts.bashCalls;
|
|
820
|
+
into.eligibleCalls += counts.eligibleCalls;
|
|
821
|
+
into.rtkCalls += counts.rtkCalls;
|
|
822
|
+
byHost[host] = into;
|
|
823
|
+
};
|
|
756
824
|
for (const file of files) {
|
|
757
825
|
let stat: fs.Stats;
|
|
758
826
|
try {
|
|
@@ -772,6 +840,7 @@ export function readRtkAdoption(): RtkAdoption {
|
|
|
772
840
|
bashCalls += entry.counts.bashCalls;
|
|
773
841
|
eligibleCalls += entry.counts.eligibleCalls;
|
|
774
842
|
rtkCalls += entry.counts.rtkCalls;
|
|
843
|
+
hostCounts(hostOfSessionFile(file), entry.counts);
|
|
775
844
|
}
|
|
776
845
|
// OpenCode keeps its own store: a v2 sqlite db, or one JSON file per message before v2.
|
|
777
846
|
const opencode: BashCounts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
|
|
@@ -795,8 +864,29 @@ export function readRtkAdoption(): RtkAdoption {
|
|
|
795
864
|
eligibleCalls += opencode.eligibleCalls;
|
|
796
865
|
rtkCalls += opencode.rtkCalls;
|
|
797
866
|
sessions += opencodeSessions.size;
|
|
867
|
+
if (opencode.bashCalls > 0) hostCounts('opencode', opencode);
|
|
868
|
+
// The hook audit carries rewrites on pi and opencode, whose transcripts record the pre-hook command.
|
|
869
|
+
for (const [host, rows] of Object.entries(countRtkAuditRows())) {
|
|
870
|
+
rtkCalls += rows;
|
|
871
|
+
hostCounts(host, { bashCalls: 0, eligibleCalls: 0, rtkCalls: rows });
|
|
872
|
+
}
|
|
798
873
|
const missedCalls = Math.max(0, eligibleCalls - rtkCalls);
|
|
799
|
-
|
|
874
|
+
const host = (counts: BashCounts): RtkHostAdoption => ({
|
|
875
|
+
bashCalls: counts.bashCalls,
|
|
876
|
+
eligibleCalls: counts.eligibleCalls,
|
|
877
|
+
rtkCalls: counts.rtkCalls,
|
|
878
|
+
missedCalls: Math.max(0, counts.eligibleCalls - counts.rtkCalls),
|
|
879
|
+
adoptionPct: counts.eligibleCalls ? (counts.rtkCalls / counts.eligibleCalls) * 100 : 0,
|
|
880
|
+
});
|
|
881
|
+
return {
|
|
882
|
+
sessions,
|
|
883
|
+
bashCalls,
|
|
884
|
+
eligibleCalls,
|
|
885
|
+
rtkCalls,
|
|
886
|
+
missedCalls,
|
|
887
|
+
adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0,
|
|
888
|
+
byHost: Object.fromEntries(Object.entries(byHost).map(([name, counts]) => [name, host(counts)])),
|
|
889
|
+
};
|
|
800
890
|
}
|
|
801
891
|
export function processSessionText(accum: SessionAccum, state: { codexProvider: string | null; codexModel?: string | null }, text: string, host?: string, agent?: string): void {
|
|
802
892
|
for (const line of text.split('\n')) {
|
|
@@ -829,15 +919,13 @@ export function processSessionText(accum: SessionAccum, state: { codexProvider:
|
|
|
829
919
|
export function readRtkRecallDiagnostics(binary: string | null = resolveRtkBinary()): RtkRecallDiagnostics {
|
|
830
920
|
if (!binary) return { mode: 'unknown', entries: 0, available: false };
|
|
831
921
|
try {
|
|
832
|
-
const configPath = process.env.RTK_CONFIG
|
|
833
|
-
|| (process.platform === 'darwin'
|
|
834
|
-
? path.join(homeDir(), 'Library', 'Application Support', 'rtk', 'config.toml')
|
|
835
|
-
: path.join(process.env.XDG_DATA_HOME || path.join(homeDir(), '.local', 'share'), 'rtk', 'config.toml'));
|
|
922
|
+
const configPath = process.env.RTK_CONFIG || path.join(rtkDirs().config, 'config.toml');
|
|
836
923
|
let config = '';
|
|
837
924
|
try { config = fs.readFileSync(configPath, 'utf8'); } catch { /* use RTK CLI fallback */ }
|
|
838
925
|
const modeMatch = /^\s*mode\s*=\s*"(sqlite|tee|disabled)"/m.exec(config);
|
|
839
926
|
const mode = (modeMatch?.[1] ?? 'unknown') as RtkRecallDiagnostics['mode'];
|
|
840
|
-
const
|
|
927
|
+
const shim = shimCommand(binary, ['recall', '--list']);
|
|
928
|
+
const output = execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
841
929
|
const entries = /\((\d+)\s+(?:entries?|stored)\)/i.exec(output)?.[1] ?? /^\s*(\d+)\s+entries?/im.exec(output)?.[1] ?? '0';
|
|
842
930
|
return { mode, entries: Number(entries) || 0, available: true };
|
|
843
931
|
} catch {
|
|
@@ -26,9 +26,32 @@ export function homeDir(): string {
|
|
|
26
26
|
return home && home.trim() !== '' ? home : os.homedir();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Windows cannot run a `.cmd` or `.bat` directly, so a shim path goes through the shell; a real `.exe` or POSIX binary passes through. */
|
|
30
|
+
export function shimCommand(cmd: string, args: string[]): { cmd: string; args: string[] } {
|
|
31
|
+
if (process.platform !== 'win32' || !/\.(?:cmd|bat)$/i.test(cmd)) return { cmd, args };
|
|
32
|
+
return { cmd: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', cmd, ...args] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** rtk's own dirs: `config_dir()` for config.toml and `data_local_dir()` for history.db, which differ per platform; a guess here empties the Gain charts. */
|
|
36
|
+
export function rtkDirs(): { data: string; config: string } {
|
|
37
|
+
if (process.platform === 'darwin') {
|
|
38
|
+
const base = path.join(homeDir(), 'Library', 'Application Support', 'rtk');
|
|
39
|
+
return { data: base, config: base };
|
|
40
|
+
}
|
|
41
|
+
if (process.platform === 'win32') {
|
|
42
|
+
const local = process.env.LOCALAPPDATA || path.join(homeDir(), 'AppData', 'Local');
|
|
43
|
+
const roaming = process.env.APPDATA || path.join(homeDir(), 'AppData', 'Roaming');
|
|
44
|
+
return { data: path.join(local, 'rtk'), config: path.join(roaming, 'rtk') };
|
|
45
|
+
}
|
|
46
|
+
const xdgData = process.env.XDG_DATA_HOME || path.join(homeDir(), '.local', 'share');
|
|
47
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(homeDir(), '.config');
|
|
48
|
+
return { data: path.join(xdgData, 'rtk'), config: path.join(xdgConfig, 'rtk') };
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
|
|
30
52
|
export const CAVEMAN_REMOTE_ULTRA = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/ultracave/SKILL.md';
|
|
31
53
|
export const CAVEMAN_REMOTE_MEGACAVE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/megacave/SKILL.md';
|
|
54
|
+
export const CAVEMAN_REMOTE_FULL = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/caveman/SKILL.md';
|
|
32
55
|
|
|
33
56
|
// A fetch that returns an error page or a truncated body must not reach the extension tree.
|
|
34
57
|
export function ruleBodyProblem(text: string): string | null {
|
|
@@ -104,6 +127,12 @@ export function findPackageRoot(fromDir: string): string {
|
|
|
104
127
|
}
|
|
105
128
|
}
|
|
106
129
|
|
|
130
|
+
/** Where a mode's rule bodies sit: beside the module, else the package's own `extensions/<name>`. */
|
|
131
|
+
export function ruleDir(name: string, ownDir: string): string {
|
|
132
|
+
if (existsSync(path.join(ownDir, 'rule.md'))) return ownDir;
|
|
133
|
+
try { return path.join(findPackageRoot(ownDir), 'extensions', name); } catch { return ownDir; }
|
|
134
|
+
}
|
|
135
|
+
|
|
107
136
|
// The 3xx+location redirect rule shared by httpsGet/httpsDownload.
|
|
108
137
|
function redirectNext(res: { statusCode?: number; headers: { location?: string } }, url: string): string | null {
|
|
109
138
|
if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
@@ -12,10 +12,12 @@ import { setExtensionLabel } from '../shared/host.ts';
|
|
|
12
12
|
import {
|
|
13
13
|
CAVEMAN_REMOTE_ULTRA,
|
|
14
14
|
CAVEMAN_REMOTE_MEGACAVE,
|
|
15
|
+
CAVEMAN_REMOTE_FULL,
|
|
15
16
|
RTK_RELEASE_API,
|
|
16
17
|
fetchJson,
|
|
17
18
|
findFile,
|
|
18
19
|
findHoistedPackage,
|
|
20
|
+
homeDir,
|
|
19
21
|
httpsGet,
|
|
20
22
|
httpsDownload,
|
|
21
23
|
sha256Hex,
|
|
@@ -38,15 +40,20 @@ const CAVEMAN_SIBLING = (name: string): string => path.join(EXTENSION_DIR, '..',
|
|
|
38
40
|
function cavemanRule(name: string): string {
|
|
39
41
|
const beside = CAVEMAN_SIBLING(name);
|
|
40
42
|
if (fsSync.existsSync(beside)) return beside;
|
|
41
|
-
const plugin = path.join(
|
|
43
|
+
const plugin = path.join(homeDir(), '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman', name);
|
|
42
44
|
return fsSync.existsSync(plugin) ? plugin : beside;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
const CAVEMAN_LOCAL = cavemanRule('rule.md');
|
|
46
|
-
|
|
48
|
+
// `rule.md` is upstream's body with one adapted line, so a refresh re-applies the adaptation.
|
|
49
|
+
const adaptCavemanFull = (text: string): string =>
|
|
50
|
+
text.replace('`/caveman-compress` exempt.', '`/caveman-compress`, where the host offers it, is exempt.');
|
|
51
|
+
const CAVEMAN_TRACKED: ReadonlyArray<{ file: string; remote: string; adapt?: (text: string) => string }> = [
|
|
52
|
+
{ file: CAVEMAN_LOCAL, remote: CAVEMAN_REMOTE_FULL, adapt: adaptCavemanFull },
|
|
47
53
|
{ file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
|
|
48
54
|
{ file: cavemanRule('rule-megacave.md'), remote: CAVEMAN_REMOTE_MEGACAVE },
|
|
49
55
|
];
|
|
56
|
+
export { adaptCavemanFull };
|
|
50
57
|
// Host-neutral: this updater runs inside pi, OMP, and OpenCode alike.
|
|
51
58
|
const RELOAD_MSG = 'Reminder: restart your agent (or reload extensions) for updates to take effect.';
|
|
52
59
|
|
|
@@ -144,11 +151,13 @@ function checkRtk(): Promise<AddonStatus> {
|
|
|
144
151
|
function checkCaveman(): Promise<AddonStatus> {
|
|
145
152
|
return runCheck('Caveman', async () => {
|
|
146
153
|
const parts: string[] = [(await readTextIfExists(CAVEMAN_LOCAL)) ? 'rule.md ok' : 'rule.md missing'];
|
|
147
|
-
for (const { file, remote } of CAVEMAN_TRACKED) {
|
|
154
|
+
for (const { file, remote, adapt } of CAVEMAN_TRACKED) {
|
|
148
155
|
const name = path.basename(file);
|
|
149
156
|
try {
|
|
150
157
|
const [remoteText, localText] = await Promise.all([httpsGet(remote), readTextIfExists(file)]);
|
|
151
|
-
|
|
158
|
+
// Compare the text a refresh would write, so the adapted line never reads as drift.
|
|
159
|
+
const nextHash = shortHash(adapt ? adapt(remoteText) : remoteText);
|
|
160
|
+
parts.push(!localText ? `${name} missing` : shortHash(localText) === nextHash ? `${name} up to date` : `${name} update available`);
|
|
152
161
|
} catch {
|
|
153
162
|
parts.push(`${name} check skipped`);
|
|
154
163
|
}
|
|
@@ -366,13 +375,15 @@ async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string>
|
|
|
366
375
|
|
|
367
376
|
async function updateCaveman(ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
|
|
368
377
|
const results: string[] = [];
|
|
369
|
-
for (const { file, remote } of CAVEMAN_TRACKED) {
|
|
378
|
+
for (const { file, remote, adapt } of CAVEMAN_TRACKED) {
|
|
370
379
|
const name = path.basename(file);
|
|
371
380
|
let remoteText: string;
|
|
372
381
|
try { remoteText = await httpsGet(remote); }
|
|
373
382
|
catch (e) { results.push(report(ctx, `Caveman ${name} update failed: ${(e as Error).message}`, 'warning')); continue; }
|
|
374
383
|
|
|
375
|
-
|
|
384
|
+
// A tracked fork compares against the text it would write, so an adapted body still reads up to date.
|
|
385
|
+
const nextText = adapt ? adapt(remoteText) : remoteText;
|
|
386
|
+
const remoteHash = shortHash(nextText);
|
|
376
387
|
const oldLocal = await readTextIfExists(file);
|
|
377
388
|
const oldHash = oldLocal ? shortHash(oldLocal) : null;
|
|
378
389
|
if (oldHash === remoteHash) { results.push(`${name} up to date: ${remoteHash}`); continue; }
|
|
@@ -386,7 +397,7 @@ async function updateCaveman(ctx: AddonUpdaterCtx, dryRun = false): Promise<stri
|
|
|
386
397
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
387
398
|
const backupPath = `${file}.bak`;
|
|
388
399
|
if (oldLocal !== null) await fs.writeFile(backupPath, oldLocal, 'utf8');
|
|
389
|
-
await fs.writeFile(file,
|
|
400
|
+
await fs.writeFile(file, nextText, 'utf8');
|
|
390
401
|
const writtenHash = shortHash(await fs.readFile(file, 'utf8'));
|
|
391
402
|
if (writtenHash !== remoteHash) {
|
|
392
403
|
if (oldLocal !== null) await fs.writeFile(file, oldLocal, 'utf8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krtclcdy/tersio",
|
|
3
|
-
"version": "2.25.
|
|
3
|
+
"version": "2.25.8",
|
|
4
4
|
"description": "✂️ Token saver and usage tracker for coding agents: terse Caveman replies, compact RTK shell output, lean Ponytail code, pstack rigor, and a gain dashboard — one-command combos over one shared extension tree.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"token-saving",
|
|
@@ -123,8 +123,6 @@
|
|
|
123
123
|
},
|
|
124
124
|
"files": [
|
|
125
125
|
"dist/**/*.js",
|
|
126
|
-
"dist/extensions/caveman/*.md",
|
|
127
|
-
"dist/extensions/pstack/*.md",
|
|
128
126
|
"dashboard/dist/**/*",
|
|
129
127
|
"extensions/**/*.ts",
|
|
130
128
|
"extensions/caveman/rule.md",
|
|
@@ -136,8 +134,8 @@
|
|
|
136
134
|
"extensions/pstack/**/*.md"
|
|
137
135
|
],
|
|
138
136
|
"scripts": {
|
|
139
|
-
"clean": "
|
|
140
|
-
"build": "tsc && bun run build:dashboard
|
|
137
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
138
|
+
"build": "tsc && bun run build:dashboard",
|
|
141
139
|
"build:dashboard": "bun install --cwd dashboard/app --frozen-lockfile && bun run --cwd dashboard/app build",
|
|
142
140
|
"prepublishOnly": "bun run build",
|
|
143
141
|
"check": "tsc --noEmit",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// Debounce: fires fn once, ms after the last call, using that last call's arguments. A call during the wait restarts it.
|
|
2
|
-
export function debounce(fn, ms) {
|
|
3
|
-
let timer;
|
|
4
|
-
return (...args) => {
|
|
5
|
-
if (timer !== undefined) {
|
|
6
|
-
clearTimeout(timer);
|
|
7
|
-
}
|
|
8
|
-
timer = setTimeout(() => {
|
|
9
|
-
timer = undefined;
|
|
10
|
-
fn(...args);
|
|
11
|
-
}, ms);
|
|
12
|
-
};
|
|
13
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
|
|
2
|
-
export function deepClone(value) {
|
|
3
|
-
if (value === null || typeof value !== 'object') {
|
|
4
|
-
return value;
|
|
5
|
-
}
|
|
6
|
-
if (value instanceof Date) {
|
|
7
|
-
return new Date(value.getTime());
|
|
8
|
-
}
|
|
9
|
-
if (Array.isArray(value)) {
|
|
10
|
-
return value.map((item) => deepClone(item));
|
|
11
|
-
}
|
|
12
|
-
const source = value;
|
|
13
|
-
const clone = {};
|
|
14
|
-
for (const key of Object.keys(source)) {
|
|
15
|
-
clone[key] = deepClone(source[key]);
|
|
16
|
-
}
|
|
17
|
-
return clone;
|
|
18
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
|
|
2
|
-
export function groupBy(items, keyFn) {
|
|
3
|
-
const groups = {};
|
|
4
|
-
for (const item of items) {
|
|
5
|
-
const key = keyFn(item);
|
|
6
|
-
const bucket = groups[key];
|
|
7
|
-
if (bucket === undefined) {
|
|
8
|
-
groups[key] = [item];
|
|
9
|
-
}
|
|
10
|
-
else {
|
|
11
|
-
bucket.push(item);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return groups;
|
|
15
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export function debounce(fn, ms) {
|
|
2
|
-
let timer;
|
|
3
|
-
return (...args) => {
|
|
4
|
-
if (timer !== undefined) {
|
|
5
|
-
clearTimeout(timer);
|
|
6
|
-
}
|
|
7
|
-
// The closure rebinds per call, so only the last call's arguments fire.
|
|
8
|
-
timer = setTimeout(() => {
|
|
9
|
-
timer = undefined;
|
|
10
|
-
fn(...args);
|
|
11
|
-
}, ms);
|
|
12
|
-
};
|
|
13
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export function deepClone(value) {
|
|
2
|
-
if (value === null || typeof value !== 'object') {
|
|
3
|
-
return value;
|
|
4
|
-
}
|
|
5
|
-
if (value instanceof Date) {
|
|
6
|
-
return new Date(value.getTime());
|
|
7
|
-
}
|
|
8
|
-
if (Array.isArray(value)) {
|
|
9
|
-
return value.map((item) => deepClone(item));
|
|
10
|
-
}
|
|
11
|
-
const source = value;
|
|
12
|
-
const clone = {};
|
|
13
|
-
for (const key of Object.keys(source)) {
|
|
14
|
-
clone[key] = deepClone(source[key]);
|
|
15
|
-
}
|
|
16
|
-
return clone;
|
|
17
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export function groupBy(items, keyFn) {
|
|
2
|
-
const grouped = {};
|
|
3
|
-
for (const item of items) {
|
|
4
|
-
const key = keyFn(item);
|
|
5
|
-
const bucket = grouped[key];
|
|
6
|
-
if (bucket === undefined) {
|
|
7
|
-
grouped[key] = [item];
|
|
8
|
-
}
|
|
9
|
-
else {
|
|
10
|
-
bucket.push(item);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
return grouped;
|
|
14
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// debounce: fires fn once, ms after the last call, using that last call's arguments. A call during the wait restarts it.
|
|
2
|
-
export function debounce(fn, ms) {
|
|
3
|
-
let timer;
|
|
4
|
-
return (...args) => {
|
|
5
|
-
if (timer !== undefined) {
|
|
6
|
-
clearTimeout(timer);
|
|
7
|
-
}
|
|
8
|
-
timer = setTimeout(() => {
|
|
9
|
-
timer = undefined;
|
|
10
|
-
fn(...args);
|
|
11
|
-
}, ms);
|
|
12
|
-
};
|
|
13
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
|
|
2
|
-
export function deepClone(value) {
|
|
3
|
-
if (value === null || typeof value !== 'object') {
|
|
4
|
-
return value;
|
|
5
|
-
}
|
|
6
|
-
if (value instanceof Date) {
|
|
7
|
-
return new Date(value.getTime());
|
|
8
|
-
}
|
|
9
|
-
if (Array.isArray(value)) {
|
|
10
|
-
return value.map((item) => deepClone(item));
|
|
11
|
-
}
|
|
12
|
-
const source = value;
|
|
13
|
-
const clone = {};
|
|
14
|
-
for (const key of Object.keys(source)) {
|
|
15
|
-
clone[key] = deepClone(source[key]);
|
|
16
|
-
}
|
|
17
|
-
return clone;
|
|
18
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
|
|
2
|
-
export function groupBy(items, keyFn) {
|
|
3
|
-
const groups = {};
|
|
4
|
-
for (const item of items) {
|
|
5
|
-
const key = keyFn(item);
|
|
6
|
-
const bucket = groups[key];
|
|
7
|
-
if (bucket === undefined) {
|
|
8
|
-
groups[key] = [item];
|
|
9
|
-
}
|
|
10
|
-
else {
|
|
11
|
-
bucket.push(item);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return groups;
|
|
15
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export function deepClone(value) {
|
|
2
|
-
if (value === null || typeof value !== 'object') {
|
|
3
|
-
return value;
|
|
4
|
-
}
|
|
5
|
-
if (value instanceof Date) {
|
|
6
|
-
return new Date(value.getTime());
|
|
7
|
-
}
|
|
8
|
-
if (Array.isArray(value)) {
|
|
9
|
-
return value.map((item) => deepClone(item));
|
|
10
|
-
}
|
|
11
|
-
const source = value;
|
|
12
|
-
const clone = {};
|
|
13
|
-
for (const key of Object.keys(source)) {
|
|
14
|
-
clone[key] = deepClone(source[key]);
|
|
15
|
-
}
|
|
16
|
-
return clone;
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export function deepClone(value) {
|
|
2
|
-
if (value === null || typeof value !== 'object') {
|
|
3
|
-
return value;
|
|
4
|
-
}
|
|
5
|
-
if (value instanceof Date) {
|
|
6
|
-
return new Date(value.getTime());
|
|
7
|
-
}
|
|
8
|
-
if (Array.isArray(value)) {
|
|
9
|
-
return value.map((item) => deepClone(item));
|
|
10
|
-
}
|
|
11
|
-
const source = value;
|
|
12
|
-
const clone = {};
|
|
13
|
-
for (const key of Object.keys(source)) {
|
|
14
|
-
clone[key] = deepClone(source[key]);
|
|
15
|
-
}
|
|
16
|
-
return clone;
|
|
17
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export function groupBy(items, keyFn) {
|
|
2
|
-
const groups = {};
|
|
3
|
-
for (const item of items) {
|
|
4
|
-
const key = keyFn(item);
|
|
5
|
-
const bucket = groups[key];
|
|
6
|
-
if (bucket === undefined) {
|
|
7
|
-
groups[key] = [item];
|
|
8
|
-
}
|
|
9
|
-
else {
|
|
10
|
-
bucket.push(item);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
return groups;
|
|
14
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// Debounce: fires fn once, ms after the last call, using that last call's arguments. A call during the wait restarts it.
|
|
2
|
-
export function debounce(fn, ms) {
|
|
3
|
-
let timer;
|
|
4
|
-
return (...args) => {
|
|
5
|
-
if (timer !== undefined) {
|
|
6
|
-
clearTimeout(timer);
|
|
7
|
-
}
|
|
8
|
-
timer = setTimeout(() => {
|
|
9
|
-
timer = undefined;
|
|
10
|
-
fn(...args);
|
|
11
|
-
}, ms);
|
|
12
|
-
};
|
|
13
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
|
|
2
|
-
export function deepClone(value) {
|
|
3
|
-
if (value === null || typeof value !== 'object') {
|
|
4
|
-
return value;
|
|
5
|
-
}
|
|
6
|
-
if (value instanceof Date) {
|
|
7
|
-
return new Date(value.getTime());
|
|
8
|
-
}
|
|
9
|
-
if (Array.isArray(value)) {
|
|
10
|
-
return value.map((item) => deepClone(item));
|
|
11
|
-
}
|
|
12
|
-
const source = value;
|
|
13
|
-
const clone = {};
|
|
14
|
-
for (const key of Object.keys(source)) {
|
|
15
|
-
clone[key] = deepClone(source[key]);
|
|
16
|
-
}
|
|
17
|
-
return clone;
|
|
18
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
|
|
2
|
-
export function groupBy(items, keyFn) {
|
|
3
|
-
const groups = {};
|
|
4
|
-
for (const item of items) {
|
|
5
|
-
const key = keyFn(item);
|
|
6
|
-
const bucket = groups[key];
|
|
7
|
-
if (bucket === undefined) {
|
|
8
|
-
groups[key] = [item];
|
|
9
|
-
}
|
|
10
|
-
else {
|
|
11
|
-
bucket.push(item);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return groups;
|
|
15
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export function debounce(fn, ms) {
|
|
2
|
-
let timer;
|
|
3
|
-
return (...args) => {
|
|
4
|
-
if (timer !== undefined) {
|
|
5
|
-
clearTimeout(timer);
|
|
6
|
-
}
|
|
7
|
-
// Close over the latest arguments so the trailing call forwards them.
|
|
8
|
-
timer = setTimeout(() => {
|
|
9
|
-
timer = undefined;
|
|
10
|
-
fn(...args);
|
|
11
|
-
}, ms);
|
|
12
|
-
};
|
|
13
|
-
}
|