@tractorscorch/clank 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  }
@@ -1165,6 +1276,10 @@ var init_registry = __esm({
1165
1276
  });
1166
1277
 
1167
1278
  // src/tools/path-guard.ts
1279
+ var path_guard_exports = {};
1280
+ __export(path_guard_exports, {
1281
+ guardPath: () => guardPath
1282
+ });
1168
1283
  import { resolve, isAbsolute, normalize, relative } from "path";
1169
1284
  function guardPath(inputPath, projectRoot, opts) {
1170
1285
  const resolved = isAbsolute(inputPath) ? normalize(inputPath) : normalize(resolve(projectRoot, inputPath));
@@ -1186,7 +1301,7 @@ var init_path_guard = __esm({
1186
1301
  });
1187
1302
 
1188
1303
  // src/tools/read-file.ts
1189
- import { readFile as readFile2, stat } from "fs/promises";
1304
+ import { readFile as readFile3, stat } from "fs/promises";
1190
1305
  var readFileTool;
1191
1306
  var init_read_file = __esm({
1192
1307
  "src/tools/read-file.ts"() {
@@ -1235,7 +1350,7 @@ var init_read_file = __esm({
1235
1350
  if (probe.subarray(0, probeLen).includes(0)) {
1236
1351
  return `Binary file detected: ${filePath} (${fileStats.size} bytes)`;
1237
1352
  }
1238
- const content = await readFile2(filePath, "utf-8");
1353
+ const content = await readFile3(filePath, "utf-8");
1239
1354
  const lines = content.split("\n");
1240
1355
  const offset = Math.max(1, Number(args.offset) || 1);
1241
1356
  const limit = Number(args.limit) || lines.length;
@@ -1252,7 +1367,7 @@ var init_read_file = __esm({
1252
1367
  });
1253
1368
 
1254
1369
  // src/tools/write-file.ts
1255
- import { writeFile, mkdir } from "fs/promises";
1370
+ import { writeFile as writeFile2, mkdir } from "fs/promises";
1256
1371
  import { dirname, isAbsolute as isAbsolute2 } from "path";
1257
1372
  var writeFileTool;
1258
1373
  var init_write_file = __esm({
@@ -1294,7 +1409,7 @@ var init_write_file = __esm({
1294
1409
  const filePath = guard.path;
1295
1410
  try {
1296
1411
  await mkdir(dirname(filePath), { recursive: true });
1297
- await writeFile(filePath, args.content, "utf-8");
1412
+ await writeFile2(filePath, args.content, "utf-8");
1298
1413
  const lines = args.content.split("\n").length;
1299
1414
  return `Wrote ${lines} lines to ${filePath}`;
1300
1415
  } catch (err) {
@@ -1310,7 +1425,7 @@ var init_write_file = __esm({
1310
1425
  });
1311
1426
 
1312
1427
  // src/tools/edit-file.ts
1313
- import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1428
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1314
1429
  import { isAbsolute as isAbsolute3 } from "path";
1315
1430
  var editFileTool;
1316
1431
  var init_edit_file = __esm({
@@ -1351,7 +1466,7 @@ var init_edit_file = __esm({
1351
1466
  if (!guard.ok) return guard.error;
1352
1467
  const filePath = guard.path;
1353
1468
  try {
1354
- const content = await readFile3(filePath, "utf-8");
1469
+ const content = await readFile4(filePath, "utf-8");
1355
1470
  const oldStr = args.old_string;
1356
1471
  const newStr = args.new_string;
1357
1472
  const replaceAll = Boolean(args.replace_all);
@@ -1365,7 +1480,7 @@ var init_edit_file = __esm({
1365
1480
  }
1366
1481
  }
1367
1482
  const updated = replaceAll ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
1368
- await writeFile2(filePath, updated, "utf-8");
1483
+ await writeFile3(filePath, updated, "utf-8");
1369
1484
  const replacements = replaceAll ? content.split(oldStr).length - 1 : 1;
1370
1485
  return `Edited ${filePath} (${replacements} replacement${replacements > 1 ? "s" : ""})`;
1371
1486
  } catch (err) {
@@ -1382,7 +1497,7 @@ var init_edit_file = __esm({
1382
1497
 
1383
1498
  // src/tools/list-directory.ts
1384
1499
  import { readdir, stat as stat2 } from "fs/promises";
1385
- import { join as join2 } from "path";
1500
+ import { join as join3 } from "path";
1386
1501
  function formatSize(bytes) {
1387
1502
  if (bytes < 1024) return `${bytes}B`;
1388
1503
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
@@ -1423,7 +1538,7 @@ var init_list_directory = __esm({
1423
1538
  const lines = [];
1424
1539
  for (const entry of entries.slice(0, 100)) {
1425
1540
  try {
1426
- const full = join2(dirPath, entry);
1541
+ const full = join3(dirPath, entry);
1427
1542
  const s = await stat2(full);
1428
1543
  const type = s.isDirectory() ? "dir" : "file";
1429
1544
  const size = s.isDirectory() ? "" : ` (${formatSize(s.size)})`;
@@ -1446,8 +1561,8 @@ var init_list_directory = __esm({
1446
1561
  });
1447
1562
 
1448
1563
  // 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";
1564
+ import { readdir as readdir2, readFile as readFile5, stat as stat3 } from "fs/promises";
1565
+ import { join as join4, relative as relative2 } from "path";
1451
1566
  var IGNORE_DIRS, searchFilesTool;
1452
1567
  var init_search_files = __esm({
1453
1568
  "src/tools/search-files.ts"() {
@@ -1522,7 +1637,7 @@ var init_search_files = __esm({
1522
1637
  for (const entry of entries) {
1523
1638
  if (results.length >= maxResults) return;
1524
1639
  if (IGNORE_DIRS.has(entry)) continue;
1525
- const full = join3(dir, entry);
1640
+ const full = join4(dir, entry);
1526
1641
  let s;
1527
1642
  try {
1528
1643
  s = await stat3(full);
@@ -1537,7 +1652,7 @@ var init_search_files = __esm({
1537
1652
  if (!entry.endsWith(ext)) continue;
1538
1653
  }
1539
1654
  try {
1540
- const content = await readFile4(full, "utf-8");
1655
+ const content = await readFile5(full, "utf-8");
1541
1656
  const lines = content.split("\n");
1542
1657
  for (let i = 0; i < lines.length; i++) {
1543
1658
  regex.lastIndex = 0;
@@ -1562,7 +1677,7 @@ var init_search_files = __esm({
1562
1677
 
1563
1678
  // src/tools/glob-files.ts
1564
1679
  import { readdir as readdir3, stat as stat4 } from "fs/promises";
1565
- import { join as join4 } from "path";
1680
+ import { join as join5 } from "path";
1566
1681
  function globToRegex(pattern) {
1567
1682
  let regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\{\{GLOBSTAR\}\}/g, ".*");
1568
1683
  return new RegExp(`^${regex}$`, "i");
@@ -1628,7 +1743,7 @@ var init_glob_files = __esm({
1628
1743
  for (const entry of entries) {
1629
1744
  if (matches.length >= 200) return;
1630
1745
  if (IGNORE_DIRS2.has(entry)) continue;
1631
- const full = join4(dir, entry);
1746
+ const full = join5(dir, entry);
1632
1747
  const rel = relDir ? `${relDir}/${entry}` : entry;
1633
1748
  let s;
1634
1749
  try {
@@ -1854,19 +1969,19 @@ var init_git = __esm({
1854
1969
  });
1855
1970
 
1856
1971
  // 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";
1972
+ import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
1973
+ import { existsSync as existsSync3 } from "fs";
1974
+ import { join as join6 } from "path";
1860
1975
  import { homedir, platform as platform3 } from "os";
1861
1976
  import JSON5 from "json5";
1862
1977
  function getConfigDir() {
1863
1978
  if (platform3() === "win32") {
1864
- return join5(process.env.APPDATA || join5(homedir(), "AppData", "Roaming"), "Clank");
1979
+ return join6(process.env.APPDATA || join6(homedir(), "AppData", "Roaming"), "Clank");
1865
1980
  }
1866
- return join5(homedir(), ".clank");
1981
+ return join6(homedir(), ".clank");
1867
1982
  }
1868
1983
  function getConfigPath() {
1869
- return join5(getConfigDir(), "config.json5");
1984
+ return join6(getConfigDir(), "config.json5");
1870
1985
  }
1871
1986
  function defaultConfig() {
1872
1987
  return {
@@ -1878,7 +1993,7 @@ function defaultConfig() {
1878
1993
  agents: {
1879
1994
  defaults: {
1880
1995
  model: { primary: "ollama/qwen3.5" },
1881
- workspace: join5(getConfigDir(), "workspace"),
1996
+ workspace: join6(getConfigDir(), "workspace"),
1882
1997
  toolTier: "auto",
1883
1998
  temperature: 0.7
1884
1999
  },
@@ -1937,11 +2052,11 @@ function deepMerge(target, source) {
1937
2052
  async function loadConfig() {
1938
2053
  const configPath = getConfigPath();
1939
2054
  const defaults = defaultConfig();
1940
- if (!existsSync2(configPath)) {
2055
+ if (!existsSync3(configPath)) {
1941
2056
  return defaults;
1942
2057
  }
1943
2058
  try {
1944
- const raw = await readFile5(configPath, "utf-8");
2059
+ const raw = await readFile6(configPath, "utf-8");
1945
2060
  const parsed = JSON5.parse(raw);
1946
2061
  const substituted = substituteEnvVars(parsed);
1947
2062
  return deepMerge(defaults, substituted);
@@ -1954,15 +2069,15 @@ async function saveConfig(config) {
1954
2069
  const configPath = getConfigPath();
1955
2070
  await mkdir2(getConfigDir(), { recursive: true });
1956
2071
  const content = JSON5.stringify(config, null, 2);
1957
- await writeFile3(configPath, content, "utf-8");
2072
+ await writeFile4(configPath, content, "utf-8");
1958
2073
  }
1959
2074
  async function ensureConfigDir() {
1960
2075
  const configDir = getConfigDir();
1961
2076
  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 });
2077
+ await mkdir2(join6(configDir, "workspace"), { recursive: true });
2078
+ await mkdir2(join6(configDir, "conversations"), { recursive: true });
2079
+ await mkdir2(join6(configDir, "memory"), { recursive: true });
2080
+ await mkdir2(join6(configDir, "logs"), { recursive: true });
1966
2081
  }
1967
2082
  var init_config = __esm({
1968
2083
  "src/config/config.ts"() {
@@ -2144,6 +2259,12 @@ var init_web_fetch = __esm({
2144
2259
  if (host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "0.0.0.0") {
2145
2260
  return { ok: false, error: "localhost URLs are blocked (SSRF protection)" };
2146
2261
  }
2262
+ if (/^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host)) {
2263
+ return { ok: false, error: "Private network IPs are blocked (SSRF protection)" };
2264
+ }
2265
+ if (host.startsWith("[::ffff:")) {
2266
+ return { ok: false, error: "IPv4-mapped IPv6 addresses are blocked" };
2267
+ }
2147
2268
  if (host === "169.254.169.254" || host === "metadata.google.internal") {
2148
2269
  return { ok: false, error: "Cloud metadata endpoints are blocked" };
2149
2270
  }
@@ -2280,9 +2401,21 @@ var init_config_tool = __esm({
2280
2401
  return `Key not found: ${key}`;
2281
2402
  }
2282
2403
  }
2283
- return typeof current === "object" ? JSON.stringify(current, null, 2) : String(current);
2404
+ if (typeof current === "object") {
2405
+ return JSON.stringify(redactConfig(current), null, 2);
2406
+ }
2407
+ const SENSITIVE = /* @__PURE__ */ new Set(["apikey", "api_key", "apiKey", "token", "bottoken", "botToken", "secret", "password", "pin"]);
2408
+ const lastKey = keys[keys.length - 1];
2409
+ if (SENSITIVE.has(lastKey) && typeof current === "string") {
2410
+ return "[REDACTED]";
2411
+ }
2412
+ return String(current);
2284
2413
  }
2285
2414
  if (action === "set") {
2415
+ const BLOCKED_KEYS = ["__proto__", "constructor", "prototype"];
2416
+ if (keys.some((k) => BLOCKED_KEYS.includes(k))) {
2417
+ return "Error: blocked \u2014 unsafe key";
2418
+ }
2286
2419
  let parsed = args.value;
2287
2420
  try {
2288
2421
  parsed = JSON.parse(args.value);
@@ -3358,9 +3491,9 @@ var init_model_tool = __esm({
3358
3491
  });
3359
3492
 
3360
3493
  // 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";
3494
+ import { readFile as readFile7, writeFile as writeFile5, unlink, mkdir as mkdir3 } from "fs/promises";
3495
+ import { existsSync as existsSync4 } from "fs";
3496
+ import { join as join7 } from "path";
3364
3497
  import { randomUUID } from "crypto";
3365
3498
  var SessionStore;
3366
3499
  var init_store = __esm({
@@ -3373,14 +3506,14 @@ var init_store = __esm({
3373
3506
  index = /* @__PURE__ */ new Map();
3374
3507
  constructor(storeDir) {
3375
3508
  this.storeDir = storeDir;
3376
- this.indexPath = join6(storeDir, "sessions.json");
3509
+ this.indexPath = join7(storeDir, "sessions.json");
3377
3510
  }
3378
3511
  /** Initialize the store — load index from disk */
3379
3512
  async init() {
3380
3513
  await mkdir3(this.storeDir, { recursive: true });
3381
- if (existsSync3(this.indexPath)) {
3514
+ if (existsSync4(this.indexPath)) {
3382
3515
  try {
3383
- const raw = await readFile6(this.indexPath, "utf-8");
3516
+ const raw = await readFile7(this.indexPath, "utf-8");
3384
3517
  const entries = JSON.parse(raw);
3385
3518
  for (const entry of entries) {
3386
3519
  this.index.set(entry.normalizedKey, entry);
@@ -3393,7 +3526,7 @@ var init_store = __esm({
3393
3526
  /** Save the index to disk */
3394
3527
  async saveIndex() {
3395
3528
  const entries = Array.from(this.index.values());
3396
- await writeFile4(this.indexPath, JSON.stringify(entries, null, 2), "utf-8");
3529
+ await writeFile5(this.indexPath, JSON.stringify(entries, null, 2), "utf-8");
3397
3530
  }
3398
3531
  /** Get or create a session for a normalized key */
3399
3532
  async resolve(normalizedKey, opts) {
@@ -3419,10 +3552,10 @@ var init_store = __esm({
3419
3552
  }
3420
3553
  /** Load conversation messages for a session */
3421
3554
  async loadMessages(sessionId) {
3422
- const path2 = join6(this.storeDir, `${sessionId}.json`);
3423
- if (!existsSync3(path2)) return [];
3555
+ const path2 = join7(this.storeDir, `${sessionId}.json`);
3556
+ if (!existsSync4(path2)) return [];
3424
3557
  try {
3425
- const raw = await readFile6(path2, "utf-8");
3558
+ const raw = await readFile7(path2, "utf-8");
3426
3559
  return JSON.parse(raw);
3427
3560
  } catch {
3428
3561
  return [];
@@ -3430,8 +3563,8 @@ var init_store = __esm({
3430
3563
  }
3431
3564
  /** Save conversation messages for a session */
3432
3565
  async saveMessages(sessionId, messages) {
3433
- const path2 = join6(this.storeDir, `${sessionId}.json`);
3434
- await writeFile4(path2, JSON.stringify(messages, null, 2), "utf-8");
3566
+ const path2 = join7(this.storeDir, `${sessionId}.json`);
3567
+ await writeFile5(path2, JSON.stringify(messages, null, 2), "utf-8");
3435
3568
  }
3436
3569
  /** List all sessions, sorted by last used */
3437
3570
  list() {
@@ -3443,7 +3576,7 @@ var init_store = __esm({
3443
3576
  if (!entry) return false;
3444
3577
  this.index.delete(normalizedKey);
3445
3578
  await this.saveIndex();
3446
- const path2 = join6(this.storeDir, `${entry.id}.json`);
3579
+ const path2 = join7(this.storeDir, `${entry.id}.json`);
3447
3580
  try {
3448
3581
  await unlink(path2);
3449
3582
  } catch {
@@ -3454,7 +3587,7 @@ var init_store = __esm({
3454
3587
  async reset(normalizedKey) {
3455
3588
  const entry = this.index.get(normalizedKey);
3456
3589
  if (!entry) return null;
3457
- const path2 = join6(this.storeDir, `${entry.id}.json`);
3590
+ const path2 = join7(this.storeDir, `${entry.id}.json`);
3458
3591
  try {
3459
3592
  await unlink(path2);
3460
3593
  } catch {
@@ -3500,7 +3633,7 @@ var init_sessions = __esm({
3500
3633
  });
3501
3634
 
3502
3635
  // src/tools/self-config/session-tool.ts
3503
- import { join as join7 } from "path";
3636
+ import { join as join8 } from "path";
3504
3637
  var sessionTool;
3505
3638
  var init_session_tool = __esm({
3506
3639
  "src/tools/self-config/session-tool.ts"() {
@@ -3534,7 +3667,7 @@ var init_session_tool = __esm({
3534
3667
  return { ok: true };
3535
3668
  },
3536
3669
  async execute(args) {
3537
- const store = new SessionStore(join7(getConfigDir(), "conversations"));
3670
+ const store = new SessionStore(join8(getConfigDir(), "conversations"));
3538
3671
  await store.init();
3539
3672
  const action = args.action;
3540
3673
  if (action === "list") {
@@ -3563,9 +3696,9 @@ var init_session_tool = __esm({
3563
3696
  });
3564
3697
 
3565
3698
  // 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";
3699
+ import { readFile as readFile8, appendFile, mkdir as mkdir4, writeFile as writeFile6 } from "fs/promises";
3700
+ import { existsSync as existsSync5 } from "fs";
3701
+ import { join as join9 } from "path";
3569
3702
  import { randomUUID as randomUUID2 } from "crypto";
3570
3703
  var CronScheduler;
3571
3704
  var init_scheduler = __esm({
@@ -3580,8 +3713,8 @@ var init_scheduler = __esm({
3580
3713
  running = false;
3581
3714
  onJobDue;
3582
3715
  constructor(storeDir) {
3583
- this.jobsPath = join8(storeDir, "jobs.jsonl");
3584
- this.runsDir = join8(storeDir, "runs");
3716
+ this.jobsPath = join9(storeDir, "jobs.jsonl");
3717
+ this.runsDir = join9(storeDir, "runs");
3585
3718
  }
3586
3719
  /** Initialize — load jobs from disk */
3587
3720
  async init() {
@@ -3703,9 +3836,9 @@ var init_scheduler = __esm({
3703
3836
  }
3704
3837
  /** Load jobs from JSONL file */
3705
3838
  async loadJobs() {
3706
- if (!existsSync4(this.jobsPath)) return;
3839
+ if (!existsSync5(this.jobsPath)) return;
3707
3840
  try {
3708
- const raw = await readFile7(this.jobsPath, "utf-8");
3841
+ const raw = await readFile8(this.jobsPath, "utf-8");
3709
3842
  this.jobs = raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
3710
3843
  } catch {
3711
3844
  this.jobs = [];
@@ -3714,11 +3847,11 @@ var init_scheduler = __esm({
3714
3847
  /** Save jobs to JSONL file */
3715
3848
  async saveJobs() {
3716
3849
  const content = this.jobs.map((j) => JSON.stringify(j)).join("\n") + "\n";
3717
- await writeFile5(this.jobsPath, content, "utf-8");
3850
+ await writeFile6(this.jobsPath, content, "utf-8");
3718
3851
  }
3719
3852
  /** Log a run result */
3720
3853
  async logRun(log) {
3721
- const logPath = join8(this.runsDir, `${log.jobId}.jsonl`);
3854
+ const logPath = join9(this.runsDir, `${log.jobId}.jsonl`);
3722
3855
  await appendFile(logPath, JSON.stringify(log) + "\n", "utf-8");
3723
3856
  }
3724
3857
  };
@@ -3739,7 +3872,7 @@ var init_cron = __esm({
3739
3872
  });
3740
3873
 
3741
3874
  // src/tools/self-config/cron-tool.ts
3742
- import { join as join9 } from "path";
3875
+ import { join as join10 } from "path";
3743
3876
  var cronTool;
3744
3877
  var init_cron_tool = __esm({
3745
3878
  "src/tools/self-config/cron-tool.ts"() {
@@ -3777,7 +3910,7 @@ var init_cron_tool = __esm({
3777
3910
  return { ok: true };
3778
3911
  },
3779
3912
  async execute(args, ctx) {
3780
- const scheduler = new CronScheduler(join9(getConfigDir(), "cron"));
3913
+ const scheduler = new CronScheduler(join10(getConfigDir(), "cron"));
3781
3914
  await scheduler.init();
3782
3915
  const action = args.action;
3783
3916
  if (action === "list") {
@@ -4041,12 +4174,12 @@ var init_tts = __esm({
4041
4174
  /** Transcribe via local whisper.cpp */
4042
4175
  async transcribeLocal(audioBuffer, format) {
4043
4176
  try {
4044
- const { writeFile: writeFile9, unlink: unlink5 } = await import("fs/promises");
4177
+ const { writeFile: writeFile10, unlink: unlink5 } = await import("fs/promises");
4045
4178
  const { execSync: execSync3 } = await import("child_process");
4046
- const { join: join19 } = await import("path");
4179
+ const { join: join20 } = await import("path");
4047
4180
  const { tmpdir } = await import("os");
4048
- const tmpFile = join19(tmpdir(), `clank-stt-${Date.now()}.${format}`);
4049
- await writeFile9(tmpFile, audioBuffer);
4181
+ const tmpFile = join20(tmpdir(), `clank-stt-${Date.now()}.${format}`);
4182
+ await writeFile10(tmpFile, audioBuffer);
4050
4183
  const output = execSync3(`whisper "${tmpFile}" --model base.en --output-txt`, {
4051
4184
  encoding: "utf-8",
4052
4185
  timeout: 6e4
@@ -4114,11 +4247,11 @@ var init_voice_tool = __esm({
4114
4247
  voiceId: args.voice_id
4115
4248
  });
4116
4249
  if (!result) return "Error: TTS synthesis failed";
4117
- const { writeFile: writeFile9 } = await import("fs/promises");
4118
- const { join: join19 } = await import("path");
4250
+ const { writeFile: writeFile10 } = await import("fs/promises");
4251
+ const { join: join20 } = await import("path");
4119
4252
  const { tmpdir } = await import("os");
4120
- const outPath = join19(tmpdir(), `clank-tts-${Date.now()}.${result.format}`);
4121
- await writeFile9(outPath, result.audioBuffer);
4253
+ const outPath = join20(tmpdir(), `clank-tts-${Date.now()}.${result.format}`);
4254
+ await writeFile10(outPath, result.audioBuffer);
4122
4255
  return `Audio generated: ${outPath} (${result.format}, ${Math.round(result.audioBuffer.length / 1024)}KB)`;
4123
4256
  }
4124
4257
  };
@@ -4136,21 +4269,24 @@ var init_voice_tool = __esm({
4136
4269
  },
4137
4270
  safetyLevel: "low",
4138
4271
  readOnly: true,
4139
- validate(args) {
4272
+ validate(args, ctx) {
4140
4273
  if (!args.file_path || typeof args.file_path !== "string") return { ok: false, error: "file_path is required" };
4141
4274
  return { ok: true };
4142
4275
  },
4143
- async execute(args) {
4144
- const { readFile: readFile12 } = await import("fs/promises");
4145
- const { existsSync: existsSync10 } = await import("fs");
4146
- const filePath = args.file_path;
4147
- if (!existsSync10(filePath)) return `Error: File not found: ${filePath}`;
4276
+ async execute(args, ctx) {
4277
+ const { readFile: readFile13 } = await import("fs/promises");
4278
+ const { existsSync: existsSync12 } = await import("fs");
4279
+ const { guardPath: guardPath2 } = await Promise.resolve().then(() => (init_path_guard(), path_guard_exports));
4280
+ const guard = guardPath2(args.file_path, ctx.projectRoot, { allowExternal: ctx.allowExternal });
4281
+ if (!guard.ok) return guard.error;
4282
+ const filePath = guard.path;
4283
+ if (!existsSync12(filePath)) return `Error: File not found: ${filePath}`;
4148
4284
  const config = await loadConfig();
4149
4285
  const engine = new STTEngine(config);
4150
4286
  if (!engine.isAvailable()) {
4151
4287
  return "Error: Speech-to-text not configured. Need OpenAI API key or local whisper.cpp installed.";
4152
4288
  }
4153
- const audioBuffer = await readFile12(filePath);
4289
+ const audioBuffer = await readFile13(filePath);
4154
4290
  const ext = filePath.split(".").pop() || "wav";
4155
4291
  const result = await engine.transcribe(audioBuffer, ext);
4156
4292
  if (!result) return "Error: Transcription failed";
@@ -4182,6 +4318,53 @@ var init_voice_tool = __esm({
4182
4318
  }
4183
4319
  });
4184
4320
 
4321
+ // src/tools/self-config/file-share-tool.ts
4322
+ import { existsSync as existsSync6 } from "fs";
4323
+ import { stat as stat5 } from "fs/promises";
4324
+ var MAX_FILE_SIZE, fileShareTool;
4325
+ var init_file_share_tool = __esm({
4326
+ "src/tools/self-config/file-share-tool.ts"() {
4327
+ "use strict";
4328
+ init_esm_shims();
4329
+ init_path_guard();
4330
+ MAX_FILE_SIZE = 10 * 1024 * 1024;
4331
+ fileShareTool = {
4332
+ definition: {
4333
+ name: "share_file",
4334
+ 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.",
4335
+ parameters: {
4336
+ type: "object",
4337
+ properties: {
4338
+ path: { type: "string", description: "Path to the file to share" },
4339
+ caption: { type: "string", description: "Optional message to send with the file" }
4340
+ },
4341
+ required: ["path"]
4342
+ }
4343
+ },
4344
+ safetyLevel: "medium",
4345
+ readOnly: true,
4346
+ validate(args, ctx) {
4347
+ if (!args.path || typeof args.path !== "string") return { ok: false, error: "path is required" };
4348
+ const guard = guardPath(args.path, ctx.projectRoot);
4349
+ if (!guard.ok) return { ok: false, error: guard.error };
4350
+ return { ok: true };
4351
+ },
4352
+ async execute(args, ctx) {
4353
+ const guard = guardPath(args.path, ctx.projectRoot, { allowExternal: ctx.allowExternal });
4354
+ if (!guard.ok) return guard.error;
4355
+ if (!existsSync6(guard.path)) return `Error: File not found: ${guard.path}`;
4356
+ const fileStats = await stat5(guard.path);
4357
+ if (fileStats.size > MAX_FILE_SIZE) return `Error: File too large (${Math.round(fileStats.size / 1024 / 1024)}MB, max 10MB)`;
4358
+ const caption = args.caption ? ` with caption: "${args.caption}"` : "";
4359
+ return `File ready to share: ${guard.path} (${Math.round(fileStats.size / 1024)}KB)${caption}. The file will be sent through the current channel.`;
4360
+ },
4361
+ formatConfirmation(args) {
4362
+ return `Share file: ${args.path}`;
4363
+ }
4364
+ };
4365
+ }
4366
+ });
4367
+
4185
4368
  // src/tools/self-config/index.ts
4186
4369
  function registerSelfConfigTools(registry) {
4187
4370
  registry.register(configTool);
@@ -4195,6 +4378,7 @@ function registerSelfConfigTools(registry) {
4195
4378
  registry.register(ttsTool);
4196
4379
  registry.register(sttTool);
4197
4380
  registry.register(voiceListTool);
4381
+ registry.register(fileShareTool);
4198
4382
  }
4199
4383
  var init_self_config = __esm({
4200
4384
  "src/tools/self-config/index.ts"() {
@@ -4209,6 +4393,7 @@ var init_self_config = __esm({
4209
4393
  init_gateway_tool();
4210
4394
  init_message_tool();
4211
4395
  init_voice_tool();
4396
+ init_file_share_tool();
4212
4397
  init_config_tool();
4213
4398
  init_channel_tool();
4214
4399
  init_agent_tool();
@@ -4218,6 +4403,7 @@ var init_self_config = __esm({
4218
4403
  init_gateway_tool();
4219
4404
  init_message_tool();
4220
4405
  init_voice_tool();
4406
+ init_file_share_tool();
4221
4407
  }
4222
4408
  });
4223
4409
 
@@ -4279,7 +4465,7 @@ __export(chat_exports, {
4279
4465
  runChat: () => runChat
4280
4466
  });
4281
4467
  import { createInterface } from "readline";
4282
- import { join as join10 } from "path";
4468
+ import { join as join11 } from "path";
4283
4469
  async function runChat(opts) {
4284
4470
  await ensureConfigDir();
4285
4471
  const config = await loadConfig();
@@ -4296,9 +4482,9 @@ async function runChat(opts) {
4296
4482
  console.log(dim("Starting gateway..."));
4297
4483
  const { fork: fork2 } = await import("child_process");
4298
4484
  const { fileURLToPath: fileURLToPath6 } = await import("url");
4299
- const { dirname: dirname6, join: join19 } = await import("path");
4485
+ const { dirname: dirname6, join: join20 } = await import("path");
4300
4486
  const __filename4 = fileURLToPath6(import.meta.url);
4301
- const entryPoint = join19(dirname6(__filename4), "index.js");
4487
+ const entryPoint = join20(dirname6(__filename4), "index.js");
4302
4488
  const child = fork2(entryPoint, ["gateway", "start", "--foreground"], {
4303
4489
  detached: true,
4304
4490
  stdio: "ignore"
@@ -4346,7 +4532,7 @@ async function runChat(opts) {
4346
4532
  console.error(dim("Make sure Ollama is running or configure a cloud provider in ~/.clank/config.json5"));
4347
4533
  process.exit(1);
4348
4534
  }
4349
- const sessionStore = new SessionStore(join10(getConfigDir(), "conversations"));
4535
+ const sessionStore = new SessionStore(join11(getConfigDir(), "conversations"));
4350
4536
  await sessionStore.init();
4351
4537
  const toolRegistry = createFullRegistry();
4352
4538
  const identity = {
@@ -4504,9 +4690,9 @@ var init_chat = __esm({
4504
4690
  });
4505
4691
 
4506
4692
  // 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";
4693
+ import { readFile as readFile9, writeFile as writeFile7, readdir as readdir5, mkdir as mkdir5 } from "fs/promises";
4694
+ import { existsSync as existsSync7 } from "fs";
4695
+ import { join as join12 } from "path";
4510
4696
  import { randomUUID as randomUUID3 } from "crypto";
4511
4697
  function tokenize(text) {
4512
4698
  return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3 && !STOPWORDS.has(t));
@@ -4654,12 +4840,12 @@ var init_memory = __esm({
4654
4840
  meta = /* @__PURE__ */ new Map();
4655
4841
  constructor(memoryDir) {
4656
4842
  this.memoryDir = memoryDir;
4657
- this.metaPath = join11(memoryDir, "_meta.json");
4843
+ this.metaPath = join12(memoryDir, "_meta.json");
4658
4844
  }
4659
4845
  /** Initialize — create dirs and load metadata */
4660
4846
  async init() {
4661
4847
  for (const cat of ["identity", "knowledge", "lessons", "context"]) {
4662
- await mkdir5(join11(this.memoryDir, cat), { recursive: true });
4848
+ await mkdir5(join12(this.memoryDir, cat), { recursive: true });
4663
4849
  }
4664
4850
  await this.loadMeta();
4665
4851
  }
@@ -4704,10 +4890,10 @@ var init_memory = __esm({
4704
4890
  let used = 0;
4705
4891
  if (projectRoot) {
4706
4892
  for (const name of [".clank.md", ".clankbuild.md", ".llamabuild.md"]) {
4707
- const path2 = join11(projectRoot, name);
4708
- if (existsSync5(path2)) {
4893
+ const path2 = join12(projectRoot, name);
4894
+ if (existsSync7(path2)) {
4709
4895
  try {
4710
- const content = await readFile8(path2, "utf-8");
4896
+ const content = await readFile9(path2, "utf-8");
4711
4897
  if (content.trim() && used + content.length < budgetChars) {
4712
4898
  parts.push("## Project Memory\n" + content.trim());
4713
4899
  used += content.length;
@@ -4718,10 +4904,10 @@ var init_memory = __esm({
4718
4904
  }
4719
4905
  }
4720
4906
  }
4721
- const globalPath = join11(this.memoryDir, "..", "workspace", "MEMORY.md");
4722
- if (existsSync5(globalPath)) {
4907
+ const globalPath = join12(this.memoryDir, "..", "workspace", "MEMORY.md");
4908
+ if (existsSync7(globalPath)) {
4723
4909
  try {
4724
- const content = await readFile8(globalPath, "utf-8");
4910
+ const content = await readFile9(globalPath, "utf-8");
4725
4911
  if (content.trim() && used + content.length < budgetChars) {
4726
4912
  parts.push("## Global Memory\n" + content.trim());
4727
4913
  used += content.length;
@@ -4742,8 +4928,8 @@ ${entry.content}`);
4742
4928
  async add(category, title, content) {
4743
4929
  const id = randomUUID3();
4744
4930
  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}
4931
+ const filePath = join12(this.memoryDir, category, filename);
4932
+ await writeFile7(filePath, `# ${title}
4747
4933
 
4748
4934
  ${content}`, "utf-8");
4749
4935
  this.meta.set(id, {
@@ -4788,15 +4974,15 @@ ${content}`, "utf-8");
4788
4974
  async loadAll() {
4789
4975
  const entries = [];
4790
4976
  for (const category of ["identity", "knowledge", "lessons", "context"]) {
4791
- const dir = join11(this.memoryDir, category);
4792
- if (!existsSync5(dir)) continue;
4977
+ const dir = join12(this.memoryDir, category);
4978
+ if (!existsSync7(dir)) continue;
4793
4979
  try {
4794
4980
  const files = await readdir5(dir);
4795
4981
  for (const file of files) {
4796
4982
  if (!file.endsWith(".md")) continue;
4797
- const filePath = join11(dir, file);
4983
+ const filePath = join12(dir, file);
4798
4984
  try {
4799
- const content = await readFile8(filePath, "utf-8");
4985
+ const content = await readFile9(filePath, "utf-8");
4800
4986
  const title = content.split("\n")[0]?.replace(/^#\s*/, "") || file;
4801
4987
  entries.push({
4802
4988
  id: file,
@@ -4821,9 +5007,9 @@ ${content}`, "utf-8");
4821
5007
  return recency * frequencyBoost;
4822
5008
  }
4823
5009
  async loadMeta() {
4824
- if (!existsSync5(this.metaPath)) return;
5010
+ if (!existsSync7(this.metaPath)) return;
4825
5011
  try {
4826
- const raw = await readFile8(this.metaPath, "utf-8");
5012
+ const raw = await readFile9(this.metaPath, "utf-8");
4827
5013
  const entries = JSON.parse(raw);
4828
5014
  for (const e of entries) this.meta.set(e.id, e);
4829
5015
  } catch {
@@ -4831,7 +5017,7 @@ ${content}`, "utf-8");
4831
5017
  }
4832
5018
  async saveMeta() {
4833
5019
  const entries = Array.from(this.meta.values());
4834
- await writeFile6(this.metaPath, JSON.stringify(entries, null, 2), "utf-8");
5020
+ await writeFile7(this.metaPath, JSON.stringify(entries, null, 2), "utf-8");
4835
5021
  }
4836
5022
  };
4837
5023
  }
@@ -4843,6 +5029,7 @@ var init_memory2 = __esm({
4843
5029
  "use strict";
4844
5030
  init_esm_shims();
4845
5031
  init_memory();
5032
+ init_auto_persist();
4846
5033
  }
4847
5034
  });
4848
5035
 
@@ -5038,15 +5225,53 @@ var init_telegram = __esm({
5038
5225
  if (!this.gateway) return;
5039
5226
  try {
5040
5227
  await ctx.api.sendChatAction(chatId, "typing");
5041
- const response = await this.gateway.handleInboundMessage(
5228
+ let streamMsgId = null;
5229
+ let accumulated = "";
5230
+ let lastEditTime = 0;
5231
+ const EDIT_INTERVAL = 800;
5232
+ const response = await this.gateway.handleInboundMessageStreaming(
5042
5233
  {
5043
5234
  channel: "telegram",
5044
5235
  peerId: chatId,
5045
5236
  peerKind: isGroup ? "group" : "dm"
5046
5237
  },
5047
- msg.text
5238
+ msg.text,
5239
+ {
5240
+ onToken: (content) => {
5241
+ accumulated += content;
5242
+ const now = Date.now();
5243
+ if (!streamMsgId && accumulated.length > 20) {
5244
+ bot.api.sendMessage(chatId, accumulated + " \u258D").then((sent) => {
5245
+ streamMsgId = sent.message_id;
5246
+ lastEditTime = now;
5247
+ }).catch(() => {
5248
+ });
5249
+ return;
5250
+ }
5251
+ if (streamMsgId && now - lastEditTime > EDIT_INTERVAL) {
5252
+ lastEditTime = now;
5253
+ const display = accumulated.length > 4e3 ? accumulated.slice(-3900) + " \u258D" : accumulated + " \u258D";
5254
+ bot.api.editMessageText(chatId, streamMsgId, display).catch(() => {
5255
+ });
5256
+ }
5257
+ },
5258
+ onToolStart: (name) => {
5259
+ if (!streamMsgId) {
5260
+ bot.api.sendChatAction(chatId, "typing").catch(() => {
5261
+ });
5262
+ }
5263
+ },
5264
+ onError: (message) => {
5265
+ bot.api.sendMessage(chatId, `Error: ${message.slice(0, 200)}`).catch(() => {
5266
+ });
5267
+ }
5268
+ }
5048
5269
  );
5049
- if (response) {
5270
+ if (streamMsgId && response) {
5271
+ const finalText = response.length > 4e3 ? response.slice(0, 3950) + "\n... (truncated)" : response;
5272
+ await bot.api.editMessageText(chatId, streamMsgId, finalText).catch(() => {
5273
+ });
5274
+ } else if (response && !streamMsgId) {
5050
5275
  const chunks = splitMessage(response, 4e3);
5051
5276
  for (const chunk of chunks) {
5052
5277
  await ctx.api.sendMessage(chatId, chunk);
@@ -5110,7 +5335,8 @@ var init_telegram = __esm({
5110
5335
  const { TTSEngine: TTSEngine2 } = await Promise.resolve().then(() => (init_voice(), voice_exports));
5111
5336
  const tts = new TTSEngine2(config);
5112
5337
  if (tts.isAvailable() && response.length < 2e3) {
5113
- const audio = await tts.synthesize(response);
5338
+ const agentVoice = config.agents.list.find((a) => a.voiceId)?.voiceId;
5339
+ const audio = await tts.synthesize(response, { voiceId: agentVoice });
5114
5340
  if (audio) {
5115
5341
  const { InputFile } = await import("grammy");
5116
5342
  await ctx.api.sendVoice(chatId, new InputFile(audio.audioBuffer, "reply.mp3"));
@@ -5132,6 +5358,94 @@ var init_telegram = __esm({
5132
5358
  });
5133
5359
  chatLocks.set(chatId, next);
5134
5360
  });
5361
+ bot.on("message:photo", async (ctx) => {
5362
+ const msg = ctx.message;
5363
+ const chatId = msg.chat.id;
5364
+ if (msg.date < startupTime - 30) return;
5365
+ if (telegramConfig.allowFrom && telegramConfig.allowFrom.length > 0) {
5366
+ const username = msg.from?.username ? `@${msg.from.username}` : "";
5367
+ const userIdStr = String(msg.from?.id || "");
5368
+ const allowed = telegramConfig.allowFrom.map(String);
5369
+ if (!allowed.some((a) => a === userIdStr || a.toLowerCase() === username.toLowerCase() || a.toLowerCase() === (msg.from?.username || "").toLowerCase())) return;
5370
+ }
5371
+ const processPhoto = async () => {
5372
+ if (!this.gateway) return;
5373
+ try {
5374
+ const photo = msg.photo[msg.photo.length - 1];
5375
+ const file = await bot.api.getFile(photo.file_id);
5376
+ const fileUrl = `https://api.telegram.org/file/bot${telegramConfig.botToken}/${file.file_path}`;
5377
+ const caption = msg.caption || "";
5378
+ const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
5379
+ const response = await this.gateway.handleInboundMessage(
5380
+ { channel: "telegram", peerId: chatId, peerKind: isGroup ? "group" : "dm" },
5381
+ `[Image received: ${fileUrl}]${caption ? ` Caption: ${caption}` : ""}
5382
+
5383
+ Describe or analyze the image if you can, or acknowledge it.`
5384
+ );
5385
+ if (response) {
5386
+ const chunks = splitMessage(response, 4e3);
5387
+ for (const chunk of chunks) await ctx.api.sendMessage(chatId, chunk);
5388
+ }
5389
+ } catch (err) {
5390
+ await ctx.api.sendMessage(chatId, `Error: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
5391
+ }
5392
+ };
5393
+ const prev = chatLocks.get(chatId) || Promise.resolve();
5394
+ chatLocks.set(chatId, prev.then(processPhoto).catch(() => {
5395
+ }));
5396
+ });
5397
+ bot.on("message:document", async (ctx) => {
5398
+ const msg = ctx.message;
5399
+ const chatId = msg.chat.id;
5400
+ if (msg.date < startupTime - 30) return;
5401
+ if (telegramConfig.allowFrom && telegramConfig.allowFrom.length > 0) {
5402
+ const username = msg.from?.username ? `@${msg.from.username}` : "";
5403
+ const userIdStr = String(msg.from?.id || "");
5404
+ const allowed = telegramConfig.allowFrom.map(String);
5405
+ if (!allowed.some((a) => a === userIdStr || a.toLowerCase() === username.toLowerCase() || a.toLowerCase() === (msg.from?.username || "").toLowerCase())) return;
5406
+ }
5407
+ const processDoc = async () => {
5408
+ if (!this.gateway) return;
5409
+ try {
5410
+ const doc = msg.document;
5411
+ if (!doc) return;
5412
+ if (doc.file_size && doc.file_size > 10 * 1024 * 1024) {
5413
+ await ctx.api.sendMessage(chatId, "File too large (max 10MB).");
5414
+ return;
5415
+ }
5416
+ const file = await bot.api.getFile(doc.file_id);
5417
+ const fileUrl = `https://api.telegram.org/file/bot${telegramConfig.botToken}/${file.file_path}`;
5418
+ const res = await fetch(fileUrl);
5419
+ if (!res.ok) {
5420
+ await ctx.api.sendMessage(chatId, "Could not download file.");
5421
+ return;
5422
+ }
5423
+ const { writeFile: wf } = await import("fs/promises");
5424
+ const { join: join20 } = await import("path");
5425
+ const { tmpdir } = await import("os");
5426
+ const safeName = (doc.file_name || "file").replace(/[^a-zA-Z0-9._-]/g, "_");
5427
+ const savePath = join20(tmpdir(), `clank-upload-${Date.now()}-${safeName}`);
5428
+ await wf(savePath, Buffer.from(await res.arrayBuffer()));
5429
+ const caption = msg.caption || "";
5430
+ const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
5431
+ const response = await this.gateway.handleInboundMessage(
5432
+ { channel: "telegram", peerId: chatId, peerKind: isGroup ? "group" : "dm" },
5433
+ `[File received: "${doc.file_name}" saved to ${savePath}]${caption ? ` Note: ${caption}` : ""}
5434
+
5435
+ You can read this file with the read_file tool.`
5436
+ );
5437
+ if (response) {
5438
+ const chunks = splitMessage(response, 4e3);
5439
+ for (const chunk of chunks) await ctx.api.sendMessage(chatId, chunk);
5440
+ }
5441
+ } catch (err) {
5442
+ await ctx.api.sendMessage(chatId, `Error: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
5443
+ }
5444
+ };
5445
+ const prev = chatLocks.get(chatId) || Promise.resolve();
5446
+ chatLocks.set(chatId, prev.then(processDoc).catch(() => {
5447
+ }));
5448
+ });
5135
5449
  bot.start({
5136
5450
  onStart: () => {
5137
5451
  this.running = true;
@@ -5374,9 +5688,9 @@ var init_web = __esm({
5374
5688
  });
5375
5689
 
5376
5690
  // 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";
5691
+ import { readdir as readdir6, readFile as readFile10 } from "fs/promises";
5692
+ import { existsSync as existsSync8 } from "fs";
5693
+ import { join as join13 } from "path";
5380
5694
  import { homedir as homedir2 } from "os";
5381
5695
  var PluginLoader;
5382
5696
  var init_loader = __esm({
@@ -5409,25 +5723,25 @@ var init_loader = __esm({
5409
5723
  /** Discover plugin directories */
5410
5724
  async discoverPlugins() {
5411
5725
  const dirs = [];
5412
- const userPluginDir = join12(homedir2(), ".clank", "plugins");
5413
- if (existsSync6(userPluginDir)) {
5726
+ const userPluginDir = join13(homedir2(), ".clank", "plugins");
5727
+ if (existsSync8(userPluginDir)) {
5414
5728
  try {
5415
5729
  const entries = await readdir6(userPluginDir, { withFileTypes: true });
5416
5730
  for (const entry of entries) {
5417
5731
  if (entry.isDirectory()) {
5418
- dirs.push(join12(userPluginDir, entry.name));
5732
+ dirs.push(join13(userPluginDir, entry.name));
5419
5733
  }
5420
5734
  }
5421
5735
  } catch {
5422
5736
  }
5423
5737
  }
5424
- const nodeModulesDir = join12(process.cwd(), "node_modules");
5425
- if (existsSync6(nodeModulesDir)) {
5738
+ const nodeModulesDir = join13(process.cwd(), "node_modules");
5739
+ if (existsSync8(nodeModulesDir)) {
5426
5740
  try {
5427
5741
  const entries = await readdir6(nodeModulesDir);
5428
5742
  for (const entry of entries) {
5429
5743
  if (entry.startsWith("clank-plugin-")) {
5430
- dirs.push(join12(nodeModulesDir, entry));
5744
+ dirs.push(join13(nodeModulesDir, entry));
5431
5745
  }
5432
5746
  }
5433
5747
  } catch {
@@ -5437,9 +5751,9 @@ var init_loader = __esm({
5437
5751
  }
5438
5752
  /** Load a single plugin from a directory */
5439
5753
  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");
5754
+ const manifestPath = join13(dir, "clank-plugin.json");
5755
+ if (!existsSync8(manifestPath)) return null;
5756
+ const raw = await readFile10(manifestPath, "utf-8");
5443
5757
  const manifest = JSON.parse(raw);
5444
5758
  if (!manifest.name) return null;
5445
5759
  const plugin = {
@@ -5451,7 +5765,7 @@ var init_loader = __esm({
5451
5765
  if (manifest.tools) {
5452
5766
  for (const toolEntry of manifest.tools) {
5453
5767
  try {
5454
- const entrypoint = join12(dir, toolEntry.entrypoint);
5768
+ const entrypoint = join13(dir, toolEntry.entrypoint);
5455
5769
  const mod = await import(entrypoint);
5456
5770
  const tool = mod.default || mod.tool;
5457
5771
  if (tool) {
@@ -5465,7 +5779,7 @@ var init_loader = __esm({
5465
5779
  if (manifest.hooks) {
5466
5780
  for (const hookEntry of manifest.hooks) {
5467
5781
  try {
5468
- const handlerPath = join12(dir, hookEntry.handler);
5782
+ const handlerPath = join13(dir, hookEntry.handler);
5469
5783
  const mod = await import(handlerPath);
5470
5784
  const handler = mod.default || mod.handler;
5471
5785
  if (handler) {
@@ -5523,8 +5837,8 @@ var init_plugins = __esm({
5523
5837
  // src/gateway/server.ts
5524
5838
  import { createServer } from "http";
5525
5839
  import { WebSocketServer, WebSocket } from "ws";
5526
- import { readFile as readFile10 } from "fs/promises";
5527
- import { join as join13, dirname as dirname2 } from "path";
5840
+ import { readFile as readFile11 } from "fs/promises";
5841
+ import { join as join14, dirname as dirname2 } from "path";
5528
5842
  import { fileURLToPath as fileURLToPath2 } from "url";
5529
5843
  var GatewayServer;
5530
5844
  var init_server = __esm({
@@ -5558,12 +5872,18 @@ var init_server = __esm({
5558
5872
  pluginLoader;
5559
5873
  adapters = [];
5560
5874
  running = false;
5875
+ /** Rate limiting: track message timestamps per session */
5876
+ rateLimiter = /* @__PURE__ */ new Map();
5877
+ RATE_LIMIT_WINDOW = 6e4;
5878
+ // 1 minute
5879
+ RATE_LIMIT_MAX = 20;
5880
+ // max 20 messages per minute per session
5561
5881
  constructor(config) {
5562
5882
  this.config = config;
5563
- this.sessionStore = new SessionStore(join13(getConfigDir(), "conversations"));
5883
+ this.sessionStore = new SessionStore(join14(getConfigDir(), "conversations"));
5564
5884
  this.toolRegistry = createFullRegistry();
5565
- this.memoryManager = new MemoryManager(join13(getConfigDir(), "memory"));
5566
- this.cronScheduler = new CronScheduler(join13(getConfigDir(), "cron"));
5885
+ this.memoryManager = new MemoryManager(join14(getConfigDir(), "memory"));
5886
+ this.cronScheduler = new CronScheduler(join14(getConfigDir(), "cron"));
5567
5887
  this.configWatcher = new ConfigWatcher();
5568
5888
  this.pluginLoader = new PluginLoader();
5569
5889
  }
@@ -5633,6 +5953,10 @@ var init_server = __esm({
5633
5953
  * This is the main entry point for all non-WebSocket messages.
5634
5954
  */
5635
5955
  async handleInboundMessage(context, text) {
5956
+ const rlKey = deriveSessionKey(context);
5957
+ if (this.isRateLimited(rlKey)) {
5958
+ throw new Error("Rate limited \u2014 too many messages. Wait a moment.");
5959
+ }
5636
5960
  const agentId = resolveRoute(
5637
5961
  context,
5638
5962
  [],
@@ -5644,6 +5968,58 @@ var init_server = __esm({
5644
5968
  const engine = await this.getOrCreateEngine(sessionKey, agentId, context.channel);
5645
5969
  return engine.sendMessage(text);
5646
5970
  }
5971
+ /**
5972
+ * Handle an inbound message with streaming callbacks.
5973
+ * Used by channel adapters for real-time streaming (e.g., Telegram message editing).
5974
+ */
5975
+ async handleInboundMessageStreaming(context, text, callbacks) {
5976
+ const rlKey = deriveSessionKey(context);
5977
+ if (this.isRateLimited(rlKey)) {
5978
+ throw new Error("Rate limited \u2014 too many messages. Wait a moment.");
5979
+ }
5980
+ return this._handleInboundMessageStreamingInner(context, text, callbacks);
5981
+ }
5982
+ async _handleInboundMessageStreamingInner(context, text, callbacks) {
5983
+ const agentId = resolveRoute(
5984
+ context,
5985
+ [],
5986
+ this.config.agents.list.map((a) => ({ id: a.id, name: a.name })),
5987
+ this.config.agents.list[0]?.id || "default"
5988
+ );
5989
+ const sessionKey = deriveSessionKey(context);
5990
+ const engine = await this.getOrCreateEngine(sessionKey, agentId, context.channel);
5991
+ const listeners = [];
5992
+ if (callbacks.onToken) {
5993
+ const fn = (data) => callbacks.onToken(data.content);
5994
+ engine.on("token", fn);
5995
+ listeners.push(["token", fn]);
5996
+ }
5997
+ if (callbacks.onToolStart) {
5998
+ const fn = (data) => callbacks.onToolStart(data.name);
5999
+ engine.on("tool-start", fn);
6000
+ listeners.push(["tool-start", fn]);
6001
+ }
6002
+ if (callbacks.onToolResult) {
6003
+ const fn = (data) => {
6004
+ const d = data;
6005
+ callbacks.onToolResult(d.name, d.success);
6006
+ };
6007
+ engine.on("tool-result", fn);
6008
+ listeners.push(["tool-result", fn]);
6009
+ }
6010
+ if (callbacks.onError) {
6011
+ const fn = (data) => callbacks.onError(data.message);
6012
+ engine.on("error", fn);
6013
+ listeners.push(["error", fn]);
6014
+ }
6015
+ try {
6016
+ return await engine.sendMessage(text);
6017
+ } finally {
6018
+ for (const [event, fn] of listeners) {
6019
+ engine.removeListener(event, fn);
6020
+ }
6021
+ }
6022
+ }
5647
6023
  /** Stop the gateway server */
5648
6024
  async stop() {
5649
6025
  this.running = false;
@@ -5677,7 +6053,7 @@ var init_server = __esm({
5677
6053
  res.writeHead(200, { "Content-Type": "application/json" });
5678
6054
  res.end(JSON.stringify({
5679
6055
  status: "ok",
5680
- version: "1.3.1",
6056
+ version: "1.4.1",
5681
6057
  uptime: process.uptime(),
5682
6058
  clients: this.clients.size,
5683
6059
  agents: this.engines.size
@@ -5707,14 +6083,14 @@ var init_server = __esm({
5707
6083
  if (url === "/chat" || url === "/") {
5708
6084
  try {
5709
6085
  const __dirname4 = dirname2(fileURLToPath2(import.meta.url));
5710
- const htmlPath = join13(__dirname4, "..", "web", "index.html");
5711
- const html = await readFile10(htmlPath, "utf-8");
6086
+ const htmlPath = join14(__dirname4, "..", "web", "index.html");
6087
+ const html = await readFile11(htmlPath, "utf-8");
5712
6088
  res.writeHead(200, { "Content-Type": "text/html" });
5713
6089
  res.end(html);
5714
6090
  return;
5715
6091
  } catch {
5716
6092
  try {
5717
- const html = await readFile10(join13(process.cwd(), "src", "web", "index.html"), "utf-8");
6093
+ const html = await readFile11(join14(process.cwd(), "src", "web", "index.html"), "utf-8");
5718
6094
  res.writeHead(200, { "Content-Type": "text/html" });
5719
6095
  res.end(html);
5720
6096
  return;
@@ -5785,7 +6161,7 @@ var init_server = __esm({
5785
6161
  const hello = {
5786
6162
  type: "hello",
5787
6163
  protocol: PROTOCOL_VERSION,
5788
- version: "1.3.1",
6164
+ version: "1.4.1",
5789
6165
  agents: this.config.agents.list.map((a) => ({
5790
6166
  id: a.id,
5791
6167
  name: a.name || a.id,
@@ -5959,6 +6335,10 @@ var init_server = __esm({
5959
6335
  this.sendResponse(client, frame.id, false, void 0, "message is required");
5960
6336
  return;
5961
6337
  }
6338
+ if (this.isRateLimited(client.sessionKey)) {
6339
+ this.sendResponse(client, frame.id, false, void 0, "Rate limited \u2014 too many messages. Wait a moment.");
6340
+ return;
6341
+ }
5962
6342
  try {
5963
6343
  const engine = await this.getOrCreateEngine(client.sessionKey, client.agentId, client.clientName);
5964
6344
  const cleanup = this.wireEngineEvents(engine, client);
@@ -5973,6 +6353,15 @@ var init_server = __esm({
5973
6353
  this.sendResponse(client, frame.id, false, void 0, msg);
5974
6354
  }
5975
6355
  }
6356
+ /** Check if a session is rate limited */
6357
+ isRateLimited(sessionKey) {
6358
+ const now = Date.now();
6359
+ const timestamps = this.rateLimiter.get(sessionKey) || [];
6360
+ const recent = timestamps.filter((t) => now - t < this.RATE_LIMIT_WINDOW);
6361
+ recent.push(now);
6362
+ this.rateLimiter.set(sessionKey, recent);
6363
+ return recent.length > this.RATE_LIMIT_MAX;
6364
+ }
5976
6365
  /** Cancel current request for a client */
5977
6366
  handleCancel(client) {
5978
6367
  const engine = this.engines.get(client.sessionKey);
@@ -5998,10 +6387,14 @@ var init_server = __esm({
5998
6387
  toolTier: agentConfig?.toolTier || this.config.agents.defaults.toolTier || "auto",
5999
6388
  tools: agentConfig?.tools
6000
6389
  };
6390
+ const compact = agentConfig?.compactPrompt ?? this.config.agents.defaults.compactPrompt ?? false;
6391
+ const thinking = agentConfig?.thinking ?? this.config.agents.defaults.thinking ?? "auto";
6001
6392
  const systemPrompt = await buildSystemPrompt({
6002
6393
  identity,
6003
6394
  workspaceDir: identity.workspace,
6004
- channel
6395
+ channel,
6396
+ compact,
6397
+ thinking
6005
6398
  });
6006
6399
  const memoryBlock = await this.memoryManager.buildMemoryBlock("", identity.workspace);
6007
6400
  const fullPrompt = memoryBlock ? systemPrompt + "\n\n---\n\n" + memoryBlock : systemPrompt;
@@ -6116,9 +6509,9 @@ __export(gateway_cmd_exports, {
6116
6509
  isGatewayRunning: () => isGatewayRunning
6117
6510
  });
6118
6511
  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";
6512
+ import { writeFile as writeFile8, readFile as readFile12, unlink as unlink3 } from "fs/promises";
6513
+ import { existsSync as existsSync9 } from "fs";
6514
+ import { join as join15, dirname as dirname3 } from "path";
6122
6515
  import { fileURLToPath as fileURLToPath3 } from "url";
6123
6516
  async function isGatewayRunning(port) {
6124
6517
  const config = await loadConfig();
@@ -6131,7 +6524,7 @@ async function isGatewayRunning(port) {
6131
6524
  }
6132
6525
  }
6133
6526
  function pidFilePath() {
6134
- return join14(getConfigDir(), "gateway.pid");
6527
+ return join15(getConfigDir(), "gateway.pid");
6135
6528
  }
6136
6529
  async function gatewayStartForeground(opts) {
6137
6530
  await ensureConfigDir();
@@ -6143,7 +6536,7 @@ async function gatewayStartForeground(opts) {
6143
6536
  console.log(green2(` Gateway already running on port ${config.gateway.port}`));
6144
6537
  return;
6145
6538
  }
6146
- await writeFile7(pidFilePath(), String(process.pid), "utf-8");
6539
+ await writeFile8(pidFilePath(), String(process.pid), "utf-8");
6147
6540
  const server = new GatewayServer(config);
6148
6541
  const shutdown = async () => {
6149
6542
  console.log(dim2("\nShutting down..."));
@@ -6176,10 +6569,10 @@ async function gatewayStartBackground() {
6176
6569
  return true;
6177
6570
  }
6178
6571
  console.log(dim2(" Starting gateway in background..."));
6179
- const entryPoint = join14(dirname3(__filename2), "index.js");
6180
- const logFile = join14(getConfigDir(), "logs", "gateway.log");
6572
+ const entryPoint = join15(dirname3(__filename2), "index.js");
6573
+ const logFile = join15(getConfigDir(), "logs", "gateway.log");
6181
6574
  const { mkdir: mkdir7 } = await import("fs/promises");
6182
- await mkdir7(join14(getConfigDir(), "logs"), { recursive: true });
6575
+ await mkdir7(join15(getConfigDir(), "logs"), { recursive: true });
6183
6576
  const child = fork(entryPoint, ["gateway", "start", "--foreground"], {
6184
6577
  detached: true,
6185
6578
  stdio: ["ignore", "ignore", "ignore", "ipc"]
@@ -6208,9 +6601,9 @@ async function gatewayStart(opts) {
6208
6601
  }
6209
6602
  async function gatewayStop() {
6210
6603
  const pidPath = pidFilePath();
6211
- if (existsSync7(pidPath)) {
6604
+ if (existsSync9(pidPath)) {
6212
6605
  try {
6213
- const pid = parseInt(await readFile11(pidPath, "utf-8"), 10);
6606
+ const pid = parseInt(await readFile12(pidPath, "utf-8"), 10);
6214
6607
  process.kill(pid, "SIGTERM");
6215
6608
  await unlink3(pidPath);
6216
6609
  console.log(green2("Gateway stopped"));
@@ -6241,8 +6634,8 @@ async function gatewayStatus() {
6241
6634
  console.log(dim2(` Clients: ${data.clients?.length || 0}`));
6242
6635
  console.log(dim2(` Sessions: ${data.sessions?.length || 0}`));
6243
6636
  const pidPath = pidFilePath();
6244
- if (existsSync7(pidPath)) {
6245
- const pid = await readFile11(pidPath, "utf-8");
6637
+ if (existsSync9(pidPath)) {
6638
+ const pid = await readFile12(pidPath, "utf-8");
6246
6639
  console.log(dim2(` PID: ${pid.trim()}`));
6247
6640
  }
6248
6641
  } else {
@@ -6270,8 +6663,8 @@ var init_gateway_cmd = __esm({
6270
6663
  });
6271
6664
 
6272
6665
  // 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";
6666
+ import { writeFile as writeFile9, mkdir as mkdir6, unlink as unlink4 } from "fs/promises";
6667
+ import { join as join16 } from "path";
6275
6668
  import { homedir as homedir3, platform as platform4 } from "os";
6276
6669
  import { execSync } from "child_process";
6277
6670
  async function installDaemon() {
@@ -6339,8 +6732,8 @@ async function daemonStatus() {
6339
6732
  }
6340
6733
  }
6341
6734
  async function installLaunchd() {
6342
- const plistDir = join15(homedir3(), "Library", "LaunchAgents");
6343
- const plistPath = join15(plistDir, "com.clank.gateway.plist");
6735
+ const plistDir = join16(homedir3(), "Library", "LaunchAgents");
6736
+ const plistPath = join16(plistDir, "com.clank.gateway.plist");
6344
6737
  const clankPath = execSync("which clank || echo clank", { encoding: "utf-8" }).trim();
6345
6738
  await mkdir6(plistDir, { recursive: true });
6346
6739
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
@@ -6361,18 +6754,18 @@ async function installLaunchd() {
6361
6754
  <key>KeepAlive</key>
6362
6755
  <true/>
6363
6756
  <key>StandardOutPath</key>
6364
- <string>${join15(homedir3(), ".clank", "logs", "gateway.log")}</string>
6757
+ <string>${join16(homedir3(), ".clank", "logs", "gateway.log")}</string>
6365
6758
  <key>StandardErrorPath</key>
6366
- <string>${join15(homedir3(), ".clank", "logs", "gateway-error.log")}</string>
6759
+ <string>${join16(homedir3(), ".clank", "logs", "gateway-error.log")}</string>
6367
6760
  </dict>
6368
6761
  </plist>`;
6369
- await writeFile8(plistPath, plist, "utf-8");
6762
+ await writeFile9(plistPath, plist, "utf-8");
6370
6763
  execSync(`launchctl load "${plistPath}"`);
6371
6764
  console.log(green3("Daemon installed (launchd)"));
6372
6765
  console.log(dim3(` Plist: ${plistPath}`));
6373
6766
  }
6374
6767
  async function uninstallLaunchd() {
6375
- const plistPath = join15(homedir3(), "Library", "LaunchAgents", "com.clank.gateway.plist");
6768
+ const plistPath = join16(homedir3(), "Library", "LaunchAgents", "com.clank.gateway.plist");
6376
6769
  try {
6377
6770
  execSync(`launchctl unload "${plistPath}"`);
6378
6771
  await unlink4(plistPath);
@@ -6410,8 +6803,8 @@ async function uninstallTaskScheduler() {
6410
6803
  }
6411
6804
  }
6412
6805
  async function installSystemd() {
6413
- const unitDir = join15(homedir3(), ".config", "systemd", "user");
6414
- const unitPath = join15(unitDir, "clank-gateway.service");
6806
+ const unitDir = join16(homedir3(), ".config", "systemd", "user");
6807
+ const unitPath = join16(unitDir, "clank-gateway.service");
6415
6808
  const clankPath = execSync("which clank || echo clank", { encoding: "utf-8" }).trim();
6416
6809
  await mkdir6(unitDir, { recursive: true });
6417
6810
  const unit = `[Unit]
@@ -6426,7 +6819,7 @@ RestartSec=5
6426
6819
  [Install]
6427
6820
  WantedBy=default.target
6428
6821
  `;
6429
- await writeFile8(unitPath, unit, "utf-8");
6822
+ await writeFile9(unitPath, unit, "utf-8");
6430
6823
  execSync("systemctl --user daemon-reload");
6431
6824
  execSync("systemctl --user enable clank-gateway");
6432
6825
  execSync("systemctl --user start clank-gateway");
@@ -6437,7 +6830,7 @@ async function uninstallSystemd() {
6437
6830
  try {
6438
6831
  execSync("systemctl --user stop clank-gateway");
6439
6832
  execSync("systemctl --user disable clank-gateway");
6440
- const unitPath = join15(homedir3(), ".config", "systemd", "user", "clank-gateway.service");
6833
+ const unitPath = join16(homedir3(), ".config", "systemd", "user", "clank-gateway.service");
6441
6834
  await unlink4(unitPath);
6442
6835
  execSync("systemctl --user daemon-reload");
6443
6836
  console.log(green3("Daemon uninstalled"));
@@ -6478,7 +6871,7 @@ __export(setup_exports, {
6478
6871
  });
6479
6872
  import { createInterface as createInterface2 } from "readline";
6480
6873
  import { randomBytes } from "crypto";
6481
- import { dirname as dirname4, join as join16 } from "path";
6874
+ import { dirname as dirname4, join as join17 } from "path";
6482
6875
  import { fileURLToPath as fileURLToPath4 } from "url";
6483
6876
  function ask(rl, question) {
6484
6877
  return new Promise((resolve4) => rl.question(question, resolve4));
@@ -6581,8 +6974,8 @@ async function runSetup(opts) {
6581
6974
  console.log("");
6582
6975
  console.log(dim4(" Creating workspace..."));
6583
6976
  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");
6977
+ const templateDir = join17(__dirname2, "..", "workspace", "templates");
6978
+ const wsDir = join17(getConfigDir(), "workspace");
6586
6979
  try {
6587
6980
  await ensureWorkspaceFiles2(wsDir, templateDir);
6588
6981
  } catch {
@@ -6743,9 +7136,9 @@ var fix_exports = {};
6743
7136
  __export(fix_exports, {
6744
7137
  runFix: () => runFix
6745
7138
  });
6746
- import { existsSync as existsSync8 } from "fs";
7139
+ import { existsSync as existsSync10 } from "fs";
6747
7140
  import { readdir as readdir7 } from "fs/promises";
6748
- import { join as join17 } from "path";
7141
+ import { join as join18 } from "path";
6749
7142
  async function runFix(opts) {
6750
7143
  console.log("");
6751
7144
  console.log(" Clank Diagnostics");
@@ -6775,7 +7168,7 @@ async function runFix(opts) {
6775
7168
  }
6776
7169
  async function checkConfig() {
6777
7170
  const configPath = getConfigPath();
6778
- if (!existsSync8(configPath)) {
7171
+ if (!existsSync10(configPath)) {
6779
7172
  return {
6780
7173
  name: "Config",
6781
7174
  status: "warn",
@@ -6843,8 +7236,8 @@ async function checkModels() {
6843
7236
  return { name: "Model (primary)", status: "ok", message: modelId };
6844
7237
  }
6845
7238
  async function checkSessions() {
6846
- const sessDir = join17(getConfigDir(), "conversations");
6847
- if (!existsSync8(sessDir)) {
7239
+ const sessDir = join18(getConfigDir(), "conversations");
7240
+ if (!existsSync10(sessDir)) {
6848
7241
  return { name: "Sessions", status: "ok", message: "no sessions yet" };
6849
7242
  }
6850
7243
  try {
@@ -6856,8 +7249,8 @@ async function checkSessions() {
6856
7249
  }
6857
7250
  }
6858
7251
  async function checkWorkspace() {
6859
- const wsDir = join17(getConfigDir(), "workspace");
6860
- if (!existsSync8(wsDir)) {
7252
+ const wsDir = join18(getConfigDir(), "workspace");
7253
+ if (!existsSync10(wsDir)) {
6861
7254
  return {
6862
7255
  name: "Workspace",
6863
7256
  status: "warn",
@@ -7150,7 +7543,7 @@ async function runTui(opts) {
7150
7543
  ws.on("open", () => {
7151
7544
  ws.send(JSON.stringify({
7152
7545
  type: "connect",
7153
- params: { auth: { token }, mode: "tui", version: "1.3.1" }
7546
+ params: { auth: { token }, mode: "tui", version: "1.4.1" }
7154
7547
  }));
7155
7548
  });
7156
7549
  ws.on("message", (data) => {
@@ -7499,7 +7892,7 @@ __export(uninstall_exports, {
7499
7892
  });
7500
7893
  import { createInterface as createInterface4 } from "readline";
7501
7894
  import { rm } from "fs/promises";
7502
- import { existsSync as existsSync9 } from "fs";
7895
+ import { existsSync as existsSync11 } from "fs";
7503
7896
  async function runUninstall(opts) {
7504
7897
  const configDir = getConfigDir();
7505
7898
  console.log("");
@@ -7538,7 +7931,7 @@ async function runUninstall(opts) {
7538
7931
  } catch {
7539
7932
  }
7540
7933
  console.log(dim10(" Deleting data..."));
7541
- if (existsSync9(configDir)) {
7934
+ if (existsSync11(configDir)) {
7542
7935
  await rm(configDir, { recursive: true, force: true });
7543
7936
  console.log(green10(` Removed ${configDir}`));
7544
7937
  } else {
@@ -7576,12 +7969,12 @@ init_esm_shims();
7576
7969
  import { Command } from "commander";
7577
7970
  import { readFileSync } from "fs";
7578
7971
  import { fileURLToPath as fileURLToPath5 } from "url";
7579
- import { dirname as dirname5, join as join18 } from "path";
7972
+ import { dirname as dirname5, join as join19 } from "path";
7580
7973
  var __filename3 = fileURLToPath5(import.meta.url);
7581
7974
  var __dirname3 = dirname5(__filename3);
7582
- var version = "1.3.1";
7975
+ var version = "1.4.1";
7583
7976
  try {
7584
- const pkg = JSON.parse(readFileSync(join18(__dirname3, "..", "package.json"), "utf-8"));
7977
+ const pkg = JSON.parse(readFileSync(join19(__dirname3, "..", "package.json"), "utf-8"));
7585
7978
  version = pkg.version;
7586
7979
  } catch {
7587
7980
  }
@@ -7690,10 +8083,10 @@ pipeline.command("status <id>").description("Check pipeline execution status").a
7690
8083
  });
7691
8084
  var cron = program.command("cron").description("Manage scheduled jobs");
7692
8085
  cron.command("list").description("List cron jobs").action(async () => {
7693
- const { join: join19 } = await import("path");
8086
+ const { join: join20 } = await import("path");
7694
8087
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7695
8088
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
7696
- const scheduler = new CronScheduler2(join19(getConfigDir3(), "cron"));
8089
+ const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
7697
8090
  await scheduler.init();
7698
8091
  const jobs = scheduler.listJobs();
7699
8092
  if (jobs.length === 0) {
@@ -7705,10 +8098,10 @@ cron.command("list").description("List cron jobs").action(async () => {
7705
8098
  }
7706
8099
  });
7707
8100
  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");
8101
+ const { join: join20 } = await import("path");
7709
8102
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7710
8103
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
7711
- const scheduler = new CronScheduler2(join19(getConfigDir3(), "cron"));
8104
+ const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
7712
8105
  await scheduler.init();
7713
8106
  const job = await scheduler.addJob({
7714
8107
  name: opts.name || "CLI Job",
@@ -7719,10 +8112,10 @@ cron.command("add").description("Add a cron job").requiredOption("--schedule <ex
7719
8112
  console.log(` Job created: ${job.id.slice(0, 8)} \u2014 "${job.name}" every ${job.schedule}`);
7720
8113
  });
7721
8114
  cron.command("remove <id>").description("Remove a cron job").action(async (id) => {
7722
- const { join: join19 } = await import("path");
8115
+ const { join: join20 } = await import("path");
7723
8116
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7724
8117
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
7725
- const scheduler = new CronScheduler2(join19(getConfigDir3(), "cron"));
8118
+ const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
7726
8119
  await scheduler.init();
7727
8120
  const removed = await scheduler.removeJob(id);
7728
8121
  console.log(removed ? ` Job ${id.slice(0, 8)} removed` : ` Job not found`);