jinzd-ai-cli 0.4.143 → 0.4.145

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,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConfigManager
4
- } from "./chunk-DX5JYNVN.js";
4
+ } from "./chunk-MII3VYZ7.js";
5
5
  import "./chunk-2ZD3YTVM.js";
6
- import "./chunk-VXFBUMWG.js";
6
+ import "./chunk-EYUL75S4.js";
7
7
  import "./chunk-PDX44BCA.js";
8
8
 
9
9
  // src/cli/batch.ts
@@ -5,12 +5,14 @@ import {
5
5
  } from "./chunk-UQQJWHRV.js";
6
6
  import {
7
7
  runTestsTool
8
- } from "./chunk-IMYOBDJG.js";
8
+ } from "./chunk-QWP4RFMS.js";
9
9
  import {
10
- getDangerLevel,
11
- isFileWriteTool,
12
10
  runTool
13
- } from "./chunk-2JLVHUMU.js";
11
+ } from "./chunk-GZNBAFTO.js";
12
+ import {
13
+ getDangerLevel,
14
+ isFileWriteTool
15
+ } from "./chunk-OWPFDHKC.js";
14
16
  import {
15
17
  EnvLoader,
16
18
  NetworkError,
@@ -23,7 +25,7 @@ import {
23
25
  SUBAGENT_ALLOWED_TOOLS,
24
26
  SUBAGENT_DEFAULT_MAX_ROUNDS,
25
27
  SUBAGENT_MAX_ROUNDS_LIMIT
26
- } from "./chunk-VXFBUMWG.js";
28
+ } from "./chunk-EYUL75S4.js";
27
29
  import {
28
30
  fileCheckpoints
29
31
  } from "./chunk-4BKXL7SM.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/core/constants.ts
4
- var VERSION = "0.4.143";
4
+ var VERSION = "0.4.145";
5
5
  var APP_NAME = "ai-cli";
6
6
  var CONFIG_DIR_NAME = ".aicli";
7
7
  var CONFIG_FILE_NAME = "config.json";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CONFIG_DIR_NAME
4
- } from "./chunk-VXFBUMWG.js";
4
+ } from "./chunk-EYUL75S4.js";
5
5
 
6
6
  // src/diagnostics/tool-stats.ts
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
@@ -118,69 +118,11 @@ function installFlushOnExit() {
118
118
  process.on("exit", () => flush());
119
119
  }
120
120
 
