@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.
Files changed (120) hide show
  1. package/README.md +17 -10
  2. package/dashboard/dist/index.html +1 -1
  3. package/dist/cli/common.js +25 -3
  4. package/dist/cli/dashboard.js +7 -2
  5. package/dist/cli/doctor-fix.js +12 -7
  6. package/dist/cli/doctor.js +16 -3
  7. package/dist/cli/install.js +5 -5
  8. package/dist/cli/profile.js +28 -3
  9. package/dist/cli/rtk-wiring.js +13 -10
  10. package/dist/cli/usage.js +5 -0
  11. package/dist/extensions/caveman/index.js +3 -6
  12. package/dist/extensions/combo/index.js +1 -6
  13. package/dist/extensions/commands/index.js +7 -2
  14. package/dist/extensions/pstack/index.js +2 -1
  15. package/dist/extensions/rtk/filter.js +45 -4
  16. package/dist/extensions/rtk/session.js +4 -2
  17. package/dist/extensions/server.js +44 -29
  18. package/dist/extensions/shared/agent-stats.js +21 -13
  19. package/dist/extensions/shared/pricing.js +5 -4
  20. package/dist/extensions/shared/rtk-eligible.js +7 -11
  21. package/dist/extensions/shared/rtk-gain.js +2 -6
  22. package/dist/extensions/shared/sqlite.js +1 -6
  23. package/dist/extensions/shared/usage-ledger.js +97 -12
  24. package/dist/extensions/shared/utils.js +33 -0
  25. package/dist/extensions/updater/index.js +15 -7
  26. package/extensions/caveman/index.ts +3 -6
  27. package/extensions/combo/index.ts +1 -6
  28. package/extensions/commands/index.ts +7 -2
  29. package/extensions/pstack/index.ts +2 -1
  30. package/extensions/pstack/playbooks/autopilot-full.md +1 -1
  31. package/extensions/pstack/playbooks/babysit.md +1 -1
  32. package/extensions/pstack/playbooks/eval.md +1 -1
  33. package/extensions/pstack/playbooks/multi-phase-plan.md +2 -2
  34. package/extensions/pstack/playbooks/orchestrate.md +3 -3
  35. package/extensions/pstack/playbooks/session-pickup.md +1 -1
  36. package/extensions/pstack/playbooks/shipping.md +2 -2
  37. package/extensions/pstack/playbooks/worktree-cleanup.md +1 -1
  38. package/extensions/pstack/skills/reflect/SKILL.md +2 -2
  39. package/extensions/pstack/skills/show-me-your-work/SKILL.md +2 -2
  40. package/extensions/rtk/filter.ts +51 -4
  41. package/extensions/rtk/session.ts +4 -2
  42. package/extensions/server.ts +31 -23
  43. package/extensions/shared/agent-stats.ts +21 -12
  44. package/extensions/shared/pricing.ts +7 -5
  45. package/extensions/shared/rtk-eligible.ts +7 -11
  46. package/extensions/shared/rtk-gain.ts +2 -6
  47. package/extensions/shared/sqlite.ts +1 -6
  48. package/extensions/shared/usage-ledger.ts +102 -14
  49. package/extensions/shared/utils.ts +29 -0
  50. package/extensions/updater/index.ts +18 -7
  51. package/package.json +3 -5
  52. package/dist/bench/code-out/pfull-0/debounce.js +0 -13
  53. package/dist/bench/code-out/pfull-0/deepClone.js +0 -18
  54. package/dist/bench/code-out/pfull-0/groupBy.js +0 -15
  55. package/dist/bench/code-out/pfull-1/debounce.js +0 -13
  56. package/dist/bench/code-out/pfull-1/deepClone.js +0 -17
  57. package/dist/bench/code-out/pfull-1/groupBy.js +0 -14
  58. package/dist/bench/code-out/pfull-2/debounce.js +0 -13
  59. package/dist/bench/code-out/pfull-2/deepClone.js +0 -18
  60. package/dist/bench/code-out/pfull-2/groupBy.js +0 -15
  61. package/dist/bench/code-out/pfull-3/debounce.js +0 -12
  62. package/dist/bench/code-out/pfull-3/deepClone.js +0 -17
  63. package/dist/bench/code-out/pfull-3/groupBy.js +0 -7
  64. package/dist/bench/code-out/pfull-4/debounce.js +0 -10
  65. package/dist/bench/code-out/pfull-4/deepClone.js +0 -17
  66. package/dist/bench/code-out/pfull-4/groupBy.js +0 -14
  67. package/dist/bench/code-out/plite-0/debounce.js +0 -13
  68. package/dist/bench/code-out/plite-0/deepClone.js +0 -18
  69. package/dist/bench/code-out/plite-0/groupBy.js +0 -15
  70. package/dist/bench/code-out/plite-1/debounce.js +0 -13
  71. package/dist/bench/code-out/plite-1/deepClone.js +0 -17
  72. package/dist/bench/code-out/plite-1/groupBy.js +0 -11
  73. package/dist/bench/code-out/plite-2/debounce.js +0 -12
  74. package/dist/bench/code-out/plite-2/deepClone.js +0 -17
  75. package/dist/bench/code-out/plite-2/groupBy.js +0 -14
  76. package/dist/bench/code-out/plite-3/debounce.js +0 -12
  77. package/dist/bench/code-out/plite-3/deepClone.js +0 -17
  78. package/dist/bench/code-out/plite-3/groupBy.js +0 -14
  79. package/dist/bench/code-out/plite-4/debounce.js +0 -12
  80. package/dist/bench/code-out/plite-4/deepClone.js +0 -17
  81. package/dist/bench/code-out/plite-4/groupBy.js +0 -14
  82. package/dist/bench/code-out/pultra-0/debounce.js +0 -12
  83. package/dist/bench/code-out/pultra-0/deepClone.js +0 -20
  84. package/dist/bench/code-out/pultra-0/groupBy.js +0 -14
  85. package/dist/bench/code-out/pultra-1/debounce.js +0 -10
  86. package/dist/bench/code-out/pultra-1/deepClone.js +0 -17
  87. package/dist/bench/code-out/pultra-1/groupBy.js +0 -7
  88. package/dist/bench/code-out/pultra-2/debounce.js +0 -13
  89. package/dist/bench/code-out/pultra-2/deepClone.js +0 -18
  90. package/dist/bench/code-out/pultra-2/groupBy.js +0 -15
  91. package/dist/bench/code-out/pultra-3/debounce.js +0 -13
  92. package/dist/bench/code-out/pultra-3/deepClone.js +0 -18
  93. package/dist/bench/code-out/pultra-3/groupBy.js +0 -15
  94. package/dist/bench/code-out/pultra-4/debounce.js +0 -13
  95. package/dist/bench/code-out/pultra-4/deepClone.js +0 -18
  96. package/dist/bench/code-out/pultra-4/groupBy.js +0 -15
  97. package/dist/bench/code-out/run-0/debounce.js +0 -12
  98. package/dist/bench/code-out/run-0/deepClone.js +0 -17
  99. package/dist/bench/code-out/run-0/groupBy.js +0 -14
  100. package/dist/bench/code-out/run-1/debounce.js +0 -13
  101. package/dist/bench/code-out/run-1/deepClone.js +0 -15
  102. package/dist/bench/code-out/run-1/groupBy.js +0 -13
  103. package/dist/bench/code-out/run-2/debounce.js +0 -12
  104. package/dist/bench/code-out/run-2/deepClone.js +0 -18
  105. package/dist/bench/code-out/run-2/groupBy.js +0 -13
  106. package/dist/bench/code-out/run-3/debounce.js +0 -12
  107. package/dist/bench/code-out/run-3/deepClone.js +0 -18
  108. package/dist/bench/code-out/run-3/groupBy.js +0 -14
  109. package/dist/bench/code-out/run-4/debounce.js +0 -12
  110. package/dist/bench/code-out/run-4/deepClone.js +0 -16
  111. package/dist/bench/code-out/run-4/groupBy.js +0 -13
  112. package/dist/extensions/caveman/rule-lite.md +0 -77
  113. package/dist/extensions/caveman/rule-megacave-lite.md +0 -59
  114. package/dist/extensions/caveman/rule-megacave-ultra.md +0 -59
  115. package/dist/extensions/caveman/rule-megacave.md +0 -65
  116. package/dist/extensions/caveman/rule-ultra.md +0 -63
  117. package/dist/extensions/caveman/rule.md +0 -89
  118. package/dist/extensions/pstack/rule.md +0 -141
  119. package/dist/probe-catalog.js +0 -48
  120. 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?: string;
