talon-agent 4.3.2 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/package.json +1 -1
  2. package/src/app.ts +1 -1
  3. package/src/backend/claude-sdk/doctor.ts +1 -1
  4. package/src/backend/claude-sdk/state.ts +1 -1
  5. package/src/backend/codex/doctor.ts +1 -1
  6. package/src/backend/codex/init.ts +1 -1
  7. package/src/backend/codex/state.ts +1 -1
  8. package/src/backend/openai-agents/doctor.ts +1 -1
  9. package/src/backend/openai-agents/init.ts +1 -1
  10. package/src/backend/openai-agents/state.ts +1 -1
  11. package/src/backend/remote-server/factory.ts +1 -1
  12. package/src/backend/remote-server/mcp.ts +1 -1
  13. package/src/backend/remote-server/server-bindings.ts +1 -1
  14. package/src/backend/remote-server/state.ts +1 -1
  15. package/src/backend/shared/system-prompt.ts +1 -1
  16. package/src/bootstrap.ts +3 -3
  17. package/src/cli/chat.ts +1 -1
  18. package/src/cli/config.ts +2 -2
  19. package/src/cli/doctor.ts +1 -1
  20. package/src/cli/index.ts +8 -0
  21. package/src/cli/memory.ts +166 -0
  22. package/src/cli/setup.ts +1 -1
  23. package/src/core/agent-runtime/backend-registry.ts +2 -2
  24. package/src/core/agent-runtime/model-ref.ts +2 -2
  25. package/src/core/auth/expiry-monitor.ts +1 -1
  26. package/src/{util/config.ts → core/config/index.ts} +8 -8
  27. package/src/core/{doctor.ts → doctor/index.ts} +11 -11
  28. package/src/core/engine/backend-controller/legacy.ts +1 -1
  29. package/src/core/engine/backend-controller/pool.ts +1 -1
  30. package/src/core/engine/backend-controller/rebind.ts +1 -1
  31. package/src/core/engine/backend-controller/state.ts +1 -1
  32. package/src/core/engine/gateway-actions/plugins.ts +1 -1
  33. package/src/core/engine/model-audit.ts +1 -1
  34. package/src/core/{notify.ts → frontend-runtime/admin-notify.ts} +1 -1
  35. package/src/core/frontend-runtime/capabilities.ts +1 -1
  36. package/src/core/models/active-model.ts +1 -1
  37. package/src/core/plugin/builtins.ts +2 -2
  38. package/src/core/plugin/manage.ts +1 -1
  39. package/src/core/plugin/native-runtimes.ts +1 -1
  40. package/src/frontend/discord/admin.ts +1 -1
  41. package/src/frontend/discord/callbacks/components/index.ts +1 -1
  42. package/src/frontend/discord/callbacks/components/settings.ts +1 -1
  43. package/src/frontend/discord/callbacks/components/types.ts +1 -1
  44. package/src/frontend/discord/callbacks/modals.ts +1 -1
  45. package/src/frontend/discord/commands/admin.ts +2 -2
  46. package/src/frontend/discord/commands/definitions.ts +1 -1
  47. package/src/frontend/discord/commands/info.ts +1 -1
  48. package/src/frontend/discord/commands/router.ts +1 -1
  49. package/src/frontend/discord/commands/session.ts +1 -1
  50. package/src/frontend/discord/commands/settings.ts +1 -1
  51. package/src/frontend/discord/handlers/delivery.ts +1 -1
  52. package/src/frontend/discord/handlers/messages.ts +1 -1
  53. package/src/frontend/discord/handlers/queue.ts +1 -1
  54. package/src/frontend/discord/handlers/state.ts +1 -1
  55. package/src/frontend/discord/index.ts +1 -1
  56. package/src/frontend/discord/middleware.ts +1 -1
  57. package/src/frontend/discord/render.ts +1 -1
  58. package/src/frontend/discord/runtime.ts +1 -1
  59. package/src/frontend/native/extensions.ts +4 -1
  60. package/src/frontend/native/index.ts +1 -1
  61. package/src/frontend/native/runtime.ts +1 -1
  62. package/src/frontend/native/settings.ts +1 -1
  63. package/src/frontend/shared/model-commands.ts +1 -1
  64. package/src/frontend/shared/plan-usage-report.ts +1 -1
  65. package/src/frontend/shared/reasoning-levels.ts +1 -1
  66. package/src/frontend/shared/session-status.ts +1 -1
  67. package/src/frontend/teams/index.ts +1 -1
  68. package/src/frontend/teams/runtime.ts +1 -1
  69. package/src/frontend/telegram/actions/chat-info.ts +1 -1
  70. package/src/frontend/telegram/actions/coerce.ts +15 -0
  71. package/src/frontend/telegram/actions/index.ts +2 -2
  72. package/src/frontend/telegram/actions/media.ts +1 -1
  73. package/src/frontend/telegram/actions/messaging.ts +3 -4
  74. package/src/frontend/telegram/actions/moderation/chat.ts +63 -0
  75. package/src/frontend/telegram/actions/moderation/index.ts +74 -0
  76. package/src/frontend/telegram/actions/moderation/members.ts +77 -0
  77. package/src/frontend/telegram/actions/moderation/permissions.ts +69 -0
  78. package/src/frontend/telegram/actions/moderation/topics.ts +48 -0
  79. package/src/frontend/telegram/actions/moderation/types.ts +16 -0
  80. package/src/frontend/telegram/actions/rich-messages.ts +51 -0
  81. package/src/frontend/telegram/actions/{shared.ts → send.ts} +9 -59
  82. package/src/frontend/telegram/admin/background.ts +66 -0
  83. package/src/frontend/telegram/admin/chunked-reply.ts +20 -0
  84. package/src/frontend/telegram/admin/health.ts +81 -0
  85. package/src/frontend/telegram/admin/sessions.ts +105 -0
  86. package/src/frontend/telegram/admin.ts +57 -262
  87. package/src/frontend/telegram/callbacks/auth.ts +1 -1
  88. package/src/frontend/telegram/callbacks/effort.ts +1 -1
  89. package/src/frontend/telegram/callbacks/index.ts +3 -3
  90. package/src/frontend/telegram/callbacks/metrics.ts +1 -1
  91. package/src/frontend/telegram/callbacks/model/backend.ts +156 -0
  92. package/src/frontend/telegram/callbacks/model/control.ts +29 -0
  93. package/src/frontend/telegram/callbacks/model/nav.ts +54 -0
  94. package/src/frontend/telegram/callbacks/model/select.ts +100 -0
  95. package/src/frontend/telegram/callbacks/model/types.ts +39 -0
  96. package/src/frontend/telegram/callbacks/model/views.ts +116 -0
  97. package/src/frontend/telegram/callbacks/model.ts +61 -331
  98. package/src/frontend/telegram/callbacks/pulse.ts +1 -1
  99. package/src/frontend/telegram/callbacks/{shared.ts → query.ts} +3 -2
  100. package/src/frontend/telegram/callbacks/settings.ts +1 -1
  101. package/src/frontend/telegram/callbacks/whatsapp.ts +1 -1
  102. package/src/frontend/telegram/commands/admin.ts +108 -77
  103. package/src/frontend/telegram/commands/index.ts +1 -1
  104. package/src/frontend/telegram/commands/settings.ts +65 -43
  105. package/src/frontend/telegram/commands/state.ts +1 -1
  106. package/src/frontend/telegram/commands/whatsapp-pairing.ts +1 -1
  107. package/src/frontend/telegram/handlers/context.ts +1 -1
  108. package/src/frontend/telegram/handlers/delivery.ts +2 -2
  109. package/src/frontend/telegram/handlers/messages.ts +1 -1
  110. package/src/frontend/telegram/handlers/queue.ts +1 -1
  111. package/src/frontend/telegram/handlers/state.ts +1 -1
  112. package/src/frontend/telegram/helpers/diagnostics.ts +1 -1
  113. package/src/frontend/telegram/index.ts +1 -1
  114. package/src/frontend/telegram/middleware.ts +167 -202
  115. package/src/frontend/telegram/model-menu.ts +1 -1
  116. package/src/frontend/terminal/builtins/context.ts +184 -0
  117. package/src/frontend/terminal/builtins/help.ts +35 -0
  118. package/src/frontend/terminal/builtins/index.ts +23 -0
  119. package/src/frontend/terminal/builtins/model.ts +198 -0
  120. package/src/frontend/terminal/builtins/session.ts +101 -0
  121. package/src/frontend/terminal/builtins/status.ts +202 -0
  122. package/src/frontend/terminal/command-registry.ts +80 -0
  123. package/src/frontend/terminal/commands.ts +11 -674
  124. package/src/frontend/terminal/index.ts +1 -1
  125. package/src/frontend/terminal/input/history.ts +64 -0
  126. package/src/frontend/terminal/input/keys.ts +143 -0
  127. package/src/frontend/terminal/input/parts.ts +33 -0
  128. package/src/frontend/terminal/input/paste.ts +53 -0
  129. package/src/frontend/terminal/input/render.ts +42 -0
  130. package/src/frontend/terminal/input/state.ts +60 -0
  131. package/src/frontend/terminal/input.ts +27 -342
  132. package/src/frontend/whatsapp/connection.ts +1 -1
  133. package/src/frontend/whatsapp/index.ts +2 -2
  134. package/src/frontend/whatsapp/pairing-service.ts +1 -1
  135. package/src/frontend/whatsapp/runtime.ts +1 -1
  136. package/src/plugins/github/provision.ts +1 -1
  137. package/src/plugins/mempalace/provision.ts +1 -1
  138. package/src/plugins/playwright/provision.ts +1 -1
  139. package/src/storage/memory.ts +505 -0
  140. package/src/storage/repositories/memory-repo.ts +308 -0
  141. package/src/storage/sql/memory.sql +95 -0
  142. package/src/storage/sql/schema.sql +81 -0
  143. package/src/storage/sql/statements.generated.ts +146 -0
  144. package/src/frontend/telegram/actions/moderation.ts +0 -255
  145. /package/src/core/{doctor-types.ts → doctor/types.ts} +0 -0
  146. /package/src/core/{pairing-broker.ts → frontend-runtime/pairing-broker.ts} +0 -0
