jinzd-ai-cli 0.4.15 → 0.4.17

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.
@@ -129,6 +129,61 @@ var UndoStack = class {
129
129
  };
130
130
  var undoStack = new UndoStack();
131
131
 
132
+ // src/core/errors.ts
133
+ var AiCliError = class extends Error {
134
+ constructor(message, options) {
135
+ super(message, options);
136
+ this.name = "AiCliError";
137
+ }
138
+ };
139
+ var ProviderError = class extends AiCliError {
140
+ constructor(providerId, message, cause) {
141
+ super(`[${providerId}] ${message}`, cause !== void 0 ? { cause } : void 0);
142
+ this.providerId = providerId;
143
+ this.name = "ProviderError";
144
+ }
145
+ };
146
+ var AuthError = class extends ProviderError {
147
+ constructor(providerId) {
148
+ super(providerId, "Invalid or missing API key. Run: ai-cli config");
149
+ this.name = "AuthError";
150
+ }
151
+ };
152
+ var RateLimitError = class extends ProviderError {
153
+ constructor(providerId) {
154
+ super(providerId, "Rate limit exceeded. Please wait before trying again.");
155
+ this.name = "RateLimitError";
156
+ }
157
+ };
158
+ var ConfigError = class extends AiCliError {
159
+ constructor(message) {
160
+ super(message);
161
+ this.name = "ConfigError";
162
+ }
163
+ };
164
+ var ProviderNotFoundError = class extends AiCliError {
165
+ constructor(providerId) {
166
+ super(
167
+ `Provider '${providerId}' is not configured. Run: ai-cli config`
168
+ );
169
+ this.name = "ProviderNotFoundError";
170
+ }
171
+ };
172
+ var ToolError = class extends AiCliError {
173
+ constructor(toolName, message, cause) {
174
+ super(`[${toolName}] ${message}`, cause !== void 0 ? { cause } : void 0);
175
+ this.toolName = toolName;
176
+ this.name = "ToolError";
177
+ }
178
+ };
179
+ var NetworkError = class extends AiCliError {
180
+ constructor(message, statusCode, cause) {
181
+ super(message, cause !== void 0 ? { cause } : void 0);
182
+ this.statusCode = statusCode;
183
+ this.name = "NetworkError";
184
+ }
185
+ };
186
+
132
187
  // src/tools/builtin/bash.ts
133
188
  var IS_WINDOWS = platform() === "win32";
134
189
  var SHELL = IS_WINDOWS ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
@@ -174,7 +229,7 @@ Important rules:
174
229
  const timeout = Math.min(Math.max(Number(args["timeout"] ?? 3e4), 1e3), MAX_TIMEOUT);
175
230
  const cwdArg = args["cwd"] ? String(args["cwd"]) : void 0;
176
231
  if (!command.trim()) {
177
- throw new Error("command is required");
232
+ throw new ToolError("bash", "command is required");
178
233
  }
