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.
- package/package.json +1 -1
- package/src/app.ts +3 -3
- package/src/backend/claude-sdk/factory.ts +21 -41
- package/src/backend/claude-sdk/host/in-process.ts +147 -0
- package/src/backend/codex/mcp-config.ts +1 -1
- package/src/backend/openai-agents/mcp-pool.ts +1 -1
- package/src/backend/runtime/index.ts +2 -2
- package/src/backend/runtime/turn/delivery.ts +1 -1
- package/src/backend/runtime/turn/turn-phases.ts +1 -1
- package/src/bootstrap.ts +2 -2
- package/src/cli.ts +5 -2
- package/src/core/agent-runtime/README.md +15 -0
- package/src/core/agent-runtime/agent-host.ts +387 -0
- package/src/core/background/cron/scheduler.ts +1 -1
- package/src/core/background/triggers/command.ts +1 -1
- package/src/{util → core/config}/harden.ts +1 -1
- package/src/core/config/index.ts +1 -1
- package/src/core/daemon/pidfile.ts +1 -1
- package/src/core/daemon/resource-sampler.ts +2 -2
- package/src/{util → core/daemon}/respawn.ts +1 -1
- package/src/core/doctor/index.ts +1 -1
- package/src/core/engine/gateway-actions/fetch-url.ts +1 -1
- package/src/core/engine/gateway-actions/index.ts +1 -1
- package/src/core/engine/gateway-actions/memory.ts +2 -2
- package/src/core/engine/gateway-actions/native/exec-background.ts +129 -0
- package/src/core/engine/gateway-actions/native/exec-remote.ts +85 -0
- package/src/core/engine/gateway-actions/native/exec.ts +182 -0
- package/src/core/engine/gateway-actions/native/index.ts +46 -0
- package/src/core/engine/gateway-actions/native/params.ts +46 -0
- package/src/core/engine/gateway-actions/native/read.ts +182 -0
- package/src/core/engine/gateway-actions/native/results.ts +34 -0
- package/src/core/engine/gateway-actions/native/search.ts +212 -0
- package/src/core/engine/gateway-actions/native/shell.ts +39 -0
- package/src/core/engine/gateway-actions/native/teleport.ts +55 -0
- package/src/core/engine/gateway-actions/native/write.ts +170 -0
- package/src/core/frontend-runtime/builtins.ts +2 -2
- package/src/core/mcp-hub/index.ts +1 -1
- package/src/core/mcp-hub/talon-server.ts +1 -1
- package/src/core/memory/taps.ts +1 -1
- package/src/core/plugin/mcp.ts +1 -1
- package/src/core/scripts/lua.ts +1 -1
- package/src/core/tools/{chat.ts → chat/chat.ts} +1 -1
- package/src/core/tools/{cross-send.ts → chat/cross-send.ts} +1 -1
- package/src/core/tools/{history.ts → chat/history.ts} +2 -2
- package/src/core/tools/{media.ts → chat/media.ts} +1 -1
- package/src/core/tools/{members.ts → chat/members.ts} +2 -2
- package/src/core/tools/{messaging.ts → chat/messaging.ts} +2 -2
- package/src/core/tools/{moderation.ts → chat/moderation.ts} +2 -2
- package/src/core/tools/{stickers.ts → chat/stickers.ts} +2 -2
- package/src/core/tools/{whatsapp.ts → chat/whatsapp.ts} +1 -1
- package/src/core/tools/{memory.ts → content/memory.ts} +2 -2
- package/src/core/tools/{web.ts → content/web.ts} +1 -1
- package/src/core/tools/index.ts +20 -20
- package/src/core/tools/{admin.ts → ops/admin.ts} +1 -1
- package/src/core/tools/{bridge.ts → ops/bridge.ts} +2 -2
- package/src/core/tools/{goals.ts → ops/goals.ts} +2 -2
- package/src/core/tools/{mesh.ts → ops/mesh.ts} +1 -1
- package/src/core/tools/{models.ts → ops/models.ts} +1 -1
- package/src/core/tools/{native.ts → ops/native.ts} +1 -1
- package/src/core/tools/{scheduling.ts → ops/scheduling.ts} +1 -1
- package/src/core/tools/{scripts.ts → ops/scripts.ts} +1 -1
- package/src/core/tools/{skills.ts → ops/skills.ts} +1 -1
- package/src/core/tools/{triggers.ts → ops/triggers.ts} +1 -1
- package/src/{util → core/vfs}/workspace.ts +2 -2
- package/src/frontend/discord/callbacks/components/effort.ts +1 -1
- package/src/frontend/discord/callbacks/components/index.ts +1 -1
- package/src/frontend/discord/callbacks/components/settings.ts +1 -1
- package/src/frontend/discord/commands/admin.ts +1 -1
- package/src/frontend/discord/commands/info.ts +1 -1
- package/src/frontend/discord/commands/interaction.ts +1 -1
- package/src/frontend/discord/commands/session.ts +3 -3
- package/src/frontend/discord/commands/settings.ts +1 -1
- package/src/frontend/discord/handlers/messages.ts +1 -1
- package/src/frontend/discord/handlers/state.ts +1 -1
- package/src/frontend/discord/middleware.ts +1 -1
- package/src/frontend/discord/ready.ts +1 -1
- package/src/frontend/discord/render.ts +41 -259
- package/src/frontend/native/chats/chats.ts +1 -1
- package/src/frontend/native/surface/models.ts +1 -1
- package/src/frontend/native/turn/context.ts +1 -1
- package/src/frontend/native/turn/emit.ts +1 -1
- package/src/frontend/{shared → presentation}/format.ts +4 -3
- package/src/frontend/{shared → presentation}/reasoning-levels.ts +12 -0
- package/src/frontend/presentation/reports.ts +493 -0
- package/src/frontend/{shared → presentation}/session-status.ts +1 -1
- package/src/frontend/teams/commands.ts +2 -2
- package/src/frontend/teams/turn.ts +1 -1
- package/src/frontend/telegram/admin/health.ts +1 -1
- package/src/frontend/telegram/admin/sessions.ts +1 -1
- package/src/frontend/telegram/callbacks/effort.ts +2 -2
- package/src/frontend/telegram/callbacks/metrics.ts +1 -1
- package/src/frontend/telegram/callbacks/model/views.ts +1 -1
- package/src/frontend/telegram/callbacks/query.ts +1 -1
- package/src/frontend/telegram/callbacks/settings.ts +2 -2
- package/src/frontend/telegram/commands/admin.ts +4 -4
- package/src/frontend/telegram/commands/info.ts +2 -2
- package/src/frontend/telegram/commands/session.ts +4 -4
- package/src/frontend/telegram/commands/settings.ts +4 -2
- package/src/frontend/telegram/handlers/state.ts +1 -1
- package/src/frontend/telegram/model-menu.ts +1 -1
- package/src/frontend/telegram/render/html.ts +30 -0
- package/src/frontend/telegram/{helpers → render}/menu.ts +46 -19
- package/src/frontend/telegram/render/reports.ts +182 -0
- package/src/frontend/terminal/builtins/context.ts +2 -2
- package/src/frontend/terminal/builtins/session.ts +1 -1
- package/src/frontend/terminal/builtins/status.ts +2 -2
- package/src/frontend/terminal/index.ts +2 -2
- package/src/frontend/terminal/renderer.ts +1 -1
- package/src/frontend/whatsapp/commands.ts +5 -5
- package/src/frontend/whatsapp/registry.ts +1 -1
- package/src/index.ts +5 -2
- package/src/util/runtime.ts +1 -1
- package/src/core/engine/gateway-actions/native.ts +0 -1035
- package/src/frontend/telegram/helpers/diagnostics.ts +0 -428
- package/src/frontend/telegram/helpers/format.ts +0 -42
- package/src/frontend/telegram/helpers/index.ts +0 -13
- package/src/util/cleanup-registry.ts +0 -36
- /package/src/{util → core/daemon}/boot-timer.ts +0 -0
- /package/src/{util → core/frontend-runtime}/chat-id.ts +0 -0
- /package/src/{util/mcp-launcher.ts → core/mcp-hub/launcher.ts} +0 -0
- /package/src/{util → core/tools/content}/web-content.ts +0 -0
- /package/src/core/tools/{mcp-env.ts → ops/mcp-env.ts} +0 -0
- /package/src/{util → core/weaver}/session-name.ts +0 -0
- /package/src/frontend/{shared → presentation}/access.ts +0 -0
- /package/src/frontend/{shared → presentation}/model-commands.ts +0 -0
- /package/src/frontend/{shared → presentation}/plan-usage-report.ts +0 -0
- /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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|