jinzd-ai-cli 0.4.14 → 0.4.16

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.
@@ -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-WIWNSN7U.js";
10
+ } from "./chunk-DMM3XL26.js";
6
11
  import {
7
12
  APP_NAME,
8
13
  CONFIG_DIR_NAME,
@@ -15,7 +20,7 @@ import {
15
20
  MCP_TOOL_PREFIX,
16
21
  PLUGINS_DIR_NAME,
17
22
  VERSION
18
- } from "./chunk-244SVJXW.js";
23
+ } from "./chunk-KOD3C2CU.js";
19
24
 
20
25
  // src/config/config-manager.ts
21
26
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -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
  }
@@ -2920,8 +2887,12 @@ var McpManager = class {
2920
2887
  * 关闭所有 MCP 服务器连接。
2921
2888
  */
2922
2889
  async closeAll() {
2923
- const promises = [...this.clients.values()].map((c) => c.close().catch(() => {
2924
- }));
2890
+ const promises = [...this.clients.entries()].map(
2891
+ ([id, c]) => c.close().catch((err) => {
2892
+ process.stderr.write(`[mcp] Failed to close ${id}: ${err instanceof Error ? err.message : err}
2893
+ `);
2894
+ })
2895
+ );
2925
2896
  await Promise.allSettled(promises);
2926
2897
  this.clients.clear();
2927
2898
  }
@@ -6,7 +6,7 @@ import {
6
6
  SUBAGENT_DEFAULT_MAX_ROUNDS,
7
7
  SUBAGENT_MAX_ROUNDS_LIMIT,
8
8
  runTestsTool
9
- } from "./chunk-244SVJXW.js";
9
+ } from "./chunk-KOD3C2CU.js";
10
10
 
11
11
  // src/tools/builtin/bash.ts
12
12
  import { execSync } from "child_process";
@@ -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.`
@@ -599,7 +658,7 @@ Important: For long content (over 500 lines or 3000 chars), you MUST split into
599
658
  const content = String(args["content"] ?? "");
600
659
  const encoding = args["encoding"] ?? "utf-8";
601
660
  const appendMode = String(args["append"] ?? "false").toLowerCase() === "true";
602
- if (!filePath) throw new Error("path is required");
661
+ if (!filePath) throw new ToolError("write_file", "path is required");
603
662
  undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
604
663
  mkdirSync(dirname2(filePath), { recursive: true });
605
664
  if (appendMode) {
@@ -739,15 +798,15 @@ Note: Path can be absolute or relative to the current working directory.`,
739
798
  async execute(args) {
740
799
  const filePath = String(args["path"] ?? "");
741
800
  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}`);
801
+ if (!filePath) throw new ToolError("edit_file", "path is required");
802
+ if (!existsSync4(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
744
803
  const original = readFileSync3(filePath, encoding);
745
804
  if (args["old_str"] !== void 0) {
746
805
  const oldStr = String(args["old_str"]);
747
806
  const newStr = String(args["new_str"] ?? "");
748
807
  const ignoreWs = Boolean(args["ignore_whitespace"]);
749
808
  const replaceAll = Boolean(args["replace_all"]);
750
- if (oldStr === "") throw new Error("old_str cannot be empty");
809
+ if (oldStr === "") throw new ToolError("edit_file", "old_str cannot be empty");
751
810
  if (ignoreWs) {
752
811
  const fileLines = original.split("\n");
753
812
  const searchLines = oldStr.split("\n");
@@ -825,7 +884,7 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
825
884
  const content = String(args["insert_content"] ?? "");
826
885
  const lines = original.split("\n");
827
886
  if (afterLine < 0 || afterLine > lines.length) {
828
- throw new Error(`insert_after_line ${afterLine} is out of range (file has ${lines.length} lines)`);
887
+ throw new ToolError("edit_file", `insert_after_line ${afterLine} is out of range (file has ${lines.length} lines)`);
829
888
  }
830
889
  undoStack.push(filePath, `edit_file (insert): ${filePath}`);
831
890
  lines.splice(afterLine, 0, content);
@@ -837,7 +896,8 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
837
896
  const toLine = Number(args["delete_to_line"] ?? args["delete_from_line"]);
838
897
  const lines = original.split("\n");
839
898
  if (fromLine < 1 || toLine < fromLine || toLine > lines.length) {
840
- throw new Error(
899
+ throw new ToolError(
900
+ "edit_file",
841
901
  `Invalid line range: ${fromLine}-${toLine} (file has ${lines.length} lines, lines are 1-indexed)`
842
902
  );
843
903
  }
@@ -846,7 +906,8 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
846
906
  writeFileSync3(filePath, lines.join("\n"), encoding);
847
907
  return `Successfully deleted lines ${fromLine}-${toLine} (${deleted.length} lines) from ${filePath}`;
848
908
  }
849
- throw new Error(
909
+ throw new ToolError(
910
+ "edit_file",
850
911
  "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
912
  );
852
913
  }
@@ -894,7 +955,8 @@ var listDirTool = {
894
955
  } catch {
895
956
  }
896
957
  if (suggestions.length > 0) {
897
- throw new Error(
958
+ throw new ToolError(
959
+ "list_dir",
898
960
  `Directory not found: ${dirPath}
