@krtclcdy/tersio 2.22.1 → 2.23.0
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 +25 -22
- package/cli/common.js +40 -36
- package/cli/dashboard.js +27 -12
- package/cli/doctor-fix.js +35 -29
- package/cli/doctor.js +26 -15
- package/cli/install.js +13 -32
- package/cli/profile.js +1 -0
- package/cli/settings.js +1 -1
- package/cli/update.js +12 -11
- package/cli/usage.js +6 -1
- package/dashboard/dist/index.html +1 -1
- package/extensions/ai-addons-updater/index.js +7 -3
- package/extensions/ai-addons-updater/index.ts +15 -3
- package/extensions/caveman-session/index.js +17 -12
- package/extensions/caveman-session/index.ts +17 -12
- package/extensions/caveman-session/rule.md +90 -0
- package/extensions/combo-toggle/index.js +22 -2
- package/extensions/combo-toggle/index.ts +26 -2
- package/extensions/lib/utils.js +20 -4
- package/extensions/lib/utils.ts +18 -4
- package/extensions/rtk-session/index.js +11 -10
- package/extensions/rtk-session/index.ts +10 -11
- package/extensions/shared/plugin-settings.js +37 -3
- package/extensions/shared/plugin-settings.ts +34 -3
- package/extensions/shared/rtk-gain.js +2 -2
- package/extensions/shared/rtk-gain.ts +3 -3
- package/extensions/shared/session-state.js +5 -3
- package/extensions/shared/session-state.ts +4 -3
- package/extensions/shared/types.ts +5 -0
- package/extensions/shared/usage-ledger.js +113 -3
- package/extensions/shared/usage-ledger.ts +121 -3
- package/package.json +12 -6
- package/tersio.js +2 -2
- package/extensions/shared/mode-reinforcement.ts +0 -29
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.ts';
|
|
1
|
+
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.ts';
|
|
4
2
|
import { readRtkDefault } from '../shared/plugin-settings.ts';
|
|
5
3
|
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
6
4
|
|
|
@@ -17,13 +15,12 @@ function resolveEnabled(entries: SessionEntry[] | null | undefined): boolean | n
|
|
|
17
15
|
return lastCustomValue(entries, 'rtk-mode', (data) => asBoolean(data?.enabled));
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
function setRtkProcessEnabled(enabled: boolean): void {
|
|
19
|
+
if (enabled) delete process.env.RTK_DISABLED;
|
|
20
|
+
else process.env.RTK_DISABLED = '1';
|
|
21
|
+
}
|
|
23
22
|
|
|
24
|
-
const RTK_PROMPT = `RTK
|
|
25
|
-
Use Rust Token Killer for shell output that would otherwise be noisy. Prefer explicit RTK commands in bash: \`rtk git status\`, \`rtk git diff\`, \`rtk read <file>\`, \`rtk grep <pattern> <path>\`, \`rtk find <glob> <path>\`, \`rtk test <cmd...>\`, \`rtk tsc\`, \`rtk lint\`, or \`rtk <tool> ...\` for supported dev commands.
|
|
26
|
-
Do not use RTK when exact raw output is required, when a specialized OMP tool is required by system policy, or when the command changes state and RTK would hide important confirmation text. Specialized OMP tools still win: read/glob/grep/edit/lsp stay preferred over shell equivalents.`;
|
|
23
|
+
const RTK_PROMPT = `RTK guidance active. RTK automatically rewrites eligible Bash calls through the installed rtk hook. Prefer rtk for noisy shell output, but use exact raw output for state changes, checksums, patches, and diagnostics that need full bytes.`;
|
|
27
24
|
|
|
28
25
|
interface ZodChain {
|
|
29
26
|
min: (n: number) => ZodChain;
|
|
@@ -52,7 +49,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
52
49
|
enabled = Boolean(next);
|
|
53
50
|
pi.appendEntry?.('rtk-mode', { enabled });
|
|
54
51
|
setSharedComboMode('rtk', enabled);
|
|
55
|
-
|
|
52
|
+
setRtkProcessEnabled(enabled);
|
|
56
53
|
const active = activeModesSummary(getSharedComboState());
|
|
57
54
|
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
58
55
|
}
|
|
@@ -63,6 +60,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
63
60
|
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
64
61
|
function syncFromShared(state: { rtk: string }): void {
|
|
65
62
|
enabled = state.rtk === 'on';
|
|
63
|
+
setRtkProcessEnabled(enabled);
|
|
66
64
|
syncStatus();
|
|
67
65
|
}
|
|
68
66
|
setSharedComboListener(syncFromShared);
|
|
@@ -102,7 +100,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
102
100
|
};
|
|
103
101
|
}
|
|
104
102
|
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
105
|
-
const result = await pi.exec!(
|
|
103
|
+
const result = await pi.exec!('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
106
104
|
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
107
105
|
return {
|
|
108
106
|
isError: result.code !== 0,
|
|
@@ -129,6 +127,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
129
127
|
// installer/user-configured default (off unless configured).
|
|
130
128
|
const persisted = resolveEnabled(entries);
|
|
131
129
|
enabled = typeof persisted === 'boolean' ? persisted : readRtkDefault();
|
|
130
|
+
setRtkProcessEnabled(enabled);
|
|
132
131
|
syncStatus(ctx);
|
|
133
132
|
}
|
|
134
133
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// written by `omp plugin config set @krtclcdy/tersio <key> <value>` and by
|
|
4
4
|
// the installer profile step. Tolerant: any parse failure yields {} so every
|
|
5
5
|
// caller falls back to its own default.
|
|
6
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
@@ -30,8 +30,8 @@ export function readPluginSettings() {
|
|
|
30
30
|
return {};
|
|
31
31
|
}
|
|
32
32
|
const COMBO_LEVELS = new Set(['off', 'medium', 'balanced', 'max']);
|
|
33
|
-
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'wenyan']);
|
|
34
|
-
const PONYTAIL_MODES = new Set(['off', 'lite', 'full', 'ultra'
|
|
33
|
+
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
|
|
34
|
+
const PONYTAIL_MODES = new Set(['off', 'lite', 'full', 'ultra']);
|
|
35
35
|
function readStringDefault(key, valid) {
|
|
36
36
|
const raw = readPluginSettings()[key];
|
|
37
37
|
return typeof raw === 'string' && valid.has(raw) ? raw : 'off';
|
|
@@ -49,3 +49,37 @@ export function readRtkDefault() {
|
|
|
49
49
|
export function readPonytailDefault() {
|
|
50
50
|
return readStringDefault('ponytailDefault', PONYTAIL_MODES);
|
|
51
51
|
}
|
|
52
|
+
export function isComboSetupComplete() {
|
|
53
|
+
return readPluginSettings().comboSetupComplete === true;
|
|
54
|
+
}
|
|
55
|
+
const COMBO_MODE_DEFAULTS = {
|
|
56
|
+
off: { cavemanDefault: 'off', rtkDefault: false, ponytailDefault: 'off' },
|
|
57
|
+
medium: { cavemanDefault: 'lite', rtkDefault: true, ponytailDefault: 'lite' },
|
|
58
|
+
balanced: { cavemanDefault: 'full', rtkDefault: true, ponytailDefault: 'full' },
|
|
59
|
+
max: { cavemanDefault: 'ultra', rtkDefault: true, ponytailDefault: 'ultra' },
|
|
60
|
+
};
|
|
61
|
+
export function saveComboSetup(level) {
|
|
62
|
+
if (!(level in COMBO_MODE_DEFAULTS))
|
|
63
|
+
return false;
|
|
64
|
+
const lockPath = lockPaths()[0];
|
|
65
|
+
let lock = {};
|
|
66
|
+
if (existsSync(lockPath)) {
|
|
67
|
+
try {
|
|
68
|
+
lock = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
lock = {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
lock.plugins ||= {};
|
|
75
|
+
lock.settings ||= {};
|
|
76
|
+
lock.settings[PLUGIN_NAME] = {
|
|
77
|
+
...lock.settings[PLUGIN_NAME],
|
|
78
|
+
comboDefault: level,
|
|
79
|
+
...COMBO_MODE_DEFAULTS[level],
|
|
80
|
+
comboSetupComplete: true,
|
|
81
|
+
};
|
|
82
|
+
mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
83
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf8');
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// the installer profile step. Tolerant: any parse failure yields {} so every
|
|
5
5
|
// caller falls back to its own default.
|
|
6
6
|
|
|
7
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
@@ -31,8 +31,8 @@ export function readPluginSettings(): Record<string, unknown> {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const COMBO_LEVELS = new Set(['off', 'medium', 'balanced', 'max']);
|
|
34
|
-
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'wenyan']);
|
|
35
|
-
const PONYTAIL_MODES = new Set(['off', 'lite', 'full', 'ultra'
|
|
34
|
+
const CAVEMAN_MODES = new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']);
|
|
35
|
+
const PONYTAIL_MODES = new Set(['off', 'lite', 'full', 'ultra']);
|
|
36
36
|
|
|
37
37
|
function readStringDefault(key: string, valid: Set<string>): string {
|
|
38
38
|
const raw = readPluginSettings()[key];
|
|
@@ -55,3 +55,34 @@ export function readRtkDefault(): boolean {
|
|
|
55
55
|
export function readPonytailDefault(): string {
|
|
56
56
|
return readStringDefault('ponytailDefault', PONYTAIL_MODES);
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
export function isComboSetupComplete(): boolean {
|
|
60
|
+
return readPluginSettings().comboSetupComplete === true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const COMBO_MODE_DEFAULTS = {
|
|
64
|
+
off: { cavemanDefault: 'off', rtkDefault: false, ponytailDefault: 'off' },
|
|
65
|
+
medium: { cavemanDefault: 'lite', rtkDefault: true, ponytailDefault: 'lite' },
|
|
66
|
+
balanced: { cavemanDefault: 'full', rtkDefault: true, ponytailDefault: 'full' },
|
|
67
|
+
max: { cavemanDefault: 'ultra', rtkDefault: true, ponytailDefault: 'ultra' },
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
export function saveComboSetup(level: string): boolean {
|
|
71
|
+
if (!(level in COMBO_MODE_DEFAULTS)) return false;
|
|
72
|
+
const lockPath = lockPaths()[0];
|
|
73
|
+
let lock: { plugins?: Record<string, unknown>; settings?: Record<string, Record<string, unknown>> } = {};
|
|
74
|
+
if (existsSync(lockPath)) {
|
|
75
|
+
try { lock = JSON.parse(readFileSync(lockPath, 'utf8')) as typeof lock; } catch { lock = {}; }
|
|
76
|
+
}
|
|
77
|
+
lock.plugins ||= {};
|
|
78
|
+
lock.settings ||= {};
|
|
79
|
+
lock.settings[PLUGIN_NAME] = {
|
|
80
|
+
...lock.settings[PLUGIN_NAME],
|
|
81
|
+
comboDefault: level,
|
|
82
|
+
...COMBO_MODE_DEFAULTS[level as keyof typeof COMBO_MODE_DEFAULTS],
|
|
83
|
+
comboSetupComplete: true,
|
|
84
|
+
};
|
|
85
|
+
mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
86
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf8');
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
@@ -55,8 +55,8 @@ export function readRtkGain(limit = 10, cutoffMs) {
|
|
|
55
55
|
const where = cutoffMs && cutoffMs > 0
|
|
56
56
|
? `WHERE CAST(strftime('%s', timestamp) AS INTEGER) >= ${Math.floor(cutoffMs / 1000)}`
|
|
57
57
|
: '';
|
|
58
|
-
const [[commands = '0', saved = '0', input = '0',
|
|
59
|
-
const byCommand = query(rtkDbPath(), `SELECT substr(${CMD_ONE_LINE}, 1, ${MAX_CMD_CHARS}), COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(
|
|
58
|
+
const [[commands = '0', saved = '0', input = '0', totalMs = '0'] = []] = query(rtkDbPath(), `SELECT COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(SUM(input_tokens),0), COALESCE(SUM(exec_time_ms),0) FROM commands ${where};`);
|
|
59
|
+
const byCommand = query(rtkDbPath(), `SELECT substr(${CMD_ONE_LINE}, 1, ${MAX_CMD_CHARS}), COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(100.0 * SUM(saved_tokens) / NULLIF(SUM(input_tokens), 0), 0), COALESCE(AVG(exec_time_ms),0) FROM commands ${where} GROUP BY rtk_cmd ORDER BY SUM(saved_tokens) DESC LIMIT ${Math.max(1, Math.floor(limit))};`).map(([command, count, savedRow, pct, ms]) => ({
|
|
60
60
|
command,
|
|
61
61
|
count: Number(count) || 0,
|
|
62
62
|
saved: Number(savedRow) || 0,
|
|
@@ -76,13 +76,13 @@ export function readRtkGain(limit = 10, cutoffMs?: number): RtkGain {
|
|
|
76
76
|
const where = cutoffMs && cutoffMs > 0
|
|
77
77
|
? `WHERE CAST(strftime('%s', timestamp) AS INTEGER) >= ${Math.floor(cutoffMs / 1000)}`
|
|
78
78
|
: '';
|
|
79
|
-
const [[commands = '0', saved = '0', input = '0',
|
|
79
|
+
const [[commands = '0', saved = '0', input = '0', totalMs = '0'] = []] = query(
|
|
80
80
|
rtkDbPath(),
|
|
81
|
-
`SELECT COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(SUM(input_tokens),0), COALESCE(
|
|
81
|
+
`SELECT COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(SUM(input_tokens),0), COALESCE(SUM(exec_time_ms),0) FROM commands ${where};`,
|
|
82
82
|
);
|
|
83
83
|
const byCommand = query(
|
|
84
84
|
rtkDbPath(),
|
|
85
|
-
`SELECT substr(${CMD_ONE_LINE}, 1, ${MAX_CMD_CHARS}), COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(
|
|
85
|
+
`SELECT substr(${CMD_ONE_LINE}, 1, ${MAX_CMD_CHARS}), COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(100.0 * SUM(saved_tokens) / NULLIF(SUM(input_tokens), 0), 0), COALESCE(AVG(exec_time_ms),0) FROM commands ${where} GROUP BY rtk_cmd ORDER BY SUM(saved_tokens) DESC LIMIT ${Math.max(1, Math.floor(limit))};`,
|
|
86
86
|
).map(([command, count, savedRow, pct, ms]) => ({
|
|
87
87
|
command,
|
|
88
88
|
count: Number(count) || 0,
|
|
@@ -7,9 +7,9 @@ export const COMBO_LEVELS = Object.freeze({
|
|
|
7
7
|
max: Object.freeze({ level: 'max', caveman: 'ultra', rtk: 'on', ponytail: 'ultra' }),
|
|
8
8
|
});
|
|
9
9
|
const MODE_VALUES = {
|
|
10
|
-
caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan']),
|
|
10
|
+
caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']),
|
|
11
11
|
rtk: new Set(['off', 'on']),
|
|
12
|
-
ponytail: new Set(['off', 'lite', 'full', 'ultra'
|
|
12
|
+
ponytail: new Set(['off', 'lite', 'full', 'ultra']),
|
|
13
13
|
};
|
|
14
14
|
const MODE_ENTRY_TYPES = {
|
|
15
15
|
'caveman-mode': 'caveman',
|
|
@@ -20,7 +20,9 @@ export function normalizeMode(name, value) {
|
|
|
20
20
|
if (name === 'rtk' && typeof value === 'boolean')
|
|
21
21
|
return value ? 'on' : 'off';
|
|
22
22
|
const mode = String(value ?? '').trim().toLowerCase();
|
|
23
|
-
|
|
23
|
+
if (!MODE_VALUES[name]?.has(mode))
|
|
24
|
+
return null;
|
|
25
|
+
return name === 'caveman' && mode === 'wenyan' ? 'wenyan-full' : mode;
|
|
24
26
|
}
|
|
25
27
|
export function deriveLevel(modes) {
|
|
26
28
|
for (const level of Object.keys(COMBO_LEVELS)) {
|
|
@@ -12,9 +12,9 @@ export const COMBO_LEVELS: Record<string, Readonly<ComboState>> = Object.freeze(
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
const MODE_VALUES: Record<string, Set<string>> = {
|
|
15
|
-
caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan']),
|
|
15
|
+
caveman: new Set(['off', 'lite', 'full', 'ultra', 'wenyan', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']),
|
|
16
16
|
rtk: new Set(['off', 'on']),
|
|
17
|
-
ponytail: new Set(['off', 'lite', 'full', 'ultra'
|
|
17
|
+
ponytail: new Set(['off', 'lite', 'full', 'ultra']),
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
type ModeName = 'caveman' | 'rtk' | 'ponytail';
|
|
@@ -34,7 +34,8 @@ interface Bridge {
|
|
|
34
34
|
export function normalizeMode(name: ModeName, value: unknown): string | null {
|
|
35
35
|
if (name === 'rtk' && typeof value === 'boolean') return value ? 'on' : 'off';
|
|
36
36
|
const mode = String(value ?? '').trim().toLowerCase();
|
|
37
|
-
|
|
37
|
+
if (!MODE_VALUES[name]?.has(mode)) return null;
|
|
38
|
+
return name === 'caveman' && mode === 'wenyan' ? 'wenyan-full' : mode;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function deriveLevel(modes: Modes | null | undefined): ComboLevel {
|
|
@@ -22,6 +22,11 @@ export type SessionEntry = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
export interface UiApi {
|
|
25
|
+
select?: (
|
|
26
|
+
title: string,
|
|
27
|
+
options: Array<string | { label: string; description?: string }>,
|
|
28
|
+
dialogOptions?: Record<string, unknown>,
|
|
29
|
+
) => Promise<string | undefined>;
|
|
25
30
|
setStatus?: (name: string, value: string | undefined) => void;
|
|
26
31
|
notify?: (message: string, level?: string) => void;
|
|
27
32
|
theme?: {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { tersioDataPath } from '../lib/utils.js';
|
|
8
|
+
import { resolveRtkBinary, tersioDataPath } from '../lib/utils.js';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
9
10
|
export function ledgerPath() {
|
|
10
11
|
const override = process.env.TERSIO_USAGE_FILE;
|
|
11
12
|
if (override)
|
|
@@ -137,6 +138,11 @@ export function walkJsonl(dir, out, cap) {
|
|
|
137
138
|
// (model, tokens, timing, status), so a few hundred cost little to ship.
|
|
138
139
|
export const RECENT_LIMIT = 200;
|
|
139
140
|
const FREE_SUFFIX = /(?::free|-free)$/i;
|
|
141
|
+
const RTK_ELIGIBLE_HEADS = new Set([
|
|
142
|
+
'rtk', 'git', 'grep', 'rg', 'find', 'cat', 'ls', 'tree', 'diff', 'log',
|
|
143
|
+
'npm', 'npx', 'pnpm', 'bun', 'bunx', 'cargo', 'go', 'python', 'pytest',
|
|
144
|
+
'ruff', 'mypy', 'docker', 'kubectl', 'psql', 'aws', 'gh', 'glab', 'wc',
|
|
145
|
+
]);
|
|
140
146
|
export function newSessionAccum() {
|
|
141
147
|
return { byModel: {}, byDay: {}, byDayModel: {}, byTool: {}, byModelMessages: {}, totals: zeroBreakdown(), messages: 0, costMeasured: 0, recent: [] };
|
|
142
148
|
}
|
|
@@ -194,9 +200,14 @@ export function classifySessionLine(row) {
|
|
|
194
200
|
const computedDur = (typeof msg.completedAt === 'number' && typeof msg.timestamp === 'number') ? msg.completedAt - msg.timestamp : undefined;
|
|
195
201
|
const tools = [];
|
|
196
202
|
for (const part of msg.content ?? []) {
|
|
197
|
-
if (part?.type
|
|
198
|
-
|
|
203
|
+
if (part?.type !== 'toolCall' || typeof part.name !== 'string' || !part.name)
|
|
204
|
+
continue;
|
|
205
|
+
if (part.name !== 'bash') {
|
|
206
|
+
tools.push(part.name);
|
|
207
|
+
continue;
|
|
199
208
|
}
|
|
209
|
+
const command = part.arguments?.command;
|
|
210
|
+
tools.push(`bash:${leadBinary(command)}`);
|
|
200
211
|
}
|
|
201
212
|
return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, run: statusOf(msg), tools };
|
|
202
213
|
}
|
|
@@ -251,6 +262,82 @@ export function importSessionTokens() {
|
|
|
251
262
|
accum.recent.sort((a, b) => b.t - a.t);
|
|
252
263
|
return { messages: accum.messages, totals: accum.totals, byModel: accum.byModel, byDay: accum.byDay, byDayModel: accum.byDayModel, byTool: accum.byTool, byModelMessages: accum.byModelMessages, costMeasured: accum.costMeasured, recent: accum.recent.slice(0, RECENT_LIMIT) };
|
|
253
264
|
}
|
|
265
|
+
const adoptionFileCache = new Map();
|
|
266
|
+
function parseAdoptionFile(file) {
|
|
267
|
+
const counts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
|
|
268
|
+
let text;
|
|
269
|
+
try {
|
|
270
|
+
text = fs.readFileSync(file, 'utf8');
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return { counts, sessions: 0 };
|
|
274
|
+
}
|
|
275
|
+
let sawBash = false;
|
|
276
|
+
for (const line of text.split('\n')) {
|
|
277
|
+
if (!line.includes('tool_execution_start'))
|
|
278
|
+
continue;
|
|
279
|
+
try {
|
|
280
|
+
const row = JSON.parse(line);
|
|
281
|
+
if (row.type !== 'custom' || row.customType !== 'tool_execution_start' || row.data?.toolName !== 'bash')
|
|
282
|
+
continue;
|
|
283
|
+
const command = row.data.args?.command;
|
|
284
|
+
if (typeof command !== 'string')
|
|
285
|
+
continue;
|
|
286
|
+
sawBash = true;
|
|
287
|
+
counts.bashCalls += 1;
|
|
288
|
+
const head = leadBinary(command);
|
|
289
|
+
if (RTK_ELIGIBLE_HEADS.has(head))
|
|
290
|
+
counts.eligibleCalls += 1;
|
|
291
|
+
if (head === 'rtk')
|
|
292
|
+
counts.rtkCalls += 1;
|
|
293
|
+
}
|
|
294
|
+
catch { /* skip corrupt line */ }
|
|
295
|
+
}
|
|
296
|
+
return { counts, sessions: sawBash ? 1 : 0 };
|
|
297
|
+
}
|
|
298
|
+
export function clearRtkAdoptionCache() {
|
|
299
|
+
adoptionFileCache.clear();
|
|
300
|
+
}
|
|
301
|
+
export function readRtkAdoption() {
|
|
302
|
+
const files = [];
|
|
303
|
+
walkJsonl(sessionsDir(), files, 2000);
|
|
304
|
+
if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
|
|
305
|
+
walkJsonl(codexSessionsDir(), files, 2000);
|
|
306
|
+
}
|
|
307
|
+
const live = new Set(files);
|
|
308
|
+
for (const file of adoptionFileCache.keys()) {
|
|
309
|
+
if (!live.has(file))
|
|
310
|
+
adoptionFileCache.delete(file);
|
|
311
|
+
}
|
|
312
|
+
let bashCalls = 0;
|
|
313
|
+
let eligibleCalls = 0;
|
|
314
|
+
let rtkCalls = 0;
|
|
315
|
+
let sessions = 0;
|
|
316
|
+
for (const file of files) {
|
|
317
|
+
let stat;
|
|
318
|
+
try {
|
|
319
|
+
stat = fs.statSync(file);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
let entry = adoptionFileCache.get(file);
|
|
325
|
+
if (!entry || entry.size !== stat.size || entry.mtimeMs !== stat.mtimeMs) {
|
|
326
|
+
const parsed = parseAdoptionFile(file);
|
|
327
|
+
entry = { size: stat.size, mtimeMs: stat.mtimeMs, counts: parsed.counts };
|
|
328
|
+
adoptionFileCache.set(file, entry);
|
|
329
|
+
sessions += parsed.sessions;
|
|
330
|
+
}
|
|
331
|
+
else if (entry.counts.bashCalls > 0) {
|
|
332
|
+
sessions += 1;
|
|
333
|
+
}
|
|
334
|
+
bashCalls += entry.counts.bashCalls;
|
|
335
|
+
eligibleCalls += entry.counts.eligibleCalls;
|
|
336
|
+
rtkCalls += entry.counts.rtkCalls;
|
|
337
|
+
}
|
|
338
|
+
const missedCalls = Math.max(0, eligibleCalls - rtkCalls);
|
|
339
|
+
return { sessions, bashCalls, eligibleCalls, rtkCalls, missedCalls, adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0 };
|
|
340
|
+
}
|
|
254
341
|
export function processSessionText(accum, state, text) {
|
|
255
342
|
for (const line of text.split('\n')) {
|
|
256
343
|
if (!line.trim())
|
|
@@ -275,6 +362,29 @@ export function processSessionText(accum, state, text) {
|
|
|
275
362
|
catch { /* skip corrupt lines */ }
|
|
276
363
|
}
|
|
277
364
|
}
|
|
365
|
+
export function readRtkRecallDiagnostics(binary = resolveRtkBinary()) {
|
|
366
|
+
if (!binary)
|
|
367
|
+
return { mode: 'unknown', entries: 0, available: false };
|
|
368
|
+
try {
|
|
369
|
+
const configPath = process.env.RTK_CONFIG
|
|
370
|
+
|| (process.platform === 'darwin'
|
|
371
|
+
? path.join(os.homedir(), 'Library', 'Application Support', 'rtk', 'config.toml')
|
|
372
|
+
: path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'rtk', 'config.toml'));
|
|
373
|
+
let config = '';
|
|
374
|
+
try {
|
|
375
|
+
config = fs.readFileSync(configPath, 'utf8');
|
|
376
|
+
}
|
|
377
|
+
catch { /* use RTK CLI fallback */ }
|
|
378
|
+
const modeMatch = /^\s*mode\s*=\s*"(sqlite|tee|disabled)"/m.exec(config);
|
|
379
|
+
const mode = (modeMatch?.[1] ?? 'unknown');
|
|
380
|
+
const output = execFileSync(binary, ['recall', '--list'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
381
|
+
const entries = /\((\d+)\s+(?:entries?|stored)\)/i.exec(output)?.[1] ?? /^\s*(\d+)\s+entries?/im.exec(output)?.[1] ?? '0';
|
|
382
|
+
return { mode, entries: Number(entries) || 0, available: true };
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return { mode: 'unknown', entries: 0, available: false };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
278
388
|
// Lead binary of a shell string: first segment head past `cd` chains and
|
|
279
389
|
// VAR=x assignments (`cd /x && git status` → `git`). Falls back to `bash`.
|
|
280
390
|
const SKIP_HEADS = ['cd', 'echo', 'export', 'true', 'false'];
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { tersioDataPath } from '../lib/utils.ts';
|
|
8
|
+
import { resolveRtkBinary, tersioDataPath } from '../lib/utils.ts';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
9
10
|
|
|
10
11
|
export type UsageKind = 'command' | 'toggle' | 'install' | 'update' | 'rtk-audit';
|
|
11
12
|
|
|
@@ -104,6 +105,21 @@ export interface SessionTokens {
|
|
|
104
105
|
recent: RecentRequest[];
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
export interface RtkAdoption {
|
|
109
|
+
sessions: number;
|
|
110
|
+
bashCalls: number;
|
|
111
|
+
eligibleCalls: number;
|
|
112
|
+
rtkCalls: number;
|
|
113
|
+
missedCalls: number;
|
|
114
|
+
adoptionPct: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface RtkRecallDiagnostics {
|
|
118
|
+
mode: 'sqlite' | 'tee' | 'disabled' | 'unknown';
|
|
119
|
+
entries: number;
|
|
120
|
+
available: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
107
123
|
function zeroBreakdown(): TokenBreakdown {
|
|
108
124
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
109
125
|
}
|
|
@@ -187,6 +203,11 @@ export function walkJsonl(dir: string, out: string[], cap: number): void {
|
|
|
187
203
|
// (model, tokens, timing, status), so a few hundred cost little to ship.
|
|
188
204
|
export const RECENT_LIMIT = 200;
|
|
189
205
|
const FREE_SUFFIX = /(?::free|-free)$/i;
|
|
206
|
+
const RTK_ELIGIBLE_HEADS = new Set([
|
|
207
|
+
'rtk', 'git', 'grep', 'rg', 'find', 'cat', 'ls', 'tree', 'diff', 'log',
|
|
208
|
+
'npm', 'npx', 'pnpm', 'bun', 'bunx', 'cargo', 'go', 'python', 'pytest',
|
|
209
|
+
'ruff', 'mypy', 'docker', 'kubectl', 'psql', 'aws', 'gh', 'glab', 'wc',
|
|
210
|
+
]);
|
|
190
211
|
// Session-parse buffer shared by live reads and usage.db syncs: identical
|
|
191
212
|
// inputs produce identical aggregates, so the store is a cache, never a fork.
|
|
192
213
|
export interface SessionAccum {
|
|
@@ -253,9 +274,13 @@ export function classifySessionLine(row: { timestamp?: string | number; type?: u
|
|
|
253
274
|
const computedDur = (typeof msg.completedAt === 'number' && typeof msg.timestamp === 'number') ? msg.completedAt - msg.timestamp : undefined;
|
|
254
275
|
const tools: string[] = [];
|
|
255
276
|
for (const part of msg.content ?? []) {
|
|
256
|
-
if (part?.type
|
|
257
|
-
|
|
277
|
+
if (part?.type !== 'toolCall' || typeof part.name !== 'string' || !part.name) continue;
|
|
278
|
+
if (part.name !== 'bash') {
|
|
279
|
+
tools.push(part.name);
|
|
280
|
+
continue;
|
|
258
281
|
}
|
|
282
|
+
const command = (part.arguments as { command?: unknown } | null)?.command;
|
|
283
|
+
tools.push(`bash:${leadBinary(command)}`);
|
|
259
284
|
}
|
|
260
285
|
return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, run: statusOf(msg), tools };
|
|
261
286
|
}
|
|
@@ -305,6 +330,80 @@ export function importSessionTokens(): SessionTokens {
|
|
|
305
330
|
accum.recent.sort((a, b) => b.t - a.t);
|
|
306
331
|
return { messages: accum.messages, totals: accum.totals, byModel: accum.byModel, byDay: accum.byDay, byDayModel: accum.byDayModel, byTool: accum.byTool, byModelMessages: accum.byModelMessages, costMeasured: accum.costMeasured, recent: accum.recent.slice(0, RECENT_LIMIT) };
|
|
307
332
|
}
|
|
333
|
+
|
|
334
|
+
const adoptionFileCache = new Map<string, { size: number; mtimeMs: number; counts: { bashCalls: number; eligibleCalls: number; rtkCalls: number } }>();
|
|
335
|
+
|
|
336
|
+
function parseAdoptionFile(file: string): { counts: { bashCalls: number; eligibleCalls: number; rtkCalls: number }; sessions: number } {
|
|
337
|
+
const counts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
|
|
338
|
+
let text: string;
|
|
339
|
+
try {
|
|
340
|
+
text = fs.readFileSync(file, 'utf8');
|
|
341
|
+
} catch {
|
|
342
|
+
return { counts, sessions: 0 };
|
|
343
|
+
}
|
|
344
|
+
let sawBash = false;
|
|
345
|
+
for (const line of text.split('\n')) {
|
|
346
|
+
if (!line.includes('tool_execution_start')) continue;
|
|
347
|
+
try {
|
|
348
|
+
const row = JSON.parse(line) as {
|
|
349
|
+
type?: string;
|
|
350
|
+
customType?: string;
|
|
351
|
+
data?: { toolName?: unknown; args?: { command?: unknown } };
|
|
352
|
+
};
|
|
353
|
+
if (row.type !== 'custom' || row.customType !== 'tool_execution_start' || row.data?.toolName !== 'bash') continue;
|
|
354
|
+
const command = row.data.args?.command;
|
|
355
|
+
if (typeof command !== 'string') continue;
|
|
356
|
+
sawBash = true;
|
|
357
|
+
counts.bashCalls += 1;
|
|
358
|
+
const head = leadBinary(command);
|
|
359
|
+
if (RTK_ELIGIBLE_HEADS.has(head)) counts.eligibleCalls += 1;
|
|
360
|
+
if (head === 'rtk') counts.rtkCalls += 1;
|
|
361
|
+
} catch { /* skip corrupt line */ }
|
|
362
|
+
}
|
|
363
|
+
return { counts, sessions: sawBash ? 1 : 0 };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function clearRtkAdoptionCache(): void {
|
|
367
|
+
adoptionFileCache.clear();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function readRtkAdoption(): RtkAdoption {
|
|
371
|
+
const files: string[] = [];
|
|
372
|
+
walkJsonl(sessionsDir(), files, 2000);
|
|
373
|
+
if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
|
|
374
|
+
walkJsonl(codexSessionsDir(), files, 2000);
|
|
375
|
+
}
|
|
376
|
+
const live = new Set(files);
|
|
377
|
+
for (const file of adoptionFileCache.keys()) {
|
|
378
|
+
if (!live.has(file)) adoptionFileCache.delete(file);
|
|
379
|
+
}
|
|
380
|
+
let bashCalls = 0;
|
|
381
|
+
let eligibleCalls = 0;
|
|
382
|
+
let rtkCalls = 0;
|
|
383
|
+
let sessions = 0;
|
|
384
|
+
for (const file of files) {
|
|
385
|
+
let stat: fs.Stats;
|
|
386
|
+
try {
|
|
387
|
+
stat = fs.statSync(file);
|
|
388
|
+
} catch {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
let entry = adoptionFileCache.get(file);
|
|
392
|
+
if (!entry || entry.size !== stat.size || entry.mtimeMs !== stat.mtimeMs) {
|
|
393
|
+
const parsed = parseAdoptionFile(file);
|
|
394
|
+
entry = { size: stat.size, mtimeMs: stat.mtimeMs, counts: parsed.counts };
|
|
395
|
+
adoptionFileCache.set(file, entry);
|
|
396
|
+
sessions += parsed.sessions;
|
|
397
|
+
} else if (entry.counts.bashCalls > 0) {
|
|
398
|
+
sessions += 1;
|
|
399
|
+
}
|
|
400
|
+
bashCalls += entry.counts.bashCalls;
|
|
401
|
+
eligibleCalls += entry.counts.eligibleCalls;
|
|
402
|
+
rtkCalls += entry.counts.rtkCalls;
|
|
403
|
+
}
|
|
404
|
+
const missedCalls = Math.max(0, eligibleCalls - rtkCalls);
|
|
405
|
+
return { sessions, bashCalls, eligibleCalls, rtkCalls, missedCalls, adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0 };
|
|
406
|
+
}
|
|
308
407
|
export function processSessionText(accum: SessionAccum, state: { codexProvider: string | null }, text: string): void {
|
|
309
408
|
for (const line of text.split('\n')) {
|
|
310
409
|
if (!line.trim()) continue;
|
|
@@ -325,6 +424,25 @@ export function processSessionText(accum: SessionAccum, state: { codexProvider:
|
|
|
325
424
|
} catch { /* skip corrupt lines */ }
|
|
326
425
|
}
|
|
327
426
|
}
|
|
427
|
+
|
|
428
|
+
export function readRtkRecallDiagnostics(binary: string | null = resolveRtkBinary()): RtkRecallDiagnostics {
|
|
429
|
+
if (!binary) return { mode: 'unknown', entries: 0, available: false };
|
|
430
|
+
try {
|
|
431
|
+
const configPath = process.env.RTK_CONFIG
|
|
432
|
+
|| (process.platform === 'darwin'
|
|
433
|
+
? path.join(os.homedir(), 'Library', 'Application Support', 'rtk', 'config.toml')
|
|
434
|
+
: path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'rtk', 'config.toml'));
|
|
435
|
+
let config = '';
|
|
436
|
+
try { config = fs.readFileSync(configPath, 'utf8'); } catch { /* use RTK CLI fallback */ }
|
|
437
|
+
const modeMatch = /^\s*mode\s*=\s*"(sqlite|tee|disabled)"/m.exec(config);
|
|
438
|
+
const mode = (modeMatch?.[1] ?? 'unknown') as RtkRecallDiagnostics['mode'];
|
|
439
|
+
const output = execFileSync(binary, ['recall', '--list'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
440
|
+
const entries = /\((\d+)\s+(?:entries?|stored)\)/i.exec(output)?.[1] ?? /^\s*(\d+)\s+entries?/im.exec(output)?.[1] ?? '0';
|
|
441
|
+
return { mode, entries: Number(entries) || 0, available: true };
|
|
442
|
+
} catch {
|
|
443
|
+
return { mode: 'unknown', entries: 0, available: false };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
328
446
|
// Lead binary of a shell string: first segment head past `cd` chains and
|
|
329
447
|
// VAR=x assignments (`cd /x && git status` → `git`). Falls back to `bash`.
|
|
330
448
|
const SKIP_HEADS = ['cd', 'echo', 'export', 'true', 'false'];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krtclcdy/tersio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.0",
|
|
4
4
|
"description": "✂️ Token-saving OMP add-ons: terse Caveman replies, compact RTK shell output, lean Ponytail code calls — one-command combo presets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"omp",
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"./extensions/caveman-session/index.ts",
|
|
30
30
|
"./extensions/rtk-session/index.ts",
|
|
31
31
|
"./extensions/combo-toggle/index.ts",
|
|
32
|
-
"./extensions/shared/mode-reinforcement.ts",
|
|
33
32
|
"./extensions/tersio-commands/index.ts"
|
|
34
33
|
],
|
|
35
34
|
"skills": [
|
|
@@ -56,6 +55,11 @@
|
|
|
56
55
|
],
|
|
57
56
|
"default": "off"
|
|
58
57
|
},
|
|
58
|
+
"comboSetupComplete": {
|
|
59
|
+
"type": "boolean",
|
|
60
|
+
"description": "Whether the first interactive OMP launch has completed the Combo preset prompt",
|
|
61
|
+
"default": false
|
|
62
|
+
},
|
|
59
63
|
"cavemanDefault": {
|
|
60
64
|
"type": "enum",
|
|
61
65
|
"description": "Session-start default (no saved state)",
|
|
@@ -64,7 +68,9 @@
|
|
|
64
68
|
"lite",
|
|
65
69
|
"full",
|
|
66
70
|
"ultra",
|
|
67
|
-
"wenyan"
|
|
71
|
+
"wenyan-lite",
|
|
72
|
+
"wenyan-full",
|
|
73
|
+
"wenyan-ultra"
|
|
68
74
|
],
|
|
69
75
|
"default": "off"
|
|
70
76
|
},
|
|
@@ -80,8 +86,7 @@
|
|
|
80
86
|
"off",
|
|
81
87
|
"lite",
|
|
82
88
|
"full",
|
|
83
|
-
"ultra"
|
|
84
|
-
"review"
|
|
89
|
+
"ultra"
|
|
85
90
|
],
|
|
86
91
|
"default": "off"
|
|
87
92
|
},
|
|
@@ -109,7 +114,8 @@
|
|
|
109
114
|
"cli/**/*.js",
|
|
110
115
|
"dashboard/dist/**/*",
|
|
111
116
|
"extensions/**/*.js",
|
|
112
|
-
"extensions/**/*.ts"
|
|
117
|
+
"extensions/**/*.ts",
|
|
118
|
+
"extensions/caveman-session/rule.md"
|
|
113
119
|
],
|
|
114
120
|
"scripts": {
|
|
115
121
|
"clean": "rm -f tersio.js cli/*.js extensions/**/*.js && rm -rf dashboard/dist",
|
package/tersio.js
CHANGED
|
@@ -34,8 +34,8 @@ Options:
|
|
|
34
34
|
--keep-ponytail (uninstall: keep the bundled Ponytail copy — removed by default)
|
|
35
35
|
--remove-rtk (uninstall: also remove the RTK binary)
|
|
36
36
|
--combo-default off|medium|balanced|max (implies caveman, rtk, ponytail)
|
|
37
|
-
--caveman-default off|lite|full|ultra|wenyan (override; default follows combo)
|
|
38
|
-
--ponytail-default off|lite|full|ultra
|
|
37
|
+
--caveman-default off|lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra (override; default follows combo)
|
|
38
|
+
--ponytail-default off|lite|full|ultra (override; default follows combo)
|
|
39
39
|
--rtk-default on|off (override; default follows combo)
|
|
40
40
|
--yes, -y
|
|
41
41
|
--dry-run
|