@krtclcdy/tersio 2.23.0 → 2.24.1
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 +56 -45
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/banner.js +2 -3
- package/cli/common.js +16 -20
- package/cli/currency.js +11 -19
- package/cli/dashboard.js +216 -40
- package/cli/doctor-fix.js +40 -26
- package/cli/doctor.js +41 -41
- package/cli/hosts.js +114 -0
- package/cli/install.js +284 -179
- package/cli/interactive.js +62 -2
- package/cli/layer.js +22 -0
- package/cli/manifest.js +33 -0
- package/cli/omp-layer.js +51 -0
- package/cli/pi-layer.js +37 -0
- package/cli/pi-wiring.js +92 -0
- package/cli/profile.js +48 -48
- package/cli/reset.js +5 -32
- package/cli/rtk-wiring.js +7 -17
- package/cli/settings.js +3 -8
- package/cli/uninstall.js +162 -52
- package/cli/update.js +19 -27
- package/cli/usage.js +15 -5
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +37 -29
- package/extensions/ai-addons-updater/index.ts +39 -38
- package/extensions/caveman-session/index.js +19 -34
- package/extensions/caveman-session/index.ts +20 -35
- package/extensions/combo-toggle/index.js +47 -49
- package/extensions/combo-toggle/index.ts +42 -58
- package/extensions/lib/utils.js +40 -9
- package/extensions/lib/utils.ts +40 -9
- package/extensions/rtk-session/index.js +34 -38
- package/extensions/rtk-session/index.ts +32 -41
- package/extensions/shared/carbon.js +3 -11
- package/extensions/shared/carbon.ts +4 -11
- package/extensions/shared/host.js +58 -0
- package/extensions/shared/host.ts +94 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +33 -30
- package/extensions/shared/plugin-settings.ts +35 -26
- package/extensions/shared/pricing.js +25 -16
- package/extensions/shared/pricing.ts +27 -17
- package/extensions/shared/rtk-gain.js +6 -22
- package/extensions/shared/rtk-gain.ts +6 -22
- package/extensions/shared/session-state.js +44 -36
- package/extensions/shared/session-state.ts +48 -35
- package/extensions/shared/types.ts +10 -3
- package/extensions/shared/usage-ledger.js +160 -51
- package/extensions/shared/usage-ledger.ts +170 -67
- package/extensions/shared/usage-store.js +178 -24
- package/extensions/shared/usage-store.ts +176 -25
- package/extensions/tersio-commands/index.js +3 -6
- package/extensions/tersio-commands/index.ts +3 -6
- package/package.json +6 -5
- package/tersio.js +4 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
package/cli/interactive.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cli/interactive.ts — TTY layer: readline, Clack spinners, selects, task phases.
|
|
2
2
|
import readline from 'node:readline';
|
|
3
|
-
import { cancel as clackCancel, confirm as clackConfirm, select as clackSelect, spinner as clackSpinner, tasks as clackTasks } from '@clack/prompts';
|
|
3
|
+
import { cancel as clackCancel, confirm as clackConfirm, log as clackLog, select as clackSelect, spinner as clackSpinner, tasks as clackTasks } from '@clack/prompts';
|
|
4
4
|
import { execP } from './common.js';
|
|
5
5
|
const RL = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6
6
|
let rlOpen = true;
|
|
@@ -67,6 +67,32 @@ async function askInteractiveConfirm(message, initialValue = true) {
|
|
|
67
67
|
}
|
|
68
68
|
return { status: 'confirmed', value: answer };
|
|
69
69
|
}
|
|
70
|
+
// Confirm a destructive run: a Clack dialog at a terminal, a plain prompt in a
|
|
71
|
+
// pipe or script. False means the user declined or aborted.
|
|
72
|
+
async function confirmDestructive(message) {
|
|
73
|
+
if (tty()) {
|
|
74
|
+
closeRL();
|
|
75
|
+
const answer = await clackConfirm({ message, initialValue: false });
|
|
76
|
+
if (typeof answer !== 'boolean') {
|
|
77
|
+
clackCancel('Aborted.');
|
|
78
|
+
closeRL();
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (!answer) {
|
|
82
|
+
console.log('Aborted.');
|
|
83
|
+
closeRL();
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const typed = await ask('\nProceed? [y/N]: ');
|
|
89
|
+
if (!typed.toLowerCase().startsWith('y')) {
|
|
90
|
+
console.log('Aborted.');
|
|
91
|
+
closeRL();
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
70
96
|
// Run collecting work under one TTY-only Clack task. Callers print after the
|
|
71
97
|
// task completes, keeping normal output out of the spinner animation.
|
|
72
98
|
async function runInteractivePhase(title, collect) {
|
|
@@ -77,7 +103,41 @@ async function runInteractivePhase(title, collect) {
|
|
|
77
103
|
await clackTasks([{ title, task: async () => { result = await collect(); } }]);
|
|
78
104
|
return result;
|
|
79
105
|
}
|
|
106
|
+
const TAG_KIND = {
|
|
107
|
+
fail: 'error', warn: 'warn', hint: 'info', ok: 'success', note: 'message',
|
|
108
|
+
write: 'step', rm: 'step', migrate: 'step', skip: 'info', 'dry-run': 'info',
|
|
109
|
+
};
|
|
110
|
+
function sayTagged(line) {
|
|
111
|
+
const m = /^\s*\[([\w-]+)\]\s?([\s\S]*)$/.exec(line);
|
|
112
|
+
const kind = (m && TAG_KIND[m[1]]) || 'info';
|
|
113
|
+
if (tty() && m)
|
|
114
|
+
clackLog[kind](m[2]);
|
|
115
|
+
else
|
|
116
|
+
console.log(line);
|
|
117
|
+
}
|
|
118
|
+
// One live-updating task line instead of a spinner per step.
|
|
119
|
+
async function withInteractiveTask(title, work) {
|
|
120
|
+
if (!tty())
|
|
121
|
+
return work(() => { });
|
|
122
|
+
closeRL();
|
|
123
|
+
let result;
|
|
124
|
+
const spin = clackSpinner();
|
|
125
|
+
spin.start(title);
|
|
126
|
+
try {
|
|
127
|
+
result = await work((message) => spin.message(message));
|
|
128
|
+
spin.stop('done');
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
spin.error(shortSpinnerError(e));
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
function shortSpinnerError(e) {
|
|
137
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
138
|
+
return (msg.split('\n')[0] || 'failed').slice(0, 120);
|
|
139
|
+
}
|
|
80
140
|
async function execNetwork(label, cmd, args, opts = {}) {
|
|
81
141
|
return withInteractiveSpinner(label, () => execP(cmd, args, opts));
|
|
82
142
|
}
|
|
83
|
-
export { ask, closeRL, tty, withInteractiveSpinner, execNetwork, askInteractiveChoice, askInteractiveConfirm, runInteractivePhase, };
|
|
143
|
+
export { ask, closeRL, tty, withInteractiveSpinner, withInteractiveTask, execNetwork, sayTagged, askInteractiveChoice, askInteractiveConfirm, confirmDestructive, runInteractivePhase, };
|
package/cli/layer.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// cli/layer.ts — the shared shape of an extension tree.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
// Turns a `[dir, label]` table into resolved paths under `extDir`.
|
|
4
|
+
export function layerEntries(extDir, table) {
|
|
5
|
+
return table.map(([dir, label]) => ({ label, path: path.join(extDir, dir) }));
|
|
6
|
+
}
|
|
7
|
+
// Directory names only, for callers that key off the names alone.
|
|
8
|
+
export function layerDirs(table) {
|
|
9
|
+
return table.map(([dir]) => dir);
|
|
10
|
+
}
|
|
11
|
+
// A port source sits two levels below extensions/, one level once installed.
|
|
12
|
+
export function rewritePortImports(content) {
|
|
13
|
+
return content.replace(/from '\.\.\/\.\.\/(shared|lib)\//g, "from '../$1/");
|
|
14
|
+
}
|
|
15
|
+
// What of the layer is on disk.
|
|
16
|
+
export function reportLayer(extDir, tables, exists) {
|
|
17
|
+
const all = tables.flatMap((table) => layerEntries(extDir, table));
|
|
18
|
+
return {
|
|
19
|
+
present: all.filter((entry) => exists(entry.path)),
|
|
20
|
+
missing: all.filter((entry) => !exists(entry.path)),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/cli/manifest.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Single list of install writes, shared by installer, doctor, and tests.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
|
|
5
|
+
/** Every file in the tree, as paths relative to the extensions directory. */
|
|
6
|
+
export const TREE_FILES = [
|
|
7
|
+
'shared/host.ts',
|
|
8
|
+
'shared/session-state.ts',
|
|
9
|
+
'shared/types.ts',
|
|
10
|
+
'shared/plugin-settings.ts',
|
|
11
|
+
'shared/usage-ledger.ts',
|
|
12
|
+
'shared/pricing.ts',
|
|
13
|
+
'shared/carbon.ts',
|
|
14
|
+
'lib/utils.ts',
|
|
15
|
+
'caveman-session/index.ts',
|
|
16
|
+
'caveman-session/rule.md',
|
|
17
|
+
'rtk-session/index.ts',
|
|
18
|
+
'combo-toggle/index.ts',
|
|
19
|
+
'tersio-commands/index.ts',
|
|
20
|
+
'ai-addons-updater/index.ts',
|
|
21
|
+
];
|
|
22
|
+
export function sourcePath(relative) {
|
|
23
|
+
return path.join(EXT_DIR, ...relative.split('/'));
|
|
24
|
+
}
|
|
25
|
+
/** The entries under one directory, as source paths and tree-relative targets. */
|
|
26
|
+
export function filesUnder(dir) {
|
|
27
|
+
const found = [];
|
|
28
|
+
for (const file of TREE_FILES) {
|
|
29
|
+
if (file.startsWith(`${dir}/`))
|
|
30
|
+
found.push([sourcePath(file), file]);
|
|
31
|
+
}
|
|
32
|
+
return found;
|
|
33
|
+
}
|
package/cli/omp-layer.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The Oh My Pi layer as data: the extension directories, the RTK wiring and the Ponytail package, named in one place...
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { layerEntries, reportLayer } from './layer.js';
|
|
4
|
+
const INSTALLED_EXTENSIONS = [
|
|
5
|
+
['caveman-session', 'caveman session mode'],
|
|
6
|
+
['rtk-session', 'rtk session mode'],
|
|
7
|
+
['ai-addons-updater', 'add-on updater'],
|
|
8
|
+
['combo-toggle', 'combo preset switch'],
|
|
9
|
+
['tersio-commands', '/tersio command'],
|
|
10
|
+
['shared', 'session bridge, usage and pricing store'],
|
|
11
|
+
// Read by the add-on updater; the updater itself is removed above.
|
|
12
|
+
['lib', 'shared installer and updater helpers'],
|
|
13
|
+
];
|
|
14
|
+
const RETIRED_EXTENSIONS = [
|
|
15
|
+
// Legacy always-on combo helper; imports shared/session-state.js, so it breaks with a module-not-found warning once the...
|
|
16
|
+
['aaa-combo-boot', 'retired combo boot helper'],
|
|
17
|
+
];
|
|
18
|
+
// `rtkBinaryName` is passed in because it is platform-dependent and already resolved by the caller.
|
|
19
|
+
export function ompLayer(home, rtkBinaryName) {
|
|
20
|
+
const agentDir = path.join(home, '.omp', 'agent');
|
|
21
|
+
const extDir = path.join(agentDir, 'extensions');
|
|
22
|
+
const pluginsDir = path.join(home, '.omp', 'plugins');
|
|
23
|
+
return {
|
|
24
|
+
extDir,
|
|
25
|
+
configFile: path.join(agentDir, 'config.yml'),
|
|
26
|
+
pluginsDir,
|
|
27
|
+
ponytailPackage: path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail'),
|
|
28
|
+
rtkBinary: path.join(home, '.bun', 'bin', rtkBinaryName),
|
|
29
|
+
rtkExtension: path.join(extDir, 'rtk.ts'),
|
|
30
|
+
installed: layerEntries(extDir, INSTALLED_EXTENSIONS),
|
|
31
|
+
retired: layerEntries(extDir, RETIRED_EXTENSIONS),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Every extension directory the layer owns, installed and retired together.
|
|
35
|
+
export function ompExtensionTargets(layer) {
|
|
36
|
+
return [...layer.installed, ...layer.retired].map((entry) => entry.path);
|
|
37
|
+
}
|
|
38
|
+
export function tersioPluginDir(home) {
|
|
39
|
+
return path.join(home, '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio');
|
|
40
|
+
}
|
|
41
|
+
export function reportOmpLayer(home, exists) {
|
|
42
|
+
const extDir = path.join(home, '.omp', 'agent', 'extensions');
|
|
43
|
+
const pluginPath = tersioPluginDir(home);
|
|
44
|
+
const { present, missing } = reportLayer(extDir, [INSTALLED_EXTENSIONS], exists);
|
|
45
|
+
return {
|
|
46
|
+
pluginPath: exists(pluginPath) ? pluginPath : null,
|
|
47
|
+
extensions: present,
|
|
48
|
+
missing,
|
|
49
|
+
installed: exists(pluginPath),
|
|
50
|
+
};
|
|
51
|
+
}
|
package/cli/pi-layer.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// cli/pi-layer.ts — the Pi extension layer as data: the five extension directories plus shared/ and lib/.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { layerDirs, layerEntries, reportLayer } from './layer.js';
|
|
4
|
+
const INSTALLED_EXTENSIONS = [
|
|
5
|
+
['caveman-session', 'caveman session mode'],
|
|
6
|
+
['rtk-session', 'rtk session mode'],
|
|
7
|
+
['ai-addons-updater', 'add-on updater'],
|
|
8
|
+
['combo-toggle', 'combo preset switch'],
|
|
9
|
+
['tersio-commands', '/tersio command'],
|
|
10
|
+
];
|
|
11
|
+
// Imported by the extension directories but never loaded by Pi itself, so they stay out of the install list and only...
|
|
12
|
+
const MODULE_DIRECTORIES = [
|
|
13
|
+
['shared', 'Pi types, session bridge, usage and pricing store'],
|
|
14
|
+
['lib', 'shared installer and updater helpers'],
|
|
15
|
+
];
|
|
16
|
+
export const PI_EXTENSION_DIRS = layerDirs(INSTALLED_EXTENSIONS);
|
|
17
|
+
export const PI_MODULE_DIRS = layerDirs(MODULE_DIRECTORIES);
|
|
18
|
+
export function piLayer(home) {
|
|
19
|
+
const relocated = process.env.PI_CODING_AGENT_DIR;
|
|
20
|
+
const base = relocated && relocated.trim() !== '' ? relocated : path.join(home, '.pi', 'agent');
|
|
21
|
+
const extDir = path.join(base, 'extensions');
|
|
22
|
+
return {
|
|
23
|
+
extDir,
|
|
24
|
+
rtkExtension: path.join(extDir, 'rtk.ts'),
|
|
25
|
+
installed: layerEntries(extDir, INSTALLED_EXTENSIONS),
|
|
26
|
+
modules: layerEntries(extDir, MODULE_DIRECTORIES),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Every directory the layer owns. Removal walks this list and nothing else.
|
|
30
|
+
export function piExtensionTargets(layer) {
|
|
31
|
+
return [...layer.installed, ...layer.modules].map((entry) => entry.path);
|
|
32
|
+
}
|
|
33
|
+
// Reports the layer without needing a platform rtk binary name.
|
|
34
|
+
export function reportPiLayer(home, exists) {
|
|
35
|
+
const { present, missing } = reportLayer(piLayer(home).extDir, [INSTALLED_EXTENSIONS, MODULE_DIRECTORIES], exists);
|
|
36
|
+
return { extensions: present, missing, installed: present.length > 0 };
|
|
37
|
+
}
|
package/cli/pi-wiring.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Install and remove Tersio's Pi extension tree: Pi loads ~/.pi/agent/extensions/<dir>/index.ts through jiti, no build...
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
5
|
+
import { rewritePortImports } from './layer.js';
|
|
6
|
+
import { piExtensionTargets, piLayer } from './pi-layer.js';
|
|
7
|
+
// `$HOME`-relative form of a path, for output that must stay readable.
|
|
8
|
+
function rel(target, home) {
|
|
9
|
+
if (!home)
|
|
10
|
+
return target;
|
|
11
|
+
const prefix = home.endsWith(path.sep) ? home : `${home}${path.sep}`;
|
|
12
|
+
return target.startsWith(prefix) ? `~/${target.slice(prefix.length).split(path.sep).join('/')}` : target;
|
|
13
|
+
}
|
|
14
|
+
export function piExtensionDir(home) {
|
|
15
|
+
return piLayer(home).extDir;
|
|
16
|
+
}
|
|
17
|
+
// Writes only when the content differs, leaving a `.bak` of what it replaced.
|
|
18
|
+
async function writeIfChanged(target, content, options) {
|
|
19
|
+
const current = await readTextIfExists(target);
|
|
20
|
+
if (current === content)
|
|
21
|
+
return false;
|
|
22
|
+
if (options.dryRun)
|
|
23
|
+
return true;
|
|
24
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
25
|
+
if (current !== null)
|
|
26
|
+
await fs.copyFile(target, `${target}.bak`).catch(() => { });
|
|
27
|
+
await fs.writeFile(target, content, 'utf8');
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
async function exists(target) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.stat(target);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Returns the paths it changed, so a no-op re-run is not counted as work.
|
|
40
|
+
export async function installPiTersio(home, tree, options = {}) {
|
|
41
|
+
const extDir = piExtensionDir(home);
|
|
42
|
+
const written = [];
|
|
43
|
+
for (const [from, to] of tree.sources) {
|
|
44
|
+
const content = await readTextIfExists(from);
|
|
45
|
+
if (content === null) {
|
|
46
|
+
// Named by its path under the extension dir, never by the repo source.
|
|
47
|
+
if (!options.quiet) {
|
|
48
|
+
console.log(` [fail] Pi extension source missing: ${to}`);
|
|
49
|
+
console.log(' [hint] Reinstall tersio — extensions/pi ships with the CLI');
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (await writeIfChanged(path.join(extDir, to), rewritePortImports(content), options))
|
|
54
|
+
written.push(to);
|
|
55
|
+
}
|
|
56
|
+
for (const [dir, rule] of tree.rules) {
|
|
57
|
+
if (rule === null)
|
|
58
|
+
continue;
|
|
59
|
+
const target = path.join(dir, 'rule.md');
|
|
60
|
+
if (await writeIfChanged(path.join(extDir, target), rule, options))
|
|
61
|
+
written.push(target);
|
|
62
|
+
}
|
|
63
|
+
if (written.length > 0 && !options.quiet) {
|
|
64
|
+
console.log(` ${options.dryRun ? '[dry-run] would write' : '[write]'} Pi — ${written.length} file(s) under ${rel(extDir, home)}`);
|
|
65
|
+
}
|
|
66
|
+
return written;
|
|
67
|
+
}
|
|
68
|
+
// Removes the Pi tree: every directory the layer owns, the `.bak` copies, and the flat module a pre-tree install wrote at...
|
|
69
|
+
export async function removePiTersio(home, options = {}) {
|
|
70
|
+
let removed = false;
|
|
71
|
+
for (const target of [...piExtensionTargets(piLayer(home)), path.join(piExtensionDir(home), 'tersio.ts')]) {
|
|
72
|
+
if (!(await exists(target)))
|
|
73
|
+
continue;
|
|
74
|
+
if (!options.quiet) {
|
|
75
|
+
console.log(` ${options.dryRun ? '[dry-run] would remove' : '[rm]'} ${rel(target, home)}`);
|
|
76
|
+
}
|
|
77
|
+
if (!options.dryRun) {
|
|
78
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
79
|
+
await fs.rm(`${target}.bak`, { force: true, recursive: true });
|
|
80
|
+
}
|
|
81
|
+
removed = true;
|
|
82
|
+
}
|
|
83
|
+
return removed;
|
|
84
|
+
}
|
|
85
|
+
// True when any module of the Pi tree is present.
|
|
86
|
+
export async function piTersioInstalled(home) {
|
|
87
|
+
for (const entry of piLayer(home).installed) {
|
|
88
|
+
if (await exists(path.join(entry.path, 'index.ts')))
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
package/cli/profile.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
// cli/profile.ts — session-start defaults
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
// Extracted from cli/install.ts so both install and settings share it.
|
|
5
|
-
import { promises as fs } from 'node:fs';
|
|
1
|
+
// cli/profile.ts — session-start defaults and the display-currency default,
|
|
2
|
+
// stored in ~/.tersio/settings.json. Shared by install and settings.
|
|
3
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
6
4
|
import path from 'node:path';
|
|
7
|
-
import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES,
|
|
5
|
+
import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, PACKAGE_NAME, PONYTAIL_DEFAULTS, verbose, } from './common.js';
|
|
6
|
+
import os from 'node:os';
|
|
8
7
|
import { DEFAULT_CURRENCY, isCurrencyCode } from './currency.js';
|
|
9
8
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
9
|
+
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
10
|
+
export const BACKUP_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
10
11
|
function defaultProfile() {
|
|
11
12
|
return {
|
|
12
13
|
comboDefault: 'off',
|
|
@@ -14,31 +15,17 @@ function defaultProfile() {
|
|
|
14
15
|
rtkDefault: false,
|
|
15
16
|
ponytailDefault: 'off',
|
|
16
17
|
currency: DEFAULT_CURRENCY,
|
|
18
|
+
backupSchedule: 'monthly',
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
|
-
//
|
|
20
|
-
// or prompts must preserve the user's choice, never reset it to off.
|
|
21
|
+
// Seed once from OMP plugin settings so a pre-~/.tersio install keeps its values.
|
|
21
22
|
async function storedProfile() {
|
|
22
23
|
const base = defaultProfile();
|
|
23
|
-
const
|
|
24
|
-
|
|
24
|
+
const stored = (await readTextIfExists(tersioSettingsFile())) !== null
|
|
25
|
+
? parseStored(await readTextIfExists(tersioSettingsFile()))
|
|
26
|
+
: parseStored(await readTextIfExists(legacyOmpLockPath()));
|
|
27
|
+
if (!stored)
|
|
25
28
|
return base;
|
|
26
|
-
let stored;
|
|
27
|
-
try {
|
|
28
|
-
const parsed = JSON.parse(raw);
|
|
29
|
-
if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
|
|
30
|
-
return base;
|
|
31
|
-
const settings = parsed.settings;
|
|
32
|
-
if (!settings || typeof settings !== 'object' || !(PACKAGE_NAME in settings))
|
|
33
|
-
return base;
|
|
34
|
-
const entry = settings[PACKAGE_NAME];
|
|
35
|
-
if (!entry || typeof entry !== 'object')
|
|
36
|
-
return base;
|
|
37
|
-
stored = entry;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return base;
|
|
41
|
-
}
|
|
42
29
|
if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
|
|
43
30
|
base.comboDefault = stored.comboDefault;
|
|
44
31
|
if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
|
|
@@ -50,42 +37,55 @@ async function storedProfile() {
|
|
|
50
37
|
if (typeof stored.currency === 'string' && isCurrencyCode(stored.currency.trim().toUpperCase())) {
|
|
51
38
|
base.currency = stored.currency.trim().toUpperCase();
|
|
52
39
|
}
|
|
40
|
+
if (typeof stored.backupSchedule === 'string' && BACKUP_SCHEDULES.has(stored.backupSchedule)) {
|
|
41
|
+
base.backupSchedule = stored.backupSchedule;
|
|
42
|
+
}
|
|
53
43
|
return base;
|
|
54
44
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
45
|
+
function legacyOmpLockPath() {
|
|
46
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
47
|
+
return [
|
|
48
|
+
path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
|
|
49
|
+
path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
|
|
50
|
+
].find((p) => existsSync(p)) ?? path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json');
|
|
51
|
+
}
|
|
52
|
+
function parseStored(raw) {
|
|
53
|
+
if (!raw)
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
if (!parsed || typeof parsed !== 'object')
|
|
58
|
+
return null;
|
|
59
|
+
const direct = parsed;
|
|
60
|
+
const nested = direct.settings?.[PACKAGE_NAME];
|
|
61
|
+
return (nested ?? direct);
|
|
69
62
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Persist the profile where every host reads it. `omp plugin config get` no
|
|
68
|
+
// longer reports it; `tersio settings` does.
|
|
69
|
+
async function writePluginSettings(profile, options) {
|
|
70
|
+
const file = tersioSettingsFile();
|
|
71
|
+
const values = {
|
|
72
|
+
...(parseStored(await readTextIfExists(file)) ?? {}),
|
|
74
73
|
comboDefault: profile.comboDefault,
|
|
75
74
|
comboSetupComplete: true,
|
|
76
75
|
cavemanDefault: profile.cavemanDefault,
|
|
77
76
|
rtkDefault: profile.rtkDefault,
|
|
78
77
|
ponytailDefault: profile.ponytailDefault,
|
|
79
78
|
currency: profile.currency,
|
|
79
|
+
backupSchedule: profile.backupSchedule,
|
|
80
80
|
};
|
|
81
81
|
if (options.dryRun) {
|
|
82
82
|
if (verbose && !options.quiet)
|
|
83
|
-
console.log(` [dry-run] would write
|
|
83
|
+
console.log(` [dry-run] would write session defaults to ${file}`);
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
|
-
await fs.mkdir(
|
|
87
|
-
await fs.writeFile(
|
|
88
|
-
console.log(` [write]
|
|
86
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
87
|
+
await fs.writeFile(file, `${JSON.stringify(values, null, 2)}\n`, 'utf8');
|
|
88
|
+
console.log(` [write] Session defaults in ${file}`);
|
|
89
89
|
}
|
|
90
90
|
function formatProfile(profile) {
|
|
91
91
|
return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault}) · currency=${profile.currency}`;
|
package/cli/reset.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
// watermark that filters session-derived and RTK-metered rows out of every
|
|
3
|
-
// derived view. Session transcripts (~/.omp/agent/sessions) and the RTK
|
|
4
|
-
// database are host/tool-owned and are never touched; the watermark only
|
|
5
|
-
// changes what tersio shows.
|
|
6
|
-
import { cancel as clackCancel, confirm as clackConfirm } from '@clack/prompts';
|
|
1
|
+
// Clears tersio-owned stats only; transcripts and RTK data stay.
|
|
7
2
|
import { dryRun, yes } from './common.js';
|
|
8
|
-
import {
|
|
3
|
+
import { confirmDestructive, sayTagged } from './interactive.js';
|
|
9
4
|
import { clearUsageLedger, importSessionTokens, ledgerPath, markReset, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
10
5
|
import { clearUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
11
6
|
import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
@@ -25,34 +20,12 @@ async function runReset() {
|
|
|
25
20
|
console.log('Nothing to reset — no statistics recorded.');
|
|
26
21
|
return true;
|
|
27
22
|
}
|
|
28
|
-
if (!yes)
|
|
29
|
-
|
|
30
|
-
closeRL();
|
|
31
|
-
const confirmedChoice = await clackConfirm({ message: 'Clear tersio statistics?', initialValue: false });
|
|
32
|
-
if (typeof confirmedChoice !== 'boolean') {
|
|
33
|
-
clackCancel('Aborted.');
|
|
34
|
-
closeRL();
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
if (!confirmedChoice) {
|
|
38
|
-
console.log('Aborted.');
|
|
39
|
-
closeRL();
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
const answer = await ask('\nProceed? [y/N]: ');
|
|
45
|
-
if (!answer.toLowerCase().startsWith('y')) {
|
|
46
|
-
console.log('Aborted.');
|
|
47
|
-
closeRL();
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
23
|
+
if (!yes && !(await confirmDestructive('Clear tersio statistics?')))
|
|
24
|
+
return false;
|
|
52
25
|
const cleared = clearUsageLedger();
|
|
53
26
|
const dbCleared = clearUsageDb();
|
|
54
27
|
const ts = markReset();
|
|
55
|
-
|
|
28
|
+
sayTagged(`[ok] reset — removed ${cleared} usage rows${dbCleared ? ' + usage.db' : ''}; statistics view starts at ${new Date(ts).toISOString()}`);
|
|
56
29
|
return true;
|
|
57
30
|
}
|
|
58
31
|
export { runReset };
|
package/cli/rtk-wiring.js
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// `rtk init -g --agent omp` writes rtk's tool_call extension to
|
|
3
|
-
// ~/.omp/agent/extensions/rtk.ts, and we register that path in config.yml
|
|
4
|
-
// (OMP only loads listed extensions — rtk init does not register it).
|
|
5
|
-
// Once loaded, OMP rewrites bash commands to rtk before execution, so every
|
|
6
|
-
// rewritten call lands in rtk's history.db and shows metered savings.
|
|
7
|
-
// Fail-open: a failed wire only downgrades to manual `rtk` prefixing.
|
|
8
|
-
// Deliberately free of cli/common.ts imports (argv side effects) so tests
|
|
9
|
-
// can load it directly.
|
|
1
|
+
// OMP loads only listed extensions, so register rtk's path; fail-open.
|
|
10
2
|
import { promises as fs } from 'node:fs';
|
|
11
3
|
import os from 'node:os';
|
|
12
4
|
import path from 'node:path';
|
|
@@ -34,8 +26,7 @@ function execFileP(cmd, args, timeout) {
|
|
|
34
26
|
});
|
|
35
27
|
});
|
|
36
28
|
}
|
|
37
|
-
// Idempotent: rtk rewrites its
|
|
38
|
-
// reinstalling tersio refreshes the wiring for free.
|
|
29
|
+
// Idempotent: rtk rewrites its file on every init run.
|
|
39
30
|
export async function wireRtkOmp(rtkBin, options = {}) {
|
|
40
31
|
if (!options.dryRun)
|
|
41
32
|
debugWire(options, 'Wiring rtk → OMP (bash tool_call rewrite)…');
|
|
@@ -47,14 +38,13 @@ export async function wireRtkOmp(rtkBin, options = {}) {
|
|
|
47
38
|
await ensureRtkInConfig(options);
|
|
48
39
|
return true;
|
|
49
40
|
}
|
|
50
|
-
// The rtk.ts path rtk init writes (
|
|
51
|
-
// without importing it — see header note on argv side effects).
|
|
41
|
+
// The rtk.ts path rtk init writes (OMP_AGENT_DIR inlined; see header note).
|
|
52
42
|
function rtkExtensionPath() {
|
|
53
43
|
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
54
44
|
return path.join(home, '.omp', 'agent', 'extensions', 'rtk.ts');
|
|
55
45
|
}
|
|
56
|
-
//
|
|
57
|
-
//
|
|
46
|
+
// Append rtk.ts after the existing entries (or create the key) so the wire
|
|
47
|
+
// takes effect on the next OMP start.
|
|
58
48
|
export async function ensureRtkInConfig(options) {
|
|
59
49
|
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
60
50
|
const configPath = path.join(home, '.omp', 'agent', 'config.yml');
|
|
@@ -98,8 +88,8 @@ async function runRtkInit(rtkBin, options) {
|
|
|
98
88
|
return true;
|
|
99
89
|
}
|
|
100
90
|
catch (first) {
|
|
101
|
-
// A
|
|
102
|
-
//
|
|
91
|
+
// A fresh binary can lose its first exec to macOS Gatekeeper, so settle and
|
|
92
|
+
// retry once before reporting failure.
|
|
103
93
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
104
94
|
try {
|
|
105
95
|
await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
|
package/cli/settings.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
// cli/settings.ts — view/edit session-start defaults
|
|
2
|
-
// plus the display-currency default for usage/dashboard reports.
|
|
3
|
-
// Non-interactive: `tersio settings --combo-default balanced` (plus per-mode
|
|
4
|
-
// override flags and --currency). Interactive: clack selects seeded from the
|
|
5
|
-
// stored profile.
|
|
1
|
+
// cli/settings.ts — view/edit session-start defaults and the display currency.
|
|
6
2
|
import path from 'node:path';
|
|
7
3
|
import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, } from './common.js';
|
|
8
4
|
import { CURRENCY_CODES } from './currency.js';
|
|
@@ -44,6 +40,7 @@ async function askProfile(current) {
|
|
|
44
40
|
rtkDefault: preset.rtk,
|
|
45
41
|
ponytailDefault: preset.ponytail,
|
|
46
42
|
currency: current.currency,
|
|
43
|
+
backupSchedule: current.backupSchedule,
|
|
47
44
|
};
|
|
48
45
|
const caveman = await askInteractiveChoice('Caveman default', [...CAVEMAN_DEFAULTS].map((v) => ({
|
|
49
46
|
value: v, label: v,
|
|
@@ -162,9 +159,7 @@ async function runSingleSetting(name, current, nextDiag) {
|
|
|
162
159
|
}
|
|
163
160
|
function printSettingsTable(current) {
|
|
164
161
|
console.log('\n=== Tersio Settings ===');
|
|
165
|
-
//
|
|
166
|
-
// persisting it (its picker POSTs to /currency); settings can still set
|
|
167
|
-
// the stored default via --currency or the prompt below.
|
|
162
|
+
// The dashboard owns displaying currency; settings only sets the default.
|
|
168
163
|
const rows = [
|
|
169
164
|
['combo', current.comboDefault, [...COMBO_DEFAULTS].join(' | ')],
|
|
170
165
|
['caveman', current.cavemanDefault, [...CAVEMAN_DEFAULTS].join(' | ')],
|