@tractorscorch/clank 1.7.0 → 1.7.2

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/dist/index.js CHANGED
@@ -1175,6 +1175,74 @@ ${results}`
1175
1175
  getContextEngine() {
1176
1176
  return this.contextEngine;
1177
1177
  }
1178
+ /**
1179
+ * Compact: summarize current state, clear context, inject summary.
1180
+ * Returns the summary so callers can display it.
1181
+ */
1182
+ async compactSession() {
1183
+ const messages = this.contextEngine.getMessages();
1184
+ if (messages.length < 3) {
1185
+ return "Nothing to compact \u2014 session is too short.";
1186
+ }
1187
+ const conversationText = messages.slice(-30).map((m) => {
1188
+ const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1189
+ const truncated = content.length > 400 ? content.slice(0, 400) + "..." : content;
1190
+ return `${m.role}: ${truncated}`;
1191
+ }).join("\n\n");
1192
+ const summaryPrompt = [
1193
+ "You are summarizing a conversation for context continuity.",
1194
+ "The user is compacting their session \u2014 they want to clear context but continue seamlessly.",
1195
+ "",
1196
+ "Produce a concise state summary covering:",
1197
+ "- What task(s) the user is working on",
1198
+ "- Key decisions made so far",
1199
+ "- Files created, modified, or discussed",
1200
+ "- Current progress and what comes next",
1201
+ "- Any important context (preferences, constraints, blockers)",
1202
+ "",
1203
+ "Format as bullet points. Be brief but complete \u2014 this is the ONLY context the model will have when resuming.",
1204
+ "",
1205
+ "Conversation:",
1206
+ conversationText
1207
+ ].join("\n");
1208
+ let summary = "";
1209
+ if (this.resolvedProvider) {
1210
+ try {
1211
+ for await (const event of this.resolvedProvider.provider.stream(
1212
+ [{ role: "user", content: summaryPrompt }],
1213
+ "You are a conversation summarizer. Output only the summary.",
1214
+ []
1215
+ )) {
1216
+ if (event.type === "text") {
1217
+ summary += event.content;
1218
+ }
1219
+ }
1220
+ } catch {
1221
+ summary = messages.slice(-6).map((m) => {
1222
+ const content = typeof m.content === "string" ? m.content : "";
1223
+ return `- [${m.role}] ${content.slice(0, 150)}`;
1224
+ }).join("\n");
1225
+ }
1226
+ }
1227
+ if (!summary.trim()) {
1228
+ summary = "Session was active but summary generation failed. Ask the user for context.";
1229
+ }
1230
+ this.contextEngine.clear();
1231
+ this.contextEngine.ingest({
1232
+ role: "user",
1233
+ content: `[Session compacted \u2014 previous context summarized below]
1234
+
1235
+ ${summary.trim()}
1236
+
1237
+ ---
1238
+ The session was compacted by the user. Continue from where you left off. You have full context above.`,
1239
+ _compacted: true
1240
+ });
1241
+ if (this.currentSession) {
1242
+ await this.sessionStore.saveMessages(this.currentSession.id, this.contextEngine.getMessages());
1243
+ }
1244
+ return summary.trim();
1245
+ }
1178
1246
  /** Destroy the engine and clean up */
1179
1247
  destroy() {
1180
1248
  this.cancel();
@@ -1197,12 +1265,20 @@ import { platform, hostname } from "os";
1197
1265
  async function buildSystemPrompt(opts) {
1198
1266
  const parts = [];
1199
1267
  const compact = opts.compact ?? false;
1268
+ const isSubAgent = (opts.spawnDepth ?? 0) > 0;
1200
1269
  if (!compact) {
1201
1270
  const workspaceContent = await loadWorkspaceFiles(opts.workspaceDir);
1202
1271
  if (workspaceContent) {
1203
1272
  parts.push(workspaceContent);
1204
1273
  parts.push("---");
1205
1274
  }
1275
+ if (isSubAgent) {
1276
+ const runnerContent = await loadSingleFile(opts.workspaceDir, SUB_AGENT_FILE);
1277
+ if (runnerContent) {
1278
+ parts.push(runnerContent);
1279
+ parts.push("---");
1280
+ }
1281
+ }
1206
1282
  }
1207
1283
  if (compact) {
1208
1284
  parts.push(`Agent: ${opts.identity.name} | Model: ${opts.identity.model.primary} | Dir: ${opts.identity.workspace}`);
@@ -1269,6 +1345,18 @@ async function loadWorkspaceFiles(workspaceDir) {
1269
1345
  }
1270
1346
  return sections.length > 0 ? sections.join("\n\n---\n\n") : null;
1271
1347
  }
1348
+ async function loadSingleFile(workspaceDir, filename) {
1349
+ const filePath = join2(workspaceDir, filename);
1350
+ if (existsSync2(filePath)) {
1351
+ try {
1352
+ const content = await readFile2(filePath, "utf-8");
1353
+ return content.trim() || null;
1354
+ } catch {
1355
+ return null;
1356
+ }
1357
+ }
1358
+ return null;
1359
+ }
1272
1360
  async function loadProjectMemory(projectRoot) {
1273
1361
  const candidates = [".clank.md", ".clankbuild.md", ".llamabuild.md"];
1274
1362
  for (const filename of candidates) {
@@ -1287,7 +1375,7 @@ async function loadProjectMemory(projectRoot) {
1287
1375
  async function ensureWorkspaceFiles(workspaceDir, templateDir) {
1288
1376
  const { mkdir: mkdir7, copyFile } = await import("fs/promises");
1289
1377
  await mkdir7(workspaceDir, { recursive: true });
1290
- for (const filename of [...WORKSPACE_FILES, "BOOTSTRAP.md", "HEARTBEAT.md"]) {
1378
+ for (const filename of [...WORKSPACE_FILES, "BOOTSTRAP.md", "HEARTBEAT.md", "RUNNER.md"]) {
1291
1379
  const target = join2(workspaceDir, filename);
1292
1380
  const source = join2(templateDir, filename);
1293
1381
  if (!existsSync2(target) && existsSync2(source)) {
@@ -1295,7 +1383,7 @@ async function ensureWorkspaceFiles(workspaceDir, templateDir) {
1295
1383
  }
1296
1384
  }
1297
1385
  }
1298
- var WORKSPACE_FILES;
1386
+ var WORKSPACE_FILES, SUB_AGENT_FILE;
1299
1387
  var init_system_prompt = __esm({
1300
1388
  "src/engine/system-prompt.ts"() {
1301
1389
  "use strict";
@@ -1308,6 +1396,7 @@ var init_system_prompt = __esm({
1308
1396
  "TOOLS.md",
1309
1397
  "MEMORY.md"
1310
1398
  ];
1399
+ SUB_AGENT_FILE = "RUNNER.md";
1311
1400
  }
1312
1401
  });
1313
1402
 
@@ -5057,8 +5146,12 @@ async function runChat(opts) {
5057
5146
  console.error(red(`Error: ${message}${recoverable ? " (recoverable)" : ""}`));
5058
5147
  });
5059
5148
  console.log("");
5060
- console.log(bold("Clank") + dim(` v0.1.0 | ${resolved.modelId} | ${identity.toolTier} tier`));
5061
- console.log(dim("Type your message. Press Ctrl+C to exit.\n"));
5149
+ console.log(cyan(" ___ _ _ "));
5150
+ console.log(cyan(" / __|| | __ _ _ _ | |__"));
5151
+ console.log(cyan(" | (__ | |/ _` || ' \\| / /"));
5152
+ console.log(cyan(" \\___||_|\\__,_||_||_|_\\_\\"));
5153
+ console.log(dim(` v1.7.2 | ${resolved.modelId} | ${identity.toolTier} tier`));
5154
+ console.log(dim(" Type your message. Press Ctrl+C to exit.\n"));
5062
5155
  const rl = createInterface({
5063
5156
  input: process.stdin,
5064
5157
  output: process.stdout,
@@ -5123,7 +5216,7 @@ async function handleSlashCommand(input, engine, _rl) {
5123
5216
  console.log(dim(`Unknown command: /${cmd}. Type /help for available commands.`));
5124
5217
  }
5125
5218
  }
5126
- var dim, bold, green, yellow, red, cyan;
5219
+ var dim, green, yellow, red, cyan;
5127
5220
  var init_chat = __esm({
5128
5221
  "src/cli/chat.ts"() {
5129
5222
  "use strict";
@@ -5134,7 +5227,6 @@ var init_chat = __esm({
5134
5227
  init_config2();
5135
5228
  init_sessions();
5136
5229
  dim = (s) => `\x1B[2m${s}\x1B[0m`;
5137
- bold = (s) => `\x1B[1m${s}\x1B[0m`;
5138
5230
  green = (s) => `\x1B[32m${s}\x1B[0m`;
5139
5231
  yellow = (s) => `\x1B[33m${s}\x1B[0m`;
5140
5232
  red = (s) => `\x1B[31m${s}\x1B[0m`;
@@ -6085,6 +6177,67 @@ var init_base = __esm({
6085
6177
 
6086
6178
  // src/adapters/telegram.ts
6087
6179
  import { Bot } from "grammy";
6180
+ function toolEmoji(name) {
6181
+ const map = {
6182
+ read_file: "\u{1F4C4}",
6183
+ write_file: "\u270F\uFE0F",
6184
+ edit_file: "\u270F\uFE0F",
6185
+ list_directory: "\u{1F4C1}",
6186
+ search_files: "\u{1F50D}",
6187
+ glob_files: "\u{1F50D}",
6188
+ bash: "\u{1F4BB}",
6189
+ git: "\u{1F4E6}",
6190
+ web_search: "\u{1F310}",
6191
+ web_fetch: "\u{1F310}",
6192
+ spawn_task: "\u{1F680}",
6193
+ manage_agent: "\u{1F916}",
6194
+ manage_model: "\u{1F9E0}",
6195
+ manage_config: "\u2699\uFE0F",
6196
+ manage_session: "\u{1F4CB}",
6197
+ manage_cron: "\u23F0",
6198
+ tts: "\u{1F50A}",
6199
+ stt: "\u{1F3A4}"
6200
+ };
6201
+ return map[name] || "\u{1F527}";
6202
+ }
6203
+ function formatTool(name, done) {
6204
+ const emoji = toolEmoji(name);
6205
+ if (done === void 0) return `${emoji} ${name}`;
6206
+ return done ? `${emoji} ${name} \u2713` : `${emoji} ${name} \u2717`;
6207
+ }
6208
+ function buildStreamDisplay(response, thinking, tools, showThinking) {
6209
+ const parts = [];
6210
+ if (showThinking && thinking) {
6211
+ const truncated = thinking.length > 500 ? thinking.slice(-450) + "..." : thinking;
6212
+ parts.push(`\u{1F4AD} ${truncated}`);
6213
+ parts.push("");
6214
+ }
6215
+ if (tools.length > 0) {
6216
+ const toolLine = tools.map((t) => {
6217
+ if (t.done === void 0) return `${toolEmoji(t.name)} ${t.name}...`;
6218
+ return formatTool(t.name, t.done);
6219
+ }).join(" ");
6220
+ parts.push(toolLine);
6221
+ parts.push("");
6222
+ }
6223
+ parts.push(response);
6224
+ return parts.join("\n");
6225
+ }
6226
+ function buildFinalDisplay(response, thinking, tools, showThinking) {
6227
+ const parts = [];
6228
+ if (showThinking && thinking) {
6229
+ const truncated = thinking.length > 1e3 ? thinking.slice(0, 950) + "..." : thinking;
6230
+ parts.push(`\u{1F4AD} _${truncated}_`);
6231
+ parts.push("");
6232
+ }
6233
+ if (tools.length > 0) {
6234
+ const toolLine = tools.map((t) => formatTool(t.name, t.done ?? true)).join(" ");
6235
+ parts.push(toolLine);
6236
+ parts.push("");
6237
+ }
6238
+ parts.push(response);
6239
+ return parts.join("\n");
6240
+ }
6088
6241
  function splitMessage(text, maxLen) {
6089
6242
  if (text.length <= maxLen) return [text];
6090
6243
  const chunks = [];
@@ -6101,12 +6254,13 @@ function splitMessage(text, maxLen) {
6101
6254
  }
6102
6255
  return chunks;
6103
6256
  }
6104
- var TelegramAdapter;
6257
+ var thinkingEnabled, TelegramAdapter;
6105
6258
  var init_telegram = __esm({
6106
6259
  "src/adapters/telegram.ts"() {
6107
6260
  "use strict";
6108
6261
  init_esm_shims();
6109
6262
  init_base();
6263
+ thinkingEnabled = /* @__PURE__ */ new Map();
6110
6264
  TelegramAdapter = class extends ChannelAdapter {
6111
6265
  id = "telegram";
6112
6266
  name = "Telegram";
@@ -6114,6 +6268,7 @@ var init_telegram = __esm({
6114
6268
  config = null;
6115
6269
  bot = null;
6116
6270
  running = false;
6271
+ startedAt = 0;
6117
6272
  init(gateway2, config) {
6118
6273
  this.gateway = gateway2;
6119
6274
  this.config = config;
@@ -6124,9 +6279,26 @@ var init_telegram = __esm({
6124
6279
  console.log(" Telegram: disabled or no bot token configured");
6125
6280
  return;
6126
6281
  }
6282
+ this.startedAt = Date.now();
6127
6283
  try {
6128
6284
  this.bot = new Bot(telegramConfig.botToken);
6129
6285
  const bot = this.bot;
6286
+ await bot.api.setMyCommands([
6287
+ { command: "help", description: "Show available commands" },
6288
+ { command: "new", description: "Start a new session" },
6289
+ { command: "reset", description: "Clear current session" },
6290
+ { command: "compact", description: "Save state and clear context" },
6291
+ { command: "status", description: "Agent status and info" },
6292
+ { command: "agents", description: "List available agents" },
6293
+ { command: "tasks", description: "Show background tasks" },
6294
+ { command: "kill", description: "Kill a background task" },
6295
+ { command: "killall", description: "Kill all running tasks" },
6296
+ { command: "model", description: "Show current model" },
6297
+ { command: "sessions", description: "List recent sessions" },
6298
+ { command: "think", description: "Toggle thinking display" },
6299
+ { command: "version", description: "Show Clank version" }
6300
+ ]).catch(() => {
6301
+ });
6130
6302
  const startupTime = Math.floor(Date.now() / 1e3);
6131
6303
  const chatLocks = /* @__PURE__ */ new Map();
6132
6304
  bot.on("message:text", async (ctx) => {
@@ -6173,8 +6345,11 @@ var init_telegram = __esm({
6173
6345
  let streamMsgId = null;
6174
6346
  let sendingInitial = false;
6175
6347
  let accumulated = "";
6348
+ let thinkingText = "";
6176
6349
  let lastEditTime = 0;
6177
6350
  const EDIT_INTERVAL = 800;
6351
+ const showThinking = thinkingEnabled.get(chatId) ?? false;
6352
+ let toolIndicators = [];
6178
6353
  const response = await this.gateway.handleInboundMessageStreaming(
6179
6354
  {
6180
6355
  channel: "telegram",
@@ -6188,7 +6363,8 @@ var init_telegram = __esm({
6188
6363
  const now = Date.now();
6189
6364
  if (!streamMsgId && !sendingInitial && accumulated.length > 20) {
6190
6365
  sendingInitial = true;
6191
- bot.api.sendMessage(chatId, accumulated + " \u258D").then((sent) => {
6366
+ const display = buildStreamDisplay(accumulated, thinkingText, toolIndicators, showThinking);
6367
+ bot.api.sendMessage(chatId, display + " \u258D").then((sent) => {
6192
6368
  streamMsgId = sent.message_id;
6193
6369
  lastEditTime = now;
6194
6370
  }).catch(() => {
@@ -6197,19 +6373,32 @@ var init_telegram = __esm({
6197
6373
  }
6198
6374
  if (streamMsgId && now - lastEditTime > EDIT_INTERVAL) {
6199
6375
  lastEditTime = now;
6200
- const display = accumulated.length > 4e3 ? accumulated.slice(-3900) + " \u258D" : accumulated + " \u258D";
6201
- bot.api.editMessageText(chatId, streamMsgId, display).catch(() => {
6376
+ const display = buildStreamDisplay(accumulated, thinkingText, toolIndicators, showThinking);
6377
+ const truncated = display.length > 4e3 ? display.slice(-3900) + " \u258D" : display + " \u258D";
6378
+ bot.api.editMessageText(chatId, streamMsgId, truncated).catch(() => {
6202
6379
  });
6203
6380
  }
6204
6381
  },
6382
+ onThinking: (content) => {
6383
+ thinkingText += content;
6384
+ },
6205
6385
  onToolStart: (name) => {
6206
- if (!streamMsgId) {
6386
+ toolIndicators.push({ name });
6387
+ if (streamMsgId) {
6388
+ const display = buildStreamDisplay(accumulated, thinkingText, toolIndicators, showThinking);
6389
+ bot.api.editMessageText(chatId, streamMsgId, display + " \u258D").catch(() => {
6390
+ });
6391
+ } else {
6207
6392
  bot.api.sendChatAction(chatId, "typing").catch(() => {
6208
6393
  });
6209
6394
  }
6210
6395
  },
6396
+ onToolResult: (name, success) => {
6397
+ const tool = toolIndicators.find((t) => t.name === name && t.done === void 0);
6398
+ if (tool) tool.done = success;
6399
+ },
6211
6400
  onError: (message) => {
6212
- bot.api.sendMessage(chatId, `Error: ${message.slice(0, 200)}`).catch(() => {
6401
+ bot.api.sendMessage(chatId, `\u26A0\uFE0F ${message.slice(0, 200)}`).catch(() => {
6213
6402
  });
6214
6403
  }
6215
6404
  }
@@ -6229,23 +6418,25 @@ var init_telegram = __esm({
6229
6418
  });
6230
6419
  }
6231
6420
  if (streamMsgId && response) {
6232
- const finalText = response.length > 4e3 ? response.slice(0, 3950) + "\n... (truncated)" : response;
6421
+ const display = buildFinalDisplay(response, thinkingText, toolIndicators, showThinking);
6422
+ const finalText = display.length > 4e3 ? display.slice(0, 3950) + "\n... (truncated)" : display;
6233
6423
  await bot.api.editMessageText(chatId, streamMsgId, finalText).catch(() => {
6234
6424
  });
6235
6425
  } else if (response && !streamMsgId) {
6236
- const chunks = splitMessage(response, 4e3);
6426
+ const display = buildFinalDisplay(response, thinkingText, toolIndicators, showThinking);
6427
+ const chunks = splitMessage(display, 4e3);
6237
6428
  for (const chunk of chunks) {
6238
6429
  await ctx.api.sendMessage(chatId, chunk);
6239
6430
  }
6240
6431
  }
6241
- clearInterval(typingInterval2);
6242
6432
  console.log(` Telegram: response complete (${response?.length || 0} chars)`);
6243
6433
  } catch (err) {
6244
- clearInterval(typingInterval);
6245
6434
  const errMsg = err instanceof Error ? err.message : String(err);
6246
6435
  console.error(` Telegram: message handler error \u2014 ${errMsg}`);
6247
- await ctx.api.sendMessage(chatId, `Error: ${errMsg.slice(0, 200)}`).catch(() => {
6436
+ await ctx.api.sendMessage(chatId, `\u26A0\uFE0F Error: ${errMsg.slice(0, 200)}`).catch(() => {
6248
6437
  });
6438
+ } finally {
6439
+ clearInterval(typingInterval);
6249
6440
  }
6250
6441
  };
6251
6442
  const prev = chatLocks.get(chatId) || Promise.resolve();
@@ -6276,7 +6467,7 @@ var init_telegram = __esm({
6276
6467
  const fileUrl = `https://api.telegram.org/file/bot${telegramConfig.botToken}/${file.file_path}`;
6277
6468
  const res = await fetch(fileUrl);
6278
6469
  if (!res.ok) {
6279
- await ctx.api.sendMessage(chatId, "Error: could not download voice message");
6470
+ await ctx.api.sendMessage(chatId, "\u26A0\uFE0F Could not download voice message");
6280
6471
  return;
6281
6472
  }
6282
6473
  const audioBuffer = Buffer.from(await res.arrayBuffer());
@@ -6285,7 +6476,7 @@ var init_telegram = __esm({
6285
6476
  const config = await loadConfig2();
6286
6477
  const stt = new STTEngine2(config);
6287
6478
  if (!stt.isAvailable()) {
6288
- await ctx.api.sendMessage(chatId, "Voice messages require speech-to-text. Set up Whisper: /help");
6479
+ await ctx.api.sendMessage(chatId, "Voice messages require speech-to-text. Configure Whisper in settings.");
6289
6480
  return;
6290
6481
  }
6291
6482
  const transcription = await stt.transcribe(audioBuffer, "ogg");
@@ -6317,7 +6508,7 @@ var init_telegram = __esm({
6317
6508
  }
6318
6509
  } catch (err) {
6319
6510
  const errMsg = err instanceof Error ? err.message : String(err);
6320
- await ctx.api.sendMessage(chatId, `Error: ${errMsg.slice(0, 200)}`);
6511
+ await ctx.api.sendMessage(chatId, `\u26A0\uFE0F Error: ${errMsg.slice(0, 200)}`);
6321
6512
  }
6322
6513
  };
6323
6514
  const prev = chatLocks.get(chatId) || Promise.resolve();
@@ -6354,7 +6545,7 @@ Describe or analyze the image if you can, or acknowledge it.`
6354
6545
  for (const chunk of chunks) await ctx.api.sendMessage(chatId, chunk);
6355
6546
  }
6356
6547
  } catch (err) {
6357
- await ctx.api.sendMessage(chatId, `Error: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
6548
+ await ctx.api.sendMessage(chatId, `\u26A0\uFE0F Error: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
6358
6549
  }
6359
6550
  };
6360
6551
  const prev = chatLocks.get(chatId) || Promise.resolve();
@@ -6406,7 +6597,7 @@ You can read this file with the read_file tool.`
6406
6597
  for (const chunk of chunks) await ctx.api.sendMessage(chatId, chunk);
6407
6598
  }
6408
6599
  } catch (err) {
6409
- await ctx.api.sendMessage(chatId, `Error: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
6600
+ await ctx.api.sendMessage(chatId, `\u26A0\uFE0F Error: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
6410
6601
  }
6411
6602
  };
6412
6603
  const prev = chatLocks.get(chatId) || Promise.resolve();
@@ -6441,45 +6632,95 @@ You can read this file with the read_file tool.`
6441
6632
  case "help":
6442
6633
  case "start":
6443
6634
  return [
6444
- "*Clank Commands*",
6635
+ "\u{1F527} *Clank Commands*",
6445
6636
  "",
6446
- "/help \u2014 Show this help",
6447
- "/status \u2014 Agent and model info",
6448
- "/agents \u2014 List available agents",
6449
- "/agent <name> \u2014 Switch to a different agent",
6450
- "/sessions \u2014 List recent sessions",
6637
+ "\u{1F4AC} *Chat*",
6451
6638
  "/new \u2014 Start a new session",
6452
- "/reset \u2014 Clear current session",
6639
+ "/reset \u2014 Clear current session history",
6640
+ "/compact \u2014 Save state, clear context, continue",
6641
+ "",
6642
+ "\u{1F4CA} *Info*",
6643
+ "/status \u2014 Agent, model, and session info",
6644
+ "/agents \u2014 List available agents",
6453
6645
  "/model \u2014 Show current model",
6454
6646
  "/tasks \u2014 Show background tasks",
6455
- "/think \u2014 Toggle thinking display"
6647
+ "/kill <id> \u2014 Kill a background task",
6648
+ "/killall \u2014 Kill all running tasks",
6649
+ "/version \u2014 Show Clank version",
6650
+ "",
6651
+ "\u2699\uFE0F *Settings*",
6652
+ "/agent <name> \u2014 Switch to a different agent",
6653
+ "/think \u2014 Toggle thinking display",
6654
+ "",
6655
+ "_Send any message to chat with the agent._"
6456
6656
  ].join("\n");
6457
6657
  case "status": {
6458
6658
  const cfg = this.config;
6459
6659
  const model = cfg?.agents?.defaults?.model?.primary || "unknown";
6460
- const agents2 = cfg?.agents?.list?.length || 0;
6660
+ const agentCount = cfg?.agents?.list?.length || 0;
6661
+ const tasks = this.gateway?.getTaskRegistry()?.list() || [];
6662
+ const runningTasks = tasks.filter((t) => t.status === "running").length;
6663
+ const uptime = Math.round((Date.now() - this.startedAt) / 6e4);
6664
+ const thinking = thinkingEnabled.get(chatId) ? "on" : "off";
6461
6665
  return [
6462
- "*Status*",
6463
- `Model: \`${model}\``,
6464
- `Agents: ${agents2} configured`,
6465
- `Chat: ${isGroup ? "group" : "DM"} (${chatId})`
6666
+ "\u{1F4CA} *Status*",
6667
+ "",
6668
+ `*Model:* \`${model}\``,
6669
+ `*Agents:* ${agentCount || 1} configured`,
6670
+ `*Tasks:* ${runningTasks} running / ${tasks.length} total`,
6671
+ `*Thinking:* ${thinking}`,
6672
+ `*Chat:* ${isGroup ? "group" : "DM"} (\`${chatId}\`)`,
6673
+ `*Uptime:* ${uptime} min`
6466
6674
  ].join("\n");
6467
6675
  }
6468
6676
  case "agents": {
6469
6677
  const list = this.config?.agents?.list || [];
6470
- if (list.length === 0) return "No custom agents configured. Using default agent.";
6471
- return "*Agents:*\n" + list.map(
6472
- (a) => `\u2022 *${a.name || a.id}* \u2014 \`${a.model?.primary || "default"}\``
6473
- ).join("\n");
6474
- }
6475
- case "agent":
6476
- if (args[0]) {
6477
- return `Agent switching via Telegram coming soon. Use the config tool in chat: "switch to agent ${args[0]}"`;
6678
+ const defaultModel = this.config?.agents?.defaults?.model?.primary || "unknown";
6679
+ if (list.length === 0) {
6680
+ return `\u{1F4CB} *Agents*
6681
+
6682
+ \u2022 *default* \u2014 \`${defaultModel}\`
6683
+
6684
+ _No custom agents. Configure in config.json5._`;
6478
6685
  }
6479
- return "Usage: /agent <name>";
6686
+ const lines = list.map(
6687
+ (a) => `\u2022 *${a.name || a.id}* \u2014 \`${a.model?.primary || defaultModel}\``
6688
+ );
6689
+ return `\u{1F4CB} *Agents*
6690
+
6691
+ \u2022 *default* \u2014 \`${defaultModel}\`
6692
+ ${lines.join("\n")}
6693
+
6694
+ _Switch with /agent <name>_`;
6695
+ }
6696
+ case "agent": {
6697
+ if (!args[0]) return "Usage: /agent <name>\n\nSee /agents for available agents.";
6698
+ const targetId = args[0].toLowerCase();
6699
+ const list = this.config?.agents?.list || [];
6700
+ const found = list.find((a) => a.id.toLowerCase() === targetId || (a.name || "").toLowerCase() === targetId);
6701
+ if (!found && targetId !== "default") {
6702
+ return `Agent "${args[0]}" not found. See /agents for available agents.`;
6703
+ }
6704
+ if (this.gateway) {
6705
+ await this.gateway.resetSession({
6706
+ channel: "telegram",
6707
+ peerId: chatId,
6708
+ peerKind: isGroup ? "group" : "dm"
6709
+ });
6710
+ }
6711
+ const name = found ? found.name || found.id : "default";
6712
+ return `Switched to agent *${name}*. Session reset \u2014 send a message to begin.`;
6713
+ }
6480
6714
  case "sessions": {
6481
- if (!this.gateway) return "Gateway not connected";
6482
- return "Use /new to start a fresh session, or /reset to clear the current one.";
6715
+ if (!this.gateway) return "Gateway not connected.";
6716
+ return [
6717
+ "\u{1F4C1} *Sessions*",
6718
+ "",
6719
+ "/new \u2014 Start a fresh session",
6720
+ "/reset \u2014 Clear current session history",
6721
+ "",
6722
+ `Current: \`${isGroup ? "group" : "dm"}:telegram:${chatId}\``
6723
+ ].join("\n");
6483
6724
  }
6484
6725
  case "new":
6485
6726
  case "reset":
@@ -6490,23 +6731,94 @@ You can read this file with the read_file tool.`
6490
6731
  peerKind: isGroup ? "group" : "dm"
6491
6732
  });
6492
6733
  }
6493
- return command === "new" ? "New session started. Send a message to begin." : "Session reset. History cleared.";
6734
+ return command === "new" ? "\u2728 New session started. Send a message to begin." : "\u{1F5D1} Session cleared. History erased.";
6735
+ case "compact": {
6736
+ if (!this.gateway) return "Gateway not connected.";
6737
+ const summary = await this.gateway.compactSession({
6738
+ channel: "telegram",
6739
+ peerId: chatId,
6740
+ peerKind: isGroup ? "group" : "dm"
6741
+ });
6742
+ if (!summary) return "Nothing to compact \u2014 no active session.";
6743
+ const preview = summary.length > 300 ? summary.slice(0, 300) + "..." : summary;
6744
+ return `\u{1F4E6} *Session compacted*
6745
+
6746
+ Context cleared and state saved. The agent will continue where it left off.
6747
+
6748
+ _Summary:_
6749
+ ${preview}`;
6750
+ }
6494
6751
  case "model": {
6495
6752
  const model = this.config?.agents?.defaults?.model?.primary || "unknown";
6496
- return `Current model: \`${model}\``;
6753
+ const fallbacks = this.config?.agents?.defaults?.model?.fallbacks || [];
6754
+ const lines = [`\u{1F916} *Current Model*
6755
+
6756
+ Primary: \`${model}\``];
6757
+ if (fallbacks.length > 0) {
6758
+ lines.push(`Fallbacks: ${fallbacks.map((f) => `\`${f}\``).join(", ")}`);
6759
+ }
6760
+ return lines.join("\n");
6497
6761
  }
6498
6762
  case "tasks": {
6499
6763
  const tasks = this.gateway?.getTaskRegistry()?.list() || [];
6500
- if (tasks.length === 0) return "No background tasks.";
6501
- return "*Background Tasks:*\n" + tasks.map((t) => {
6764
+ if (tasks.length === 0) return "\u{1F4CB} No background tasks.";
6765
+ const lines = tasks.map((t) => {
6502
6766
  const elapsed = Math.round(((t.completedAt || Date.now()) - t.startedAt) / 1e3);
6767
+ const status = t.status === "running" ? "\u23F3" : t.status === "completed" ? "\u2705" : t.status === "failed" ? "\u274C" : "\u23F1";
6503
6768
  const depth = t.spawnDepth > 0 ? ` [depth ${t.spawnDepth}]` : "";
6504
6769
  const kids = t.children.length > 0 ? ` (${t.children.length} children)` : "";
6505
- return `\u2022 *${t.label.slice(0, 40)}* (${t.agentId})${depth}${kids} \u2014 ${t.status} (${elapsed}s)`;
6506
- }).join("\n");
6770
+ const shortId = t.id.slice(0, 8);
6771
+ return `${status} \`${shortId}\` *${t.label.slice(0, 35)}* (${t.agentId})${depth}${kids} \u2014 ${elapsed}s`;
6772
+ });
6773
+ return `\u{1F4CB} *Background Tasks*
6774
+
6775
+ ${lines.join("\n")}
6776
+
6777
+ _Kill with /kill <id> or /killall_`;
6778
+ }
6779
+ case "kill": {
6780
+ if (!this.gateway) return "Gateway not connected.";
6781
+ if (!args[0]) return "Usage: /kill <task-id>\n\nSee /tasks for task IDs.";
6782
+ const registry = this.gateway.getTaskRegistry();
6783
+ const shortId = args[0];
6784
+ const allTasks = registry.list();
6785
+ const match = allTasks.find((t) => t.id.startsWith(shortId) && t.status === "running");
6786
+ if (!match) return `No running task matching \`${shortId}\`. See /tasks.`;
6787
+ const subEngine = this.gateway.engines?.get(`task:${match.id}`);
6788
+ if (subEngine) {
6789
+ subEngine.cancel();
6790
+ subEngine.destroy();
6791
+ this.gateway.engines?.delete(`task:${match.id}`);
6792
+ }
6793
+ registry.cancel(match.id);
6794
+ const cascaded = registry.cascadeCancel(`task:${match.id}`);
6795
+ const cascade = cascaded > 0 ? ` + ${cascaded} child task(s)` : "";
6796
+ return `\u{1F5D1} Killed task \`${match.id.slice(0, 8)}\` \u2014 *${match.label.slice(0, 40)}*${cascade}`;
6797
+ }
6798
+ case "killall": {
6799
+ if (!this.gateway) return "Gateway not connected.";
6800
+ const registry = this.gateway.getTaskRegistry();
6801
+ const running = registry.list({ status: "running" });
6802
+ if (running.length === 0) return "No running tasks to kill.";
6803
+ for (const t of running) {
6804
+ const subEngine = this.gateway.engines?.get(`task:${t.id}`);
6805
+ if (subEngine) {
6806
+ subEngine.cancel();
6807
+ subEngine.destroy();
6808
+ this.gateway.engines?.delete(`task:${t.id}`);
6809
+ }
6810
+ registry.cancel(t.id);
6811
+ }
6812
+ return `\u{1F5D1} Killed *${running.length}* running task(s).`;
6813
+ }
6814
+ case "think": {
6815
+ const current = thinkingEnabled.get(chatId) ?? false;
6816
+ thinkingEnabled.set(chatId, !current);
6817
+ return !current ? "\u{1F4AD} Thinking display *on* \u2014 you'll see the model's reasoning above responses." : "\u{1F4AD} Thinking display *off* \u2014 only the final response will be shown.";
6818
+ }
6819
+ case "version": {
6820
+ return `\u{1F527} *Clank* v1.7.2`;
6507
6821
  }
6508
- case "think":
6509
- return "Thinking display toggled. (Note: thinking visibility is per-client in the TUI/Web UI)";
6510
6822
  default:
6511
6823
  return null;
6512
6824
  }
@@ -6960,6 +7272,17 @@ var init_server = __esm({
6960
7272
  this.engines.delete(sessionKey);
6961
7273
  }
6962
7274
  }
7275
+ /**
7276
+ * Compact a session — summarize state, clear context, inject summary.
7277
+ * Used by channel adapters (Telegram /compact command).
7278
+ * Returns the summary text, or null if no active session.
7279
+ */
7280
+ async compactSession(context) {
7281
+ const sessionKey = deriveSessionKey(context);
7282
+ const engine = this.engines.get(sessionKey);
7283
+ if (!engine) return null;
7284
+ return engine.compactSession();
7285
+ }
6963
7286
  /**
6964
7287
  * Handle an inbound message from any channel adapter.
6965
7288
  * This is the main entry point for all non-WebSocket messages.
@@ -7006,6 +7329,11 @@ var init_server = __esm({
7006
7329
  engine.on("token", fn);
7007
7330
  listeners.push(["token", fn]);
7008
7331
  }
7332
+ if (callbacks.onThinking) {
7333
+ const fn = (data) => callbacks.onThinking(data.content);
7334
+ engine.on("thinking", fn);
7335
+ listeners.push(["thinking", fn]);
7336
+ }
7009
7337
  if (callbacks.onToolStart) {
7010
7338
  const fn = (data) => callbacks.onToolStart(data.name);
7011
7339
  engine.on("tool-start", fn);
@@ -7079,7 +7407,7 @@ var init_server = __esm({
7079
7407
  res.writeHead(200, { "Content-Type": "application/json" });
7080
7408
  res.end(JSON.stringify({
7081
7409
  status: "ok",
7082
- version: "1.7.0",
7410
+ version: "1.7.2",
7083
7411
  uptime: process.uptime(),
7084
7412
  clients: this.clients.size,
7085
7413
  agents: this.engines.size
@@ -7191,7 +7519,7 @@ var init_server = __esm({
7191
7519
  const hello = {
7192
7520
  type: "hello",
7193
7521
  protocol: PROTOCOL_VERSION,
7194
- version: "1.7.0",
7522
+ version: "1.7.2",
7195
7523
  agents: this.config.agents.list.map((a) => ({
7196
7524
  id: a.id,
7197
7525
  name: a.name || a.id,
@@ -7248,6 +7576,17 @@ var init_server = __esm({
7248
7576
  this.sendResponse(client, frame.id, true);
7249
7577
  break;
7250
7578
  }
7579
+ case "session.compact": {
7580
+ const compactKey = frame.params?.sessionKey || client.sessionKey;
7581
+ const compactEngine = this.engines.get(compactKey);
7582
+ if (compactEngine) {
7583
+ const summary = await compactEngine.compactSession();
7584
+ this.sendResponse(client, frame.id, true, { summary });
7585
+ } else {
7586
+ this.sendResponse(client, frame.id, false, "No active session to compact");
7587
+ }
7588
+ break;
7589
+ }
7251
7590
  // === Agents ===
7252
7591
  case "agent.list":
7253
7592
  this.sendResponse(client, frame.id, true, this.config.agents.list.map((a) => ({
@@ -7481,6 +7820,7 @@ var init_server = __esm({
7481
7820
  toolTier: agentConfig?.toolTier || this.config.agents.defaults.toolTier || "auto",
7482
7821
  tools: agentConfig?.tools
7483
7822
  };
7823
+ const currentDepth = sessionKey.startsWith("task:") ? (this.taskRegistry.getBySessionKey(sessionKey)?.spawnDepth ?? 0) + 1 : 0;
7484
7824
  const compact = agentConfig?.compactPrompt ?? this.config.agents.defaults.compactPrompt ?? false;
7485
7825
  const thinking = agentConfig?.thinking ?? this.config.agents.defaults.thinking ?? "auto";
7486
7826
  const systemPrompt = await buildSystemPrompt({
@@ -7488,12 +7828,12 @@ var init_server = __esm({
7488
7828
  workspaceDir: identity.workspace,
7489
7829
  channel,
7490
7830
  compact,
7491
- thinking
7831
+ thinking,
7832
+ spawnDepth: currentDepth
7492
7833
  });
7493
7834
  const memoryBudget = resolved.isLocal ? 1500 : 4e3;
7494
7835
  const memoryBlock = await this.memoryManager.buildMemoryBlock("", identity.workspace, memoryBudget);
7495
7836
  const fullPrompt = memoryBlock ? systemPrompt + "\n\n---\n\n" + memoryBlock : systemPrompt;
7496
- const currentDepth = sessionKey.startsWith("task:") ? (this.taskRegistry.getBySessionKey(sessionKey)?.spawnDepth ?? 0) + 1 : 0;
7497
7837
  const maxSpawnDepth = this.config.agents.defaults.subagents?.maxSpawnDepth ?? 1;
7498
7838
  const maxConcurrent = this.config.agents.defaults.subagents?.maxConcurrent ?? 8;
7499
7839
  const canSpawn = currentDepth < maxSpawnDepth;
@@ -8080,7 +8420,7 @@ async function runSetup(opts) {
8080
8420
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
8081
8421
  try {
8082
8422
  console.log("");
8083
- console.log(bold2(" Welcome to Clank"));
8423
+ console.log(bold(" Welcome to Clank"));
8084
8424
  console.log("");
8085
8425
  console.log(" Clank is an AI agent that can read, write, and");
8086
8426
  console.log(" delete files, execute commands, and access the web.");
@@ -8098,7 +8438,7 @@ async function runSetup(opts) {
8098
8438
  console.log("");
8099
8439
  console.log(" How would you like to set up Clank?");
8100
8440
  console.log("");
8101
- console.log(" 1. " + bold2("Quick Start") + " (recommended)");
8441
+ console.log(" 1. " + bold("Quick Start") + " (recommended)");
8102
8442
  console.log(dim4(" Auto-detect local models, sensible defaults"));
8103
8443
  console.log(" 2. Advanced");
8104
8444
  console.log(dim4(" Full control over gateway, models, channels"));
@@ -8365,7 +8705,7 @@ async function runSetup(opts) {
8365
8705
  await saveConfig(config);
8366
8706
  console.log(green4("\n Config saved to " + getConfigDir() + "/config.json5"));
8367
8707
  console.log("");
8368
- console.log(bold2(" Clank is ready!"));
8708
+ console.log(bold(" Clank is ready!"));
8369
8709
  console.log("");
8370
8710
  console.log(" Start chatting:");
8371
8711
  console.log(dim4(" clank chat \u2014 CLI chat"));
@@ -8376,7 +8716,7 @@ async function runSetup(opts) {
8376
8716
  rl.close();
8377
8717
  }
8378
8718
  }
8379
- var __dirname2, dim4, bold2, green4, yellow2, cyan2;
8719
+ var __dirname2, dim4, bold, green4, yellow2, cyan2;
8380
8720
  var init_setup = __esm({
8381
8721
  "src/cli/setup.ts"() {
8382
8722
  "use strict";
@@ -8386,7 +8726,7 @@ var init_setup = __esm({
8386
8726
  init_daemon();
8387
8727
  __dirname2 = dirname4(fileURLToPath4(import.meta.url));
8388
8728
  dim4 = (s) => `\x1B[2m${s}\x1B[0m`;
8389
- bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
8729
+ bold = (s) => `\x1B[1m${s}\x1B[0m`;
8390
8730
  green4 = (s) => `\x1B[32m${s}\x1B[0m`;
8391
8731
  yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
8392
8732
  cyan2 = (s) => `\x1B[36m${s}\x1B[0m`;
@@ -8799,13 +9139,17 @@ async function runTui(opts) {
8799
9139
  return;
8800
9140
  }
8801
9141
  console.log("");
8802
- console.log(bold3(" Clank TUI") + dim8(` | connecting to ${wsUrl}...`));
9142
+ console.log(cyan5(" ___ _ _ "));
9143
+ console.log(cyan5(" / __|| | __ _ _ _ | |__"));
9144
+ console.log(cyan5(" | (__ | |/ _` || ' \\| / /"));
9145
+ console.log(cyan5(" \\___||_|\\__,_||_||_|_\\_\\"));
9146
+ console.log(dim8(` TUI | connecting to ${wsUrl}...`));
8803
9147
  const ws = new WebSocket2(wsUrl);
8804
9148
  state.ws = ws;
8805
9149
  ws.on("open", () => {
8806
9150
  ws.send(JSON.stringify({
8807
9151
  type: "connect",
8808
- params: { auth: { token }, mode: "tui", version: "1.7.0" }
9152
+ params: { auth: { token }, mode: "tui", version: "1.7.2" }
8809
9153
  }));
8810
9154
  });
8811
9155
  ws.on("message", (data) => {
@@ -8978,6 +9322,11 @@ function handleFrame(state, frame) {
8978
9322
  if (!res.ok && res.error) {
8979
9323
  console.log(red5(`
8980
9324
  Error: ${res.error}`));
9325
+ } else if (res.ok && res.data?.summary) {
9326
+ const summary = res.data.summary.slice(0, 500);
9327
+ console.log(green8(" Session compacted.") + dim8(" Context cleared, state saved."));
9328
+ console.log(dim8(` Summary:
9329
+ ${summary}`));
8981
9330
  }
8982
9331
  state.streaming = false;
8983
9332
  }
@@ -8994,6 +9343,7 @@ async function handleSlashCommand2(state, input, rl) {
8994
9343
  console.log(dim8(" /model [id] \u2014 Show current model"));
8995
9344
  console.log(dim8(" /think \u2014 Toggle thinking display"));
8996
9345
  console.log(dim8(" /tools \u2014 Toggle tool output"));
9346
+ console.log(dim8(" /compact \u2014 Save state, clear context, continue"));
8997
9347
  console.log(dim8(" /new \u2014 Start new session"));
8998
9348
  console.log(dim8(" /reset \u2014 Reset current session"));
8999
9349
  console.log(dim8(" /exit \u2014 Exit"));
@@ -9050,6 +9400,15 @@ async function handleSlashCommand2(state, input, rl) {
9050
9400
  state.showToolOutput = !state.showToolOutput;
9051
9401
  console.log(dim8(` Tool output: ${state.showToolOutput ? "on" : "off"}`));
9052
9402
  break;
9403
+ case "compact":
9404
+ console.log(dim8(" Compacting session..."));
9405
+ state.ws?.send(JSON.stringify({
9406
+ type: "req",
9407
+ id: ++state.reqId,
9408
+ method: "session.compact",
9409
+ params: { sessionKey: state.sessionKey }
9410
+ }));
9411
+ break;
9053
9412
  case "new":
9054
9413
  state.sessionKey = `tui:${state.agentId}:${Date.now()}`;
9055
9414
  console.log(green8(` New session: ${state.sessionKey}`));
@@ -9074,7 +9433,7 @@ async function handleSlashCommand2(state, input, rl) {
9074
9433
  function printStatusBar(state) {
9075
9434
  console.log(dim8(` ${state.agentName} | ${state.modelId} | ${state.sessionKey}`));
9076
9435
  }
9077
- var dim8, bold3, green8, red5, cyan5, italic;
9436
+ var dim8, green8, red5, cyan5, italic;
9078
9437
  var init_tui = __esm({
9079
9438
  "src/cli/tui.ts"() {
9080
9439
  "use strict";
@@ -9082,7 +9441,6 @@ var init_tui = __esm({
9082
9441
  init_config2();
9083
9442
  init_protocol();
9084
9443
  dim8 = (s) => `\x1B[2m${s}\x1B[0m`;
9085
- bold3 = (s) => `\x1B[1m${s}\x1B[0m`;
9086
9444
  green8 = (s) => `\x1B[32m${s}\x1B[0m`;
9087
9445
  red5 = (s) => `\x1B[31m${s}\x1B[0m`;
9088
9446
  cyan5 = (s) => `\x1B[36m${s}\x1B[0m`;
@@ -9158,7 +9516,7 @@ import { existsSync as existsSync12 } from "fs";
9158
9516
  async function runUninstall(opts) {
9159
9517
  const configDir = getConfigDir();
9160
9518
  console.log("");
9161
- console.log(bold4(" Uninstall Clank"));
9519
+ console.log(bold2(" Uninstall Clank"));
9162
9520
  console.log("");
9163
9521
  console.log(" This will permanently remove:");
9164
9522
  console.log(red7(` ${configDir}`));
@@ -9211,7 +9569,7 @@ async function runUninstall(opts) {
9211
9569
  console.log(green10(" Clank has been completely removed."));
9212
9570
  console.log("");
9213
9571
  }
9214
- var dim10, bold4, green10, red7, yellow4;
9572
+ var dim10, bold2, green10, red7, yellow4;
9215
9573
  var init_uninstall = __esm({
9216
9574
  "src/cli/uninstall.ts"() {
9217
9575
  "use strict";
@@ -9219,7 +9577,7 @@ var init_uninstall = __esm({
9219
9577
  init_config2();
9220
9578
  init_gateway_cmd();
9221
9579
  dim10 = (s) => `\x1B[2m${s}\x1B[0m`;
9222
- bold4 = (s) => `\x1B[1m${s}\x1B[0m`;
9580
+ bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
9223
9581
  green10 = (s) => `\x1B[32m${s}\x1B[0m`;
9224
9582
  red7 = (s) => `\x1B[31m${s}\x1B[0m`;
9225
9583
  yellow4 = (s) => `\x1B[33m${s}\x1B[0m`;
@@ -9234,7 +9592,7 @@ import { fileURLToPath as fileURLToPath5 } from "url";
9234
9592
  import { dirname as dirname5, join as join20 } from "path";
9235
9593
  var __filename3 = fileURLToPath5(import.meta.url);
9236
9594
  var __dirname3 = dirname5(__filename3);
9237
- var version = "1.7.0";
9595
+ var version = "1.7.2";
9238
9596
  try {
9239
9597
  const pkg = JSON.parse(readFileSync(join20(__dirname3, "..", "package.json"), "utf-8"));
9240
9598
  version = pkg.version;