899
961
  Current working directory: ${cwd}
900
962
  Found similar directories:
@@ -902,7 +964,8 @@ Found similar directories:
902
964
  Please retry with the correct relative path.`
903
965
  );
904
966
  }
905
- throw new Error(
967
+ throw new ToolError(
968
+ "list_dir",
906
969
  `Directory not found: ${dirPath}
907
970
  Current working directory: ${cwd}
908
971
  Please use list_dir (without path) to see the current directory structure first.`
@@ -1008,11 +1071,11 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
1008
1071
  const ignoreCase = Boolean(args["ignore_case"] ?? false);
1009
1072
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
1010
1073
  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}`);
1074
+ if (!pattern) throw new ToolError("grep_files", "pattern is required");
1075
+ if (!existsSync6(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
1013
1076
  const MAX_PATTERN_LENGTH = 1e3;
1014
1077
  if (pattern.length > MAX_PATTERN_LENGTH) {
1015
- throw new Error(`Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
1078
+ throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
1016
1079
  }
1017
1080
  let regex;
1018
1081
  try {
@@ -1169,8 +1232,8 @@ Results sorted by most recent modification time. Automatically skips node_module
1169
1232
  const pattern = String(args["pattern"] ?? "");
1170
1233
  const rootPath = String(args["path"] ?? process.cwd());
1171
1234
  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}`);
1235
+ if (!pattern) throw new ToolError("glob_files", "pattern is required");
1236
+ if (!existsSync7(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
1174
1237
  const regex = globToRegex(pattern);
1175
1238
  const matches = [];
1176
1239
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -1319,7 +1382,7 @@ var runInteractiveTool = {
1319
1382
  })() : [];
1320
1383
  const timeout = Math.min(Math.max(Number(args["timeout"] ?? 2e4), 1e3), 3e5);
1321
1384
  if (!executable) {
1322
- throw new Error("executable is required");
1385
+ throw new ToolError("run_interactive", "executable is required");
1323
1386
  }
1324
1387
  const env = {
1325
1388
  ...process.env,
@@ -1461,7 +1524,7 @@ async function resolveAndCheck(hostname) {
1461
1524
  try {
1462
1525
  const { address } = await dnsPromises.lookup(h);
1463
1526
  if (isPrivateHost(address)) {
1464
- throw new Error(`Blocked: "${hostname}" resolves to private address ${address}. web_fetch is restricted to public URLs.`);
1527
+ throw new NetworkError(`Blocked: "${hostname}" resolves to private address ${address}. web_fetch is restricted to public URLs.`);
1465
1528
  }
1466
1529
  } catch (e) {
1467
1530
  if (e.message.startsWith("Blocked:")) throw e;
@@ -1488,17 +1551,17 @@ var webFetchTool = {
1488
1551
  const url = String(args["url"] ?? "").trim();
1489
1552
  const selector = args["selector"] ? String(args["selector"]).trim() : "";
1490
1553
  if (!url.startsWith("http://") && !url.startsWith("https://")) {
1491
- throw new Error(`Invalid URL: "${url}". URL must start with http:// or https://`);
1554
+ throw new NetworkError(`Invalid URL: "${url}". URL must start with http:// or https://`);
1492
1555
  }
1493
1556
  try {
1494
1557
  const parsedUrl = new URL(url);
1495
1558
  if (isPrivateHost(parsedUrl.hostname)) {
1496
- throw new Error(`Blocked: "${url}" resolves to a private/internal address. web_fetch is restricted to public URLs.`);
1559
+ throw new NetworkError(`Blocked: "${url}" resolves to a private/internal address. web_fetch is restricted to public URLs.`);
1497
1560
  }
1498
1561
  await resolveAndCheck(parsedUrl.hostname);
1499
1562
  } catch (e) {
1500
1563
  if (e.message.startsWith("Blocked:")) throw e;
1501
- throw new Error(`Invalid URL: "${url}"`);
1564
+ throw new NetworkError(`Invalid URL: "${url}"`);
1502
1565
  }
1503
1566
  const controller = new AbortController();
1504
1567
  const timeoutId = setTimeout(() => controller.abort(), 2e4);
@@ -1517,7 +1580,7 @@ var webFetchTool = {
1517
1580
  for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
1518
1581
  const parsedHop = new URL(currentUrl);
1519
1582
  if (isPrivateHost(parsedHop.hostname)) {
1520
- throw new Error(`Blocked: redirect to private/internal address "${currentUrl}".`);
1583
+ throw new NetworkError(`Blocked: redirect to private/internal address "${currentUrl}".`);
1521
1584
  }
1522
1585
  await resolveAndCheck(parsedHop.hostname);
1523
1586
  const r = await fetch(currentUrl, {
@@ -1528,7 +1591,7 @@ var webFetchTool = {
1528
1591
  });
1529
1592
  if (r.status >= 300 && r.status < 400) {
1530
1593
  if (hop >= MAX_REDIRECTS) {
1531
- throw new Error(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
1594
+ throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
1532
1595
  }
1533
1596
  const location = r.headers.get("Location");
1534
1597
  if (!location) {
@@ -1542,18 +1605,18 @@ var webFetchTool = {
1542
1605
  break;
1543
1606
  }
1544
1607
  clearTimeout(timeoutId);
1545
- if (!resp) throw new Error(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
1608
+ if (!resp) throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
1546
1609
  finalUrl = currentUrl;
1547
1610
  contentType = resp.headers.get("content-type") ?? "";
1548
1611
  if (!resp.ok) {
1549
- throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
1612
+ throw new NetworkError(`HTTP ${resp.status} ${resp.statusText}`, resp.status);
1550
1613
  }
1551
1614
  const buf = await resp.arrayBuffer();
1552
1615
  rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(buf.slice(0, 2e6));
1553
1616
  } catch (err) {
1554
1617
  clearTimeout(timeoutId);
1555
1618
  if (err.name === "AbortError") {
1556
- throw new Error(`Request timed out after 20s: ${url}`);
1619
+ throw new NetworkError(`Request timed out after 20s: ${url}`, void 0, err);
1557
1620
  }
1558
1621
  throw err;
1559
1622
  }
@@ -1625,10 +1688,10 @@ var saveLastResponseTool = {
1625
1688
  },
1626
1689
  async execute(args) {
1627
1690
  const filePath = String(args["path"] ?? "");
1628
- if (!filePath) throw new Error("path is required");
1691
+ if (!filePath) throw new ToolError("save_last_response", "path is required");
1629
1692
  const content = lastResponseStore.content;
1630
1693
  if (!content) {
1631
- throw new Error("No content to save: AI has not produced any response yet, or the last response was empty.");
1694
+ throw new ToolError("save_last_response", "No content to save: AI has not produced any response yet, or the last response was empty.");
1632
1695
  }
1633
1696
  undoStack.push(filePath, `save_last_response: ${filePath}`);
1634
1697
  mkdirSync2(dirname3(filePath), { recursive: true });
@@ -1665,7 +1728,7 @@ var saveMemoryTool = {
1665
1728
  },
1666
1729
  async execute(args) {
1667
1730
  const content = String(args["content"] ?? "").trim();
1668
- if (!content) throw new Error("content is required");
1731
+ if (!content) throw new ToolError("save_memory", "content is required");
1669
1732
  const memoryPath = getMemoryFilePath();
1670
1733
  const configDir = join4(homedir2(), CONFIG_DIR_NAME);
1671
1734
  if (!existsSync8(configDir)) {
@@ -1702,9 +1765,9 @@ var askUserTool = {
1702
1765
  },
1703
1766
  async execute(args) {
1704
1767
  const question = String(args["question"] ?? "").trim();
1705
- if (!question) throw new Error("question parameter is required");
1768
+ if (!question) throw new ToolError("ask_user", "question parameter is required");
1706
1769
  if (!askUserContext.rl) {
1707
- throw new Error("ask_user is not available in this context (readline not initialized)");
1770
+ throw new ToolError("ask_user", "readline not initialized \u2014 not available in this context");
1708
1771
  }
1709
1772
  const answer = await promptUser(askUserContext.rl, question);
1710
1773
  if (answer === null) {
@@ -1769,30 +1832,30 @@ Valid statuses: pending, in_progress, completed.`,
1769
1832
  let parsed;
1770
1833
  if (typeof raw === "string") {
1771
1834
  const trimmed = raw.trim();
1772
- if (!trimmed) throw new Error("todos parameter is required");
1835
+ if (!trimmed) throw new ToolError("write_todos", "todos parameter is required");
1773
1836
  try {
1774
1837
  parsed = JSON.parse(trimmed);
1775
1838
  } catch (err) {
1776
- throw new Error(`Invalid JSON in todos parameter: ${err.message}`);
1839
+ throw new ToolError("write_todos", `Invalid JSON in todos parameter: ${err.message}`, err);
1777
1840
  }
1778
1841
  } else if (Array.isArray(raw)) {
1779
1842
  parsed = raw;
1780
1843
  } else {
1781
- throw new Error("todos parameter must be a JSON array string");
1844
+ throw new ToolError("write_todos", "todos parameter must be a JSON array string");
1782
1845
  }
1783
1846
  if (!Array.isArray(parsed)) {
1784
- throw new Error("todos must be a JSON array");
1847
+ throw new ToolError("write_todos", "todos must be a JSON array");
1785
1848
  }
1786
1849
  const todos = parsed.map((item, i) => {
1787
1850
  if (typeof item !== "object" || item === null) {
1788
- throw new Error(`todos[${i}] must be an object`);
1851
+ throw new ToolError("write_todos", `todos[${i}] must be an object`);
1789
1852
  }
1790
1853
  const obj = item;
1791
1854
  const title = String(obj["title"] ?? "").trim();
1792
1855
  const status = String(obj["status"] ?? "").trim();
1793
- if (!title) throw new Error(`todos[${i}].title is required`);
1856
+ if (!title) throw new ToolError("write_todos", `todos[${i}].title is required`);
1794
1857
  if (!VALID_STATUSES.has(status)) {
1795
- throw new Error(`todos[${i}].status must be one of: pending, in_progress, completed (got "${status}")`);
1858
+ throw new ToolError("write_todos", `todos[${i}].status must be one of: pending, in_progress, completed (got "${status}")`);
1796
1859
  }
1797
1860
  return { title, status };
1798
1861
  });
@@ -1854,10 +1917,6 @@ var EnvLoader = class {
1854
1917
  */
1855
1918
  static getApiKey(providerId) {
1856
1919
  const fixedEnvVar = ENV_KEY_MAP[providerId];
1857
- if (fixedEnvVar) {
1858
- const val = process.env[fixedEnvVar];
1859
- if (val) return val;
1860
- }
1861
1920
  const dynamicEnvVar = `AICLI_API_KEY_${providerId.toUpperCase().replace(/-/g, "_")}`;
1862
1921
  if (fixedEnvVar && fixedEnvVar !== dynamicEnvVar) {
1863
1922
  const fixedVal = process.env[fixedEnvVar];
@@ -1867,6 +1926,10 @@ var EnvLoader = class {
1867
1926
  `);
1868
1927
  }
1869
1928
  }
1929
+ if (fixedEnvVar) {
1930
+ const val = process.env[fixedEnvVar];
1931
+ if (val) return val;
1932
+ }
1870
1933
  return process.env[dynamicEnvVar] || void 0;
1871
1934
  }
1872
1935
  static getDefaultProvider() {
@@ -1907,7 +1970,7 @@ var googleSearchTool = {
1907
1970
  },
1908
1971
  async execute(args) {
1909
1972
  const query = String(args["query"] ?? "").trim();
1910
- if (!query) throw new Error("query parameter is required");
1973
+ if (!query) throw new ToolError("google_search", "query parameter is required");
1911
1974
  const numResults = Math.min(
1912
1975
  Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
1913
1976
  MAX_RESULTS
@@ -1931,24 +1994,26 @@ var googleSearchTool = {
1931
1994
  if (!response.ok) {
1932
1995
  const errorBody = await response.text().catch(() => "");
1933
1996
  if (response.status === 403) {
1934
- throw new Error(
1997
+ throw new NetworkError(
1935
1998
  `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.`
1999
+ Please check your API Key and Search Engine ID configuration.`,
2000
+ 403
1937
2001
  );
1938
2002
  }
1939
2003
  if (response.status === 429) {
1940
- throw new Error("Google Search API 429 \u2014 Too many requests, please try again later.");
2004
+ throw new NetworkError("Google Search API 429 \u2014 Too many requests, please try again later.", 429);
1941
2005
  }
1942
- throw new Error(
2006
+ throw new NetworkError(
1943
2007
  `Google Search API error: HTTP ${response.status} ${response.statusText}
1944
- ${errorBody.slice(0, 500)}`
2008
+ ${errorBody.slice(0, 500)}`,
2009
+ response.status
1945
2010
  );
1946
2011
  }
1947
2012
  const data = await response.json();
1948
2013
  return formatResults(query, data, numResults);
1949
2014
  } catch (err) {
1950
2015
  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.`);
2016
+ throw new NetworkError(`Google Search request timed out (${REQUEST_TIMEOUT_MS / 1e3}s). Please check your network or proxy configuration.`, void 0, err);
1952
2017
  }
1953
2018
  throw err;
1954
2019
  } finally {
@@ -1967,12 +2032,14 @@ function resolveConfig() {
1967
2032
  cx = EnvLoader.getGoogleSearchEngineId();
1968
2033
  }
1969
2034
  if (!apiKey) {
1970
- throw new Error(
2035
+ throw new ToolError(
2036
+ "google_search",
1971
2037
  '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
2038
  );
1973
2039
  }
1974
2040
  if (!cx) {
1975
- throw new Error(
2041
+ throw new ToolError(
2042
+ "google_search",
1976
2043
  "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
2044
  );
1978
2045
  }
@@ -2309,7 +2376,7 @@ var spawnAgentTool = {
2309
2376
  },
2310
2377
  async execute(args) {
2311
2378
  const task = String(args["task"] ?? "").trim();
2312
- if (!task) throw new Error("task parameter is required");
2379
+ if (!task) throw new ToolError("spawn_agent", "task parameter is required");
2313
2380
  const rawMaxRounds = Number(args["max_rounds"] ?? SUBAGENT_DEFAULT_MAX_ROUNDS);
2314
2381
  const maxRounds = Math.min(
2315
2382
  Math.max(Math.round(rawMaxRounds), 1),
@@ -2317,7 +2384,7 @@ var spawnAgentTool = {
2317
2384
  );
2318
2385
  const ctx = spawnAgentContext;
2319
2386
  if (!ctx.provider) {
2320
- throw new Error("spawn_agent: provider not initialized (context not injected)");
2387
+ throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
2321
2388
  }
2322
2389
  const subRegistry = new ToolRegistry();
2323
2390
  for (const tool of subRegistry.listAll()) {
@@ -2533,6 +2600,11 @@ var ToolRegistry = class {
2533
2600
 
2534
2601
  export {
2535
2602
  EnvLoader,
2603
+ ProviderError,
2604
+ AuthError,
2605
+ RateLimitError,
2606
+ ConfigError,
2607
+ ProviderNotFoundError,
2536
2608
  isFileWriteTool,
2537
2609
  getDangerLevel,
2538
2610
  schemaToJsonSchema,
@@ -8,7 +8,7 @@ import { platform } from "os";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/core/constants.ts
11
- var VERSION = "0.4.14";
11
+ var VERSION = "0.4.15";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -6,7 +6,7 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.14";
9
+ var VERSION = "0.4.15";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -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-EGJZA6Y4.js");
384
+ const { TaskOrchestrator } = await import("./task-orchestrator-USDQ6J6V.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-ROMSAKP6.js";
26
+ } from "./chunk-2OKRGXVU.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-WIWNSN7U.js";
41
+ } from "./chunk-DMM3XL26.js";
42
42
  import {
43
43
  AGENTIC_BEHAVIOR_GUIDELINE,
44
44
  AUTHOR,
@@ -58,7 +58,7 @@ import {
58
58
  REPO_URL,
59
59
  SKILLS_DIR_NAME,
60
60
  VERSION
61
- } from "./chunk-244SVJXW.js";
61
+ } from "./chunk-KOD3C2CU.js";
62
62
 
63
63
  // src/index.ts
64
64
  import { program } from "commander";
@@ -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
  /**
@@ -1914,7 +1926,7 @@ ${hint}` : "")
1914
1926
  description: "Run project tests and show structured report",
1915
1927
  usage: "/test [command|filter]",
1916
1928
  async execute(args, _ctx) {
1917
- const { executeTests } = await import("./run-tests-YU52BWHE.js");
1929
+ const { executeTests } = await import("./run-tests-H7IVHUZO.js");
1918
1930
  const argStr = args.join(" ").trim();
1919
1931
  let testArgs = {};
1920
1932
  if (argStr) {
@@ -5436,7 +5448,9 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5436
5448
  if (sessionId) {
5437
5449
  this.events.emit("session.end", { sessionId });
5438
5450
  }
5439
- this.mcpManager?.closeAll().catch(() => {
5451
+ this.mcpManager?.closeAll().catch((err) => {
5452
+ process.stderr.write(`[mcp] cleanup error: ${err instanceof Error ? err.message : err}
5453
+ `);
5440
5454
  });
5441
5455
  this.rl.close();
5442
5456
  console.log(theme.dim("\nGoodbye!"));
@@ -5528,7 +5542,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5528
5542
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5529
5543
  process.exit(1);
5530
5544
  }
5531
- const { startWebServer } = await import("./server-KVHJCPUO.js");
5545
+ const { startWebServer } = await import("./server-4JOL6AJL.js");
5532
5546
  await startWebServer({ port, host: options.host });
5533
5547
  });
5534
5548
  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 +5775,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5761
5775
  }),
5762
5776
  config.get("customProviders")
5763
5777
  );
5764
- const { startHub } = await import("./hub-QZXQ6JYS.js");
5778
+ const { startHub } = await import("./hub-2MUUWFAZ.js");
5765
5779
  await startHub(
5766
5780
  {
5767
5781
  topic: topic ?? "",
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-244SVJXW.js";
5
+ } from "./chunk-KOD3C2CU.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-QW2UGW6D.js";
4
+ } from "./chunk-SNUHVNSD.js";
5
5
  export {
6
6
  executeTests,
7
7
  runTestsTool
@@ -18,7 +18,7 @@ import {
18
18
  renderDiff,
19
19
  runHook,
20
20
  setupProxy
21
- } from "./chunk-ROMSAKP6.js";
21
+ } from "./chunk-2OKRGXVU.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-WIWNSN7U.js";
35
+ } from "./chunk-DMM3XL26.js";
36
36
  import {
37
37
  AGENTIC_BEHAVIOR_GUIDELINE,
38
38
  AUTHOR,
@@ -49,7 +49,7 @@ import {
49
49
  PLUGINS_DIR_NAME,
50
50
  SKILLS_DIR_NAME,
51
51
  VERSION
52
- } from "./chunk-244SVJXW.js";
52
+ } from "./chunk-KOD3C2CU.js";
53
53
 
54
54
  // src/web/server.ts
55
55
  import express from "express";
@@ -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;
@@ -1482,7 +1490,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1482
1490
  case "test": {
1483
1491
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1484
1492
  try {
1485
- const { executeTests } = await import("./run-tests-YU52BWHE.js");
1493
+ const { executeTests } = await import("./run-tests-H7IVHUZO.js");
1486
1494
  const argStr = args.join(" ").trim();
1487
1495
  let testArgs = {};
1488
1496
  if (argStr) {
@@ -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,10 +4,10 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-WIWNSN7U.js";
7
+ } from "./chunk-DMM3XL26.js";
8
8
  import {
9
9
  SUBAGENT_ALLOWED_TOOLS
10
- } from "./chunk-244SVJXW.js";
10
+ } from "./chunk-KOD3C2CU.js";
11
11
 
12
12
  // src/hub/task-orchestrator.ts
13
13
  import { createInterface } from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",