jinzd-ai-cli 0.4.12 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@
34
34
  - **PWA Support** — Install Web UI as a desktop/mobile app, accessible over LAN
35
35
  - **Hierarchical Context** — 3-layer context files (global / project / subdirectory) auto-injected
36
36
  - **Headless Mode** — `ai-cli -p "prompt"` for CI/CD pipelines and scripting
37
- - **35+ REPL Commands** — Session management, checkpointing, code review, scaffolding, and more
37
+ - **37 REPL Commands** — Session management, checkpointing, code review, scaffolding, and more
38
38
  - **GitHub Actions CI/CD** — Automated testing on Node 20/22 + npm publish on release tags
39
39
  - **Cross-Platform** — Windows, macOS, Linux
40
40
 
@@ -172,7 +172,7 @@ AI autonomously invokes these 16 tools during conversations:
172
172
 
173
173
  **Multi-line input**: Use `\` at end of line for continuation, or paste multi-line content directly (auto-detected and merged).
174
174
 
175
- See the [full command reference](USAGE.md) for all 35+ commands.
175
+ Type `/help` in the REPL to see all 37 commands.
176
176
 
177
177
  ## CLI Parameters
178
178
 
@@ -356,7 +356,6 @@ npm run test:watch # Watch mode
356
356
 
357
357
  ## Documentation
358
358
 
359
- - [Full Usage Guide](USAGE.md) — Comprehensive reference for all features
360
359
  - [Chinese README](README.zh-CN.md) — 中文说明文档
361
360
 
362
361
  ## License
package/README.zh-CN.md CHANGED
@@ -26,7 +26,7 @@
26
26
  - **PWA 支持** — Web UI 可安装为桌面/移动应用,支持局域网访问
27
27
  - **三层级上下文** — 全局 / 项目 / 子目录上下文文件自动注入
28
28
  - **无头模式** — `aicli -p "提示词"` 用于 CI/CD 管道和脚本
29
- - **35+ REPL 命令** — 会话管理、检查点、代码审查、脚手架等
29
+ - **37 REPL 命令** — 会话管理、检查点、代码审查、脚手架等
30
30
  - **GitHub Actions CI/CD** — Node 20/22 自动测试 + Release tag 自动发布 npm
31
31
  - **跨平台** — Windows、macOS、Linux
32
32
 
@@ -164,7 +164,7 @@ AI 在对话中可自主调用 16 个工具:
164
164
 
165
165
  **多行输入**:行末加 `\` 续行,或直接粘贴多行内容(自动检测合并)。
166
166
 
167
- 完整命令参考见 [使用说明](USAGE.md)。
167
+ REPL 中输入 `/help` 查看全部 37 个命令。
168
168
 
169
169
  ## CLI 参数
170
170
 
@@ -369,7 +369,6 @@ npm run test:watch # 监听模式
369
369
 
370
370
  ## 文档
371
371
 
372
- - [完整使用说明](USAGE.md) — 所有功能的详细参考
373
372
  - [English README](README.md) — English documentation
374
373
 
375
374
  ## License
@@ -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.12";
11
+ var VERSION = "0.4.14";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -6,13 +6,14 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.12";
9
+ var VERSION = "0.4.14";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
13
13
  var HISTORY_DIR_NAME = "history";
14
14
  var PLUGINS_DIR_NAME = "plugins";
15
15
  var SKILLS_DIR_NAME = "skills";
16
+ var CUSTOM_COMMANDS_DIR_NAME = "commands";
16
17
  var CONTEXT_FILE_CANDIDATES = ["AICLI.md", "CLAUDE.md"];
17
18
  var MEMORY_FILE_NAME = "memory.md";
18
19
  var MEMORY_MAX_CHARS = 1e4;
@@ -84,6 +85,9 @@ var AGENTIC_BEHAVIOR_GUIDELINE = `# Important Behavioral Guidelines
84
85
  - Only begin using write/execute tools when the user **explicitly requests** an action (e.g., "generate", "create", "modify", "run", "start", etc.).
85
86
  - Project context files (CLAUDE.md, AICLI.md) provide background information about the project. They are NOT instructions to start working. Only use them as reference when the user asks a project-related question or task.
86
87
  - If you are unsure about the user's intent, use the ask_user tool to confirm with the user, rather than assuming and executing on your own.`;