121
- // src/tools/types.ts
122
- function isFileWriteTool(name) {
123
- return name === "write_file" || name === "edit_file" || name === "notebook_edit";
124
- }
125
- function getDangerLevel(toolName, args) {
126
- if (toolName.startsWith("mcp__")) return "safe";
127
- if (toolName === "bash") {
128
- const cmd = String(args["command"] ?? "");
129
- if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
130
- if (/\brm\s+\S/.test(cmd)) return "destructive";
131
- if (/\brmdir\b|\bformat\b|\bmkfs\b|\bdd\s+if=|\bshred\b|\bfdisk\b|\bparted\b/.test(cmd)) return "destructive";
132
- if (/\bshutdown\b|\breboot\b|\bhalt\b|\bpoweroff\b/.test(cmd)) return "destructive";
133
- if (/\bkill\s+-9\b|\bkillall\b/.test(cmd)) return "destructive";
134
- if (/\bRemove-Item\b|\bri\s+\S/i.test(cmd)) return "destructive";
135
- if (/\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
136
- if (/\bdel\s+\S/.test(cmd)) return "destructive";
137
- if (/\bShutdown(-Computer)?\b|\bRestart-Computer\b/i.test(cmd)) return "destructive";
138
- if (/(?:^|[\s|;&])>>?\s*\S/.test(cmd)) return "write";
139
- if (/\btee\b|\bcp\b|\bmv\b|\bln\s+-s/.test(cmd)) return "write";
140
- if (/\bchmod\b|\bchown\b/.test(cmd)) return "write";
141
- if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b|\bSet-ItemProperty\b|\bNew-ItemProperty\b/i.test(cmd)) return "write";
142
- return "safe";
143
- }
144
- if (toolName === "write_file") return "write";
145
- if (toolName === "edit_file") return "write";
146
- if (toolName === "save_last_response") return "write";
147
- if (toolName === "run_interactive") {
148
- const exe = String(args["executable"] ?? "").toLowerCase();
149
- if (/\b(rm|rmdir|del|format|mkfs|Remove-Item)\b/i.test(exe)) return "destructive";
150
- if (/\b(bash|sh|zsh|cmd|powershell|pwsh|python|node|ruby|perl)\b/i.test(exe)) return "write";
151
- return "write";
152
- }
153
- if (toolName === "task_create" || toolName === "task_stop") return "write";
154
- if (toolName === "task_list") return "safe";
155
- if (toolName === "git_commit") return "write";
156
- if (toolName === "git_status" || toolName === "git_diff" || toolName === "git_log") return "safe";
157
- if (toolName === "notebook_edit") return "write";
158
- if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent" || toolName === "run_tests") return "safe";
159
- return "write";
160
- }
161
- function schemaToJsonSchema(schema) {
162
- const result = {
163
- type: schema.type,
164
- description: schema.description
165
- };
166
- if (schema.enum) result["enum"] = schema.enum;
167
- if (schema.items) result["items"] = schemaToJsonSchema(schema.items);
168
- if (schema.properties) {
169
- result["properties"] = Object.fromEntries(
170
- Object.entries(schema.properties).map(([k, v]) => [k, schemaToJsonSchema(v)])
171
- );
172
- }
173
- return result;
174
- }
175
-
176
121
  export {
177
122
  runTool,
178
123
  getStatsSnapshot,
179
124
  getTopFailingTools,
180
125
  getTopUsedTools,
181
126
  resetStats,
182
- installFlushOnExit,
183
- isFileWriteTool,
184
- getDangerLevel,
185
- schemaToJsonSchema
127
+ installFlushOnExit
186
128
  };
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/review-prompts.ts
4
+ function buildReviewPrompt(diff, gitContextStr, detailed) {
5
+ const level = detailed ? "Please perform a detailed in-depth review covering: security, performance, maintainability, error handling, naming conventions, and code duplication." : "Please perform a concise code review focusing on bugs, security issues, and key improvement suggestions.";
6
+ return `# Code Review Request
7
+
8
+ ${level}
9
+
10
+ ## Git Status
11
+ ${gitContextStr}
12
+
13
+ ## Code Changes (diff)
14
+ \`\`\`diff
15
+ ${diff}
16
+ \`\`\`
17
+
18
+ ## Output Format
19
+ Please structure your review as follows:
20
+ 1. **Overall Assessment**: One-sentence summary of the change quality
21
+ 2. **Issues** (if any): Each issue with [Severity] file:line \u2014 description + suggested fix
22
+ 3. **Improvement Suggestions** (if any): Non-critical but recommended optimizations
23
+ 4. **Highlights** (if any): Good practices worth acknowledging
24
+
25
+ Severity levels: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
26
+ }
27
+ function buildSecurityReviewPrompt(diff, gitContextStr) {
28
+ return `# Security Vulnerability Review
29
+
30
+ Analyze the following code changes **exclusively for security vulnerabilities**.
31
+
32
+ ## Categories to check:
33
+ 1. **Injection** \u2014 SQL, command, path traversal, XSS, template injection
34
+ 2. **Authentication & Authorization** \u2014 hardcoded credentials, missing auth checks, privilege escalation
35
+ 3. **Secrets & Sensitive Data** \u2014 API keys, tokens, passwords in code, logging sensitive data
36
+ 4. **Input Validation** \u2014 missing validation, unsafe deserialization, buffer issues
37
+ 5. **Cryptography** \u2014 weak algorithms, improper random, hardcoded IVs/salts
38
+ 6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
39
+ 7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
40
+ 8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
41
+
42
+ ## Git Status
43
+ ${gitContextStr}
44
+
45
+ ## Code Changes (diff)
46
+ \`\`\`diff
47
+ ${diff}
48
+ \`\`\`
49
+
50
+ ## Output Format
51
+ For each finding:
52
+ - **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
53
+ - **Category**: (from list above)
54
+ - **File & location**: file:line
55
+ - **Description**: what the vulnerability is and how it could be exploited
56
+ - **Recommended fix**: specific code change to resolve
57
+
58
+ If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
59
+ }
60
+ function truncateDiff(diff, maxChars) {
61
+ if (diff.length <= maxChars) return { diff, truncated: false };
62
+ const head = diff.slice(0, Math.floor(maxChars * 0.7));
63
+ const tail = diff.slice(diff.length - Math.floor(maxChars * 0.2));
64
+ return {
65
+ diff: head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail,
66
+ truncated: true
67
+ };
68
+ }
69
+
70
+ export {
71
+ buildReviewPrompt,
72
+ buildSecurityReviewPrompt,
73
+ truncateDiff
74
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  truncateForPersist
4
- } from "./chunk-YPFCJ5KC.js";
4
+ } from "./chunk-A3RPBWBF.js";
5
5
  import {
6
6
  APP_NAME,
7
7
  CONFIG_DIR_NAME,
@@ -11,7 +11,7 @@ import {
11
11
  MCP_PROTOCOL_VERSION,
12
12
  MCP_TOOL_PREFIX,
13
13
  VERSION
14
- } from "./chunk-VXFBUMWG.js";
14
+ } from "./chunk-EYUL75S4.js";
15
15
  import {
16
16
  redactJson
17
17
  } from "./chunk-7ZJN4KLV.js";
@@ -8,7 +8,7 @@ import {
8
8
  CONFIG_FILE_NAME,
9
9
  HISTORY_DIR_NAME,
10
10
  PLUGINS_DIR_NAME
11
- } from "./chunk-VXFBUMWG.js";
11
+ } from "./chunk-EYUL75S4.js";
12
12
 
13
13
  // src/config/config-manager.ts
14
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -235,7 +235,27 @@ var ConfigSchema = z.object({
235
235
  name: z.string().optional()
236
236
  })).default([]),
237
237
  fallback: z.string().optional()
238
- }).default({ enabled: false, rules: [] })
238
+ }).default({ enabled: false, rules: [] }),
239
+ // Provider 容错(v0.4.144+):网络抖动 / 5xx / 429 时,先在当前 provider 上做指数退避重试,
240
+ // 仍失败则按顺序尝试 chain 里的 fallback provider。auth / 400 / 422 等"硬失败"直接跳过同
241
+ // provider 重试,但仍会尝试 chain(不同 key/schema 可能能成功)。
242
+ // 关键约束:流式 API 一旦已经向终端 yield 过 chunk,错误立刻向上抛 —— 不能"已经吐了一半"再
243
+ // 重新发起请求,会撕裂用户看到的输出。
244
+ // enabled 默认 false:保持向前兼容,未配置 chain 的用户行为不变。
245
+ fallback: z.object({
246
+ enabled: z.boolean().default(false),
247
+ /** 同 provider 重试次数(不含首次调用)。0 = 不重试,直接走 chain。 */
248
+ retries: z.number().int().min(0).max(10).default(2),
249
+ /** 首次退避等待(ms),后续按 2 的幂指数增长。 */
250
+ initialBackoffMs: z.number().int().positive().default(500),
251
+ /** 单次退避的硬上限(ms)。 */
252
+ maxBackoffMs: z.number().int().positive().default(8e3),
253
+ /** 兜底 provider 链,每个 entry 仅尝试 1 次。model 省略则使用 provider 的 defaultModel。 */
254
+ chain: z.array(z.object({
255
+ provider: z.string(),
256
+ model: z.string().optional()
257
+ })).default([])
258
+ }).default({ enabled: false, retries: 2, initialBackoffMs: 500, maxBackoffMs: 8e3, chain: [] })
239
259
  });
240
260
 
241
261
  // src/config/config-manager.ts
@@ -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.143";
9
+ var VERSION = "0.4.145";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/tools/types.ts
4
+ function isFileWriteTool(name) {
5
+ return name === "write_file" || name === "edit_file" || name === "notebook_edit";
6
+ }
7
+ function getDangerLevel(toolName, args) {
8
+ if (toolName.startsWith("mcp__")) return "safe";
9
+ if (toolName === "bash") {
10
+ const cmd = String(args["command"] ?? "");
11
+ if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
12
+ if (/\brm\s+\S/.test(cmd)) return "destructive";
13
+ if (/\brmdir\b|\bformat\b|\bmkfs\b|\bdd\s+if=|\bshred\b|\bfdisk\b|\bparted\b/.test(cmd)) return "destructive";
14
+ if (/\bshutdown\b|\breboot\b|\bhalt\b|\bpoweroff\b/.test(cmd)) return "destructive";
15
+ if (/\bkill\s+-9\b|\bkillall\b/.test(cmd)) return "destructive";
16
+ if (/\bRemove-Item\b|\bri\s+\S/i.test(cmd)) return "destructive";
17
+ if (/\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
18
+ if (/\bdel\s+\S/.test(cmd)) return "destructive";
19
+ if (/\bShutdown(-Computer)?\b|\bRestart-Computer\b/i.test(cmd)) return "destructive";
20
+ if (/(?:^|[\s|;&])>>?\s*\S/.test(cmd)) return "write";
21
+ if (/\btee\b|\bcp\b|\bmv\b|\bln\s+-s/.test(cmd)) return "write";
22
+ if (/\bchmod\b|\bchown\b/.test(cmd)) return "write";
23
+ if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b|\bSet-ItemProperty\b|\bNew-ItemProperty\b/i.test(cmd)) return "write";
24
+ return "safe";
25
+ }
26
+ if (toolName === "write_file") return "write";
27
+ if (toolName === "edit_file") return "write";
28
+ if (toolName === "save_last_response") return "write";
29
+ if (toolName === "run_interactive") {
30
+ const exe = String(args["executable"] ?? "").toLowerCase();
31
+ if (/\b(rm|rmdir|del|format|mkfs|Remove-Item)\b/i.test(exe)) return "destructive";
32
+ if (/\b(bash|sh|zsh|cmd|powershell|pwsh|python|node|ruby|perl)\b/i.test(exe)) return "write";
33
+ return "write";
34
+ }
35
+ if (toolName === "task_create" || toolName === "task_stop") return "write";
36
+ if (toolName === "task_list") return "safe";
37
+ if (toolName === "git_commit") return "write";
38
+ if (toolName === "git_status" || toolName === "git_diff" || toolName === "git_log") return "safe";
39
+ if (toolName === "notebook_edit") return "write";
40
+ if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent" || toolName === "run_tests") return "safe";
41
+ return "write";
42
+ }
43
+ function schemaToJsonSchema(schema) {
44
+ const result = {
45
+ type: schema.type,
46
+ description: schema.description
47
+ };
48
+ if (schema.enum) result["enum"] = schema.enum;
49
+ if (schema.items) result["items"] = schemaToJsonSchema(schema.items);
50
+ if (schema.properties) {
51
+ result["properties"] = Object.fromEntries(
52
+ Object.entries(schema.properties).map(([k, v]) => [k, schemaToJsonSchema(v)])
53
+ );
54
+ }
55
+ return result;
56
+ }
57
+
58
+ export {
59
+ isFileWriteTool,
60
+ getDangerLevel,
61
+ schemaToJsonSchema
62
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TEST_TIMEOUT
4
- } from "./chunk-VXFBUMWG.js";
4
+ } from "./chunk-EYUL75S4.js";
5
5
 
6
6
  // src/tools/builtin/run-tests.ts
7
7
  import { execSync, spawnSync } from "child_process";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  schemaToJsonSchema
4
- } from "./chunk-2JLVHUMU.js";
4
+ } from "./chunk-OWPFDHKC.js";
5
5
  import {
6
6
  AuthError,
7
7
  ProviderError,
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  CONFIG_DIR_NAME,
4
4
  VERSION
5
- } from "./chunk-VXFBUMWG.js";
5
+ } from "./chunk-EYUL75S4.js";
6
6
 
7
7
  // src/diagnostics/crash-log.ts
8
8
  import {
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildReviewPrompt,
4
+ buildSecurityReviewPrompt,
5
+ truncateDiff
6
+ } from "./chunk-HLWUDRBO.js";
7
+ import {
8
+ ProviderRegistry
9
+ } from "./chunk-XZBA3NHG.js";
10
+ import {
11
+ ConfigManager
12
+ } from "./chunk-MII3VYZ7.js";
13
+ import "./chunk-OWPFDHKC.js";
14
+ import "./chunk-2ZD3YTVM.js";
15
+ import {
16
+ VERSION
17
+ } from "./chunk-EYUL75S4.js";
18
+ import "./chunk-PDX44BCA.js";
19
+
20
+ // src/cli/ci.ts
21
+ import { execFileSync, execSync } from "child_process";
22
+ var CI_COMMENT_MARKER = "<!-- aicli-ci-review -->";
23
+ function fetchDiff(opts) {
24
+ if (opts.diffOverride != null) return opts.diffOverride;
25
+ if (opts.pr != null) {
26
+ try {
27
+ return execFileSync("gh", ["pr", "diff", String(opts.pr)], {
28
+ encoding: "utf-8",
29
+ maxBuffer: 50 * 1024 * 1024
30
+ });
31
+ } catch (err) {
32
+ const msg = err.stderr?.toString() ?? err.message;
33
+ throw new Error(`gh pr diff ${opts.pr} failed: ${msg.trim()}`);
34
+ }
35
+ }
36
+ if (opts.base) {
37
+ try {
38
+ return execSync(`git diff ${shellQuote(opts.base)}...HEAD`, {
39
+ encoding: "utf-8",
40
+ maxBuffer: 50 * 1024 * 1024,
41
+ timeout: 3e4
42
+ });
43
+ } catch (err) {
44
+ throw new Error(`git diff ${opts.base}...HEAD failed: ${err.message}`);
45
+ }
46
+ }
47
+ throw new Error("aicli ci: must supply --pr <num> or --base <ref> to determine the diff source");
48
+ }
49
+ function shellQuote(ref) {
50
+ if (!/^[A-Za-z0-9._/\-]+$/.test(ref)) {
51
+ throw new Error(`unsafe ref: ${ref}`);
52
+ }
53
+ return ref;
54
+ }
55
+ function buildGitContextStr(opts) {
56
+ const parts = [];
57
+ if (opts.pr != null) parts.push(`PR: #${opts.pr}`);
58
+ if (opts.base) parts.push(`Base: ${opts.base}`);
59
+ try {
60
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", timeout: 5e3 }).trim();
61
+ if (branch) parts.push(`Branch: ${branch}`);
62
+ } catch {
63
+ }
64
+ parts.push(`Reviewer: aicli v${VERSION}`);
65
+ return parts.join(" | ");
66
+ }
67
+ function countSeverity(md) {
68
+ const count = (re) => (md.match(re) ?? []).length;
69
+ return {
70
+ critical: count(/🔴\s*(?:CRITICAL|Critical|critical)/g),
71
+ high: count(/🟠\s*(?:HIGH|High|high)/g),
72
+ warning: count(/🟡\s*(?:WARNING|Warning|warning|MEDIUM|Medium|medium)/g),
73
+ info: count(/🔵\s*(?:INFO|Info|info|LOW|Low|low)/g)
74
+ };
75
+ }
76
+ async function runOnePrompt(registry, providerId, modelId, prompt) {
77
+ const provider = registry.get(providerId);
78
+ const resp = await provider.chat({
79
+ messages: [{ role: "user", content: prompt }],
80
+ model: modelId,
81
+ stream: false,
82
+ temperature: 0.3,
83
+ maxTokens: 8192
84
+ });
85
+ return resp.content?.trim() ?? "";
86
+ }
87
+ function postOrUpdatePrComment(prNumber, body, update) {
88
+ const fullBody = `${body}
89
+
90
+ ${CI_COMMENT_MARKER}`;
91
+ if (update) {
92
+ let comments = [];
93
+ try {
94
+ const out = execFileSync("gh", ["pr", "view", String(prNumber), "--json", "comments"], {
95
+ encoding: "utf-8",
96
+ maxBuffer: 20 * 1024 * 1024
97
+ });
98
+ const parsed = JSON.parse(out);
99
+ comments = parsed.comments ?? [];
100
+ } catch {
101
+ }
102
+ const existing = comments.find((c) => c.body.includes(CI_COMMENT_MARKER));
103
+ if (existing) {
104
+ execFileSync(
105
+ "gh",
106
+ ["api", "--method", "PATCH", `repos/{owner}/{repo}/issues/comments/${gqlIdToRest(existing.id)}`, "-f", `body=${fullBody}`],
107
+ { stdio: "pipe" }
108
+ );
109
+ return { updated: true, id: existing.id };
110
+ }
111
+ }
112
+ execFileSync("gh", ["pr", "comment", String(prNumber), "--body-file", "-"], {
113
+ input: fullBody,
114
+ stdio: ["pipe", "pipe", "pipe"]
115
+ });
116
+ return { updated: false };
117
+ }
118
+ function gqlIdToRest(graphqlId) {
119
+ if (/^\d+$/.test(graphqlId)) return graphqlId;
120
+ const query = `query($id:ID!){ node(id:$id){ ... on IssueComment { databaseId } } }`;
121
+ const out = execFileSync("gh", ["api", "graphql", "-f", `query=${query}`, "-F", `id=${graphqlId}`], {
122
+ encoding: "utf-8"
123
+ });
124
+ const parsed = JSON.parse(out);
125
+ const id = parsed.data?.node?.databaseId;
126
+ if (!id) throw new Error(`could not resolve comment id ${graphqlId}`);
127
+ return String(id);
128
+ }
129
+ async function runCi(opts) {
130
+ const maxDiff = opts.maxDiffChars ?? 3e4;
131
+ let diff;
132
+ try {
133
+ diff = fetchDiff(opts).trim();
134
+ } catch (err) {
135
+ return {
136
+ exitCode: 2,
137
+ markdown: `\u274C ${err.message}`,
138
+ posted: false,
139
+ severity: { critical: 0, high: 0, warning: 0, info: 0 }
140
+ };
141
+ }
142
+ if (!diff) {
143
+ return {
144
+ exitCode: 0,
145
+ markdown: "\u2705 No changes to review.",
146
+ posted: false,
147
+ severity: { critical: 0, high: 0, warning: 0, info: 0 }
148
+ };
149
+ }
150
+ const { diff: trimmedDiff, truncated } = truncateDiff(diff, maxDiff);
151
+ const gitCtx = buildGitContextStr(opts);
152
+ const config = new ConfigManager();
153
+ await config.load();
154
+ const registry = new ProviderRegistry(config);
155
+ await registry.initialize();
156
+ const providerId = opts.provider ?? config.getDefaultProvider();
157
+ if (!registry.has(providerId)) {
158
+ return {
159
+ exitCode: 2,
160
+ markdown: `\u274C Provider '${providerId}' is not configured. Set the right AICLI_API_KEY_* env var or pass --provider.`,
161
+ posted: false,
162
+ severity: { critical: 0, high: 0, warning: 0, info: 0 }
163
+ };
164
+ }
165
+ const providerInfo = registry.get(providerId).info;
166
+ const modelId = opts.model ?? config.get("defaultModels")[providerId] ?? providerInfo.defaultModel;
167
+ const sections = [];
168
+ sections.push(`## \u{1F916} aicli code review`);
169
+ sections.push(`*Provider: \`${providerId}\` \xB7 Model: \`${modelId}\` \xB7 aicli v${VERSION}*`);
170
+ sections.push("");
171
+ try {
172
+ if (!opts.skipCode) {
173
+ const prompt = buildReviewPrompt(trimmedDiff, gitCtx, !!opts.detailed);
174
+ const review = await runOnePrompt(registry, providerId, modelId, prompt);
175
+ sections.push("### Code Review");
176
+ sections.push(review);
177
+ sections.push("");
178
+ }
179
+ if (!opts.skipSecurity) {
180
+ const prompt = buildSecurityReviewPrompt(trimmedDiff, gitCtx);
181
+ const review = await runOnePrompt(registry, providerId, modelId, prompt);
182
+ sections.push("### Security Review");
183
+ sections.push(review);
184
+ sections.push("");
185
+ }
186
+ } catch (err) {
187
+ return {
188
+ exitCode: 2,
189
+ markdown: `\u274C Review call failed: ${err.message}`,
190
+ posted: false,
191
+ severity: { critical: 0, high: 0, warning: 0, info: 0 }
192
+ };
193
+ }
194
+ if (truncated) {
195
+ sections.push(`> \u26A0 Diff was truncated to ${maxDiff} chars (original: ${diff.length}). Consider splitting large PRs.`);
196
+ }
197
+ const markdown = sections.join("\n");
198
+ const severity = countSeverity(markdown);
199
+ let posted = false;
200
+ let updatedCommentId;
201
+ if (opts.post && opts.pr != null && !opts.dryRun) {
202
+ try {
203
+ const result = postOrUpdatePrComment(opts.pr, markdown, opts.update !== false);
204
+ posted = true;
205
+ updatedCommentId = result.id;
206
+ } catch (err) {
207
+ return {
208
+ exitCode: 2,
209
+ markdown: `${markdown}
210
+
211
+ ---
212
+
213
+ \u274C Failed to post comment: ${err.message}`,
214
+ posted: false,
215
+ severity
216
+ };
217
+ }
218
+ }
219
+ const hasBlocker = severity.critical > 0 || severity.high > 0;
220
+ return {
221
+ exitCode: hasBlocker ? 1 : 0,
222
+ markdown,
223
+ posted,
224
+ updatedCommentId,
225
+ severity
226
+ };
227
+ }
228
+ export {
229
+ CI_COMMENT_MARKER,
230
+ countSeverity,
231
+ runCi
232
+ };
@@ -36,7 +36,7 @@ import {
36
36
  TEST_TIMEOUT,
37
37
  VERSION,
38
38
  buildUserIdentityPrompt
39
- } from "./chunk-VXFBUMWG.js";
39
+ } from "./chunk-EYUL75S4.js";
40
40
  import "./chunk-PDX44BCA.js";
