@krtclcdy/tersio 2.24.0 → 2.25.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.
Files changed (63) hide show
  1. package/README.md +28 -24
  2. package/cli/banner.js +7 -5
  3. package/cli/common.js +14 -13
  4. package/cli/currency.js +2 -7
  5. package/cli/dashboard.js +220 -13
  6. package/cli/doctor-fix.js +99 -29
  7. package/cli/doctor.js +32 -35
  8. package/cli/hosts.js +23 -26
  9. package/cli/install.js +228 -115
  10. package/cli/interactive.js +65 -21
  11. package/cli/manifest.js +8 -4
  12. package/cli/profile.js +49 -16
  13. package/cli/reset.js +3 -7
  14. package/cli/rtk-wiring.js +29 -32
  15. package/cli/settings.js +40 -16
  16. package/cli/uninstall.js +79 -29
  17. package/cli/update.js +27 -20
  18. package/cli/usage.js +54 -19
  19. package/dashboard/dist/index.html +18 -18
  20. package/extensions/ai-addons-updater/index.js +75 -20
  21. package/extensions/ai-addons-updater/index.ts +67 -23
  22. package/extensions/caveman-session/index.js +33 -75
  23. package/extensions/caveman-session/index.ts +35 -77
  24. package/extensions/combo-toggle/index.js +40 -62
  25. package/extensions/combo-toggle/index.ts +38 -72
  26. package/extensions/lib/utils.js +64 -15
  27. package/extensions/lib/utils.ts +62 -15
  28. package/extensions/opencode/index.js +104 -0
  29. package/extensions/opencode/server.js +190 -0
  30. package/extensions/opencode/server.ts +200 -0
  31. package/extensions/rtk-session/index.js +38 -57
  32. package/extensions/rtk-session/index.ts +36 -55
  33. package/extensions/shared/carbon.js +4 -10
  34. package/extensions/shared/carbon.ts +5 -10
  35. package/extensions/shared/host.js +95 -56
  36. package/extensions/shared/host.ts +106 -60
  37. package/extensions/shared/plugin-settings.js +6 -18
  38. package/extensions/shared/plugin-settings.ts +6 -18
  39. package/extensions/shared/pricing.js +79 -27
  40. package/extensions/shared/pricing.ts +80 -28
  41. package/extensions/shared/rtk-gain.js +4 -11
  42. package/extensions/shared/rtk-gain.ts +4 -11
  43. package/extensions/shared/session-state.js +28 -39
  44. package/extensions/shared/session-state.ts +29 -38
  45. package/extensions/shared/status.js +38 -0
  46. package/extensions/shared/status.ts +45 -0
  47. package/extensions/shared/types.js +1 -2
  48. package/extensions/shared/types.ts +2 -2
  49. package/extensions/shared/usage-ledger.js +312 -57
  50. package/extensions/shared/usage-ledger.ts +321 -61
  51. package/extensions/shared/usage-store.js +354 -38
  52. package/extensions/shared/usage-store.ts +360 -43
  53. package/extensions/tersio-commands/index.js +5 -14
  54. package/extensions/tersio-commands/index.ts +5 -14
  55. package/package.json +12 -7
  56. package/tersio.js +4 -6
  57. package/cli/agent-hosts.js +0 -42
  58. package/cli/agents.js +0 -266
  59. package/cli/layer.js +0 -24
  60. package/cli/omp-layer.js +0 -51
  61. package/cli/pi-layer.js +0 -37
  62. package/cli/pi-wiring.js +0 -92
  63. package/extensions/shared/omp-prompt.js +0 -12
