@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.
- package/README.md +28 -24
- package/cli/banner.js +7 -5
- package/cli/common.js +14 -13
- package/cli/currency.js +2 -7
- package/cli/dashboard.js +220 -13
- package/cli/doctor-fix.js +99 -29
- package/cli/doctor.js +32 -35
- package/cli/hosts.js +23 -26
- package/cli/install.js +228 -115
- package/cli/interactive.js +65 -21
- package/cli/manifest.js +8 -4
- package/cli/profile.js +49 -16
- package/cli/reset.js +3 -7
- package/cli/rtk-wiring.js +29 -32
- package/cli/settings.js +40 -16
- package/cli/uninstall.js +79 -29
- package/cli/update.js +27 -20
- package/cli/usage.js +54 -19
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +75 -20
- package/extensions/ai-addons-updater/index.ts +67 -23
- package/extensions/caveman-session/index.js +33 -75
- package/extensions/caveman-session/index.ts +35 -77
- package/extensions/combo-toggle/index.js +40 -62
- package/extensions/combo-toggle/index.ts +38 -72
- package/extensions/lib/utils.js +64 -15
- package/extensions/lib/utils.ts +62 -15
- package/extensions/opencode/index.js +104 -0
- package/extensions/opencode/server.js +190 -0
- package/extensions/opencode/server.ts +200 -0
- package/extensions/rtk-session/index.js +38 -57
- package/extensions/rtk-session/index.ts +36 -55
- package/extensions/shared/carbon.js +4 -10
- package/extensions/shared/carbon.ts +5 -10
- package/extensions/shared/host.js +95 -56
- package/extensions/shared/host.ts +106 -60
- package/extensions/shared/plugin-settings.js +6 -18
- package/extensions/shared/plugin-settings.ts +6 -18
- package/extensions/shared/pricing.js +79 -27
- package/extensions/shared/pricing.ts +80 -28
- package/extensions/shared/rtk-gain.js +4 -11
- package/extensions/shared/rtk-gain.ts +4 -11
- package/extensions/shared/session-state.js +28 -39
- package/extensions/shared/session-state.ts +29 -38
- package/extensions/shared/status.js +38 -0
- package/extensions/shared/status.ts +45 -0
- package/extensions/shared/types.js +1 -2
- package/extensions/shared/types.ts +2 -2
- package/extensions/shared/usage-ledger.js +312 -57
- package/extensions/shared/usage-ledger.ts +321 -61
- package/extensions/shared/usage-store.js +354 -38
- package/extensions/shared/usage-store.ts +360 -43
- package/extensions/tersio-commands/index.js +5 -14
- package/extensions/tersio-commands/index.ts +5 -14
- package/package.json +12 -7
- package/tersio.js +4 -6
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -24
- package/cli/omp-layer.js +0 -51
- package/cli/pi-layer.js +0 -37
- package/cli/pi-wiring.js +0 -92
- package/extensions/shared/omp-prompt.js +0 -12
package/README.md
CHANGED
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
[](https://github.com/KurutoDenzeru/tersio/actions)
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
|
-
Token
|
|
12
|
-
RTK shell output, lean Ponytail code calls, usage tracking with a gain
|
|
13
|
-
dashboard — one-command combo presets, one shared extension tree.
|
|
11
|
+
Token saver & 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.
|
|
14
12
|
|
|
15
13
|
## ⚡ Getting Started
|
|
16
14
|
|
|
@@ -25,29 +23,23 @@ curl -fsSL https://github.com/KurutoDenzeru/tersio/releases/latest/download/inst
|
|
|
25
23
|
Then run the CLI. Bare `tersio` opens a menu and asks which agent to install into:
|
|
26
24
|
|
|
27
25
|
```bash
|
|
28
|
-
tersio # menu — picks Oh My Pi or
|
|
26
|
+
tersio # menu — picks Oh My Pi, pi, or OpenCode
|
|
29
27
|
tersio install --host pi # pi, non-interactive
|
|
30
28
|
tersio install --host omp # Oh My Pi, non-interactive
|
|
29
|
+
tersio install --host opencode # OpenCode, non-interactive
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
Or let
|
|
32
|
+
Or let each host install the package itself:
|
|
34
33
|
|
|
35
34
|
```bash
|
|
36
35
|
omp plugin install @krtclcdy/tersio
|
|
37
36
|
pi install npm:@krtclcdy/tersio
|
|
37
|
+
opencode plugin add @krtclcdy/tersio
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Pick one install method per host, not both. Using both registers every command twice.
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```text
|
|
45
|
-
/combo balanced
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Individual toggles: `/caveman full` · `/rtk on` · `/ponytail full`. Everything starts off until you enable it.
|
|
49
|
-
|
|
50
|
-
When Tersio is installed through OMP, the first interactive launch asks for a session-start Combo preset once (`off`, `medium`, `balanced`, or `max`) and saves it as `comboDefault`. Running `tersio install` performs the same setup through the CLI.
|
|
42
|
+
All hosts load the same five extensions from the same sources and read their session-start defaults from `~/.tersio/settings.json`. All five load always.
|
|
51
43
|
|
|
52
44
|
One-off use without installing:
|
|
53
45
|
|
|
@@ -57,14 +49,15 @@ npm exec --yes --prefer-online --package=@krtclcdy/tersio@latest -- tersio insta
|
|
|
57
49
|
|
|
58
50
|
### Requirements
|
|
59
51
|
|
|
60
|
-
- [OMP](https://github.com/can1357/oh-my-pi)
|
|
52
|
+
- [OMP](https://github.com/can1357/oh-my-pi), [pi](https://github.com/earendil-works/pi), or [OpenCode](https://opencode.ai) — all three are supported; pick one with `tersio install --host <omp|pi|opencode>`
|
|
61
53
|
- Node.js 20.12+ with npm
|
|
54
|
+
- macOS or Linux, native or WSL
|
|
62
55
|
|
|
63
|
-
Windows
|
|
56
|
+
On Windows, use WSL. There is no native Windows path: the installer, the extensions, and the RTK wiring are tested on macOS and Linux only. WSL has its own home directory, so install from inside WSL — `command -v npm` must resolve to a Linux path, not `/mnt/c/`.
|
|
64
57
|
|
|
65
58
|
## 📊 Benchmarks
|
|
66
59
|
|
|
67
|
-
Every mode is measured against a base run with all modes off. Measured surfaces use different boundaries. Do not treat reply-text, code-output, and shell-output reductions as total bill savings.
|
|
60
|
+
Every mode is measured against a base run with all modes off. Measured surfaces use different boundaries. Do not treat reply-text, code-output, and shell-output reductions as total bill savings. **A mode also spends tokens on the prompt, on every turn — read the caveat under the table before choosing.** Results and the rerun protocol are in [docs/BENCHMARK.md](./docs/BENCHMARK.md).
|
|
68
61
|
|
|
69
62
|
| Mode | Surface (n) | Base → Tersio | Δ |
|
|
70
63
|
|---|---|---|---|
|
|
@@ -80,7 +73,9 @@ Every mode is measured against a base run with all modes off. Measured surfaces
|
|
|
80
73
|
| `/rtk on` — `git status` | Command output (3 runs, p50) | 72 → 18 | **−75.0% output** |
|
|
81
74
|
| `/rtk on` — `bun run build` | Command output (3 runs, p50) | 140 → 114 | **−18.6% output** |
|
|
82
75
|
|
|
83
|
-
|
|
76
|
+
The two `/caveman` rows above are the exception worth naming: both levels spend more prompt tokens than the reply text they save, so they cost you money on net. Use Caveman for the terser writing style, not as a token saving.
|
|
77
|
+
|
|
78
|
+
Prompt overhead is included in [docs/BENCHMARK.md](./docs/BENCHMARK.md) with a full prompt-base example. Summary: `/rtk on` pays back immediately, `/ponytail ultra` repays in about 7 code tasks, `/combo medium` in about 8, and every Caveman level costs more in prompt than it saves in reply text.
|
|
84
79
|
|
|
85
80
|
The Command tools dashboard reports weighted RTK command-output savings and OMP-specific adoption from executed Bash records. It does not estimate provider billing.
|
|
86
81
|
Upstream Ponytail's fair agentic benchmark reports 54% less code, 22% fewer tokens, 20% lower cost, and 100% retained safety. RTK estimates command-output tokens, not bill savings. Caveman and Ponytail compliance varies by model. Measure your own sessions with `tersio usage` and the dashboard.
|
|
@@ -123,7 +118,13 @@ Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-defa
|
|
|
123
118
|
|
|
124
119
|
## ⌨️ Commands reference
|
|
125
120
|
|
|
126
|
-
Mode switches live on their own commands; bare `/tersio` prints status.
|
|
121
|
+
Mode switches live on their own commands; bare `/tersio` prints status. Every mode command, and every session start, prints the status line:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
🧩 combo MAX: 🪨caveman=ULTRA ⚡rtk=ON 🦥ponytail=ULTRA
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
It is display-only — it never enters the model context, so it costs the session nothing.
|
|
127
128
|
|
|
128
129
|
| Command | Purpose |
|
|
129
130
|
|---|---|
|
|
@@ -131,7 +132,7 @@ Mode switches live on their own commands; bare `/tersio` prints status.
|
|
|
131
132
|
| `/caveman lite\|full\|ultra\|wenyan-lite\|wenyan-full\|wenyan-ultra\|off\|status` | Terse replies with upstream-aligned Caveman levels. Legacy `wenyan` restores as `wenyan-full`. |
|
|
132
133
|
| `/rtk on\|off\|status` | Controls RTK session guidance, `rtk_run`, and the automatic hook through shared `RTK_DISABLED` state. Exact bytes bypass RTK. |
|
|
133
134
|
| `/ponytail off\|lite\|full\|ultra\|status` | Minimal code using upstream Ponytail modes. Review runs separately through `/ponytail-review`. |
|
|
134
|
-
| `/tersio status` | Active modes + combo level; state persists
|
|
135
|
+
| `/tersio status` | Active modes + combo level; state persists and propagates to subagents. |
|
|
135
136
|
| `/tersio check` | Add-on version check |
|
|
136
137
|
| `/tersio update <ponytail\|rtk\|caveman\|all> [--dry-run]` | Update add-ons, preview with dry-run |
|
|
137
138
|
| `/tersio dashboard` | Open the Dashboard. |
|
|
@@ -149,14 +150,15 @@ Shared, and identical for every host:
|
|
|
149
150
|
| RTK binary | `~/.bun/bin/rtk` (`rtk.exe` on Windows). The runtime resolves `PATH` first, then this managed path. |
|
|
150
151
|
| Usage ledger and price cache | `~/.tersio/` — local only, nothing leaves the machine |
|
|
151
152
|
|
|
152
|
-
Per host, written by `tersio install --host <omp|pi>`:
|
|
153
|
+
Per host, written by `tersio install --host <omp|pi|opencode>`:
|
|
153
154
|
|
|
154
155
|
| Host | Extensions | Ponytail |
|
|
155
156
|
|---|---|---|
|
|
156
157
|
| Oh My Pi | `~/.omp/agent/extensions/{caveman-session,rtk-session,combo-toggle,tersio-commands,ai-addons-updater}/` | nested plugin dependency under `~/.omp/plugins/node_modules/`, one Plugins row |
|
|
157
158
|
| pi | `~/.pi/agent/extensions/{caveman-session,rtk-session,combo-toggle,tersio-commands,ai-addons-updater}/` — pi auto-discovers `<agent-dir>/extensions`, so there is nothing to register | `npm:@dietrichgebert/ponytail`, installed as a pi package |
|
|
159
|
+
| OpenCode | `~/.config/opencode/plugins/tersio/` — registered in `opencode.json` plugins, so there is nothing else to register | bundled with Tersio; no separate package |
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
All three hosts carry the same `shared/` and `lib/` modules and the same mode extensions from the same sources.
|
|
160
162
|
|
|
161
163
|
The installer writes `<file>.bak` before replacing an extension source; the updater keeps `rtk.bak` / `rule.md.bak` and restores them if the replacement fails validation. `tersio doctor --fix extensions` restores a damaged tree, and `doctor --fix registrations` drops retired or duplicate `config.yml` entries.
|
|
162
164
|
|
|
@@ -168,6 +170,8 @@ An OMP install also registers the package in `~/.omp/plugins`, so it shows under
|
|
|
168
170
|
|
|
169
171
|
**Commands missing on pi:** check the `Hosts` section of `tersio doctor` — it reports whether pi is installed and where. Then run `tersio install --host pi` and restart pi.
|
|
170
172
|
|
|
173
|
+
**Commands missing on OpenCode:** run `tersio install --host opencode` and restart OpenCode.
|
|
174
|
+
|
|
171
175
|
**RTK missing or not executable:** run `tersio install`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
|
|
172
176
|
|
|
173
177
|
**RTK commands not metered in the Dashboard:** only bash calls routed through rtk's OMP wiring (`~/.omp/agent/extensions/rtk.ts`, written by `rtk init -g --agent omp`) are metered. Missing: run that command (needs rtk ≥ 0.49) or `tersio doctor --fix rtk`, then restart OMP. Native tool calls (`read`/`edit`/`eval`) stay unmetered.
|
|
@@ -178,7 +182,7 @@ An OMP install also registers the package in `~/.omp/plugins`, so it shows under
|
|
|
178
182
|
|
|
179
183
|
Contributions are always welcome, whether you're fixing bugs, improving docs, or shipping new features that make the project better for everyone.
|
|
180
184
|
|
|
181
|
-
Check out [
|
|
185
|
+
Check out [CONTRIBUTING.md](CONTRIBUTING.md) to learn how to get started and follow the recommended workflow.
|
|
182
186
|
|
|
183
187
|
## ⚖️ License
|
|
184
188
|
|
package/cli/banner.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
// cli/banner.ts — mini pixel-scissor welcome
|
|
2
|
-
// OMP-style launch moment for interactive runs: clack intro + note box.
|
|
3
|
-
// TTY-only and skipped on --dry-run, so piped output stays byte-identical.
|
|
1
|
+
// cli/banner.ts — mini pixel-scissor welcome. TTY-only and skipped on --dry-run, so piped output stays byte-identical.
|
|
4
2
|
import { intro as clackIntro, note as clackNote } from '@clack/prompts';
|
|
5
3
|
import { PACKAGE_VERSION, dryRun } from './common.js';
|
|
6
4
|
function tty() {
|
|
@@ -72,8 +70,7 @@ function bannerLines(tier = bannerTier()) {
|
|
|
72
70
|
return out;
|
|
73
71
|
}
|
|
74
72
|
let shown = false;
|
|
75
|
-
//
|
|
76
|
-
// No-op unless interactive (and never on --dry-run).
|
|
73
|
+
// Interactive only, so piped and --dry-run output stays byte-identical.
|
|
77
74
|
function printWelcome() {
|
|
78
75
|
if (shown || dryRun || !tty())
|
|
79
76
|
return;
|
|
@@ -84,6 +81,11 @@ function printWelcome() {
|
|
|
84
81
|
'tersio update refresh CLI + add-ons',
|
|
85
82
|
'tersio doctor verify the install',
|
|
86
83
|
];
|
|
84
|
+
// Below 26 rows the note scrolls off the top and slices the art.
|
|
85
|
+
if (process.stdout.rows < 26) {
|
|
86
|
+
clackNote(tips.map((t) => ` ${t}`).join('\n'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
87
89
|
const art = bannerLines();
|
|
88
90
|
const rows = art.map((line, i) => (i >= 1 && i <= 3 ? `${line} ${tips[i - 1]}` : line));
|
|
89
91
|
clackNote(rows.join('\n'));
|
package/cli/common.js
CHANGED
|
@@ -20,8 +20,7 @@ const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan-lite',
|
|
|
20
20
|
const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra']);
|
|
21
21
|
const RTK_DEFAULTS = new Set(['on', 'off']);
|
|
22
22
|
const DIAG_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
23
|
-
// Mirrors COMBO_LEVELS in extensions/shared/session-state.ts (rtk as a
|
|
24
|
-
// boolean here), so the combo preset implies all three modes.
|
|
23
|
+
// Mirrors COMBO_LEVELS in extensions/shared/session-state.ts (rtk as a boolean here), so the combo preset implies all three modes.
|
|
25
24
|
const COMBO_PRESET_MODES = {
|
|
26
25
|
off: { caveman: 'off', rtk: false, ponytail: 'off' },
|
|
27
26
|
medium: { caveman: 'lite', rtk: true, ponytail: 'lite' },
|
|
@@ -51,8 +50,7 @@ const COMMANDS = { install: true, update: true, doctor: true, uninstall: true, u
|
|
|
51
50
|
const args = process.argv.slice(2);
|
|
52
51
|
const commandArg = args.find((arg) => !arg.startsWith('-'));
|
|
53
52
|
const command = commandArg?.toLowerCase() || null;
|
|
54
|
-
// `tersio settings diagnosis` jumps to one prompt instead of walking the
|
|
55
|
-
// chain. Index-based, so flag values never count as positionals.
|
|
53
|
+
// `tersio settings diagnosis` jumps to one prompt instead of walking the chain. Index-based, so flag values never count as positionals.
|
|
56
54
|
const settingArg = (() => {
|
|
57
55
|
const at = args.indexOf(commandArg ?? '');
|
|
58
56
|
const next = at >= 0 ? args[at + 1] : undefined;
|
|
@@ -69,6 +67,8 @@ const applyUpdate = args.includes('--apply-update');
|
|
|
69
67
|
const dryRun = args.includes('--dry-run');
|
|
70
68
|
const yes = args.includes('--yes') || args.includes('-y') || install || update || applyUpdate;
|
|
71
69
|
const verbose = args.includes('--verbose');
|
|
70
|
+
// Off by default: an unverified RTK binary runs with the user's privileges.
|
|
71
|
+
const allowUnverified = args.includes('--allow-unverified');
|
|
72
72
|
const doctor = command === 'doctor' || args.includes('--doctor');
|
|
73
73
|
const fix = args.includes('--fix') || args.some((a) => a.startsWith('--fix='));
|
|
74
74
|
const usage = command === 'usage' || args.includes('--usage');
|
|
@@ -85,8 +85,7 @@ const uninstall = command === 'uninstall' || args.includes('--uninstall');
|
|
|
85
85
|
const removePonytail = args.includes('--remove-ponytail');
|
|
86
86
|
const keepPonytail = args.includes('--keep-ponytail');
|
|
87
87
|
const removeRtk = args.includes('--remove-rtk');
|
|
88
|
-
// User scope only (all OMP sessions). A bare `--scope user` still parses for
|
|
89
|
-
// old scripts; anything else fails.
|
|
88
|
+
// User scope only (all OMP sessions). A bare `--scope user` still parses for old scripts; anything else fails.
|
|
90
89
|
const legacyScope = flagValue('--scope')?.toLowerCase() ?? null;
|
|
91
90
|
if (legacyScope !== null && legacyScope !== 'user') {
|
|
92
91
|
console.error(`[fail] Invalid --scope: ${legacyScope}. Project scope was removed; tersio installs user-level only.`);
|
|
@@ -100,6 +99,11 @@ const rtkDefaultFlag = parseEnum(flagValue('--rtk-default'), RTK_DEFAULTS, '--rt
|
|
|
100
99
|
const diagScheduleFlag = parseEnum(flagValue('--diag-schedule'), DIAG_SCHEDULES, '--diag-schedule');
|
|
101
100
|
const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag]
|
|
102
101
|
.some((flag) => flag !== undefined);
|
|
102
|
+
// Repeatable `--subagent-marker`; overrides the built-in host prompt marker.
|
|
103
|
+
const subagentMarkerFlags = args
|
|
104
|
+
.map((a, i) => (a === '--subagent-marker' ? args[i + 1] : undefined))
|
|
105
|
+
.filter((v) => typeof v === 'string' && !v.startsWith('-') && v.trim() !== '');
|
|
106
|
+
const subagentMarkersGiven = subagentMarkerFlags.length > 0;
|
|
103
107
|
function debug(...a) {
|
|
104
108
|
if (verbose)
|
|
105
109
|
console.log(' [debug]', ...a);
|
|
@@ -133,8 +137,7 @@ async function writeIfChanged(dest, content, options = {}) {
|
|
|
133
137
|
console.log(` [write] ${dest}`);
|
|
134
138
|
return true;
|
|
135
139
|
}
|
|
136
|
-
// omp ships 'extensions: null', and list items under a null scalar break YAML:
|
|
137
|
-
// normalize null/~/[]/empty to a mapping key first.
|
|
140
|
+
// omp ships 'extensions: null', and list items under a null scalar break YAML: normalize null/~/[]/empty to a mapping key first.
|
|
138
141
|
const EXTENSIONS_KEY_RE = /^\s*extensions\s*:/i;
|
|
139
142
|
const EXTENSIONS_NULL_RE = /^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i;
|
|
140
143
|
function normalizeExtensionsKey(lines) {
|
|
@@ -156,8 +159,7 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
156
159
|
const line = ` - ${normalizedPath}`;
|
|
157
160
|
const raw = await readTextIfExists(configPath);
|
|
158
161
|
let lines = (raw || '').split('\n');
|
|
159
|
-
// Drop legacy compiled twins (same dir/name, .js): OMP must never load both
|
|
160
|
-
// copies. Matched on the trailing path so relative entries are caught too.
|
|
162
|
+
// Drop legacy compiled twins (same dir/name, .js): OMP must never load both copies. Matched on the trailing path so relative entries are caught too.
|
|
161
163
|
if (normalizedPath.endsWith('.ts')) {
|
|
162
164
|
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
163
165
|
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
@@ -277,8 +279,7 @@ async function ensurePonytailConfigValue(key, value, options = {}) {
|
|
|
277
279
|
return true;
|
|
278
280
|
}, `would set Ponytail ${key}=${value}`, `Set Ponytail ${key}=${value}`, options);
|
|
279
281
|
}
|
|
280
|
-
// Read plugins/package.json tolerantly; a corrupt or missing file starts
|
|
281
|
-
// fresh with installer-managed defaults.
|
|
282
|
+
// Read plugins/package.json tolerantly; a corrupt or missing file starts fresh with installer-managed defaults.
|
|
282
283
|
async function readPluginsPackage(pkgPath) {
|
|
283
284
|
const pkg = parseJsonObject(await readTextIfExists(pkgPath)) ?? {};
|
|
284
285
|
return { ...pkg, name: pkg.name || 'omp-plugins', private: true, dependencies: pkg.dependencies || {} };
|
|
@@ -298,4 +299,4 @@ function relTime(ageMs) {
|
|
|
298
299
|
const weeks = Math.floor(days / 7);
|
|
299
300
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
300
301
|
}
|
|
301
|
-
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
|
|
302
|
+
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
|
package/cli/currency.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
// cli/currency.ts —
|
|
2
|
-
// Same 10 currencies, symbols, and snapshot rates as the dashboard's
|
|
3
|
-
// converter (dashboard/app); the dashboard replaces rates with live
|
|
4
|
-
// frankfurter figures when reachable, the CLI always uses the snapshot.
|
|
1
|
+
// cli/currency.ts — USD-base currency helpers on offline snapshot rates. Same 10 currencies as the dashboard, which prefers live frankfurter figures.
|
|
5
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
3
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
7
4
|
const CURRENCY_CODES = [
|
|
@@ -31,9 +28,7 @@ function parseCurrencyFlag(raw) {
|
|
|
31
28
|
}
|
|
32
29
|
return v;
|
|
33
30
|
}
|
|
34
|
-
// Stored default
|
|
35
|
-
// USD when missing, corrupt, or holding an unknown code. Sync so flag
|
|
36
|
-
// parsing in cli/common.ts can fall back to it at startup.
|
|
31
|
+
// Stored default; USD when missing, corrupt, or an unknown code.
|
|
37
32
|
function readStoredCurrency() {
|
|
38
33
|
const file = tersioSettingsFile();
|
|
39
34
|
if (!existsSync(file))
|
package/cli/dashboard.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// Serves the built Dashboard with local usage APIs. Binds 127.0.0.1 only;
|
|
2
|
-
// --export writes a file instead. No Promise.withResolvers: Node 20.12 lacks it.
|
|
1
|
+
// Serves the built Dashboard with local usage APIs. Binds 127.0.0.1 only; --export writes a file instead. No Promise.withResolvers: Node 20.12 lacks it.
|
|
3
2
|
import { spawn } from 'node:child_process';
|
|
4
3
|
import { execFileSync } from 'node:child_process';
|
|
5
4
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -10,9 +9,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
9
|
import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
11
10
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
12
11
|
import { summarizeUsage } from './usage.js';
|
|
12
|
+
import { backupUsageDb, deleteUsageBackup, listUsageBackups, restoreUsageBackup, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
13
13
|
import { isCurrencyCode } from './currency.js';
|
|
14
14
|
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, } from './common.js';
|
|
15
|
-
import { storedProfile, writePluginSettings } from './profile.js';
|
|
15
|
+
import { BACKUP_SCHEDULES, formatCliStatus, storedProfile, writePluginSettings } from './profile.js';
|
|
16
|
+
import { normalizeComboLevel } from '../extensions/shared/session-state.js';
|
|
17
|
+
import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS } from './common.js';
|
|
16
18
|
import { PACKAGE_NAME } from './common.js';
|
|
17
19
|
import { resolveRtkBinary } from '../extensions/lib/utils.js';
|
|
18
20
|
const DASHBOARD_DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard', 'dist');
|
|
@@ -50,6 +52,17 @@ async function brandDataUri() {
|
|
|
50
52
|
function dataJson() {
|
|
51
53
|
return JSON.stringify(summarizeUsage(readUsage()));
|
|
52
54
|
}
|
|
55
|
+
// Runs outside any agent session, so it reports the persisted defaults.
|
|
56
|
+
async function statusJson() {
|
|
57
|
+
const profile = await storedProfile();
|
|
58
|
+
return JSON.stringify({
|
|
59
|
+
status: formatCliStatus(profile),
|
|
60
|
+
level: profile.comboDefault,
|
|
61
|
+
caveman: profile.cavemanDefault,
|
|
62
|
+
rtk: profile.rtkDefault ? 'on' : 'off',
|
|
63
|
+
ponytail: profile.ponytailDefault,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
53
66
|
// Local-only health: find each agent on PATH, run `--version`, report both.
|
|
54
67
|
function binOnPath(bin) {
|
|
55
68
|
const names = process.platform === 'win32' ? [`${bin}.cmd`, `${bin}.exe`, `${bin}.bat`, bin] : [bin];
|
|
@@ -71,6 +84,9 @@ function ompPath() {
|
|
|
71
84
|
function piPath() {
|
|
72
85
|
return binOnPath('pi');
|
|
73
86
|
}
|
|
87
|
+
function opencodePath() {
|
|
88
|
+
return binOnPath('opencode');
|
|
89
|
+
}
|
|
74
90
|
// omp prints `omp/18.4.1`, pi prints a bare `0.87.1`; both read as name/version.
|
|
75
91
|
function versionLabel(bin, raw) {
|
|
76
92
|
const out = raw.trim();
|
|
@@ -115,14 +131,27 @@ function healthJson() {
|
|
|
115
131
|
const rtkPresent = rtkBin !== null;
|
|
116
132
|
const detectedOmpPath = ompPath();
|
|
117
133
|
const detectedPiPath = piPath();
|
|
134
|
+
const detectedOpencodePath = opencodePath();
|
|
135
|
+
const agentDir = (id) => {
|
|
136
|
+
if (id === 'omp')
|
|
137
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', '.omp');
|
|
138
|
+
if (id === 'pi')
|
|
139
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', '.pi');
|
|
140
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', '.config', 'opencode');
|
|
141
|
+
};
|
|
118
142
|
return JSON.stringify({
|
|
119
143
|
tersio: PACKAGE_VERSION,
|
|
120
144
|
node: process.version,
|
|
121
145
|
platform: `${process.platform}/${process.arch}`,
|
|
122
146
|
omp: agentVersion(detectedOmpPath),
|
|
123
147
|
ompPath: detectedOmpPath,
|
|
148
|
+
ompDir: agentDir('omp'),
|
|
124
149
|
pi: agentVersion(detectedPiPath),
|
|
125
150
|
piPath: detectedPiPath,
|
|
151
|
+
piDir: agentDir('pi'),
|
|
152
|
+
opencode: agentVersion(detectedOpencodePath),
|
|
153
|
+
opencodePath: detectedOpencodePath,
|
|
154
|
+
opencodeDir: agentDir('opencode'),
|
|
126
155
|
provider: ompDefaultModel(),
|
|
127
156
|
rtk: { present: rtkPresent, version: rtkPresent ? rtkVersion(rtkBin) : null, path: rtkBin || 'not found in PATH' },
|
|
128
157
|
home: tersioHomePath(),
|
|
@@ -216,12 +245,14 @@ function computeDoctorRows() {
|
|
|
216
245
|
}
|
|
217
246
|
catch { /* missing config is reported by extension rows */ }
|
|
218
247
|
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
219
|
-
addon('Unique config registrations',
|
|
220
|
-
|
|
221
|
-
|
|
248
|
+
addon('Unique config registrations', true, 'ok', 'Extensions');
|
|
249
|
+
// A listed extension whose file is gone makes the host warn on every load, so surface it rather than leaving the user with only a startup message.
|
|
250
|
+
const dangling = explicitEntries
|
|
251
|
+
.filter((entry) => entry.includes(`extensions${path.sep}`) || entry.includes('extensions/'))
|
|
252
|
+
.filter((entry) => !ok(path.isAbsolute(entry) ? entry : path.resolve(OMP_AGENT_DIR, entry)));
|
|
253
|
+
addon('Extension files present', dangling.length === 0, dangling.length ? dangling.join(', ') : 'ok', 'Extensions');
|
|
222
254
|
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts'));
|
|
223
255
|
ext('RTK extension', path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts'));
|
|
224
|
-
ext('Ponytail extension', path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'));
|
|
225
256
|
const rule = path.join(tersioPluginDir, 'extensions', 'caveman-session', 'rule.md');
|
|
226
257
|
const ruleAge = ageStr(rule);
|
|
227
258
|
const ruleAt = absTime(rule);
|
|
@@ -246,8 +277,7 @@ function computeDoctorRows() {
|
|
|
246
277
|
addon('Prices feed', pricesAge !== null, pricesAge && pricesAt ? `(pulled ${pricesAge} · ${pricesAt})` : prices, 'Usage');
|
|
247
278
|
return rows;
|
|
248
279
|
}
|
|
249
|
-
// Recompute when forced, missing, or past the schedule; a report holding a
|
|
250
|
-
// retired label is stale by definition.
|
|
280
|
+
// Recompute when forced, missing, or past the schedule; a report holding a retired label is stale by definition.
|
|
251
281
|
const RETIRED_DIAG_LABELS = new Set([
|
|
252
282
|
'OMP CLI',
|
|
253
283
|
'Tersio CLI',
|
|
@@ -257,7 +287,13 @@ const RETIRED_DIAG_LABELS = new Set([
|
|
|
257
287
|
'Combo extension',
|
|
258
288
|
'Self plugin',
|
|
259
289
|
'Ponytail registered',
|
|
290
|
+
'Ponytail extension',
|
|
260
291
|
'Usage store',
|
|
292
|
+
'RTK OMP wiring (rtk.ts)',
|
|
293
|
+
'OpenCode plugin entry',
|
|
294
|
+
'OpenCode RTK plugin',
|
|
295
|
+
'pi RTK extension',
|
|
296
|
+
'No foreign opencode trees',
|
|
261
297
|
]);
|
|
262
298
|
function isRetiredReport(report) {
|
|
263
299
|
return report.rows.some((r) => RETIRED_DIAG_LABELS.has(r.label));
|
|
@@ -279,11 +315,54 @@ function setDiagSchedule(schedule) {
|
|
|
279
315
|
writeDiagReport(base);
|
|
280
316
|
return base;
|
|
281
317
|
}
|
|
318
|
+
// One field per call, validated against the same sets the CLI uses.
|
|
319
|
+
async function saveDefaults(body) {
|
|
320
|
+
const profile = await storedProfile();
|
|
321
|
+
if (body.comboDefault !== undefined) {
|
|
322
|
+
const level = normalizeComboLevel(body.comboDefault);
|
|
323
|
+
if (!level)
|
|
324
|
+
return { ok: false, error: 'unknown combo level' };
|
|
325
|
+
profile.comboDefault = level;
|
|
326
|
+
}
|
|
327
|
+
if (body.cavemanDefault !== undefined) {
|
|
328
|
+
const v = String(body.cavemanDefault);
|
|
329
|
+
if (!CAVEMAN_DEFAULTS.has(v))
|
|
330
|
+
return { ok: false, error: 'unknown caveman level' };
|
|
331
|
+
profile.cavemanDefault = v;
|
|
332
|
+
}
|
|
333
|
+
if (body.ponytailDefault !== undefined) {
|
|
334
|
+
const v = String(body.ponytailDefault);
|
|
335
|
+
if (!PONYTAIL_DEFAULTS.has(v))
|
|
336
|
+
return { ok: false, error: 'unknown ponytail level' };
|
|
337
|
+
profile.ponytailDefault = v;
|
|
338
|
+
}
|
|
339
|
+
if (body.backupSchedule !== undefined) {
|
|
340
|
+
const v = String(body.backupSchedule);
|
|
341
|
+
if (!BACKUP_SCHEDULES.has(v))
|
|
342
|
+
return { ok: false, error: 'unknown backup schedule' };
|
|
343
|
+
profile.backupSchedule = v;
|
|
344
|
+
}
|
|
345
|
+
if (body.rtkDefault !== undefined) {
|
|
346
|
+
if (typeof body.rtkDefault !== 'boolean')
|
|
347
|
+
return { ok: false, error: 'rtk default must be true or false' };
|
|
348
|
+
profile.rtkDefault = body.rtkDefault;
|
|
349
|
+
}
|
|
350
|
+
await writePluginSettings(profile, {});
|
|
351
|
+
return {
|
|
352
|
+
ok: true,
|
|
353
|
+
profile: {
|
|
354
|
+
comboDefault: profile.comboDefault,
|
|
355
|
+
cavemanDefault: profile.cavemanDefault,
|
|
356
|
+
rtkDefault: profile.rtkDefault,
|
|
357
|
+
ponytailDefault: profile.ponytailDefault,
|
|
358
|
+
backupSchedule: profile.backupSchedule,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
282
362
|
function readDiagSchedule() {
|
|
283
363
|
return readDiagReport()?.schedule ?? 'manual';
|
|
284
364
|
}
|
|
285
|
-
// Persist the picker choice: every run serves a fresh port, so localStorage
|
|
286
|
-
// alone cannot survive a restart.
|
|
365
|
+
// Persist the picker choice: every run serves a fresh port, so localStorage alone cannot survive a restart.
|
|
287
366
|
async function saveDashboardCurrency(raw) {
|
|
288
367
|
if (typeof raw !== 'string')
|
|
289
368
|
return null;
|
|
@@ -295,6 +374,50 @@ async function saveDashboardCurrency(raw) {
|
|
|
295
374
|
await writePluginSettings(profile, {});
|
|
296
375
|
return code;
|
|
297
376
|
}
|
|
377
|
+
export const EXPORT_FORMATS = ['json', 'jsonl', 'csv'];
|
|
378
|
+
export function exportRows(report) {
|
|
379
|
+
return report.recent.map((r) => ({
|
|
380
|
+
timestamp: new Date(r.t).toISOString(),
|
|
381
|
+
local: new Date(r.t).toLocaleString(),
|
|
382
|
+
agent: r.h ?? 'pi',
|
|
383
|
+
model: r.m,
|
|
384
|
+
input: r.i,
|
|
385
|
+
output: r.o,
|
|
386
|
+
cacheRead: r.cr ?? 0,
|
|
387
|
+
cacheWrite: r.cw ?? 0,
|
|
388
|
+
costUsd: r.usd,
|
|
389
|
+
elapsedMs: r.d,
|
|
390
|
+
status: r.st,
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
export function csvCell(v) {
|
|
394
|
+
const s = v === undefined || v === null ? '' : String(v);
|
|
395
|
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
396
|
+
}
|
|
397
|
+
export function exportBody(format, report) {
|
|
398
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
399
|
+
if (format === 'json') {
|
|
400
|
+
return { body: JSON.stringify({ exportedAt: new Date().toISOString(), version: report.version, source: report.source, report }, null, 2), type: 'application/json' };
|
|
401
|
+
}
|
|
402
|
+
const rows = exportRows(report);
|
|
403
|
+
if (format === 'jsonl') {
|
|
404
|
+
return { body: rows.map((r) => JSON.stringify(r)).join('\n') + '\n', type: 'application/x-ndjson' };
|
|
405
|
+
}
|
|
406
|
+
const cols = Object.keys(rows[0] ?? { timestamp: '', agent: '' });
|
|
407
|
+
const head = cols.join(',');
|
|
408
|
+
const body = [head, ...rows.map((r) => cols.map((c) => csvCell(r[c])).join(','))].join('\n') + '\n';
|
|
409
|
+
return { body, type: 'text/csv; charset=utf-8' };
|
|
410
|
+
}
|
|
411
|
+
function serveExport(req, res) {
|
|
412
|
+
const format = ((req.url || '').split('?')[1] || '').match(/format=([a-z]+)/)?.[1];
|
|
413
|
+
const fmt = format && EXPORT_FORMATS.includes(format) ? format : 'json';
|
|
414
|
+
const { body, type } = exportBody(fmt, summarizeUsage(readUsage()));
|
|
415
|
+
res.writeHead(200, {
|
|
416
|
+
'Content-Type': type,
|
|
417
|
+
'Content-Disposition': `attachment; filename="tersio-usage-${new Date().toISOString().slice(0, 10)}.${fmt === 'jsonl' ? 'ndjson' : fmt}"`,
|
|
418
|
+
});
|
|
419
|
+
res.end(body);
|
|
420
|
+
}
|
|
298
421
|
function readBody(req) {
|
|
299
422
|
return new Promise((resolve) => {
|
|
300
423
|
let body = '';
|
|
@@ -307,8 +430,7 @@ function openBrowser(url) {
|
|
|
307
430
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
308
431
|
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
309
432
|
}
|
|
310
|
-
// Replacers throughout: session data holds `$'` (shell quoting) and literal
|
|
311
|
-
// `</script>`, which String.replace would mangle.
|
|
433
|
+
// Replacers throughout: session data holds `$'` (shell quoting) and literal `</script>`, which String.replace would mangle.
|
|
312
434
|
function escapeInline(json) {
|
|
313
435
|
return json.replace(/<\/(script)/gi, '<\\/$1');
|
|
314
436
|
}
|
|
@@ -351,6 +473,82 @@ async function runDashboard(options) {
|
|
|
351
473
|
res.end(JSON.stringify({ ok: true, rows }));
|
|
352
474
|
return;
|
|
353
475
|
}
|
|
476
|
+
if (req.url === '/backups' && req.method === 'GET') {
|
|
477
|
+
const rows = listUsageBackups().map((b) => ({ file: path.basename(b.file), mtime: b.mtime, size: b.size }));
|
|
478
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
479
|
+
res.end(JSON.stringify({ backups: rows }));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (req.url === '/backups/create' && req.method === 'POST') {
|
|
483
|
+
try {
|
|
484
|
+
backupUsageDb(usageDbPath());
|
|
485
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
486
|
+
res.end(JSON.stringify({ ok: true }));
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
490
|
+
res.end(JSON.stringify({ ok: false, error: 'backup failed' }));
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (req.url === '/backups/delete' && req.method === 'POST') {
|
|
495
|
+
let file = '';
|
|
496
|
+
try {
|
|
497
|
+
file = String(JSON.parse(await readBody(req)).file ?? '');
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
file = '';
|
|
501
|
+
}
|
|
502
|
+
const ok = deleteUsageBackup(file);
|
|
503
|
+
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
504
|
+
res.end(JSON.stringify(ok ? { ok: true, file } : { ok: false, error: 'unknown backup' }));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (req.url === '/backups/restore' && req.method === 'POST') {
|
|
508
|
+
let file = '';
|
|
509
|
+
try {
|
|
510
|
+
file = String(JSON.parse(await readBody(req)).file ?? '');
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
file = '';
|
|
514
|
+
}
|
|
515
|
+
const ok = restoreUsageBackup(file);
|
|
516
|
+
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
517
|
+
res.end(JSON.stringify(ok ? { ok: true, file } : { ok: false, error: 'unknown backup' }));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (req.url === '/settings' && req.method === 'GET') {
|
|
521
|
+
storedProfile().then((profile) => {
|
|
522
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
523
|
+
res.end(JSON.stringify({
|
|
524
|
+
comboDefault: profile.comboDefault,
|
|
525
|
+
cavemanDefault: profile.cavemanDefault,
|
|
526
|
+
rtkDefault: profile.rtkDefault,
|
|
527
|
+
ponytailDefault: profile.ponytailDefault,
|
|
528
|
+
backupSchedule: profile.backupSchedule,
|
|
529
|
+
}));
|
|
530
|
+
});
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (req.url === '/settings' && req.method === 'POST') {
|
|
534
|
+
let body = {};
|
|
535
|
+
try {
|
|
536
|
+
body = JSON.parse(await readBody(req)) ?? {};
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
body = {};
|
|
540
|
+
}
|
|
541
|
+
const saved = await saveDefaults(body);
|
|
542
|
+
if (!saved.ok) {
|
|
543
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
544
|
+
res.end(JSON.stringify({ ok: false, error: saved.error }));
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
548
|
+
res.end(JSON.stringify({ ok: true, ...saved.profile }));
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
354
552
|
if (req.url === '/currency' && req.method === 'POST') {
|
|
355
553
|
let code = null;
|
|
356
554
|
try {
|
|
@@ -375,11 +573,20 @@ async function runDashboard(options) {
|
|
|
375
573
|
res.end(dataJson());
|
|
376
574
|
return;
|
|
377
575
|
}
|
|
576
|
+
if (req.url?.startsWith('/export')) {
|
|
577
|
+
serveExport(req, res);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
378
580
|
if (req.url === '/health') {
|
|
379
581
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
380
582
|
res.end(healthJson());
|
|
381
583
|
return;
|
|
382
584
|
}
|
|
585
|
+
if (req.url === '/status' && req.method === 'GET') {
|
|
586
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
587
|
+
res.end(await statusJson());
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
383
590
|
if (req.url === '/doctor' && req.method === 'POST') {
|
|
384
591
|
let schedule = 'manual';
|
|
385
592
|
try {
|