41
41
  export {
42
42
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -2,25 +2,26 @@
2
2
  import {
3
3
  getConfigDirUsage,
4
4
  listRecentCrashes
5
- } from "./chunk-CSWA553M.js";
5
+ } from "./chunk-ZFEFS5HS.js";
6
6
  import {
7
7
  ProviderRegistry
8
- } from "./chunk-YCIJZ2XS.js";
8
+ } from "./chunk-XZBA3NHG.js";
9
9
  import {
10
10
  ConfigManager
11
- } from "./chunk-DX5JYNVN.js";
11
+ } from "./chunk-MII3VYZ7.js";
12
12
  import {
13
13
  getStatsSnapshot,
14
14
  getTopFailingTools,
15
15
  getTopUsedTools,
16
16
  resetStats
17
- } from "./chunk-2JLVHUMU.js";
17
+ } from "./chunk-GZNBAFTO.js";
18
+ import "./chunk-OWPFDHKC.js";
18
19
  import "./chunk-2ZD3YTVM.js";
19
20
  import {
20
21
  DEV_STATE_FILE_NAME,
21
22
  MEMORY_FILE_NAME,
22
23
  VERSION
23
- } from "./chunk-VXFBUMWG.js";
24
+ } from "./chunk-EYUL75S4.js";
24
25
  import "./chunk-PDX44BCA.js";