88
+ var AUTHOR = "Jin Zhengdong";
89
+ var AUTHOR_EMAIL = "zhengdong.jin@gmail.com";
90
+ var DESCRIPTION = "Cross-platform REPL-style AI conversation tool with multi-provider and agentic tool calling support";
87
91
 
88
92
  // src/tools/builtin/run-tests.ts
89
93
  var IS_WINDOWS = platform() === "win32";
@@ -447,6 +451,7 @@ export {
447
451
  HISTORY_DIR_NAME,
448
452
  PLUGINS_DIR_NAME,
449
453
  SKILLS_DIR_NAME,
454
+ CUSTOM_COMMANDS_DIR_NAME,
450
455
  CONTEXT_FILE_CANDIDATES,
451
456
  MEMORY_FILE_NAME,
452
457
  MEMORY_MAX_CHARS,
@@ -463,6 +468,9 @@ export {
463
468
  SUBAGENT_MAX_ROUNDS_LIMIT,
464
469
  SUBAGENT_ALLOWED_TOOLS,
465
470
  AGENTIC_BEHAVIOR_GUIDELINE,
471
+ AUTHOR,
472
+ AUTHOR_EMAIL,
473
+ DESCRIPTION,
466
474
  executeTests,
467
475
  runTestsTool
468
476
  };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  EnvLoader,
4
4
  schemaToJsonSchema
5
- } from "./chunk-A3S7PUMM.js";
5
+ } from "./chunk-WIWNSN7U.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-Z5IRVRGL.js";
18
+ } from "./chunk-244SVJXW.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 等展示) */
@@ -2535,6 +2542,7 @@ var McpClient = class {
2535
2542
  config;
2536
2543
  process = null;
2537
2544
  nextId = 1;
2545
+ // M8: wraps at MAX_SAFE_INTEGER via getNextId()
2538
2546
  connected = false;
2539
2547
  serverInfo = null;
2540
2548
  /** stderr 收集(最多保留最后 2KB,用于错误报告) */
@@ -2667,6 +2675,7 @@ var McpClient = class {
2667
2675
  return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
2668
2676
  }
2669
2677
  const id = this.nextId++;
2678
+ if (this.nextId > Number.MAX_SAFE_INTEGER - 1) this.nextId = 1;
2670
2679
  const request = {
2671
2680
  jsonrpc: "2.0",
2672
2681
  id,
@@ -6,7 +6,7 @@ import {
6
6
  SUBAGENT_DEFAULT_MAX_ROUNDS,
7
7
  SUBAGENT_MAX_ROUNDS_LIMIT,
8
8
  runTestsTool
9
- } from "./chunk-Z5IRVRGL.js";
9
+ } from "./chunk-244SVJXW.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-A5X2GUSH.js");
384
+ const { TaskOrchestrator } = await import("./task-orchestrator-EGJZA6Y4.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-JPCRXQ2E.js";
26
+ } from "./chunk-ROMSAKP6.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-A3S7PUMM.js";
41
+ } from "./chunk-WIWNSN7U.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-Z5IRVRGL.js";
61
+ } from "./chunk-244SVJXW.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-EUOAU3RZ.js");
1917
+ const { executeTests } = await import("./run-tests-YU52BWHE.js");
1918
1918
  const argStr = args.join(" ").trim();
1919
1919
  let testArgs = {};
1920
1920
  if (argStr) {
@@ -5528,7 +5528,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5528
5528
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5529
5529
  process.exit(1);
5530
5530
  }
5531
- const { startWebServer } = await import("./server-PWWT4P52.js");
5531
+ const { startWebServer } = await import("./server-KVHJCPUO.js");
5532
5532
  await startWebServer({ port, host: options.host });
5533
5533
  });
5534
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) => {
@@ -5761,7 +5761,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5761
5761
  }),
5762
5762
  config.get("customProviders")
5763
5763
  );
5764
- const { startHub } = await import("./hub-J5ZOVM4Q.js");
5764
+ const { startHub } = await import("./hub-QZXQ6JYS.js");
5765
5765
  await startHub(
5766
5766
  {
5767
5767
  topic: topic ?? "",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-3HJSWTKO.js";
4
+ } from "./chunk-QW2UGW6D.js";
5
5
  export {
6
6
  executeTests,
7
7
  runTestsTool
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-Z5IRVRGL.js";
5
+ } from "./chunk-244SVJXW.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -18,7 +18,7 @@ import {
18
18
  renderDiff,
19
19
  runHook,
20
20
  setupProxy
21
- } from "./chunk-JPCRXQ2E.js";
21
+ } from "./chunk-ROMSAKP6.js";
22
22
  import {
23
23
  AuthManager
24
24
  } from "./chunk-BYNY5JPB.js";
