@krtclcdy/tersio 2.25.3 → 2.25.4

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.
@@ -1,492 +0,0 @@
1
- // cli/headroom.ts — the Headroom add-on: bundle, detect, wrap, unwrap, status, doctor facts.
2
- // Tersio installs the bundle itself; a Headroom the user installed is detected and left alone.
3
- import { existsSync, promises as fs, readFileSync } from 'node:fs';
4
- import path from 'node:path';
5
- import { execFileSync, spawn } from 'node:child_process';
6
- import { HOME, args, commandArg, dryRun, execP, flagValue, verbose } from './common.js';
7
- import { detectHosts } from './hosts.js';
8
- import { installHeadroom, updateHeadroom, removeHeadroomBundle } from './headroom-install.js';
9
- import { readTextIfExists, resolveHeadroomBinary, sha256File, tersioDataPath } from '../extensions/lib/utils.js';
10
- import { appendUsage } from '../extensions/shared/usage-ledger.js';
11
- import { HEADROOM_AGENTS, HEADROOM_INSTALL_HINT, HEADROOM_MIN_VERSION, HEADROOM_TESTED_VERSION, headroomAgentState, headroomCapability, headroomProxyUp, headroomProxyUrl, headroomVersionIssue, headroomWiringPresent, parseHeadroomVersion, readHeadroomConfig, writeHeadroomConfig, } from '../extensions/shared/headroom.js';
12
- const configHome = () => process.env.XDG_CONFIG_HOME || path.join(HOME, '.config');
13
- // The spike verified these two rewrites. pi has no wrap entry, so it stays manual.
14
- export const HEADROOM_ADAPTERS = {
15
- omp: {
16
- label: 'Oh My Pi',
17
- files: () => [path.join(HOME, '.omp', 'agent', 'models.yml')],
18
- wrapArgs: () => ['wrap', 'omp', '--prepare-only'],
19
- manualHint: '',
20
- },
21
- opencode: {
22
- label: 'OpenCode',
23
- files: () => [path.join(configHome(), 'opencode', 'opencode.json')],
24
- // No MCP server and no Serena: Headroom's memory features are not part of this add-on.
25
- wrapArgs: () => ['wrap', 'opencode', '--prepare-only', '--no-mcp', '--no-serena'],
26
- manualHint: '',
27
- },
28
- pi: {
29
- label: 'Pi',
30
- files: () => [path.join(HOME, '.pi', 'agent', 'models-store.json')],
31
- wrapArgs: () => [],
32
- manualHint: 'Pi has no `headroom wrap` entry. Point one model baseUrl at '
33
- + `${headroomProxyUrl()}/v1 in ~/.pi/agent/models-store.json, then set HEADROOM_BEACON=off for that session.`,
34
- },
35
- };
36
- function proxyRecordPath() {
37
- return tersioDataPath('headroom-proxy.json', 'tersio-headroom-proxy.json');
38
- }
39
- function readProxyRecord() {
40
- try {
41
- const raw = JSON.parse(readFileSync(proxyRecordPath(), 'utf8'));
42
- if (typeof raw.port !== 'number' || typeof raw.pid !== 'number')
43
- return null;
44
- return { port: raw.port, pid: raw.pid, startedAt: raw.startedAt ?? 0 };
45
- }
46
- catch {
47
- return null;
48
- }
49
- }
50
- async function writeProxyRecord(record) {
51
- try {
52
- await fs.mkdir(path.dirname(proxyRecordPath()), { recursive: true });
53
- await fs.writeFile(proxyRecordPath(), `${JSON.stringify(record, null, 2)}\n`, 'utf8');
54
- }
55
- catch { /* best-effort; a missing record only means an orphan proxy */ }
56
- }
57
- async function clearProxyRecord() {
58
- try {
59
- await fs.rm(proxyRecordPath(), { force: true });
60
- }
61
- catch { /* nothing to clear */ }
62
- }
63
- async function waitForProxy(port, timeoutMs = proxyTimeoutMs()) {
64
- const deadline = Date.now() + timeoutMs;
65
- for (;;) {
66
- if (await headroomProxyUp(port))
67
- return true;
68
- if (Date.now() >= deadline)
69
- return false;
70
- await new Promise((resolve) => setTimeout(resolve, 250));
71
- }
72
- }
73
- /** How long enable polls a freshly started proxy. Overridable so a test or CI run does not wait the full window. */
74
- function proxyTimeoutMs() {
75
- const raw = Number.parseInt(process.env.TERSIO_HEADROOM_PROXY_TIMEOUT_MS ?? '', 10);
76
- return Number.isInteger(raw) && raw > 0 ? raw : 6000;
77
- }
78
- /** Beacon and update check off: Tersio is local-only, and the user opted into neither. */
79
- async function startProxy(bin, port) {
80
- try {
81
- const child = spawn(bin, ['proxy', '--port', String(port)], {
82
- detached: true,
83
- stdio: 'ignore',
84
- env: { ...process.env, HEADROOM_BEACON: 'off', HEADROOM_UPDATE_CHECK: 'off' },
85
- });
86
- child.unref();
87
- await writeProxyRecord({ port, pid: child.pid ?? -1, startedAt: Date.now() });
88
- }
89
- catch (e) {
90
- console.log(` [warn] could not start the Headroom proxy: ${e.message}`);
91
- return false;
92
- }
93
- const up = await waitForProxy(port);
94
- if (!up)
95
- console.log(` [warn] the Headroom proxy did not answer on port ${port}.`);
96
- return up;
97
- }
98
- async function stopProxy() {
99
- const record = readProxyRecord();
100
- if (record && record.pid > 0) {
101
- try {
102
- process.kill(record.pid, 'SIGTERM');
103
- }
104
- catch { /* already gone */ }
105
- }
106
- await clearProxyRecord();
107
- }
108
- function backupStorePath() {
109
- return tersioDataPath('headroom-backup.json', 'tersio-headroom-backup.json');
110
- }
111
- async function readBackupStore() {
112
- const raw = await readTextIfExists(backupStorePath());
113
- if (!raw)
114
- return {};
115
- try {
116
- const parsed = JSON.parse(raw);
117
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
118
- return {};
119
- return parsed;
120
- }
121
- catch {
122
- return {};
123
- }
124
- }
125
- async function writeBackupRecord(record) {
126
- const store = await readBackupStore();
127
- store[record.agent] = record;
128
- try {
129
- await fs.mkdir(path.dirname(backupStorePath()), { recursive: true });
130
- await fs.writeFile(backupStorePath(), `${JSON.stringify(store, null, 2)}\n`, 'utf8');
131
- }
132
- catch { /* best-effort */ }
133
- }
134
- async function snapshotAgent(agent) {
135
- const files = [];
136
- for (const file of HEADROOM_ADAPTERS[agent].files()) {
137
- files.push({ path: file, sha256: existsSync(file) ? await sha256File(file).catch(() => null) : null });
138
- }
139
- const record = { agent, at: Date.now(), files };
140
- await writeBackupRecord(record);
141
- return record;
142
- }
143
- /** True when every snapshot file is back to the hash recorded before the wrap. */
144
- async function verifyRestore(record) {
145
- for (const file of record.files) {
146
- if (file.sha256 === null) {
147
- if (existsSync(file.path))
148
- return false;
149
- continue;
150
- }
151
- if (!existsSync(file.path))
152
- return false;
153
- if (await sha256File(file.path).catch(() => null) !== file.sha256)
154
- return false;
155
- }
156
- return true;
157
- }
158
- // --- headroom binary -------------------------------------------------------------------
159
- async function headroomVersion(bin) {
160
- try {
161
- const result = await execP(bin, ['--version'], { timeout: 5000 });
162
- return parseHeadroomVersion(result.stdout || result.stderr);
163
- }
164
- catch (e) {
165
- const err = e;
166
- return parseHeadroomVersion(err.stdout || err.stderr);
167
- }
168
- }
169
- /** Sync variant for callers that cannot await (the dashboard's doctor rows). */
170
- export function headroomVersionSync(bin) {
171
- try {
172
- return parseHeadroomVersion(execFileSync(bin, ['--version'], { encoding: 'utf8', timeout: 5000 }));
173
- }
174
- catch (e) {
175
- const err = e;
176
- return parseHeadroomVersion(err.stdout || err.stderr);
177
- }
178
- }
179
- /** True when the proxy Tersio started is still running. Sync, so the dashboard can use it inside row building. */
180
- export function headroomProxyAlive() {
181
- const record = readProxyRecord();
182
- if (!record || record.pid <= 0)
183
- return false;
184
- try {
185
- process.kill(record.pid, 0);
186
- return true;
187
- }
188
- catch {
189
- return false;
190
- }
191
- }
192
- /** The port Tersio recorded for the running proxy, or null when nothing was started. */
193
- export function headroomProxyPort() {
194
- const record = readProxyRecord();
195
- return record && record.port > 0 ? record.port : null;
196
- }
197
- async function runHeadroomArgv(bin, argv) {
198
- try {
199
- await execP(bin, argv, { timeout: 60000 });
200
- return true;
201
- }
202
- catch (e) {
203
- const err = e;
204
- console.log(` [fail] headroom ${argv.join(' ')}: ${(err.stderr || err.message || '').trim().slice(0, 200)}`);
205
- return false;
206
- }
207
- }
208
- // --- selectors -------------------------------------------------------------------------
209
- function installedAgents() {
210
- const installed = detectHosts().filter((host) => host.installed).map((host) => host.id);
211
- const found = HEADROOM_AGENTS.filter((agent) => installed.includes(agent));
212
- return found.length > 0 ? found : [...HEADROOM_AGENTS];
213
- }
214
- function parseAgentArg() {
215
- const raw = flagValue('--agent');
216
- if (raw === undefined)
217
- return null;
218
- const value = raw.trim().toLowerCase();
219
- if (HEADROOM_AGENTS.includes(value))
220
- return value;
221
- console.error(`[fail] Invalid --agent: ${raw}. Valid: ${HEADROOM_AGENTS.join(', ')}`);
222
- process.exit(1);
223
- }
224
- function selectedAgents(explicit = parseAgentArg()) {
225
- if (explicit)
226
- return [explicit];
227
- if (args.includes('--all'))
228
- return [...HEADROOM_AGENTS];
229
- return installedAgents();
230
- }
231
- // --- enable / disable ------------------------------------------------------------------
232
- /** What Headroom actually routes for one agent, read from its config. Null when nothing is wired. Only the routed provider is compressed. */
233
- export function headroomRouteFor(agent, port) {
234
- const file = HEADROOM_ADAPTERS[agent].files()[0];
235
- let raw;
236
- try {
237
- if (!existsSync(file))
238
- return null;
239
- raw = readFileSync(file, 'utf8');
240
- }
241
- catch {
242
- return null;
243
- }
244
- if (agent === 'pi')
245
- return 'manual — point one model baseUrl at the proxy';
246
- if (agent === 'omp') {
247
- const lines = raw.split('\n');
248
- const at = lines.findIndex((line) => line.includes(`127.0.0.1:${port}`));
249
- if (at === -1)
250
- return null;
251
- // The wrapped provider is the block that owns the proxy baseUrl.
252
- for (let i = at; i >= 0; i -= 1) {
253
- const match = /^ {2}([A-Za-z0-9_-]+):\s*$/.exec(lines[i]);
254
- if (match)
255
- return `providers.${match[1]}.baseUrl`;
256
- }
257
- return null;
258
- }
259
- try {
260
- const parsed = JSON.parse(raw);
261
- return parsed.provider?.headroom ? 'provider.headroom — use a headroom/* model' : null;
262
- }
263
- catch {
264
- return null;
265
- }
266
- }
267
- /** The kill switch mirrors the agent flags: on only while at least one agent is wired. */
268
- function writeAgent(config, agent, enabled, wrappedByTersio) {
269
- const agents = { ...config.agents, [agent]: { enabled, wrappedByTersio } };
270
- const next = {
271
- support: HEADROOM_AGENTS.some((other) => agents[other]?.enabled === true),
272
- port: config.port,
273
- agents,
274
- };
275
- writeHeadroomConfig(next);
276
- return next;
277
- }
278
- async function enableAgent(agent, bin, port) {
279
- const config = readHeadroomConfig();
280
- const { label, wrapArgs, manualHint } = HEADROOM_ADAPTERS[agent];
281
- const current = headroomAgentState(config, agent);
282
- const native = headroomCapability(agent) === 'native';
283
- if (current.enabled) {
284
- console.log(` [skip] ${label}: already enabled.`);
285
- return true;
286
- }
287
- if (dryRun) {
288
- console.log(` [dry-run] would ${native ? `run headroom ${wrapArgs().join(' ')}` : 'print the manual base URL'}, start the proxy on port ${port}, and record ${label}`);
289
- return true;
290
- }
291
- // Snapshot before the wrap so the hashes prove a byte-identical restore later.
292
- const record = await snapshotAgent(agent);
293
- if (native && !(await runHeadroomArgv(bin, wrapArgs())))
294
- return false;
295
- const record2 = readProxyRecord();
296
- const alreadyUp = record2 ? await headroomProxyUp(record2.port) : false;
297
- const proxyUp = alreadyUp || await startProxy(bin, port);
298
- const next = writeAgent(config, agent, true, native);
299
- appendUsage('headroom', `enable ${agent}${proxyUp ? '' : ' (proxy down)'}`, { source: 'headroom', agent });
300
- console.log(` [ok] Headroom enabled for ${label} (${native ? 'native wrap' : 'manual base URL'}).`);
301
- if (native && headroomWiringPresent(HEADROOM_ADAPTERS[agent].files()))
302
- console.log(` [ok] Wiring present · snapshot ${record.files.length} file(s) recorded.`);
303
- console.log(` [info] Proxy: ${headroomProxyUrl(next.port)} ${proxyUp ? '(up)' : '(DOWN — run: headroom proxy --port ' + next.port + ')'}`);
304
- console.log(' [info] Beacon off, update check off.');
305
- console.log(' [info] Tersio leaves HEADROOM_OUTPUT_SHAPER unset: Caveman owns terse output.');
306
- if (!native)
307
- console.log(` [manual] ${manualHint}`);
308
- console.log(` [info] Headroom sees prompts and tool output on loopback. It is a separate process Tersio starts and stops.`);
309
- return true;
310
- }
311
- async function disableAgent(agent, bin) {
312
- const config = readHeadroomConfig();
313
- const { label } = HEADROOM_ADAPTERS[agent];
314
- const current = headroomAgentState(config, agent);
315
- if (!current.enabled) {
316
- console.log(` [skip] ${label}: already off.`);
317
- return true;
318
- }
319
- if (dryRun) {
320
- console.log(` [dry-run] would ${current.wrappedByTersio ? `run headroom unwrap ${agent}` : 'clear the manual flag'}, verify the snapshot, and stop the proxy when no agent remains`);
321
- return true;
322
- }
323
- const store = await readBackupStore();
324
- const record = store[agent];
325
- // Another agent on the same proxy must keep it, so its unwrap may not stop the proxy.
326
- const othersEnabled = HEADROOM_AGENTS.some((other) => other !== agent && headroomAgentState(config, other).enabled);
327
- if (current.wrappedByTersio) {
328
- if (!bin) {
329
- console.log(` [fail] ${label}: the headroom binary is missing, so unwrap cannot run.`);
330
- return false;
331
- }
332
- const unwrapArgs = ['unwrap', agent, ...(othersEnabled ? ['--no-stop-proxy'] : [])];
333
- if (!(await runHeadroomArgv(bin, unwrapArgs)))
334
- return false;
335
- if (record && !(await verifyRestore(record))) {
336
- console.log(` [warn] ${label}: the config did not return to its recorded hash.`);
337
- console.log(` [hint] headroom keeps its own copy at ${HEADROOM_ADAPTERS[agent].files()[0]}.headroom-backup`);
338
- return false;
339
- }
340
- console.log(` [ok] ${label}: restored${record ? ' byte-identical' : ''}.`);
341
- }
342
- else {
343
- console.log(` [ok] ${label}: manual flag cleared. Tersio never edited its config.`);
344
- }
345
- const next = writeAgent(config, agent, false, false);
346
- const stillEnabled = HEADROOM_AGENTS.some((other) => headroomAgentState(next, other).enabled);
347
- if (!stillEnabled)
348
- await stopProxy();
349
- appendUsage('headroom', `disable ${agent}`, { source: 'headroom', agent });
350
- return true;
351
- }
352
- // --- reporting -------------------------------------------------------------------------
353
- async function statusReport() {
354
- const bin = resolveHeadroomBinary();
355
- const version = bin ? await headroomVersion(bin) : null;
356
- const config = readHeadroomConfig();
357
- const up = await headroomProxyUp(config.port);
358
- console.log('\n=== Tersio Headroom ===');
359
- console.log(` Binary: ${bin ? `${bin}${version ? ` ${version}` : ''}` : `not found — ${HEADROOM_INSTALL_HINT}`}`);
360
- console.log(` Support: ${config.support ? 'on' : 'off'}`);
361
- console.log(` Proxy: ${headroomProxyUrl(config.port)} ${up ? '(up)' : '(down)'}`);
362
- for (const agent of HEADROOM_AGENTS) {
363
- const state = headroomAgentState(config, agent);
364
- const kind = headroomCapability(agent);
365
- const how = state.wrappedByTersio ? 'wrapped by tersio' : kind;
366
- console.log(` — ${HEADROOM_ADAPTERS[agent].label}: ${state.enabled ? `enabled (${how})` : 'off'}`);
367
- }
368
- }
369
- /** The data doctor renders; it makes no decision of its own. */
370
- export async function headroomFacts() {
371
- const bin = resolveHeadroomBinary();
372
- const version = bin ? await headroomVersion(bin) : null;
373
- const config = readHeadroomConfig();
374
- const wiring = {};
375
- for (const agent of HEADROOM_AGENTS) {
376
- if (!headroomAgentState(config, agent).enabled)
377
- continue;
378
- wiring[agent] = headroomWiringPresent(HEADROOM_ADAPTERS[agent].files());
379
- }
380
- return { bin, version, issue: headroomVersionIssue(version), proxyUp: await headroomProxyUp(config.port), config, wiring };
381
- }
382
- // --- uninstall hooks -------------------------------------------------------------------
383
- export async function headroomUnwrapAgents(agents) {
384
- const config = readHeadroomConfig();
385
- const bin = resolveHeadroomBinary();
386
- for (const agent of agents) {
387
- if (headroomAgentState(config, agent).wrappedByTersio)
388
- await disableAgent(agent, bin);
389
- }
390
- }
391
- /** Unwraps every agent Tersio wrapped. A manual agent is only unflagged. */
392
- export async function headroomUnwrapConfigured() {
393
- await headroomUnwrapAgents([...HEADROOM_AGENTS]);
394
- }
395
- /** Removes the Tersio-owned bundle once no agent is left enabled, so uninstalling one host cannot break another agent's proxy. */
396
- export async function headroomRemoveBundleIfUnused(options = {}) {
397
- const config = readHeadroomConfig();
398
- if (HEADROOM_AGENTS.some((agent) => headroomAgentState(config, agent).enabled))
399
- return false;
400
- return removeHeadroomBundle(options);
401
- }
402
- /** Removes the binary only when the user asks for it. D4: Tersio mirrors `--remove-rtk`. */
403
- export async function removeHeadroomBinary() {
404
- const bin = resolveHeadroomBinary();
405
- if (!bin)
406
- return true;
407
- try {
408
- await fs.rm(bin, { force: true });
409
- console.log(` [ok] removed ${bin}`);
410
- return true;
411
- }
412
- catch (e) {
413
- console.log(` [fail] could not remove ${bin}: ${e.message}`);
414
- return false;
415
- }
416
- }
417
- // --- command ---------------------------------------------------------------------------
418
- function subcommand() {
419
- const at = args.indexOf(commandArg ?? '');
420
- const next = at >= 0 ? args[at + 1] : undefined;
421
- if (!next || next.startsWith('-'))
422
- return 'status';
423
- return next.toLowerCase();
424
- }
425
- function usage() {
426
- console.log(' Usage: tersio headroom <install|enable|disable|status|update> [--agent omp|pi|opencode|--all] [--dry-run]');
427
- }
428
- /** Enable or disable with every precheck, for callers that want a boolean instead of an exit code (the dashboard HTTP routes). */
429
- export async function headroomAction(sub, agent = null) {
430
- let bin = resolveHeadroomBinary();
431
- // Only enable needs the binary: disabling a manual flag is a file write, and a wrapped agent still needs its unwrap.
432
- if (sub === 'enable' && !bin) {
433
- // The bundle is Tersio's to provide, so enable installs it rather than sending the user to a Python tool.
434
- console.log(' [info] Headroom is not installed yet — building the Tersio bundle.');
435
- const installed = await installHeadroom({ dryRun, verbose });
436
- if (!installed.ok) {
437
- console.log(` [fail] ${installed.detail}`);
438
- return false;
439
- }
440
- bin = resolveHeadroomBinary();
441
- if (!bin && !dryRun) {
442
- console.log(` [fail] Headroom installed, but no entrypoint resolved.`);
443
- return false;
444
- }
445
- }
446
- const version = bin ? await headroomVersion(bin) : null;
447
- const issue = headroomVersionIssue(version);
448
- if (sub === 'enable' && issue === 'old') {
449
- console.log(` [fail] Headroom ${version} is older than the supported ${HEADROOM_MIN_VERSION}. Update it, then retry.`);
450
- return false;
451
- }
452
- if (sub === 'enable' && issue === 'newer') {
453
- console.log(` [warn] Headroom ${version} is newer than the tested ${HEADROOM_TESTED_VERSION}. Tersio tests the pinned range only.`);
454
- }
455
- const port = readHeadroomConfig().port;
456
- const targets = agent ? [agent] : selectedAgents();
457
- let ok = true;
458
- for (const target of targets) {
459
- ok = (sub === 'enable' ? await enableAgent(target, bin ?? 'headroom', port) : await disableAgent(target, bin)) && ok;
460
- }
461
- return ok;
462
- }
463
- /** One entry point for the CLI and the interactive menu: an explicit subcommand, plus an optional single agent. */
464
- export async function runHeadroomCommand(sub, agent = null) {
465
- if (!['enable', 'disable', 'status', 'install', 'update'].includes(sub)) {
466
- console.error(`[fail] Unknown headroom subcommand: ${sub}`);
467
- usage();
468
- process.exitCode = 1;
469
- return;
470
- }
471
- if (sub === 'status') {
472
- await statusReport();
473
- return;
474
- }
475
- if (sub === 'install' || sub === 'update') {
476
- const result = sub === 'install'
477
- ? await installHeadroom({ dryRun, verbose })
478
- : await updateHeadroom({ dryRun, verbose });
479
- if (result.ok)
480
- console.log(` [ok] Headroom ${result.version ?? ''} → ${result.bin} (${result.detail})`);
481
- else {
482
- console.log(` [fail] ${result.detail}`);
483
- process.exitCode = 1;
484
- }
485
- return;
486
- }
487
- if (!(await headroomAction(sub, agent)))
488
- process.exitCode = 1;
489
- }
490
- export async function runHeadroom() {
491
- await runHeadroomCommand(subcommand(), parseAgentArg());
492
- }
@@ -1,93 +0,0 @@
1
- // /headroom session toggle. Headroom is a machine-level add-on, so this extension mirrors the stored per-agent flags and delegates every state change to the CLI.
2
- import { HEADROOM_INSTALL_HINT, headroomAgentState, headroomCapability, headroomProxyUp, headroomStatusFor, readHeadroomConfig, setHeadroomStatus, } from '../shared/headroom.js';
3
- import { onHostEvent, setExtensionLabel } from '../shared/host.js';
4
- import { announceStatus } from '../shared/status.js';
5
- /** The host's own key in the per-agent Headroom flags. */
6
- function agentOf(pi) {
7
- return pi.hostId ?? 'omp';
8
- }
9
- function statusText(agent, status) {
10
- const manual = headroomCapability(agent) === 'manual';
11
- const port = readHeadroomConfig().port;
12
- if (status === 'down')
13
- return `Headroom is on for ${agent}${manual ? ' (manual base URL)' : ''}, but the proxy does not answer. Restart it with: headroom proxy --port ${port}`;
14
- if (status === 'manual')
15
- return `Headroom is on for ${agent} (manual base URL). Point the model base URL at the proxy.`;
16
- if (status === 'on')
17
- return `Headroom is on for ${agent}.`;
18
- return `Headroom is off for ${agent}.`;
19
- }
20
- export default function headroomSessionExtension(pi) {
21
- setExtensionLabel(pi, 'Headroom session toggle');
22
- const agent = agentOf(pi);
23
- let shaperWarned = false;
24
- // Reads the stored flags, then probes the proxy only when this agent is wired. Off installs never touch the network.
25
- async function refresh(ctx) {
26
- const config = readHeadroomConfig();
27
- const state = headroomAgentState(config, agent);
28
- // No segment until the user opts in, so a machine that merely has Headroom installed keeps the old status line.
29
- if (!config.support || !state.enabled) {
30
- setHeadroomStatus(null);
31
- return null;
32
- }
33
- const up = await headroomProxyUp(config.port);
34
- const status = headroomStatusFor(config, agent, up);
35
- setHeadroomStatus(status);
36
- // Caveman already owns terse output, so two shapers would fight over one prompt.
37
- if (!shaperWarned && process.env.HEADROOM_OUTPUT_SHAPER && process.env.HEADROOM_OUTPUT_SHAPER !== '0') {
38
- shaperWarned = true;
39
- ctx?.ui?.notify?.(`HEADROOM_OUTPUT_SHAPER is set while Caveman is active. Unset it: one prompt owner only.`, 'warning');
40
- }
41
- return status;
42
- }
43
- async function runCli(action, ctx) {
44
- if (typeof pi.exec !== 'function') {
45
- ctx?.ui?.notify?.(`Run in a shell: tersio headroom ${action} --agent ${agent}`, 'warning');
46
- return;
47
- }
48
- try {
49
- const result = await pi.exec('tersio', ['headroom', action, '--agent', agent], { cwd: ctx?.cwd ?? pi.cwd });
50
- const text = [result.stdout, result.stderr].map((part) => part.trim()).filter(Boolean).join('\n');
51
- ctx?.ui?.notify?.(text || `tersio headroom ${action} exited ${result.code}`, result.code === 0 ? 'info' : 'warning');
52
- }
53
- catch (e) {
54
- ctx?.ui?.notify?.(`tersio headroom ${action} failed (${e.message}). Run it in a shell.`, 'warning');
55
- }
56
- await refresh(ctx);
57
- announceStatus(ctx);
58
- }
59
- pi.registerCommand?.('headroom', {
60
- description: 'Toggle the Headroom compression proxy for this agent',
61
- handler: async (args, ctx) => {
62
- const arg = String(args || '').trim().toLowerCase();
63
- if (!arg || arg === 'status') {
64
- const status = await refresh(ctx);
65
- ctx?.ui?.notify?.(status === null
66
- ? `Headroom is not installed — ${HEADROOM_INSTALL_HINT}`
67
- : statusText(agent, status), 'info');
68
- announceStatus(ctx);
69
- return;
70
- }
71
- if (['on', 'enable', 'enabled', 'true'].includes(arg)) {
72
- await runCli('enable', ctx);
73
- return;
74
- }
75
- if (['off', 'disable', 'disabled', 'false'].includes(arg)) {
76
- await runCli('disable', ctx);
77
- return;
78
- }
79
- ctx?.ui?.notify?.('Usage: /headroom [on|off|status]', 'warning');
80
- },
81
- });
82
- // Start, resume, branch, and compaction all land here, so every session type shows the state.
83
- pi.on('session_start', async (_event, ctx) => {
84
- await refresh(ctx);
85
- announceStatus(ctx);
86
- });
87
- for (const event of ['session_branch', 'session_tree', 'session_switch', 'session_compact']) {
88
- onHostEvent(pi, event, async (_event, ctx) => {
89
- await refresh(ctx);
90
- announceStatus(ctx);
91
- });
92
- }
93
- }