179
234
  if (!existsSync2(persistentCwd)) {
180
235
  const fallback = process.cwd();
@@ -188,7 +243,8 @@ Important rules:
188
243
  if (cwdArg) {
189
244
  const resolved = resolve(persistentCwd, cwdArg);
190
245
  if (!existsSync2(resolved)) {
191
- throw new Error(
246
+ throw new ToolError(
247
+ "bash",
192
248
  `cwd directory does not exist: "${resolved}". Create it first (e.g. mkdir -p "${resolved}") before specifying it as cwd.`
193
249
  );
194
250
  }
@@ -232,7 +288,8 @@ Important rules:
232
288
  const stderr = IS_WINDOWS && Buffer.isBuffer(execErr.stderr) ? execErr.stderr.toString("utf-8").trim() : execErr.stderr?.toString().trim() ?? "";
233
289
  const stdout = IS_WINDOWS && Buffer.isBuffer(execErr.stdout) ? execErr.stdout.toString("utf-8").trim() : execErr.stdout?.toString().trim() ?? "";
234
290
  const combined = [stdout, stderr].filter(Boolean).join("\n");
235
- throw new Error(
291
+ throw new ToolError(
292
+ "bash",
236
293
  `Exit code ${execErr.status}:
237
294
  ${combined || (execErr.message ?? "Unknown error")}
238
295
 
@@ -479,12 +536,13 @@ var readFileTool = {
479
536
  async execute(args) {
480
537
  const filePath = String(args["path"] ?? "");
481
538
  const encoding = args["encoding"] ?? "utf-8";
482
- if (!filePath) throw new Error("path is required");
539
+ if (!filePath) throw new ToolError("read_file", "path is required");
483
540
  const normalizedPath = resolve2(filePath);
484
541
  if (!existsSync3(normalizedPath)) {
485
542
  const suggestions = findSimilarFiles(filePath);
486
543
  if (suggestions.length > 0) {
487
- throw new Error(
544
+ throw new ToolError(
545
+ "read_file",
488
546
  `File not found: ${filePath}
489
547
  Current working directory: ${process.cwd()}
490
548
  Found similar files, did you mean:
@@ -492,7 +550,8 @@ Found similar files, did you mean:
492
550
  Please retry with the correct relative path.`
493
551
  );
494
552
  }
495
- throw new Error(
553
+ throw new ToolError(
554
+ "read_file",
496
555
  `File not found: ${filePath}
497
556
  Current working directory: ${process.cwd()}
498
557
  Please use list_dir to verify the file path and retry.`
@@ -535,14 +594,8 @@ Suggestion: read existing text versions (.md / .txt) in the project, or install
535
594
  return `[Binary file: ${filePath} (${ext})]
536
595
  This is a binary file and cannot be read as text. If there is a text version (.md / .txt) in the project, please read that instead.`;
537
596
  }
538
- try {
539
- const fstat = statSync2(normalizedPath);
540
- if (fstat.size > MAX_FILE_BYTES) {
541
- return `[File too large: ${filePath} | ${(fstat.size / 1024 / 1024).toFixed(1)} MB exceeds ${MAX_FILE_BYTES / 1024 / 1024} MB limit]`;
542
- }
543
- } catch {
544
- }
545
- const buf = readFileSync2(normalizedPath);
597
+ const { readFile: readFile2 } = await import("fs/promises");
598
+ const buf = size > 1048576 ? await readFile2(normalizedPath) : readFileSync2(normalizedPath);
546
599
  if (encoding === "base64") {
547
600
  return `[File: ${filePath} | base64]
548
601
 
@@ -599,7 +652,7 @@ Important: For long content (over 500 lines or 3000 chars), you MUST split into
599
652
  const content = String(args["content"] ?? "");
600
653
  const encoding = args["encoding"] ?? "utf-8";
601
654
  const appendMode = String(args["append"] ?? "false").toLowerCase() === "true";
602
- if (!filePath) throw new Error("path is required");
655
+ if (!filePath) throw new ToolError("write_file", "path is required");
603
656
  undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
604
657
  mkdirSync(dirname2(filePath), { recursive: true });
605
658
  if (appendMode) {
@@ -739,15 +792,15 @@ Note: Path can be absolute or relative to the current working directory.`,
739
792
  async execute(args) {
740
793
  const filePath = String(args["path"] ?? "");
741
794
  const encoding = args["encoding"] ?? "utf-8";
742
- if (!filePath) throw new Error("path is required");
743
- if (!existsSync4(filePath)) throw new Error(`File not found: ${filePath}`);
795
+ if (!filePath) throw new ToolError("edit_file", "path is required");
796
+ if (!existsSync4(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
744
797
  const original = readFileSync3(filePath, encoding);
745
798
  if (args["old_str"] !== void 0) {
746
799
  const oldStr = String(args["old_str"]);
747
800
  const newStr = String(args["new_str"] ?? "");
748
801
  const ignoreWs = Boolean(args["ignore_whitespace"]);
749
802
  const replaceAll = Boolean(args["replace_all"]);
750
- if (oldStr === "") throw new Error("old_str cannot be empty");
803
+ if (oldStr === "") throw new ToolError("edit_file", "old_str cannot be empty");
751
804
  if (ignoreWs) {
752
805
  const fileLines = original.split("\n");
753
806
  const searchLines = oldStr.split("\n");
@@ -825,7 +878,7 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
825
878
  const content = String(args["insert_content"] ?? "");
826
879
  const lines = original.split("\n");
827
880
  if (afterLine < 0 || afterLine > lines.length) {
828
- throw new Error(`insert_after_line ${afterLine} is out of range (file has ${lines.length} lines)`);
881
+ throw new ToolError("edit_file", `insert_after_line ${afterLine} is out of range (file has ${lines.length} lines)`);
829
882
  }
830
883
  undoStack.push(filePath, `edit_file (insert): ${filePath}`);
831
884
  lines.splice(afterLine, 0, content);
@@ -837,7 +890,8 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
837
890
  const toLine = Number(args["delete_to_line"] ?? args["delete_from_line"]);
838
891
  const lines = original.split("\n");
839
892
  if (fromLine < 1 || toLine < fromLine || toLine > lines.length) {
840
- throw new Error(
893
+ throw new ToolError(
894
+ "edit_file",
841
895
  `Invalid line range: ${fromLine}-${toLine} (file has ${lines.length} lines, lines are 1-indexed)`
842
896
  );
843
897
  }
@@ -846,7 +900,8 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
846
900
  writeFileSync3(filePath, lines.join("\n"), encoding);
847
901
  return `Successfully deleted lines ${fromLine}-${toLine} (${deleted.length} lines) from ${filePath}`;
848
902
  }
849
- throw new Error(
903
+ throw new ToolError(
904
+ "edit_file",
850
905
  "No operation specified. Provide either: (old_str + new_str) for replace, (insert_after_line + insert_content) for insert, or (delete_from_line + delete_to_line) for delete."
851
906
  );
852
907
  }
@@ -894,7 +949,8 @@ var listDirTool = {
894
949
  } catch {
895
950
  }
896
951
  if (suggestions.length > 0) {
897
- throw new Error(
952
+ throw new ToolError(
953
+ "list_dir",
898
954
  `Directory not found: ${dirPath}
899
955
  Current working directory: ${cwd}
900
956
  Found similar directories:
@@ -902,7 +958,8 @@ Found similar directories:
902
958
  Please retry with the correct relative path.`
903
959
  );
904
960
  }
905
- throw new Error(
961
+ throw new ToolError(
962
+ "list_dir",
906
963
  `Directory not found: ${dirPath}
907
964
  Current working directory: ${cwd}
908
965
  Please use list_dir (without path) to see the current directory structure first.`
@@ -958,6 +1015,7 @@ function formatSize(bytes) {
958
1015
 
959
1016
  // src/tools/builtin/grep-files.ts
960
1017
  import { readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync4, existsSync as existsSync6 } from "fs";
1018
+ import { readFile } from "fs/promises";
961
1019
  import { join as join2, relative } from "path";
962
1020
  var grepFilesTool = {
963
1021
  definition: {
@@ -1008,11 +1066,11 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
1008
1066
  const ignoreCase = Boolean(args["ignore_case"] ?? false);
1009
1067
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
1010
1068
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
1011
- if (!pattern) throw new Error("pattern is required");
1012
- if (!existsSync6(rootPath)) throw new Error(`Path not found: ${rootPath}`);
1069
+ if (!pattern) throw new ToolError("grep_files", "pattern is required");
1070
+ if (!existsSync6(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
1013
1071
  const MAX_PATTERN_LENGTH = 1e3;
1014
1072
  if (pattern.length > MAX_PATTERN_LENGTH) {
1015
- throw new Error(`Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
1073
+ throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
1016
1074
  }
1017
1075
  let regex;
1018
1076
  try {
@@ -1026,7 +1084,22 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
1026
1084
  if (stat.isFile()) {
1027
1085
  searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
1028
1086
  } else {
1029
- collectFiles(rootPath, filePattern, results, regex, contextLines, maxResults, rootPath);
1087
+ const filePaths = collectFilePaths(rootPath, filePattern, maxResults);
1088
+ const BATCH_SIZE = 16;
1089
+ for (let i = 0; i < filePaths.length && results.length < maxResults; i += BATCH_SIZE) {
1090
+ const batch = filePaths.slice(i, i + BATCH_SIZE);
1091
+ const batchResults = await Promise.all(
1092
+ batch.map(
1093
+ ({ fullPath, relPath }) => searchInFileAsync(fullPath, relPath, regex, contextLines, maxResults - results.length)
1094
+ )
1095
+ );
1096
+ for (const br of batchResults) {
1097
+ for (const r of br) {
1098
+ if (results.length >= maxResults) break;
1099
+ results.push(r);
1100
+ }
1101
+ }
1102
+ }
1030
1103
  }
1031
1104
  if (results.length === 0) {
1032
1105
  return `No matches found for pattern: ${pattern}
@@ -1074,27 +1147,69 @@ function isBinary(filename) {
1074
1147
  const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
1075
1148
  return BINARY_EXTS.has(ext);
1076
1149
  }
1077
- function collectFiles(dirPath, filePattern, results, regex, contextLines, maxResults, rootPath) {
1078
- if (results.length >= maxResults) return;
1079
- let entries;
1150
+ function collectFilePaths(rootPath, filePattern, maxFiles) {
1151
+ const paths = [];
1152
+ function walk(dirPath) {
1153
+ if (paths.length >= maxFiles) return;
1154
+ let entries;
1155
+ try {
1156
+ entries = readdirSync4(dirPath, { withFileTypes: true });
1157
+ } catch {
1158
+ return;
1159
+ }
1160
+ for (const entry of entries) {
1161
+ if (paths.length >= maxFiles) return;
1162
+ if (entry.isDirectory()) {
1163
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
1164
+ walk(join2(dirPath, entry.name));
1165
+ } else if (entry.isFile()) {
1166
+ if (isBinary(entry.name)) continue;
1167
+ if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
1168
+ const fullPath = join2(dirPath, entry.name);
1169
+ try {
1170
+ if (statSync4(fullPath).size > 1e6) continue;
1171
+ } catch {
1172
+ continue;
1173
+ }
1174
+ paths.push({ fullPath, relPath: relative(rootPath, fullPath) });
1175
+ }
1176
+ }
1177
+ }
1178
+ walk(rootPath);
1179
+ return paths;
1180
+ }
1181
+ async function searchInFileAsync(fullPath, displayPath, regex, contextLines, maxResults) {
1182
+ let content;
1080
1183
  try {
1081
- entries = readdirSync4(dirPath, { withFileTypes: true });
1184
+ content = await readFile(fullPath, "utf-8");
1082
1185
  } catch {
1083
- return;
1186
+ return [];
1084
1187
  }
1085
- for (const entry of entries) {
1086
- if (results.length >= maxResults) return;
1087
- if (entry.isDirectory()) {
1088
- if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
1089
- collectFiles(join2(dirPath, entry.name), filePattern, results, regex, contextLines, maxResults, rootPath);
1090
- } else if (entry.isFile()) {
1091
- if (isBinary(entry.name)) continue;
1092
- if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
1093
- const fullPath = join2(dirPath, entry.name);
1094
- const relPath = relative(rootPath, fullPath);
1095
- searchInFile(fullPath, relPath, regex, contextLines, maxResults, results);
1188
+ const results = [];
1189
+ const lines = content.split("\n");
1190
+ regex.lastIndex = 0;
1191
+ for (let i = 0; i < lines.length && results.length < maxResults; i++) {
1192
+ regex.lastIndex = 0;
1193
+ if (regex.test(lines[i])) {
1194
+ const match = {
1195
+ file: displayPath.replace(/\\/g, "/"),
1196
+ lineNumber: i + 1,
1197
+ lineText: lines[i]
1198
+ };
1199
+ if (contextLines > 0) {
1200
+ match.contextBefore = [];
1201
+ for (let j = Math.max(0, i - contextLines); j < i; j++) {
1202
+ match.contextBefore.push([j + 1, lines[j]]);
1203
+ }
1204
+ match.contextAfter = [];
1205
+ for (let j = i + 1; j <= Math.min(lines.length - 1, i + contextLines); j++) {
1206
+ match.contextAfter.push([j + 1, lines[j]]);
1207
+ }
1208
+ }
1209
+ results.push(match);
1096
1210
  }
1097
1211
  }
1212
+ return results;
1098
1213
  }
1099
1214
  function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, results) {
1100
1215
  try {
@@ -1169,8 +1284,8 @@ Results sorted by most recent modification time. Automatically skips node_module
1169
1284
  const pattern = String(args["pattern"] ?? "");
1170
1285
  const rootPath = String(args["path"] ?? process.cwd());
1171
1286
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
1172
- if (!pattern) throw new Error("pattern is required");
1173
- if (!existsSync7(rootPath)) throw new Error(`Path not found: ${rootPath}`);
1287
+ if (!pattern) throw new ToolError("glob_files", "pattern is required");
1288
+ if (!existsSync7(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
1174
1289
  const regex = globToRegex(pattern);
1175
1290
  const matches = [];
1176
1291
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -1319,7 +1434,7 @@ var runInteractiveTool = {
1319
1434
  })() : [];
1320
1435
  const timeout = Math.min(Math.max(Number(args["timeout"] ?? 2e4), 1e3), 3e5);
1321
1436
  if (!executable) {
1322
- throw new Error("executable is required");
1437
+ throw new ToolError("run_interactive", "executable is required");
1323
1438
  }
1324
1439
  const env = {
1325
1440
  ...process.env,
@@ -1461,7 +1576,7 @@ async function resolveAndCheck(hostname) {
1461
1576
  try {
1462
1577
  const { address } = await dnsPromises.lookup(h);
1463
1578
  if (isPrivateHost(address)) {
1464
- throw new Error(`Blocked: "${hostname}" resolves to private address ${address}. web_fetch is restricted to public URLs.`);
1579
+ throw new NetworkError(`Blocked: "${hostname}" resolves to private address ${address}. web_fetch is restricted to public URLs.`);
1465
1580
  }
1466
1581
  } catch (e) {
1467
1582
  if (e.message.startsWith("Blocked:")) throw e;
@@ -1488,17 +1603,17 @@ var webFetchTool = {
1488
1603
  const url = String(args["url"] ?? "").trim();
1489
1604
  const selector = args["selector"] ? String(args["selector"]).trim() : "";
1490
1605
  if (!url.startsWith("http://") && !url.startsWith("https://")) {
1491
- throw new Error(`Invalid URL: "${url}". URL must start with http:// or https://`);
1606
+ throw new NetworkError(`Invalid URL: "${url}". URL must start with http:// or https://`);
1492
1607
  }
1493
1608
  try {
1494
1609
  const parsedUrl = new URL(url);
1495
1610
  if (isPrivateHost(parsedUrl.hostname)) {
1496
- throw new Error(`Blocked: "${url}" resolves to a private/internal address. web_fetch is restricted to public URLs.`);
1611
+ throw new NetworkError(`Blocked: "${url}" resolves to a private/internal address. web_fetch is restricted to public URLs.`);
1497
1612
  }
1498
1613
  await resolveAndCheck(parsedUrl.hostname);
1499
1614
  } catch (e) {
1500
1615
  if (e.message.startsWith("Blocked:")) throw e;
1501
- throw new Error(`Invalid URL: "${url}"`);
1616
+ throw new NetworkError(`Invalid URL: "${url}"`);
1502
1617
  }
1503
1618
  const controller = new AbortController();
1504
1619
  const timeoutId = setTimeout(() => controller.abort(), 2e4);
@@ -1517,7 +1632,7 @@ var webFetchTool = {
1517
1632
  for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
1518
1633
  const parsedHop = new URL(currentUrl);
1519
1634
  if (isPrivateHost(parsedHop.hostname)) {
1520
- throw new Error(`Blocked: redirect to private/internal address "${currentUrl}".`);
1635
+ throw new NetworkError(`Blocked: redirect to private/internal address "${currentUrl}".`);
1521
1636
  }
1522
1637
  await resolveAndCheck(parsedHop.hostname);
1523
1638
  const r = await fetch(currentUrl, {
@@ -1528,7 +1643,7 @@ var webFetchTool = {
1528
1643
  });
1529
1644
  if (r.status >= 300 && r.status < 400) {
1530
1645
  if (hop >= MAX_REDIRECTS) {
1531
- throw new Error(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
1646
+ throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
1532
1647
  }
1533
1648
  const location = r.headers.get("Location");
1534
1649
  if (!location) {
@@ -1542,18 +1657,18 @@ var webFetchTool = {
1542
1657
  break;
1543
1658
  }
1544
1659
  clearTimeout(timeoutId);
1545
- if (!resp) throw new Error(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
1660
+ if (!resp) throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
1546
1661
  finalUrl = currentUrl;
1547
1662
  contentType = resp.headers.get("content-type") ?? "";
1548
1663
  if (!resp.ok) {
1549
- throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
1664
+ throw new NetworkError(`HTTP ${resp.status} ${resp.statusText}`, resp.status);
1550
1665
  }
1551
1666
  const buf = await resp.arrayBuffer();
1552
1667
  rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(buf.slice(0, 2e6));
1553
1668
  } catch (err) {
1554
1669
  clearTimeout(timeoutId);
1555
1670
  if (err.name === "AbortError") {
1556
- throw new Error(`Request timed out after 20s: ${url}`);
1671
+ throw new NetworkError(`Request timed out after 20s: ${url}`, void 0, err);
1557
1672
  }
1558
1673
  throw err;
1559
1674
  }
@@ -1625,10 +1740,10 @@ var saveLastResponseTool = {
1625
1740
  },
1626
1741
  async execute(args) {
1627
1742
  const filePath = String(args["path"] ?? "");
1628
- if (!filePath) throw new Error("path is required");
1743
+ if (!filePath) throw new ToolError("save_last_response", "path is required");
1629
1744
  const content = lastResponseStore.content;
1630
1745
  if (!content) {
1631
- throw new Error("No content to save: AI has not produced any response yet, or the last response was empty.");
1746
+ throw new ToolError("save_last_response", "No content to save: AI has not produced any response yet, or the last response was empty.");
1632
1747
  }
1633
1748
  undoStack.push(filePath, `save_last_response: ${filePath}`);
1634
1749
  mkdirSync2(dirname3(filePath), { recursive: true });
@@ -1665,7 +1780,7 @@ var saveMemoryTool = {
1665
1780
  },
1666
1781
  async execute(args) {
1667
1782
  const content = String(args["content"] ?? "").trim();
1668
- if (!content) throw new Error("content is required");
1783
+ if (!content) throw new ToolError("save_memory", "content is required");
1669
1784
  const memoryPath = getMemoryFilePath();
1670
1785
  const configDir = join4(homedir2(), CONFIG_DIR_NAME);
1671
1786
  if (!existsSync8(configDir)) {
@@ -1702,9 +1817,9 @@ var askUserTool = {
1702
1817
  },
1703
1818
  async execute(args) {
1704
1819
  const question = String(args["question"] ?? "").trim();
1705
- if (!question) throw new Error("question parameter is required");
1820
+ if (!question) throw new ToolError("ask_user", "question parameter is required");
1706
1821
  if (!askUserContext.rl) {
1707
- throw new Error("ask_user is not available in this context (readline not initialized)");
1822
+ throw new ToolError("ask_user", "readline not initialized \u2014 not available in this context");
1708
1823
  }
1709
1824
  const answer = await promptUser(askUserContext.rl, question);
1710
1825
  if (answer === null) {
@@ -1769,30 +1884,30 @@ Valid statuses: pending, in_progress, completed.`,
1769
1884
  let parsed;
1770
1885
  if (typeof raw === "string") {
1771
1886
  const trimmed = raw.trim();
1772
- if (!trimmed) throw new Error("todos parameter is required");
1887
+ if (!trimmed) throw new ToolError("write_todos", "todos parameter is required");
1773
1888
  try {
1774
1889
  parsed = JSON.parse(trimmed);
1775
1890
  } catch (err) {
1776
- throw new Error(`Invalid JSON in todos parameter: ${err.message}`);
1891
+ throw new ToolError("write_todos", `Invalid JSON in todos parameter: ${err.message}`, err);
1777
1892
  }
1778
1893
  } else if (Array.isArray(raw)) {
1779
1894
  parsed = raw;
1780
1895
  } else {
1781
- throw new Error("todos parameter must be a JSON array string");
1896
+ throw new ToolError("write_todos", "todos parameter must be a JSON array string");
1782
1897
  }
1783
1898
  if (!Array.isArray(parsed)) {
1784
- throw new Error("todos must be a JSON array");
1899
+ throw new ToolError("write_todos", "todos must be a JSON array");
1785
1900
  }
1786
1901
  const todos = parsed.map((item, i) => {
1787
1902
  if (typeof item !== "object" || item === null) {
1788
- throw new Error(`todos[${i}] must be an object`);
1903
+ throw new ToolError("write_todos", `todos[${i}] must be an object`);
1789
1904
  }
1790
1905
  const obj = item;
1791
1906
  const title = String(obj["title"] ?? "").trim();
1792
1907
  const status = String(obj["status"] ?? "").trim();
1793
- if (!title) throw new Error(`todos[${i}].title is required`);
1908
+ if (!title) throw new ToolError("write_todos", `todos[${i}].title is required`);
1794
1909
  if (!VALID_STATUSES.has(status)) {
1795
- throw new Error(`todos[${i}].status must be one of: pending, in_progress, completed (got "${status}")`);
1910
+ throw new ToolError("write_todos", `todos[${i}].status must be one of: pending, in_progress, completed (got "${status}")`);
1796
1911
  }
1797
1912
  return { title, status };
1798
1913
  });
@@ -1907,7 +2022,7 @@ var googleSearchTool = {
1907
2022
  },
1908
2023
  async execute(args) {
1909
2024
  const query = String(args["query"] ?? "").trim();
1910
- if (!query) throw new Error("query parameter is required");
2025
+ if (!query) throw new ToolError("google_search", "query parameter is required");
1911
2026
  const numResults = Math.min(
1912
2027
  Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
1913
2028
  MAX_RESULTS
@@ -1931,24 +2046,26 @@ var googleSearchTool = {
1931
2046
  if (!response.ok) {
1932
2047
  const errorBody = await response.text().catch(() => "");
1933
2048
  if (response.status === 403) {
1934
- throw new Error(
2049
+ throw new NetworkError(
1935
2050
  `Google Search API 403 Forbidden \u2014 Invalid API Key or daily free quota exceeded (100/day).
1936
- Please check your API Key and Search Engine ID configuration.`
2051
+ Please check your API Key and Search Engine ID configuration.`,
2052
+ 403
1937
2053
  );
1938
2054
  }
1939
2055
  if (response.status === 429) {
1940
- throw new Error("Google Search API 429 \u2014 Too many requests, please try again later.");
2056
+ throw new NetworkError("Google Search API 429 \u2014 Too many requests, please try again later.", 429);
1941
2057
  }
1942
- throw new Error(
2058
+ throw new NetworkError(
1943
2059
  `Google Search API error: HTTP ${response.status} ${response.statusText}
1944
- ${errorBody.slice(0, 500)}`
2060
+ ${errorBody.slice(0, 500)}`,
2061
+ response.status
1945
2062
  );
1946
2063
  }
1947
2064
  const data = await response.json();
1948
2065
  return formatResults(query, data, numResults);
1949
2066
  } catch (err) {
1950
2067
  if (err instanceof Error && err.name === "AbortError") {
1951
- throw new Error(`Google Search request timed out (${REQUEST_TIMEOUT_MS / 1e3}s). Please check your network or proxy configuration.`);
2068
+ throw new NetworkError(`Google Search request timed out (${REQUEST_TIMEOUT_MS / 1e3}s). Please check your network or proxy configuration.`, void 0, err);
1952
2069
  }
1953
2070
  throw err;
1954
2071
  } finally {
@@ -1967,12 +2084,14 @@ function resolveConfig() {
1967
2084
  cx = EnvLoader.getGoogleSearchEngineId();
1968
2085
  }
1969
2086
  if (!apiKey) {
1970
- throw new Error(
2087
+ throw new ToolError(
2088
+ "google_search",
1971
2089
  'Google Search API Key not configured.\nConfigure via one of:\n 1. Run /config \u2192 Configure Google Search\n 2. Set env var AICLI_API_KEY_GOOGLESEARCH\n 3. Add apiKeys["google-search"] to ~/.aicli/config.json'
1972
2090
  );
1973
2091
  }
1974
2092
  if (!cx) {
1975
- throw new Error(
2093
+ throw new ToolError(
2094
+ "google_search",
1976
2095
  "Google Search Engine ID (cx) not configured.\nConfigure via one of:\n 1. Run /config \u2192 Configure Google Search\n 2. Set env var AICLI_GOOGLE_CX\n 3. Add googleSearchEngineId to ~/.aicli/config.json\n\nGet one at: https://programmablesearchengine.google.com/ \u2192 Create search engine \u2192 Copy Search Engine ID"
1977
2096
  );
1978
2097
  }
@@ -2309,7 +2428,7 @@ var spawnAgentTool = {
2309
2428
  },
2310
2429
  async execute(args) {
2311
2430
  const task = String(args["task"] ?? "").trim();
2312
- if (!task) throw new Error("task parameter is required");
2431
+ if (!task) throw new ToolError("spawn_agent", "task parameter is required");
2313
2432
  const rawMaxRounds = Number(args["max_rounds"] ?? SUBAGENT_DEFAULT_MAX_ROUNDS);
2314
2433
  const maxRounds = Math.min(
2315
2434
  Math.max(Math.round(rawMaxRounds), 1),
@@ -2317,7 +2436,7 @@ var spawnAgentTool = {
2317
2436
  );
2318
2437
  const ctx = spawnAgentContext;
2319
2438
  if (!ctx.provider) {
2320
- throw new Error("spawn_agent: provider not initialized (context not injected)");
2439
+ throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
2321
2440
  }
2322
2441
  const subRegistry = new ToolRegistry();
2323
2442
  for (const tool of subRegistry.listAll()) {
@@ -2533,6 +2652,11 @@ var ToolRegistry = class {
2533
2652
 
2534
2653
  export {
2535
2654
  EnvLoader,
2655
+ ProviderError,
2656
+ AuthError,
2657
+ RateLimitError,
2658
+ ConfigError,
2659
+ ProviderNotFoundError,
2536
2660
  isFileWriteTool,
2537
2661
  getDangerLevel,
2538
2662
  schemaToJsonSchema,
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AuthError,
4
+ ConfigError,
3
5
  EnvLoader,
6
+ ProviderError,
7
+ ProviderNotFoundError,
8
+ RateLimitError,
4
9
  schemaToJsonSchema
5
- } from "./chunk-HOOMQACE.js";
10
+ } from "./chunk-NLT5FT2W.js";
6
11
  import {
7
12
  APP_NAME,
8
13
  CONFIG_DIR_NAME,
@@ -152,47 +157,6 @@ var ConfigSchema = z.object({
152
157
  allowPlugins: z.boolean().default(false)
153
158
  });
154
159
 
155
- // src/core/errors.ts
156
- var AiCliError = class extends Error {
157
- constructor(message, options) {
158
- super(message, options);
159
- this.name = "AiCliError";
160
- }
161
- };
162
- var ProviderError = class extends AiCliError {
163
- constructor(providerId, message, cause) {
164
- super(`[${providerId}] ${message}`, cause !== void 0 ? { cause } : void 0);
165
- this.providerId = providerId;
166
- this.name = "ProviderError";
167
- }
168
- };
169
- var AuthError = class extends ProviderError {
170
- constructor(providerId) {
171
- super(providerId, "Invalid or missing API key. Run: ai-cli config");
172
- this.name = "AuthError";
173
- }
174
- };
175
- var RateLimitError = class extends ProviderError {
176
- constructor(providerId) {
177
- super(providerId, "Rate limit exceeded. Please wait before trying again.");
178
- this.name = "RateLimitError";
179
- }
180
- };
181
- var ConfigError = class extends AiCliError {
182
- constructor(message) {
183
- super(message);
184
- this.name = "ConfigError";
185
- }
186
- };
187
- var ProviderNotFoundError = class extends AiCliError {
188
- constructor(providerId) {
189
- super(
190
- `Provider '${providerId}' is not configured. Run: ai-cli config`
191
- );
192
- this.name = "ProviderNotFoundError";
193
- }
194
- };
195
-
196
160
  // src/config/config-manager.ts
197
161
  var ConfigManager = class {
198
162
  configDir;
@@ -978,10 +942,13 @@ var GeminiProvider = class extends BaseProvider {
978
942
  }
979
943
  wrapError(err) {
980
944
  if (err instanceof Error) {
981
- if (err.message.includes("API key") || err.message.includes("PERMISSION_DENIED")) {
945
+ const msg = err.message;
946
+ const statusMatch = msg.match(/\b(4\d{2}|5\d{2})\b/);
947
+ const status = statusMatch ? Number(statusMatch[1]) : void 0;
948
+ if (msg.includes("API key") || msg.includes("PERMISSION_DENIED") || status === 401 || status === 403) {
982
949
  return new AuthError("gemini");
983
950
  }
984
- if (err.message.includes("429") || err.message.includes("Too Many Requests") || err.message.includes("RESOURCE_EXHAUSTED") || err.message.includes("quota")) {
951
+ if (status === 429 || msg.includes("RESOURCE_EXHAUSTED") || msg.includes("quota")) {
985
952
  return new ProviderError(
986
953
  "gemini",
987
954
  `Rate limit exceeded (429): The current model requires a paid plan or the free quota has been exhausted.
@@ -990,17 +957,17 @@ var GeminiProvider = class extends BaseProvider {
990
957
  err
991
958
  );
992
959
  }
993
- if (err.message.includes("fetch failed") || err.message.includes("ECONNREFUSED") || err.message.includes("ETIMEDOUT")) {
960
+ if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("ENOTFOUND") || msg.includes("socket hang up")) {
994
961
  return new ProviderError(
995
962
  "gemini",
996
- `Network connection failed: ${err.message}
963
+ `Network connection failed: ${msg}
997
964
  Node.js does not automatically use system proxies. Try one of the following:
998
965
  1. Set environment variable: set HTTPS_PROXY=http://127.0.0.1:<proxy-port>
999
966
  2. Configure an accessible Gemini API mirror URL in config.json under customBaseUrls.gemini`,
1000
967
  err
1001
968
  );
1002
969
  }
1003
- return new ProviderError("gemini", err.message, err);
970
+ return new ProviderError("gemini", msg, err);
1004
971
  }
1005
972
  return new ProviderError("gemini", String(err));
1006
973
  }
@@ -2083,7 +2050,7 @@ var ProviderRegistry = class {
2083
2050
  };
2084
2051
 
2085
2052
  // src/session/session-manager.ts
2086
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, unlinkSync, renameSync } from "fs";
2053
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, unlinkSync, renameSync, openSync, readSync, closeSync } from "fs";
2087
2054
  import { join as join2 } from "path";
2088
2055
  import { v4 as uuidv4 } from "uuid";
2089
2056
 
@@ -2289,6 +2256,11 @@ function safeDate(value) {
2289
2256
  const d = new Date(value);
2290
2257
  return isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
2291
2258
  }
2259
+ function extractJsonField(header, field) {
2260
+ const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, "i");
2261
+ const m = header.match(re);
2262
+ return m ? m[1] : void 0;
2263
+ }
2292
2264
  var SessionManager = class {
2293
2265
  constructor(config) {
2294
2266
  this.config = config;
@@ -2333,18 +2305,8 @@ var SessionManager = class {
2333
2305
  const metas = [];
2334
2306
  for (const file of files) {
2335
2307
  try {
2336
- const data = JSON.parse(
2337
- readFileSync2(join2(this.historyDir, file), "utf-8")
2338
- );
2339
- metas.push({
2340
- id: data.id,
2341
- provider: data.provider,
2342
- model: data.model,
2343
- messageCount: data.messages?.length ?? 0,
2344
- created: safeDate(data.created),
2345
- updated: safeDate(data.updated),
2346
- title: data.title
2347
- });
2308
+ const meta = this.readSessionMeta(join2(this.historyDir, file));
2309
+ if (meta) metas.push(meta);
2348
2310
  } catch (err) {
2349
2311
  process.stderr.write(
2350
2312
  `[Warning] Skipping corrupted session file "${file}": ${err instanceof Error ? err.message : String(err)}
@@ -2354,6 +2316,59 @@ var SessionManager = class {
2354
2316
  }
2355
2317
  return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
2356
2318
  }
2319
+ /**
2320
+ * P1-B: Read only the first ~1KB of a session file to extract metadata fields.
2321
+ * Session JSON format puts id/provider/model/created/updated/title before the
2322
+ * large "messages" array, so a small header read suffices for metadata extraction.
2323
+ * Falls back to full file read if header parsing fails.
2324
+ */
2325
+ readSessionMeta(filePath) {
2326
+ const HEADER_SIZE = 1024;
2327
+ let header;
2328
+ try {
2329
+ const fd = openSync(filePath, "r");
2330
+ const buf = Buffer.alloc(HEADER_SIZE);
2331
+ const bytesRead = readSync(fd, buf, 0, HEADER_SIZE, 0);
2332
+ closeSync(fd);
2333
+ header = buf.toString("utf-8", 0, bytesRead);
2334
+ } catch {
2335
+ return null;
2336
+ }
2337
+ const id = extractJsonField(header, "id");
2338
+ const provider = extractJsonField(header, "provider");
2339
+ const model = extractJsonField(header, "model");
2340
+ const created = extractJsonField(header, "created");
2341
+ const updated = extractJsonField(header, "updated");
2342
+ const title = extractJsonField(header, "title");
2343
+ if (id && provider && model) {
2344
+ let messageCount = 0;
2345
+ try {
2346
+ const full = readFileSync2(filePath, "utf-8");
2347
+ const matches = full.match(/"role"\s*:/g);
2348
+ messageCount = matches ? matches.length : 0;
2349
+ } catch {
2350
+ }
2351
+ return {
2352
+ id,
2353
+ provider,
2354
+ model,
2355
+ messageCount,
2356
+ created: safeDate(created),
2357
+ updated: safeDate(updated),
2358
+ title: title || void 0
2359
+ };
2360
+ }
2361
+ const data = JSON.parse(readFileSync2(filePath, "utf-8"));
2362
+ return {
2363
+ id: data.id,
2364
+ provider: data.provider,
2365
+ model: data.model,
2366
+ messageCount: data.messages?.length ?? 0,
2367
+ created: safeDate(data.created),
2368
+ updated: safeDate(data.updated),
2369
+ title: data.title
2370
+ };
2371
+ }
2357
2372
  deleteSession(id) {
2358
2373
  const filePath = join2(this.historyDir, `${id}.json`);
2359
2374
  if (!existsSync2(filePath)) return false;
@@ -2920,8 +2935,12 @@ var McpManager = class {
2920
2935
  * 关闭所有 MCP 服务器连接。
2921
2936
  */
2922
2937
  async closeAll() {
2923
- const promises = [...this.clients.values()].map((c) => c.close().catch(() => {
2924
- }));
2938
+ const promises = [...this.clients.entries()].map(
2939
+ ([id, c]) => c.close().catch((err) => {
2940
+ process.stderr.write(`[mcp] Failed to close ${id}: ${err instanceof Error ? err.message : err}
2941
+ `);
2942
+ })
2943
+ );
2925
2944
  await Promise.allSettled(promises);
2926
2945
  this.clients.clear();
2927
2946
  }
@@ -381,7 +381,7 @@ ${content}`);
381
381
  }
382
382
  }
383
383
  async function runTaskMode(config, providers, configManager, topic) {
384
- const { TaskOrchestrator } = await import("./task-orchestrator-AFMHSW4N.js");
384
+ const { TaskOrchestrator } = await import("./task-orchestrator-FVBUXFLC.js");
385
385
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
386
386
  let interrupted = false;
387
387
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  saveDevState,
24
24
  sessionHasMeaningfulContent,
25
25
  setupProxy
26
- } from "./chunk-KYWU74D5.js";
26
+ } from "./chunk-OBFFL5DJ.js";
27
27
  import {
28
28
  ToolRegistry,
29
29
  askUserContext,
@@ -38,7 +38,7 @@ import {
38
38
  theme,
39
39
  truncateOutput,
40
40
  undoStack
41
- } from "./chunk-HOOMQACE.js";
41
+ } from "./chunk-NLT5FT2W.js";
42
42
  import {
43
43
  AGENTIC_BEHAVIOR_GUIDELINE,
44
44
  AUTHOR,
@@ -442,8 +442,20 @@ var Renderer = class {
442
442
  }
443
443
  renderError(err) {
444
444
  const message = err instanceof Error ? err.message : String(err);
445
+ const lines = [message];
446
+ if (err instanceof Error && err.cause) {
447
+ let cause = err.cause;
448
+ let depth = 0;
449
+ while (cause && depth < 3) {
450
+ const causeMsg = cause instanceof Error ? cause.message : String(cause);
451
+ lines.push(` Caused by: ${causeMsg}`);
452
+ cause = cause instanceof Error ? cause.cause : void 0;
453
+ depth++;
454
+ }
455
+ }
456
+ const typeName = err instanceof Error && err.name !== "Error" ? ` [${err.name}]` : "";
445
457
  console.error(theme.error(`
446
- Error: ${message}
458
+ Error${typeName}: ${lines.join("\n")}
447
459
  `));
448
460
  }
449
461
  /**
@@ -4158,21 +4170,27 @@ Session '${this.resumeSessionId}' not found.
4158
4170
  }
4159
4171
  if (Object.keys(mergedMcpServers).length > 0) {
4160
4172
  this.mcpManager = new McpManager();
4161
- await this.mcpManager.connectAll(mergedMcpServers);
4162
- const mcpTools = this.mcpManager.getAllTools();
4163
- for (const tool of mcpTools) {
4164
- this.toolRegistry.registerMcpTool(tool);
4165
- }
4166
- const connectedCount = this.mcpManager.getConnectedCount();
4167
- const totalTools = this.mcpManager.getTotalToolCount();
4168
- if (connectedCount > 0) {
4169
- const projectCount = Object.keys(projectMcpServers).length;
4170
- const sourceInfo = projectCount > 0 ? ` (${projectCount} from .mcp.json)` : "";
4171
- process.stdout.write(
4172
- theme.dim(` \u{1F50C} MCP: ${connectedCount} server(s), ${totalTools} tool(s)${sourceInfo}
4173
+ const mcpProjectCount = Object.keys(projectMcpServers).length;
4174
+ this.mcpManager.connectAll(mergedMcpServers).then(() => {
4175
+ const mcpTools = this.mcpManager.getAllTools();
4176
+ for (const tool of mcpTools) {
4177
+ this.toolRegistry.registerMcpTool(tool);
4178
+ }
4179
+ const connectedCount = this.mcpManager.getConnectedCount();
4180
+ const totalTools = this.mcpManager.getTotalToolCount();
4181
+ if (connectedCount > 0) {
4182
+ const sourceInfo = mcpProjectCount > 0 ? ` (${mcpProjectCount} from .mcp.json)` : "";
4183
+ process.stdout.write(
4184
+ theme.dim(`
4185
+ \u{1F50C} MCP: ${connectedCount} server(s), ${totalTools} tool(s)${sourceInfo}
4173
4186
  `)
4174
- );
4175
- }
4187
+ );
4188
+ this.showPrompt();
4189
+ }
4190
+ }).catch((err) => {
4191
+ process.stderr.write(`[mcp] connectAll error: ${err instanceof Error ? err.message : err}
4192
+ `);
4193
+ });
4176
4194
  }
4177
4195
  this.setupClipboardPaste();
4178
4196
  this.rl.on("SIGINT", () => {
@@ -5436,7 +5454,9 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5436
5454
  if (sessionId) {
5437
5455
  this.events.emit("session.end", { sessionId });
5438
5456
  }
5439
- this.mcpManager?.closeAll().catch(() => {
5457
+ this.mcpManager?.closeAll().catch((err) => {
5458
+ process.stderr.write(`[mcp] cleanup error: ${err instanceof Error ? err.message : err}
5459
+ `);
5440
5460
  });
5441
5461
  this.rl.close();
5442
5462
  console.log(theme.dim("\nGoodbye!"));
@@ -5528,7 +5548,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5528
5548
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5529
5549
  process.exit(1);
5530
5550
  }
5531
- const { startWebServer } = await import("./server-CL6Q5R2O.js");
5551
+ const { startWebServer } = await import("./server-KSH5U7QY.js");
5532
5552
  await startWebServer({ port, host: options.host });
5533
5553
  });
5534
5554
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -5761,7 +5781,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5761
5781
  }),
5762
5782
  config.get("customProviders")
5763
5783
  );
5764
- const { startHub } = await import("./hub-34MLKYPL.js");
5784
+ const { startHub } = await import("./hub-WF6CNNUT.js");
5765
5785
  await startHub(
5766
5786
  {
5767
5787
  topic: topic ?? "",
@@ -18,7 +18,7 @@ import {
18
18
  renderDiff,
19
19
  runHook,
20
20
  setupProxy
21
- } from "./chunk-KYWU74D5.js";
21
+ } from "./chunk-OBFFL5DJ.js";
22
22
  import {
23
23
  AuthManager
24
24
  } from "./chunk-BYNY5JPB.js";
@@ -32,7 +32,7 @@ import {
32
32
  spawnAgentContext,
33
33
  truncateOutput,
34
34
  undoStack
35
- } from "./chunk-HOOMQACE.js";
35
+ } from "./chunk-NLT5FT2W.js";
36
36
  import {
37
37
  AGENTIC_BEHAVIOR_GUIDELINE,
38
38
  AUTHOR,
@@ -658,7 +658,15 @@ var SessionHandler = class _SessionHandler {
658
658
  await this.handleChatSimple(provider, session.messages);
659
659
  }
660
660
  } catch (err) {
661
- const message = err instanceof Error ? err.message : String(err);
661
+ const parts = [];
662
+ let current = err;
663
+ let depth = 0;
664
+ while (current && depth < 3) {
665
+ parts.push(current instanceof Error ? current.message : String(current));
666
+ current = current instanceof Error ? current.cause : void 0;
667
+ depth++;
668
+ }
669
+ const message = parts.join(" \u2192 Caused by: ");
662
670
  this.send({ type: "error", message });
663
671
  } finally {
664
672
  this.processing = false;
@@ -1963,7 +1971,9 @@ Add .md files to create commands.` });
1963
1971
  if (existsSync3(memPath)) {
1964
1972
  content = readFileSync3(memPath, "utf-8");
1965
1973
  }
1966
- } catch {
1974
+ } catch (err) {
1975
+ process.stderr.write(`[web] Failed to read memory file: ${err instanceof Error ? err.message : err}
1976
+ `);
1967
1977
  }
1968
1978
  this.send({
1969
1979
  type: "memory_content",
@@ -4,7 +4,7 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-HOOMQACE.js";
7
+ } from "./chunk-NLT5FT2W.js";
8
8
  import {
9
9
  SUBAGENT_ALLOWED_TOOLS
10
10
  } from "./chunk-KOD3C2CU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.15",
3
+ "version": "0.4.17",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",