25
26
 
26
27
  // src/diagnostics/doctor-cli.ts
@@ -36,7 +36,7 @@ import {
36
36
  VERSION,
37
37
  buildUserIdentityPrompt,
38
38
  runTestsTool
39
- } from "./chunk-2YEBAHAB.js";
39
+ } from "./chunk-OH5HVL5O.js";
40
40
  import {
41
41
  hasSemanticIndex,
42
42
  semanticSearch
@@ -286,7 +286,27 @@ var ConfigSchema = z.object({
286
286
  name: z.string().optional()
287
287
  })).default([]),
288
288
  fallback: z.string().optional()
289
- }).default({ enabled: false, rules: [] })
289
+ }).default({ enabled: false, rules: [] }),
290
+ // Provider 容错(v0.4.144+):网络抖动 / 5xx / 429 时,先在当前 provider 上做指数退避重试,
291
+ // 仍失败则按顺序尝试 chain 里的 fallback provider。auth / 400 / 422 等"硬失败"直接跳过同
292
+ // provider 重试,但仍会尝试 chain(不同 key/schema 可能能成功)。
293
+ // 关键约束:流式 API 一旦已经向终端 yield 过 chunk,错误立刻向上抛 —— 不能"已经吐了一半"再
294
+ // 重新发起请求,会撕裂用户看到的输出。
295
+ // enabled 默认 false:保持向前兼容,未配置 chain 的用户行为不变。
296
+ fallback: z.object({
297
+ enabled: z.boolean().default(false),
298
+ /** 同 provider 重试次数(不含首次调用)。0 = 不重试,直接走 chain。 */
299
+ retries: z.number().int().min(0).max(10).default(2),
300
+ /** 首次退避等待(ms),后续按 2 的幂指数增长。 */
301
+ initialBackoffMs: z.number().int().positive().default(500),
302
+ /** 单次退避的硬上限(ms)。 */
303
+ maxBackoffMs: z.number().int().positive().default(8e3),
304
+ /** 兜底 provider 链,每个 entry 仅尝试 1 次。model 省略则使用 provider 的 defaultModel。 */
305
+ chain: z.array(z.object({
306
+ provider: z.string(),
307
+ model: z.string().optional()
308
+ })).default([])
309
+ }).default({ enabled: false, retries: 2, initialBackoffMs: 500, maxBackoffMs: 8e3, chain: [] })
290
310
  });
291
311
 
292
312
  // src/config/env-loader.ts
@@ -12548,7 +12568,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
12548
12568
  case "test": {
12549
12569
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
12550
12570
  try {
12551
- const { executeTests } = await import("./run-tests-VK2S7V3X.js");
12571
+ const { executeTests } = await import("./run-tests-MDJLWONR.js");
12552
12572
  const argStr = args.join(" ").trim();
12553
12573
  let testArgs = {};
12554
12574
  if (argStr) {
@@ -386,7 +386,7 @@ ${content}`);
386
386
  }
387
387
  }
388
388
  async function runTaskMode(config, providers, configManager, topic) {
389
- const { TaskOrchestrator } = await import("./task-orchestrator-E46H7NUK.js");
389
+ const { TaskOrchestrator } = await import("./task-orchestrator-5N4WD2VT.js");
390
390
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
391
391
  let interrupted = false;
392
392
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ buildReviewPrompt,
4
+ buildSecurityReviewPrompt
5
+ } from "./chunk-HLWUDRBO.js";
2
6
  import {
3
7
  McpManager,
4
8
  SNAPSHOT_PROMPT,
@@ -16,12 +20,12 @@ import {
16
20
  saveDevState,
17
21
  sessionHasMeaningfulContent,
18
22
  setupProxy
19
- } from "./chunk-GDCLDXEC.js";
23
+ } from "./chunk-JFLT57TG.js";
20
24
  import {
21
25
  getConfigDirUsage,
22
26
  listRecentCrashes,
23
27
  writeCrashLog
24
- } from "./chunk-CSWA553M.js";
28
+ } from "./chunk-ZFEFS5HS.js";
25
29
  import {
26
30
  CONTENT_ONLY_STREAM_REMINDER,
27
31
  HALLUCINATION_CORRECTION_MESSAGE,
@@ -39,10 +43,10 @@ import {
39
43
  looksLikeDocumentBody,
40
44
  stripPseudoToolCalls,
41
45
  stripToolCallReminder
42
- } from "./chunk-YCIJZ2XS.js";
46
+ } from "./chunk-XZBA3NHG.js";
43
47
  import {
44
48
  ConfigManager
45
- } from "./chunk-DX5JYNVN.js";
49
+ } from "./chunk-MII3VYZ7.js";
46
50
  import {
47
51
  ToolExecutor,
48
52
  ToolRegistry,
@@ -61,17 +65,22 @@ import {
61
65
  spawnAgentContext,
62
66
  theme,
63
67
  undoStack
64
- } from "./chunk-YPFCJ5KC.js";
68
+ } from "./chunk-A3RPBWBF.js";
65
69
  import "./chunk-UQQJWHRV.js";
66
70
  import "./chunk-2DXY7UGF.js";
67
- import "./chunk-IMYOBDJG.js";
71
+ import "./chunk-QWP4RFMS.js";
68
72
  import {
69
73
  getStatsSnapshot,
70
74
  getTopFailingTools,
71
75
  getTopUsedTools,
72
76
  installFlushOnExit
73
- } from "./chunk-2JLVHUMU.js";
74
- import "./chunk-2ZD3YTVM.js";
77
+ } from "./chunk-GZNBAFTO.js";
78
+ import "./chunk-OWPFDHKC.js";
79
+ import {
80
+ AuthError,
81
+ ProviderError,
82
+ RateLimitError
83
+ } from "./chunk-2ZD3YTVM.js";
75
84
  import {
76
85
  AGENTIC_BEHAVIOR_GUIDELINE,
77
86
  AUTHOR,
@@ -93,7 +102,7 @@ import {
93
102
  SKILLS_DIR_NAME,
94
103
  VERSION,
95
104
  buildUserIdentityPrompt
96
- } from "./chunk-VXFBUMWG.js";
105
+ } from "./chunk-EYUL75S4.js";
97
106
  import {
98
107
  formatGitContextForPrompt,
99
108
  getGitContext,
@@ -444,6 +453,8 @@ var Renderer = class {
444
453
  console.log(feat("Tool history ordering (v0.4.100+): preserve original tool-call order across multi-turn rounds \u2014 fixes reasoning drift on long agentic loops"));
445
454
  console.log(feat("save_last_response Web mode (v0.4.101\u20130.4.102+): hidden from CLI-only contexts and tee-streams chunks to disk in Web UI as the response arrives"));
446
455
  console.log(feat("write_file long-content guidance (v0.4.103+): tool description no longer encourages AI to chunk long files \u2014 single-shot writes prevented from being split into truncated parts"));
456
+ console.log(feat("Provider retry + fallback chain (v0.4.144+): transient network / 5xx / 429 errors retry on the same provider with exponential backoff; persistent failures walk config.fallback.chain (per-entry provider+model). Opt-in via config.fallback.enabled. Stream-safe: never retries after first chunk yielded"));
457
+ console.log(feat("aicli ci \u2014 headless PR review for GitHub Actions (v0.4.145+): `aicli ci --pr <num> --post` reads diff via gh CLI, runs code + security review, posts/updates a single PR comment via sentinel marker. Drop-in workflow YAML at docs/github-actions-example.yml. Critical/high findings \u2192 exit 1 (CI gate)"));
447
458
  console.log();
448
459
  }
449
460
  printPrompt(provider, _model) {
@@ -988,62 +999,6 @@ function copyToClipboard(text) {
988
999
  }
989
1000
  }
990
1001
  }
991
- function buildReviewPrompt(diff, gitContextStr, detailed) {
992
- const level = detailed ? "Please perform a detailed in-depth review covering: security, performance, maintainability, error handling, naming conventions, and code duplication." : "Please perform a concise code review focusing on bugs, security issues, and key improvement suggestions.";
993
- return `# Code Review Request
994
-
995
- ${level}
996
-
997
- ## Git Status
998
- ${gitContextStr}
999
-
1000
- ## Code Changes (diff)
1001
- \`\`\`diff
1002
- ${diff}
1003
- \`\`\`
1004
-
1005
- ## Output Format
1006
- Please structure your review as follows:
1007
- 1. **Overall Assessment**: One-sentence summary of the change quality
1008
- 2. **Issues** (if any): Each issue with [Severity] file:line \u2014 description + suggested fix
1009
- 3. **Improvement Suggestions** (if any): Non-critical but recommended optimizations
1010
- 4. **Highlights** (if any): Good practices worth acknowledging
1011
-
1012
- Severity levels: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
1013
- }
1014
- function buildSecurityReviewPrompt(diff, gitContextStr) {
1015
- return `# Security Vulnerability Review
1016
-
1017
- Analyze the following code changes **exclusively for security vulnerabilities**.
1018
-
1019
- ## Categories to check:
1020
- 1. **Injection** \u2014 SQL, command, path traversal, XSS, template injection
1021
- 2. **Authentication & Authorization** \u2014 hardcoded credentials, missing auth checks, privilege escalation
1022
- 3. **Secrets & Sensitive Data** \u2014 API keys, tokens, passwords in code, logging sensitive data
1023
- 4. **Input Validation** \u2014 missing validation, unsafe deserialization, buffer issues
1024
- 5. **Cryptography** \u2014 weak algorithms, improper random, hardcoded IVs/salts
1025
- 6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
1026
- 7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
1027
- 8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
1028
-
1029
- ## Git Status
1030
- ${gitContextStr}
1031
-
1032
- ## Code Changes (diff)
1033
- \`\`\`diff
1034
- ${diff}
1035
- \`\`\`
1036
-
1037
- ## Output Format
1038
- For each finding:
1039
- - **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
1040
- - **Category**: (from list above)
1041
- - **File & location**: file:line
1042
- - **Description**: what the vulnerability is and how it could be exploited
1043
- - **Recommended fix**: specific code change to resolve
1044
-
1045
- If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
1046
- }
1047
1002
  var CommandRegistry = class {
1048
1003
  commands = /* @__PURE__ */ new Map();
1049
1004
  register(command) {
@@ -1814,7 +1769,7 @@ No tools match "${filter}".
1814
1769
  const { join: join6 } = await import("path");
1815
1770
  const { existsSync: existsSync6 } = await import("fs");
1816
1771
  const { getGitRoot: getGitRoot2 } = await import("./git-context-7KIP4X2V.js");
1817
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-YVTEGGKB.js");
1772
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-MNI3S4EV.js");
1818
1773
  const { approveProject, hashMcpFile } = await import("./project-trust-IFM7FXEV.js");
1819
1774
  const cwd = process.cwd();
1820
1775
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -2875,7 +2830,7 @@ ${hint}` : "")
2875
2830
  usage: "/test [command|filter]",
2876
2831
  async execute(args, ctx) {
2877
2832
  try {
2878
- const { executeTests } = await import("./run-tests-3KUDRXXZ.js");
2833
+ const { executeTests } = await import("./run-tests-MYF3A4YR.js");
2879
2834
  const argStr = args.join(" ").trim();
2880
2835
  let testArgs = {};
2881
2836
  if (argStr) {
@@ -4464,6 +4419,154 @@ function stripRoutingTags(message) {
4464
4419
  return message.replace(/(?:^|\s)#(fast|deep|default)\b/gi, " ").replace(/\s{2,}/g, " ").trim();
4465
4420
  }
4466
4421
 
4422
+ // src/providers/fallback.ts
4423
+ var TRANSIENT_CODES = /* @__PURE__ */ new Set([
4424
+ "ECONNRESET",
4425
+ "ETIMEDOUT",
4426
+ "EAI_AGAIN",
4427
+ "EHOSTUNREACH",
4428
+ "ENOTFOUND",
4429
+ "EPIPE",
4430
+ "ECONNREFUSED",
4431
+ "EHOSTDOWN",
4432
+ "ENETUNREACH",
4433
+ "UND_ERR_SOCKET",
4434
+ // undici socket errors
4435
+ "UND_ERR_CONNECT_TIMEOUT"
4436
+ ]);
4437
+ function classifyError(err) {
4438
+ if (err == null) return "unknown";
4439
+ const seen = /* @__PURE__ */ new Set();
4440
+ let cur = err;
4441
+ for (let i = 0; i < 5 && cur && !seen.has(cur); i++) {
4442
+ seen.add(cur);
4443
+ const e = cur;
4444
+ if (e.name === "AbortError" || e.code === "ABORT_ERR") return "aborted";
4445
+ if (err instanceof AuthError) return "auth";
4446
+ if (err instanceof RateLimitError) return "rate_limit";
4447
+ const status = e.status ?? e.statusCode;
4448
+ if (typeof status === "number") {
4449
+ if (status === 429) return "rate_limit";
4450
+ if (status === 401 || status === 403) return "auth";
4451
+ if (status === 400 || status === 422) return "bad_request";
4452
+ if (status >= 500 && status < 600) return "transient";
4453
+ }
4454
+ if (e.code && TRANSIENT_CODES.has(e.code)) return "transient";
4455
+ cur = e.cause;
4456
+ }
4457
+ return "unknown";
4458
+ }
4459
+ function planAttempts(initialProvider, initialModel, registry, opts) {
4460
+ const out = [];
4461
+ for (let i = 0; i <= opts.retries; i++) {
4462
+ out.push({ providerId: initialProvider, model: initialModel, retryNumber: i });
4463
+ }
4464
+ for (const entry of opts.chain) {
4465
+ if (entry.provider === initialProvider) continue;
4466
+ if (!registry.has(entry.provider)) continue;
4467
+ const p = registry.get(entry.provider);
4468
+ const model = entry.model ?? p.info.defaultModel;
4469
+ out.push({ providerId: entry.provider, model, retryNumber: 0 });
4470
+ }
4471
+ return out;
4472
+ }
4473
+ function backoffDelay(retryNumber, opts, klass) {
4474
+ const base = klass === "rate_limit" ? Math.max(opts.initialBackoffMs, 2e3) : opts.initialBackoffMs;
4475
+ const expo = base * Math.pow(2, retryNumber);
4476
+ const jitter = Math.random() * (base / 2);
4477
+ return Math.min(opts.maxBackoffMs, expo + jitter);
4478
+ }
4479
+ function sleep(ms) {
4480
+ return new Promise((r) => setTimeout(r, ms));
4481
+ }
4482
+ async function withFallback(initialProvider, initialModel, registry, opts, call) {
4483
+ if (!opts.enabled) {
4484
+ return call(registry.get(initialProvider), initialModel);
4485
+ }
4486
+ const attempts = planAttempts(initialProvider, initialModel, registry, opts);
4487
+ let lastErr;
4488
+ for (let i = 0; i < attempts.length; i++) {
4489
+ const a = attempts[i];
4490
+ const prev = i > 0 ? attempts[i - 1] : void 0;
4491
+ if (prev && prev.providerId !== a.providerId) {
4492
+ opts.onFallback?.(prev.providerId, a.providerId, a.model, lastErr);
4493
+ } else if (a.retryNumber > 0) {
4494
+ opts.onRetry?.(a.retryNumber, opts.retries, a.providerId, lastErr);
4495
+ await sleep(backoffDelay(a.retryNumber - 1, opts, classifyError(lastErr)));
4496
+ }
4497
+ try {
4498
+ return await call(registry.get(a.providerId), a.model);
4499
+ } catch (err) {
4500
+ lastErr = err;
4501
+ const klass = classifyError(err);
4502
+ if (klass === "aborted") throw err;
4503
+ if (klass === "auth" || klass === "bad_request") {
4504
+ let j = i + 1;
4505
+ while (j < attempts.length && attempts[j].providerId === a.providerId) j++;
4506
+ if (j >= attempts.length) throw err;
4507
+ i = j - 1;
4508
+ continue;
4509
+ }
4510
+ if (i === attempts.length - 1) throw err;
4511
+ }
4512
+ }
4513
+ throw lastErr;
4514
+ }
4515
+ async function* withFallbackStream(initialProvider, initialModel, registry, opts, createStream) {
4516
+ if (!opts.enabled) {
4517
+ yield* createStream(registry.get(initialProvider), initialModel);
4518
+ return;
4519
+ }
4520
+ const attempts = planAttempts(initialProvider, initialModel, registry, opts);
4521
+ let lastErr;
4522
+ for (let i = 0; i < attempts.length; i++) {
4523
+ const a = attempts[i];
4524
+ const prev = i > 0 ? attempts[i - 1] : void 0;
4525
+ if (prev && prev.providerId !== a.providerId) {
4526
+ opts.onFallback?.(prev.providerId, a.providerId, a.model, lastErr);
4527
+ } else if (a.retryNumber > 0) {
4528
+ opts.onRetry?.(a.retryNumber, opts.retries, a.providerId, lastErr);
4529
+ await sleep(backoffDelay(a.retryNumber - 1, opts, classifyError(lastErr)));
4530
+ }
4531
+ let yielded = false;
4532
+ try {
4533
+ const gen = createStream(registry.get(a.providerId), a.model);
4534
+ for await (const chunk of gen) {
4535
+ yielded = true;
4536
+ yield chunk;
4537
+ }
4538
+ return;
4539
+ } catch (err) {
4540
+ lastErr = err;
4541
+ if (yielded) throw err;
4542
+ const klass = classifyError(err);
4543
+ if (klass === "aborted") throw err;
4544
+ if (klass === "auth" || klass === "bad_request") {
4545
+ let j = i + 1;
4546
+ while (j < attempts.length && attempts[j].providerId === a.providerId) j++;
4547
+ if (j >= attempts.length) throw err;
4548
+ i = j - 1;
4549
+ continue;
4550
+ }
4551
+ if (i === attempts.length - 1) throw err;
4552
+ }
4553
+ }
4554
+ throw lastErr;
4555
+ }
4556
+ function shortErrorTag(err) {
4557
+ if (err == null) return "unknown";
4558
+ const e = err;
4559
+ if (e.code) return e.code;
4560
+ const status = e.status ?? e.statusCode;
4561
+ if (typeof status === "number") return `HTTP ${status}`;
4562
+ if (e instanceof ProviderError && e.message) {
4563
+ const m = e.message.match(/\[[^\]]+\]\s*(.{0,60})/);
4564
+ if (m) return m[1].trim();
4565
+ }
4566
+ if (e instanceof Error && e.message) return e.message.slice(0, 60);
4567
+ return "error";
4568
+ }
4569
+
4467
4570
  // src/repl/notify.ts
4468
4571
  import { spawn } from "child_process";
4469
4572
  import { platform as platform2 } from "os";
@@ -5820,6 +5923,35 @@ Session '${this.resumeSessionId}' not found.
5820
5923
  thinkingBudget: params.thinkingBudget
5821
5924
  };
5822
5925
  }
5926
+ /**
5927
+ * Build FallbackOptions from config + wire one-line notice callbacks to stderr.
5928
+ * v0.4.144+: when config.fallback.enabled is true, network/5xx/429 failures retry
5929
+ * on the same provider with exponential backoff; persistent failures walk the
5930
+ * config.fallback.chain. Returns disabled-opts if user hasn't opted in.
5931
+ */
5932
+ getFallbackOptions(spinner) {
5933
+ const cfg = this.config.get("fallback");
5934
+ const enabled = !!cfg?.enabled;
5935
+ return {
5936
+ enabled,
5937
+ retries: cfg?.retries ?? 2,
5938
+ initialBackoffMs: cfg?.initialBackoffMs ?? 500,
5939
+ maxBackoffMs: cfg?.maxBackoffMs ?? 8e3,
5940
+ chain: cfg?.chain ?? [],
5941
+ onRetry: (attempt, total, providerId, err) => {
5942
+ spinner.stop();
5943
+ process.stderr.write(theme.warning(` [${providerId}] retry ${attempt}/${total}: ${shortErrorTag(err)}
5944
+ `));
5945
+ spinner.start("Retrying...");
5946
+ },
5947
+ onFallback: (from, to, toModel, err) => {
5948
+ spinner.stop();
5949
+ process.stderr.write(theme.warning(` [${from} \u2192 ${to}] falling over to ${toModel}: ${shortErrorTag(err)}
5950
+ `));
5951
+ spinner.start("Retrying...");
5952
+ }
5953
+ };
5954
+ }
5823
5955
  /**
5824
5956
  * Compute smart-routing decision for this user turn.
5825
5957
  * Only considers models available for the current provider (rule skipped otherwise).
@@ -6515,9 +6647,19 @@ ${mcpBudgetNote}` : "");
6515
6647
  if (supportsStreamingTools) {
6516
6648
  const streamAc = this.setupStreamInterrupt();
6517
6649
  try {
6518
- const streamGen = provider.chatWithToolsStream(
6519
- { ...chatRequest, signal: streamAc.signal },
6520
- toolDefs
6650
+ const fallbackOpts = this.getFallbackOptions(spinner);
6651
+ const streamGen = withFallbackStream(
6652
+ this.currentProvider,
6653
+ effectiveModel,
6654
+ this.providers,
6655
+ fallbackOpts,
6656
+ (p, m) => {
6657
+ const tc = p;
6658
+ if (typeof tc.chatWithToolsStream !== "function") {
6659
+ throw new Error(`provider ${p.info.id} does not support streaming tool calls`);
6660
+ }
6661
+ return tc.chatWithToolsStream({ ...chatRequest, model: m, signal: streamAc.signal }, toolDefs);
6662
+ }
6521
6663
  );
6522
6664
  const streamResult = await this.consumeToolStream(streamGen, spinner);
6523
6665
  if (streamResult.toolCalls.length > 0) {
@@ -6546,7 +6688,14 @@ ${mcpBudgetNote}` : "");
6546
6688
  this.teardownStreamInterrupt();
6547
6689
  }
6548
6690
  } else {
6549
- result = await provider.chatWithTools(chatRequest, toolDefs);
6691
+ const fallbackOpts = this.getFallbackOptions(spinner);
6692
+ result = await withFallback(
6693
+ this.currentProvider,
6694
+ effectiveModel,
6695
+ this.providers,
6696
+ fallbackOpts,
6697
+ (p, m) => p.chatWithTools({ ...chatRequest, model: m }, toolDefs)
6698
+ );
6550
6699
  }
6551
6700
  if (result.usage) {
6552
6701
  roundUsage.inputTokens += result.usage.inputTokens;
@@ -7381,7 +7530,7 @@ program.command("web").description("Start Web UI server with browser-based chat
7381
7530
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
7382
7531
  process.exit(1);
7383
7532
  }
7384
- const { startWebServer } = await import("./server-KZOHU7OE.js");
7533
+ const { startWebServer } = await import("./server-RB4ACTOJ.js");
7385
7534
  await startWebServer({ port, host: options.host });
7386
7535
  });
7387
7536
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | logout-all <name> | migrate <name>)").action(async (action, username) => {
@@ -7548,12 +7697,12 @@ program.command("sessions").description("List recent conversation sessions").opt
7548
7697
  console.log(footer + "\n");
7549
7698
  });
7550
7699
  program.command("doctor").description("Health check: API keys, config, MCP, recent crashes, tool usage, disk usage").option("--json", "Output as JSON (for scripting)").option("--reset-stats", "Reset accumulated tool usage statistics").action(async (options) => {
7551
- const { runDoctorCli } = await import("./doctor-cli-EGD6OLJO.js");
7700
+ const { runDoctorCli } = await import("./doctor-cli-56AJTKP2.js");
7552
7701
  await runDoctorCli({ json: !!options.json, resetStats: !!options.resetStats });
7553
7702
  });
7554
7703
  program.command("batch <action> [arg] [arg2]").description("Anthropic Message Batches: submit | list | status <id> | results <id> [out] | cancel <id>").option("--dry-run", "Parse and validate input without submitting (submit only)").action(async (action, arg, arg2, options) => {
7555
7704
  try {
7556
- const batch = await import("./batch-FTCB7YHA.js");
7705
+ const batch = await import("./batch-ZMDHCPMO.js");
7557
7706
  switch (action) {
7558
7707
  case "submit":
7559
7708
  if (!arg) {
@@ -7596,7 +7745,7 @@ program.command("batch <action> [arg] [arg2]").description("Anthropic Message Ba
7596
7745
  }
7597
7746
  });
7598
7747
  program.command("mcp-serve").description("Start an MCP server over STDIO, exposing aicli's built-in tools to Claude Desktop / Cursor / other MCP clients").option("--allow-destructive", "Allow bash / run_interactive / task_create (always destructive in MCP mode)").option("--allow-outside-cwd", "Allow tool path arguments to escape the sandbox root \u2014 disabled by default").option("--tools <list>", "Comma-separated whitelist of tools to expose (default: all eligible tools)").option("--cwd <path>", "Working directory AND sandbox root (default: current directory)").action(async (options) => {
7599
- const { startMcpServer } = await import("./server-7UH5SJSC.js");
7748
+ const { startMcpServer } = await import("./server-IITOENMZ.js");
7600
7749
  await startMcpServer({
7601
7750
  allowDestructive: !!options.allowDestructive,
7602
7751
  allowOutsideCwd: !!options.allowOutsideCwd,
@@ -7604,6 +7753,29 @@ program.command("mcp-serve").description("Start an MCP server over STDIO, exposi
7604
7753
  cwd: options.cwd
7605
7754
  });
7606
7755
  });
7756
+ program.command("ci").description("Headless PR review (code + security) \u2014 reads git/gh diff, optionally posts to PR. Designed for GitHub Actions.").option("--pr <num>", "PR number; diff fetched via `gh pr diff <num>`", (v) => parseInt(v, 10)).option("--base <ref>", "Base ref for `git diff <ref>...HEAD` (ignored when --pr set)").option("--post", "Post review as a PR comment (requires gh CLI + GH_TOKEN, needs --pr)").option("--no-update", "Always create a new comment instead of updating the previous aicli review").option("--skip-code", "Skip the code review section").option("--skip-security", "Skip the security review section").option("--detailed", "Use the detailed code-review prompt").option("--max-diff <n>", "Max diff chars sent to the model (default 30000)", (v) => parseInt(v, 10)).option("--provider <id>", "Override provider (default: config.defaultProvider)").option("--model <id>", "Override model").option("--dry-run", "Print result to stdout instead of posting (overrides --post)").action(async (options) => {
7757
+ const { runCi } = await import("./ci-FCVBC4WJ.js");
7758
+ const result = await runCi({
7759
+ pr: options.pr,
7760
+ base: options.base,
7761
+ post: !!options.post,
7762
+ update: options.update !== false,
7763
+ skipCode: !!options.skipCode,
7764
+ skipSecurity: !!options.skipSecurity,
7765
+ detailed: !!options.detailed,
7766
+ maxDiffChars: options.maxDiff,
7767
+ provider: options.provider,
7768
+ model: options.model,
7769
+ dryRun: !!options.dryRun
7770
+ });
7771
+ process.stdout.write(result.markdown + "\n");
7772
+ if (result.posted) {
7773
+ process.stderr.write(`
7774
+ \u2705 Posted review to PR #${options.pr}${result.updatedCommentId ? " (updated existing comment)" : ""}
7775
+ `);
7776
+ }
7777
+ process.exit(result.exitCode);
7778
+ });
7607
7779
  program.command("help").description("Show a comprehensive guide to all aicli features and commands").action(() => {
7608
7780
  const B = "\x1B[1m";
7609
7781
  const D = "\x1B[2m";
@@ -7723,7 +7895,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
7723
7895
  }),
7724
7896
  config.get("customProviders")
7725
7897
  );
7726
- const { startHub } = await import("./hub-YNT2PVCA.js");
7898
+ const { startHub } = await import("./hub-DEZX2PAA.js");
7727
7899
  await startHub(
7728
7900
  {
7729
7901
  topic: topic ?? "",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-2YEBAHAB.js";
4
+ } from "./chunk-OH5HVL5O.js";
5
5
  import "./chunk-3RG5ZIWI.js";
6
6
  export {
7
7
  executeTests,
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-IMYOBDJG.js";
6
- import "./chunk-VXFBUMWG.js";
5
+ } from "./chunk-QWP4RFMS.js";
6
+ import "./chunk-EYUL75S4.js";
7
7
  import "./chunk-PDX44BCA.js";
8
8
  export {
9
9
  executeTests,
@@ -1,19 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ToolRegistry
4
- } from "./chunk-YPFCJ5KC.js";
4
+ } from "./chunk-A3RPBWBF.js";
5
5
  import "./chunk-UQQJWHRV.js";
6
6
  import "./chunk-2DXY7UGF.js";
7
- import "./chunk-IMYOBDJG.js";
7
+ import "./chunk-QWP4RFMS.js";
8
+ import {
9
+ runTool
10
+ } from "./chunk-GZNBAFTO.js";
8
11
  import {
9
12
  getDangerLevel,
10
- runTool,
11
13
  schemaToJsonSchema
12
- } from "./chunk-2JLVHUMU.js";
14
+ } from "./chunk-OWPFDHKC.js";
13
15
  import "./chunk-2ZD3YTVM.js";
14
16
  import {
15
17
  VERSION
16
- } from "./chunk-VXFBUMWG.js";
18
+ } from "./chunk-EYUL75S4.js";
17
19
  import "./chunk-4BKXL7SM.js";
18
20
  import "./chunk-7ZJN4KLV.js";
19
21
  import "./chunk-KHYD3WXE.js";
@@ -14,7 +14,7 @@ import {
14
14
  loadDevState,
15
15
  persistToolRound,
16
16
  setupProxy
17
- } from "./chunk-GDCLDXEC.js";
17
+ } from "./chunk-JFLT57TG.js";
18
18
  import {
19
19
  CONTENT_ONLY_STREAM_REMINDER,
20
20
  HALLUCINATION_CORRECTION_MESSAGE,
@@ -28,10 +28,10 @@ import {
28
28
  looksLikeDocumentBody,
29
29
  stripPseudoToolCalls,
30
30
  stripToolCallReminder
31
- } from "./chunk-YCIJZ2XS.js";
31
+ } from "./chunk-XZBA3NHG.js";
32
32
  import {
33
33
  ConfigManager
34
- } from "./chunk-DX5JYNVN.js";
34
+ } from "./chunk-MII3VYZ7.js";
35
35
  import {
36
36
  ToolExecutor,
37
37
  ToolRegistry,
@@ -49,14 +49,16 @@ import {
49
49
  spawnAgentContext,
50
50
  truncateOutput,
51
51
  undoStack
52
- } from "./chunk-YPFCJ5KC.js";
52
+ } from "./chunk-A3RPBWBF.js";
53
53
  import "./chunk-UQQJWHRV.js";
54
54
  import "./chunk-2DXY7UGF.js";
55
- import "./chunk-IMYOBDJG.js";
55
+ import "./chunk-QWP4RFMS.js";
56
56
  import {
57
- getDangerLevel,
58
57
  runTool
59
- } from "./chunk-2JLVHUMU.js";
58
+ } from "./chunk-GZNBAFTO.js";
59
+ import {
60
+ getDangerLevel
61
+ } from "./chunk-OWPFDHKC.js";
60
62
  import "./chunk-2ZD3YTVM.js";
61
63
  import {
62
64
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -76,7 +78,7 @@ import {
76
78
  SKILLS_DIR_NAME,
77
79
  VERSION,
78
80
  buildUserIdentityPrompt
79
- } from "./chunk-VXFBUMWG.js";
81
+ } from "./chunk-EYUL75S4.js";
80
82
  import {
81
83
  formatGitContextForPrompt,
82
84
  getGitContext,
@@ -2460,7 +2462,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
2460
2462
  case "test": {
2461
2463
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
2462
2464
  try {
2463
- const { executeTests } = await import("./run-tests-3KUDRXXZ.js");
2465
+ const { executeTests } = await import("./run-tests-MYF3A4YR.js");
2464
2466
  const argStr = args.join(" ").trim();
2465
2467
  let testArgs = {};
2466
2468
  if (argStr) {
@@ -3,18 +3,20 @@ import {
3
3
  ToolRegistry,
4
4
  googleSearchContext,
5
5
  truncateOutput
6
- } from "./chunk-YPFCJ5KC.js";
6
+ } from "./chunk-A3RPBWBF.js";
7
7
  import "./chunk-UQQJWHRV.js";
8
8
  import "./chunk-2DXY7UGF.js";
9
- import "./chunk-IMYOBDJG.js";
9
+ import "./chunk-QWP4RFMS.js";
10
10
  import {
11
- getDangerLevel,
12
11
  runTool
13
- } from "./chunk-2JLVHUMU.js";
12
+ } from "./chunk-GZNBAFTO.js";
13
+ import {
14
+ getDangerLevel
15
+ } from "./chunk-OWPFDHKC.js";
14
16
  import "./chunk-2ZD3YTVM.js";
15
17
  import {
16
18
  SUBAGENT_ALLOWED_TOOLS
17
- } from "./chunk-VXFBUMWG.js";
19
+ } from "./chunk-EYUL75S4.js";
18
20
  import "./chunk-4BKXL7SM.js";
19
21
  import "./chunk-7ZJN4KLV.js";
20
22
  import "./chunk-KHYD3WXE.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.143",
3
+ "version": "0.4.145",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",