725
- customType?: string;
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 !== 'custom' || row.customType !== 'tool_execution_start' || row.data?.toolName !== 'bash') continue;
729
- const command = row.data.args?.command;
730
- if (typeof command === 'string') commands.push(command);
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
- return { sessions, bashCalls, eligibleCalls, rtkCalls, missedCalls, adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0 };
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 output = execFileSync(binary, ['recall', '--list'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
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(os.homedir(), '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman', name);
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
- const CAVEMAN_TRACKED: ReadonlyArray<{ file: string; remote: string }> = [
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
- parts.push(!localText ? `${name} missing` : shortHash(localText) === shortHash(remoteText) ? `${name} up to date` : `${name} update available`);
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
- const remoteHash = shortHash(remoteText);
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, remoteText, 'utf8');
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.7",
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": "rm -rf dist dashboard/dist",
140
- "build": "tsc && bun run build:dashboard && cp extensions/caveman/*.md dist/extensions/caveman/ && cp extensions/pstack/*.md dist/extensions/pstack/ && cp -R extensions/pstack/principles extensions/pstack/playbooks extensions/pstack/skills extensions/pstack/references dist/extensions/pstack/",
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,12 +0,0 @@
1
- export function debounce(fn, ms) {
2
- let timer;
3
- return (...args) => {
4
- if (timer !== undefined) {
5
- clearTimeout(timer);
6
- }
7
- timer = setTimeout(() => {
8
- timer = undefined;
9
- fn(...args);
10
- }, ms);
11
- };
12
- }
@@ -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,7 +0,0 @@
1
- export function groupBy(items, keyFn) {
2
- const groups = {};
3
- for (const item of items) {
4
- (groups[keyFn(item)] ??= []).push(item);
5
- }
6
- return groups;
7
- }
@@ -1,10 +0,0 @@
1
- export function debounce(fn, ms) {
2
- let timer;
3
- return (...args) => {
4
- clearTimeout(timer);
5
- timer = setTimeout(() => {
6
- timer = undefined;
7
- fn(...args);
8
- }, ms);
9
- };
10
- }
@@ -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
- }