talon-agent 5.0.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/package.json +1 -1
  2. package/src/app.ts +3 -3
  3. package/src/backend/claude-sdk/factory.ts +21 -41
  4. package/src/backend/claude-sdk/host/in-process.ts +147 -0
  5. package/src/backend/codex/mcp-config.ts +1 -1
  6. package/src/backend/openai-agents/mcp-pool.ts +1 -1
  7. package/src/backend/runtime/index.ts +2 -2
  8. package/src/backend/runtime/turn/delivery.ts +1 -1
  9. package/src/backend/runtime/turn/turn-phases.ts +1 -1
  10. package/src/bootstrap.ts +2 -2
  11. package/src/cli.ts +5 -2
  12. package/src/core/agent-runtime/README.md +15 -0
  13. package/src/core/agent-runtime/agent-host.ts +387 -0
  14. package/src/core/background/cron/scheduler.ts +1 -1
  15. package/src/core/background/triggers/command.ts +1 -1
  16. package/src/{util → core/config}/harden.ts +1 -1
  17. package/src/core/config/index.ts +1 -1
  18. package/src/core/daemon/pidfile.ts +1 -1
  19. package/src/core/daemon/resource-sampler.ts +2 -2
  20. package/src/{util → core/daemon}/respawn.ts +1 -1
  21. package/src/core/doctor/index.ts +1 -1
  22. package/src/core/engine/gateway-actions/fetch-url.ts +1 -1
  23. package/src/core/engine/gateway-actions/index.ts +1 -1
  24. package/src/core/engine/gateway-actions/memory.ts +2 -2
  25. package/src/core/engine/gateway-actions/native/exec-background.ts +129 -0
  26. package/src/core/engine/gateway-actions/native/exec-remote.ts +85 -0
  27. package/src/core/engine/gateway-actions/native/exec.ts +182 -0
  28. package/src/core/engine/gateway-actions/native/index.ts +46 -0
  29. package/src/core/engine/gateway-actions/native/params.ts +46 -0
  30. package/src/core/engine/gateway-actions/native/read.ts +182 -0
  31. package/src/core/engine/gateway-actions/native/results.ts +34 -0
  32. package/src/core/engine/gateway-actions/native/search.ts +212 -0
  33. package/src/core/engine/gateway-actions/native/shell.ts +39 -0
  34. package/src/core/engine/gateway-actions/native/teleport.ts +55 -0
  35. package/src/core/engine/gateway-actions/native/write.ts +170 -0
  36. package/src/core/frontend-runtime/builtins.ts +2 -2
  37. package/src/core/mcp-hub/index.ts +1 -1
  38. package/src/core/mcp-hub/talon-server.ts +1 -1
  39. package/src/core/memory/taps.ts +1 -1
  40. package/src/core/plugin/mcp.ts +1 -1
  41. package/src/core/scripts/lua.ts +1 -1
  42. package/src/core/tools/{chat.ts → chat/chat.ts} +1 -1
  43. package/src/core/tools/{cross-send.ts → chat/cross-send.ts} +1 -1
  44. package/src/core/tools/{history.ts → chat/history.ts} +2 -2
  45. package/src/core/tools/{media.ts → chat/media.ts} +1 -1
  46. package/src/core/tools/{members.ts → chat/members.ts} +2 -2
  47. package/src/core/tools/{messaging.ts → chat/messaging.ts} +2 -2
  48. package/src/core/tools/{moderation.ts → chat/moderation.ts} +2 -2
  49. package/src/core/tools/{stickers.ts → chat/stickers.ts} +2 -2
  50. package/src/core/tools/{whatsapp.ts → chat/whatsapp.ts} +1 -1
  51. package/src/core/tools/{memory.ts → content/memory.ts} +2 -2
  52. package/src/core/tools/{web.ts → content/web.ts} +1 -1
  53. package/src/core/tools/index.ts +20 -20
  54. package/src/core/tools/{admin.ts → ops/admin.ts} +1 -1
  55. package/src/core/tools/{bridge.ts → ops/bridge.ts} +2 -2
  56. package/src/core/tools/{goals.ts → ops/goals.ts} +2 -2
  57. package/src/core/tools/{mesh.ts → ops/mesh.ts} +1 -1
  58. package/src/core/tools/{models.ts → ops/models.ts} +1 -1
  59. package/src/core/tools/{native.ts → ops/native.ts} +1 -1
  60. package/src/core/tools/{scheduling.ts → ops/scheduling.ts} +1 -1
  61. package/src/core/tools/{scripts.ts → ops/scripts.ts} +1 -1
  62. package/src/core/tools/{skills.ts → ops/skills.ts} +1 -1
  63. package/src/core/tools/{triggers.ts → ops/triggers.ts} +1 -1
  64. package/src/{util → core/vfs}/workspace.ts +2 -2
  65. package/src/frontend/discord/callbacks/components/effort.ts +1 -1
  66. package/src/frontend/discord/callbacks/components/index.ts +1 -1
  67. package/src/frontend/discord/callbacks/components/settings.ts +1 -1
  68. package/src/frontend/discord/commands/admin.ts +1 -1
  69. package/src/frontend/discord/commands/info.ts +1 -1
  70. package/src/frontend/discord/commands/interaction.ts +1 -1
  71. package/src/frontend/discord/commands/session.ts +3 -3
  72. package/src/frontend/discord/commands/settings.ts +1 -1
  73. package/src/frontend/discord/handlers/messages.ts +1 -1
  74. package/src/frontend/discord/handlers/state.ts +1 -1
  75. package/src/frontend/discord/middleware.ts +1 -1
  76. package/src/frontend/discord/ready.ts +1 -1
  77. package/src/frontend/discord/render.ts +41 -259
  78. package/src/frontend/native/chats/chats.ts +1 -1
  79. package/src/frontend/native/surface/models.ts +1 -1
  80. package/src/frontend/native/turn/context.ts +1 -1
  81. package/src/frontend/native/turn/emit.ts +1 -1
  82. package/src/frontend/{shared → presentation}/format.ts +4 -3
  83. package/src/frontend/{shared → presentation}/reasoning-levels.ts +12 -0
  84. package/src/frontend/presentation/reports.ts +493 -0
  85. package/src/frontend/{shared → presentation}/session-status.ts +1 -1
  86. package/src/frontend/teams/commands.ts +2 -2
  87. package/src/frontend/teams/turn.ts +1 -1
  88. package/src/frontend/telegram/admin/health.ts +1 -1
  89. package/src/frontend/telegram/admin/sessions.ts +1 -1
  90. package/src/frontend/telegram/callbacks/effort.ts +2 -2
  91. package/src/frontend/telegram/callbacks/metrics.ts +1 -1
  92. package/src/frontend/telegram/callbacks/model/views.ts +1 -1
  93. package/src/frontend/telegram/callbacks/query.ts +1 -1
  94. package/src/frontend/telegram/callbacks/settings.ts +2 -2
  95. package/src/frontend/telegram/commands/admin.ts +4 -4
  96. package/src/frontend/telegram/commands/info.ts +2 -2
  97. package/src/frontend/telegram/commands/session.ts +4 -4
  98. package/src/frontend/telegram/commands/settings.ts +4 -2
  99. package/src/frontend/telegram/handlers/state.ts +1 -1
  100. package/src/frontend/telegram/model-menu.ts +1 -1
  101. package/src/frontend/telegram/render/html.ts +30 -0
  102. package/src/frontend/telegram/{helpers → render}/menu.ts +46 -19
  103. package/src/frontend/telegram/render/reports.ts +182 -0
  104. package/src/frontend/terminal/builtins/context.ts +2 -2
  105. package/src/frontend/terminal/builtins/session.ts +1 -1
  106. package/src/frontend/terminal/builtins/status.ts +2 -2
  107. package/src/frontend/terminal/index.ts +2 -2
  108. package/src/frontend/terminal/renderer.ts +1 -1
  109. package/src/frontend/whatsapp/commands.ts +5 -5
  110. package/src/frontend/whatsapp/registry.ts +1 -1
  111. package/src/index.ts +5 -2
  112. package/src/util/runtime.ts +1 -1
  113. package/src/core/engine/gateway-actions/native.ts +0 -1035
  114. package/src/frontend/telegram/helpers/diagnostics.ts +0 -428
  115. package/src/frontend/telegram/helpers/format.ts +0 -42
  116. package/src/frontend/telegram/helpers/index.ts +0 -13
  117. package/src/util/cleanup-registry.ts +0 -36
  118. /package/src/{util → core/daemon}/boot-timer.ts +0 -0
  119. /package/src/{util → core/frontend-runtime}/chat-id.ts +0 -0
  120. /package/src/{util/mcp-launcher.ts → core/mcp-hub/launcher.ts} +0 -0
  121. /package/src/{util → core/tools/content}/web-content.ts +0 -0
  122. /package/src/core/tools/{mcp-env.ts → ops/mcp-env.ts} +0 -0
  123. /package/src/{util → core/weaver}/session-name.ts +0 -0
  124. /package/src/frontend/{shared → presentation}/access.ts +0 -0
  125. /package/src/frontend/{shared → presentation}/model-commands.ts +0 -0
  126. /package/src/frontend/{shared → presentation}/plan-usage-report.ts +0 -0
  127. /package/src/frontend/{shared → presentation}/status-context.ts +0 -0