@@ -3,685 +3,22 @@
3
3
  *
4
4
  * Each command is a self-contained handler registered via `registerCommand()`.
5
5
  * New commands = one function call. Handlers are independently testable.
6
+ * The builtins live one module per group under `./builtins/`.
6
7
  */
7
8
 
8
- import pc from "picocolors";
9
- import type { TalonConfig } from "../../util/config.js";
10
- import type { Backend } from "../../core/agent-runtime/capabilities.js";
11
- import type { Renderer } from "./renderer.js";
12
- import { formatTimeAgo } from "./renderer.js";
13
- import { isTerminalChatId } from "../../util/chat-id.js";
14
- import {
15
- resolveModel as coreResolveModel,
16
- getModels,
17
- } from "../../core/models/catalog.js";
18
- import {
19
- buildCacheDisplay,
20
- buildContextDisplay,
21
- buildPlanDisplay,
22
- buildContextBreakdown,
23
- estimateContextTokens,
24
- apportionCells,
25
- type ContextBreakdown,
26
- type ContextSegmentKey,
27
- } from "../shared/status-context.js";
28
- import { getRecentHistory } from "../../storage/history.js";
29
- import {
30
- formatDuration,
31
- formatTokenCount,
32
- formatUsd,
33
- } from "../shared/format.js";
34
- import {
35
- getChatSettings,
36
- setChatModel,
37
- setChatEffort,
38
- } from "../../storage/chat-settings.js";
39
- import { resolveModelId as resolveModelName } from "../../core/models/catalog.js";
40
- import {
41
- getAllSessions,
42
- getSession,
43
- getSessionInfo,
44
- setSessionName,
45
- } from "../../storage/sessions.js";
46
- import { getLoadedPlugins } from "../../core/plugin/index.js";
47
- import { getPooledBackend } from "../../core/engine/backend-controller/index.js";
9
+ import { registerCommand } from "./command-registry.js";
10
+ import { BUILTIN_COMMANDS } from "./builtins/index.js";
48
11
 
