@tractorscorch/clank 1.3.0 → 1.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.
package/dist/index.js CHANGED
@@ -297,6 +297,67 @@ ${summary.trim()}`,
297
297
  }
298
298
  });
299
299
 
300
+ // src/memory/auto-persist.ts
301
+ import { readFile, writeFile } from "fs/promises";
302
+ import { existsSync } from "fs";
303
+ import { join } from "path";
304
+ function shouldPersist(userMessage) {
305
+ return PERSIST_TRIGGERS.some((pattern) => pattern.test(userMessage));
306
+ }
307
+ function extractMemory(userMessage) {
308
+ const rememberMatch = userMessage.match(/remember\s+(?:that\s+)?(.+)/i);
309
+ if (rememberMatch) return rememberMatch[1].trim();
310
+ for (const pattern of PERSIST_TRIGGERS) {
311
+ if (pattern.test(userMessage)) {
312
+ const firstSentence = userMessage.split(/[.!?\n]/)[0]?.trim();
313
+ return firstSentence && firstSentence.length < 200 ? firstSentence : userMessage.slice(0, 200);
314
+ }
315
+ }
316
+ return null;
317
+ }
318
+ async function appendToMemory(workspaceDir, entry) {
319
+ const memoryPath = join(workspaceDir, "MEMORY.md");
320
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
321
+ const newEntry = `
322
+ - [${timestamp}] ${entry}`;
323
+ if (existsSync(memoryPath)) {
324
+ const existing = await readFile(memoryPath, "utf-8");
325
+ if (existing.includes(entry)) return;
326
+ const lines = existing.split("\n");
327
+ if (lines.length > 200) return;
328
+ await writeFile(memoryPath, existing.trimEnd() + newEntry + "\n", "utf-8");
329
+ } else {
330
+ await writeFile(
331
+ memoryPath,
332
+ `# MEMORY.md \u2014 Persistent Memory
333
+
334
+ Things learned across sessions:
335
+ ${newEntry}
336
+ `,
337
+ "utf-8"
338
+ );
339
+ }
340
+ }
341
+ var PERSIST_TRIGGERS;
342
+ var init_auto_persist = __esm({
343
+ "src/memory/auto-persist.ts"() {
344
+ "use strict";
345
+ init_esm_shims();
346
+ PERSIST_TRIGGERS = [
347
+ /remember\s+(that|this|:)/i,
348
+ /don'?t\s+forget/i,
349
+ /always\s+(use|do|make|keep)/i,
350
+ /never\s+(use|do|make)/i,
351
+ /my\s+(name|email|timezone|preference)/i,
352
+ /i\s+(prefer|like|want|need|use)\s/i,
353
+ /from now on/i,
354
+ /going forward/i,
355
+ /important:\s/i,
356
+ /note:\s/i
357
+ ];
358
+ }
359
+ });
360
+
300
361
  // src/providers/types.ts
301
362
  var BaseProvider;
302
363
  var init_types = __esm({
@@ -685,6 +746,7 @@ var init_agent = __esm({
685
746
  "use strict";
686
747
  init_esm_shims();
687
748
  init_context_engine();
749
+ init_auto_persist();
688
750
  init_ollama();
689
751
  init_prompt_fallback();
690
752
  MAX_ITERATIONS = 50;
@@ -731,6 +793,9 @@ var init_agent = __esm({
731
793
  const ctxSize = await this.resolvedProvider.provider.detectContextWindow();
732
794
  this.contextEngine.setContextWindow(ctxSize);
733
795
  }
796
+ if (this.contextEngine.needsCompaction()) {
797
+ await this.contextEngine.compactSmart();
798
+ }
734
799
  }
735
800
  /** Cancel the current request */
736
801
  cancel() {
@@ -748,6 +813,13 @@ var init_agent = __esm({
748
813
  this.abortController = new AbortController();
749
814
  const signal = this.abortController.signal;
750
815
  this.contextEngine.ingest({ role: "user", content: text });
816
+ if (shouldPersist(text)) {
817
+ const memory = extractMemory(text);
818
+ if (memory) {
819
+ appendToMemory(this.identity.workspace, memory).catch(() => {
820
+ });
821
+ }
822
+ }
751
823
  if (this.currentSession && !this.currentSession.label) {
752
824
  const label = text.length > 60 ? text.slice(0, 57) + "..." : text;
753
825
  await this.sessionStore.setLabel(this.currentSession.normalizedKey, label);
@@ -788,36 +860,58 @@ var init_agent = __esm({
788
860
  const toolCalls = [];
789
861
  let promptTokens = 0;
790
862
  let outputTokens = 0;
863
+ let streamSuccess = false;
791
864
  this.emit("response-start");
792
- for await (const event of activeProvider.stream(
793
- this.contextEngine.getMessages(),
794
- this.systemPrompt,
795
- toolDefs,
796
- signal
797
- )) {
798
- switch (event.type) {
799
- case "text":
800
- iterationText += event.content;
801
- this.emit("token", { content: event.content });
802
- break;
803
- case "thinking":
804
- this.emit("thinking-start");
805
- break;
806
- case "tool_call":
807
- toolCalls.push({
808
- id: event.id,
809
- name: event.name,
810
- arguments: event.arguments
865
+ for (let attempt = 0; attempt < 2; attempt++) {
866
+ try {
867
+ const streamIterator = activeProvider.stream(
868
+ this.contextEngine.getMessages(),
869
+ this.systemPrompt,
870
+ toolDefs,
871
+ signal
872
+ );
873
+ for await (const event of streamIterator) {
874
+ switch (event.type) {
875
+ case "text":
876
+ iterationText += event.content;
877
+ this.emit("token", { content: event.content });
878
+ break;
879
+ case "thinking":
880
+ this.emit("thinking-start");
881
+ break;
882
+ case "tool_call":
883
+ toolCalls.push({
884
+ id: event.id,
885
+ name: event.name,
886
+ arguments: event.arguments
887
+ });
888
+ break;
889
+ case "usage":
890
+ promptTokens = event.promptTokens;
891
+ outputTokens = event.outputTokens;
892
+ break;
893
+ case "done":
894
+ break;
895
+ }
896
+ }
897
+ streamSuccess = true;
898
+ break;
899
+ } catch (streamErr) {
900
+ if (attempt === 0 && !signal.aborted) {
901
+ this.emit("error", {
902
+ message: `Model connection failed, retrying... (${streamErr instanceof Error ? streamErr.message : "unknown"})`,
903
+ recoverable: true
811
904
  });
812
- break;
813
- case "usage":
814
- promptTokens = event.promptTokens;
815
- outputTokens = event.outputTokens;
816
- break;
817
- case "done":
818
- break;
905
+ await new Promise((r) => setTimeout(r, 2e3));
906
+ continue;
907
+ }
908
+ throw streamErr;
819
909
  }
820
910
  }
911
+ if (!streamSuccess) {
912
+ this.emit("error", { message: "Model failed to respond after retry", recoverable: false });
913
+ break;
914
+ }
821
915
  this.emit("usage", {
822
916
  promptTokens,
823
917
  outputTokens,
@@ -956,30 +1050,47 @@ __export(system_prompt_exports, {
956
1050
  buildSystemPrompt: () => buildSystemPrompt,
957
1051
  ensureWorkspaceFiles: () => ensureWorkspaceFiles
958
1052
  });
959
- import { readFile } from "fs/promises";
960
- import { existsSync } from "fs";
961
- import { join } from "path";
1053
+ import { readFile as readFile2 } from "fs/promises";
1054
+ import { existsSync as existsSync2 } from "fs";
1055
+ import { join as join2 } from "path";
962
1056
  import { platform, hostname } from "os";
963
1057
  async function buildSystemPrompt(opts) {
964
1058
  const parts = [];
965
- const workspaceContent = await loadWorkspaceFiles(opts.workspaceDir);
966
- if (workspaceContent) {
967
- parts.push(workspaceContent);
968
- parts.push("---");
969
- }
970
- parts.push("## Runtime");
971
- parts.push(`Agent: ${opts.identity.name} (${opts.identity.id})`);
972
- parts.push(`Model: ${opts.identity.model.primary}`);
973
- parts.push(`Workspace: ${opts.identity.workspace}`);
974
- parts.push(`Platform: ${platform()} (${hostname()})`);
975
- parts.push(`Channel: ${opts.channel || "cli"}`);
976
- parts.push(`Tool tier: ${opts.identity.toolTier}`);
1059
+ const compact = opts.compact ?? false;
1060
+ if (!compact) {
1061
+ const workspaceContent = await loadWorkspaceFiles(opts.workspaceDir);
1062
+ if (workspaceContent) {
1063
+ parts.push(workspaceContent);
1064
+ parts.push("---");
1065
+ }
1066
+ }
1067
+ if (compact) {
1068
+ parts.push(`Agent: ${opts.identity.name} | Model: ${opts.identity.model.primary} | Dir: ${opts.identity.workspace}`);
1069
+ } else {
1070
+ parts.push("## Runtime");
1071
+ parts.push(`Agent: ${opts.identity.name} (${opts.identity.id})`);
1072
+ parts.push(`Model: ${opts.identity.model.primary}`);
1073
+ parts.push(`Workspace: ${opts.identity.workspace}`);
1074
+ parts.push(`Platform: ${platform()} (${hostname()})`);
1075
+ parts.push(`Channel: ${opts.channel || "cli"}`);
1076
+ parts.push(`Tool tier: ${opts.identity.toolTier}`);
1077
+ }
977
1078
  parts.push("");
978
- parts.push("## Instructions");
979
- parts.push("You are a helpful AI assistant with access to tools for reading/writing files, running commands, and more.");
980
- parts.push("Be concise and direct. Use tools proactively to accomplish tasks.");
981
- parts.push("When you need to make changes, read the relevant files first to understand the context.");
982
- parts.push("You can configure yourself \u2014 use the config, channel, agent, and model management tools to modify your own setup.");
1079
+ if (compact) {
1080
+ parts.push("You are a helpful AI assistant with tools. Be concise. Use tools proactively. Read files before editing.");
1081
+ } else {
1082
+ parts.push("## Instructions");
1083
+ parts.push("You are a helpful AI assistant with access to tools for reading/writing files, running commands, and more.");
1084
+ parts.push("Be concise and direct. Use tools proactively to accomplish tasks.");
1085
+ parts.push("When you need to make changes, read the relevant files first to understand the context.");
1086
+ parts.push("You can configure yourself \u2014 use the config, channel, agent, and model management tools to modify your own setup.");
1087
+ }
1088
+ if (opts.thinking === "off") {
1089
+ parts.push("");
1090
+ parts.push("Do NOT use extended thinking or reasoning blocks. Respond directly and concisely.");
1091
+ }
1092
+ parts.push("");
1093
+ parts.push("When you learn something important about the user or project, save it using the config or memory tools so you remember it next time.");
983
1094
  parts.push("");
984
1095
  const projectMemory = await loadProjectMemory(opts.identity.workspace);
985
1096
  if (projectMemory) {
@@ -992,10 +1103,10 @@ async function buildSystemPrompt(opts) {
992
1103
  async function loadWorkspaceFiles(workspaceDir) {
993
1104
  const sections = [];
994
1105
  for (const filename of WORKSPACE_FILES) {
995
- const filePath = join(workspaceDir, filename);
996
- if (existsSync(filePath)) {
1106
+ const filePath = join2(workspaceDir, filename);
1107
+ if (existsSync2(filePath)) {
997
1108
  try {
998
- const content = await readFile(filePath, "utf-8");
1109
+ const content = await readFile2(filePath, "utf-8");
999
1110
  if (content.trim()) {
1000
1111
  sections.push(content.trim());
1001
1112
  }
@@ -1008,10 +1119,10 @@ async function loadWorkspaceFiles(workspaceDir) {
1008
1119
  async function loadProjectMemory(projectRoot) {
1009
1120
  const candidates = [".clank.md", ".clankbuild.md", ".llamabuild.md"];
1010
1121
  for (const filename of candidates) {
1011
- const filePath = join(projectRoot, filename);
1012
- if (existsSync(filePath)) {
1122
+ const filePath = join2(projectRoot, filename);
1123
+ if (existsSync2(filePath)) {
1013
1124
  try {
1014
- const content = await readFile(filePath, "utf-8");
1125
+ const content = await readFile2(filePath, "utf-8");
1015
1126
  return content.trim() || null;
1016
1127
  } catch {
1017
1128
  continue;
@@ -1024,9 +1135,9 @@ async function ensureWorkspaceFiles(workspaceDir, templateDir) {
1024
1135
  const { mkdir: mkdir7, copyFile } = await import("fs/promises");
1025
1136
  await mkdir7(workspaceDir, { recursive: true });
1026
1137
  for (const filename of [...WORKSPACE_FILES, "BOOTSTRAP.md", "HEARTBEAT.md"]) {
1027
- const target = join(workspaceDir, filename);
1028
- const source = join(templateDir, filename);
1029
- if (!existsSync(target) && existsSync(source)) {
1138
+ const target = join2(workspaceDir, filename);
1139
+ const source = join2(templateDir, filename);
1140
+ if (!existsSync2(target) && existsSync2(source)) {
1030
1141
  await copyFile(source, target);
1031
1142
  }
1032
1143
  }
@@ -1186,7 +1297,7 @@ var init_path_guard = __esm({
1186
1297
  });
1187
1298
 
1188
1299
  // src/tools/read-file.ts
1189
- import { readFile as readFile2, stat } from "fs/promises";
1300
+ import { readFile as readFile3, stat } from "fs/promises";
1190
1301
  var readFileTool;
1191
1302
  var init_read_file = __esm({
1192
1303
  "src/tools/read-file.ts"() {
@@ -1235,7 +1346,7 @@ var init_read_file = __esm({
1235
1346
  if (probe.subarray(0, probeLen).includes(0)) {
1236
1347
  return `Binary file detected: ${filePath} (${fileStats.size} bytes)`;
1237
1348
  }
1238
- const content = await readFile2(filePath, "utf-8");
1349
+ const content = await readFile3(filePath, "utf-8");
1239
1350
  const lines = content.split("\n");
1240
1351
  const offset = Math.max(1, Number(args.offset) || 1);
1241
1352
  const limit = Number(args.limit) || lines.length;
@@ -1252,7 +1363,7 @@ var init_read_file = __esm({
1252
1363
  });
1253
1364
 
1254
1365
  // src/tools/write-file.ts
1255
- import { writeFile, mkdir } from "fs/promises";
1366
+ import { writeFile as writeFile2, mkdir } from "fs/promises";
1256
1367
  import { dirname, isAbsolute as isAbsolute2 } from "path";
1257
1368
  var writeFileTool;
1258
1369
  var init_write_file = __esm({
@@ -1294,7 +1405,7 @@ var init_write_file = __esm({
1294
1405
  const filePath = guard.path;
1295
1406
  try {
1296
1407
  await mkdir(dirname(filePath), { recursive: true });
1297
- await writeFile(filePath, args.content, "utf-8");
1408
+ await writeFile2(filePath, args.content, "utf-8");
1298
1409
  const lines = args.content.split("\n").length;
1299
1410
  return `Wrote ${lines} lines to ${filePath}`;
1300
1411
  } catch (err) {
@@ -1310,7 +1421,7 @@ var init_write_file = __esm({
1310
1421
  });
1311
1422
 
1312
1423
  // src/tools/edit-file.ts
1313
- import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1424
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1314
1425
  import { isAbsolute as isAbsolute3 } from "path";
1315
1426
  var editFileTool;
1316
1427
  var init_edit_file = __esm({
@@ -1351,7 +1462,7 @@ var init_edit_file = __esm({
1351
1462
  if (!guard.ok) return guard.error;
1352
1463
  const filePath = guard.path;
1353
1464
  try {
1354
- const content = await readFile3(filePath, "utf-8");
1465
+ const content = await readFile4(filePath, "utf-8");
1355
1466
  const oldStr = args.old_string;
1356
1467
  const newStr = args.new_string;
1357
1468
  const replaceAll = Boolean(args.replace_all);
@@ -1365,7 +1476,7 @@ var init_edit_file = __esm({
1365
1476
  }
1366
1477
  }
1367
1478
  const updated = replaceAll ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
1368
- await writeFile2(filePath, updated, "utf-8");
1479
+ await writeFile3(filePath, updated, "utf-8");
1369
1480
  const replacements = replaceAll ? content.split(oldStr).length - 1 : 1;
1370
1481
  return `Edited ${filePath} (${replacements} replacement${replacements > 1 ? "s" : ""})`;
1371
1482
  } catch (err) {
@@ -1382,7 +1493,7 @@ var init_edit_file = __esm({
1382
1493
 
1383
1494
  // src/tools/list-directory.ts
1384
1495
  import { readdir, stat as stat2 } from "fs/promises";
1385
- import { join as join2 } from "path";
1496
+ import { join as join3 } from "path";
1386
1497
  function formatSize(bytes) {
1387
1498
  if (bytes < 1024) return `${bytes}B`;
1388
1499
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
@@ -1423,7 +1534,7 @@ var init_list_directory = __esm({
1423
1534
  const lines = [];
1424
1535
  for (const entry of entries.slice(0, 100)) {
1425
1536
  try {
1426
- const full = join2(dirPath, entry);
1537
+ const full = join3(dirPath, entry);
1427
1538
  const s = await stat2(full);
1428
1539
  const type = s.isDirectory() ? "dir" : "file";
1429
1540
  const size = s.isDirectory() ? "" : ` (${formatSize(s.size)})`;
@@ -1446,8 +1557,8 @@ var init_list_directory = __esm({
1446
1557
  });
1447
1558
 
1448
1559
  // src/tools/search-files.ts
1449
- import { readdir as readdir2, readFile as readFile4, stat as stat3 } from "fs/promises";
1450
- import { join as join3, relative as relative2 } from "path";
1560
+ import { readdir as readdir2, readFile as readFile5, stat as stat3 } from "fs/promises";
1561
+ import { join as join4, relative as relative2 } from "path";
1451
1562
  var IGNORE_DIRS, searchFilesTool;
1452
1563
  var init_search_files = __esm({
1453
1564
  "src/tools/search-files.ts"() {
@@ -1522,7 +1633,7 @@ var init_search_files = __esm({
1522
1633
  for (const entry of entries) {
1523
1634
  if (results.length >= maxResults) return;
1524
1635
  if (IGNORE_DIRS.has(entry)) continue;
1525
- const full = join3(dir, entry);
1636
+ const full = join4(dir, entry);
1526
1637
  let s;
1527
1638
  try {
1528
1639
  s = await stat3(full);
@@ -1537,7 +1648,7 @@ var init_search_files = __esm({
1537
1648
  if (!entry.endsWith(ext)) continue;
1538
1649
  }
1539
1650
  try {
1540
- const content = await readFile4(full, "utf-8");
1651
+ const content = await readFile5(full, "utf-8");
1541
1652
  const lines = content.split("\n");
1542
1653
  for (let i = 0; i < lines.length; i++) {
1543
1654
  regex.lastIndex = 0;
@@ -1562,7 +1673,7 @@ var init_search_files = __esm({
1562
1673
 
1563
1674
  // src/tools/glob-files.ts
1564
1675
  import { readdir as readdir3, stat as stat4 } from "fs/promises";
1565
- import { join as join4 } from "path";
1676
+ import { join as join5 } from "path";
1566
1677
  function globToRegex(pattern) {
1567
1678
  let regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\{\{GLOBSTAR\}\}/g, ".*");
1568
1679
  return new RegExp(`^${regex}$`, "i");
@@ -1628,7 +1739,7 @@ var init_glob_files = __esm({
1628
1739
  for (const entry of entries) {
1629
1740
  if (matches.length >= 200) return;
1630
1741
  if (IGNORE_DIRS2.has(entry)) continue;
1631
- const full = join4(dir, entry);
1742
+ const full = join5(dir, entry);
1632
1743
  const rel = relDir ? `${relDir}/${entry}` : entry;
1633
1744
  let s;
1634
1745
  try {
@@ -1854,19 +1965,19 @@ var init_git = __esm({
1854
1965
  });
1855
1966
 
1856
1967
  // src/config/config.ts
1857
- import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
1858
- import { existsSync as existsSync2 } from "fs";
1859
- import { join as join5 } from "path";
1968
+ import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
1969
+ import { existsSync as existsSync3 } from "fs";
1970
+ import { join as join6 } from "path";
1860
1971
  import { homedir, platform as platform3 } from "os";
1861
1972
  import JSON5 from "json5";
1862
1973
  function getConfigDir() {
1863
1974
  if (platform3() === "win32") {
1864
- return join5(process.env.APPDATA || join5(homedir(), "AppData", "Roaming"), "Clank");
1975
+ return join6(process.env.APPDATA || join6(homedir(), "AppData", "Roaming"), "Clank");
1865
1976
  }
1866
- return join5(homedir(), ".clank");
1977
+ return join6(homedir(), ".clank");
1867
1978
  }
1868
1979
  function getConfigPath() {
1869
- return join5(getConfigDir(), "config.json5");
1980
+ return join6(getConfigDir(), "config.json5");
1870
1981
  }
1871
1982
  function defaultConfig() {
1872
1983
  return {
@@ -1878,7 +1989,7 @@ function defaultConfig() {
1878
1989
  agents: {
1879
1990
  defaults: {
1880
1991
  model: { primary: "ollama/qwen3.5" },
1881
- workspace: join5(getConfigDir(), "workspace"),
1992
+ workspace: join6(getConfigDir(), "workspace"),
1882
1993
  toolTier: "auto",
1883
1994
  temperature: 0.7
1884
1995
  },
@@ -1937,11 +2048,11 @@ function deepMerge(target, source) {
1937
2048
  async function loadConfig() {
1938
2049
  const configPath = getConfigPath();
1939
2050
  const defaults = defaultConfig();
1940
- if (!existsSync2(configPath)) {
2051
+ if (!existsSync3(configPath)) {
1941
2052
  return defaults;
1942
2053
  }
1943
2054
  try {
1944
- const raw = await readFile5(configPath, "utf-8");
2055
+ const raw = await readFile6(configPath, "utf-8");
1945
2056
  const parsed = JSON5.parse(raw);
1946
2057
  const substituted = substituteEnvVars(parsed);
1947
2058
  return deepMerge(defaults, substituted);
@@ -1954,15 +2065,15 @@ async function saveConfig(config) {
1954
2065
  const configPath = getConfigPath();
1955
2066
  await mkdir2(getConfigDir(), { recursive: true });
1956
2067
  const content = JSON5.stringify(config, null, 2);
1957
- await writeFile3(configPath, content, "utf-8");
2068
+ await writeFile4(configPath, content, "utf-8");
1958
2069
  }
1959
2070
  async function ensureConfigDir() {
1960
2071
  const configDir = getConfigDir();
1961
2072
  await mkdir2(configDir, { recursive: true });
1962
- await mkdir2(join5(configDir, "workspace"), { recursive: true });
1963
- await mkdir2(join5(configDir, "conversations"), { recursive: true });
1964
- await mkdir2(join5(configDir, "memory"), { recursive: true });
1965
- await mkdir2(join5(configDir, "logs"), { recursive: true });
2073
+ await mkdir2(join6(configDir, "workspace"), { recursive: true });
2074
+ await mkdir2(join6(configDir, "conversations"), { recursive: true });
2075
+ await mkdir2(join6(configDir, "memory"), { recursive: true });
2076
+ await mkdir2(join6(configDir, "logs"), { recursive: true });
1966
2077
  }
1967
2078
  var init_config = __esm({
1968
2079
  "src/config/config.ts"() {
@@ -3358,9 +3469,9 @@ var init_model_tool = __esm({
3358
3469
  });
3359
3470
 
3360
3471
  // src/sessions/store.ts
3361
- import { readFile as readFile6, writeFile as writeFile4, unlink, mkdir as mkdir3 } from "fs/promises";
3362
- import { existsSync as existsSync3 } from "fs";
3363
- import { join as join6 } from "path";
3472
+ import { readFile as readFile7, writeFile as writeFile5, unlink, mkdir as mkdir3 } from "fs/promises";
3473
+ import { existsSync as existsSync4 } from "fs";
3474
+ import { join as join7 } from "path";
3364
3475
  import { randomUUID } from "crypto";
3365
3476
  var SessionStore;
3366
3477
  var init_store = __esm({
@@ -3373,14 +3484,14 @@ var init_store = __esm({
3373
3484
  index = /* @__PURE__ */ new Map();
3374
3485
  constructor(storeDir) {
3375
3486
  this.storeDir = storeDir;
3376
- this.indexPath = join6(storeDir, "sessions.json");
3487
+ this.indexPath = join7(storeDir, "sessions.json");
3377
3488
  }
3378
3489
  /** Initialize the store — load index from disk */
3379
3490
  async init() {
3380
3491
  await mkdir3(this.storeDir, { recursive: true });
3381
- if (existsSync3(this.indexPath)) {
3492
+ if (existsSync4(this.indexPath)) {
3382
3493
  try {
3383
- const raw = await readFile6(this.indexPath, "utf-8");
3494
+ const raw = await readFile7(this.indexPath, "utf-8");
3384
3495
  const entries = JSON.parse(raw);
3385
3496
  for (const entry of entries) {
3386
3497
  this.index.set(entry.normalizedKey, entry);
@@ -3393,7 +3504,7 @@ var init_store = __esm({
3393
3504
  /** Save the index to disk */
3394
3505
  async saveIndex() {
3395
3506
  const entries = Array.from(this.index.values());
3396
- await writeFile4(this.indexPath, JSON.stringify(entries, null, 2), "utf-8");
3507
+ await writeFile5(this.indexPath, JSON.stringify(entries, null, 2), "utf-8");
3397
3508
  }
3398
3509
  /** Get or create a session for a normalized key */
3399
3510
  async resolve(normalizedKey, opts) {
@@ -3419,10 +3530,10 @@ var init_store = __esm({
3419
3530
  }
3420
3531
  /** Load conversation messages for a session */
3421
3532
  async loadMessages(sessionId) {
3422
- const path2 = join6(this.storeDir, `${sessionId}.json`);
3423
- if (!existsSync3(path2)) return [];
3533
+ const path2 = join7(this.storeDir, `${sessionId}.json`);
3534
+ if (!existsSync4(path2)) return [];
3424
3535
  try {
3425
- const raw = await readFile6(path2, "utf-8");
3536
+ const raw = await readFile7(path2, "utf-8");
3426
3537
  return JSON.parse(raw);
3427
3538
  } catch {
3428
3539
  return [];
@@ -3430,8 +3541,8 @@ var init_store = __esm({
3430
3541
  }
3431
3542
  /** Save conversation messages for a session */
3432
3543
  async saveMessages(sessionId, messages) {
3433
- const path2 = join6(this.storeDir, `${sessionId}.json`);
3434
- await writeFile4(path2, JSON.stringify(messages, null, 2), "utf-8");
3544
+ const path2 = join7(this.storeDir, `${sessionId}.json`);
3545
+ await writeFile5(path2, JSON.stringify(messages, null, 2), "utf-8");
3435
3546
  }
3436
3547
  /** List all sessions, sorted by last used */
3437
3548
  list() {
@@ -3443,7 +3554,7 @@ var init_store = __esm({
3443
3554
  if (!entry) return false;
3444
3555
  this.index.delete(normalizedKey);
3445
3556
  await this.saveIndex();
3446
- const path2 = join6(this.storeDir, `${entry.id}.json`);
3557
+ const path2 = join7(this.storeDir, `${entry.id}.json`);
3447
3558
  try {
3448
3559
  await unlink(path2);
3449
3560
  } catch {
@@ -3454,7 +3565,7 @@ var init_store = __esm({
3454
3565
  async reset(normalizedKey) {
3455
3566
  const entry = this.index.get(normalizedKey);
3456
3567
  if (!entry) return null;
3457
- const path2 = join6(this.storeDir, `${entry.id}.json`);
3568
+ const path2 = join7(this.storeDir, `${entry.id}.json`);
3458
3569
  try {
3459
3570
  await unlink(path2);
3460
3571
  } catch {
@@ -3500,7 +3611,7 @@ var init_sessions = __esm({
3500
3611
  });
3501
3612
 
3502
3613
  // src/tools/self-config/session-tool.ts
3503
- import { join as join7 } from "path";
3614
+ import { join as join8 } from "path";
3504
3615
  var sessionTool;
3505
3616
  var init_session_tool = __esm({
3506
3617
  "src/tools/self-config/session-tool.ts"() {
@@ -3534,7 +3645,7 @@ var init_session_tool = __esm({
3534
3645
  return { ok: true };
3535
3646
  },
3536
3647
  async execute(args) {
3537
- const store = new SessionStore(join7(getConfigDir(), "conversations"));
3648
+ const store = new SessionStore(join8(getConfigDir(), "conversations"));
3538
3649
  await store.init();
3539
3650
  const action = args.action;
3540
3651
  if (action === "list") {
@@ -3563,9 +3674,9 @@ var init_session_tool = __esm({
3563
3674
  });
3564
3675
 
3565
3676
  // src/cron/scheduler.ts
3566
- import { readFile as readFile7, appendFile, mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
3567
- import { existsSync as existsSync4 } from "fs";
3568
- import { join as join8 } from "path";
3677
+ import { readFile as readFile8, appendFile, mkdir as mkdir4, writeFile as writeFile6 } from "fs/promises";
3678
+ import { existsSync as existsSync5 } from "fs";
3679
+ import { join as join9 } from "path";
3569
3680
  import { randomUUID as randomUUID2 } from "crypto";
3570
3681
  var CronScheduler;
3571
3682
  var init_scheduler = __esm({
@@ -3580,8 +3691,8 @@ var init_scheduler = __esm({
3580
3691
  running = false;
3581
3692
  onJobDue;
3582
3693
  constructor(storeDir) {
3583
- this.jobsPath = join8(storeDir, "jobs.jsonl");
3584
- this.runsDir = join8(storeDir, "runs");
3694
+ this.jobsPath = join9(storeDir, "jobs.jsonl");
3695
+ this.runsDir = join9(storeDir, "runs");
3585
3696
  }
3586
3697
  /** Initialize — load jobs from disk */
3587
3698
  async init() {
@@ -3703,9 +3814,9 @@ var init_scheduler = __esm({
3703
3814
  }
3704
3815
  /** Load jobs from JSONL file */
3705
3816
  async loadJobs() {
3706
- if (!existsSync4(this.jobsPath)) return;
3817
+ if (!existsSync5(this.jobsPath)) return;
3707
3818
  try {
3708
- const raw = await readFile7(this.jobsPath, "utf-8");
3819
+ const raw = await readFile8(this.jobsPath, "utf-8");
3709
3820
  this.jobs = raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
3710
3821
  } catch {
3711
3822
  this.jobs = [];
@@ -3714,11 +3825,11 @@ var init_scheduler = __esm({
3714
3825
  /** Save jobs to JSONL file */
3715
3826
  async saveJobs() {
3716
3827
  const content = this.jobs.map((j) => JSON.stringify(j)).join("\n") + "\n";
3717
- await writeFile5(this.jobsPath, content, "utf-8");
3828
+ await writeFile6(this.jobsPath, content, "utf-8");
3718
3829
  }
3719
3830
  /** Log a run result */
3720
3831
  async logRun(log) {
3721
- const logPath = join8(this.runsDir, `${log.jobId}.jsonl`);
3832
+ const logPath = join9(this.runsDir, `${log.jobId}.jsonl`);
3722
3833
  await appendFile(logPath, JSON.stringify(log) + "\n", "utf-8");
3723
3834
  }
3724
3835
  };
@@ -3739,7 +3850,7 @@ var init_cron = __esm({
3739
3850
  });
3740
3851
 
3741
3852
  // src/tools/self-config/cron-tool.ts
3742
- import { join as join9 } from "path";
3853
+ import { join as join10 } from "path";
3743
3854
  var cronTool;
3744
3855
  var init_cron_tool = __esm({
3745
3856
  "src/tools/self-config/cron-tool.ts"() {
@@ -3777,7 +3888,7 @@ var init_cron_tool = __esm({
3777
3888
  return { ok: true };
3778
3889
  },
3779
3890
  async execute(args, ctx) {
3780
- const scheduler = new CronScheduler(join9(getConfigDir(), "cron"));
3891
+ const scheduler = new CronScheduler(join10(getConfigDir(), "cron"));
3781
3892
  await scheduler.init();
3782
3893
  const action = args.action;
3783
3894
  if (action === "list") {
@@ -3931,11 +4042,9 @@ var init_tts = __esm({
3931
4042
  constructor(config) {
3932
4043
  this.config = config;
3933
4044
  }
3934
- /** Check if TTS is available */
3935
4045
  isAvailable() {
3936
4046
  return !!(this.config.integrations.elevenlabs?.enabled && this.config.integrations.elevenlabs?.apiKey);
3937
4047
  }
3938
- /** Convert text to speech */
3939
4048
  async synthesize(text, opts) {
3940
4049
  const elevenlabs = this.config.integrations.elevenlabs;
3941
4050
  if (!elevenlabs?.enabled || !elevenlabs.apiKey) return null;
@@ -3953,29 +4062,21 @@ var init_tts = __esm({
3953
4062
  body: JSON.stringify({
3954
4063
  text,
3955
4064
  model_id: model,
3956
- voice_settings: {
3957
- stability: 0.5,
3958
- similarity_boost: 0.75
3959
- }
4065
+ voice_settings: { stability: 0.5, similarity_boost: 0.75 }
3960
4066
  })
3961
4067
  }
3962
4068
  );
3963
4069
  if (!res.ok) {
3964
- const err = await res.text().catch(() => "");
3965
- console.error(`ElevenLabs TTS error ${res.status}: ${err}`);
4070
+ console.error(`ElevenLabs TTS error ${res.status}`);
3966
4071
  return null;
3967
4072
  }
3968
4073
  const arrayBuffer = await res.arrayBuffer();
3969
- return {
3970
- audioBuffer: Buffer.from(arrayBuffer),
3971
- format: "mp3"
3972
- };
4074
+ return { audioBuffer: Buffer.from(arrayBuffer), format: "mp3" };
3973
4075
  } catch (err) {
3974
4076
  console.error(`TTS error: ${err instanceof Error ? err.message : err}`);
3975
4077
  return null;
3976
4078
  }
3977
4079
  }
3978
- /** List available voices from ElevenLabs */
3979
4080
  async listVoices() {
3980
4081
  const elevenlabs = this.config.integrations.elevenlabs;
3981
4082
  if (!elevenlabs?.enabled || !elevenlabs.apiKey) return [];
@@ -3996,53 +4097,67 @@ var init_tts = __esm({
3996
4097
  constructor(config) {
3997
4098
  this.config = config;
3998
4099
  }
3999
- /** Check if STT is available */
4000
4100
  isAvailable() {
4001
4101
  const whisper = this.config.integrations.whisper;
4002
4102
  if (whisper?.enabled) {
4103
+ if (whisper.provider === "groq" && whisper.apiKey) return true;
4003
4104
  if (whisper.provider === "openai" && whisper.apiKey) return true;
4004
4105
  if (whisper.provider === "local") return true;
4005
4106
  }
4006
4107
  if (this.config.models.providers.openai?.apiKey) return true;
4108
+ if (this.config.integrations.groq?.apiKey) return true;
4007
4109
  return false;
4008
4110
  }
4009
- /** Transcribe audio to text */
4010
4111
  async transcribe(audioBuffer, format = "ogg") {
4011
4112
  const whisper = this.config.integrations.whisper;
4012
- const apiKey = whisper?.apiKey || this.config.models.providers.openai?.apiKey;
4013
- if (apiKey && whisper?.provider !== "local") {
4014
- return this.transcribeOpenAI(audioBuffer, format, apiKey);
4113
+ const groqKey = whisper?.provider === "groq" && whisper?.apiKey ? whisper.apiKey : this.config.integrations.groq?.apiKey;
4114
+ if (groqKey) {
4115
+ const result = await this.transcribeAPI(audioBuffer, format, groqKey, "https://api.groq.com/openai/v1/audio/transcriptions", "whisper-large-v3-turbo");
4116
+ if (result) return result;
4117
+ }
4118
+ const openaiKey = whisper?.provider === "openai" && whisper?.apiKey ? whisper.apiKey : this.config.models.providers.openai?.apiKey;
4119
+ if (openaiKey) {
4120
+ const result = await this.transcribeAPI(audioBuffer, format, openaiKey, "https://api.openai.com/v1/audio/transcriptions", "whisper-1");
4121
+ if (result) return result;
4015
4122
  }
4016
- return this.transcribeLocal(audioBuffer, format);
4123
+ if (whisper?.provider === "local") {
4124
+ return this.transcribeLocal(audioBuffer, format);
4125
+ }
4126
+ return null;
4017
4127
  }
4018
- /** Transcribe via OpenAI Whisper API */
4019
- async transcribeOpenAI(audioBuffer, format, apiKey) {
4128
+ /** Transcribe via OpenAI-compatible API (works for both OpenAI and Groq) */
4129
+ async transcribeAPI(audioBuffer, format, apiKey, endpoint, model) {
4020
4130
  try {
4021
4131
  const blob = new Blob([new Uint8Array(audioBuffer)], { type: `audio/${format}` });
4022
4132
  const formData = new FormData();
4023
4133
  formData.append("file", blob, `audio.${format}`);
4024
- formData.append("model", "whisper-1");
4025
- const res = await fetch("https://api.openai.com/v1/audio/transcriptions", {
4134
+ formData.append("model", model);
4135
+ const res = await fetch(endpoint, {
4026
4136
  method: "POST",
4027
4137
  headers: { "Authorization": `Bearer ${apiKey}` },
4028
4138
  body: formData
4029
4139
  });
4030
- if (!res.ok) return null;
4140
+ if (!res.ok) {
4141
+ const errText = await res.text().catch(() => "");
4142
+ console.error(`STT API error ${res.status}: ${errText.slice(0, 200)}`);
4143
+ return null;
4144
+ }
4031
4145
  const data = await res.json();
4032
4146
  return data.text ? { text: data.text, language: data.language } : null;
4033
- } catch {
4147
+ } catch (err) {
4148
+ console.error(`STT error: ${err instanceof Error ? err.message : err}`);
4034
4149
  return null;
4035
4150
  }
4036
4151
  }
4037
4152
  /** Transcribe via local whisper.cpp */
4038
4153
  async transcribeLocal(audioBuffer, format) {
4039
4154
  try {
4040
- const { writeFile: writeFile9, unlink: unlink5 } = await import("fs/promises");
4155
+ const { writeFile: writeFile10, unlink: unlink5 } = await import("fs/promises");
4041
4156
  const { execSync: execSync3 } = await import("child_process");
4042
- const { join: join19 } = await import("path");
4157
+ const { join: join20 } = await import("path");
4043
4158
  const { tmpdir } = await import("os");
4044
- const tmpFile = join19(tmpdir(), `clank-stt-${Date.now()}.${format}`);
4045
- await writeFile9(tmpFile, audioBuffer);
4159
+ const tmpFile = join20(tmpdir(), `clank-stt-${Date.now()}.${format}`);
4160
+ await writeFile10(tmpFile, audioBuffer);
4046
4161
  const output = execSync3(`whisper "${tmpFile}" --model base.en --output-txt`, {
4047
4162
  encoding: "utf-8",
4048
4163
  timeout: 6e4
@@ -4110,11 +4225,11 @@ var init_voice_tool = __esm({
4110
4225
  voiceId: args.voice_id
4111
4226
  });
4112
4227
  if (!result) return "Error: TTS synthesis failed";
4113
- const { writeFile: writeFile9 } = await import("fs/promises");
4114
- const { join: join19 } = await import("path");
4228
+ const { writeFile: writeFile10 } = await import("fs/promises");
4229
+ const { join: join20 } = await import("path");
4115
4230
  const { tmpdir } = await import("os");
4116
- const outPath = join19(tmpdir(), `clank-tts-${Date.now()}.${result.format}`);
4117
- await writeFile9(outPath, result.audioBuffer);
4231
+ const outPath = join20(tmpdir(), `clank-tts-${Date.now()}.${result.format}`);
4232
+ await writeFile10(outPath, result.audioBuffer);
4118
4233
  return `Audio generated: ${outPath} (${result.format}, ${Math.round(result.audioBuffer.length / 1024)}KB)`;
4119
4234
  }
4120
4235
  };
@@ -4137,16 +4252,16 @@ var init_voice_tool = __esm({
4137
4252
  return { ok: true };
4138
4253
  },
4139
4254
  async execute(args) {
4140
- const { readFile: readFile12 } = await import("fs/promises");
4141
- const { existsSync: existsSync10 } = await import("fs");
4255
+ const { readFile: readFile13 } = await import("fs/promises");
4256
+ const { existsSync: existsSync12 } = await import("fs");
4142
4257
  const filePath = args.file_path;
4143
- if (!existsSync10(filePath)) return `Error: File not found: ${filePath}`;
4258
+ if (!existsSync12(filePath)) return `Error: File not found: ${filePath}`;
4144
4259
  const config = await loadConfig();
4145
4260
  const engine = new STTEngine(config);
4146
4261
  if (!engine.isAvailable()) {
4147
4262
  return "Error: Speech-to-text not configured. Need OpenAI API key or local whisper.cpp installed.";
4148
4263
  }
4149
- const audioBuffer = await readFile12(filePath);
4264
+ const audioBuffer = await readFile13(filePath);
4150
4265
  const ext = filePath.split(".").pop() || "wav";
4151
4266
  const result = await engine.transcribe(audioBuffer, ext);
4152
4267
  if (!result) return "Error: Transcription failed";
@@ -4178,6 +4293,53 @@ var init_voice_tool = __esm({
4178
4293
  }
4179
4294
  });
4180
4295
 
4296
+ // src/tools/self-config/file-share-tool.ts
4297
+ import { existsSync as existsSync6 } from "fs";
4298
+ import { stat as stat5 } from "fs/promises";
4299
+ var MAX_FILE_SIZE, fileShareTool;
4300
+ var init_file_share_tool = __esm({
4301
+ "src/tools/self-config/file-share-tool.ts"() {
4302
+ "use strict";
4303
+ init_esm_shims();
4304
+ init_path_guard();
4305
+ MAX_FILE_SIZE = 10 * 1024 * 1024;
4306
+ fileShareTool = {
4307
+ definition: {
4308
+ name: "share_file",
4309
+ description: "Share a file from the workspace with the user via the current channel (Telegram, Discord, etc.). The file must be within the workspace. Max 10MB.",
4310
+ parameters: {
4311
+ type: "object",
4312
+ properties: {
4313
+ path: { type: "string", description: "Path to the file to share" },
4314
+ caption: { type: "string", description: "Optional message to send with the file" }
4315
+ },
4316
+ required: ["path"]
4317
+ }
4318
+ },
4319
+ safetyLevel: "medium",
4320
+ readOnly: true,
4321
+ validate(args, ctx) {
4322
+ if (!args.path || typeof args.path !== "string") return { ok: false, error: "path is required" };
4323
+ const guard = guardPath(args.path, ctx.projectRoot);
4324
+ if (!guard.ok) return { ok: false, error: guard.error };
4325
+ return { ok: true };
4326
+ },
4327
+ async execute(args, ctx) {
4328
+ const guard = guardPath(args.path, ctx.projectRoot, { allowExternal: ctx.allowExternal });
4329
+ if (!guard.ok) return guard.error;
4330
+ if (!existsSync6(guard.path)) return `Error: File not found: ${guard.path}`;
4331
+ const fileStats = await stat5(guard.path);
4332
+ if (fileStats.size > MAX_FILE_SIZE) return `Error: File too large (${Math.round(fileStats.size / 1024 / 1024)}MB, max 10MB)`;
4333
+ const caption = args.caption ? ` with caption: "${args.caption}"` : "";
4334
+ return `File ready to share: ${guard.path} (${Math.round(fileStats.size / 1024)}KB)${caption}. The file will be sent through the current channel.`;
4335
+ },
4336
+ formatConfirmation(args) {
4337
+ return `Share file: ${args.path}`;
4338
+ }
4339
+ };
4340
+ }
4341
+ });
4342
+
4181
4343
  // src/tools/self-config/index.ts
4182
4344
  function registerSelfConfigTools(registry) {
4183
4345
  registry.register(configTool);
@@ -4191,6 +4353,7 @@ function registerSelfConfigTools(registry) {
4191
4353
  registry.register(ttsTool);
4192
4354
  registry.register(sttTool);
4193
4355
  registry.register(voiceListTool);
4356
+ registry.register(fileShareTool);
4194
4357
  }
4195
4358
  var init_self_config = __esm({
4196
4359
  "src/tools/self-config/index.ts"() {
@@ -4205,6 +4368,7 @@ var init_self_config = __esm({
4205
4368
  init_gateway_tool();
4206
4369
  init_message_tool();
4207
4370
  init_voice_tool();
4371
+ init_file_share_tool();
4208
4372
  init_config_tool();
4209
4373
  init_channel_tool();
4210
4374
  init_agent_tool();
@@ -4214,6 +4378,7 @@ var init_self_config = __esm({
4214
4378
  init_gateway_tool();
4215
4379
  init_message_tool();
4216
4380
  init_voice_tool();
4381
+ init_file_share_tool();
4217
4382
  }
4218
4383
  });
4219
4384
 
@@ -4275,7 +4440,7 @@ __export(chat_exports, {
4275
4440
  runChat: () => runChat
4276
4441
  });
4277
4442
  import { createInterface } from "readline";
4278
- import { join as join10 } from "path";
4443
+ import { join as join11 } from "path";
4279
4444
  async function runChat(opts) {
4280
4445
  await ensureConfigDir();
4281
4446
  const config = await loadConfig();
@@ -4292,9 +4457,9 @@ async function runChat(opts) {
4292
4457
  console.log(dim("Starting gateway..."));
4293
4458
  const { fork: fork2 } = await import("child_process");
4294
4459
  const { fileURLToPath: fileURLToPath6 } = await import("url");
4295
- const { dirname: dirname6, join: join19 } = await import("path");
4460
+ const { dirname: dirname6, join: join20 } = await import("path");
4296
4461
  const __filename4 = fileURLToPath6(import.meta.url);
4297
- const entryPoint = join19(dirname6(__filename4), "index.js");
4462
+ const entryPoint = join20(dirname6(__filename4), "index.js");
4298
4463
  const child = fork2(entryPoint, ["gateway", "start", "--foreground"], {
4299
4464
  detached: true,
4300
4465
  stdio: "ignore"
@@ -4342,7 +4507,7 @@ async function runChat(opts) {
4342
4507
  console.error(dim("Make sure Ollama is running or configure a cloud provider in ~/.clank/config.json5"));
4343
4508
  process.exit(1);
4344
4509
  }
4345
- const sessionStore = new SessionStore(join10(getConfigDir(), "conversations"));
4510
+ const sessionStore = new SessionStore(join11(getConfigDir(), "conversations"));
4346
4511
  await sessionStore.init();
4347
4512
  const toolRegistry = createFullRegistry();
4348
4513
  const identity = {
@@ -4500,9 +4665,9 @@ var init_chat = __esm({
4500
4665
  });
4501
4666
 
4502
4667
  // src/memory/memory.ts
4503
- import { readFile as readFile8, writeFile as writeFile6, readdir as readdir5, mkdir as mkdir5 } from "fs/promises";
4504
- import { existsSync as existsSync5 } from "fs";
4505
- import { join as join11 } from "path";
4668
+ import { readFile as readFile9, writeFile as writeFile7, readdir as readdir5, mkdir as mkdir5 } from "fs/promises";
4669
+ import { existsSync as existsSync7 } from "fs";
4670
+ import { join as join12 } from "path";
4506
4671
  import { randomUUID as randomUUID3 } from "crypto";
4507
4672
  function tokenize(text) {
4508
4673
  return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3 && !STOPWORDS.has(t));
@@ -4650,12 +4815,12 @@ var init_memory = __esm({
4650
4815
  meta = /* @__PURE__ */ new Map();
4651
4816
  constructor(memoryDir) {
4652
4817
  this.memoryDir = memoryDir;
4653
- this.metaPath = join11(memoryDir, "_meta.json");
4818
+ this.metaPath = join12(memoryDir, "_meta.json");
4654
4819
  }
4655
4820
  /** Initialize — create dirs and load metadata */
4656
4821
  async init() {
4657
4822
  for (const cat of ["identity", "knowledge", "lessons", "context"]) {
4658
- await mkdir5(join11(this.memoryDir, cat), { recursive: true });
4823
+ await mkdir5(join12(this.memoryDir, cat), { recursive: true });
4659
4824
  }
4660
4825
  await this.loadMeta();
4661
4826
  }
@@ -4700,10 +4865,10 @@ var init_memory = __esm({
4700
4865
  let used = 0;
4701
4866
  if (projectRoot) {
4702
4867
  for (const name of [".clank.md", ".clankbuild.md", ".llamabuild.md"]) {
4703
- const path2 = join11(projectRoot, name);
4704
- if (existsSync5(path2)) {
4868
+ const path2 = join12(projectRoot, name);
4869
+ if (existsSync7(path2)) {
4705
4870
  try {
4706
- const content = await readFile8(path2, "utf-8");
4871
+ const content = await readFile9(path2, "utf-8");
4707
4872
  if (content.trim() && used + content.length < budgetChars) {
4708
4873
  parts.push("## Project Memory\n" + content.trim());
4709
4874
  used += content.length;
@@ -4714,10 +4879,10 @@ var init_memory = __esm({
4714
4879
  }
4715
4880
  }
4716
4881
  }
4717
- const globalPath = join11(this.memoryDir, "..", "workspace", "MEMORY.md");
4718
- if (existsSync5(globalPath)) {
4882
+ const globalPath = join12(this.memoryDir, "..", "workspace", "MEMORY.md");
4883
+ if (existsSync7(globalPath)) {
4719
4884
  try {
4720
- const content = await readFile8(globalPath, "utf-8");
4885
+ const content = await readFile9(globalPath, "utf-8");
4721
4886
  if (content.trim() && used + content.length < budgetChars) {
4722
4887
  parts.push("## Global Memory\n" + content.trim());
4723
4888
  used += content.length;
@@ -4738,8 +4903,8 @@ ${entry.content}`);
4738
4903
  async add(category, title, content) {
4739
4904
  const id = randomUUID3();
4740
4905
  const filename = `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 50)}.md`;
4741
- const filePath = join11(this.memoryDir, category, filename);
4742
- await writeFile6(filePath, `# ${title}
4906
+ const filePath = join12(this.memoryDir, category, filename);
4907
+ await writeFile7(filePath, `# ${title}
4743
4908
 
4744
4909
  ${content}`, "utf-8");
4745
4910
  this.meta.set(id, {
@@ -4784,15 +4949,15 @@ ${content}`, "utf-8");
4784
4949
  async loadAll() {
4785
4950
  const entries = [];
4786
4951
  for (const category of ["identity", "knowledge", "lessons", "context"]) {
4787
- const dir = join11(this.memoryDir, category);
4788
- if (!existsSync5(dir)) continue;
4952
+ const dir = join12(this.memoryDir, category);
4953
+ if (!existsSync7(dir)) continue;
4789
4954
  try {
4790
4955
  const files = await readdir5(dir);
4791
4956
  for (const file of files) {
4792
4957
  if (!file.endsWith(".md")) continue;
4793
- const filePath = join11(dir, file);
4958
+ const filePath = join12(dir, file);
4794
4959
  try {
4795
- const content = await readFile8(filePath, "utf-8");
4960
+ const content = await readFile9(filePath, "utf-8");
4796
4961
  const title = content.split("\n")[0]?.replace(/^#\s*/, "") || file;
4797
4962
  entries.push({
4798
4963
  id: file,
@@ -4817,9 +4982,9 @@ ${content}`, "utf-8");
4817
4982
  return recency * frequencyBoost;
4818
4983
  }
4819
4984
  async loadMeta() {
4820
- if (!existsSync5(this.metaPath)) return;
4985
+ if (!existsSync7(this.metaPath)) return;
4821
4986
  try {
4822
- const raw = await readFile8(this.metaPath, "utf-8");
4987
+ const raw = await readFile9(this.metaPath, "utf-8");
4823
4988
  const entries = JSON.parse(raw);
4824
4989
  for (const e of entries) this.meta.set(e.id, e);
4825
4990
  } catch {
@@ -4827,7 +4992,7 @@ ${content}`, "utf-8");
4827
4992
  }
4828
4993
  async saveMeta() {
4829
4994
  const entries = Array.from(this.meta.values());
4830
- await writeFile6(this.metaPath, JSON.stringify(entries, null, 2), "utf-8");
4995
+ await writeFile7(this.metaPath, JSON.stringify(entries, null, 2), "utf-8");
4831
4996
  }
4832
4997
  };
4833
4998
  }
@@ -4839,6 +5004,7 @@ var init_memory2 = __esm({
4839
5004
  "use strict";
4840
5005
  init_esm_shims();
4841
5006
  init_memory();
5007
+ init_auto_persist();
4842
5008
  }
4843
5009
  });
4844
5010
 
@@ -5034,15 +5200,53 @@ var init_telegram = __esm({
5034
5200
  if (!this.gateway) return;
5035
5201
  try {
5036
5202
  await ctx.api.sendChatAction(chatId, "typing");
5037
- const response = await this.gateway.handleInboundMessage(
5203
+ let streamMsgId = null;
5204
+ let accumulated = "";
5205
+ let lastEditTime = 0;
5206
+ const EDIT_INTERVAL = 800;
5207
+ const response = await this.gateway.handleInboundMessageStreaming(
5038
5208
  {
5039
5209
  channel: "telegram",
5040
5210
  peerId: chatId,
5041
5211
  peerKind: isGroup ? "group" : "dm"
5042
5212
  },
5043
- msg.text
5213
+ msg.text,
5214
+ {
5215
+ onToken: (content) => {
5216
+ accumulated += content;
5217
+ const now = Date.now();
5218
+ if (!streamMsgId && accumulated.length > 20) {
5219
+ bot.api.sendMessage(chatId, accumulated + " \u258D").then((sent) => {
5220
+ streamMsgId = sent.message_id;
5221
+ lastEditTime = now;
5222
+ }).catch(() => {
5223
+ });
5224
+ return;
5225
+ }
5226
+ if (streamMsgId && now - lastEditTime > EDIT_INTERVAL) {
5227
+ lastEditTime = now;
5228
+ const display = accumulated.length > 4e3 ? accumulated.slice(-3900) + " \u258D" : accumulated + " \u258D";
5229
+ bot.api.editMessageText(chatId, streamMsgId, display).catch(() => {
5230
+ });
5231
+ }
5232
+ },
5233
+ onToolStart: (name) => {
5234
+ if (!streamMsgId) {
5235
+ bot.api.sendChatAction(chatId, "typing").catch(() => {
5236
+ });
5237
+ }
5238
+ },
5239
+ onError: (message) => {
5240
+ bot.api.sendMessage(chatId, `Error: ${message.slice(0, 200)}`).catch(() => {
5241
+ });
5242
+ }
5243
+ }
5044
5244
  );
5045
- if (response) {
5245
+ if (streamMsgId && response) {
5246
+ const finalText = response.length > 4e3 ? response.slice(0, 3950) + "\n... (truncated)" : response;
5247
+ await bot.api.editMessageText(chatId, streamMsgId, finalText).catch(() => {
5248
+ });
5249
+ } else if (response && !streamMsgId) {
5046
5250
  const chunks = splitMessage(response, 4e3);
5047
5251
  for (const chunk of chunks) {
5048
5252
  await ctx.api.sendMessage(chatId, chunk);
@@ -5106,7 +5310,8 @@ var init_telegram = __esm({
5106
5310
  const { TTSEngine: TTSEngine2 } = await Promise.resolve().then(() => (init_voice(), voice_exports));
5107
5311
  const tts = new TTSEngine2(config);
5108
5312
  if (tts.isAvailable() && response.length < 2e3) {
5109
- const audio = await tts.synthesize(response);
5313
+ const agentVoice = config.agents.list.find((a) => a.voiceId)?.voiceId;
5314
+ const audio = await tts.synthesize(response, { voiceId: agentVoice });
5110
5315
  if (audio) {
5111
5316
  const { InputFile } = await import("grammy");
5112
5317
  await ctx.api.sendVoice(chatId, new InputFile(audio.audioBuffer, "reply.mp3"));
@@ -5128,6 +5333,94 @@ var init_telegram = __esm({
5128
5333
  });
5129
5334
  chatLocks.set(chatId, next);
5130
5335
  });
5336
+ bot.on("message:photo", async (ctx) => {
5337
+ const msg = ctx.message;
5338
+ const chatId = msg.chat.id;
5339
+ if (msg.date < startupTime - 30) return;
5340
+ if (telegramConfig.allowFrom && telegramConfig.allowFrom.length > 0) {
5341
+ const username = msg.from?.username ? `@${msg.from.username}` : "";
5342
+ const userIdStr = String(msg.from?.id || "");
5343
+ const allowed = telegramConfig.allowFrom.map(String);
5344
+ if (!allowed.some((a) => a === userIdStr || a.toLowerCase() === username.toLowerCase() || a.toLowerCase() === (msg.from?.username || "").toLowerCase())) return;
5345
+ }
5346
+ const processPhoto = async () => {
5347
+ if (!this.gateway) return;
5348
+ try {
5349
+ const photo = msg.photo[msg.photo.length - 1];
5350
+ const file = await bot.api.getFile(photo.file_id);
5351
+ const fileUrl = `https://api.telegram.org/file/bot${telegramConfig.botToken}/${file.file_path}`;
5352
+ const caption = msg.caption || "";
5353
+ const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
5354
+ const response = await this.gateway.handleInboundMessage(
5355
+ { channel: "telegram", peerId: chatId, peerKind: isGroup ? "group" : "dm" },
5356
+ `[Image received: ${fileUrl}]${caption ? ` Caption: ${caption}` : ""}
5357
+
5358
+ Describe or analyze the image if you can, or acknowledge it.`
5359
+ );
5360
+ if (response) {
5361
+ const chunks = splitMessage(response, 4e3);
5362
+ for (const chunk of chunks) await ctx.api.sendMessage(chatId, chunk);
5363
+ }
5364
+ } catch (err) {
5365
+ await ctx.api.sendMessage(chatId, `Error: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
5366
+ }
5367
+ };
5368
+ const prev = chatLocks.get(chatId) || Promise.resolve();
5369
+ chatLocks.set(chatId, prev.then(processPhoto).catch(() => {
5370
+ }));
5371
+ });
5372
+ bot.on("message:document", async (ctx) => {
5373
+ const msg = ctx.message;
5374
+ const chatId = msg.chat.id;
5375
+ if (msg.date < startupTime - 30) return;
5376
+ if (telegramConfig.allowFrom && telegramConfig.allowFrom.length > 0) {
5377
+ const username = msg.from?.username ? `@${msg.from.username}` : "";
5378
+ const userIdStr = String(msg.from?.id || "");
5379
+ const allowed = telegramConfig.allowFrom.map(String);
5380
+ if (!allowed.some((a) => a === userIdStr || a.toLowerCase() === username.toLowerCase() || a.toLowerCase() === (msg.from?.username || "").toLowerCase())) return;
5381
+ }
5382
+ const processDoc = async () => {
5383
+ if (!this.gateway) return;
5384
+ try {
5385
+ const doc = msg.document;
5386
+ if (!doc) return;
5387
+ if (doc.file_size && doc.file_size > 10 * 1024 * 1024) {
5388
+ await ctx.api.sendMessage(chatId, "File too large (max 10MB).");
5389
+ return;
5390
+ }
5391
+ const file = await bot.api.getFile(doc.file_id);
5392
+ const fileUrl = `https://api.telegram.org/file/bot${telegramConfig.botToken}/${file.file_path}`;
5393
+ const res = await fetch(fileUrl);
5394
+ if (!res.ok) {
5395
+ await ctx.api.sendMessage(chatId, "Could not download file.");
5396
+ return;
5397
+ }
5398
+ const { writeFile: wf } = await import("fs/promises");
5399
+ const { join: join20 } = await import("path");
5400
+ const { tmpdir } = await import("os");
5401
+ const safeName = (doc.file_name || "file").replace(/[^a-zA-Z0-9._-]/g, "_");
5402
+ const savePath = join20(tmpdir(), `clank-upload-${Date.now()}-${safeName}`);
5403
+ await wf(savePath, Buffer.from(await res.arrayBuffer()));
5404
+ const caption = msg.caption || "";
5405
+ const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
5406
+ const response = await this.gateway.handleInboundMessage(
5407
+ { channel: "telegram", peerId: chatId, peerKind: isGroup ? "group" : "dm" },
5408
+ `[File received: "${doc.file_name}" saved to ${savePath}]${caption ? ` Note: ${caption}` : ""}
5409
+
5410
+ You can read this file with the read_file tool.`
5411
+ );
5412
+ if (response) {
5413
+ const chunks = splitMessage(response, 4e3);
5414
+ for (const chunk of chunks) await ctx.api.sendMessage(chatId, chunk);
5415
+ }
5416
+ } catch (err) {
5417
+ await ctx.api.sendMessage(chatId, `Error: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
5418
+ }
5419
+ };
5420
+ const prev = chatLocks.get(chatId) || Promise.resolve();
5421
+ chatLocks.set(chatId, prev.then(processDoc).catch(() => {
5422
+ }));
5423
+ });
5131
5424
  bot.start({
5132
5425
  onStart: () => {
5133
5426
  this.running = true;
@@ -5370,9 +5663,9 @@ var init_web = __esm({
5370
5663
  });
5371
5664
 
5372
5665
  // src/plugins/loader.ts
5373
- import { readdir as readdir6, readFile as readFile9 } from "fs/promises";
5374
- import { existsSync as existsSync6 } from "fs";
5375
- import { join as join12 } from "path";
5666
+ import { readdir as readdir6, readFile as readFile10 } from "fs/promises";
5667
+ import { existsSync as existsSync8 } from "fs";
5668
+ import { join as join13 } from "path";
5376
5669
  import { homedir as homedir2 } from "os";
5377
5670
  var PluginLoader;
5378
5671
  var init_loader = __esm({
@@ -5405,25 +5698,25 @@ var init_loader = __esm({
5405
5698
  /** Discover plugin directories */
5406
5699
  async discoverPlugins() {
5407
5700
  const dirs = [];
5408
- const userPluginDir = join12(homedir2(), ".clank", "plugins");
5409
- if (existsSync6(userPluginDir)) {
5701
+ const userPluginDir = join13(homedir2(), ".clank", "plugins");
5702
+ if (existsSync8(userPluginDir)) {
5410
5703
  try {
5411
5704
  const entries = await readdir6(userPluginDir, { withFileTypes: true });
5412
5705
  for (const entry of entries) {
5413
5706
  if (entry.isDirectory()) {
5414
- dirs.push(join12(userPluginDir, entry.name));
5707
+ dirs.push(join13(userPluginDir, entry.name));
5415
5708
  }
5416
5709
  }
5417
5710
  } catch {
5418
5711
  }
5419
5712
  }
5420
- const nodeModulesDir = join12(process.cwd(), "node_modules");
5421
- if (existsSync6(nodeModulesDir)) {
5713
+ const nodeModulesDir = join13(process.cwd(), "node_modules");
5714
+ if (existsSync8(nodeModulesDir)) {
5422
5715
  try {
5423
5716
  const entries = await readdir6(nodeModulesDir);
5424
5717
  for (const entry of entries) {
5425
5718
  if (entry.startsWith("clank-plugin-")) {
5426
- dirs.push(join12(nodeModulesDir, entry));
5719
+ dirs.push(join13(nodeModulesDir, entry));
5427
5720
  }
5428
5721
  }
5429
5722
  } catch {
@@ -5433,9 +5726,9 @@ var init_loader = __esm({
5433
5726
  }
5434
5727
  /** Load a single plugin from a directory */
5435
5728
  async loadPlugin(dir) {
5436
- const manifestPath = join12(dir, "clank-plugin.json");
5437
- if (!existsSync6(manifestPath)) return null;
5438
- const raw = await readFile9(manifestPath, "utf-8");
5729
+ const manifestPath = join13(dir, "clank-plugin.json");
5730
+ if (!existsSync8(manifestPath)) return null;
5731
+ const raw = await readFile10(manifestPath, "utf-8");
5439
5732
  const manifest = JSON.parse(raw);
5440
5733
  if (!manifest.name) return null;
5441
5734
  const plugin = {
@@ -5447,7 +5740,7 @@ var init_loader = __esm({
5447
5740
  if (manifest.tools) {
5448
5741
  for (const toolEntry of manifest.tools) {
5449
5742
  try {
5450
- const entrypoint = join12(dir, toolEntry.entrypoint);
5743
+ const entrypoint = join13(dir, toolEntry.entrypoint);
5451
5744
  const mod = await import(entrypoint);
5452
5745
  const tool = mod.default || mod.tool;
5453
5746
  if (tool) {
@@ -5461,7 +5754,7 @@ var init_loader = __esm({
5461
5754
  if (manifest.hooks) {
5462
5755
  for (const hookEntry of manifest.hooks) {
5463
5756
  try {
5464
- const handlerPath = join12(dir, hookEntry.handler);
5757
+ const handlerPath = join13(dir, hookEntry.handler);
5465
5758
  const mod = await import(handlerPath);
5466
5759
  const handler = mod.default || mod.handler;
5467
5760
  if (handler) {
@@ -5519,8 +5812,8 @@ var init_plugins = __esm({
5519
5812
  // src/gateway/server.ts
5520
5813
  import { createServer } from "http";
5521
5814
  import { WebSocketServer, WebSocket } from "ws";
5522
- import { readFile as readFile10 } from "fs/promises";
5523
- import { join as join13, dirname as dirname2 } from "path";
5815
+ import { readFile as readFile11 } from "fs/promises";
5816
+ import { join as join14, dirname as dirname2 } from "path";
5524
5817
  import { fileURLToPath as fileURLToPath2 } from "url";
5525
5818
  var GatewayServer;
5526
5819
  var init_server = __esm({
@@ -5554,12 +5847,18 @@ var init_server = __esm({
5554
5847
  pluginLoader;
5555
5848
  adapters = [];
5556
5849
  running = false;
5850
+ /** Rate limiting: track message timestamps per session */
5851
+ rateLimiter = /* @__PURE__ */ new Map();
5852
+ RATE_LIMIT_WINDOW = 6e4;
5853
+ // 1 minute
5854
+ RATE_LIMIT_MAX = 20;
5855
+ // max 20 messages per minute per session
5557
5856
  constructor(config) {
5558
5857
  this.config = config;
5559
- this.sessionStore = new SessionStore(join13(getConfigDir(), "conversations"));
5858
+ this.sessionStore = new SessionStore(join14(getConfigDir(), "conversations"));
5560
5859
  this.toolRegistry = createFullRegistry();
5561
- this.memoryManager = new MemoryManager(join13(getConfigDir(), "memory"));
5562
- this.cronScheduler = new CronScheduler(join13(getConfigDir(), "cron"));
5860
+ this.memoryManager = new MemoryManager(join14(getConfigDir(), "memory"));
5861
+ this.cronScheduler = new CronScheduler(join14(getConfigDir(), "cron"));
5563
5862
  this.configWatcher = new ConfigWatcher();
5564
5863
  this.pluginLoader = new PluginLoader();
5565
5864
  }
@@ -5629,6 +5928,10 @@ var init_server = __esm({
5629
5928
  * This is the main entry point for all non-WebSocket messages.
5630
5929
  */
5631
5930
  async handleInboundMessage(context, text) {
5931
+ const rlKey = deriveSessionKey(context);
5932
+ if (this.isRateLimited(rlKey)) {
5933
+ throw new Error("Rate limited \u2014 too many messages. Wait a moment.");
5934
+ }
5632
5935
  const agentId = resolveRoute(
5633
5936
  context,
5634
5937
  [],
@@ -5640,6 +5943,51 @@ var init_server = __esm({
5640
5943
  const engine = await this.getOrCreateEngine(sessionKey, agentId, context.channel);
5641
5944
  return engine.sendMessage(text);
5642
5945
  }
5946
+ /**
5947
+ * Handle an inbound message with streaming callbacks.
5948
+ * Used by channel adapters for real-time streaming (e.g., Telegram message editing).
5949
+ */
5950
+ async handleInboundMessageStreaming(context, text, callbacks) {
5951
+ const agentId = resolveRoute(
5952
+ context,
5953
+ [],
5954
+ this.config.agents.list.map((a) => ({ id: a.id, name: a.name })),
5955
+ this.config.agents.list[0]?.id || "default"
5956
+ );
5957
+ const sessionKey = deriveSessionKey(context);
5958
+ const engine = await this.getOrCreateEngine(sessionKey, agentId, context.channel);
5959
+ const listeners = [];
5960
+ if (callbacks.onToken) {
5961
+ const fn = (data) => callbacks.onToken(data.content);
5962
+ engine.on("token", fn);
5963
+ listeners.push(["token", fn]);
5964
+ }
5965
+ if (callbacks.onToolStart) {
5966
+ const fn = (data) => callbacks.onToolStart(data.name);
5967
+ engine.on("tool-start", fn);
5968
+ listeners.push(["tool-start", fn]);
5969
+ }
5970
+ if (callbacks.onToolResult) {
5971
+ const fn = (data) => {
5972
+ const d = data;
5973
+ callbacks.onToolResult(d.name, d.success);
5974
+ };
5975
+ engine.on("tool-result", fn);
5976
+ listeners.push(["tool-result", fn]);
5977
+ }
5978
+ if (callbacks.onError) {
5979
+ const fn = (data) => callbacks.onError(data.message);
5980
+ engine.on("error", fn);
5981
+ listeners.push(["error", fn]);
5982
+ }
5983
+ try {
5984
+ return await engine.sendMessage(text);
5985
+ } finally {
5986
+ for (const [event, fn] of listeners) {
5987
+ engine.removeListener(event, fn);
5988
+ }
5989
+ }
5990
+ }
5643
5991
  /** Stop the gateway server */
5644
5992
  async stop() {
5645
5993
  this.running = false;
@@ -5673,7 +6021,7 @@ var init_server = __esm({
5673
6021
  res.writeHead(200, { "Content-Type": "application/json" });
5674
6022
  res.end(JSON.stringify({
5675
6023
  status: "ok",
5676
- version: "1.3.0",
6024
+ version: "1.4.0",
5677
6025
  uptime: process.uptime(),
5678
6026
  clients: this.clients.size,
5679
6027
  agents: this.engines.size
@@ -5703,14 +6051,14 @@ var init_server = __esm({
5703
6051
  if (url === "/chat" || url === "/") {
5704
6052
  try {
5705
6053
  const __dirname4 = dirname2(fileURLToPath2(import.meta.url));
5706
- const htmlPath = join13(__dirname4, "..", "web", "index.html");
5707
- const html = await readFile10(htmlPath, "utf-8");
6054
+ const htmlPath = join14(__dirname4, "..", "web", "index.html");
6055
+ const html = await readFile11(htmlPath, "utf-8");
5708
6056
  res.writeHead(200, { "Content-Type": "text/html" });
5709
6057
  res.end(html);
5710
6058
  return;
5711
6059
  } catch {
5712
6060
  try {
5713
- const html = await readFile10(join13(process.cwd(), "src", "web", "index.html"), "utf-8");
6061
+ const html = await readFile11(join14(process.cwd(), "src", "web", "index.html"), "utf-8");
5714
6062
  res.writeHead(200, { "Content-Type": "text/html" });
5715
6063
  res.end(html);
5716
6064
  return;
@@ -5781,7 +6129,7 @@ var init_server = __esm({
5781
6129
  const hello = {
5782
6130
  type: "hello",
5783
6131
  protocol: PROTOCOL_VERSION,
5784
- version: "1.3.0",
6132
+ version: "1.4.0",
5785
6133
  agents: this.config.agents.list.map((a) => ({
5786
6134
  id: a.id,
5787
6135
  name: a.name || a.id,
@@ -5955,6 +6303,10 @@ var init_server = __esm({
5955
6303
  this.sendResponse(client, frame.id, false, void 0, "message is required");
5956
6304
  return;
5957
6305
  }
6306
+ if (this.isRateLimited(client.sessionKey)) {
6307
+ this.sendResponse(client, frame.id, false, void 0, "Rate limited \u2014 too many messages. Wait a moment.");
6308
+ return;
6309
+ }
5958
6310
  try {
5959
6311
  const engine = await this.getOrCreateEngine(client.sessionKey, client.agentId, client.clientName);
5960
6312
  const cleanup = this.wireEngineEvents(engine, client);
@@ -5969,6 +6321,15 @@ var init_server = __esm({
5969
6321
  this.sendResponse(client, frame.id, false, void 0, msg);
5970
6322
  }
5971
6323
  }
6324
+ /** Check if a session is rate limited */
6325
+ isRateLimited(sessionKey) {
6326
+ const now = Date.now();
6327
+ const timestamps = this.rateLimiter.get(sessionKey) || [];
6328
+ const recent = timestamps.filter((t) => now - t < this.RATE_LIMIT_WINDOW);
6329
+ recent.push(now);
6330
+ this.rateLimiter.set(sessionKey, recent);
6331
+ return recent.length > this.RATE_LIMIT_MAX;
6332
+ }
5972
6333
  /** Cancel current request for a client */
5973
6334
  handleCancel(client) {
5974
6335
  const engine = this.engines.get(client.sessionKey);
@@ -5994,10 +6355,14 @@ var init_server = __esm({
5994
6355
  toolTier: agentConfig?.toolTier || this.config.agents.defaults.toolTier || "auto",
5995
6356
  tools: agentConfig?.tools
5996
6357
  };
6358
+ const compact = agentConfig?.compactPrompt ?? this.config.agents.defaults.compactPrompt ?? false;
6359
+ const thinking = agentConfig?.thinking ?? this.config.agents.defaults.thinking ?? "auto";
5997
6360
  const systemPrompt = await buildSystemPrompt({
5998
6361
  identity,
5999
6362
  workspaceDir: identity.workspace,
6000
- channel
6363
+ channel,
6364
+ compact,
6365
+ thinking
6001
6366
  });
6002
6367
  const memoryBlock = await this.memoryManager.buildMemoryBlock("", identity.workspace);
6003
6368
  const fullPrompt = memoryBlock ? systemPrompt + "\n\n---\n\n" + memoryBlock : systemPrompt;
@@ -6112,9 +6477,9 @@ __export(gateway_cmd_exports, {
6112
6477
  isGatewayRunning: () => isGatewayRunning
6113
6478
  });
6114
6479
  import { fork } from "child_process";
6115
- import { writeFile as writeFile7, readFile as readFile11, unlink as unlink3 } from "fs/promises";
6116
- import { existsSync as existsSync7 } from "fs";
6117
- import { join as join14, dirname as dirname3 } from "path";
6480
+ import { writeFile as writeFile8, readFile as readFile12, unlink as unlink3 } from "fs/promises";
6481
+ import { existsSync as existsSync9 } from "fs";
6482
+ import { join as join15, dirname as dirname3 } from "path";
6118
6483
  import { fileURLToPath as fileURLToPath3 } from "url";
6119
6484
  async function isGatewayRunning(port) {
6120
6485
  const config = await loadConfig();
@@ -6127,7 +6492,7 @@ async function isGatewayRunning(port) {
6127
6492
  }
6128
6493
  }
6129
6494
  function pidFilePath() {
6130
- return join14(getConfigDir(), "gateway.pid");
6495
+ return join15(getConfigDir(), "gateway.pid");
6131
6496
  }
6132
6497
  async function gatewayStartForeground(opts) {
6133
6498
  await ensureConfigDir();
@@ -6139,7 +6504,7 @@ async function gatewayStartForeground(opts) {
6139
6504
  console.log(green2(` Gateway already running on port ${config.gateway.port}`));
6140
6505
  return;
6141
6506
  }
6142
- await writeFile7(pidFilePath(), String(process.pid), "utf-8");
6507
+ await writeFile8(pidFilePath(), String(process.pid), "utf-8");
6143
6508
  const server = new GatewayServer(config);
6144
6509
  const shutdown = async () => {
6145
6510
  console.log(dim2("\nShutting down..."));
@@ -6172,10 +6537,10 @@ async function gatewayStartBackground() {
6172
6537
  return true;
6173
6538
  }
6174
6539
  console.log(dim2(" Starting gateway in background..."));
6175
- const entryPoint = join14(dirname3(__filename2), "index.js");
6176
- const logFile = join14(getConfigDir(), "logs", "gateway.log");
6540
+ const entryPoint = join15(dirname3(__filename2), "index.js");
6541
+ const logFile = join15(getConfigDir(), "logs", "gateway.log");
6177
6542
  const { mkdir: mkdir7 } = await import("fs/promises");
6178
- await mkdir7(join14(getConfigDir(), "logs"), { recursive: true });
6543
+ await mkdir7(join15(getConfigDir(), "logs"), { recursive: true });
6179
6544
  const child = fork(entryPoint, ["gateway", "start", "--foreground"], {
6180
6545
  detached: true,
6181
6546
  stdio: ["ignore", "ignore", "ignore", "ipc"]
@@ -6204,9 +6569,9 @@ async function gatewayStart(opts) {
6204
6569
  }
6205
6570
  async function gatewayStop() {
6206
6571
  const pidPath = pidFilePath();
6207
- if (existsSync7(pidPath)) {
6572
+ if (existsSync9(pidPath)) {
6208
6573
  try {
6209
- const pid = parseInt(await readFile11(pidPath, "utf-8"), 10);
6574
+ const pid = parseInt(await readFile12(pidPath, "utf-8"), 10);
6210
6575
  process.kill(pid, "SIGTERM");
6211
6576
  await unlink3(pidPath);
6212
6577
  console.log(green2("Gateway stopped"));
@@ -6237,8 +6602,8 @@ async function gatewayStatus() {
6237
6602
  console.log(dim2(` Clients: ${data.clients?.length || 0}`));
6238
6603
  console.log(dim2(` Sessions: ${data.sessions?.length || 0}`));
6239
6604
  const pidPath = pidFilePath();
6240
- if (existsSync7(pidPath)) {
6241
- const pid = await readFile11(pidPath, "utf-8");
6605
+ if (existsSync9(pidPath)) {
6606
+ const pid = await readFile12(pidPath, "utf-8");
6242
6607
  console.log(dim2(` PID: ${pid.trim()}`));
6243
6608
  }
6244
6609
  } else {
@@ -6266,8 +6631,8 @@ var init_gateway_cmd = __esm({
6266
6631
  });
6267
6632
 
6268
6633
  // src/daemon/install.ts
6269
- import { writeFile as writeFile8, mkdir as mkdir6, unlink as unlink4 } from "fs/promises";
6270
- import { join as join15 } from "path";
6634
+ import { writeFile as writeFile9, mkdir as mkdir6, unlink as unlink4 } from "fs/promises";
6635
+ import { join as join16 } from "path";
6271
6636
  import { homedir as homedir3, platform as platform4 } from "os";
6272
6637
  import { execSync } from "child_process";
6273
6638
  async function installDaemon() {
@@ -6335,8 +6700,8 @@ async function daemonStatus() {
6335
6700
  }
6336
6701
  }
6337
6702
  async function installLaunchd() {
6338
- const plistDir = join15(homedir3(), "Library", "LaunchAgents");
6339
- const plistPath = join15(plistDir, "com.clank.gateway.plist");
6703
+ const plistDir = join16(homedir3(), "Library", "LaunchAgents");
6704
+ const plistPath = join16(plistDir, "com.clank.gateway.plist");
6340
6705
  const clankPath = execSync("which clank || echo clank", { encoding: "utf-8" }).trim();
6341
6706
  await mkdir6(plistDir, { recursive: true });
6342
6707
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
@@ -6357,18 +6722,18 @@ async function installLaunchd() {
6357
6722
  <key>KeepAlive</key>
6358
6723
  <true/>
6359
6724
  <key>StandardOutPath</key>
6360
- <string>${join15(homedir3(), ".clank", "logs", "gateway.log")}</string>
6725
+ <string>${join16(homedir3(), ".clank", "logs", "gateway.log")}</string>
6361
6726
  <key>StandardErrorPath</key>
6362
- <string>${join15(homedir3(), ".clank", "logs", "gateway-error.log")}</string>
6727
+ <string>${join16(homedir3(), ".clank", "logs", "gateway-error.log")}</string>
6363
6728
  </dict>
6364
6729
  </plist>`;
6365
- await writeFile8(plistPath, plist, "utf-8");
6730
+ await writeFile9(plistPath, plist, "utf-8");
6366
6731
  execSync(`launchctl load "${plistPath}"`);
6367
6732
  console.log(green3("Daemon installed (launchd)"));
6368
6733
  console.log(dim3(` Plist: ${plistPath}`));
6369
6734
  }
6370
6735
  async function uninstallLaunchd() {
6371
- const plistPath = join15(homedir3(), "Library", "LaunchAgents", "com.clank.gateway.plist");
6736
+ const plistPath = join16(homedir3(), "Library", "LaunchAgents", "com.clank.gateway.plist");
6372
6737
  try {
6373
6738
  execSync(`launchctl unload "${plistPath}"`);
6374
6739
  await unlink4(plistPath);
@@ -6406,8 +6771,8 @@ async function uninstallTaskScheduler() {
6406
6771
  }
6407
6772
  }
6408
6773
  async function installSystemd() {
6409
- const unitDir = join15(homedir3(), ".config", "systemd", "user");
6410
- const unitPath = join15(unitDir, "clank-gateway.service");
6774
+ const unitDir = join16(homedir3(), ".config", "systemd", "user");
6775
+ const unitPath = join16(unitDir, "clank-gateway.service");
6411
6776
  const clankPath = execSync("which clank || echo clank", { encoding: "utf-8" }).trim();
6412
6777
  await mkdir6(unitDir, { recursive: true });
6413
6778
  const unit = `[Unit]
@@ -6422,7 +6787,7 @@ RestartSec=5
6422
6787
  [Install]
6423
6788
  WantedBy=default.target
6424
6789
  `;
6425
- await writeFile8(unitPath, unit, "utf-8");
6790
+ await writeFile9(unitPath, unit, "utf-8");
6426
6791
  execSync("systemctl --user daemon-reload");
6427
6792
  execSync("systemctl --user enable clank-gateway");
6428
6793
  execSync("systemctl --user start clank-gateway");
@@ -6433,7 +6798,7 @@ async function uninstallSystemd() {
6433
6798
  try {
6434
6799
  execSync("systemctl --user stop clank-gateway");
6435
6800
  execSync("systemctl --user disable clank-gateway");
6436
- const unitPath = join15(homedir3(), ".config", "systemd", "user", "clank-gateway.service");
6801
+ const unitPath = join16(homedir3(), ".config", "systemd", "user", "clank-gateway.service");
6437
6802
  await unlink4(unitPath);
6438
6803
  execSync("systemctl --user daemon-reload");
6439
6804
  console.log(green3("Daemon uninstalled"));
@@ -6474,7 +6839,7 @@ __export(setup_exports, {
6474
6839
  });
6475
6840
  import { createInterface as createInterface2 } from "readline";
6476
6841
  import { randomBytes } from "crypto";
6477
- import { dirname as dirname4, join as join16 } from "path";
6842
+ import { dirname as dirname4, join as join17 } from "path";
6478
6843
  import { fileURLToPath as fileURLToPath4 } from "url";
6479
6844
  function ask(rl, question) {
6480
6845
  return new Promise((resolve4) => rl.question(question, resolve4));
@@ -6577,8 +6942,8 @@ async function runSetup(opts) {
6577
6942
  console.log("");
6578
6943
  console.log(dim4(" Creating workspace..."));
6579
6944
  const { ensureWorkspaceFiles: ensureWorkspaceFiles2 } = await Promise.resolve().then(() => (init_system_prompt(), system_prompt_exports));
6580
- const templateDir = join16(__dirname2, "..", "workspace", "templates");
6581
- const wsDir = join16(getConfigDir(), "workspace");
6945
+ const templateDir = join17(__dirname2, "..", "workspace", "templates");
6946
+ const wsDir = join17(getConfigDir(), "workspace");
6582
6947
  try {
6583
6948
  await ensureWorkspaceFiles2(wsDir, templateDir);
6584
6949
  } catch {
@@ -6642,27 +7007,35 @@ async function runSetup(opts) {
6642
7007
  console.log(green4(" ElevenLabs configured (TTS available)"));
6643
7008
  }
6644
7009
  }
6645
- const addWhisper = await ask(rl, cyan2(" Set up speech-to-text (Whisper)? [y/N] "));
7010
+ const addWhisper = await ask(rl, cyan2(" Set up speech-to-text (voice messages)? [y/N] "));
6646
7011
  if (addWhisper.toLowerCase() === "y") {
6647
- console.log(dim4(" 1. OpenAI Whisper API (cloud, uses OpenAI key)"));
6648
- console.log(dim4(" 2. Local whisper.cpp (requires whisper installed)"));
7012
+ console.log(dim4(" 1. Groq (recommended \u2014 free, fast)"));
7013
+ console.log(dim4(" 2. OpenAI Whisper API (paid, uses OpenAI key)"));
7014
+ console.log(dim4(" 3. Local whisper.cpp (requires manual install)"));
6649
7015
  const whisperChoice = await ask(rl, cyan2(" Choice [1]: "));
6650
- if (whisperChoice === "2") {
7016
+ if (whisperChoice === "3") {
6651
7017
  config.integrations.whisper = { enabled: true, provider: "local" };
6652
7018
  console.log(green4(" Local whisper.cpp configured"));
6653
7019
  console.log(dim4(" Make sure whisper is installed and in PATH"));
6654
- } else {
7020
+ } else if (whisperChoice === "2") {
6655
7021
  const existingKey = config.models.providers.openai?.apiKey;
6656
7022
  if (existingKey) {
6657
7023
  config.integrations.whisper = { enabled: true, provider: "openai", apiKey: existingKey };
6658
7024
  console.log(green4(" Whisper configured (using existing OpenAI key)"));
6659
7025
  } else {
6660
- const key = await ask(rl, cyan2(" OpenAI API key for Whisper: "));
7026
+ const key = await ask(rl, cyan2(" OpenAI API key: "));
6661
7027
  if (key.trim()) {
6662
7028
  config.integrations.whisper = { enabled: true, provider: "openai", apiKey: key.trim() };
6663
7029
  console.log(green4(" Whisper configured"));
6664
7030
  }
6665
7031
  }
7032
+ } else {
7033
+ console.log(dim4(" Get a free API key at: https://console.groq.com/keys"));
7034
+ const key = await ask(rl, cyan2(" Groq API key: "));
7035
+ if (key.trim()) {
7036
+ config.integrations.whisper = { enabled: true, provider: "groq", apiKey: key.trim() };
7037
+ console.log(green4(" Groq Whisper configured (free, fast)"));
7038
+ }
6666
7039
  }
6667
7040
  }
6668
7041
  if (isAdvanced) {
@@ -6731,9 +7104,9 @@ var fix_exports = {};
6731
7104
  __export(fix_exports, {
6732
7105
  runFix: () => runFix
6733
7106
  });
6734
- import { existsSync as existsSync8 } from "fs";
7107
+ import { existsSync as existsSync10 } from "fs";
6735
7108
  import { readdir as readdir7 } from "fs/promises";
6736
- import { join as join17 } from "path";
7109
+ import { join as join18 } from "path";
6737
7110
  async function runFix(opts) {
6738
7111
  console.log("");
6739
7112
  console.log(" Clank Diagnostics");
@@ -6763,7 +7136,7 @@ async function runFix(opts) {
6763
7136
  }
6764
7137
  async function checkConfig() {
6765
7138
  const configPath = getConfigPath();
6766
- if (!existsSync8(configPath)) {
7139
+ if (!existsSync10(configPath)) {
6767
7140
  return {
6768
7141
  name: "Config",
6769
7142
  status: "warn",
@@ -6831,8 +7204,8 @@ async function checkModels() {
6831
7204
  return { name: "Model (primary)", status: "ok", message: modelId };
6832
7205
  }
6833
7206
  async function checkSessions() {
6834
- const sessDir = join17(getConfigDir(), "conversations");
6835
- if (!existsSync8(sessDir)) {
7207
+ const sessDir = join18(getConfigDir(), "conversations");
7208
+ if (!existsSync10(sessDir)) {
6836
7209
  return { name: "Sessions", status: "ok", message: "no sessions yet" };
6837
7210
  }
6838
7211
  try {
@@ -6844,8 +7217,8 @@ async function checkSessions() {
6844
7217
  }
6845
7218
  }
6846
7219
  async function checkWorkspace() {
6847
- const wsDir = join17(getConfigDir(), "workspace");
6848
- if (!existsSync8(wsDir)) {
7220
+ const wsDir = join18(getConfigDir(), "workspace");
7221
+ if (!existsSync10(wsDir)) {
6849
7222
  return {
6850
7223
  name: "Workspace",
6851
7224
  status: "warn",
@@ -7138,7 +7511,7 @@ async function runTui(opts) {
7138
7511
  ws.on("open", () => {
7139
7512
  ws.send(JSON.stringify({
7140
7513
  type: "connect",
7141
- params: { auth: { token }, mode: "tui", version: "1.3.0" }
7514
+ params: { auth: { token }, mode: "tui", version: "1.4.0" }
7142
7515
  }));
7143
7516
  });
7144
7517
  ws.on("message", (data) => {
@@ -7487,7 +7860,7 @@ __export(uninstall_exports, {
7487
7860
  });
7488
7861
  import { createInterface as createInterface4 } from "readline";
7489
7862
  import { rm } from "fs/promises";
7490
- import { existsSync as existsSync9 } from "fs";
7863
+ import { existsSync as existsSync11 } from "fs";
7491
7864
  async function runUninstall(opts) {
7492
7865
  const configDir = getConfigDir();
7493
7866
  console.log("");
@@ -7526,7 +7899,7 @@ async function runUninstall(opts) {
7526
7899
  } catch {
7527
7900
  }
7528
7901
  console.log(dim10(" Deleting data..."));
7529
- if (existsSync9(configDir)) {
7902
+ if (existsSync11(configDir)) {
7530
7903
  await rm(configDir, { recursive: true, force: true });
7531
7904
  console.log(green10(` Removed ${configDir}`));
7532
7905
  } else {
@@ -7564,12 +7937,12 @@ init_esm_shims();
7564
7937
  import { Command } from "commander";
7565
7938
  import { readFileSync } from "fs";
7566
7939
  import { fileURLToPath as fileURLToPath5 } from "url";
7567
- import { dirname as dirname5, join as join18 } from "path";
7940
+ import { dirname as dirname5, join as join19 } from "path";
7568
7941
  var __filename3 = fileURLToPath5(import.meta.url);
7569
7942
  var __dirname3 = dirname5(__filename3);
7570
- var version = "1.3.0";
7943
+ var version = "1.4.0";
7571
7944
  try {
7572
- const pkg = JSON.parse(readFileSync(join18(__dirname3, "..", "package.json"), "utf-8"));
7945
+ const pkg = JSON.parse(readFileSync(join19(__dirname3, "..", "package.json"), "utf-8"));
7573
7946
  version = pkg.version;
7574
7947
  } catch {
7575
7948
  }
@@ -7678,10 +8051,10 @@ pipeline.command("status <id>").description("Check pipeline execution status").a
7678
8051
  });
7679
8052
  var cron = program.command("cron").description("Manage scheduled jobs");
7680
8053
  cron.command("list").description("List cron jobs").action(async () => {
7681
- const { join: join19 } = await import("path");
8054
+ const { join: join20 } = await import("path");
7682
8055
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7683
8056
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
7684
- const scheduler = new CronScheduler2(join19(getConfigDir3(), "cron"));
8057
+ const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
7685
8058
  await scheduler.init();
7686
8059
  const jobs = scheduler.listJobs();
7687
8060
  if (jobs.length === 0) {
@@ -7693,10 +8066,10 @@ cron.command("list").description("List cron jobs").action(async () => {
7693
8066
  }
7694
8067
  });
7695
8068
  cron.command("add").description("Add a cron job").requiredOption("--schedule <expr>", "Schedule (e.g., '1h', '30m', 'daily')").requiredOption("--prompt <text>", "What the agent should do").option("--name <name>", "Job name").option("--agent <id>", "Agent ID", "default").action(async (opts) => {
7696
- const { join: join19 } = await import("path");
8069
+ const { join: join20 } = await import("path");
7697
8070
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7698
8071
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
7699
- const scheduler = new CronScheduler2(join19(getConfigDir3(), "cron"));
8072
+ const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
7700
8073
  await scheduler.init();
7701
8074
  const job = await scheduler.addJob({
7702
8075
  name: opts.name || "CLI Job",
@@ -7707,10 +8080,10 @@ cron.command("add").description("Add a cron job").requiredOption("--schedule <ex
7707
8080
  console.log(` Job created: ${job.id.slice(0, 8)} \u2014 "${job.name}" every ${job.schedule}`);
7708
8081
  });
7709
8082
  cron.command("remove <id>").description("Remove a cron job").action(async (id) => {
7710
- const { join: join19 } = await import("path");
8083
+ const { join: join20 } = await import("path");
7711
8084
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7712
8085
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
7713
- const scheduler = new CronScheduler2(join19(getConfigDir3(), "cron"));
8086
+ const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
7714
8087
  await scheduler.init();
7715
8088
  const removed = await scheduler.removeJob(id);
7716
8089
  console.log(removed ? ` Job ${id.slice(0, 8)} removed` : ` Job not found`);