@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.
Files changed (58) hide show
  1. package/README.md +56 -45
  2. package/cli/agent-hosts.js +42 -0
  3. package/cli/agents.js +266 -0
  4. package/cli/banner.js +2 -3
  5. package/cli/common.js +16 -20
  6. package/cli/currency.js +11 -19
  7. package/cli/dashboard.js +216 -40
  8. package/cli/doctor-fix.js +40 -26
  9. package/cli/doctor.js +41 -41
  10. package/cli/hosts.js +114 -0
  11. package/cli/install.js +284 -179
  12. package/cli/interactive.js +62 -2
  13. package/cli/layer.js +22 -0
  14. package/cli/manifest.js +33 -0
  15. package/cli/omp-layer.js +51 -0
  16. package/cli/pi-layer.js +37 -0
  17. package/cli/pi-wiring.js +92 -0
  18. package/cli/profile.js +48 -48
  19. package/cli/reset.js +5 -32
  20. package/cli/rtk-wiring.js +7 -17
  21. package/cli/settings.js +3 -8
  22. package/cli/uninstall.js +162 -52
  23. package/cli/update.js +19 -27
  24. package/cli/usage.js +15 -5
  25. package/dashboard/dist/index.html +18 -18
  26. package/extensions/ai-addons-updater/index.js +37 -29
  27. package/extensions/ai-addons-updater/index.ts +39 -38
  28. package/extensions/caveman-session/index.js +19 -34
  29. package/extensions/caveman-session/index.ts +20 -35
  30. package/extensions/combo-toggle/index.js +47 -49
  31. package/extensions/combo-toggle/index.ts +42 -58
  32. package/extensions/lib/utils.js +40 -9
  33. package/extensions/lib/utils.ts +40 -9
  34. package/extensions/rtk-session/index.js +34 -38
  35. package/extensions/rtk-session/index.ts +32 -41
  36. package/extensions/shared/carbon.js +3 -11
  37. package/extensions/shared/carbon.ts +4 -11
  38. package/extensions/shared/host.js +58 -0
  39. package/extensions/shared/host.ts +94 -0
  40. package/extensions/shared/omp-prompt.js +12 -0
  41. package/extensions/shared/plugin-settings.js +33 -30
  42. package/extensions/shared/plugin-settings.ts +35 -26
  43. package/extensions/shared/pricing.js +25 -16
  44. package/extensions/shared/pricing.ts +27 -17
  45. package/extensions/shared/rtk-gain.js +6 -22
  46. package/extensions/shared/rtk-gain.ts +6 -22
  47. package/extensions/shared/session-state.js +44 -36
  48. package/extensions/shared/session-state.ts +48 -35
  49. package/extensions/shared/types.ts +10 -3
  50. package/extensions/shared/usage-ledger.js +160 -51
  51. package/extensions/shared/usage-ledger.ts +170 -67
  52. package/extensions/shared/usage-store.js +178 -24
  53. package/extensions/shared/usage-store.ts +176 -25
  54. package/extensions/tersio-commands/index.js +3 -6
  55. package/extensions/tersio-commands/index.ts +3 -6
  56. package/package.json +6 -5
  57. package/tersio.js +4 -6
  58. package/extensions/shared/mode-reinforcement.js +0 -26
package/cli/doctor.js CHANGED
@@ -1,17 +1,16 @@
1
1
  // cli/doctor.ts — installation health checks.