49
- // ── Types ────────────────────────────────────────────────────────────────────
50
-
51
- export type CommandContext = {
52
- /** Current chat ID (getter — may change on /resume). */
53
- chatId: () => string;
54
- config: TalonConfig;
55
- renderer: Renderer;
56
- reprompt: () => void;
57
- initNewChat: (id?: string) => void;
58
- waitForInput: () => Promise<string>;
59
- /** Close the terminal (for /quit). */
60
- close: () => void;
61
- /** AI backend (available after bootstrap). */
62
- backend?: Backend | null;
63
- };
64
-
65
- type CommandHandler = (args: string, ctx: CommandContext) => Promise<void>;
66
-
67
- export type Command = {
68
- name: string;
69
- aliases?: string[];
70
- argHint?: string;
71
- description: string;
72
- handler: CommandHandler;
73
- };
74
-
75
- // ── Registry ─────────────────────────────────────────────────────────────────
76
-
77
- const commands: Command[] = [];
78
- const nameIndex = new Map<string, Command>();
79
-
80
- export function registerCommand(cmd: Command): void {
81
- commands.push(cmd);
82
- nameIndex.set(cmd.name, cmd);
83
- if (cmd.aliases) {
84
- for (const alias of cmd.aliases) {
85
- nameIndex.set(alias, cmd);
86
- }
87
- }
88
- }
89
-
90
- /** Try to run a slash command. Returns true if handled, false if not a command. */
91
- export async function tryRunCommand(
92
- text: string,
93
- ctx: CommandContext,
94
- ): Promise<boolean> {
95
- if (!text.startsWith("/")) return false;
96
-
97
- const spaceIdx = text.indexOf(" ");
98
- const cmdName = (spaceIdx === -1 ? text : text.slice(0, spaceIdx))
99
- .slice(1)
100
- .toLowerCase();
101
- const args = spaceIdx === -1 ? "" : text.slice(spaceIdx + 1).trim();
102
-
103
- const cmd = nameIndex.get(cmdName);
104
- if (!cmd) return false;
105
-
106
- await cmd.handler(args, ctx);
107
- return true;
108
- }
109
-
110
- /** Get all registered commands (for /help rendering). */
111
- export function getCommands(): readonly Command[] {
112
- return commands;
113
- }
114
-
115
- /** Clear all registered commands (for testing). */
116
- export function clearCommands(): void {
117
- commands.length = 0;
118
- nameIndex.clear();
119
- }
120
-
121
- // ── /context rendering ───────────────────────────────────────────────────────
122
-
123
- /** Each used segment gets its own colour; free is a dim hatch. */
124
- const CONTEXT_SEGMENT_COLOR: Record<ContextSegmentKey, (s: string) => string> =
125
- {
126
- system: pc.blue,
127
- tools: pc.yellow,
128
- conversation: pc.cyan,
129
- };
130
-
131
- const CONTEXT_BAR_WIDTH = 42;
132
-
133
- /**
134
- * The segmented bar: one coloured run per segment (proportional to the window),
135
- * then the free space as a dim `░` hatch. Cell counts come from
136
- * `apportionCells`, so the runs always sum to exactly the bar width.
137
- */
138
- function renderContextBar(bd: ContextBreakdown): string {
139
- const weights = bd.segments.map((s) => s.tokens);
140
- if (bd.windowKnown) weights.push(bd.free);
141
- const cells = apportionCells(weights, CONTEXT_BAR_WIDTH);
142
- let bar = "";
143
- bd.segments.forEach((s, i) => {
144
- bar += CONTEXT_SEGMENT_COLOR[s.key]("█".repeat(cells[i] ?? 0));
145
- });
146
- if (bd.windowKnown) {
147
- bar += pc.dim("░".repeat(cells[bd.segments.length] ?? 0));
148
- }
149
- return bar;
150
- }
151
-
152
- /** One aligned legend row per segment (and Free), colour-matched to the bar. */
153
- function renderContextLegend(bd: ContextBreakdown): string[] {
154
- const rows = bd.segments.map((s) => ({
155
- dot: CONTEXT_SEGMENT_COLOR[s.key]("●"),
156
- label: s.label,
157
- tokens: s.tokens,
158
- pct: s.pct,
159
- }));
160
- if (bd.windowKnown) {
161
- rows.push({
162
- dot: pc.dim("░"),
163
- label: "Free",
164
- tokens: bd.free,
165
- pct: bd.freePct,
166
- });
167
- }
168
- const labelW = Math.max(...rows.map((r) => r.label.length));
169
- const tokW = Math.max(...rows.map((r) => formatTokenCount(r.tokens).length));
170
- return rows.map((r) => {
171
- const label = r.label.padEnd(labelW);
172
- const tok = formatTokenCount(r.tokens).padStart(tokW);
173
- const pct = `${r.pct}%`.padStart(6);
174
- return `${r.dot} ${label} ${pc.dim(tok)} ${pc.dim(pct)}`;
175
- });
176
- }
12
+ export {
13
+ registerCommand,
14
+ tryRunCommand,
15
+ getCommands,
16
+ clearCommands,
17
+ type CommandContext,
18
+ } from "./command-registry.js";
177
19
 