@@ -32,19 +32,24 @@ import {
32
32
  spawnAgentContext,
33
33
  truncateOutput,
34
34
  undoStack
35
- } from "./chunk-A3S7PUMM.js";
35
+ } from "./chunk-WIWNSN7U.js";
36
36
  import {
37
37
  AGENTIC_BEHAVIOR_GUIDELINE,
38
+ AUTHOR,
39
+ AUTHOR_EMAIL,
38
40
  CONTEXT_FILE_CANDIDATES,
41
+ CUSTOM_COMMANDS_DIR_NAME,
39
42
  DEFAULT_MAX_TOKENS,
43
+ DESCRIPTION,
40
44
  MCP_PROJECT_CONFIG_NAME,
41
45
  MEMORY_FILE_NAME,
42
46
  MEMORY_MAX_CHARS,
43
47
  PLAN_MODE_READONLY_TOOLS,
44
48
  PLAN_MODE_SYSTEM_ADDON,
49
+ PLUGINS_DIR_NAME,
45
50
  SKILLS_DIR_NAME,
46
51
  VERSION
47
- } from "./chunk-Z5IRVRGL.js";
52
+ } from "./chunk-244SVJXW.js";
48
53
 
49
54
  // src/web/server.ts
50
55
  import express from "express";
@@ -57,7 +62,7 @@ import { networkInterfaces } from "os";
57
62
  // src/web/tool-executor-web.ts
58
63
  import { randomUUID } from "crypto";
59
64
  import { existsSync, readFileSync } from "fs";
