@tractorscorch/clank 1.3.1 → 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") {
@@ -4041,12 +4152,12 @@ var init_tts = __esm({
4041
4152
  /** Transcribe via local whisper.cpp */
4042
4153
  async transcribeLocal(audioBuffer, format) {
4043
4154
  try {
4044
- const { writeFile: writeFile9, unlink: unlink5 } = await import("fs/promises");
4155
+ const { writeFile: writeFile10, unlink: unlink5 } = await import("fs/promises");
4045
4156
  const { execSync: execSync3 } = await import("child_process");
4046
- const { join: join19 } = await import("path");
4157
+ const { join: join20 } = await import("path");
4047
4158
  const { tmpdir } = await import("os");
4048
- const tmpFile = join19(tmpdir(), `clank-stt-${Date.now()}.${format}`);
4049
- await writeFile9(tmpFile, audioBuffer);
4159
+ const tmpFile = join20(tmpdir(), `clank-stt-${Date.now()}.${format}`);
4160
+ await writeFile10(tmpFile, audioBuffer);
4050
4161
  const output = execSync3(`whisper "${tmpFile}" --model base.en --output-txt`, {
4051
4162
  encoding: "utf-8",
4052
4163
  timeout: 6e4
@@ -4114,11 +4225,11 @@ var init_voice_tool = __esm({
4114
4225
  voiceId: args.voice_id
4115
4226
  });
4116
4227
  if (!result) return "Error: TTS synthesis failed";
4117
- const { writeFile: writeFile9 } = await import("fs/promises");
4118
- const { join: join19 } = await import("path");
4228
+ const { writeFile: writeFile10 } = await import("fs/promises");
4229
+ const { join: join20 } = await import("path");
4119
4230
  const { tmpdir } = await import("os");
4120
- const outPath = join19(tmpdir(), `clank-tts-${Date.now()}.${result.format}`);
4121
- await writeFile9(outPath, result.audioBuffer);
4231
+ const outPath = join20(tmpdir(), `clank-tts-${Date.now()}.${result.format}`);
4232
+ await writeFile10(outPath, result.audioBuffer);
4122
4233
  return `Audio generated: ${outPath} (${result.format}, ${Math.round(result.audioBuffer.length / 1024)}KB)`;
4123
4234
  }
4124
4235
  };
@@ -4141,16 +4252,16 @@ var init_voice_tool = __esm({
4141
4252
  return { ok: true };
4142
4253
  },
4143
4254
  async execute(args) {
4144
- const { readFile: readFile12 } = await import("fs/promises");
4145
- const { existsSync: existsSync10 } = await import("fs");
4255
+ const { readFile: readFile13 } = await import("fs/promises");
4256
+ const { existsSync: existsSync12 } = await import("fs");
4146
4257
  const filePath = args.file_path;
4147
- if (!existsSync10(filePath)) return `Error: File not found: ${filePath}`;
4258
+ if (!existsSync12(filePath)) return `Error: File not found: ${filePath}`;
4148
4259
  const config = await loadConfig();
4149
4260
  const engine = new STTEngine(config);
4150
4261
  if (!engine.isAvailable()) {
4151
4262
  return "Error: Speech-to-text not configured. Need OpenAI API key or local whisper.cpp installed.";
4152
4263
  }
4153
- const audioBuffer = await readFile12(filePath);
4264
+ const audioBuffer = await readFile13(filePath);
4154
4265
  const ext = filePath.split(".").pop() || "wav";
4155
4266
  const result = await engine.transcribe(audioBuffer, ext);
4156
4267
  if (!result) return "Error: Transcription failed";
@@ -4182,6 +4293,53 @@ var init_voice_tool = __esm({
4182
4293
  }
4183
4294
  });
4184
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
+
4185
4343
  // src/tools/self-config/index.ts
4186
4344
  function registerSelfConfigTools(registry) {
4187
4345
  registry.register(configTool);
@@ -4195,6 +4353,7 @@ function registerSelfConfigTools(registry) {
4195
4353
  registry.register(ttsTool);
4196
4354
  registry.register(sttTool);
4197
4355
  registry.register(voiceListTool);
4356
+ registry.register(fileShareTool);
4198
4357
  }
4199
4358
  var init_self_config = __esm({
4200
4359
  "src/tools/self-config/index.ts"() {
@@ -4209,6 +4368,7 @@ var init_self_config = __esm({
4209
4368
  init_gateway_tool();
4210
4369
  init_message_tool();
4211
4370
  init_voice_tool();
4371
+ init_file_share_tool();
4212
4372
  init_config_tool();
4213
4373
  init_channel_tool();
4214
4374
  init_agent_tool();
@@ -4218,6 +4378,7 @@ var init_self_config = __esm({
4218
4378
  init_gateway_tool();
4219
4379
  init_message_tool();
4220
4380
  init_voice_tool();
4381
+ init_file_share_tool();
4221
4382
  }
4222
4383
  });
4223
4384
 
@@ -4279,7 +4440,7 @@ __export(chat_exports, {
4279
4440
  runChat: () => runChat
4280
4441
  });
4281
4442
  import { createInterface } from "readline";
4282
- import { join as join10 } from "path";
4443
+ import { join as join11 } from "path";
4283
4444
  async function runChat(opts) {
4284
4445
  await ensureConfigDir();
4285
4446
  const config = await loadConfig();
@@ -4296,9 +4457,9 @@ async function runChat(opts) {
4296
4457
  console.log(dim("Starting gateway..."));
4297
4458
  const { fork: fork2 } = await import("child_process");
4298
4459
  const { fileURLToPath: fileURLToPath6 } = await import("url");
4299
- const { dirname: dirname6, join: join19 } = await import("path");
4460
+ const { dirname: dirname6, join: join20 } = await import("path");
4300
4461
  const __filename4 = fileURLToPath6(import.meta.url);
4301
- const entryPoint = join19(dirname6(__filename4), "index.js");
4462
+ const entryPoint = join20(dirname6(__filename4), "index.js");
4302
4463
  const child = fork2(entryPoint, ["gateway", "start", "--foreground"], {
4303
4464
  detached: true,
4304
4465
  stdio: "ignore"
@@ -4346,7 +4507,7 @@ async function runChat(opts) {
4346
4507
  console.error(dim("Make sure Ollama is running or configure a cloud provider in ~/.clank/config.json5"));
4347
4508
  process.exit(1);
4348
4509
  }
4349
- const sessionStore = new SessionStore(join10(getConfigDir(), "conversations"));
4510
+ const sessionStore = new SessionStore(join11(getConfigDir(), "conversations"));
4350
4511
  await sessionStore.init();
4351
4512
  const toolRegistry = createFullRegistry();
4352
4513
  const identity = {
@@ -4504,9 +4665,9 @@ var init_chat = __esm({
4504
4665
  });
4505
4666
 
4506
4667
  // src/memory/memory.ts
4507
- import { readFile as readFile8, writeFile as writeFile6, readdir as readdir5, mkdir as mkdir5 } from "fs/promises";
4508
- import { existsSync as existsSync5 } from "fs";
4509
- 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";
4510
4671
  import { randomUUID as randomUUID3 } from "crypto";
4511
4672
  function tokenize(text) {
4512
4673
  return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3 && !STOPWORDS.has(t));
@@ -4654,12 +4815,12 @@ var init_memory = __esm({
4654
4815
  meta = /* @__PURE__ */ new Map();
4655
4816
  constructor(memoryDir) {
4656
4817
  this.memoryDir = memoryDir;
4657
- this.metaPath = join11(memoryDir, "_meta.json");
4818
+ this.metaPath = join12(memoryDir, "_meta.json");
4658
4819
  }
4659
4820
  /** Initialize — create dirs and load metadata */
4660
4821
  async init() {
4661
4822
  for (const cat of ["identity", "knowledge", "lessons", "context"]) {
4662
- await mkdir5(join11(this.memoryDir, cat), { recursive: true });
4823
+ await mkdir5(join12(this.memoryDir, cat), { recursive: true });
4663
4824
  }
4664
4825
  await this.loadMeta();
4665
4826
  }
@@ -4704,10 +4865,10 @@ var init_memory = __esm({
4704
4865
  let used = 0;
4705
4866
  if (projectRoot) {
4706
4867
  for (const name of [".clank.md", ".clankbuild.md", ".llamabuild.md"]) {
4707
- const path2 = join11(projectRoot, name);
4708
- if (existsSync5(path2)) {
4868
+ const path2 = join12(projectRoot, name);
4869
+ if (existsSync7(path2)) {
4709
4870
  try {
4710
- const content = await readFile8(path2, "utf-8");
4871
+ const content = await readFile9(path2, "utf-8");
4711
4872
  if (content.trim() && used + content.length < budgetChars) {
4712
4873
  parts.push("## Project Memory\n" + content.trim());
4713
4874
  used += content.length;
@@ -4718,10 +4879,10 @@ var init_memory = __esm({
4718
4879
  }
4719
4880
  }
4720
4881
  }
4721
- const globalPath = join11(this.memoryDir, "..", "workspace", "MEMORY.md");
4722
- if (existsSync5(globalPath)) {
4882
+ const globalPath = join12(this.memoryDir, "..", "workspace", "MEMORY.md");
4883
+ if (existsSync7(globalPath)) {
4723
4884
  try {
4724
- const content = await readFile8(globalPath, "utf-8");
4885
+ const content = await readFile9(globalPath, "utf-8");
4725
4886
  if (content.trim() && used + content.length < budgetChars) {
4726
4887
  parts.push("## Global Memory\n" + content.trim());
4727
4888
  used += content.length;
@@ -4742,8 +4903,8 @@ ${entry.content}`);
4742
4903
  async add(category, title, content) {
4743
4904
  const id = randomUUID3();
4744
4905
  const filename = `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 50)}.md`;
4745
- const filePath = join11(this.memoryDir, category, filename);
4746
- await writeFile6(filePath, `# ${title}
4906
+ const filePath = join12(this.memoryDir, category, filename);
4907
+ await writeFile7(filePath, `# ${title}
4747
4908
 
4748
4909
  ${content}`, "utf-8");
4749
4910
  this.meta.set(id, {
@@ -4788,15 +4949,15 @@ ${content}`, "utf-8");
4788
4949
  async loadAll() {
4789
4950
  const entries = [];
4790
4951
  for (const category of ["identity", "knowledge", "lessons", "context"]) {
4791
- const dir = join11(this.memoryDir, category);
4792
- if (!existsSync5(dir)) continue;
4952
+ const dir = join12(this.memoryDir, category);
4953
+ if (!existsSync7(dir)) continue;
4793
4954
  try {
4794
4955
  const files = await readdir5(dir);
4795
4956
  for (const file of files) {
4796
4957
  if (!file.endsWith(".md")) continue;
4797
- const filePath = join11(dir, file);
4958
+ const filePath = join12(dir, file);
4798
4959
  try {
4799
- const content = await readFile8(filePath, "utf-8");
4960
+ const content = await readFile9(filePath, "utf-8");
4800
4961
  const title = content.split("\n")[0]?.replace(/^#\s*/, "") || file;
4801
4962
  entries.push({
4802
4963
  id: file,
@@ -4821,9 +4982,9 @@ ${content}`, "utf-8");
4821
4982
  return recency * frequencyBoost;
4822
4983
  }
4823
4984
  async loadMeta() {
4824
- if (!existsSync5(this.metaPath)) return;
4985
+ if (!existsSync7(this.metaPath)) return;
4825
4986
  try {
4826
- const raw = await readFile8(this.metaPath, "utf-8");
4987
+ const raw = await readFile9(this.metaPath, "utf-8");
4827
4988
  const entries = JSON.parse(raw);
4828
4989
  for (const e of entries) this.meta.set(e.id, e);
4829
4990
  } catch {
@@ -4831,7 +4992,7 @@ ${content}`, "utf-8");
4831
4992
  }
4832
4993
  async saveMeta() {
4833
4994
  const entries = Array.from(this.meta.values());
4834
- await writeFile6(this.metaPath, JSON.stringify(entries, null, 2), "utf-8");
4995
+ await writeFile7(this.metaPath, JSON.stringify(entries, null, 2), "utf-8");
4835
4996
  }
4836
4997
  };
4837
4998
  }
@@ -4843,6 +5004,7 @@ var init_memory2 = __esm({
4843
5004
  "use strict";
4844
5005
  init_esm_shims();
4845
5006
  init_memory();
5007
+ init_auto_persist();
4846
5008
  }
4847
5009
  });
4848
5010
 
@@ -5038,15 +5200,53 @@ var init_telegram = __esm({
5038
5200
  if (!this.gateway) return;
5039
5201
  try {
5040
5202
  await ctx.api.sendChatAction(chatId, "typing");
5041
- 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(
5042
5208
  {
5043
5209
  channel: "telegram",
5044
5210
  peerId: chatId,
5045
5211
  peerKind: isGroup ? "group" : "dm"
5046
5212
  },
5047
- 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
+ }
5048
5244
  );
5049
- 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) {
5050
5250
  const chunks = splitMessage(response, 4e3);
5051
5251
  for (const chunk of chunks) {
5052
5252
  await ctx.api.sendMessage(chatId, chunk);
@@ -5110,7 +5310,8 @@ var init_telegram = __esm({
5110
5310
  const { TTSEngine: TTSEngine2 } = await Promise.resolve().then(() => (init_voice(), voice_exports));
5111
5311
  const tts = new TTSEngine2(config);
5112
5312
  if (tts.isAvailable() && response.length < 2e3) {
5113
- 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 });
5114
5315
  if (audio) {
5115
5316
  const { InputFile } = await import("grammy");
5116
5317
  await ctx.api.sendVoice(chatId, new InputFile(audio.audioBuffer, "reply.mp3"));
@@ -5132,6 +5333,94 @@ var init_telegram = __esm({
5132
5333
  });
5133
5334
  chatLocks.set(chatId, next);
5134
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
+ });
5135
5424
  bot.start({
5136
5425
  onStart: () => {
5137
5426
  this.running = true;
@@ -5374,9 +5663,9 @@ var init_web = __esm({
5374
5663
  });
5375
5664
 
5376
5665
  // src/plugins/loader.ts
5377
- import { readdir as readdir6, readFile as readFile9 } from "fs/promises";
5378
- import { existsSync as existsSync6 } from "fs";
5379
- 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";
5380
5669
  import { homedir as homedir2 } from "os";
5381
5670
  var PluginLoader;
5382
5671
  var init_loader = __esm({
@@ -5409,25 +5698,25 @@ var init_loader = __esm({
5409
5698
  /** Discover plugin directories */
5410
5699
  async discoverPlugins() {
5411
5700
  const dirs = [];
5412
- const userPluginDir = join12(homedir2(), ".clank", "plugins");
5413
- if (existsSync6(userPluginDir)) {
5701
+ const userPluginDir = join13(homedir2(), ".clank", "plugins");
5702
+ if (existsSync8(userPluginDir)) {
5414
5703
  try {
5415
5704
  const entries = await readdir6(userPluginDir, { withFileTypes: true });
5416
5705
  for (const entry of entries) {
5417
5706
  if (entry.isDirectory()) {
5418
- dirs.push(join12(userPluginDir, entry.name));
5707
+ dirs.push(join13(userPluginDir, entry.name));
5419
5708
  }
5420
5709
  }
5421
5710
  } catch {
5422
5711
  }
5423
5712
  }
5424
- const nodeModulesDir = join12(process.cwd(), "node_modules");
5425
- if (existsSync6(nodeModulesDir)) {
5713
+ const nodeModulesDir = join13(process.cwd(), "node_modules");
5714
+ if (existsSync8(nodeModulesDir)) {
5426
5715
  try {
5427
5716
  const entries = await readdir6(nodeModulesDir);
5428
5717
  for (const entry of entries) {
5429
5718
  if (entry.startsWith("clank-plugin-")) {
5430
- dirs.push(join12(nodeModulesDir, entry));
5719
+ dirs.push(join13(nodeModulesDir, entry));
5431
5720
  }
5432
5721
  }
5433
5722
  } catch {
@@ -5437,9 +5726,9 @@ var init_loader = __esm({
5437
5726
  }
5438
5727
  /** Load a single plugin from a directory */
5439
5728
  async loadPlugin(dir) {
5440
- const manifestPath = join12(dir, "clank-plugin.json");
5441
- if (!existsSync6(manifestPath)) return null;
5442
- 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");
5443
5732
  const manifest = JSON.parse(raw);
5444
5733
  if (!manifest.name) return null;
5445
5734
  const plugin = {
@@ -5451,7 +5740,7 @@ var init_loader = __esm({
5451
5740
  if (manifest.tools) {
5452
5741
  for (const toolEntry of manifest.tools) {
5453
5742
  try {
5454
- const entrypoint = join12(dir, toolEntry.entrypoint);
5743
+ const entrypoint = join13(dir, toolEntry.entrypoint);
5455
5744
  const mod = await import(entrypoint);
5456
5745
  const tool = mod.default || mod.tool;
5457
5746
  if (tool) {
@@ -5465,7 +5754,7 @@ var init_loader = __esm({
5465
5754
  if (manifest.hooks) {
5466
5755
  for (const hookEntry of manifest.hooks) {
5467
5756
  try {
5468
- const handlerPath = join12(dir, hookEntry.handler);
5757
+ const handlerPath = join13(dir, hookEntry.handler);
5469
5758
  const mod = await import(handlerPath);
5470
5759
  const handler = mod.default || mod.handler;
5471
5760
  if (handler) {
@@ -5523,8 +5812,8 @@ var init_plugins = __esm({
5523
5812
  // src/gateway/server.ts
5524
5813
  import { createServer } from "http";
5525
5814
  import { WebSocketServer, WebSocket } from "ws";
5526
- import { readFile as readFile10 } from "fs/promises";
5527
- 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";
5528
5817
  import { fileURLToPath as fileURLToPath2 } from "url";
5529
5818
  var GatewayServer;
5530
5819
  var init_server = __esm({
@@ -5558,12 +5847,18 @@ var init_server = __esm({
5558
5847
  pluginLoader;
5559
5848
  adapters = [];
5560
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
5561
5856
  constructor(config) {
5562
5857
  this.config = config;
5563
- this.sessionStore = new SessionStore(join13(getConfigDir(), "conversations"));
5858
+ this.sessionStore = new SessionStore(join14(getConfigDir(), "conversations"));
5564
5859
  this.toolRegistry = createFullRegistry();
5565
- this.memoryManager = new MemoryManager(join13(getConfigDir(), "memory"));
5566
- this.cronScheduler = new CronScheduler(join13(getConfigDir(), "cron"));
5860
+ this.memoryManager = new MemoryManager(join14(getConfigDir(), "memory"));
5861
+ this.cronScheduler = new CronScheduler(join14(getConfigDir(), "cron"));
5567
5862
  this.configWatcher = new ConfigWatcher();
5568
5863
  this.pluginLoader = new PluginLoader();
5569
5864
  }
@@ -5633,6 +5928,10 @@ var init_server = __esm({
5633
5928
  * This is the main entry point for all non-WebSocket messages.
5634
5929
  */
5635
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
+ }
5636
5935
  const agentId = resolveRoute(
5637
5936
  context,
5638
5937
  [],
@@ -5644,6 +5943,51 @@ var init_server = __esm({
5644
5943
  const engine = await this.getOrCreateEngine(sessionKey, agentId, context.channel);
5645
5944
  return engine.sendMessage(text);
5646
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
+ }
5647
5991
  /** Stop the gateway server */
5648
5992
  async stop() {
5649
5993
  this.running = false;
@@ -5677,7 +6021,7 @@ var init_server = __esm({
5677
6021
  res.writeHead(200, { "Content-Type": "application/json" });
5678
6022
  res.end(JSON.stringify({
5679
6023
  status: "ok",
5680
- version: "1.3.1",
6024
+ version: "1.4.0",
5681
6025
  uptime: process.uptime(),
5682
6026
  clients: this.clients.size,
5683
6027
  agents: this.engines.size
@@ -5707,14 +6051,14 @@ var init_server = __esm({
5707
6051
  if (url === "/chat" || url === "/") {
5708
6052
  try {
5709
6053
  const __dirname4 = dirname2(fileURLToPath2(import.meta.url));
5710
- const htmlPath = join13(__dirname4, "..", "web", "index.html");
5711
- const html = await readFile10(htmlPath, "utf-8");
6054
+ const htmlPath = join14(__dirname4, "..", "web", "index.html");
6055
+ const html = await readFile11(htmlPath, "utf-8");
5712
6056
  res.writeHead(200, { "Content-Type": "text/html" });
5713
6057
  res.end(html);
5714
6058
  return;
5715
6059
  } catch {
5716
6060
  try {
5717
- 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");
5718
6062
  res.writeHead(200, { "Content-Type": "text/html" });
5719
6063
  res.end(html);
5720
6064
  return;
@@ -5785,7 +6129,7 @@ var init_server = __esm({
5785
6129
  const hello = {
5786
6130
  type: "hello",
5787
6131
  protocol: PROTOCOL_VERSION,
5788
- version: "1.3.1",
6132
+ version: "1.4.0",
5789
6133
  agents: this.config.agents.list.map((a) => ({
5790
6134
  id: a.id,
5791
6135
  name: a.name || a.id,
@@ -5959,6 +6303,10 @@ var init_server = __esm({
5959
6303
  this.sendResponse(client, frame.id, false, void 0, "message is required");
5960
6304
  return;
5961
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
+ }
5962
6310
  try {
5963
6311
  const engine = await this.getOrCreateEngine(client.sessionKey, client.agentId, client.clientName);
5964
6312
  const cleanup = this.wireEngineEvents(engine, client);
@@ -5973,6 +6321,15 @@ var init_server = __esm({
5973
6321
  this.sendResponse(client, frame.id, false, void 0, msg);
5974
6322
  }
5975
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
+ }
5976
6333
  /** Cancel current request for a client */
5977
6334
  handleCancel(client) {
5978
6335
  const engine = this.engines.get(client.sessionKey);
@@ -5998,10 +6355,14 @@ var init_server = __esm({
5998
6355
  toolTier: agentConfig?.toolTier || this.config.agents.defaults.toolTier || "auto",
5999
6356
  tools: agentConfig?.tools
6000
6357
  };
6358
+ const compact = agentConfig?.compactPrompt ?? this.config.agents.defaults.compactPrompt ?? false;
6359
+ const thinking = agentConfig?.thinking ?? this.config.agents.defaults.thinking ?? "auto";
6001
6360
  const systemPrompt = await buildSystemPrompt({
6002
6361
  identity,
6003
6362
  workspaceDir: identity.workspace,
6004
- channel
6363
+ channel,
6364
+ compact,
6365
+ thinking
6005
6366
  });
6006
6367
  const memoryBlock = await this.memoryManager.buildMemoryBlock("", identity.workspace);
6007
6368
  const fullPrompt = memoryBlock ? systemPrompt + "\n\n---\n\n" + memoryBlock : systemPrompt;
@@ -6116,9 +6477,9 @@ __export(gateway_cmd_exports, {
6116
6477
  isGatewayRunning: () => isGatewayRunning
6117
6478
  });
6118
6479
  import { fork } from "child_process";
6119
- import { writeFile as writeFile7, readFile as readFile11, unlink as unlink3 } from "fs/promises";
6120
- import { existsSync as existsSync7 } from "fs";
6121
- 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";
6122
6483
  import { fileURLToPath as fileURLToPath3 } from "url";
6123
6484
  async function isGatewayRunning(port) {
6124
6485
  const config = await loadConfig();
@@ -6131,7 +6492,7 @@ async function isGatewayRunning(port) {
6131
6492
  }
6132
6493
  }
6133
6494
  function pidFilePath() {
6134
- return join14(getConfigDir(), "gateway.pid");
6495
+ return join15(getConfigDir(), "gateway.pid");
6135
6496
  }
6136
6497
  async function gatewayStartForeground(opts) {
6137
6498
  await ensureConfigDir();
@@ -6143,7 +6504,7 @@ async function gatewayStartForeground(opts) {
6143
6504
  console.log(green2(` Gateway already running on port ${config.gateway.port}`));
6144
6505
  return;
6145
6506
  }
6146
- await writeFile7(pidFilePath(), String(process.pid), "utf-8");
6507
+ await writeFile8(pidFilePath(), String(process.pid), "utf-8");
6147
6508
  const server = new GatewayServer(config);
6148
6509
  const shutdown = async () => {
6149
6510
  console.log(dim2("\nShutting down..."));
@@ -6176,10 +6537,10 @@ async function gatewayStartBackground() {
6176
6537
  return true;
6177
6538
  }
6178
6539
  console.log(dim2(" Starting gateway in background..."));
6179
- const entryPoint = join14(dirname3(__filename2), "index.js");
6180
- const logFile = join14(getConfigDir(), "logs", "gateway.log");
6540
+ const entryPoint = join15(dirname3(__filename2), "index.js");
6541
+ const logFile = join15(getConfigDir(), "logs", "gateway.log");
6181
6542
  const { mkdir: mkdir7 } = await import("fs/promises");
6182
- await mkdir7(join14(getConfigDir(), "logs"), { recursive: true });
6543
+ await mkdir7(join15(getConfigDir(), "logs"), { recursive: true });
6183
6544
  const child = fork(entryPoint, ["gateway", "start", "--foreground"], {
6184
6545
  detached: true,
6185
6546
  stdio: ["ignore", "ignore", "ignore", "ipc"]
@@ -6208,9 +6569,9 @@ async function gatewayStart(opts) {
6208
6569
  }
6209
6570
  async function gatewayStop() {
6210
6571
  const pidPath = pidFilePath();
6211
- if (existsSync7(pidPath)) {
6572
+ if (existsSync9(pidPath)) {
6212
6573
  try {
6213
- const pid = parseInt(await readFile11(pidPath, "utf-8"), 10);
6574
+ const pid = parseInt(await readFile12(pidPath, "utf-8"), 10);
6214
6575
  process.kill(pid, "SIGTERM");
6215
6576
  await unlink3(pidPath);
6216
6577
  console.log(green2("Gateway stopped"));
@@ -6241,8 +6602,8 @@ async function gatewayStatus() {
6241
6602
  console.log(dim2(` Clients: ${data.clients?.length || 0}`));
6242
6603
  console.log(dim2(` Sessions: ${data.sessions?.length || 0}`));
6243
6604
  const pidPath = pidFilePath();
6244
- if (existsSync7(pidPath)) {
6245
- const pid = await readFile11(pidPath, "utf-8");
6605
+ if (existsSync9(pidPath)) {
6606
+ const pid = await readFile12(pidPath, "utf-8");
6246
6607
  console.log(dim2(` PID: ${pid.trim()}`));
6247
6608
  }
6248
6609
  } else {
@@ -6270,8 +6631,8 @@ var init_gateway_cmd = __esm({
6270
6631
  });
6271
6632
 
6272
6633
  // src/daemon/install.ts
6273
- import { writeFile as writeFile8, mkdir as mkdir6, unlink as unlink4 } from "fs/promises";
6274
- 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";
6275
6636
  import { homedir as homedir3, platform as platform4 } from "os";
6276
6637
  import { execSync } from "child_process";
6277
6638
  async function installDaemon() {
@@ -6339,8 +6700,8 @@ async function daemonStatus() {
6339
6700
  }
6340
6701
  }
6341
6702
  async function installLaunchd() {
6342
- const plistDir = join15(homedir3(), "Library", "LaunchAgents");
6343
- const plistPath = join15(plistDir, "com.clank.gateway.plist");
6703
+ const plistDir = join16(homedir3(), "Library", "LaunchAgents");
6704
+ const plistPath = join16(plistDir, "com.clank.gateway.plist");
6344
6705
  const clankPath = execSync("which clank || echo clank", { encoding: "utf-8" }).trim();
6345
6706
  await mkdir6(plistDir, { recursive: true });
6346
6707
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
@@ -6361,18 +6722,18 @@ async function installLaunchd() {
6361
6722
  <key>KeepAlive</key>
6362
6723
  <true/>
6363
6724
  <key>StandardOutPath</key>
6364
- <string>${join15(homedir3(), ".clank", "logs", "gateway.log")}</string>
6725
+ <string>${join16(homedir3(), ".clank", "logs", "gateway.log")}</string>
6365
6726
  <key>StandardErrorPath</key>
6366
- <string>${join15(homedir3(), ".clank", "logs", "gateway-error.log")}</string>
6727
+ <string>${join16(homedir3(), ".clank", "logs", "gateway-error.log")}</string>
6367
6728
  </dict>
6368
6729
  </plist>`;
6369
- await writeFile8(plistPath, plist, "utf-8");
6730
+ await writeFile9(plistPath, plist, "utf-8");
6370
6731
  execSync(`launchctl load "${plistPath}"`);
6371
6732
  console.log(green3("Daemon installed (launchd)"));
6372
6733
  console.log(dim3(` Plist: ${plistPath}`));
6373
6734
  }
6374
6735
  async function uninstallLaunchd() {
6375
- const plistPath = join15(homedir3(), "Library", "LaunchAgents", "com.clank.gateway.plist");
6736
+ const plistPath = join16(homedir3(), "Library", "LaunchAgents", "com.clank.gateway.plist");
6376
6737
  try {
6377
6738
  execSync(`launchctl unload "${plistPath}"`);
6378
6739
  await unlink4(plistPath);
@@ -6410,8 +6771,8 @@ async function uninstallTaskScheduler() {
6410
6771
  }
6411
6772
  }
6412
6773
  async function installSystemd() {
6413
- const unitDir = join15(homedir3(), ".config", "systemd", "user");
6414
- const unitPath = join15(unitDir, "clank-gateway.service");
6774
+ const unitDir = join16(homedir3(), ".config", "systemd", "user");
6775
+ const unitPath = join16(unitDir, "clank-gateway.service");
6415
6776
  const clankPath = execSync("which clank || echo clank", { encoding: "utf-8" }).trim();
6416
6777
  await mkdir6(unitDir, { recursive: true });
6417
6778
  const unit = `[Unit]
@@ -6426,7 +6787,7 @@ RestartSec=5
6426
6787
  [Install]
6427
6788
  WantedBy=default.target
6428
6789
  `;
6429
- await writeFile8(unitPath, unit, "utf-8");
6790
+ await writeFile9(unitPath, unit, "utf-8");
6430
6791
  execSync("systemctl --user daemon-reload");
6431
6792
  execSync("systemctl --user enable clank-gateway");
6432
6793
  execSync("systemctl --user start clank-gateway");
@@ -6437,7 +6798,7 @@ async function uninstallSystemd() {
6437
6798
  try {
6438
6799
  execSync("systemctl --user stop clank-gateway");
6439
6800
  execSync("systemctl --user disable clank-gateway");
6440
- const unitPath = join15(homedir3(), ".config", "systemd", "user", "clank-gateway.service");
6801
+ const unitPath = join16(homedir3(), ".config", "systemd", "user", "clank-gateway.service");
6441
6802
  await unlink4(unitPath);
6442
6803
  execSync("systemctl --user daemon-reload");
6443
6804
  console.log(green3("Daemon uninstalled"));
@@ -6478,7 +6839,7 @@ __export(setup_exports, {
6478
6839
  });
6479
6840
  import { createInterface as createInterface2 } from "readline";
6480
6841
  import { randomBytes } from "crypto";
6481
- import { dirname as dirname4, join as join16 } from "path";
6842
+ import { dirname as dirname4, join as join17 } from "path";
6482
6843
  import { fileURLToPath as fileURLToPath4 } from "url";
6483
6844
  function ask(rl, question) {
6484
6845
  return new Promise((resolve4) => rl.question(question, resolve4));
@@ -6581,8 +6942,8 @@ async function runSetup(opts) {
6581
6942
  console.log("");
6582
6943
  console.log(dim4(" Creating workspace..."));
6583
6944
  const { ensureWorkspaceFiles: ensureWorkspaceFiles2 } = await Promise.resolve().then(() => (init_system_prompt(), system_prompt_exports));
6584
- const templateDir = join16(__dirname2, "..", "workspace", "templates");
6585
- const wsDir = join16(getConfigDir(), "workspace");
6945
+ const templateDir = join17(__dirname2, "..", "workspace", "templates");
6946
+ const wsDir = join17(getConfigDir(), "workspace");
6586
6947
  try {
6587
6948
  await ensureWorkspaceFiles2(wsDir, templateDir);
6588
6949
  } catch {
@@ -6743,9 +7104,9 @@ var fix_exports = {};
6743
7104
  __export(fix_exports, {
6744
7105
  runFix: () => runFix
6745
7106
  });
6746
- import { existsSync as existsSync8 } from "fs";
7107
+ import { existsSync as existsSync10 } from "fs";
6747
7108
  import { readdir as readdir7 } from "fs/promises";
6748
- import { join as join17 } from "path";
7109
+ import { join as join18 } from "path";
6749
7110
  async function runFix(opts) {
6750
7111
  console.log("");
6751
7112
  console.log(" Clank Diagnostics");
@@ -6775,7 +7136,7 @@ async function runFix(opts) {
6775
7136
  }
6776
7137
  async function checkConfig() {
6777
7138
  const configPath = getConfigPath();
6778
- if (!existsSync8(configPath)) {
7139
+ if (!existsSync10(configPath)) {
6779
7140
  return {
6780
7141
  name: "Config",
6781
7142
  status: "warn",
@@ -6843,8 +7204,8 @@ async function checkModels() {
6843
7204
  return { name: "Model (primary)", status: "ok", message: modelId };
6844
7205
  }
6845
7206
  async function checkSessions() {
6846
- const sessDir = join17(getConfigDir(), "conversations");
6847
- if (!existsSync8(sessDir)) {
7207
+ const sessDir = join18(getConfigDir(), "conversations");
7208
+ if (!existsSync10(sessDir)) {
6848
7209
  return { name: "Sessions", status: "ok", message: "no sessions yet" };
6849
7210
  }
6850
7211
  try {
@@ -6856,8 +7217,8 @@ async function checkSessions() {
6856
7217
  }
6857
7218
  }
6858
7219
  async function checkWorkspace() {
6859
- const wsDir = join17(getConfigDir(), "workspace");
6860
- if (!existsSync8(wsDir)) {
7220
+ const wsDir = join18(getConfigDir(), "workspace");
7221
+ if (!existsSync10(wsDir)) {
6861
7222
  return {
6862
7223
  name: "Workspace",
6863
7224
  status: "warn",
@@ -7150,7 +7511,7 @@ async function runTui(opts) {
7150
7511
  ws.on("open", () => {
7151
7512
  ws.send(JSON.stringify({
7152
7513
  type: "connect",
7153
- params: { auth: { token }, mode: "tui", version: "1.3.1" }
7514
+ params: { auth: { token }, mode: "tui", version: "1.4.0" }
7154
7515
  }));
7155
7516
  });
7156
7517
  ws.on("message", (data) => {
@@ -7499,7 +7860,7 @@ __export(uninstall_exports, {
7499
7860
  });
7500
7861
  import { createInterface as createInterface4 } from "readline";
7501
7862
  import { rm } from "fs/promises";
7502
- import { existsSync as existsSync9 } from "fs";
7863
+ import { existsSync as existsSync11 } from "fs";
7503
7864
  async function runUninstall(opts) {
7504
7865
  const configDir = getConfigDir();
7505
7866
  console.log("");
@@ -7538,7 +7899,7 @@ async function runUninstall(opts) {
7538
7899
  } catch {
7539
7900
  }
7540
7901
  console.log(dim10(" Deleting data..."));
7541
- if (existsSync9(configDir)) {
7902
+ if (existsSync11(configDir)) {
7542
7903
  await rm(configDir, { recursive: true, force: true });
7543
7904
  console.log(green10(` Removed ${configDir}`));
7544
7905
  } else {
@@ -7576,12 +7937,12 @@ init_esm_shims();
7576
7937
  import { Command } from "commander";
7577
7938
  import { readFileSync } from "fs";
7578
7939
  import { fileURLToPath as fileURLToPath5 } from "url";
7579
- import { dirname as dirname5, join as join18 } from "path";
7940
+ import { dirname as dirname5, join as join19 } from "path";
7580
7941
  var __filename3 = fileURLToPath5(import.meta.url);
7581
7942
  var __dirname3 = dirname5(__filename3);
7582
- var version = "1.3.1";
7943
+ var version = "1.4.0";
7583
7944
  try {
7584
- const pkg = JSON.parse(readFileSync(join18(__dirname3, "..", "package.json"), "utf-8"));
7945
+ const pkg = JSON.parse(readFileSync(join19(__dirname3, "..", "package.json"), "utf-8"));
7585
7946
  version = pkg.version;
7586
7947
  } catch {
7587
7948
  }
@@ -7690,10 +8051,10 @@ pipeline.command("status <id>").description("Check pipeline execution status").a
7690
8051
  });
7691
8052
  var cron = program.command("cron").description("Manage scheduled jobs");
7692
8053
  cron.command("list").description("List cron jobs").action(async () => {
7693
- const { join: join19 } = await import("path");
8054
+ const { join: join20 } = await import("path");
7694
8055
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7695
8056
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
7696
- const scheduler = new CronScheduler2(join19(getConfigDir3(), "cron"));
8057
+ const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
7697
8058
  await scheduler.init();
7698
8059
  const jobs = scheduler.listJobs();
7699
8060
  if (jobs.length === 0) {
@@ -7705,10 +8066,10 @@ cron.command("list").description("List cron jobs").action(async () => {
7705
8066
  }
7706
8067
  });
7707
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) => {
7708
- const { join: join19 } = await import("path");
8069
+ const { join: join20 } = await import("path");
7709
8070
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7710
8071
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
7711
- const scheduler = new CronScheduler2(join19(getConfigDir3(), "cron"));
8072
+ const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
7712
8073
  await scheduler.init();
7713
8074
  const job = await scheduler.addJob({
7714
8075
  name: opts.name || "CLI Job",
@@ -7719,10 +8080,10 @@ cron.command("add").description("Add a cron job").requiredOption("--schedule <ex
7719
8080
  console.log(` Job created: ${job.id.slice(0, 8)} \u2014 "${job.name}" every ${job.schedule}`);
7720
8081
  });
7721
8082
  cron.command("remove <id>").description("Remove a cron job").action(async (id) => {
7722
- const { join: join19 } = await import("path");
8083
+ const { join: join20 } = await import("path");
7723
8084
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7724
8085
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
7725
- const scheduler = new CronScheduler2(join19(getConfigDir3(), "cron"));
8086
+ const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
7726
8087
  await scheduler.init();
7727
8088
  const removed = await scheduler.removeJob(id);
7728
8089
  console.log(removed ? ` Job ${id.slice(0, 8)} removed` : ` Job not found`);