@@ -1,428 +0,0 @@
1
- /**
2
- * Metrics, doctor, and mesh report rendering for Telegram (HTML messages).
3
- */
4
-
5
- import { escapeHtml } from "../formatting.js";
6
- import type { DoctorReport } from "../../../core/doctor/index.js";
7
- import type { MeshPingResult } from "../../../core/mesh/service.js";
8
- import type { BackendUsageEntry } from "../../shared/plan-usage-report.js";
9
- import type { SettingsButton } from "./menu.js";
10
- import { formatDuration, formatBytes } from "./format.js";
11
-
12
- const DEFAULT_METRICS_MESSAGE_MAX = 3800;
13
-
14
- /**
15
- * Rows shown in the `tool_calls` leaderboard before the tail collapses
16
- * into a "…and N more" line. A long-lived bot accumulates a lifetime
17
- * tool-call entry per distinct tool name (easily 100+ once plugin and
18
- * MCP tools are counted), which is what used to push /metrics past
19
- * Telegram's message limit and split it across messages.
20
- */
21
- const TOOL_CALLS_TOP_N = 12;
22
-
23
- type MetricsSnapshot = {
24
- counters: Record<string, number>;
25
- histograms: Record<
26
- string,
27
- { count: number; avg: number; min: number; max: number }
28
- >;
29
- };
30
-
31
- /** Which grain the /metrics panel is currently showing. */
32
- export type MetricsView = "today" | "all";
33
-
34
- const VIEW_TITLES: Record<MetricsView, string> = {
35
- today: "Metrics — today (UTC)",
36
- all: "Metrics — all time",
37
- };
38
-
39
- /**
40
- * One titled block of the panel. `hidden` counts rows dropped to make
41
- * the panel fit one message; it renders as a trailing "…and N more".
42
- */
43
- type Section = { title: string; rows: string[]; hidden: number };
44
-
45
- function truncateMetricLabel(label: string, max = 80): string {
46
- return label.length <= max ? label : `${label.slice(0, max - 3)}...`;
47
- }
48
-
49
- function buildMetricsSections(metrics: MetricsSnapshot): Section[] {
50
- const sections: Section[] = [];
51
-
52
- // Histograms come in two flavours: durations (keys ending in `_ms`,
53
- // rendered as human times) and plain counts like `tool_calls_per_turn`
54
- // (rendered as bare numbers — "min=1ms" for a count is nonsense).
55
- const histKeys = Object.keys(metrics.histograms).sort();
56
- const durationKeys = histKeys.filter((key) => key.endsWith("_ms"));
57
- const countKeys = histKeys.filter((key) => !key.endsWith("_ms"));
58
- const histLine = (key: string, fmt: (v: number) => string): string => {
59
- const h = metrics.histograms[key];
60
- return (
61
- ` <code>${escapeHtml(truncateMetricLabel(key))}</code> n=${h.count} ` +
62
- `avg=${fmt(h.avg)} min=${fmt(h.min)} ` +
63
- `max=${fmt(h.max)}`
64
- );
65
- };
66
- if (durationKeys.length > 0) {
67
- sections.push({
68
- title: "<b>Latency</b>",
69
- rows: durationKeys.map((key) => histLine(key, formatDuration)),
70
- hidden: 0,
71
- });
72
- }
73
- if (countKeys.length > 0) {
74
- sections.push({
75
- title: "<b>Distributions</b>",
76
- rows: countKeys.map((key) => histLine(key, String)),
77
- hidden: 0,
78
- });
79
- }
80
-
81
- const counterKeys = Object.keys(metrics.counters).sort();
82
- if (counterKeys.length > 0) {
83
- const groups = new Map<string, string[]>();
84
- for (const key of counterKeys) {
85
- const prefix = key.includes(".") ? key.split(".")[0]! : "general";
86
- if (!groups.has(prefix)) groups.set(prefix, []);
87
- groups.get(prefix)!.push(key);
88
- }
89
-
90
- for (const prefix of [...groups.keys()].sort()) {
91
- // tool_calls reads best as a leaderboard — busiest tools first.
92
- // Other groups keep alphabetical order (stable lookup by name).
93
- const isToolCalls = prefix === "tool_calls";
94
- const keys = isToolCalls
95
- ? [...groups.get(prefix)!].sort(
96
- (a, b) =>
97
- metrics.counters[b]! - metrics.counters[a]! || a.localeCompare(b),
98
- )
99
- : groups.get(prefix)!;
100
- // The leaderboard is pre-capped: past the top N the tail is a long
101
- // list of one-call tools nobody reads, and on the all-time view it
102
- // is what made the panel overflow.
103
- const shown = isToolCalls ? keys.slice(0, TOOL_CALLS_TOP_N) : keys;
104
- sections.push({
105
- title: `<b>${escapeHtml(prefix)}</b>`,
106
- rows: shown.map((key) => {
107
- const label = key.includes(".")
108
- ? key.split(".").slice(1).join(".")
109
- : key;
110
- return (
111
- ` <code>${escapeHtml(truncateMetricLabel(label))}</code> ` +
112
- `${metrics.counters[key]!.toLocaleString()}`
113
- );
114
- }),
115
- hidden: keys.length - shown.length,
116
- });
117
- }
118
- }
119
-
120
- return sections;
121
- }
122
-
123
- function renderSection(section: Section): string {
124
- const lines = [section.title, ...section.rows];
125
- if (section.hidden > 0) {
126
- lines.push(` <i>…and ${section.hidden} more</i>`);
127
- }
128
- return lines.join("\n");
129
- }
130
-
131
- /**
132
- * Render the sections under `header`, shrinking to fit `maxLen`.
133
- *
134
- * Telegram hard-caps a message at 4096 characters. Rather than split the
135
- * report across messages — which is what the panel replaced — rows are
136
- * dropped from the longest section first (each drop bumping that
137
- * section's "…and N more") until the whole thing fits. Sections always
138
- * keep at least their first row, so every group stays visible.
139
- */
140
- function fitPanel(header: string, sections: Section[], maxLen: number): string {
141
- const render = () => [header, ...sections.map(renderSection)].join("\n\n");
142
-
143
- let out = render();
144
- while (out.length > maxLen) {
145
- let target: Section | undefined;
146
- for (const section of sections) {
147
- if (section.rows.length <= 1) continue;
148
- if (!target || section.rows.length > target.rows.length) target = section;
149
- }
150
- if (!target) break;
151
- target.rows.pop();
152
- target.hidden += 1;
153
- out = render();
154
- }
155
-
156
- if (out.length <= maxLen) return out;
157
- // Nothing left to shed (a pathologically small maxLen). Cut on a line
158
- // boundary so we never slice through an HTML tag and fail the parse.
159
- const cut = out.lastIndexOf("\n", maxLen - 1);
160
- return cut > 0 ? out.slice(0, cut) : out.slice(0, maxLen);
161
- }
162
-
163
- /**
164
- * Render the /metrics panel for one grain as a SINGLE Telegram message.
165
- * Pair it with `renderMetricsKeyboard(view)` — the Today / All time
166
- * buttons swap grains by editing this message in place.
167
- */
168
- export function renderMetricsPanel(
169
- metrics: MetricsSnapshot,
170
- view: MetricsView,
171
- maxLen = DEFAULT_METRICS_MESSAGE_MAX,
172
- ): string {
173
- const header = `<b>${escapeHtml(VIEW_TITLES[view])}</b>`;
174
- const sections = buildMetricsSections(metrics);
175
- if (sections.length === 0) {
176
- return `${header}\n\n<i>No metrics recorded yet.</i>`;
177
- }
178
- return fitPanel(header, sections, maxLen);
179
- }
180
-
181
- /** Grain-switch buttons for the /metrics panel. */
182
- export function renderMetricsKeyboard(
183
- view: MetricsView,
184
- ): Array<Array<SettingsButton>> {
185
- return [
186
- [
187
- {
188
- text: view === "today" ? "✓ Today" : "Today",
189
- callback_data: "metrics:today",
190
- },
191
- {
192
- text: view === "all" ? "✓ All time" : "All time",
193
- callback_data: "metrics:all",
194
- },
195
- ],
196
- ];
197
- }
198
-
199
- const DOCTOR_ICONS: Record<string, string> = {
200
- ok: "✅",
201
- warn: "⚠️",
202
- fail: "❌",
203
- info: "▫️",
204
- };
205
-
206
- /**
207
- * Render a DoctorReport as one Telegram HTML message. Same data as
208
- * `talon doctor` (src/core/doctor.ts) plus in-process runtime info —
209
- * when this renders, the bot is by definition running, so the CLI's
210
- * "is the bot up" probe becomes an uptime line instead.
211
- */
212
- /** Render the `/usage` report — one block per exposed backend. */
213
- export function renderUsageMessage(entries: BackendUsageEntry[]): string {
214
- const lines = ["<b>📊 Plan usage</b>"];
215
-
216
- for (const entry of entries) {
217
- const name = escapeHtml(entry.label || entry.id);
218
- if (!entry.plan) {
219
- lines.push("", `<b>${name}</b> — <i>${escapeHtml(entry.note ?? "")}</i>`);
220
- continue;
221
- }
222
- const age = entry.plan.ageLabel ? ` <i>(${entry.plan.ageLabel})</i>` : "";
223
- const plan = entry.plan.plan ? ` · ${escapeHtml(entry.plan.plan)}` : "";
224
- lines.push("", `<b>${name}</b>${plan}${age}`);
225
- if (entry.plan.resetsAvailable) {
226
- const n = entry.plan.resetsAvailable;
227
- lines.push(
228
- ` • You have <b>${n}</b> usage limit reset${n === 1 ? "" : "s"} available`,
229
- );
230
- }
231
- for (const w of entry.plan.windows) {
232
- const reset = w.resetLabel ? ` reset ${w.resetLabel}` : "";
233
- lines.push(
234
- ` <code>${escapeHtml(w.label.padEnd(6))}${w.bar} ${String(w.percent).padStart(3)}%</code>${reset}`,
235
- );
236
- }
237
- }
238
-
239
- return lines.join("\n");
240
- }
241
-
242
- export function renderDoctorMessage(report: DoctorReport): string {
243
- const lines = ["<b>🩺 Talon Doctor</b>", "", "<b>Environment</b>"];
244
-
245
- const render = (check: DoctorReport["checks"][number]): string => {
246
- const detail = check.detail ? ` (${escapeHtml(check.detail)})` : "";
247
- return `${DOCTOR_ICONS[check.status]} ${escapeHtml(check.label)}${detail}`;
248
- };
249
-
250
- for (const check of report.checks.filter((c) => !c.inactive)) {
251
- lines.push(render(check));
252
- }
253
-
254
- // Configured-but-idle backends get their own block: they describe what a
255
- // switch would run into, not the state of the running deployment.
256
- const idle = report.checks.filter((c) => c.inactive);
257
- if (idle.length > 0) {
258
- lines.push("", "<b>Other backends</b>", ...idle.map(render));
259
- }
260
-
261
- lines.push("", "<b>Native modules</b>");
262
- for (const mod of report.native) {
263
- const size =
264
- mod.sizeBytes !== undefined ? ` · ${formatBytes(mod.sizeBytes)}` : "";
265
- const note = mod.note ? ` (${escapeHtml(mod.note)})` : "";
266
- lines.push(
267
- `${mod.ok ? DOCTOR_ICONS.ok : DOCTOR_ICONS.fail} <code>${escapeHtml(mod.name)}</code> — ${escapeHtml(mod.language)} → ${escapeHtml(mod.target)}${size}${note}`,
268
- );
269
- }
270
-
271
- lines.push(
272
- "",
273
- "<b>Process</b>",
274
- `Uptime ${formatDuration(process.uptime() * 1000)} · PID ${process.pid} · Node ${escapeHtml(process.versions.node)}`,
275
- "",
276
- report.issues === 0
277
- ? `${DOCTOR_ICONS.ok} All checks passed.`
278
- : `${DOCTOR_ICONS.warn} ${report.issues} issue(s) found.`,
279
- );
280
-
281
- return lines.join("\n");
282
- }
283
-
284
- // ── /mesh ───────────────────────────────────────────────────────────────────
285
-
286
- function meshDeviceLine(r: MeshPingResult, now: number): string {
287
- const d = r.device;
288
- const bits: string[] = [escapeHtml(d.platform)];
289
- if (r.reachable && typeof r.latencyMs === "number") {
290
- bits.push(`${r.latencyMs} ms`);
291
- } else if (d.online && r.error) {
292
- bits.push(escapeHtml(r.error));
293
- } else if (!d.online) {
294
- bits.push(`last seen ${formatDuration(now - d.lastSeen)} ago`);
295
- }
296
- if (typeof d.battery === "number") {
297
- bits.push(`${d.battery}%${d.charging ? " charging" : ""}`);
298
- }
299
- return ` <b>${escapeHtml(d.name)}</b> — ${bits.join(" · ")}`;
300
- }
301
-
302
- /**
303
- * Render the `/mesh` fleet report as one HTML message.
304
- *
305
- * Devices group under a state heading — Responding, Unreachable, Offline
306
- * — rather than carrying a coloured status glyph per row: the grouping
307
- * already says what the glyph said, and the report stays readable when
308
- * the fleet grows. Empty groups are omitted entirely.
309
- */
310
- export function renderMeshReport(
311
- results: MeshPingResult[],
312
- now = Date.now(),
313
- bridge?: MeshReachability,
314
- ): string {
315
- if (results.length === 0) {
316
- // An empty fleet is the one case where "what do I point a device at?" is
317
- // the only useful thing to say, so the bridge line carries the answer
318
- // instead of leaving the operator to hunt for host and port.
319
- return [
320
- "<b>Mesh</b>",
321
- "",
322
- "<i>No devices have registered yet.</i>",
323
- ...bridgeLines(bridge),
324
- ].join("\n");
325
- }
326
-
327
- const responding = results
328
- .filter((r) => r.reachable)
329
- .sort((a, b) => (a.latencyMs ?? Infinity) - (b.latencyMs ?? Infinity));
330
- const unreachable = results
331
- .filter((r) => !r.reachable && r.device.online)
332
- .sort((a, b) => a.device.name.localeCompare(b.device.name));
333
- const offline = results
334
- .filter((r) => !r.reachable && !r.device.online)
335
- .sort((a, b) => b.device.lastSeen - a.device.lastSeen);
336
-
337
- const summary = [
338
- `${results.length} device${results.length === 1 ? "" : "s"}`,
339
- `${responding.length} responding`,
340
- ...(unreachable.length > 0 ? [`${unreachable.length} unreachable`] : []),
341
- ...(offline.length > 0 ? [`${offline.length} offline`] : []),
342
- ].join(" · ");
343
-
344
- const lines = ["<b>Mesh</b>", summary];
345
- const section = (title: string, entries: MeshPingResult[]): void => {
346
- if (entries.length === 0) return;
347
- lines.push(
348
- "",
349
- `<b>${title}</b>`,
350
- ...entries.map((r) => meshDeviceLine(r, now)),
351
- );
352
- };
353
- section("Responding", responding);
354
- section("Unreachable", unreachable);
355
- section("Offline", offline);
356
- lines.push(...bridgeLines(bridge));
357
-
358
- return lines.join("\n");
359
- }
360
-
361
- /** What `/mesh` says about the bridge a new device would dial. */
362
- export type MeshReachability =
363
- | {
364
- ok: true;
365
- url: string;
366
- authRequired: boolean;
367
- token?: string;
368
- fingerprint?: string;
369
- }
370
- | { ok: false; text: string };
371
-
372
- /**
373
- * The bridge footer.
374
- *
375
- * The bearer token and certificate appear when the caller passed them, which
376
- * `/mesh` does for the configured admin and not for anyone else: an operator
377
- * asking their own daemon how to reach itself should get the answer, while a
378
- * group member reading the fleet has no business holding the key to it.
379
- */
380
- function bridgeLines(bridge?: MeshReachability): string[] {
381
- if (!bridge) return [];
382
- if (!bridge.ok) return ["", `<b>Bridge</b> — ${escapeHtml(bridge.text)}`];
383
- const auth = bridge.authRequired ? "token required" : "no token";
384
- return [
385
- "",
386
- `<b>Bridge</b> <code>${escapeHtml(bridge.url)}</code> · ${auth}`,
387
- ...(bridge.token ? [`Token <code>${escapeHtml(bridge.token)}</code>`] : []),
388
- ...(bridge.fingerprint
389
- ? [`Certificate <code>${escapeHtml(bridge.fingerprint)}</code>`]
390
- : []),
391
- "Run <code>/mesh link</code> for a one-tap pairing link.",
392
- ];
393
- }
394
-
395
- /**
396
- * Render a minted pairing link, or why one couldn't be minted.
397
- *
398
- * The values are printed alongside the link because the link's whole payoff
399
- * — the phone configuring itself — depends on the companion already being
400
- * installed, and the fallback needs to be right there when it isn't.
401
- */
402
- export function renderMeshPairLink(
403
- minted:
404
- | {
405
- ok: true;
406
- link: string;
407
- url: string;
408
- token: string;
409
- fingerprint?: string;
410
- }
411
- | { ok: false; text: string },
412
- ): string {
413
- if (!minted.ok) {
414
- return `<b>Pairing</b>\n\n${escapeHtml(minted.text)}`;
415
- }
416
- return [
417
- "<b>Pair a device</b>",
418
- "",
419
- `Open on the phone: ${escapeHtml(minted.link)}`,
420
- "",
421
- "Single-use, expires in 10 minutes. If the companion isn't installed yet:",
422
- ` Bridge <code>${escapeHtml(minted.url)}</code>`,
423
- ` Token <code>${escapeHtml(minted.token)}</code>`,
424
- ...(minted.fingerprint
425
- ? [` Certificate <code>${escapeHtml(minted.fingerprint)}</code>`]
426
- : []),
427
- ].join("\n");
428
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * Model-option helpers shared across commands, callbacks, and the settings
3
- * panel. Generic formatters live in frontend/shared/format.ts and are
4
- * re-exported here so existing import sites keep working.
5
- */
6
-
7
- import type { ModelInfo } from "../../../core/models/catalog.js";
8
- import { getModels } from "../../../core/models/catalog.js";
9
-
10
- export {
11
- DEFAULT_PULSE_INTERVAL_MS,
12
- parseInterval,
13
- formatDuration,
14
- formatTokenCount,
15
- formatBytes,
16
- formatUsd,
17
- formatModelLabel,
18
- } from "../../shared/format.js";
19
-
20
- /** Display name for a known ModelInfo. */
21
- export function formatModelOptionLabel(model: ModelInfo): string {
22
- return model.displayName;
23
- }
24
-
25
- /** Compact display name for a known ModelInfo. */
26
- export function formatCompactModelLabel(model: ModelInfo): string {
27
- return model.displayName;
28
- }
29
-
30
- export function getTelegramModelOptions(): ModelInfo[] {
31
- const options: ModelInfo[] = [];
32
- const seenKeys = new Set<string>();
33
-
34
- for (const model of getModels()) {
35
- const key = model.displayName.toLowerCase();
36
- if (seenKeys.has(key)) continue;
37
- seenKeys.add(key);
38
- options.push(model);
39
- }
40
-
41
- return options;
42
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Shared helpers used by commands, callbacks, and the settings panel.
3
- *
4
- * - `format` — interval parsing, duration/token/bytes formatting,
5
- * model-label/option helpers
6
- * - `diagnostics` — metrics + doctor HTML rendering
7
- * - `menu` — settings/model/backend keyboards, effort rows, the
8
- * model-menu state builder + its types
9
- */
10
-
11
- export * from "./format.js";
12
- export * from "./diagnostics.js";
13
- export * from "./menu.js";
@@ -1,36 +0,0 @@
1
- /**
2
- * Centralized process exit handler registry.
3
- *
4
- * Each storage module calls registerCleanup() instead of
5
- * process.on("exit", fn) directly. This keeps exactly ONE
6
- * "exit" listener on the process regardless of how many modules
7
- * are loaded — avoiding MaxListenersExceededWarning.
8
- */
9
-
10
- const handlers: Array<() => void> = [];
11
- let registered = false;
12
-
13
- /**
14
- * Register a synchronous cleanup function to run on process exit.
15
- * Safe to call multiple times across multiple modules — only one
16
- * process "exit" listener is ever registered.
17
- */
18
- export function registerCleanup(fn: () => void): void {
19
- handlers.push(fn);
20
- if (!registered) {
21
- registered = true;
22
- process.on("exit", runAll);
23
- }
24
- }
25
-
26
- function runAll(): void {
27
- for (const fn of handlers) {
28
- try {
29
- fn();
30
- } catch (err) {
31
- process.stderr.write(
32
- `[cleanup] Handler error: ${err instanceof Error ? err.message : err}\n`,
33
- );
34
- }
35
- }
36
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes