jinzd-ai-cli 0.4.11 → 0.4.13

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.
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  EnvLoader,
4
4
  schemaToJsonSchema
5
- } from "./chunk-ZNEOCSLS.js";
5
+ } from "./chunk-TXCLZ72H.js";
6
6
  import {
7
7
  APP_NAME,
8
8
  CONFIG_DIR_NAME,
@@ -15,7 +15,7 @@ import {
15
15
  MCP_TOOL_PREFIX,
16
16
  PLUGINS_DIR_NAME,
17
17
  VERSION
18
- } from "./chunk-QQFHC5ZL.js";
18
+ } from "./chunk-DP3M26PP.js";
19
19
 
20
20
  // src/config/config-manager.ts
21
21
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -277,7 +277,8 @@ ${err}`
277
277
  if (rawValue === "true") value = true;
278
278
  else if (rawValue === "false") value = false;
279
279
  else if (rawValue !== "" && !isNaN(Number(rawValue))) value = Number(rawValue);
280
- let current = this.config;
280
+ const draft = JSON.parse(JSON.stringify(this.config));
281
+ let current = draft;
281
282
  for (let i = 0; i < keys.length - 1; i++) {
282
283
  const key = keys[i];
283
284
  if (current[key] == null || typeof current[key] !== "object") {
@@ -286,6 +287,12 @@ ${err}`
286
287
  current = current[key];
287
288
  }
288
289
  current[keys[keys.length - 1]] = value;
290
+ const result = ConfigSchema.safeParse(draft);
291
+ if (!result.success) {
292
+ const firstErr = result.error.errors[0];
293
+ throw new ConfigError(`Invalid config value for "${path}": ${firstErr?.message ?? "validation failed"}`);
294
+ }
295
+ this.config = result.data;
289
296
  this.save();
290
297
  }
291
298
  /** 获取完整配置对象的格式化 JSON 字符串(用于 /config show 等展示) */
@@ -1331,6 +1338,9 @@ var OpenAICompatibleProvider = class extends BaseProvider {
1331
1338
  }
1332
1339
  yield { type: "done" };
1333
1340
  } catch (err) {
1341
+ if (err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
1342
+ throw err;
1343
+ }
1334
1344
  throw this.wrapError(err);
1335
1345
  }
1336
1346
  }
@@ -2073,7 +2083,7 @@ var ProviderRegistry = class {
2073
2083
  };
2074
2084
 
2075
2085
  // src/session/session-manager.ts
2076
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, unlinkSync } from "fs";
2086
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, unlinkSync, renameSync } from "fs";
2077
2087
  import { join as join2 } from "path";
2078
2088
  import { v4 as uuidv4 } from "uuid";
2079
2089
 
@@ -2298,7 +2308,9 @@ var SessionManager = class {
2298
2308
  if (!this._current) return;
2299
2309
  mkdirSync2(this.historyDir, { recursive: true });
2300
2310
  const filePath = join2(this.historyDir, `${this._current.id}.json`);
2301
- writeFileSync2(filePath, JSON.stringify(this._current.toJSON(), null, 2), "utf-8");
2311
+ const tmpPath = filePath + ".tmp";
2312
+ writeFileSync2(tmpPath, JSON.stringify(this._current.toJSON(), null, 2), "utf-8");
2313
+ renameSync(tmpPath, filePath);
2302
2314
  }
