shenxiang-ai-cli 0.4.3 → 0.5.0

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.
Files changed (2) hide show
  1. package/dist/index.js +152 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ import { Command } from "commander";
5
5
 
6
6
  // src/commands/chat.ts
7
7
  import inquirer from "inquirer";
8
+ import fs5 from "fs/promises";
9
+ import path5 from "path";
10
+ import { glob as glob4 } from "glob";
8
11
 
9
12
  // src/core/providers/openai-compatible.ts
10
13
  import OpenAI from "openai";
@@ -737,7 +740,7 @@ function banner() {
737
740
  const w = chalk.white.bold;
738
741
  const lines = [
739
742
  ``,
740
- ` ${p("/\\_/\\")} ${b("\u6C88\u7FD4\u7684AI\u52A9\u624B")} ${m("v0.4.3")}`,
743
+ ` ${p("/\\_/\\")} ${b("\u6C88\u7FD4\u7684AI\u52A9\u624B")} ${m("v0.5.0")}`,
741
744
  ` ${p("(")} ${y("o")}${p(".")}${y("o")} ${p(")")} ${d("\u7EC8\u7AEF\u91CC\u7684AI\u5168\u6808\u5F00\u53D1\u642D\u6863")}`,
742
745
  ` ${p("> ")}${r("^")}${p(" <")}`,
743
746
  ` ${p("/|")}${p(" |")}${p("\\")} ${d("\u4F60\u7684\u7F16\u7A0B\u597D\u4F19\u4F34")} ${chalk.hex("#A78BFA")("\u{1F43E}")}`,
@@ -754,9 +757,6 @@ var marked = new Marked(markedTerminal());
754
757
  function streamChunk(text) {
755
758
  process.stdout.write(text);
756
759
  }
757
- function streamThinking(text) {
758
- process.stdout.write(theme.dim(text));
759
- }
760
760
  function printColoredDiff(oldContent, newContent, filepath) {
761
761
  const oldLines = oldContent.split("\n");
762
762
  const newLines = newContent.split("\n");
@@ -1875,10 +1875,11 @@ var Agent = class {
1875
1875
  let hasThinkingOutput = false;
1876
1876
  let toolSpinnerStarted = false;
1877
1877
  let idleTimer = null;
1878
+ let isStreamingThinking = false;
1878
1879
  const resetIdleTimer = () => {
1879
1880
  if (idleTimer) clearTimeout(idleTimer);
1880
1881
  idleTimer = setTimeout(() => {
1881
- if (!spinner.isSpinning && spinnerStopped) {
1882
+ if (!spinner.isSpinning && spinnerStopped && !isStreamingThinking) {
1882
1883
  spinner.text = "AI \u6B63\u5728\u601D\u8003...";
1883
1884
  spinner.start();
1884
1885
  }
@@ -1912,21 +1913,30 @@ var Agent = class {
1912
1913
  }
1913
1914
  switch (chunk.type) {
1914
1915
  case "thinking":
1916
+ isStreamingThinking = true;
1915
1917
  if (!hasThinkingOutput) {
1916
1918
  hasThinkingOutput = true;
1917
- streamThinking("\u{1F4AD} ");
1919
+ process.stdout.write("\n");
1920
+ process.stdout.write(theme.dim(" \u250C\u2500 \u{1F4AD} \u601D\u8003\u4E2D \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
1921
+ process.stdout.write(theme.dim(" \u2502 "));
1918
1922
  }
1919
- streamThinking(chunk.content || "");
1923
+ const thinkText = (chunk.content || "").replace(/\n/g, "\n" + theme.dim(" \u2502 "));
1924
+ process.stdout.write(theme.dim(thinkText));
1920
1925
  break;
1921
1926
  case "text":
1922
1927
  if (hasThinkingOutput && !hasTextOutput) {
1923
- process.stdout.write("\n\n");
1928
+ isStreamingThinking = false;
1929
+ process.stdout.write("\n" + theme.dim(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500") + "\n\n");
1924
1930
  }
1925
1931
  responseText += chunk.content || "";
1926
1932
  hasTextOutput = true;
1927
1933
  streamChunk(chunk.content || "");
1928
1934
  break;
1929
1935
  case "tool_call_start":
1936
+ if (hasThinkingOutput && isStreamingThinking) {
1937
+ isStreamingThinking = false;
1938
+ process.stdout.write("\n" + theme.dim(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500") + "\n");
1939
+ }
1930
1940
  if (!toolSpinnerStarted) {
1931
1941
  if (hasTextOutput) process.stdout.write("\n");
1932
1942
  spinner.text = "\u6B63\u5728\u51C6\u5907\u64CD\u4F5C...";
@@ -2064,6 +2074,30 @@ var Agent = class {
2064
2074
  printWarning(t("errors.tokenLimit"));
2065
2075
  }
2066
2076
  }
2077
+ /**
2078
+ * Add file content to conversation context (for /read command)
2079
+ */
2080
+ addFileContext(filePath, content) {
2081
+ const maxChars = 5e4;
2082
+ const truncated = content.length > maxChars ? content.substring(0, maxChars) + `
2083
+
2084
+ ... [\u6587\u4EF6\u88AB\u622A\u65AD\uFF0C\u5171 ${content.length} \u5B57\u7B26\uFF0C\u53EA\u663E\u793A\u524D ${maxChars} \u5B57\u7B26]` : content;
2085
+ this.messages.push({
2086
+ role: "user",
2087
+ content: `[\u7528\u6237\u52A0\u8F7D\u4E86\u6587\u4EF6: ${filePath}]
2088
+
2089
+ \u4EE5\u4E0B\u662F\u6587\u4EF6\u5185\u5BB9\uFF0C\u8BF7\u8BB0\u4F4F\u5B83\uFF0C\u540E\u7EED\u6211\u4F1A\u57FA\u4E8E\u8FD9\u4E2A\u6587\u4EF6\u5185\u5BB9\u7ED9\u4F60\u6307\u4EE4\uFF1A
2090
+
2091
+ ---
2092
+ ${truncated}
2093
+ ---`
2094
+ });
2095
+ this.messages.push({
2096
+ role: "assistant",
2097
+ content: `\u597D\u7684\uFF0C\u6211\u5DF2\u7ECF\u8BFB\u53D6\u4E86 ${filePath} \u7684\u5185\u5BB9\u3002\u8BF7\u544A\u8BC9\u6211\u4F60\u60F3\u57FA\u4E8E\u8FD9\u4E2A\u6587\u4EF6\u505A\u4EC0\u4E48\u3002`
2098
+ });
2099
+ this.trimMessages();
2100
+ }
2067
2101
  /**
2068
2102
  * Clear conversation history (keep system prompt)
2069
2103
  */
@@ -2233,6 +2267,25 @@ async function chatCommand(options = {}) {
2233
2267
  if (projectCtx?.projectMemory) {
2234
2268
  console.log(theme.dim(` \u{1F4CB} \u5DF2\u52A0\u8F7D SXAI.md \u9879\u76EE\u6307\u4EE4`));
2235
2269
  }
2270
+ try {
2271
+ const docHintPatterns = ["*.md", "\u9700\u6C42*.md", "PRD*", "prd*", "\u8BBE\u8BA1*.md", "docs/*.md"];
2272
+ const foundDocs = [];
2273
+ for (const p of docHintPatterns) {
2274
+ const files = await glob4(p, {
2275
+ cwd: process.cwd(),
2276
+ ignore: ["node_modules/**", ".git/**", "dist/**", "SXAI.md"],
2277
+ nodir: true
2278
+ });
2279
+ for (const f of files) {
2280
+ if (!foundDocs.includes(f) && foundDocs.length < 5) foundDocs.push(f);
2281
+ }
2282
+ }
2283
+ if (foundDocs.length > 0) {
2284
+ console.log(theme.dim(` \u{1F4C4} \u68C0\u6D4B\u5230\u6587\u6863: ${foundDocs.join(", ")}`));
2285
+ console.log(theme.dim(` \u8F93\u5165 /read <\u6587\u4EF6\u540D> \u52A0\u8F7D\u5230\u5BF9\u8BDD\uFF0C\u6216 /docs \u67E5\u770B\u5168\u90E8`));
2286
+ }
2287
+ } catch {
2288
+ }
2236
2289
  printSeparator();
2237
2290
  console.log();
2238
2291
  } catch (err) {
@@ -2351,15 +2404,18 @@ async function handleSlashCommand(input, agent) {
2351
2404
  case "/h":
2352
2405
  console.log(`
2353
2406
  \u53EF\u7528\u547D\u4EE4:
2354
- /help \u663E\u793A\u5E2E\u52A9
2355
- /model \u67E5\u770B/\u5207\u6362\u6A21\u578B
2356
- /auto \u5207\u6362\u81EA\u52A8\u6A21\u5F0F\uFF08\u8DF3\u8FC7\u786E\u8BA4\uFF09
2357
- /clear \u6E05\u9664\u5BF9\u8BDD\u5386\u53F2
2358
- /account \u67E5\u770B\u8D26\u6237\u4FE1\u606F
2359
- /status \u67E5\u770B\u9879\u76EE\u4FE1\u606F
2360
- /config \u67E5\u770B\u914D\u7F6E
2361
- /logout \u9000\u51FA\u767B\u5F55
2362
- /exit \u9000\u51FA\u7A0B\u5E8F
2407
+ /read <\u8DEF\u5F84> \u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9\u5582\u7ED9AI\uFF08\u5982 /read \u9700\u6C42\u6587\u6863.md\uFF09
2408
+ /docs \u626B\u63CF\u9879\u76EE\u4E2D\u7684\u6587\u6863\u6587\u4EF6
2409
+ /model \u67E5\u770B/\u5207\u6362\u6A21\u578B
2410
+ /auto \u5207\u6362\u81EA\u52A8\u6A21\u5F0F\uFF08\u8DF3\u8FC7\u786E\u8BA4\uFF09
2411
+ /clear \u6E05\u9664\u5BF9\u8BDD\u5386\u53F2
2412
+ /account \u67E5\u770B\u8D26\u6237\u4FE1\u606F
2413
+ /status \u67E5\u770B\u9879\u76EE\u4FE1\u606F
2414
+ /config \u67E5\u770B\u914D\u7F6E
2415
+ /logout \u9000\u51FA\u767B\u5F55
2416
+ /exit \u9000\u51FA\u7A0B\u5E8F
2417
+
2418
+ \u63D0\u793A: \u53EF\u4EE5\u76F4\u63A5\u8BF4"\u8BF7\u8BFB\u53D6 xxx \u6587\u4EF6"\u8BA9AI\u81EA\u5DF1\u53BB\u8BFB
2363
2419
  `);
2364
2420
  break;
2365
2421
  case "/auto":
@@ -2373,6 +2429,82 @@ async function handleSlashCommand(input, agent) {
2373
2429
  }
2374
2430
  break;
2375
2431
  }
2432
+ case "/read": {
2433
+ const filePath = args.join(" ").trim();
2434
+ if (!filePath) {
2435
+ printError("\u7528\u6CD5: /read <\u6587\u4EF6\u8DEF\u5F84> \u4F8B\u5982: /read \u9700\u6C42\u6587\u6863.md");
2436
+ break;
2437
+ }
2438
+ try {
2439
+ const absPath = path5.resolve(process.cwd(), filePath);
2440
+ const content = await fs5.readFile(absPath, "utf-8");
2441
+ const lines = content.split("\n").length;
2442
+ const sizeKB = (Buffer.byteLength(content, "utf-8") / 1024).toFixed(1);
2443
+ printSuccess(`\u5DF2\u8BFB\u53D6: ${filePath} (${lines} \u884C, ${sizeKB}KB)`);
2444
+ agent.addFileContext(filePath, content);
2445
+ console.log(theme.dim(` \u6587\u4EF6\u5185\u5BB9\u5DF2\u6DFB\u52A0\u5230\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u73B0\u5728\u53EF\u4EE5\u57FA\u4E8E\u5B83\u63D0\u95EE\u6216\u6307\u4EE4\u3002`));
2446
+ console.log(theme.dim(` \u4F8B\u5982: "\u6839\u636E\u8FD9\u4E2A\u9700\u6C42\u6587\u6863\u5F00\u59CB\u5F00\u53D1" \u6216 "\u5206\u6790\u8FD9\u4E2A\u6587\u4EF6\u7684\u95EE\u9898"`));
2447
+ } catch (err) {
2448
+ const msg = err instanceof Error ? err.message : String(err);
2449
+ if (msg.includes("ENOENT")) {
2450
+ printError(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`);
2451
+ } else {
2452
+ printError(`\u8BFB\u53D6\u5931\u8D25: ${msg}`);
2453
+ }
2454
+ }
2455
+ break;
2456
+ }
2457
+ case "/docs": {
2458
+ const docPatterns = [
2459
+ "*.md",
2460
+ "*.txt",
2461
+ "docs/**/*.md",
2462
+ "doc/**/*.md",
2463
+ "\u9700\u6C42*.md",
2464
+ "PRD*",
2465
+ "prd*",
2466
+ "\u8BBE\u8BA1*.md",
2467
+ "DESIGN*",
2468
+ "TODO*",
2469
+ "CHANGELOG*",
2470
+ "ARCHITECTURE*"
2471
+ ];
2472
+ const docFiles = [];
2473
+ for (const pattern of docPatterns) {
2474
+ try {
2475
+ const files = await glob4(pattern, {
2476
+ cwd: process.cwd(),
2477
+ ignore: ["node_modules/**", ".git/**", "dist/**"],
2478
+ nodir: true
2479
+ });
2480
+ for (const f of files) {
2481
+ if (!docFiles.includes(f)) docFiles.push(f);
2482
+ }
2483
+ } catch {
2484
+ }
2485
+ }
2486
+ if (docFiles.length === 0) {
2487
+ printInfo("\u672A\u627E\u5230\u6587\u6863\u6587\u4EF6\u3002");
2488
+ } else {
2489
+ console.log(theme.dim(`
2490
+ \u2500\u2500 \u9879\u76EE\u6587\u6863 \u2500\u2500`));
2491
+ for (const f of docFiles.sort().slice(0, 20)) {
2492
+ try {
2493
+ const stat = await fs5.stat(path5.resolve(process.cwd(), f));
2494
+ const sizeKB = (stat.size / 1024).toFixed(1);
2495
+ console.log(` \u{1F4C4} ${theme.info(f)} ${theme.dim(`(${sizeKB}KB)`)}`);
2496
+ } catch {
2497
+ console.log(` \u{1F4C4} ${theme.info(f)}`);
2498
+ }
2499
+ }
2500
+ if (docFiles.length > 20) {
2501
+ console.log(theme.dim(` ... \u8FD8\u6709 ${docFiles.length - 20} \u4E2A\u6587\u4EF6`));
2502
+ }
2503
+ console.log(theme.dim(`
2504
+ \u4F7F\u7528 /read <\u6587\u4EF6\u540D> \u52A0\u8F7D\u6587\u4EF6\u5230\u5BF9\u8BDD\u4E0A\u4E0B\u6587`));
2505
+ }
2506
+ break;
2507
+ }
2376
2508
  case "/clear":
2377
2509
  agent.clearHistory();
2378
2510
  printSuccess("\u5BF9\u8BDD\u5386\u53F2\u5DF2\u6E05\u9664\u3002");
@@ -2897,8 +3029,8 @@ async function adminCommand() {
2897
3029
  break;
2898
3030
  }
2899
3031
  }
2900
- async function adminFetch(config3, path5, options) {
2901
- const response = await fetch(`${config3.apiBaseUrl}${path5}`, {
3032
+ async function adminFetch(config3, path6, options) {
3033
+ const response = await fetch(`${config3.apiBaseUrl}${path6}`, {
2902
3034
  ...options,
2903
3035
  headers: {
2904
3036
  "Content-Type": "application/json",
@@ -3168,7 +3300,7 @@ function fmtTokens3(n) {
3168
3300
  var program = new Command();
3169
3301
  var config2 = getConfig();
3170
3302
  setLocale(config2.locale);
3171
- program.name("sxai").description(t("description")).version("0.4.3");
3303
+ program.name("sxai").description(t("description")).version("0.5.0");
3172
3304
  program.command("chat", { isDefault: true }).description("\u542F\u52A8\u4EA4\u4E92\u5F0FAI\u5BF9\u8BDD\uFF08\u9ED8\u8BA4\u547D\u4EE4\uFF09").option("-m, --model <model>", "\u6307\u5B9AAI\u6A21\u578B", config2.model).option("-y, --yolo", "\u81EA\u52A8\u6A21\u5F0F\uFF1A\u8DF3\u8FC7\u6240\u6709\u786E\u8BA4\uFF0C\u8BA9AI\u81EA\u4E3B\u6267\u884C").action(async (options) => {
3173
3305
  if (options.model && options.model !== config2.model) {
3174
3306
  setConfig("model", options.model);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shenxiang-ai-cli",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "沈翔的AI助手 - 终端里的AI全栈开发搭档",
5
5
  "type": "module",
6
6
  "bin": {