2
- import { promises as fs } from 'node:fs';
2
+ import { existsSync, promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
4
+ import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
5
+ import { detectHosts, ompPackageDir } from './hosts.js';
5
6
  import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
6
7
  import { usageDbPath } from '../extensions/shared/usage-store.js';
7
8
  import { pricesCachePath } from '../extensions/shared/pricing.js';
8
9
  import { readTextIfExists, resolveRtkBinary } from '../extensions/lib/utils.js';
9
10
  async function runDoctor(recheck = false) {
10
11
  console.log('\n=== Tersio Doctor ===');
11
- // Directories
12
- const agentDir = OMP_AGENT_DIR;
13
- const extDir = path.join(agentDir, 'extensions');
14
- const configPath = path.join(agentDir, 'config.yml');
12
+ // OMP loads the plugin from its plugins dir; pi loads the same package as a
13
+ // pi package, which its own settings.json declares and its npm dir holds.
15
14
  const pluginsDir = OMP_PLUGINS_DIR;
16
15
  const rtkBin = resolveRtkBinary();
17
16
  // Ponytail
@@ -24,15 +23,13 @@ async function runDoctor(recheck = false) {
24
23
  const updaterIndex = path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts');
25
24
  // Independent probes start concurrently; sections report in fixed order as
26
25
  // their data settles. Every probe resolves instead of rejecting.
26
+ const hosts = detectHosts();
27
27
  const probes = {
28
- agentEntries: fs.readdir(agentDir).catch(() => null),
29
- extEntries: fs.readdir(extDir).catch(() => null),
30
- sharedStateText: readTextIfExists(path.join(tersioPluginDir, 'extensions', 'shared', 'session-state.ts')),
31
- configText: readTextIfExists(configPath),
28
+ ompPkgText: readTextIfExists(path.join(ompPackageDir(), 'package.json')),
29
+ configText: readTextIfExists(path.join(OMP_AGENT_DIR, 'config.yml')),
32
30
  ponytailPkgText: readTextIfExists(ponytailPkg),
33
31
  ponytailExtText: readTextIfExists(ponytailExt),
34
32
  rtkBinText: rtkBin ? readTextIfExists(rtkBin) : Promise.resolve(null),
35
- rtkOmpText: readTextIfExists(path.join(extDir, 'rtk.ts')),
36
33
  cavemanIndexText: readTextIfExists(cavemanIndex),
37
34
  cavemanRuleText: readTextIfExists(cavemanRule),
38
35
  rtkIndexText: readTextIfExists(rtkIndex),
@@ -46,11 +43,9 @@ async function runDoctor(recheck = false) {
46
43
  const err = e;
47
44
  return err.stdout?.trim() || err.stderr?.trim() || null;
48
45
  }) : Promise.resolve(null);
49
- const [[agentEntries, extEntries, sharedStateText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText, ponytailExtText], [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, rtkOmpText, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
46
+ const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText, ponytailExtText], [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
50
47
  Promise.all([
51
- probes.agentEntries,
52
- probes.extEntries,
53
- probes.sharedStateText,
48
+ probes.ompPkgText,
54
49
  probes.configText,
55
50
  ]),
56
51
  Promise.all([
@@ -66,7 +61,6 @@ async function runDoctor(recheck = false) {
66
61
  probes.rtkBinText,
67
62
  probes.rtkMtime,
68
63
  rtkVersionProbe,
69
- probes.rtkOmpText,
70
64
  probes.ponytailMtime,
71
65
  probes.pricesMtime,
72
66
  ]),
@@ -94,28 +88,38 @@ async function runDoctor(recheck = false) {
94
88
  tally.warn++;
95
89
  console.log(` ⚠️ ${label}: warn ${detail}`);
96
90
  }
97
- section('Environment');
98
- check('Node', true, process.version);
99
- section('Installation');
100
- check('OMP agent dir', agentEntries !== null, agentEntries === null ? agentDir : '');
101
- check('OMP extensions dir', extEntries !== null, extEntries === null ? extDir : '');
102
- check('OMP config.yml', configText !== null, configText === null ? configPath : '');
103
- check('Shared session bridge', sharedStateText !== null);
91
+ // A host you do not use is not a fault, so it stays out of the tally and only
92
+ // carries the one line that installs it.
93
+ function unused(label, detail) {
94
+ console.log(` — ${label}: not installed ${detail}`);
95
+ }
96
+ section('Hosts');
97
+ for (const host of hosts) {
98
+ if (!host.installed) {
99
+ // A pi declaration without the tree or the package is a broken install,
100
+ // so it stays a counted row rather than reading as "not installed".
101
+ if (host.declared)
102
+ check(host.label, false, `declared (${host.declared}) but not installed`);
103
+ else
104
+ unused(host.label, `· ${host.installCmd}`);
105
+ continue;
106
+ }
107
+ const where = host.via === 'tree' ? `extensions in ${host.dir}` : `package ${PACKAGE_NAME}`;
108
+ check(host.label, true, `${where}${host.version ? ` ${host.version}` : ''}`);
109
+ }
104
110
  section('Extensions & plugins');
105
- const explicitEntries = (configText ?? '').split('\n')
106
- .map((line) => line.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, ''))
107
- .filter((line) => line.startsWith('/') || line.startsWith('.'));
108
- const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
109
- const retiredReinforcement = explicitEntries.filter((entry) => entry.endsWith('/shared/mode-reinforcement.ts')).length;
110
- check('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.join(', '));
111
- if (retiredReinforcement === 0)
112
- check('No retired reinforcement', true);
113
- else
114
- warnLine('Retired reinforcement registration', `${retiredReinforcement} found; run doctor --fix registrations`);
115
- check('Caveman extension', cavemanIndexText !== null);
116
- check('RTK extension', rtkIndexText !== null);
117
- check('Updater extension', updaterIndexText !== null);
118
- check('Ponytail extension', ponytailExtText !== null);
111
+ const ompEntry = hosts.find((host) => host.id === 'omp');
112
+ if (ompEntry?.via === 'package') {
113
+ const explicitEntries = (configText ?? '').split('\n')
114
+ .map((line) => line.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, ''))
115
+ .filter((line) => line.startsWith('/') || line.startsWith('.'));
116
+ const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
117
+ check('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.length ? duplicateExtensions.join(', ') : '');
118
+ check('Caveman extension', existsSync(path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts')), existsSync(path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts')) ? '' : path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts'));
119
+ check('RTK extension', existsSync(path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts')), existsSync(path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts')) ? '' : path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts'));
120
+ check('Updater extension', existsSync(path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts')), existsSync(path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts')) ? '' : path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts'));
121
+ check('Ponytail extension', existsSync(path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js')), existsSync(path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js')) ? '' : path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'));
122
+ }
119
123
  section('Usage & records');
120
124
  console.log(` Usage DB (tersio-owned · local hosted): ${usageDbPath()}`);
121
125
  const pricesAge = pricesMtime ? `(pulled ${relTime(Date.now() - pricesMtime.mtimeMs)} · ${absDate(pricesMtime.mtimeMs)})` : '';
@@ -127,10 +131,6 @@ async function runDoctor(recheck = false) {
127
131
  check('RTK binary', rtkBin !== null, rtkBinText === null ? 'not found in PATH' : [rtkVersion, rtkAge].filter(Boolean).join(' '));
128
132
  if (rtkBinText !== null && !rtkVersion)
129
133
  warnLine('RTK version', 'unavailable — binary may not be executable');
130
- const rtkRegistered = rtkOmpText !== null && (configText ?? '').includes('extensions/rtk.ts');
131
- check('RTK OMP wiring (rtk.ts)', rtkOmpText !== null, rtkOmpText === null ? 'run: rtk init -g --agent omp' : '');
132
- if (rtkOmpText !== null && !rtkRegistered)
133
- warnLine('RTK in config.yml', 'rtk.ts not listed — OMP will not load it; rerun install');
134
134
  const ponytailAge = ponytailMtime ? `(updated ${relTime(Date.now() - ponytailMtime.mtimeMs)} · ${absDate(ponytailMtime.mtimeMs)})` : '';
135
135
  const ponytailVer = parseJsonObject(ponytailPkgText)?.version ?? '';
136
136
  check('Ponytail', ponytailPkgText !== null, [ponytailVer, ponytailAge].filter(Boolean).join(' '));
package/cli/hosts.js ADDED
@@ -0,0 +1,114 @@
1
+ // cli/hosts.ts — the agent hosts Tersio installs into, and what is on disk.
2
+ // One detector, shared by the doctor and both menus, so they cannot disagree.
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { HOME, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION } from './common.js';
6
+ import { TREE_FILES } from './manifest.js';
7
+ import { piAgentDir } from '../extensions/lib/utils.js';
8
+ export function hostExtensionsDir(id, agentDir = piAgentDir()) {
9
+ return id === 'omp'
10
+ ? path.join(HOME, '.omp', 'agent', 'extensions')
11
+ : path.join(agentDir, 'extensions');
12
+ }
13
+ export function missingTreeFiles(id, agentDir = piAgentDir()) {
14
+ const dir = hostExtensionsDir(id, agentDir);
15
+ return TREE_FILES.filter((file) => !existsSync(path.join(dir, file)));
16
+ }
17
+ export function ompPackageDir() {
18
+ return path.join(OMP_PLUGINS_DIR, 'node_modules', ...PACKAGE_NAME.split('/'));
19
+ }
20
+ function piPackageDir(agentDir = piAgentDir()) {
21
+ return path.join(agentDir, 'npm', 'node_modules', ...PACKAGE_NAME.split('/'));
22
+ }
23
+ function piSettingsFile(agentDir = piAgentDir()) {
24
+ return path.join(agentDir, 'settings.json');
25
+ }
26
+ function packageVersion(dir) {
27
+ try {
28
+ const parsed = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
29
+ return typeof parsed.version === 'string' ? parsed.version : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ // The tersio package pi declared in its own settings.json, or null. pi
36
+ // identifies a source by name, so an npm spec, git URL, or path all count.
37
+ export function piTersioSource(agentDir = piAgentDir()) {
38
+ let settings;
39
+ try {
40
+ const parsed = JSON.parse(readFileSync(piSettingsFile(agentDir), 'utf8'));
41
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
42
+ return null;
43
+ settings = parsed;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ if (!Array.isArray(settings.packages))
49
+ return null;
50
+ for (const entry of settings.packages) {
51
+ const source = typeof entry === 'string' ? entry : entry?.source;
52
+ if (typeof source !== 'string')
53
+ continue;
54
+ const trimmed = source.trim().replace(/[/\\]+$/, '');
55
+ if (trimmed.includes(PACKAGE_NAME) || /(^|[/\\])tersio(\.git)?$/i.test(trimmed))
56
+ return source.trim();
57
+ }
58
+ return null;
59
+ }
60
+ // The hosts we install into, and the commands that add and remove each.
61
+ const HOSTS = [
62
+ { id: 'omp', label: 'Oh My Pi', bin: 'omp', installCmd: `omp plugin install ${PACKAGE_NAME}`, removeCmd: `omp plugin remove ${PACKAGE_NAME}` },
63
+ { id: 'pi', label: 'Pi', bin: 'pi', installCmd: `pi install npm:${PACKAGE_NAME}`, removeCmd: `pi remove npm:${PACKAGE_NAME}` },
64
+ ];
65
+ function detect(agentDir = piAgentDir()) {
66
+ return HOSTS.map((host) => {
67
+ const pkgDir = host.id === 'omp' ? ompPackageDir() : piPackageDir(agentDir);
68
+ const tree = hostExtensionsDir(host.id, agentDir);
69
+ // A tree wins: it is what the installer writes, and it carries no
70
+ // package.json, so its version is ours.
71
+ const via = missingTreeFiles(host.id, agentDir).length === 0
72
+ ? 'tree'
73
+ : existsSync(pkgDir) ? 'package' : null;
74
+ return {
75
+ ...host,
76
+ installed: via !== null,
77
+ via,
78
+ declared: host.id === 'omp'
79
+ ? (existsSync(pkgDir) ? `plugin ${PACKAGE_NAME}` : null)
80
+ : piTersioSource(agentDir),
81
+ version: via === 'tree' ? PACKAGE_VERSION : via === 'package' ? packageVersion(pkgDir) : null,
82
+ dir: via === 'tree' ? tree : via === 'package' ? pkgDir : null,
83
+ };
84
+ });
85
+ }
86
+ /** Menu label with current state in brackets, so a rerun shows what it replaces. */
87
+ export function hostLabel(host) {
88
+ if (host.installed && host.version)
89
+ return `${host.label} (installed ${host.version})`;
90
+ if (host.installed)
91
+ return `${host.label} (installed)`;
92
+ if (host.declared)
93
+ return `${host.label} (package declared, not on disk)`;
94
+ return `${host.label} (not installed)`;
95
+ }
96
+ // Hints render inline, so menus name the install and doctor keeps the path.
97
+ export function hostHint(host) {
98
+ if (host.installed && host.via)
99
+ return host.via === 'tree' ? 'extension tree' : 'plugin package';
100
+ return host.installCmd;
101
+ }
102
+ // `--host omp|pi` from raw argv; undefined when absent. Throws on an unknown
103
+ // value so the caller can exit(1) with the message.
104
+ export function parseHostArg(argv) {
105
+ const i = argv.indexOf('--host');
106
+ const raw = (i === -1 ? undefined : argv[i + 1]) ?? argv.find((a) => a.startsWith('--host='))?.slice('--host='.length);
107
+ if (raw === undefined)
108
+ return undefined;
109
+ const value = raw.trim().toLowerCase();
110
+ if (value !== 'omp' && value !== 'pi')
111
+ throw new Error(`Invalid --host: ${raw}. Valid: omp, pi`);
112
+ return value;
113
+ }
114
+ export { HOME, detect as detectHosts };