@krtclcdy/tersio 2.19.0 → 2.20.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/cli/common.js +13 -1
- package/cli/doctor.js +7 -7
- package/cli/install.js +28 -28
- package/extensions/ai-addons-updater/index.ts +377 -0
- package/extensions/caveman-session/index.ts +161 -0
- package/extensions/combo-toggle/index.ts +217 -0
- package/extensions/lib/utils.ts +162 -0
- package/extensions/rtk-session/index.ts +164 -0
- package/extensions/shared/carbon.ts +123 -0
- package/extensions/shared/mode-reinforcement.ts +29 -0
- package/extensions/shared/plugin-settings.ts +57 -0
- package/extensions/shared/pricing.ts +168 -0
- package/extensions/shared/rtk-gain.ts +105 -0
- package/extensions/shared/session-state.ts +201 -0
- package/extensions/shared/types.ts +85 -0
- package/extensions/shared/usage-ledger.ts +406 -0
- package/extensions/shared/usage-store.ts +325 -0
- package/extensions/tersio-commands/index.ts +110 -0
- package/package.json +19 -24
- package/extensions/ai-addons-updater/index.js +0 -331
- package/extensions/caveman-session/index.js +0 -152
- package/extensions/combo-toggle/index.js +0 -165
- package/extensions/lib/utils.js +0 -154
- package/extensions/rtk-session/index.js +0 -148
- package/extensions/shared/carbon.js +0 -88
- package/extensions/shared/mode-reinforcement.js +0 -26
- package/extensions/shared/plugin-settings.js +0 -51
- package/extensions/shared/pricing.js +0 -163
- package/extensions/shared/rtk-gain.js +0 -79
- package/extensions/shared/session-state.js +0 -177
- package/extensions/shared/types.js +0 -3
- package/extensions/shared/usage-ledger.js +0 -354
- package/extensions/shared/usage-store.js +0 -294
- package/extensions/tersio-commands/index.js +0 -102
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// extensions/shared/usage-store.ts — tersio-owned usage.db.
|
|
2
|
+
// Persists the same per-message rows importSessionTokens derives live (model,
|
|
3
|
+
// tokens, cache, cost input, timing, status, tools), so top models, costing,
|
|
4
|
+
// cache, CO2 inputs, and activity survive session-file rotation. Sync is
|
|
5
|
+
// incremental: a files ledger (mtime+size) skips unchanged transcripts, and
|
|
6
|
+
// every read applies the current reset watermark — the store is a cache,
|
|
7
|
+
// never a fork. Missing sqlite3 CLI → sync false / read null, callers fall
|
|
8
|
+
// back to the live path. Never touches RTK's history.db or host transcripts.
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import {
|
|
14
|
+
RECENT_LIMIT,
|
|
15
|
+
canonicalModelId,
|
|
16
|
+
classifySessionLine,
|
|
17
|
+
codexSessionsDir,
|
|
18
|
+
costOf,
|
|
19
|
+
durOf,
|
|
20
|
+
ingestSessionRow,
|
|
21
|
+
newSessionAccum,
|
|
22
|
+
sessionsDir,
|
|
23
|
+
walkJsonl,
|
|
24
|
+
} from './usage-ledger.ts';
|
|
25
|
+
import type { RunStatus, SessionTokens } from './usage-ledger.ts';
|
|
26
|
+
|
|
27
|
+
export function usageDbPath(): string {
|
|
28
|
+
const override = process.env.TERSIO_USAGE_DB;
|
|
29
|
+
if (override) return override;
|
|
30
|
+
return path.join(os.homedir(), '.omp', 'plugins', 'tersio-usage.db');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StoredUsage {
|
|
34
|
+
tokens: SessionTokens;
|
|
35
|
+
syncedAt: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function esc(v: string): string {
|
|
39
|
+
return `'${v.replace(/'/g, "''")}'`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function intOf(v: unknown): number {
|
|
43
|
+
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function nullNum(v: number | undefined): string {
|
|
47
|
+
return v === undefined ? 'NULL' : String(v);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function nullStr(v: string | undefined): string {
|
|
51
|
+
return v === undefined ? 'NULL' : esc(v);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function run(db: string, sql: string): void {
|
|
55
|
+
execFileSync('sqlite3', [db, sql], { stdio: ['ignore', 'ignore', 'ignore'], timeout: 30000 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function query(db: string, sql: string): string[][] {
|
|
59
|
+
const out = execFileSync('sqlite3', ['-separator', '\t', db, sql], {
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
timeout: 30000,
|
|
62
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
63
|
+
});
|
|
64
|
+
return out
|
|
65
|
+
.split('\n')
|
|
66
|
+
.filter((line) => line.trim())
|
|
67
|
+
.map((line) => line.split('\t'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ensureSchema(db: string): void {
|
|
71
|
+
fs.mkdirSync(path.dirname(db), { recursive: true });
|
|
72
|
+
run(
|
|
73
|
+
db,
|
|
74
|
+
`CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);` +
|
|
75
|
+
`CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, mtime REAL NOT NULL, size INTEGER NOT NULL);` +
|
|
76
|
+
`CREATE TABLE IF NOT EXISTS messages (file TEXT NOT NULL, t REAL, model TEXT NOT NULL,` +
|
|
77
|
+
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
78
|
+
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]');` +
|
|
79
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_file ON messages(file);`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readFiles(db: string): Record<string, { mtime: number; size: number }> {
|
|
84
|
+
const known: Record<string, { mtime: number; size: number }> = {};
|
|
85
|
+
try {
|
|
86
|
+
for (const [p, mtime, size] of query(db, `SELECT path, mtime, size FROM files;`)) {
|
|
87
|
+
known[p] = { mtime: Number(mtime) || 0, size: Number(size) || 0 };
|
|
88
|
+
}
|
|
89
|
+
} catch { /* fresh db */ }
|
|
90
|
+
return known;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// One parsed assistant message, as stored. `t` is epoch ms or null when the
|
|
94
|
+
// source row carries no usable timestamp (live counts it, skips its recent).
|
|
95
|
+
interface StoredRow {
|
|
96
|
+
t: number | null;
|
|
97
|
+
model: string;
|
|
98
|
+
i: number;
|
|
99
|
+
o: number;
|
|
100
|
+
d: number | undefined;
|
|
101
|
+
cr: number;
|
|
102
|
+
cw: number;
|
|
103
|
+
usd: number | undefined;
|
|
104
|
+
st: RunStatus;
|
|
105
|
+
code: number | undefined;
|
|
106
|
+
note: string | undefined;
|
|
107
|
+
tools: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseFile(text: string): StoredRow[] {
|
|
111
|
+
const rows: StoredRow[] = [];
|
|
112
|
+
let codexProvider: string | null = null;
|
|
113
|
+
for (const line of text.split('\n')) {
|
|
114
|
+
if (!line.trim()) continue;
|
|
115
|
+
try {
|
|
116
|
+
const row = JSON.parse(line) as Parameters<typeof classifySessionLine>[0];
|
|
117
|
+
const parsed = classifySessionLine(row);
|
|
118
|
+
if (parsed.kind === 'codex_provider') {
|
|
119
|
+
if (parsed.provider) codexProvider = parsed.provider;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const ts = row.timestamp;
|
|
123
|
+
const ms = ts === undefined ? NaN : typeof ts === 'number' ? ts : Date.parse(ts);
|
|
124
|
+
const t = Number.isFinite(ms) ? ms : null;
|
|
125
|
+
if (parsed.kind === 'token_row') {
|
|
126
|
+
const provider = parsed.provider ?? codexProvider;
|
|
127
|
+
const usage = parsed.usage ?? {};
|
|
128
|
+
rows.push({
|
|
129
|
+
t,
|
|
130
|
+
model: canonicalModelId(provider ? `codex/${provider}` : 'codex'),
|
|
131
|
+
i: intOf(usage.input),
|
|
132
|
+
o: intOf(usage.output),
|
|
133
|
+
d: undefined,
|
|
134
|
+
cr: intOf(usage.cacheRead),
|
|
135
|
+
cw: intOf(usage.cacheWrite),
|
|
136
|
+
usd: costOf(usage),
|
|
137
|
+
st: 'completed',
|
|
138
|
+
code: undefined,
|
|
139
|
+
note: undefined,
|
|
140
|
+
tools: [],
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (parsed.kind !== 'assistant_row' || !parsed.model || !parsed.usage) continue;
|
|
145
|
+
const usage = parsed.usage;
|
|
146
|
+
const dur = durOf(parsed.durMs);
|
|
147
|
+
const run = parsed.run ?? { st: 'completed' as RunStatus };
|
|
148
|
+
const note = run.note?.replace(/[\t\n\r]/g, ' ').slice(0, 180);
|
|
149
|
+
rows.push({
|
|
150
|
+
t,
|
|
151
|
+
model: canonicalModelId(parsed.model),
|
|
152
|
+
i: intOf(usage.input),
|
|
153
|
+
o: intOf(usage.output),
|
|
154
|
+
d: dur,
|
|
155
|
+
cr: intOf(usage.cacheRead),
|
|
156
|
+
cw: intOf(usage.cacheWrite),
|
|
157
|
+
usd: costOf(usage),
|
|
158
|
+
st: run.st,
|
|
159
|
+
code: run.code,
|
|
160
|
+
note,
|
|
161
|
+
tools: parsed.tools ?? [],
|
|
162
|
+
});
|
|
163
|
+
} catch { /* skip corrupt lines */ }
|
|
164
|
+
}
|
|
165
|
+
return rows;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function insertSql(file: string, r: StoredRow): string {
|
|
169
|
+
return `INSERT INTO messages (file, t, model, i, o, d, cr, cw, usd, st, code, note, tools) VALUES (` +
|
|
170
|
+
`${esc(file)},${r.t === null ? 'NULL' : String(r.t)},${esc(r.model)},${r.i},${r.o},` +
|
|
171
|
+
`${nullNum(r.d)},${r.cr},${r.cw},${nullNum(r.usd)},${esc(r.st)},` +
|
|
172
|
+
`${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))});`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Incremental sync: unchanged transcripts are skipped via mtime+size, changed
|
|
176
|
+
// ones are deleted and re-inserted, removed ones are dropped. True on success
|
|
177
|
+
// (including nothing-to-do), false when sqlite3 or disk is unavailable.
|
|
178
|
+
export function syncUsageDb(): boolean {
|
|
179
|
+
let db: string;
|
|
180
|
+
try {
|
|
181
|
+
db = usageDbPath();
|
|
182
|
+
ensureSchema(db);
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const files: string[] = [];
|
|
187
|
+
try {
|
|
188
|
+
walkJsonl(sessionsDir(), files, 2000);
|
|
189
|
+
if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_CODEX_DIR !== undefined) {
|
|
190
|
+
walkJsonl(codexSessionsDir(), files, 2000);
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const known = readFiles(db);
|
|
196
|
+
const current: Record<string, { mtime: number; size: number }> = {};
|
|
197
|
+
const changed: string[] = [];
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
let st: fs.Stats;
|
|
200
|
+
try {
|
|
201
|
+
st = fs.statSync(file);
|
|
202
|
+
} catch {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const entry = { mtime: st.mtimeMs, size: st.size };
|
|
206
|
+
current[file] = entry;
|
|
207
|
+
const prev = known[file];
|
|
208
|
+
if (!prev || prev.mtime !== entry.mtime || prev.size !== entry.size) changed.push(file);
|
|
209
|
+
}
|
|
210
|
+
const removed = Object.keys(known).filter((p) => current[p] === undefined);
|
|
211
|
+
if (!changed.length && !removed.length) return true;
|
|
212
|
+
const chunks: string[] = [];
|
|
213
|
+
let chunkBytes = 0;
|
|
214
|
+
const flush = (): boolean => {
|
|
215
|
+
if (!chunks.length) return true;
|
|
216
|
+
try {
|
|
217
|
+
run(db, `BEGIN;${chunks.join('')}COMMIT;`);
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
chunks.length = 0;
|
|
222
|
+
chunkBytes = 0;
|
|
223
|
+
return true;
|
|
224
|
+
};
|
|
225
|
+
const push = (sql: string): boolean => {
|
|
226
|
+
chunks.push(sql);
|
|
227
|
+
chunkBytes += sql.length;
|
|
228
|
+
if (chunkBytes < 400_000) return true;
|
|
229
|
+
return flush();
|
|
230
|
+
};
|
|
231
|
+
for (const file of [...changed, ...removed]) {
|
|
232
|
+
if (!push(`DELETE FROM messages WHERE file=${esc(file)};DELETE FROM files WHERE path=${esc(file)};`)) return false;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
for (const file of changed) {
|
|
236
|
+
const entry = current[file];
|
|
237
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
238
|
+
for (const r of parseFile(text)) {
|
|
239
|
+
if (!push(insertSql(file, r))) return false;
|
|
240
|
+
}
|
|
241
|
+
if (!push(`INSERT OR REPLACE INTO files (path, mtime, size) VALUES (${esc(file)},${entry.mtime},${entry.size});`)) return false;
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
if (!push(`INSERT OR REPLACE INTO meta (k, v) VALUES ('last_sync',${esc(String(Date.now()))});`)) return false;
|
|
247
|
+
return flush();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function readUsageDb(): StoredUsage | null {
|
|
251
|
+
const db = usageDbPath();
|
|
252
|
+
try {
|
|
253
|
+
if (!fs.existsSync(db)) return null;
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
let rows: string[][];
|
|
258
|
+
try {
|
|
259
|
+
rows = query(db, `SELECT t, model, i, o, d, cr, cw, usd, st, code, note, tools FROM messages;`);
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const accum = newSessionAccum();
|
|
264
|
+
for (const [t, model, i, o, d, cr, cw, usd, st, code, note, tools] of rows) {
|
|
265
|
+
const ts = t === '' ? undefined : Number(t);
|
|
266
|
+
let toolNames: string[] = [];
|
|
267
|
+
try {
|
|
268
|
+
const parsed: unknown = JSON.parse(tools || '[]');
|
|
269
|
+
if (Array.isArray(parsed)) toolNames = parsed.filter((x): x is string => typeof x === 'string');
|
|
270
|
+
} catch { /* keep empty */ }
|
|
271
|
+
const usage: Record<string, unknown> = {
|
|
272
|
+
input: Number(i) || 0,
|
|
273
|
+
output: Number(o) || 0,
|
|
274
|
+
cacheRead: Number(cr) || 0,
|
|
275
|
+
cacheWrite: Number(cw) || 0,
|
|
276
|
+
};
|
|
277
|
+
if (usd !== '') {
|
|
278
|
+
const measured = Number(usd);
|
|
279
|
+
if (Number.isFinite(measured)) usage.cost = measured;
|
|
280
|
+
}
|
|
281
|
+
ingestSessionRow(
|
|
282
|
+
accum,
|
|
283
|
+
model || 'unknown',
|
|
284
|
+
usage,
|
|
285
|
+
Number.isFinite(ts) ? (ts as number) : undefined,
|
|
286
|
+
d === '' ? undefined : Number(d),
|
|
287
|
+
{
|
|
288
|
+
st: st === 'error' || st === 'aborted' ? st : 'completed',
|
|
289
|
+
code: code === '' ? undefined : Number(code),
|
|
290
|
+
note: note === '' ? undefined : note,
|
|
291
|
+
},
|
|
292
|
+
toolNames,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
accum.recent.sort((a, b) => b.t - a.t);
|
|
296
|
+
let syncedAt = 0;
|
|
297
|
+
try {
|
|
298
|
+
const meta = query(db, `SELECT v FROM meta WHERE k='last_sync';`);
|
|
299
|
+
if (meta.length) syncedAt = Number(meta[0][0]) || 0;
|
|
300
|
+
} catch { /* keep 0 */ }
|
|
301
|
+
return {
|
|
302
|
+
tokens: {
|
|
303
|
+
messages: accum.messages,
|
|
304
|
+
totals: accum.totals,
|
|
305
|
+
byModel: accum.byModel,
|
|
306
|
+
byDay: accum.byDay,
|
|
307
|
+
byDayModel: accum.byDayModel,
|
|
308
|
+
byTool: accum.byTool,
|
|
309
|
+
byModelMessages: accum.byModelMessages,
|
|
310
|
+
costMeasured: accum.costMeasured,
|
|
311
|
+
recent: accum.recent.slice(0, RECENT_LIMIT),
|
|
312
|
+
},
|
|
313
|
+
syncedAt,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Delete the store file. Returns 1 when removed, 0 when already absent.
|
|
318
|
+
export function clearUsageDb(): number {
|
|
319
|
+
try {
|
|
320
|
+
fs.unlinkSync(usageDbPath());
|
|
321
|
+
return 1;
|
|
322
|
+
} catch {
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// /tersio root command — status, check, update, gain, usage, help.
|
|
2
|
+
// Mode switches live on their own commands (/caveman, /rtk, /combo, and the
|
|
3
|
+
// upstream /ponytail): the router keeps no redundant copies. Shared-state
|
|
4
|
+
// publishes sync sibling mirrors live, so switches notify and take effect
|
|
5
|
+
// next turn — no reload.
|
|
6
|
+
import {
|
|
7
|
+
getSharedComboState,
|
|
8
|
+
reconcileSharedComboEntries,
|
|
9
|
+
sessionEntries,
|
|
10
|
+
} from '../shared/session-state.ts';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { appendUsage, readUsage } from '../shared/usage-ledger.ts';
|
|
14
|
+
import { checkAddonsSummary, runAddonUpdate } from '../ai-addons-updater/index.ts';
|
|
15
|
+
import type { ExtensionApi, ExtensionCtx } from '../shared/types.ts';
|
|
16
|
+
|
|
17
|
+
const HELP = [
|
|
18
|
+
'/tersio status — active modes + combo level',
|
|
19
|
+
'/tersio check — add-on version check',
|
|
20
|
+
'/tersio update <ponytail|rtk|caveman|all> [--dry-run]',
|
|
21
|
+
'/tersio gain — savings summary + dashboard hint',
|
|
22
|
+
'/tersio usage — ledger report for this machine',
|
|
23
|
+
'/tersio help — this table',
|
|
24
|
+
'mode switches: /caveman /rtk /combo (tersio) + /ponytail (upstream)',
|
|
25
|
+
].join('\n');
|
|
26
|
+
|
|
27
|
+
function statusLine(): string {
|
|
28
|
+
const s = getSharedComboState();
|
|
29
|
+
const level = s.level === 'custom' ? 'INACTIVE' : s.level.toUpperCase();
|
|
30
|
+
return `tersio — caveman=${s.caveman.toUpperCase()} · rtk=${s.rtk.toUpperCase()} · ponytail=${s.ponytail.toUpperCase()} (combo ${level})`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function usageSummary(): string {
|
|
34
|
+
const rows = readUsage();
|
|
35
|
+
if (rows.length === 0) return 'tersio usage: no ledger yet — run commands first.';
|
|
36
|
+
const byKind: Record<string, number> = {};
|
|
37
|
+
for (const r of rows) byKind[r.kind] = (byKind[r.kind] ?? 0) + 1;
|
|
38
|
+
const parts = Object.entries(byKind).map(([k, n]) => `${k}×${n}`);
|
|
39
|
+
const last = new Date(rows[rows.length - 1].ts).toISOString();
|
|
40
|
+
return `tersio usage: ${rows.length} rows (${parts.join(', ')}), last write ${last}.`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Gain opens instantaneously in the default browser: export a file://-ready
|
|
44
|
+
// snapshot (data inlined, no server to babysit) and open it. Falls back to
|
|
45
|
+
// the shell command when export or open fails.
|
|
46
|
+
async function openGainDashboard(pi: ExtensionApi, ctx?: ExtensionCtx): Promise<void> {
|
|
47
|
+
const file = path.join(os.tmpdir(), `tersio-gain-${Date.now()}.html`);
|
|
48
|
+
try {
|
|
49
|
+
const exported = await pi.exec?.('tersio', ['gain', '--export', file], { cwd: ctx?.cwd });
|
|
50
|
+
if (!exported || exported.code !== 0) throw new Error((exported?.stderr || 'export failed').trim());
|
|
51
|
+
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
52
|
+
await pi.exec?.(openCmd, [file], { cwd: ctx?.cwd });
|
|
53
|
+
ctx?.ui?.notify?.(`tersio gain: dashboard opened in your browser.`, 'info');
|
|
54
|
+
} catch (e) {
|
|
55
|
+
ctx?.ui?.notify?.(`tersio gain: could not open dashboard (${(e as Error).message}). Run 'tersio gain --open' in a shell.`, 'warning');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function tersioCommandsExtension(pi: ExtensionApi): void {
|
|
60
|
+
pi.setLabel?.('Tersio unified root command');
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
pi.registerCommand?.('tersio', {
|
|
64
|
+
description: 'Tersio root: status|check|update|gain|usage|help (modes: /caveman /rtk /combo /ponytail)',
|
|
65
|
+
handler: async (args, ctx) => {
|
|
66
|
+
const parts = String(args || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
67
|
+
const [sub] = [parts[0]];
|
|
68
|
+
appendUsage('command', `/tersio ${parts.join(' ')}`.trim());
|
|
69
|
+
|
|
70
|
+
if (!sub || sub === 'status') {
|
|
71
|
+
if (ctx?.hasUI) reconcileSharedComboEntries(sessionEntries(ctx));
|
|
72
|
+
ctx?.ui?.notify?.(statusLine(), 'info');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (sub === 'help') {
|
|
76
|
+
ctx?.ui?.notify?.(HELP, 'info');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (sub === 'check') {
|
|
80
|
+
await checkAddonsSummary(ctx);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (sub === 'update') {
|
|
84
|
+
const dryRun = parts.includes('--dry-run');
|
|
85
|
+
const target = parts.filter((p) => p !== '--dry-run' && p !== 'update' && p !== 'tersio').join(' ');
|
|
86
|
+
if (!target) {
|
|
87
|
+
ctx?.ui?.notify?.('Usage: /tersio update <ponytail|rtk|caveman|all> [--dry-run]', 'warning');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await runAddonUpdate(pi, ctx, target, dryRun);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (sub === 'gain') {
|
|
94
|
+
await openGainDashboard(pi, ctx);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (sub === 'usage') {
|
|
98
|
+
ctx?.ui?.notify?.(usageSummary(), 'info');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Removed mode switches redirect to their own commands so only one
|
|
102
|
+
// spelling exists to learn: /caveman, /rtk, /combo, /ponytail.
|
|
103
|
+
if (sub === 'caveman' || sub === 'rtk' || sub === 'combo' || sub === 'ponytail') {
|
|
104
|
+
ctx?.ui?.notify?.(`Use /${sub} instead — /tersio no longer duplicates mode switches.\n${HELP}`, 'warning');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
ctx?.ui?.notify?.(`Unknown subcommand: ${sub}.\n${HELP}`, 'warning');
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krtclcdy/tersio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.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",
|
|
7
|
-
"tersio",
|
|
8
7
|
"token-saving",
|
|
9
8
|
"ai-tools",
|
|
10
9
|
"developer-tools",
|
|
@@ -19,10 +18,6 @@
|
|
|
19
18
|
"type": "git",
|
|
20
19
|
"url": "git+https://github.com/KurutoDenzeru/tersio.git"
|
|
21
20
|
},
|
|
22
|
-
"homepage": "https://github.com/KurutoDenzeru/tersio",
|
|
23
|
-
"bugs": {
|
|
24
|
-
"url": "https://github.com/KurutoDenzeru/tersio/issues"
|
|
25
|
-
},
|
|
26
21
|
"type": "module",
|
|
27
22
|
"bin": {
|
|
28
23
|
"tersio": "tersio.js"
|
|
@@ -36,43 +31,43 @@
|
|
|
36
31
|
"description": "Caveman terse-reply session mode (/caveman)",
|
|
37
32
|
"default": true,
|
|
38
33
|
"extensions": [
|
|
39
|
-
"./extensions/caveman-session/index.
|
|
34
|
+
"./extensions/caveman-session/index.ts"
|
|
40
35
|
]
|
|
41
36
|
},
|
|
42
37
|
"rtk": {
|
|
43
38
|
"description": "RTK compact shell-output session mode (/rtk)",
|
|
44
39
|
"default": true,
|
|
45
40
|
"extensions": [
|
|
46
|
-
"./extensions/rtk-session/index.
|
|
41
|
+
"./extensions/rtk-session/index.ts"
|
|
47
42
|
]
|
|
48
43
|
},
|
|
49
44
|
"ponytail": {
|
|
50
|
-
"description": "Combo
|
|
45
|
+
"description": "Combo presets + Ponytail mode reinforcement (/combo)",
|
|
51
46
|
"default": true,
|
|
52
47
|
"extensions": [
|
|
53
|
-
"./extensions/combo-toggle/index.
|
|
54
|
-
"./extensions/shared/mode-reinforcement.
|
|
48
|
+
"./extensions/combo-toggle/index.ts",
|
|
49
|
+
"./extensions/shared/mode-reinforcement.ts"
|
|
55
50
|
]
|
|
56
51
|
},
|
|
57
52
|
"updater": {
|
|
58
|
-
"description": "In-session
|
|
53
|
+
"description": "In-session updater for Ponytail, RTK, and Caveman (/ai-addons)",
|
|
59
54
|
"default": true,
|
|
60
55
|
"extensions": [
|
|
61
|
-
"./extensions/ai-addons-updater/index.
|
|
56
|
+
"./extensions/ai-addons-updater/index.ts"
|
|
62
57
|
]
|
|
63
58
|
},
|
|
64
59
|
"tersio": {
|
|
65
|
-
"description": "Unified /tersio root command
|
|
60
|
+
"description": "Unified /tersio root command",
|
|
66
61
|
"default": true,
|
|
67
62
|
"extensions": [
|
|
68
|
-
"./extensions/tersio-commands/index.
|
|
63
|
+
"./extensions/tersio-commands/index.ts"
|
|
69
64
|
]
|
|
70
65
|
}
|
|
71
66
|
},
|
|
72
67
|
"settings": {
|
|
73
68
|
"comboDefault": {
|
|
74
69
|
"type": "enum",
|
|
75
|
-
"description": "
|
|
70
|
+
"description": "Session-start default (no saved state)",
|
|
76
71
|
"values": [
|
|
77
72
|
"off",
|
|
78
73
|
"medium",
|
|
@@ -83,7 +78,7 @@
|
|
|
83
78
|
},
|
|
84
79
|
"cavemanDefault": {
|
|
85
80
|
"type": "enum",
|
|
86
|
-
"description": "
|
|
81
|
+
"description": "Session-start default (no saved state)",
|
|
87
82
|
"values": [
|
|
88
83
|
"off",
|
|
89
84
|
"lite",
|
|
@@ -95,12 +90,12 @@
|
|
|
95
90
|
},
|
|
96
91
|
"rtkDefault": {
|
|
97
92
|
"type": "boolean",
|
|
98
|
-
"description": "
|
|
93
|
+
"description": "Session-start default (no saved state)",
|
|
99
94
|
"default": false
|
|
100
95
|
},
|
|
101
96
|
"ponytailDefault": {
|
|
102
97
|
"type": "enum",
|
|
103
|
-
"description": "
|
|
98
|
+
"description": "Session-start default; follows the combo preset unless overridden",
|
|
104
99
|
"values": [
|
|
105
100
|
"off",
|
|
106
101
|
"lite",
|
|
@@ -133,24 +128,24 @@
|
|
|
133
128
|
"tersio.js",
|
|
134
129
|
"cli/**/*.js",
|
|
135
130
|
"dashboard/**/*",
|
|
136
|
-
"extensions/**/*.
|
|
131
|
+
"extensions/**/*.ts"
|
|
137
132
|
],
|
|
138
133
|
"scripts": {
|
|
139
134
|
"clean": "rm -f tersio.js cli/*.js extensions/**/*.js",
|
|
140
135
|
"build": "tsc",
|
|
141
|
-
"prepublishOnly": "
|
|
136
|
+
"prepublishOnly": "bun run build",
|
|
142
137
|
"check": "tsc --noEmit",
|
|
143
138
|
"doctor": "node tersio.js --doctor",
|
|
144
139
|
"dry-run": "node tersio.js --dry-run --scope user --yes",
|
|
145
|
-
"test": "
|
|
140
|
+
"test": "vitest run"
|
|
146
141
|
},
|
|
147
142
|
"engines": {
|
|
148
143
|
"node": ">=20.12.0"
|
|
149
144
|
},
|
|
150
145
|
"devDependencies": {
|
|
151
146
|
"@types/node": "^26.4.1",
|
|
152
|
-
"
|
|
153
|
-
"
|
|
147
|
+
"typescript": "^7.0.2",
|
|
148
|
+
"vitest": "^5.0.1"
|
|
154
149
|
},
|
|
155
150
|
"dependencies": {
|
|
156
151
|
"@clack/prompts": "^1.8.0"
|