60
- var ToolExecutorWeb = class {
65
+ var ToolExecutorWeb = class _ToolExecutorWeb {
61
66
  constructor(registry, ws) {
62
67
  this.registry = registry;
63
68
  this.ws = ws;
@@ -71,6 +76,10 @@ var ToolExecutorWeb = class {
71
76
  pendingConfirms = /* @__PURE__ */ new Map();
72
77
  /** Pending batch confirm promises */
73
78
  pendingBatchConfirms = /* @__PURE__ */ new Map();
79
+ /** M7 fix: timers for confirm cleanup if client crashes */
80
+ pendingTimers = /* @__PURE__ */ new Map();
81
+ static CONFIRM_TIMEOUT_MS = 5 * 60 * 1e3;
82
+ // 5 minutes
74
83
  /** Publicly readable by SessionHandler to check if confirm is active */
75
84
  confirming = false;
76
85
  /** Session-level auto-approve toggle (/yolo command) */
@@ -86,10 +95,19 @@ var ToolExecutorWeb = class {
86
95
  if (opts.permissionRules) this.permissionRules = opts.permissionRules;
87
96
  if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
88
97
  }
98
+ /** Clear M7 timeout timer for a requestId */
99
+ clearPendingTimer(requestId) {
100
+ const timer = this.pendingTimers.get(requestId);
101
+ if (timer) {
102
+ clearTimeout(timer);
103
+ this.pendingTimers.delete(requestId);
104
+ }
105
+ }
89
106
  /** Resolve a pending confirm from client response */
90
107
  resolveConfirm(requestId, approved) {
91
108
  const resolve3 = this.pendingConfirms.get(requestId);
92
109
  if (resolve3) {
110
+ this.clearPendingTimer(requestId);
93
111
  this.pendingConfirms.delete(requestId);
94
112
  this.confirming = false;
95
113
  resolve3(approved);
@@ -99,6 +117,7 @@ var ToolExecutorWeb = class {
99
117
  resolveBatchConfirm(requestId, decision) {
100
118
  const resolve3 = this.pendingBatchConfirms.get(requestId);
101
119
  if (resolve3) {
120
+ this.clearPendingTimer(requestId);
102
121
  this.pendingBatchConfirms.delete(requestId);
103
122
  this.confirming = false;
104
123
  if (decision === "all" || decision === "none") {
@@ -190,6 +209,11 @@ var ToolExecutorWeb = class {
190
209
  this.send(msg);
191
210
  return new Promise((resolve3) => {
192
211
  this.pendingConfirms.set(requestId, resolve3);
212
+ this.pendingTimers.set(requestId, setTimeout(() => {
213
+ if (this.pendingConfirms.has(requestId)) {
214
+ this.resolveConfirm(requestId, false);
215
+ }
216
+ }, _ToolExecutorWeb.CONFIRM_TIMEOUT_MS));
193
217
  });
194
218
  }
195
219
  /** WebSocket-based batch confirm */
@@ -210,6 +234,11 @@ var ToolExecutorWeb = class {
210
234
  this.send(msg);
211
235
  return new Promise((resolve3) => {
212
236
  this.pendingBatchConfirms.set(requestId, resolve3);
237
+ this.pendingTimers.set(requestId, setTimeout(() => {
238
+ if (this.pendingBatchConfirms.has(requestId)) {
239
+ this.resolveBatchConfirm(requestId, "none");
240
+ }
241
+ }, _ToolExecutorWeb.CONFIRM_TIMEOUT_MS));
213
242
  });
214
243
  }
215
244
  async execute(call) {
@@ -451,6 +480,8 @@ var SessionHandler = class _SessionHandler {
451
480
  pendingAskUser = /* @__PURE__ */ new Map();
452
481
  /** Active system prompt from context files */
453
482
  activeSystemPrompt;
483
+ /** Directories added via /add-dir */
484
+ addedDirs = /* @__PURE__ */ new Set();
454
485
  constructor(ws, shared) {
455
486
  this.ws = ws;
456
487
  this.config = shared.config;
@@ -1054,27 +1085,25 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1054
1085
  message: [
1055
1086
  "\u{1F4D6} Available Web UI commands:",
1056
1087
  "",
1088
+ " /help \u2014 Show this help message",
1089
+ " /about \u2014 Version & author info",
1057
1090
  " /provider <id> \u2014 Switch AI provider",
1058
1091
  " /model <id> \u2014 Switch model",
1059
1092
  " /clear \u2014 Clear conversation & start new session",
1060
1093
  " /compact [hint] \u2014 Compress conversation history",
1061
1094
  " /think [on|off] \u2014 Toggle extended thinking mode",
1062
1095
  " /plan [enter|exit] \u2014 Toggle read-only planning mode",
1063
- " /session new \u2014 Create a new session",
1064
- " /session list \u2014 List saved sessions",
1065
- " /session load <id> \u2014 Resume a saved session",
1066
- " /session delete <id> \u2014 Delete a session",
1096
+ " /session new|list|load|delete <id> \u2014 Session management",
1097
+ " /system [prompt|clear] \u2014 Set/view/reset system prompt",
1098
+ " /context [reload] \u2014 Show/reload context layers",
1067
1099
  " /status \u2014 Show session info & token usage",
1068
1100
  " /cost \u2014 Show cumulative token usage",
1069
- " /tools \u2014 Show tools, MCP servers & skills in sidebar",
1070
- " /export [md|json] \u2014 Export conversation as Markdown or JSON",
1071
- " /skill \u2014 List available skills",
1072
- " /skill <name> \u2014 Activate a skill",
1073
- " /skill off \u2014 Deactivate current skill",
1074
- " /memory \u2014 Show persistent memory contents",
1075
- " /memory add <text> \u2014 Add entry to persistent memory",
1076
- " /memory clear \u2014 Clear persistent memory",
1077
- " /yolo [on|off] \u2014 Toggle session auto-approve (skip confirmations)",
1101
+ " /config [show|get|set] \u2014 View/modify configuration",
1102
+ " /tools \u2014 Show tools, MCP servers & skills",
1103
+ " /export [md|json] \u2014 Export conversation",
1104
+ " /skill [name|off|list|reload] \u2014 Manage agent skills",
1105
+ " /memory [show|add|clear] \u2014 Persistent memory management",
1106
+ " /yolo [on|off] \u2014 Toggle auto-approve (skip confirmations)",
1078
1107
  " /search <keyword> \u2014 Search across all session histories",
1079
1108
  " /undo [list|<n>] \u2014 Undo file operations",
1080
1109
  " /diff [--stats] \u2014 Show file modifications in this session",
@@ -1083,8 +1112,13 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1083
1112
  " /review [--staged] \u2014 AI code review from git diff",
1084
1113
  " /test [command] \u2014 Run project tests",
1085
1114
  " /init [--force] \u2014 Generate AICLI.md by scanning project",
1115
+ " /scaffold <desc> \u2014 Generate project scaffolding with AI",
1116
+ " /add-dir [path|remove] \u2014 Add/remove directory from AI context",
1117
+ " /mcp [reconnect] \u2014 Show/manage MCP servers",
1118
+ " /plugins \u2014 Show loaded plugins",
1119
+ " /commands \u2014 List custom commands",
1086
1120
  " /doctor \u2014 Health check (API keys, config, MCP)",
1087
- " /help \u2014 Show this help message",
1121
+ " /bug \u2014 Generate bug report template",
1088
1122
  "",
1089
1123
  "\u{1F4A1} Tips:",
1090
1124
  " \u2022 Change provider/model with the dropdowns above",
@@ -1448,7 +1482,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1448
1482
  case "test": {
1449
1483
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1450
1484
  try {
1451
- const { executeTests } = await import("./run-tests-EUOAU3RZ.js");
1485
+ const { executeTests } = await import("./run-tests-YU52BWHE.js");
1452
1486
  const argStr = args.join(" ").trim();
1453
1487
  let testArgs = {};
1454
1488
  if (argStr) {
@@ -1536,6 +1570,264 @@ Use /context reload to load it.` });
1536
1570
  this.send({ type: "info", message: lines.join("\n") });
1537
1571
  break;
1538
1572
  }
1573
+ // ── /about ──────────────────────────────────────────────────────
1574
+ case "about": {
1575
+ const providerList = this.providers.listAll();
1576
+ const toolCount = this.toolRegistry.getDefinitions().length;
1577
+ const mcpTools = this.mcpManager?.getTotalToolCount() ?? 0;
1578
+ const lines = [
1579
+ `\u{1F916} **ai-cli** v${VERSION}`,
1580
+ "",
1581
+ `${DESCRIPTION}`,
1582
+ `Author: ${AUTHOR} <${AUTHOR_EMAIL}>`,
1583
+ "",
1584
+ `**Providers:** ${providerList.length} (${providerList.map((p) => p.id).join(", ")})`,
1585
+ `**Tools:** ${toolCount + mcpTools} (${toolCount} built-in${mcpTools > 0 ? ` + ${mcpTools} MCP` : ""})`,
1586
+ `**REPL Commands:** 37`,
1587
+ "",
1588
+ `GitHub: https://github.com/jinzhengdong/ai-cli`
1589
+ ];
1590
+ this.send({ type: "info", message: lines.join("\n") });
1591
+ break;
1592
+ }
1593
+ // ── /system ─────────────────────────────────────────────────────
1594
+ case "system": {
1595
+ const text = args.join(" ").trim();
1596
+ if (!text) {
1597
+ const current = this.activeSystemPrompt;
1598
+ if (current) {
1599
+ const preview = current.length > 500 ? current.slice(0, 500) + "..." : current;
1600
+ this.send({ type: "info", message: `\u{1F4CB} Current system prompt (${current.length} chars):
1601
+
1602
+ ${preview}` });
1603
+ } else {
1604
+ this.send({ type: "info", message: "No custom system prompt set. Using default.\nUsage: /system <prompt>" });
1605
+ }
1606
+ break;
1607
+ }
1608
+ if (text === "clear" || text === "reset") {
1609
+ this.activeSystemPrompt = this.loadContextFiles();
1610
+ this.send({ type: "info", message: "\u2713 System prompt reset to default (context files)." });
1611
+ } else {
1612
+ this.activeSystemPrompt = text;
1613
+ this.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
1614
+ }
1615
+ break;
1616
+ }
1617
+ // ── /config ─────────────────────────────────────────────────────
1618
+ case "config": {
1619
+ const sub = args[0];
1620
+ if (sub === "show" || !sub) {
1621
+ this.send({ type: "info", message: `\u2699\uFE0F Configuration:
1622
+ \`\`\`json
1623
+ ${this.config.toFormattedJSON()}
1624
+ \`\`\`` });
1625
+ } else if (sub === "get" && args[1]) {
1626
+ const val = this.config.getByPath(args[1]);
1627
+ const display = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val ?? "undefined");
1628
+ this.send({ type: "info", message: `${args[1]} = ${display}` });
1629
+ } else if (sub === "set" && args[1] && args[2] !== void 0) {
1630
+ try {
1631
+ this.config.setByPath(args[1], args.slice(2).join(" "));
1632
+ this.send({ type: "info", message: `\u2713 ${args[1]} = ${args.slice(2).join(" ")}` });
1633
+ } catch (err) {
1634
+ this.send({ type: "error", message: `Config error: ${err.message}` });
1635
+ }
1636
+ } else {
1637
+ this.send({ type: "info", message: "Usage: /config [show] | /config get <key> | /config set <key> <value>" });
1638
+ }
1639
+ break;
1640
+ }
1641
+ // ── /context ────────────────────────────────────────────────────
1642
+ case "context": {
1643
+ const sub = args[0];
1644
+ if (sub === "reload") {
1645
+ this.activeSystemPrompt = this.loadContextFiles();
1646
+ this.send({ type: "info", message: this.activeSystemPrompt ? `\u2713 Context reloaded (${this.activeSystemPrompt.length} chars).` : "\u2713 No context files found." });
1647
+ break;
1648
+ }
1649
+ const configDir = this.config.getConfigDir();
1650
+ const cwd = process.cwd();
1651
+ const gitRoot = getGitRoot(cwd);
1652
+ const projectRoot = gitRoot ?? cwd;
1653
+ const layers = ["\u{1F4DA} **Context Layers:**", ""];
1654
+ const checkLayer = (label, dir) => {
1655
+ for (const name2 of CONTEXT_FILE_CANDIDATES) {
1656
+ const fullPath = join2(dir, name2);
1657
+ if (existsSync3(fullPath)) {
1658
+ try {
1659
+ const size = statSync(fullPath).size;
1660
+ layers.push(` \u2713 ${label}: ${fullPath} (${size} bytes)`);
1661
+ return;
1662
+ } catch {
1663
+ }
1664
+ }
1665
+ }
1666
+ layers.push(` \u2013 ${label}: (none)`);
1667
+ };
1668
+ checkLayer("Global", configDir);
1669
+ checkLayer("Project", projectRoot);
1670
+ if (resolve(cwd) !== resolve(projectRoot)) {
1671
+ checkLayer("Subdir", cwd);
1672
+ }
1673
+ layers.push("");
1674
+ layers.push(`Total prompt length: ${this.activeSystemPrompt?.length ?? 0} chars`);
1675
+ layers.push("Use /context reload to refresh.");
1676
+ this.send({ type: "info", message: layers.join("\n") });
1677
+ break;
1678
+ }
1679
+ // ── /mcp ────────────────────────────────────────────────────────
1680
+ case "mcp": {
1681
+ if (!this.mcpManager) {
1682
+ this.send({ type: "info", message: "MCP not configured. Add mcpServers to config.json." });
1683
+ break;
1684
+ }
1685
+ const sub = args[0];
1686
+ if (sub === "reconnect") {
1687
+ const targetId = args[1];
1688
+ this.send({ type: "info", message: "\u{1F504} Reconnecting MCP servers..." });
1689
+ try {
1690
+ await this.mcpManager.reconnectAll();
1691
+ this.send({ type: "info", message: "\u2713 MCP reconnect complete." });
1692
+ } catch (err) {
1693
+ this.send({ type: "error", message: `Reconnect failed: ${err.message}` });
1694
+ }
1695
+ this.sendToolsList();
1696
+ break;
1697
+ }
1698
+ const statuses = this.mcpManager.getStatus();
1699
+ if (statuses.length === 0) {
1700
+ this.send({ type: "info", message: "No MCP servers configured." });
1701
+ break;
1702
+ }
1703
+ const lines = [`\u{1F50C} **MCP Servers (${statuses.length}):**`, ""];
1704
+ for (const s of statuses) {
1705
+ const state = s.connected ? `\u2713 connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `\u2717 disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
1706
+ lines.push(` ${s.serverId}: ${state}`);
1707
+ }
1708
+ lines.push("");
1709
+ lines.push("Use /mcp reconnect to reconnect.");
1710
+ this.send({ type: "info", message: lines.join("\n") });
1711
+ break;
1712
+ }
1713
+ // ── /scaffold ───────────────────────────────────────────────────
1714
+ case "scaffold": {
1715
+ const description = args.join(" ").trim();
1716
+ if (!description) {
1717
+ this.send({ type: "error", message: "Usage: /scaffold <project description>" });
1718
+ break;
1719
+ }
1720
+ this.send({ type: "info", message: "\u{1F3D7}\uFE0F Generating project scaffold..." });
1721
+ const scaffoldPrompt = `Please scaffold a project based on this description: ${description}
1722
+
1723
+ Create the necessary files and directory structure. Use write_file and bash tools to set up the project.`;
1724
+ await this.handleChat(scaffoldPrompt);
1725
+ break;
1726
+ }
1727
+ // ── /add-dir ────────────────────────────────────────────────────
1728
+ case "add-dir": {
1729
+ const sub = args[0]?.trim();
1730
+ if (!sub) {
1731
+ this.send({ type: "info", message: this.addedDirs.size > 0 ? `\u{1F4C1} Added directories:
1732
+ ${[...this.addedDirs].map((d) => ` \u2022 ${d}`).join("\n")}
1733
+
1734
+ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path> | /add-dir remove" });
1735
+ break;
1736
+ }
1737
+ if (sub === "remove" || sub === "clear") {
1738
+ this.addedDirs.clear();
1739
+ this.send({ type: "info", message: "\u2713 All added directories removed from context." });
1740
+ break;
1741
+ }
1742
+ const dirPath = resolve(sub);
1743
+ if (!existsSync3(dirPath)) {
1744
+ this.send({ type: "error", message: `Directory not found: ${dirPath}` });
1745
+ break;
1746
+ }
1747
+ if (!statSync(dirPath).isDirectory()) {
1748
+ this.send({ type: "error", message: `Not a directory: ${dirPath}` });
1749
+ break;
1750
+ }
1751
+ this.addedDirs.add(dirPath);
1752
+ this.send({ type: "info", message: `\u2713 Added directory: ${dirPath}
1753
+ It will be included in AI context for subsequent messages.` });
1754
+ break;
1755
+ }
1756
+ // ── /commands ───────────────────────────────────────────────────
1757
+ case "commands": {
1758
+ const configDir = this.config.getConfigDir();
1759
+ const commandsDir = join2(configDir, CUSTOM_COMMANDS_DIR_NAME);
1760
+ if (!existsSync3(commandsDir)) {
1761
+ this.send({ type: "info", message: `No custom commands directory.
1762
+ Create: ${commandsDir}/ with .md files.` });
1763
+ break;
1764
+ }
1765
+ try {
1766
+ const files = readdirSync(commandsDir).filter((f) => f.endsWith(".md"));
1767
+ if (files.length === 0) {
1768
+ this.send({ type: "info", message: `No custom commands found in ${commandsDir}
1769
+ Add .md files to create commands.` });
1770
+ } else {
1771
+ const lines = [`\u{1F4CB} **Custom Commands (${files.length}):**`, `Dir: ${commandsDir}`, ""];
1772
+ for (const f of files) {
1773
+ const name2 = f.replace(/\.md$/, "");
1774
+ lines.push(` /${name2}`);
1775
+ }
1776
+ this.send({ type: "info", message: lines.join("\n") });
1777
+ }
1778
+ } catch {
1779
+ this.send({ type: "info", message: `Cannot read: ${commandsDir}` });
1780
+ }
1781
+ break;
1782
+ }
1783
+ // ── /plugins ────────────────────────────────────────────────────
1784
+ case "plugins": {
1785
+ const configDir = this.config.getConfigDir();
1786
+ const pluginsDir = join2(configDir, PLUGINS_DIR_NAME);
1787
+ const pluginTools = this.toolRegistry.listPluginTools();
1788
+ const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
1789
+ if (pluginTools.length === 0) {
1790
+ lines.push("No plugins loaded.");
1791
+ lines.push("Add .js files to the plugins directory and set allowPlugins:true in config.");
1792
+ } else {
1793
+ for (const t of pluginTools) {
1794
+ lines.push(` \u2022 ${t.name} \u2014 ${t.description}`);
1795
+ }
1796
+ }
1797
+ this.send({ type: "info", message: lines.join("\n") });
1798
+ break;
1799
+ }
1800
+ // ── /bug ────────────────────────────────────────────────────────
1801
+ case "bug": {
1802
+ const session = this.sessions.current;
1803
+ const lines = [
1804
+ "\u{1F41B} **Bug Report Template**",
1805
+ "",
1806
+ "**Environment:**",
1807
+ ` ai-cli version: ${VERSION}`,
1808
+ ` Node.js: ${process.version}`,
1809
+ ` OS: ${process.platform} ${process.arch}`,
1810
+ ` Provider: ${this.currentProvider}`,
1811
+ ` Model: ${this.currentModel}`,
1812
+ ` Session: ${session?.id.slice(0, 8) ?? "none"} (${session?.messages.length ?? 0} messages)`,
1813
+ "",
1814
+ "**Description:**",
1815
+ "(Describe the bug)",
1816
+ "",
1817
+ "**Steps to reproduce:**",
1818
+ "1. ...",
1819
+ "",
1820
+ "**Expected behavior:**",
1821
+ "...",
1822
+ "",
1823
+ "**Actual behavior:**",
1824
+ "...",
1825
+ "",
1826
+ "Report at: https://github.com/jinzhengdong/ai-cli/issues"
1827
+ ];
1828
+ this.send({ type: "info", message: lines.join("\n") });
1829
+ break;
1830
+ }
1539
1831
  default:
1540
1832
  this.send({ type: "error", message: `Unknown command: /${name}. Type /help for available commands.` });
1541
1833
  }
@@ -1735,12 +2027,27 @@ Use /context reload to load it.` });
1735
2027
  buildSystemPrompt() {
1736
2028
  const skillContent = this.skillManager?.getActivePromptContent();
1737
2029
  const activeSkill = skillContent && this.skillManager?.getActive() ? { name: this.skillManager.getActive().meta.name, content: skillContent } : void 0;
1738
- return buildSystemPrompt({
2030
+ let prompt = buildSystemPrompt({
1739
2031
  activeSystemPrompt: this.activeSystemPrompt,
1740
2032
  activeSkill,
1741
2033
  planMode: this.planMode,
1742
2034
  configDir: this.config.getConfigDir()
1743
2035
  });
2036
+ if (this.addedDirs.size > 0) {
2037
+ const MAX_DIR_CONTEXT = 4e4;
2038
+ let dirContext = "\n\n--- Added Directory Context ---\n";
2039
+ let totalLen = 0;
2040
+ for (const dir of this.addedDirs) {
2041
+ if (totalLen > MAX_DIR_CONTEXT) break;
2042
+ dirContext += `
2043
+ [Directory: ${dir}]
2044
+ `;
2045
+ dirContext += this.scanDirTree(dir, 2, 40) + "\n";
2046
+ totalLen = dirContext.length;
2047
+ }
2048
+ prompt += dirContext;
2049
+ }
2050
+ return prompt;
1744
2051
  }
1745
2052
  getModelParams() {
1746
2053
  const allParams = this.config.get("modelParams");
@@ -4,14 +4,14 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-A3S7PUMM.js";
7
+ } from "./chunk-WIWNSN7U.js";
8
8
  import {
9
9
  SUBAGENT_ALLOWED_TOOLS
10
- } from "./chunk-Z5IRVRGL.js";
10
+ } from "./chunk-244SVJXW.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
 
@@ -266,7 +266,9 @@ var TaskOrchestrator = class {
266
266
  }
267
267
  saveState(plan) {
268
268
  try {
269
- 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);
270
272
  } catch {
271
273
  }
272
274
  }
@@ -357,13 +359,29 @@ Example:
357
359
  if (!Array.isArray(parsed) || parsed.length === 0) {
358
360
  throw new Error("Empty task list");
359
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
+ }
360
378
  const roleMap = new Map(roles.map((r) => [r.id, r.name]));
361
379
  const tasks = parsed.map((t, i) => ({
362
- id: t.id ?? i + 1,
380
+ id: typeof t.id === "number" ? t.id : i + 1,
363
381
  description: t.description,
364
382
  assignee: t.assignee,
365
383
  assigneeName: roleMap.get(t.assignee) ?? t.assignee,
366
- dependencies: t.dependencies ?? [],
384
+ dependencies: Array.isArray(t.dependencies) ? t.dependencies : [],
367
385
  status: "pending"
368
386
  }));
369
387
  return { goal, tasks };
@@ -428,7 +446,15 @@ Example:
428
446
  if (!nextTask) {
429
447
  const pending = plan.tasks.filter((t) => t.status === "pending");
430
448
  if (pending.length === 0) break;
431
- 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
+ }
432
458
  for (const t of pending) {
433
459
  const unmet = t.dependencies.filter((d) => !completedIds.has(d));
434
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.12",
3
+ "version": "0.4.14",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",