package/package.json CHANGED
@@ -1,18 +1,17 @@
1
1
  {
2
2
  "name": "@krtclcdy/tersio",
3
- "version": "2.24.0",
4
- "description": "\u2702\ufe0f Token-saving OMP add-ons: terse Caveman replies, compact RTK shell output, lean Ponytail code calls \u2014 one-command combo presets.",
3
+ "version": "2.25.0",
4
+ "description": "✂️ Token saver and usage tracker for coding agents: terse Caveman replies, compact RTK shell output, lean Ponytail code calls, and a gain dashboard — one-command combo presets over one shared extension tree.",
5
5
  "keywords": [
6
- "omp",
7
- "pi",
8
- "pi-package",
9
6
  "token-saving",
10
7
  "ai-tools",
11
8
  "developer-tools",
12
9
  "caveman-mode",
13
10
  "rtk-rust-token-killer",
14
- "ponytail-omp",
15
- "extensions"
11
+ "ponytail",
12
+ "extensions",
13
+ "pi",
14
+ "pi-package"
16
15
  ],
17
16
  "license": "MIT",
18
17
  "author": "KurutoDenzeru",
@@ -24,6 +23,9 @@
24
23
  "bin": {
25
24
  "tersio": "tersio.js"
26
25
  },
26
+ "exports": {
27
+ "./server": "./extensions/opencode/server.js"
28
+ },
27
29
  "omp": {
28
30
  "name": "Tersio",
29
31
  "description": "Toggleable Caveman/RTK/Ponytail session modes with Combo presets",
@@ -125,6 +127,7 @@
125
127
  "build:dashboard": "bun install --cwd dashboard/app --frozen-lockfile && bun run --cwd dashboard/app build",
126
128
  "prepublishOnly": "bun run build",
127
129
  "check": "tsc --noEmit",
130
+ "verify": "bun install --frozen-lockfile && bun run build && bun run test",
128
131
  "doctor": "node tersio.js --doctor",
129
132
  "dry-run": "node tersio.js --dry-run --scope user --yes",
130
133
  "lint": "oxlint cli extensions test tersio.ts",
@@ -136,12 +139,14 @@
136
139
  "node": ">=20.12.0"
137
140
  },
138
141
  "devDependencies": {
142
+ "@opencode/plugin": "^2.0.22",
139
143
  "@types/node": "^26.4.1",
140
144
  "oxlint": "^1.85.0",
141
145
  "typescript": "^7.0.2",
142
146
  "vitest": "^5.0.1"
143
147
  },
144
148
  "dependencies": {
149
+ "@clack/core": "^1.5.1",
145
150
  "@clack/prompts": "^1.8.0",
146
151
  "@dietrichgebert/ponytail": "^4.10.0"
147
152
  }
package/tersio.js CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // tersio.ts — Install Tersio (caveman/rtk/ponytail) add-ons on any OMP device.
3
- // Usage: node tersio.js [install|update|doctor|dashboard|uninstall|version|help] [options]
4
- // Requires: node/npm and omp CLI
2
+ // Install Tersio add-ons on any OMP device; see help for commands.
5
3
  import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, dashboard, dashboardExport, dashboardOpen, dashboardPort, doctor, reset, settings, showHelp, showVersion, uninstall, unknownCommand, update, usage, } from './cli/common.js';
6
4
  import { closeRL } from './cli/interactive.js';
7
5
  import { runInstall } from './cli/install.js';