178
20
  // ── Built-in commands ────────────────────────────────────────────────────────
179
21
 
180
22
  export function registerBuiltinCommands(): void {
181
- registerCommand({
182
- name: "model",
183
- argHint: "[name]",
184
- description: "Switch model",
185
- async handler(args, ctx) {
186
- const currentModel =
187
- getChatSettings(ctx.chatId()).model ?? ctx.config.model;
188
- const be = ctx.backend;
189
-
190
- const trimmedArgs = args.trim();
191
- const lowerArgs = trimmedArgs.toLowerCase();
192
-
193
- if (!trimmedArgs) {
194
- const modelInfo = await be?.models?.getRawModelInfo?.(currentModel);
195
- const displayName = modelInfo?.displayName ?? currentModel;
196
- const details = modelInfo
197
- ? [
198
- modelInfo.providerName,
199
- modelInfo.free ? "free" : undefined,
200
- modelInfo.selectable
201
- ? "ready"
202
- : (modelInfo.unavailableReason ?? "not connected"),
203
- ].filter(Boolean)
204
- : [];
205
- ctx.renderer.writeSystem(
206
- `Model: ${displayName}${details.length ? ` · ${details.join(" · ")}` : ""}`,
207
- );
208
- if (modelInfo?.contextWindow) {
209
- ctx.renderer.writeln(
210
- ` Context window: ${modelInfo.contextWindow.toLocaleString()}`,
211
- );
212
- }
213
- if (be?.models?.getProviders) {
214
- const providers = await be.models?.getProviders();
215
- const connected = providers.filter((p) => p.connected);
216
- if (connected.length > 0) {
217
- ctx.renderer.writeln(
218
- ` Providers: ${connected.map((p) => `${p.name} (${p.modelCount})`).join(", ")}`,
219
- );
220
- }
221
- }
222
- ctx.renderer.writeln(
223
- ` Use /model free, /model all, /model providers, or /model <name>.`,
224
- );
225
- ctx.reprompt();
226
- return;
227
- }
228
-
229
- if (lowerArgs === "reset" || lowerArgs === "default") {
230
- setChatModel(ctx.chatId(), undefined);
231
- ctx.renderer.writeSystem(`Model → ${ctx.config.model}`);
232
- ctx.reprompt();
233
- return;
234
- }
235
-
236
- if (lowerArgs === "free" || lowerArgs === "list" || lowerArgs === "all") {
237
- if (be?.models?.listModels) {
238
- const filter = lowerArgs === "free" ? "free" : "all";
239
- const { models, total } = await be.models.listModels(filter);
240
- const list = models.slice(0, 20);
241
- ctx.renderer.writeSystem(
242
- `${filter === "free" ? "Free" : "Available"} models (${total})`,
243
- );
244
- for (const model of list) {
245
- ctx.renderer.writeln(
246
- ` ${model.displayName} · ${model.providerName}${model.contextWindow ? ` · ${model.contextWindow.toLocaleString()} ctx` : ""}${model.free ? " · free" : ""}`,
247
- );
248
- }
249
- if (total > list.length) {
250
- ctx.renderer.writeln(` …and ${total - list.length} more`);
251
- }
252
- } else {
253
- const names = getModels()
254
- .map((m) => m.aliases[0] ?? m.id)
255
- .join(", ");
256
- ctx.renderer.writeSystem(`Available: ${names}`);
257
- }
258
- ctx.reprompt();
259
- return;
260
- }
261
-
262
- if (lowerArgs === "providers") {
263
- if (be?.models?.getProviders) {
264
- const providers = await be.models?.getProviders();
265
- ctx.renderer.writeSystem(`Providers (${providers.length})`);
266
- for (const p of providers.slice(0, 20)) {
267
- ctx.renderer.writeln(
268
- ` ${p.name} · ${p.connected ? "connected" : "not connected"} · ${p.modelCount} models`,
269
- );
270
- }
271
- } else {
272
- ctx.renderer.writeSystem("Provider listing not supported.");
273
- }
274
- ctx.reprompt();
275
- return;
276
- }
277
-
278
- // Resolve model query via backend
279
- if (be?.models?.resolveModelInfo) {
280
- const resolution = await be.models?.resolveModelInfo(trimmedArgs);
281
- if (resolution.kind === "missing") {
282
- const msg =
283
- be.models?.formatModelError?.(trimmedArgs, resolution) ??
284
- `No model matched "${trimmedArgs}".`;
285
- ctx.renderer.writeError(msg);
286
- ctx.reprompt();
287
- return;
288
- }
289
- if (resolution.kind === "ambiguous") {
290
- const preview = resolution.matches
291
- .map((m) => `${m.displayName} (${m.providerName})`)
292
- .join(", ");
293
- ctx.renderer.writeError(
294
- `Ambiguous: "${trimmedArgs}" matches ${preview}`,
295
- );
296
- ctx.reprompt();
297
- return;
298
- }
299
- if (!resolution.model.selectable) {
300
- ctx.renderer.writeError(
301
- resolution.model.unavailableReason ??
302
- `${resolution.model.providerName} is not connected.`,
303
- );
304
- ctx.reprompt();
305
- return;
306
- }
307
- setChatModel(ctx.chatId(), resolution.storedValue);
308
- ctx.renderer.writeSystem(
309
- `Model → ${resolution.model.displayName} (${resolution.model.providerName}${resolution.model.free ? " · free" : ""})`,
310
- );
311
- } else {
312
- setChatModel(ctx.chatId(), resolveModelName(trimmedArgs));
313
- ctx.renderer.writeSystem(`Model → ${resolveModelName(trimmedArgs)}`);
314
- }
315
- ctx.reprompt();
316
- },
317
- });
318
-
319
- registerCommand({
320
- name: "effort",
321
- argHint: "[lvl]",
322
- description: "Thinking effort (off/low/medium/high/max)",
323
- async handler(args, ctx) {
324
- if (!args) {
325
- ctx.renderer.writeSystem(
326
- `Effort: ${getChatSettings(ctx.chatId()).effort ?? "adaptive"}`,
327
- );
328
- } else {
329
- setChatEffort(
330
- ctx.chatId(),
331
- args === "adaptive"
332
- ? undefined
333
- : (args as "off" | "low" | "medium" | "high" | "max"),
334
- );
335
- ctx.renderer.writeSystem(`Effort → ${args}`);
336
- }
337
- ctx.reprompt();
338
- },
339
- });
340
-
341
- registerCommand({
342
- name: "status",
343
- description: "Session stats",
344
- async handler(_args, ctx) {
345
- const info = getSessionInfo(ctx.chatId());
346
- const u = info.usage;
347
- const be = ctx.backend;
348
- const activeModel =
349
- getChatSettings(ctx.chatId()).model ?? ctx.config.model;
350
- let displayInputTokens = u.totalInputTokens;
351
- let displayOutputTokens = u.totalOutputTokens;
352
- let displayCacheRead = u.totalCacheRead;
353
- let displayCacheWrite = u.totalCacheWrite;
354
- let backendModelLine = "";
355
- let contextWindow = u.contextWindow;
356
- ctx.renderer.writeln();
357
- const nameStr = info.sessionName ? `"${info.sessionName}" · ` : "";
358
-
359
- // Enrich from backend when available
360
- if (be?.usage?.getSessionSnapshot && info.sessionId) {
361
- const snap = await be.usage
362
- ?.getSessionSnapshot(info.sessionId)
363
- .catch(() => undefined);
364
- if (snap) {
365
- displayInputTokens = snap.inputTokens ?? displayInputTokens;
366
- displayOutputTokens = snap.outputTokens ?? displayOutputTokens;
367
- displayCacheRead = snap.cacheRead ?? displayCacheRead;
368
- displayCacheWrite = snap.cacheWrite ?? displayCacheWrite;
369
- }
370
- }
371
-
372
- const cache = buildCacheDisplay({
373
- cacheMetrics: be?.cacheMetrics,
374
- inputTokens: displayInputTokens,
375
- cacheRead: displayCacheRead,
376
- cacheWrite: displayCacheWrite,
377
- });
378
- if (be?.models?.getRawModelInfo) {
379
- const modelInfo = await be.models
380
- ?.getRawModelInfo(activeModel)
381
- .catch(() => undefined);
382
- const label = be.label ?? "Backend";
383
- if (modelInfo) {
384
- backendModelLine = ` ${pc.bold(label)} ${modelInfo.displayName} · ${modelInfo.providerName}${modelInfo.free ? " · free" : ""}`;
385
- if (modelInfo.contextWindow) {
386
- contextWindow ||= modelInfo.contextWindow;
387
- }
388
- }
389
- }
390
-
391
- const context = buildContextDisplay({
392
- contextTokens: u.contextTokens,
393
- lastPromptTokens: u.lastPromptTokens,
394
- contextWindow,
395
- });
396
- const contextUsed = context.known
397
- ? formatTokenCount(context.used)
398
- : "unknown";
399
- const contextMax = context.max
400
- ? formatTokenCount(context.max)
401
- : "unknown";
402
- const avgResponseMs =
403
- info.turns > 0 && u.totalResponseMs
404
- ? Math.round(u.totalResponseMs / info.turns)
405
- : 0;
406
- const fastestResponseMs =
407
- Number.isFinite(u.fastestResponseMs) && u.fastestResponseMs > 0
408
- ? u.fastestResponseMs
409
- : 0;
410
-
411
- ctx.renderer.writeln(
412
- ` ${pc.bold("Session")} ${nameStr}turns ${info.turns}${cache ? ` · ${cache.hitPct}% cache` : ""}`,
413
- );
414
- ctx.renderer.writeln(
415
- ` ${pc.dim(`in ${displayInputTokens.toLocaleString()} · out ${displayOutputTokens.toLocaleString()} tokens`)}`,
416
- );
417
- ctx.renderer.writeln();
418
- ctx.renderer.writeln(
419
- ` ${pc.bold("Context")} ${contextUsed} / ${contextMax} (${context.known ? `${context.pct}%` : "unknown"})${context.warn ? pc.yellow(" nearing limit") : ""}`,
420
- );
421
- ctx.renderer.writeln(
422
- ` ${context.warn ? pc.yellow(context.bar) : pc.dim(context.bar)}`,
423
- );
424
- ctx.renderer.writeln(
425
- ` ${pc.dim(`response last ${u.lastResponseMs ? formatDuration(u.lastResponseMs) : "—"} · avg ${avgResponseMs ? formatDuration(avgResponseMs) : "—"} · best ${fastestResponseMs ? formatDuration(fastestResponseMs) : "—"}`)}`,
426
- );
427
- if (u.estimatedCostUsd > 0) {
428
- ctx.renderer.writeln(
429
- ` ${pc.dim(`estimated session cost ${formatUsd(u.estimatedCostUsd)}`)}`,
430
- );
431
- }
432
-
433
- const planSource = be?.usage?.getPlanUsage
434
- ? be
435
- : getPooledBackend("claude");
436
- const plan = buildPlanDisplay(
437
- await planSource?.usage?.getPlanUsage?.().catch(() => undefined),
438
- );
439
- if (plan) {
440
- ctx.renderer.writeln();
441
- ctx.renderer.writeln(
442
- ` ${pc.bold("Plan")}${plan.plan ? ` ${plan.plan}` : ""}${plan.ageLabel ? pc.dim(` (${plan.ageLabel})`) : ""}`,
443
- );
444
- for (const w of plan.windows) {
445
- ctx.renderer.writeln(
446
- ` ${w.label.padEnd(6)}${pc.dim(w.bar)} ${String(w.percent).padStart(3)}%${w.resetLabel ? pc.dim(` reset ${w.resetLabel}`) : ""}`,
447
- );
448
- }
449
- }
450
- if (backendModelLine) {
451
- ctx.renderer.writeln();
452
- ctx.renderer.writeln(backendModelLine);
453
- }
454
-
455
- const plugins = getLoadedPlugins();
456
- if (plugins.length > 0) {
457
- ctx.renderer.writeln();
458
- ctx.renderer.writeln(` ${pc.bold("Plugins")}`);
459
- for (const p of plugins) {
460
- const ver = p.plugin.version ? pc.dim(` v${p.plugin.version}`) : "";
461
- const desc = p.plugin.description
462
- ? ` ${pc.dim(p.plugin.description)}`
463
- : "";
464
- const tools = p.plugin.mcpServerPath
465
- ? pc.green("mcp")
466
- : pc.dim("actions only");
467
- ctx.renderer.writeln(
468
- ` ${pc.green("●")} ${p.plugin.name}${ver} ${tools}${desc}`,
469
- );
470
- }
471
- }
472
- ctx.reprompt();
473
- },
474
- });
475
-
476
- registerCommand({
477
- name: "context",
478
- aliases: ["ctx"],
479
- description: "Context-window usage, broken down",
480
- async handler(_args, ctx) {
481
- const chatId = ctx.chatId();
482
- const u = getSessionInfo(chatId).usage;
483
- const be = ctx.backend;
484
- const activeModel = getChatSettings(chatId).model ?? ctx.config.model;
485
-
486
- // Window + a friendly model name, enriched from the backend like /status.
487
- let contextWindow = u.contextWindow;
488
- let modelName = resolveModelName(activeModel);
489
- if (be?.models?.getRawModelInfo) {
490
- const mi = await be.models
491
- .getRawModelInfo(activeModel)
492
- .catch(() => undefined);
493
- if (mi) {
494
- if (mi.contextWindow) contextWindow ||= mi.contextWindow;
495
- if (mi.displayName) modelName = mi.displayName;
496
- }
497
- }
498
-
499
- // System = the actual frozen prompt the model is running with. Measure
500
- // it directly rather than rebuilding, so the number matches what was
501
- // really sent (the prompt is frozen per session by design).
502
- const parts = ctx.config.systemPromptParts;
503
- const systemText = parts
504
- ? [parts.staticText, parts.dynamicText].filter(Boolean).join("\n")
505
- : (ctx.config.systemPrompt ?? "");
506
- const systemTokens = estimateContextTokens(systemText);
507
-
508
- // Conversation = stored history for this chat. An estimate: the model's
509
- // real in-window history may be smaller after compaction, which the
510
- // breakdown clamps against the authoritative fill.
511
- const history = getRecentHistory(chatId, 2000);
512
- const conversationTokens = estimateContextTokens(
513
- history.map((m) => m.text ?? "").join("\n"),
514
- );
515
-
516
- const bd = buildContextBreakdown({
517
- contextTokens: u.contextTokens,
518
- contextWindow,
519
- systemTokens,
520
- conversationTokens,
521
- });
522
-
523
- ctx.renderer.writeln();
524
- if (!bd.known) {
525
- ctx.renderer.writeln(
526
- ` ${pc.bold("Context")} ${pc.dim("no usage yet — send a message first")}`,
527
- );
528
- ctx.reprompt();
529
- return;
530
- }
531
-
532
- const windowStr = bd.windowKnown
533
- ? `${formatTokenCount(bd.max)} window`
534
- : pc.dim("window unknown");
535
- ctx.renderer.writeln(
536
- ` ${pc.bold("Context")} ${modelName} · ${windowStr}`,
537
- );
538
-
539
- const usedStr = bd.windowKnown
540
- ? `${formatTokenCount(bd.used)} / ${formatTokenCount(bd.max)} · ${bd.usedPct}% used`
541
- : `${formatTokenCount(bd.used)} used`;
542
- ctx.renderer.writeln(
543
- ` ${bd.warn ? pc.yellow(usedStr) : pc.dim(usedStr)}${bd.warn ? pc.yellow(" · nearing limit") : ""}`,
544
- );
545
- ctx.renderer.writeln();
546
- ctx.renderer.writeln(` ${renderContextBar(bd)}`);
547
- ctx.renderer.writeln();
548
- for (const line of renderContextLegend(bd)) {
549
- ctx.renderer.writeln(` ${line}`);
550
- }
551
- // Explain only what's on screen: Tools is derivable (and shown) only
552
- // when the backend reported a real fill.
553
- const hasTools = bd.segments.some((s) => s.key === "tools");
554
- ctx.renderer.writeln(
555
- ` ${pc.dim(
556
- hasTools
557
- ? "System measured; Conversation estimated; Tools is the remainder."
558
- : "System measured; Conversation estimated from stored history.",
559
- )}`,
560
- );
561
- ctx.reprompt();
562
- },
563
- });
564
-
565
- registerCommand({
566
- name: "reset",
567
- description: "Start a fresh session",
568
- async handler(_args, ctx) {
569
- ctx.initNewChat();
570
- ctx.renderer.writeSystem("Session cleared.");
571
- ctx.reprompt();
572
- },
573
- });
574
-
575
- registerCommand({
576
- name: "resume",
577
- description: "List & resume a past session",
578
- async handler(_args, ctx) {
579
- const sessions = getAllSessions()
580
- .filter(
581
- (s) =>
582
- isTerminalChatId(s.chatId) &&
583
- s.chatId !== ctx.chatId() &&
584
- s.info.turns > 0,
585
- )
586
- .sort((a, b) => b.info.lastActive - a.info.lastActive)
587
- .slice(0, 10);
588
-
589
- if (sessions.length === 0) {
590
- ctx.renderer.writeSystem("No previous sessions to resume.");
591
- ctx.reprompt();
592
- return;
593
- }
594
-
595
- ctx.renderer.writeln();
596
- ctx.renderer.writeln(` ${pc.bold("Past sessions")}`);
597
- for (let i = 0; i < sessions.length; i++) {
598
- const s = sessions[i]!;
599
- const name = s.info.sessionName
600
- ? `"${s.info.sessionName}"`
601
- : pc.dim("(unnamed)");
602
- const turns = `${s.info.turns} turn${s.info.turns !== 1 ? "s" : ""}`;
603
- const ago = formatTimeAgo(s.info.lastActive);
604
- const model = s.info.lastModel
605
- ? (coreResolveModel(s.info.lastModel)?.displayName ??
606
- s.info.lastModel)
607
- : "";
608
- ctx.renderer.writeln(
609
- ` ${pc.green(String(i + 1))}. ${name} ${pc.dim(`${turns} · ${ago}${model ? ` · ${model}` : ""}`)}`,
610
- );
611
- }
612
- ctx.renderer.writeln();
613
- ctx.renderer.writeln(
614
- ` ${pc.dim("Enter number to resume (Esc to cancel):")}`,
615
- );
616
-
617
- const input = await ctx.waitForInput();
618
- const num = parseInt(input, 10);
619
- if (num >= 1 && num <= sessions.length) {
620
- const selected = sessions[num - 1]!;
621
- ctx.initNewChat(selected.chatId);
622
- const name = selected.info.sessionName
623
- ? `"${selected.info.sessionName}"`
624
- : `(${selected.info.turns} turns)`;
625
- ctx.renderer.writeSystem(`Resumed: ${name}`);
626
- } else {
627
- ctx.renderer.writeSystem("Cancelled.");
628
- }
629
- ctx.reprompt();
630
- },
631
- });
632
-
633
- registerCommand({
634
- name: "rename",
635
- argHint: "[name]",
636
- description: "Name the current session",
637
- async handler(args, ctx) {
638
- // Ensure session exists in store (auto-creates if needed)
639
- getSession(ctx.chatId());
640
- if (!args) {
641
- const session = getSession(ctx.chatId());
642
- ctx.renderer.writeSystem(
643
- session.sessionName
644
- ? `Session name: "${session.sessionName}"`
645
- : "Session has no name.",
646
- );
647
- } else {
648
- setSessionName(ctx.chatId(), args);
649
- ctx.renderer.writeSystem(`Session renamed to "${args}"`);
650
- }
651
- ctx.reprompt();
652
- },
653
- });
654
-
655
- registerCommand({
656
- name: "help",
657
- description: "Show available commands",
658
- async handler(_args, ctx) {
659
- ctx.renderer.writeln();
660
- for (const cmd of getCommands()) {
661
- if (cmd.name === "help") continue; // show help last
662
- const nameStr = `/${cmd.name}`;
663
- const argStr = cmd.argHint ? ` ${cmd.argHint}` : "";
664
- const pad = " ".repeat(
665
- Math.max(1, 16 - nameStr.length - argStr.length),
666
- );
667
- ctx.renderer.writeln(
668
- ` ${pc.cyan(nameStr)}${pc.dim(argStr)}${pad}${pc.dim(cmd.description)}`,
669
- );
670
- }
671
- // Help itself at the end
672
- ctx.renderer.writeln(
673
- ` ${pc.cyan("/help")} ${pc.dim("Show available commands")}`,
674
- );
675
- ctx.reprompt();
676
- },
677
- });
678
-
679
- registerCommand({
680
- name: "quit",
681
- aliases: ["exit"],
682
- description: "Exit",
683
- async handler(_args, ctx) {
684
- ctx.close();
685
- },
686
- });
23
+ for (const cmd of BUILTIN_COMMANDS) registerCommand(cmd);
687
24
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import pc from "picocolors";
12
- import type { TalonConfig } from "../../util/config.js";
12
+ import type { TalonConfig } from "../../core/config/index.js";
13
13
  import type { ContextManager, ActionResult } from "../../core/types.js";
14
14
  import type { Gateway } from "../../core/engine/gateway.js";
15
15
  import { log } from "../../util/log.js";