2303
2315
  loadSession(id) {
2304
2316
  const filePath = join2(this.historyDir, `${id}.json`);
@@ -2530,6 +2542,7 @@ var McpClient = class {
2530
2542
  config;
2531
2543
  process = null;
2532
2544
  nextId = 1;
2545
+ // M8: wraps at MAX_SAFE_INTEGER via getNextId()
2533
2546
  connected = false;
2534
2547
  serverInfo = null;
2535
2548
  /** stderr 收集(最多保留最后 2KB,用于错误报告) */
@@ -2662,6 +2675,7 @@ var McpClient = class {
2662
2675
  return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
2663
2676
  }
2664
2677
  const id = this.nextId++;
2678
+ if (this.nextId > Number.MAX_SAFE_INTEGER - 1) this.nextId = 1;
2665
2679
  const request = {
2666
2680
  jsonrpc: "2.0",
2667
2681
  id,
@@ -2712,6 +2726,12 @@ var McpClient = class {
2712
2726
  */
2713
2727
  handleStdoutData(chunk) {
2714
2728
  this.stdoutBuffer += chunk;
2729
+ if (this.stdoutBuffer.length > 1048576) {
2730
+ process.stderr.write(`[mcp] stdout buffer exceeded 1MB \u2014 clearing
2731
+ `);
2732
+ this.stdoutBuffer = "";
2733
+ return;
2734
+ }
2715
2735
  const lines = this.stdoutBuffer.split("\n");
2716
2736
  this.stdoutBuffer = lines.pop() ?? "";
2717
2737
  for (const line of lines) {
@@ -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.11";
11
+ var VERSION = "0.4.13";
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.11";
9
+ var VERSION = "0.4.13";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -6,7 +6,7 @@ import {
6
6
  SUBAGENT_DEFAULT_MAX_ROUNDS,
7
7
  SUBAGENT_MAX_ROUNDS_LIMIT,
8
8
  runTestsTool
9
- } from "./chunk-QQFHC5ZL.js";
9
+ } from "./chunk-DP3M26PP.js";
10
10
 
11
11
  // src/tools/builtin/bash.ts
12
12
  import { execSync } from "child_process";
@@ -534,6 +534,13 @@ Suggestion: read existing text versions (.md / .txt) in the project, or install
534
534
  if (BINARY_EXTENSIONS.has(ext)) {
535
535
  return `[Binary file: ${filePath} (${ext})]
536
536
  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
+ }
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 {
537
544
  }
538
545
  const buf = readFileSync2(normalizedPath);
539
546
  if (encoding === "base64") {
@@ -1003,6 +1010,10 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
1003
1010
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
1004
1011
  if (!pattern) throw new Error("pattern is required");
1005
1012
  if (!existsSync6(rootPath)) throw new Error(`Path not found: ${rootPath}`);
1013
+ const MAX_PATTERN_LENGTH = 1e3;
1014
+ if (pattern.length > MAX_PATTERN_LENGTH) {
1015
+ throw new Error(`Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
1016
+ }
1006
1017
  let regex;
1007
1018
  try {
1008
1019
  regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
@@ -1386,11 +1397,11 @@ ${stderr}`);
1386
1397
  // src/tools/builtin/web-fetch.ts
1387
1398
  import { promises as dnsPromises } from "dns";
1388
1399
  function htmlToText(html) {
1400
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "").replace(/<svg[\s\S]*?<\/svg>/gi, "");
1389
1401
  const HTML_REGEX_LIMIT = 2e5;
1390
- if (html.length > HTML_REGEX_LIMIT) {
1391
- html = html.slice(0, HTML_REGEX_LIMIT);
1402
+ if (text.length > HTML_REGEX_LIMIT) {
1403
+ text = text.slice(0, HTML_REGEX_LIMIT);
1392
1404
  }
1393
- let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "").replace(/<svg[\s\S]*?<\/svg>/gi, "");
1394
1405
  text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, content) => {
1395
1406
  const prefix = "#".repeat(Number(lvl));
1396
1407
  return `
@@ -1848,6 +1859,14 @@ var EnvLoader = class {
1848
1859
  if (val) return val;
1849
1860
  }
1850
1861
  const dynamicEnvVar = `AICLI_API_KEY_${providerId.toUpperCase().replace(/-/g, "_")}`;
1862
+ if (fixedEnvVar && fixedEnvVar !== dynamicEnvVar) {
1863
+ const fixedVal = process.env[fixedEnvVar];
1864
+ const dynVal = process.env[dynamicEnvVar];
1865
+ if (fixedVal && dynVal && fixedVal !== dynVal) {
1866
+ process.stderr.write(`[warn] env var collision: ${fixedEnvVar} and ${dynamicEnvVar} have different values for provider "${providerId}". Using ${fixedEnvVar}.
1867
+ `);
1868
+ }
1869
+ }
1851
1870
  return process.env[dynamicEnvVar] || void 0;
1852
1871
  }
1853
1872
  static getDefaultProvider() {
@@ -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-75SMDOOF.js");
384
+ const { TaskOrchestrator } = await import("./task-orchestrator-XXOS7WHR.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-XIG6AWFW.js";
26
+ } from "./chunk-52BIUGQ2.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-ZNEOCSLS.js";
41
+ } from "./chunk-TXCLZ72H.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-QQFHC5ZL.js";
61
+ } from "./chunk-DP3M26PP.js";
62
62
 
63
63
  // src/index.ts
64
64
  import { program } from "commander";
@@ -1914,7 +1914,7 @@ ${hint}` : "")
1914
1914
  description: "Run project tests and show structured report",
1915
1915
  usage: "/test [command|filter]",
1916
1916
  async execute(args, _ctx) {
1917
- const { executeTests } = await import("./run-tests-F7MPQZSN.js");
1917
+ const { executeTests } = await import("./run-tests-PVVT37LK.js");
1918
1918
  const argStr = args.join(" ").trim();
1919
1919
  let testArgs = {};
1920
1920
  if (argStr) {
@@ -4254,6 +4254,10 @@ Session '${this.resumeSessionId}' not found.
4254
4254
  }, PASTE_DEBOUNCE_MS);
4255
4255
  });
4256
4256
  this.rl.on("close", () => {
4257
+ if (pasteTimer) {
4258
+ clearTimeout(pasteTimer);
4259
+ pasteTimer = null;
4260
+ }
4257
4261
  if (!processing) {
4258
4262
  resolve3();
4259
4263
  }
@@ -5524,7 +5528,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5524
5528
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5525
5529
  process.exit(1);
5526
5530
  }
5527
- const { startWebServer } = await import("./server-A37MB2OC.js");
5531
+ const { startWebServer } = await import("./server-KPNMFMDU.js");
5528
5532
  await startWebServer({ port, host: options.host });
5529
5533
  });
5530
5534
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -5757,7 +5761,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5757
5761
  }),
5758
5762
  config.get("customProviders")
5759
5763
  );
5760
- const { startHub } = await import("./hub-BDIESJ2N.js");
5764
+ const { startHub } = await import("./hub-LURPCJ4Z.js");
5761
5765
  await startHub(
5762
5766
  {
5763
5767
  topic: topic ?? "",
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-QQFHC5ZL.js";
5
+ } from "./chunk-DP3M26PP.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-6VJEC2KE.js";
4
+ } from "./chunk-TLOCRYJC.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-XIG6AWFW.js";
21
+ } from "./chunk-52BIUGQ2.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-ZNEOCSLS.js";
35
+ } from "./chunk-TXCLZ72H.js";
36
36
  import {
37
37
  AGENTIC_BEHAVIOR_GUIDELINE,
38
38
  CONTEXT_FILE_CANDIDATES,
@@ -44,7 +44,7 @@ import {
44
44
  PLAN_MODE_SYSTEM_ADDON,
45
45
  SKILLS_DIR_NAME,
46
46
  VERSION
47
- } from "./chunk-QQFHC5ZL.js";
47
+ } from "./chunk-DP3M26PP.js";
48
48
 
49
49
  // src/web/server.ts
50
50
  import express from "express";
@@ -57,7 +57,7 @@ import { networkInterfaces } from "os";
57
57
  // src/web/tool-executor-web.ts
58
58
  import { randomUUID } from "crypto";
59
59
  import { existsSync, readFileSync } from "fs";
60
- var ToolExecutorWeb = class {
60
+ var ToolExecutorWeb = class _ToolExecutorWeb {
61
61
  constructor(registry, ws) {
62
62
  this.registry = registry;
63
63
  this.ws = ws;
@@ -71,6 +71,10 @@ var ToolExecutorWeb = class {
71
71
  pendingConfirms = /* @__PURE__ */ new Map();
72
72
  /** Pending batch confirm promises */
73
73
  pendingBatchConfirms = /* @__PURE__ */ new Map();
74
+ /** M7 fix: timers for confirm cleanup if client crashes */
75
+ pendingTimers = /* @__PURE__ */ new Map();
76
+ static CONFIRM_TIMEOUT_MS = 5 * 60 * 1e3;
77
+ // 5 minutes
74
78
  /** Publicly readable by SessionHandler to check if confirm is active */
75
79
  confirming = false;
76
80
  /** Session-level auto-approve toggle (/yolo command) */
@@ -86,10 +90,19 @@ var ToolExecutorWeb = class {
86
90
  if (opts.permissionRules) this.permissionRules = opts.permissionRules;
87
91
  if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
88
92
  }
93
+ /** Clear M7 timeout timer for a requestId */
94
+ clearPendingTimer(requestId) {
95
+ const timer = this.pendingTimers.get(requestId);
96
+ if (timer) {
97
+ clearTimeout(timer);
98
+ this.pendingTimers.delete(requestId);
99
+ }
100
+ }
89
101
  /** Resolve a pending confirm from client response */
90
102
  resolveConfirm(requestId, approved) {
91
103
  const resolve3 = this.pendingConfirms.get(requestId);
92
104
  if (resolve3) {
105
+ this.clearPendingTimer(requestId);
93
106
  this.pendingConfirms.delete(requestId);
94
107
  this.confirming = false;
95
108
  resolve3(approved);
@@ -99,6 +112,7 @@ var ToolExecutorWeb = class {
99
112
  resolveBatchConfirm(requestId, decision) {
100
113
  const resolve3 = this.pendingBatchConfirms.get(requestId);
101
114
  if (resolve3) {
115
+ this.clearPendingTimer(requestId);
102
116
  this.pendingBatchConfirms.delete(requestId);
103
117
  this.confirming = false;
104
118
  if (decision === "all" || decision === "none") {
@@ -190,6 +204,11 @@ var ToolExecutorWeb = class {
190
204
  this.send(msg);
191
205
  return new Promise((resolve3) => {
192
206
  this.pendingConfirms.set(requestId, resolve3);
207
+ this.pendingTimers.set(requestId, setTimeout(() => {
208
+ if (this.pendingConfirms.has(requestId)) {
209
+ this.resolveConfirm(requestId, false);
210
+ }
211
+ }, _ToolExecutorWeb.CONFIRM_TIMEOUT_MS));
193
212
  });
194
213
  }
195
214
  /** WebSocket-based batch confirm */
@@ -210,6 +229,11 @@ var ToolExecutorWeb = class {
210
229
  this.send(msg);
211
230
  return new Promise((resolve3) => {
212
231
  this.pendingBatchConfirms.set(requestId, resolve3);
232
+ this.pendingTimers.set(requestId, setTimeout(() => {
233
+ if (this.pendingBatchConfirms.has(requestId)) {
234
+ this.resolveBatchConfirm(requestId, "none");
235
+ }
236
+ }, _ToolExecutorWeb.CONFIRM_TIMEOUT_MS));
213
237
  });
214
238
  }
215
239
  async execute(call) {
@@ -585,10 +609,20 @@ var SessionHandler = class _SessionHandler {
585
609
  try {
586
610
  this.ensureSession();
587
611
  const session = this.sessions.current;
612
+ const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
613
+ const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
588
614
  let msgContent;
589
615
  if (images && images.length > 0) {
590
616
  const parts = [];
591
617
  for (const img of images) {
618
+ if (!ALLOWED_IMAGE_MIMES.has(img.mime)) {
619
+ this.send({ type: "error", message: `Rejected image: unsupported MIME type "${img.mime}"` });
620
+ return;
621
+ }
622
+ if (img.data && img.data.length > MAX_IMAGE_SIZE) {
623
+ this.send({ type: "error", message: `Rejected image: too large (${(img.data.length / 1024 / 1024).toFixed(1)} MB, max 10 MB)` });
624
+ return;
625
+ }
592
626
  parts.push({
593
627
  type: "image_url",
594
628
  image_url: { url: `data:${img.mime};base64,${img.data}` }
@@ -1438,7 +1472,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1438
1472
  case "test": {
1439
1473
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1440
1474
  try {
1441
- const { executeTests } = await import("./run-tests-F7MPQZSN.js");
1475
+ const { executeTests } = await import("./run-tests-PVVT37LK.js");
1442
1476
  const argStr = args.join(" ").trim();
1443
1477
  let testArgs = {};
1444
1478
  if (argStr) {
@@ -4,18 +4,35 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-ZNEOCSLS.js";
7
+ } from "./chunk-TXCLZ72H.js";
8
8
  import {
9
9
  SUBAGENT_ALLOWED_TOOLS
10
- } from "./chunk-QQFHC5ZL.js";
10
+ } from "./chunk-DP3M26PP.js";
11
11
 
12
12
  // src/hub/task-orchestrator.ts
13
13
  import { createInterface } from "readline";
14
- import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
14
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "fs";
15
15
  import { join } from "path";
16
16
  import chalk from "chalk";
17
17
 
18
18
  // src/hub/task-executor.ts
19
+ var TASK_EXCLUDED_TOOLS = /* @__PURE__ */ new Set(["write_todos", "save_memory", "ask_user", "spawn_agent"]);
20
+ var _cachedRegistry = null;
21
+ var _cachedToolDefs = null;
22
+ function getTaskToolRegistry() {
23
+ if (_cachedRegistry && _cachedToolDefs) {
24
+ return { registry: _cachedRegistry, toolDefs: _cachedToolDefs };
25
+ }
26
+ const registry = new ToolRegistry();
27
+ for (const tool of registry.listAll()) {
28
+ if (!SUBAGENT_ALLOWED_TOOLS.has(tool.definition.name) || TASK_EXCLUDED_TOOLS.has(tool.definition.name)) {
29
+ registry.unregister(tool.definition.name);
30
+ }
31
+ }
32
+ _cachedRegistry = registry;
33
+ _cachedToolDefs = registry.getDefinitions();
34
+ return { registry, toolDefs: _cachedToolDefs };
35
+ }
19
36
  async function executeTask(options) {
20
37
  const {
21
38
  task,
@@ -29,14 +46,7 @@ async function executeTask(options) {
29
46
  onToolResult,
30
47
  onRound
31
48
  } = options;
32
- const TASK_EXCLUDED_TOOLS = /* @__PURE__ */ new Set(["write_todos", "save_memory", "ask_user", "spawn_agent"]);
33
- const registry = new ToolRegistry();
34
- for (const tool of registry.listAll()) {
35
- if (!SUBAGENT_ALLOWED_TOOLS.has(tool.definition.name) || TASK_EXCLUDED_TOOLS.has(tool.definition.name)) {
36
- registry.unregister(tool.definition.name);
37
- }
38
- }
39
- const toolDefs = registry.getDefinitions();
49
+ const { registry, toolDefs } = getTaskToolRegistry();
40
50
  const contextSection = context ? `
41
51
 
42
52
  ## Reference Documents
@@ -256,7 +266,9 @@ var TaskOrchestrator = class {
256
266
  }
257
267
  saveState(plan) {
258
268
  try {
259
- writeFileSync(this.stateFilePath, JSON.stringify(plan, null, 2), "utf-8");
269
+ const tmpPath = this.stateFilePath + ".tmp";
270
+ writeFileSync(tmpPath, JSON.stringify(plan, null, 2), "utf-8");
271
+ renameSync(tmpPath, this.stateFilePath);
260
272
  } catch {
261
273
  }
262
274
  }
@@ -280,9 +292,14 @@ var TaskOrchestrator = class {
280
292
  async askResume() {
281
293
  const rl = createInterface({ input: process.stdin, output: process.stdout });
282
294
  try {
283
- const answer = await new Promise((resolve) => {
284
- rl.question(chalk.green(" Resume from saved state? (y/n) > "), resolve);
285
- });
295
+ const answer = await Promise.race([
296
+ new Promise((resolve) => {
297
+ rl.question(chalk.green(" Resume from saved state? (y/n) > "), resolve);
298
+ }),
299
+ new Promise(
300
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), 3e4)
301
+ )
302
+ ]).catch(() => "y");
286
303
  return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
287
304
  } finally {
288
305
  rl.close();
@@ -342,13 +359,29 @@ Example:
342
359
  if (!Array.isArray(parsed) || parsed.length === 0) {
343
360
  throw new Error("Empty task list");
344
361
  }
362
+ const validRoleIds = new Set(roles.map((r) => r.id));
363
+ for (let i = 0; i < parsed.length; i++) {
364
+ const t = parsed[i];
365
+ if (typeof t !== "object" || t === null) {
366
+ throw new Error(`Task #${i + 1}: not an object`);
367
+ }
368
+ if (typeof t.description !== "string" || !t.description.trim()) {
369
+ throw new Error(`Task #${i + 1}: missing or empty "description"`);
370
+ }
371
+ if (typeof t.assignee !== "string" || !t.assignee.trim()) {
372
+ throw new Error(`Task #${i + 1}: missing or empty "assignee"`);
373
+ }
374
+ if (t.dependencies != null && !Array.isArray(t.dependencies)) {
375
+ throw new Error(`Task #${i + 1}: "dependencies" must be an array`);
376
+ }
377
+ }
345
378
  const roleMap = new Map(roles.map((r) => [r.id, r.name]));
346
379
  const tasks = parsed.map((t, i) => ({
347
- id: t.id ?? i + 1,
380
+ id: typeof t.id === "number" ? t.id : i + 1,
348
381
  description: t.description,
349
382
  assignee: t.assignee,
350
383
  assigneeName: roleMap.get(t.assignee) ?? t.assignee,
351
- dependencies: t.dependencies ?? [],
384
+ dependencies: Array.isArray(t.dependencies) ? t.dependencies : [],
352
385
  status: "pending"
353
386
  }));
354
387
  return { goal, tasks };
@@ -413,7 +446,15 @@ Example:
413
446
  if (!nextTask) {
414
447
  const pending = plan.tasks.filter((t) => t.status === "pending");
415
448
  if (pending.length === 0) break;
416
- console.log(chalk.red(` \u2717 Cannot proceed: ${pending.length} task(s) have unmet dependencies.`));
449
+ const failedIds = new Set(plan.tasks.filter((t) => t.status === "failed").map((t) => t.id));
450
+ const hasCycle = pending.every(
451
+ (t) => t.dependencies.some((d) => !completedIds.has(d) && !failedIds.has(d) && pending.some((p) => p.id === d))
452
+ );
453
+ if (hasCycle) {
454
+ console.log(chalk.red(` \u2717 Circular dependency detected among ${pending.length} task(s):`));
455
+ } else {
456
+ console.log(chalk.red(` \u2717 Cannot proceed: ${pending.length} task(s) have unmet dependencies.`));
457
+ }
417
458
  for (const t of pending) {
418
459
  const unmet = t.dependencies.filter((d) => !completedIds.has(d));
419
460
  console.log(chalk.dim(` Task #${t.id}: waiting on [${unmet.join(", ")}]`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",