@@ -16,8 +14,8 @@ function printHelp() {
16
14
  console.log(`Usage: ${PACKAGE_BIN} [command] [options]
17
15
 
18
16
  Commands:
19
- install Install the add-ons for one agent (--host omp|pi, or ask at a terminal)
20
- update Refresh the CLI and add-ons (RTK binary, Caveman rule, Ponytail)
17
+ install Install the add-ons for one agent (--host omp|pi|opencode, or ask at a terminal)
18
+ update Refresh the CLI and add-ons (RTK binary, Caveman rule, Ponytail; --host limits to one agent)
21
19
  doctor Check the current installation (--fix repairs, --dry-run previews)
22
20
  usage Ledger-backed usage + savings report
23
21
  dashboard Open the Dashboard (localhost only)
@@ -30,7 +28,7 @@ Commands:
30
28
  Options:
31
29
  --fix (doctor: repairs all; --fix=<scope> repairs one of extensions, registrations, rtk, ponytail, cli)
32
30
  --scope user (legacy; accepted and ignored — user scope is the only scope)
33
- --host omp|pi (install|uninstall: target agent; a terminal asks instead)
31
+ --host omp|pi|opencode (install|uninstall|update: target agent; a terminal asks instead)
34
32
  --keep-ponytail (uninstall: keep the bundled Ponytail copy — removed by default)
35
33
  --remove-rtk (uninstall: also remove the RTK binary; its OMP wiring always goes)
36
34
  --combo-default off|medium|balanced|max (implies caveman, rtk, ponytail)
@@ -1,42 +0,0 @@
1
- // The host registry: one entry per agent, carrying only what the CLI needs to find it, install it and undo it.
2
- // How the shell rewrite reaches the model. Every host routes it through rtk's own module (`rtk init -g --agent <id>`), so...
3
- export const REWRITE_WIRING = 'rtk extension · auto-rewrite';
4
- const HOSTS = [
5
- {
6
- id: 'omp',
7
- label: 'Oh My Pi (OMP)',
8
- autoDetect: false,
9
- configDir: '.omp',
10
- binaries: ['omp'],
11
- // Earlier versions wrote these. The tree injects the modes every turn, so a skill is a second copy and a rules file is...
12
- retired: [
13
- { path: '.omp/agent/AGENTS.md', kind: 'merged' },
14
- { path: '.omp/agent/skills/tersio-caveman', kind: 'ours' },
15
- { path: '.omp/agent/skills/tersio-ponytail', kind: 'ours' },
16
- { path: '.omp/agent/skills/tersio-rtk', kind: 'ours' },
17
- ],
18
- caveats: 'Reference host. The modes are injected by the extension tree in ~/.omp/agent/extensions, so no static copy is written and the rewrite is rtk\'s own module (rtk init -g --agent omp).',
19
- source: 'https://omp.sh/docs/context-files',
20
- nativeInstall: { command: 'omp install @krtclcdy/tersio', note: 'or let `tersio install` write the layer for you' },
21
- },
22
- {
23
- id: 'pi',
24
- label: 'Pi',
25
- configDir: '.pi/agent',
26
- configDirEnv: 'PI_CODING_AGENT_DIR',
27
- binaries: ['pi'],
28
- retired: [
29
- { path: '.pi/agent/AGENTS.md', kind: 'merged' },
30
- { path: '.pi/agent/skills/tersio-caveman', kind: 'ours' },
31
- { path: '.pi/agent/skills/tersio-ponytail', kind: 'ours' },
32
- { path: '.pi/agent/skills/tersio-rtk', kind: 'ours' },
33
- ],
34
- caveats: 'AGENTS.override.md replaces rather than merges. Never write SYSTEM.md — it replaces the default system prompt outright. The rewrite is rtk\'s own module, written by `rtk init -g --agent pi`; rtk owns that format, so no tersio release is needed when it changes.',
35
- source: 'https://pi.dev/docs/latest/extensions',
36
- nativeInstall: { command: 'pi install npm:@krtclcdy/tersio', note: 'declares the `pi` manifest in package.json' },
37
- },
38
- ];
39
- function byId(id) {
40
- return HOSTS.find((h) => h.id === id);
41
- }
42
- export { HOSTS, byId };
package/cli/agents.js DELETED
@@ -1,266 +0,0 @@
1
- // Host selection, persistence, and the filesystem side of asking what a host has on disk.
2
- import fsSync from 'node:fs';
3
- import path from 'node:path';
4
- import { HOSTS, REWRITE_WIRING } from './agent-hosts.js';
5
- import { reportOmpLayer } from './omp-layer.js';
6
- import { reportPiLayer } from './pi-layer.js';
7
- const SELECTION_FILE = 'agents.json';
8
- // Marker pair for the one file we still merge into: a user's own AGENTS.md.
9
- const START = '<!-- tersio:start -->';
10
- const END = '<!-- tersio:end -->';
11
- function selectionPath(home) {
12
- return path.join(home, '.tersio', SELECTION_FILE);
13
- }
14
- // Host ids only, in registry order, de-duplicated, unknown ids dropped.
15
- export function normalizeIds(ids) {
16
- const wanted = new Set(ids);
17
- return HOSTS.filter((h) => wanted.has(h.id)).map((h) => h.id);
18
- }
19
- // `$HOME`-relative form of a path, for previews that must stay readable.
20
- export function displayPath(target, home) {
21
- if (!home)
22
- return target;
23
- const prefix = home.endsWith(path.sep) ? home : `${home}${path.sep}`;
24
- if (!target.startsWith(prefix))
25
- return target;
26
- return `~/${target.slice(prefix.length).split(path.sep).join('/')}`;
27
- }
28
- // The install marker goes in the *label*: clack renders hints on the cursor row only.
29
- export function agentChoices(state) {
30
- return HOSTS.map((host) => {
31
- const info = state?.get(host.id);
32
- if (!info) {
33
- return {
34
- value: host.id,
35
- label: host.id === 'omp' ? `${host.label} (extensions + Ponytail)` : host.label,
36
- hint: REWRITE_WIRING,
37
- };
38
- }
39
- const mark = info.installed ? 'installed' : 'not installed';
40
- const size = info.dirs > 0 ? `${info.dirs} dir${info.dirs === 1 ? '' : 's'}` : 'nothing';
41
- return { value: host.id, label: `${host.label} — ${size} · ${mark}`, hint: REWRITE_WIRING };
42
- });
43
- }
44
- // An uninstall row for a host with nothing of ours answers a question nobody asked.
45
- export function installedRows(state) {
46
- return agentChoices(state).filter((choice) => state.get(choice.value)?.installed !== false);
47
- }
48
- // True when the path is a directory. The layer's artifacts are directories.
49
- function isDir(target) {
50
- try {
51
- return fsSync.statSync(target).isDirectory();
52
- }
53
- catch {
54
- return false;
55
- }
56
- }
57
- // Reads one host's layer off the filesystem.
58
- export function hostLayer(host, home) {
59
- if (host.id === 'omp') {
60
- const report = reportOmpLayer(home, isDir);
61
- return {
62
- extDir: path.join(home, '.omp', 'agent', 'extensions'),
63
- plugin: report.pluginPath,
64
- present: report.extensions,
65
- missing: report.missing,
66
- installed: report.installed,
67
- };
68
- }
69
- const report = reportPiLayer(home, isDir);
70
- return {
71
- extDir: path.join(home, '.pi', 'agent', 'extensions'),
72
- plugin: null,
73
- present: report.extensions,
74
- missing: report.missing,
75
- installed: report.installed,
76
- };
77
- }
78
- // Both menus seed from the disk, not from the saved selection.
79
- export function installedState(home) {
80
- const state = new Map();
81
- for (const host of HOSTS) {
82
- const layer = hostLayer(host, home);
83
- // A partial tree is a partial install; omp's row also needs its plugin registration, which can be pruned while the...
84
- state.set(host.id, {
85
- dirs: layer.present.length,
86
- installed: layer.installed && layer.present.length > 0,
87
- });
88
- }
89
- return state;
90
- }
91
- // Host ids tersio has already installed something for.
92
- export function installedHostIds(state) {
93
- return normalizeIds([...state].filter(([, info]) => info.installed).map(([id]) => id));
94
- }
95
- // An explicit `--agent` wins outright, even for a host not installed.
96
- export async function resolveAgentSelection(opts) {
97
- const fromFlag = normalizeIds(opts.flag ?? []);
98
- if (fromFlag.length > 0) {
99
- return { ids: fromFlag, source: 'flag', addedByDetection: [] };
100
- }
101
- const stored = normalizeIds(opts.stored);
102
- const detected = normalizeIds(opts.detected);
103
- const union = normalizeIds([...stored, ...detected]);
104
- const addedByDetection = union.filter((id) => !stored.includes(id));
105
- if (opts.ask) {
106
- const answer = await opts.ask();
107
- if (answer !== null) {
108
- return { ids: normalizeIds(answer), source: 'prompt', addedByDetection };
109
- }
110
- }
111
- return { ids: union, source: union.length > 0 ? 'auto' : 'none', addedByDetection };
112
- }
113
- export function readSelection(home) {
114
- try {
115
- const raw = fsSync.readFileSync(selectionPath(home), 'utf8');
116
- const parsed = JSON.parse(raw);
117
- if (parsed === null || typeof parsed !== 'object')
118
- return { hosts: [], updatedAt: 0 };
119
- const record = parsed;
120
- const hosts = Array.isArray(record.hosts) ? record.hosts.filter((h) => typeof h === 'string') : [];
121
- const updatedAt = typeof record.updatedAt === 'number' ? record.updatedAt : 0;
122
- return { hosts: normalizeIds(hosts), updatedAt };
123
- }
124
- catch {
125
- return { hosts: [], updatedAt: 0 };
126
- }
127
- }
128
- export function writeSelection(home, ids) {
129
- const target = selectionPath(home);
130
- fsSync.mkdirSync(path.dirname(target), { recursive: true });
131
- const payload = { hosts: normalizeIds(ids), updatedAt: Date.now() };
132
- fsSync.writeFileSync(target, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
133
- }
134
- // A config dir, or a binary on PATH. Detection only widens the default set.
135
- export function detectHosts(home, env = process.env) {
136
- const found = [];
137
- for (const host of HOSTS) {
138
- if (host.autoDetect === false)
139
- continue;
140
- const dir = path.join(home, host.configDir);
141
- if (isDir(dir)) {
142
- found.push(host.id);
143
- continue;
144
- }
145
- const relocated = host.configDirEnv ? env[host.configDirEnv] : undefined;
146
- const onPath = relocated && relocated.trim() !== ''
147
- ? false
148
- : host.binaries.some((b) => hasBinary(b, env));
149
- if (onPath)
150
- found.push(host.id);
151
- }
152
- return found;
153
- }
154
- // A host's binary on PATH, or null. Shared with the dashboard.
155
- export function findHostBinary(host, env = process.env) {
156
- if (process.platform === 'win32') {
157
- const relocated = host.configDirEnv ? env[host.configDirEnv] : undefined;
158
- if (relocated && relocated.trim() !== '')
159
- return null;
160
- }
161
- const raw = env.PATH ?? env.Path ?? env.path ?? '';
162
- if (!raw)
163
- return null;
164
- const sep = process.platform === 'win32' ? ';' : ':';
165
- const exts = process.platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
166
- for (const binary of host.binaries) {
167
- if (process.platform === 'win32' && binary === 'cmd')
168
- continue;
169
- for (const dir of raw.split(sep)) {
170
- if (!dir)
171
- continue;
172
- for (const ext of exts) {
173
- const candidate = path.join(dir, binary + ext);
174
- try {
175
- if (fsSync.statSync(candidate).isFile())
176
- return candidate;
177
- }
178
- catch { /* keep looking */ }
179
- }
180
- }
181
- }
182
- return null;
183
- }
184
- function hasBinary(name, env) {
185
- // A bare `cmd` resolves to the system shell on Windows.
186
- if (process.platform === 'win32' && name === 'cmd')
187
- return false;
188
- const raw = env.PATH ?? env.Path ?? env.path ?? '';
189
- if (!raw)
190
- return false;
191
- const sep = process.platform === 'win32' ? ';' : ':';
192
- const exts = process.platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
193
- for (const dir of raw.split(sep)) {
194
- if (!dir)
195
- continue;
196
- for (const ext of exts) {
197
- try {
198
- if (fsSync.statSync(path.join(dir, name + ext)).isFile())
199
- return true;
200
- }
201
- catch { /* keep looking */ }
202
- }
203
- }
204
- return false;
205
- }
206
- // Paths an earlier version wrote and this one no longer does. `ours` is removed outright;
207
- export function clearRetiredPaths(host, home, options = {}) {
208
- const removed = [];
209
- for (const entry of host.retired ?? []) {
210
- const target = path.join(home, entry.path);
211
- if (entry.kind === 'merged') {
212
- let current = null;
213
- try {
214
- current = fsSync.readFileSync(target, 'utf8');
215
- }
216
- catch {
217
- current = null;
218
- }
219
- if (current === null)
220
- continue;
221
- const stripped = removeMarkedBlock(current);
222
- if (stripped === current)
223
- continue;
224
- if (!options.dryRun) {
225
- if (stripped === null)
226
- fsSync.rmSync(target, { force: true });
227
- else
228
- fsSync.writeFileSync(target, stripped, 'utf8');
229
- }
230
- removed.push(target);
231
- continue;
232
- }
233
- if (!isDir(target) && !existsSyncSafe(target))
234
- continue;
235
- if (!options.dryRun) {
236
- try {
237
- fsSync.rmSync(target, { recursive: true, force: true });
238
- }
239
- catch {
240
- continue;
241
- }
242
- }
243
- removed.push(target);
244
- }
245
- return removed;
246
- }
247
- // Removes the marked block and the blank line it introduced. Null when nothing is left.
248
- function removeMarkedBlock(existing) {
249
- const from = existing.indexOf(START);
250
- const to = existing.indexOf(END);
251
- if (from === -1 || to === -1 || to <= from)
252
- return existing;
253
- const before = existing.slice(0, from).replace(/\n+$/, '\n');
254
- const after = existing.slice(to + END.length).replace(/^\n+/, '');
255
- const merged = `${before}${after}`;
256
- return merged.trim() ? merged : null;
257
- }
258
- function existsSyncSafe(p) {
259
- try {
260
- return fsSync.existsSync(p);
261
- }
262
- catch {
263
- return false;
264
- }
265
- }
266
- export { selectionPath };
package/cli/layer.js DELETED
@@ -1,24 +0,0 @@
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/; installed, it sits one level
12
- // below <agent-dir>/extensions/. Rewriting on the way in keeps one spelling in
13
- // the repo and the same other spelling in every installed tree.
14
- export function rewritePortImports(content) {
15
- return content.replace(/from '\.\.\/\.\.\/(shared|lib)\//g, "from '../$1/");
16
- }
17
- // What of the layer is on disk.
18
- export function reportLayer(extDir, tables, exists) {
19
- const all = tables.flatMap((table) => layerEntries(extDir, table));
20
- return {
21
- present: all.filter((entry) => exists(entry.path)),
22
- missing: all.filter((entry) => !exists(entry.path)),
23
- };
24
- }
package/cli/omp-layer.js DELETED
@@ -1,51 +0,0 @@
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 DELETED
@@ -1,37 +0,0 @@
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 DELETED
@@ -1,92 +0,0 @@
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
- }
@@ -1,12 +0,0 @@
1
- // extensions/omp/omp-prompt.ts — OMP's system-prompt surface.
2
- // The subagent marker OMP puts in a delegated agent's prompt.
3
- export const OMP_SUBAGENT_MARKER = 'You are operating on a piece of work assigned to you by the main agent.';
4
- export function asPromptArray(systemPrompt) {
5
- return Array.isArray(systemPrompt) ? systemPrompt : [systemPrompt];
6
- }
7
- export function systemPromptIncludes(systemPrompt, marker) {
8
- return asPromptArray(systemPrompt).some((prompt) => typeof prompt === 'string' && prompt.includes(marker));
9
- }
10
- export function isOmpSubagentPrompt(systemPrompt) {
11
- return systemPromptIncludes(systemPrompt, OMP_SUBAGENT_MARKER);
12
- }