jinzd-ai-cli 0.1.22 → 0.1.23

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 +835 -181
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -129,12 +129,13 @@ var EnvLoader = class {
129
129
  };
130
130
 
131
131
  // src/core/constants.ts
132
- var VERSION = "0.1.22";
132
+ var VERSION = "0.1.23";
133
133
  var APP_NAME = "ai-cli";
134
134
  var CONFIG_DIR_NAME = ".aicli";
135
135
  var CONFIG_FILE_NAME = "config.json";
136
136
  var HISTORY_DIR_NAME = "history";
137
137
  var PLUGINS_DIR_NAME = "plugins";
138
+ var SKILLS_DIR_NAME = "skills";
138
139
  var CONTEXT_FILE_CANDIDATES = ["AICLI.md", "CLAUDE.md"];
139
140
  var MEMORY_FILE_NAME = "memory.md";
140
141
  var MEMORY_MAX_CHARS = 1e4;
@@ -1563,8 +1564,8 @@ var SessionManager = class {
1563
1564
 
1564
1565
  // src/repl/repl.ts
1565
1566
  import * as readline from "readline";
1566
- import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
1567
- import { join as join10, resolve as resolve4, extname as extname3 } from "path";
1567
+ import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
1568
+ import { join as join12, resolve as resolve4, extname as extname3 } from "path";
1568
1569
  import chalk10 from "chalk";
1569
1570
 
1570
1571
  // src/repl/renderer.ts
@@ -1668,10 +1669,10 @@ var Renderer = class {
1668
1669
  console.log(tool("write_todos", "\u62C6\u89E3\u4EFB\u52A1\u4E3A\u5B50\u4EFB\u52A1\u5217\u8868\uFF0C\u5B9E\u65F6\u663E\u793A\u8FDB\u5EA6"));
1669
1670
  console.log(tool("spawn_agent", "\u59D4\u6D3E\u72EC\u7ACB\u5B50\u4EE3\u7406\u6267\u884C\u7279\u5B9A\u4EFB\u52A1\uFF08\u9694\u79BB\u5BF9\u8BDD + \u81EA\u52A8\u5DE5\u5177\u8C03\u7528\u5FAA\u73AF\uFF09"));
1670
1671
  console.log(HR);
1671
- console.log(chalk.gray(" REPL \u547D\u4EE4\uFF0819\u4E2A\uFF09\uFF1A"));
1672
+ console.log(chalk.gray(" REPL \u547D\u4EE4\uFF0823\u4E2A\uFF09\uFF1A"));
1672
1673
  console.log(chalk.dim(" /help /about /provider /model /clear /compact /plan /session"));
1673
- console.log(chalk.dim(" /system /context /status /search /undo /export"));
1674
- console.log(chalk.dim(" /tools /plugins /mcp /config /exit"));
1674
+ console.log(chalk.dim(" /system /context /status /search /undo /export /copy /cost"));
1675
+ console.log(chalk.dim(" /init /skill /tools /plugins /mcp /config /exit"));
1675
1676
  console.log(HR);
1676
1677
  console.log(chalk.gray(" \u4E3B\u8981\u7279\u6027\uFF1A"));
1677
1678
  console.log(feat("Agentic \u5FAA\u73AF\uFF08\u6700\u591A 20 \u8F6E\u5DE5\u5177\u8C03\u7528\uFF0C\u6700\u7EC8\u56DE\u7B54\u6D41\u5F0F\u8F93\u51FA\uFF09"));
@@ -1689,6 +1690,9 @@ var Renderer = class {
1689
1690
  console.log(feat("/compact \u4E0A\u4E0B\u6587\u538B\u7F29\uFF1A\u751F\u6210\u6458\u8981\u66FF\u6362\u65E7\u6D88\u606F\uFF0C\u4FDD\u7559\u6700\u8FD1 4 \u6761\uFF0C\u89E3\u51B3 context \u6EA2\u51FA"));
1690
1691
  console.log(feat("Kimi \u5DE5\u5177\u8C03\u7528\u53EF\u9760\u6027\u589E\u5F3A\uFF1AXML \u4F2A\u8C03\u7528\u81EA\u52A8\u68C0\u6D4B\u5E76\u8F6C\u6362\u4E3A\u771F\u5B9E API \u8C03\u7528\uFF08v0.1.19\uFF09"));
1691
1692
  console.log(feat("Sub-Agent \u5B50\u4EE3\u7406\uFF1Aspawn_agent \u59D4\u6D3E\u72EC\u7ACB\u5B50\u4EE3\u7406\uFF0C\u9694\u79BB\u5FAA\u73AF\u6267\u884C\u590D\u6742\u5B50\u4EFB\u52A1"));
1693
+ console.log(feat("Agent Skills\uFF1A~/.aicli/skills/ \u53EF\u590D\u7528\u6280\u80FD\u5305\uFF0C\u6CE8\u5165 system prompt + \u5DE5\u5177\u767D\u540D\u5355"));
1694
+ console.log(feat("/init \u9879\u76EE\u521D\u59CB\u5316\uFF1A\u626B\u63CF\u9879\u76EE\u7ED3\u6784\uFF0CAI \u751F\u6210 AICLI.md \u4E0A\u4E0B\u6587\u6587\u4EF6"));
1695
+ console.log(feat("\u591A\u6587\u4EF6\u7F16\u8F91\u9884\u89C8\uFF1A\u6279\u91CF diff preview + \u9009\u62E9\u6027 approve/reject"));
1692
1696
  console.log(feat("\u72EC\u7ACB\u53EF\u6267\u884C\u6587\u4EF6\u6253\u5305\uFF08~56MB\uFF0C\u65E0\u9700 Node.js \u73AF\u5883\uFF09"));
1693
1697
  console.log();
1694
1698
  }
@@ -1831,12 +1835,104 @@ Error: ${message}
1831
1835
  };
1832
1836
 
1833
1837
  // src/repl/commands/index.ts
1834
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
1835
- import { resolve, dirname as dirname2 } from "path";
1838
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync } from "fs";
1839
+ import { execSync as execSync2 } from "child_process";
1840
+ import { platform } from "os";
1841
+ import { resolve, dirname as dirname2, join as join4 } from "path";
1836
1842
  import chalk2 from "chalk";
1837
1843
 
1844
+ // src/tools/git-context.ts
1845
+ import { execSync } from "child_process";
1846
+ import { existsSync as existsSync3 } from "fs";
1847
+ import { join as join3 } from "path";
1848
+ function runGit(cmd, cwd) {
1849
+ try {
1850
+ return execSync(`git ${cmd}`, {
1851
+ cwd,
1852
+ encoding: "utf-8",
1853
+ stdio: ["pipe", "pipe", "pipe"],
1854
+ timeout: 5e3
1855
+ }).trim();
1856
+ } catch {
1857
+ return null;
1858
+ }
1859
+ }
1860
+ function getGitRoot(cwd = process.cwd()) {
1861
+ return runGit("rev-parse --show-toplevel", cwd);
1862
+ }
1863
+ function getGitContext(cwd = process.cwd()) {
1864
+ if (!existsSync3(join3(cwd, ".git"))) {
1865
+ const result = runGit("rev-parse --git-dir", cwd);
1866
+ if (!result) return null;
1867
+ }
1868
+ const branch = runGit("rev-parse --abbrev-ref HEAD", cwd);
1869
+ if (!branch) return null;
1870
+ const statusOutput = runGit("status --porcelain", cwd) ?? "";
1871
+ const statusLines = statusOutput ? statusOutput.split("\n").filter(Boolean) : [];
1872
+ const stagedFiles = [];
1873
+ const changedFiles = [];
1874
+ for (const line of statusLines) {
1875
+ const xy = line.slice(0, 2);
1876
+ const file = line.slice(3).trim();
1877
+ const indexStatus = xy[0];
1878
+ const workStatus = xy[1];
1879
+ if (indexStatus && indexStatus !== " " && indexStatus !== "?") {
1880
+ stagedFiles.push(`${indexStatus} ${file}`);
1881
+ }
1882
+ if (workStatus && workStatus !== " ") {
1883
+ changedFiles.push(`${workStatus} ${file}`);
1884
+ }
1885
+ }
1886
+ const logOutput = runGit("log --oneline -3", cwd) ?? "";
1887
+ const recentCommits = logOutput ? logOutput.split("\n").filter(Boolean) : [];
1888
+ const unpushedOutput = runGit("log @{u}..HEAD --oneline 2>/dev/null", cwd);
1889
+ const hasUnpushed = unpushedOutput !== null && unpushedOutput.trim().length > 0;
1890
+ return {
1891
+ branch,
1892
+ changedFiles,
1893
+ stagedFiles,
1894
+ recentCommits,
1895
+ hasUnpushed
1896
+ };
1897
+ }
1898
+ function formatGitContextForPrompt(ctx) {
1899
+ const lines = ["# Git Repository Status", ""];
1900
+ lines.push(`- **Branch**: \`${ctx.branch}\``);
1901
+ if (ctx.stagedFiles.length > 0) {
1902
+ lines.push(`- **Staged** (${ctx.stagedFiles.length} files):`);
1903
+ for (const f of ctx.stagedFiles.slice(0, 10)) {
1904
+ lines.push(` - ${f}`);
1905
+ }
1906
+ if (ctx.stagedFiles.length > 10) {
1907
+ lines.push(` - ... and ${ctx.stagedFiles.length - 10} more`);
1908
+ }
1909
+ }
1910
+ if (ctx.changedFiles.length > 0) {
1911
+ lines.push(`- **Modified** (${ctx.changedFiles.length} files):`);
1912
+ for (const f of ctx.changedFiles.slice(0, 10)) {
1913
+ lines.push(` - ${f}`);
1914
+ }
1915
+ if (ctx.changedFiles.length > 10) {
1916
+ lines.push(` - ... and ${ctx.changedFiles.length - 10} more`);
1917
+ }
1918
+ }
1919
+ if (ctx.stagedFiles.length === 0 && ctx.changedFiles.length === 0) {
1920
+ lines.push("- **Working tree**: clean");
1921
+ }
1922
+ if (ctx.recentCommits.length > 0) {
1923
+ lines.push("- **Recent commits**:");
1924
+ for (const c of ctx.recentCommits) {
1925
+ lines.push(` - ${c}`);
1926
+ }
1927
+ }
1928
+ if (ctx.hasUnpushed) {
1929
+ lines.push("- \u26A0\uFE0F Has unpushed commits");
1930
+ }
1931
+ return lines.join("\n");
1932
+ }
1933
+
1838
1934
  // src/tools/undo-stack.ts
1839
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, existsSync as existsSync3 } from "fs";
1935
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, existsSync as existsSync4 } from "fs";
1840
1936
  var MAX_UNDO_DEPTH = 20;
1841
1937
  var UndoStack = class {
1842
1938
  stack = [];
@@ -1847,7 +1943,7 @@ var UndoStack = class {
1847
1943
  */
1848
1944
  push(filePath, description) {
1849
1945
  let previousContent = null;
1850
- if (existsSync3(filePath)) {
1946
+ if (existsSync4(filePath)) {
1851
1947
  try {
1852
1948
  previousContent = readFileSync3(filePath, "utf-8");
1853
1949
  } catch {
@@ -1873,7 +1969,7 @@ var UndoStack = class {
1873
1969
  if (!entry) return null;
1874
1970
  try {
1875
1971
  if (entry.previousContent === null) {
1876
- if (existsSync3(entry.filePath)) {
1972
+ if (existsSync4(entry.filePath)) {
1877
1973
  unlinkSync(entry.filePath);
1878
1974
  }
1879
1975
  return { entry, result: `Deleted newly created file: ${entry.filePath}` };
@@ -1911,6 +2007,200 @@ function fmtCtx(tokens) {
1911
2007
  if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
1912
2008
  return `${tokens}`;
1913
2009
  }
2010
+ var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
2011
+ "node_modules",
2012
+ ".git",
2013
+ "dist",
2014
+ "build",
2015
+ "out",
2016
+ "target",
2017
+ ".next",
2018
+ ".nuxt",
2019
+ "__pycache__",
2020
+ ".venv",
2021
+ "venv",
2022
+ ".tox",
2023
+ ".mypy_cache",
2024
+ ".pytest_cache",
2025
+ ".gradle",
2026
+ ".idea",
2027
+ ".vscode",
2028
+ ".vs",
2029
+ "coverage",
2030
+ ".cache",
2031
+ ".parcel-cache",
2032
+ "dist-cjs",
2033
+ "release",
2034
+ ".output",
2035
+ ".turbo",
2036
+ "vendor"
2037
+ ]);
2038
+ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
2039
+ const lines = [];
2040
+ let count = 0;
2041
+ const walk = (d, prefix, depth) => {
2042
+ if (depth > maxDepth || count >= maxEntries) return;
2043
+ let entries;
2044
+ try {
2045
+ entries = readdirSync2(d);
2046
+ } catch {
2047
+ return;
2048
+ }
2049
+ const filtered = entries.filter((e) => !e.startsWith(".") && !SCAN_SKIP_DIRS.has(e));
2050
+ const sorted = filtered.sort((a, b) => {
2051
+ const aIsDir = statSync(join4(d, a)).isDirectory();
2052
+ const bIsDir = statSync(join4(d, b)).isDirectory();
2053
+ if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
2054
+ return a.localeCompare(b);
2055
+ });
2056
+ for (let i = 0; i < sorted.length && count < maxEntries; i++) {
2057
+ const name = sorted[i];
2058
+ const fullPath = join4(d, name);
2059
+ const isLast = i === sorted.length - 1;
2060
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
2061
+ let isDir;
2062
+ try {
2063
+ isDir = statSync(fullPath).isDirectory();
2064
+ } catch {
2065
+ continue;
2066
+ }
2067
+ lines.push(prefix + connector + name + (isDir ? "/" : ""));
2068
+ count++;
2069
+ if (isDir) {
2070
+ walk(fullPath, prefix + (isLast ? " " : "\u2502 "), depth + 1);
2071
+ }
2072
+ }
2073
+ };
2074
+ walk(dir, "", 0);
2075
+ if (count >= maxEntries) lines.push("... (truncated)");
2076
+ return lines.join("\n");
2077
+ }
2078
+ function scanProject(cwd) {
2079
+ const info = {
2080
+ type: "unknown",
2081
+ language: "unknown",
2082
+ configFiles: [],
2083
+ directoryStructure: ""
2084
+ };
2085
+ const check = (file) => existsSync5(join4(cwd, file));
2086
+ const configCandidates = [
2087
+ "package.json",
2088
+ "tsconfig.json",
2089
+ "Cargo.toml",
2090
+ "pyproject.toml",
2091
+ "setup.py",
2092
+ "requirements.txt",
2093
+ "go.mod",
2094
+ "pom.xml",
2095
+ "build.gradle",
2096
+ "build.gradle.kts",
2097
+ "CMakeLists.txt",
2098
+ "Makefile",
2099
+ ".csproj",
2100
+ ".sln",
2101
+ "composer.json",
2102
+ "Gemfile",
2103
+ "mix.exs",
2104
+ "deno.json",
2105
+ "bun.lockb"
2106
+ ];
2107
+ info.configFiles = configCandidates.filter(check);
2108
+ if (check("package.json")) {
2109
+ info.type = "node";
2110
+ info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
2111
+ try {
2112
+ const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf-8"));
2113
+ const scripts = pkg.scripts ?? {};
2114
+ info.buildCommand = scripts.build ? `npm run build` : void 0;
2115
+ info.testCommand = scripts.test ? `npm test` : void 0;
2116
+ info.devCommand = scripts.dev ? `npm run dev` : void 0;
2117
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2118
+ if (allDeps["react"]) info.framework = "React";
2119
+ else if (allDeps["vue"]) info.framework = "Vue";
2120
+ else if (allDeps["@angular/core"]) info.framework = "Angular";
2121
+ else if (allDeps["next"]) info.framework = "Next.js";
2122
+ else if (allDeps["nuxt"]) info.framework = "Nuxt";
2123
+ else if (allDeps["express"]) info.framework = "Express";
2124
+ else if (allDeps["fastify"]) info.framework = "Fastify";
2125
+ else if (allDeps["svelte"]) info.framework = "Svelte";
2126
+ } catch {
2127
+ }
2128
+ } else if (check("Cargo.toml")) {
2129
+ info.type = "rust";
2130
+ info.language = "Rust";
2131
+ info.buildCommand = "cargo build";
2132
+ info.testCommand = "cargo test";
2133
+ } else if (check("pyproject.toml") || check("setup.py") || check("requirements.txt")) {
2134
+ info.type = "python";
2135
+ info.language = "Python";
2136
+ info.testCommand = check("pytest.ini") || check("pyproject.toml") ? "pytest" : "python -m unittest";
2137
+ } else if (check("go.mod")) {
2138
+ info.type = "go";
2139
+ info.language = "Go";
2140
+ info.buildCommand = "go build ./...";
2141
+ info.testCommand = "go test ./...";
2142
+ } else if (check("pom.xml")) {
2143
+ info.type = "java";
2144
+ info.language = "Java";
2145
+ info.buildCommand = "mvn package";
2146
+ info.testCommand = "mvn test";
2147
+ } else if (check("build.gradle") || check("build.gradle.kts")) {
2148
+ info.type = "java";
2149
+ info.language = "Java/Kotlin";
2150
+ info.buildCommand = "./gradlew build";
2151
+ info.testCommand = "./gradlew test";
2152
+ }
2153
+ info.directoryStructure = scanDirTree(cwd);
2154
+ return info;
2155
+ }
2156
+ function buildInitPrompt(info, cwd) {
2157
+ const parts = [
2158
+ `\u8BF7\u4E3A\u4EE5\u4E0B\u9879\u76EE\u751F\u6210\u4E00\u4E2A AICLI.md \u4E0A\u4E0B\u6587\u6587\u4EF6\uFF08Markdown \u683C\u5F0F\uFF09\u3002\u8FD9\u4E2A\u6587\u4EF6\u4F1A\u88AB\u6CE8\u5165\u5230 AI \u5BF9\u8BDD\u7684 system prompt \u4E2D\uFF0C\u5E2E\u52A9 AI \u7406\u89E3\u9879\u76EE\u7ED3\u6784\u548C\u7F16\u7801\u89C4\u8303\u3002`,
2159
+ `
2160
+ ## \u9879\u76EE\u4FE1\u606F
2161
+ `,
2162
+ `- \u5DE5\u4F5C\u76EE\u5F55: ${cwd}`,
2163
+ `- \u7C7B\u578B: ${info.type}`,
2164
+ `- \u8BED\u8A00: ${info.language}`
2165
+ ];
2166
+ if (info.framework) parts.push(`- \u6846\u67B6: ${info.framework}`);
2167
+ if (info.buildCommand) parts.push(`- \u6784\u5EFA\u547D\u4EE4: ${info.buildCommand}`);
2168
+ if (info.testCommand) parts.push(`- \u6D4B\u8BD5\u547D\u4EE4: ${info.testCommand}`);
2169
+ if (info.devCommand) parts.push(`- \u5F00\u53D1\u547D\u4EE4: ${info.devCommand}`);
2170
+ parts.push(`
2171
+ ## \u53D1\u73B0\u7684\u914D\u7F6E\u6587\u4EF6
2172
+ ${info.configFiles.map((f) => `- ${f}`).join("\n")}`);
2173
+ parts.push(`
2174
+ ## \u76EE\u5F55\u7ED3\u6784
2175
+ \`\`\`
2176
+ ${info.directoryStructure}
2177
+ \`\`\``);
2178
+ parts.push(`
2179
+ ## \u8981\u6C42
2180
+ \u8BF7\u751F\u6210\u4E00\u4E2A\u7ED3\u6784\u5316\u7684 Markdown \u6587\u4EF6\uFF0C\u5305\u542B\uFF1A
2181
+ 1. \u9879\u76EE\u7B80\u4ECB\uFF08\u4E00\u53E5\u8BDD\u6982\u8FF0\uFF09
2182
+ 2. \u6280\u672F\u6808
2183
+ 3. \u9879\u76EE\u7ED3\u6784\u8BF4\u660E\uFF08\u57FA\u4E8E\u4E0A\u9762\u7684\u76EE\u5F55\u7ED3\u6784\uFF09
2184
+ 4. \u5E38\u7528\u547D\u4EE4\uFF08build, test, dev \u7B49\uFF09
2185
+ 5. \u4EE3\u7801\u98CE\u683C\u548C\u7EA6\u5B9A\uFF08\u57FA\u4E8E\u914D\u7F6E\u6587\u4EF6\u63A8\u65AD\uFF09
2186
+
2187
+ \u76F4\u63A5\u8F93\u51FA Markdown \u5185\u5BB9\uFF0C\u4E0D\u8981\u7528\u4EE3\u7801\u5757\u5305\u88F9\u6574\u4E2A\u6587\u4EF6\u3002\u4FDD\u6301\u7B80\u6D01\uFF0C\u63A7\u5236\u5728 200 \u884C\u4EE5\u5185\u3002`);
2188
+ return parts.join("\n");
2189
+ }
2190
+ function copyToClipboard(text) {
2191
+ const plat = platform();
2192
+ if (plat === "win32") {
2193
+ execSync2("clip", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2194
+ } else if (plat === "darwin") {
2195
+ execSync2("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2196
+ } else {
2197
+ try {
2198
+ execSync2("xclip -selection clipboard", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2199
+ } catch {
2200
+ execSync2("xsel --clipboard --input", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2201
+ }
2202
+ }
2203
+ }
1914
2204
  var CommandRegistry = class {
1915
2205
  commands = /* @__PURE__ */ new Map();
1916
2206
  register(command) {
@@ -1951,6 +2241,10 @@ function createDefaultCommands() {
1951
2241
  " /plugins - Show plugin directory and loaded plugins",
1952
2242
  " /mcp - Show MCP server connections and tools",
1953
2243
  " /config - Open configuration wizard (API keys, proxy, etc.)",
2244
+ " /copy - Copy last AI response to clipboard",
2245
+ " /cost [reset] - Show session token usage (or reset counters)",
2246
+ " /init [--force] - Generate AICLI.md by scanning project structure",
2247
+ " /skill [name|off|list] - Manage agent skills (reusable prompt packs)",
1954
2248
  " /exit - Exit"
1955
2249
  ] : [];
1956
2250
  console.log("\nAvailable commands:");
@@ -2496,6 +2790,159 @@ ${text}
2496
2790
  ctx.renderer.renderError("Usage: /plan [execute|exit|status]");
2497
2791
  }
2498
2792
  },
2793
+ {
2794
+ name: "skill",
2795
+ description: "Manage agent skills (reusable prompt packs)",
2796
+ usage: "/skill [name|off|list|reload]",
2797
+ execute(args, ctx) {
2798
+ const sub = args[0]?.toLowerCase();
2799
+ const manager = ctx.getSkillManager();
2800
+ if (!manager) {
2801
+ ctx.renderer.printInfo("Skill system not initialized.");
2802
+ return;
2803
+ }
2804
+ if (!sub || sub === "list") {
2805
+ const skills = manager.listSkills();
2806
+ const active = manager.getActive();
2807
+ console.log();
2808
+ console.log(chalk2.bold(" \u{1F3AF} Agent Skills"));
2809
+ console.log(chalk2.dim(" " + "\u2500".repeat(50)));
2810
+ if (skills.length === 0) {
2811
+ console.log(chalk2.dim(" No skills available."));
2812
+ console.log(chalk2.dim(` Place .md files in: ${manager.getSkillsDir()}`));
2813
+ console.log(chalk2.dim(" Format: YAML frontmatter (name/description/tools) + prompt body"));
2814
+ } else {
2815
+ for (const s of skills) {
2816
+ const isActive = active?.meta.name === s.meta.name;
2817
+ const badge = isActive ? chalk2.green(" \u2713 active") : "";
2818
+ const toolInfo = s.meta.tools ? chalk2.dim(` [${s.meta.tools.join(", ")}]`) : "";
2819
+ console.log(
2820
+ ` ${chalk2.cyan(s.meta.name.padEnd(20))}${chalk2.dim(s.meta.description)}${toolInfo}${badge}`
2821
+ );
2822
+ }
2823
+ }
2824
+ console.log(chalk2.dim(" " + "\u2500".repeat(50)));
2825
+ console.log(chalk2.dim(` Directory: ${manager.getSkillsDir()}`));
2826
+ console.log();
2827
+ return;
2828
+ }
2829
+ if (sub === "off" || sub === "none" || sub === "deactivate") {
2830
+ if (!manager.getActive()) {
2831
+ ctx.renderer.printInfo("No skill is currently active.");
2832
+ return;
2833
+ }
2834
+ manager.deactivate();
2835
+ ctx.refreshPrompt();
2836
+ ctx.renderer.printSuccess("Skill deactivated.");
2837
+ return;
2838
+ }
2839
+ if (sub === "reload") {
2840
+ const count = manager.loadSkills();
2841
+ ctx.renderer.printSuccess(`Reloaded: ${count} skill(s) found.`);
2842
+ return;
2843
+ }
2844
+ const skill = manager.activate(sub);
2845
+ if (!skill) {
2846
+ ctx.renderer.renderError(`Skill '${sub}' not found. Use /skill list to see available skills.`);
2847
+ return;
2848
+ }
2849
+ ctx.refreshPrompt();
2850
+ ctx.renderer.printSuccess(`Skill activated: ${skill.meta.name} \u2014 ${skill.meta.description}`);
2851
+ if (skill.meta.tools) {
2852
+ ctx.renderer.printInfo(`Tool whitelist: ${skill.meta.tools.join(", ")}`);
2853
+ }
2854
+ }
2855
+ },
2856
+ {
2857
+ name: "init",
2858
+ description: "Generate project context file (AICLI.md) by scanning project structure",
2859
+ usage: "/init [--force]",
2860
+ async execute(args, ctx) {
2861
+ const cwd = process.cwd();
2862
+ const gitRoot = getGitRoot(cwd);
2863
+ const targetDir = gitRoot ?? cwd;
2864
+ const targetPath = join4(targetDir, "AICLI.md");
2865
+ const force = args.includes("--force");
2866
+ if (existsSync5(targetPath) && !force) {
2867
+ ctx.renderer.printInfo(`AICLI.md already exists at ${targetPath}`);
2868
+ ctx.renderer.printInfo("Use /init --force to overwrite, or edit it manually.");
2869
+ return;
2870
+ }
2871
+ ctx.renderer.printInfo("Scanning project structure...");
2872
+ const projectInfo = scanProject(cwd);
2873
+ if (projectInfo.type === "unknown" && projectInfo.configFiles.length === 0) {
2874
+ ctx.renderer.printInfo("Could not detect project type. Generating generic template...");
2875
+ }
2876
+ const prompt = buildInitPrompt(projectInfo, cwd);
2877
+ console.log(chalk2.dim(" Calling AI to generate AICLI.md..."));
2878
+ try {
2879
+ const content = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
2880
+ writeFileSync4(targetPath, content, "utf-8");
2881
+ ctx.renderer.printSuccess(`Generated: ${targetPath} (${content.length} chars)`);
2882
+ ctx.renderer.printInfo("Review and edit as needed. Use /context reload to load it.");
2883
+ } catch (err) {
2884
+ ctx.renderer.renderError(`Failed to generate AICLI.md: ${err instanceof Error ? err.message : String(err)}`);
2885
+ }
2886
+ }
2887
+ },
2888
+ {
2889
+ name: "copy",
2890
+ description: "Copy last AI response to system clipboard",
2891
+ usage: "/copy",
2892
+ execute(_args, ctx) {
2893
+ const content = ctx.getLastResponse();
2894
+ if (!content) {
2895
+ ctx.renderer.printInfo("No AI response to copy.");
2896
+ return;
2897
+ }
2898
+ try {
2899
+ copyToClipboard(content);
2900
+ const chars = content.length;
2901
+ const lines = content.split("\n").length;
2902
+ ctx.renderer.printSuccess(`Copied to clipboard (${lines} lines, ${chars} chars)`);
2903
+ } catch (err) {
2904
+ const msg = err instanceof Error ? err.message : String(err);
2905
+ if (platform() === "linux" && msg.includes("not found")) {
2906
+ ctx.renderer.renderError("Clipboard tool not found. Install xclip or xsel: sudo apt install xclip");
2907
+ } else {
2908
+ ctx.renderer.renderError(`Failed to copy: ${msg}`);
2909
+ }
2910
+ }
2911
+ }
2912
+ },
2913
+ {
2914
+ name: "cost",
2915
+ description: "Show session token usage summary",
2916
+ usage: "/cost [reset]",
2917
+ execute(args, ctx) {
2918
+ const sub = args[0]?.toLowerCase();
2919
+ if (sub === "reset") {
2920
+ ctx.resetSessionTokenUsage();
2921
+ ctx.renderer.printSuccess("Session token counters reset.");
2922
+ return;
2923
+ }
2924
+ const usage = ctx.getSessionTokenUsage();
2925
+ const totalTokens = usage.inputTokens + usage.outputTokens;
2926
+ if (totalTokens === 0) {
2927
+ ctx.renderer.printInfo("No token usage recorded this session.");
2928
+ return;
2929
+ }
2930
+ console.log();
2931
+ console.log(chalk2.bold(" \u{1F4CA} Session Token Usage"));
2932
+ console.log(chalk2.dim(" " + "\u2500".repeat(40)));
2933
+ console.log(chalk2.gray(" Input tokens : ") + chalk2.white(usage.inputTokens.toLocaleString()));
2934
+ console.log(chalk2.gray(" Output tokens : ") + chalk2.white(usage.outputTokens.toLocaleString()));
2935
+ console.log(chalk2.gray(" Total tokens : ") + chalk2.bold.white(totalTokens.toLocaleString()));
2936
+ console.log(chalk2.dim(" " + "\u2500".repeat(40)));
2937
+ const session = ctx.sessions.current;
2938
+ if (session) {
2939
+ console.log(chalk2.gray(" Provider : ") + chalk2.dim(ctx.getCurrentProvider()));
2940
+ console.log(chalk2.gray(" Model : ") + chalk2.dim(ctx.getCurrentModel()));
2941
+ console.log(chalk2.gray(" Messages : ") + chalk2.dim(String(session.messages.length)));
2942
+ }
2943
+ console.log();
2944
+ }
2945
+ },
2499
2946
  {
2500
2947
  name: "exit",
2501
2948
  description: "Exit the REPL",
@@ -2672,11 +3119,11 @@ function selectFromList(prompt, items, initialIndex = 0) {
2672
3119
  }
2673
3120
 
2674
3121
  // src/tools/builtin/bash.ts
2675
- import { execSync } from "child_process";
2676
- import { existsSync as existsSync4 } from "fs";
2677
- import { platform } from "os";
3122
+ import { execSync as execSync3 } from "child_process";
3123
+ import { existsSync as existsSync6 } from "fs";
3124
+ import { platform as platform2 } from "os";
2678
3125
  import { resolve as resolve2 } from "path";
2679
- var IS_WINDOWS = platform() === "win32";
3126
+ var IS_WINDOWS = platform2() === "win32";
2680
3127
  var SHELL = IS_WINDOWS ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
2681
3128
  var persistentCwd = process.cwd();
2682
3129
  var bashTool = {
@@ -2722,7 +3169,7 @@ var bashTool = {
2722
3169
  let effectiveCwd = persistentCwd;
2723
3170
  if (cwdArg) {
2724
3171
  const resolved = resolve2(persistentCwd, cwdArg);
2725
- if (existsSync4(resolved)) {
3172
+ if (existsSync6(resolved)) {
2726
3173
  effectiveCwd = resolved;
2727
3174
  persistentCwd = resolved;
2728
3175
  } else {
@@ -2737,7 +3184,7 @@ var bashTool = {
2737
3184
  actualCommand = command;
2738
3185
  }
2739
3186
  try {
2740
- const output = execSync(actualCommand, {
3187
+ const output = execSync3(actualCommand, {
2741
3188
  timeout,
2742
3189
  encoding: IS_WINDOWS ? "buffer" : "utf-8",
2743
3190
  stdio: ["pipe", "pipe", "pipe"],
@@ -2793,7 +3240,7 @@ function updateCwdFromCommand(command, baseCwd) {
2793
3240
  if (!target || target.startsWith("$") || target === "~") return;
2794
3241
  try {
2795
3242
  const newDir = resolve2(baseCwd, target);
2796
- if (existsSync4(newDir)) {
3243
+ if (existsSync6(newDir)) {
2797
3244
  persistentCwd = newDir;
2798
3245
  }
2799
3246
  } catch {
@@ -2801,14 +3248,14 @@ function updateCwdFromCommand(command, baseCwd) {
2801
3248
  }
2802
3249
 
2803
3250
  // src/tools/builtin/read-file.ts
2804
- import { readFileSync as readFileSync4, existsSync as existsSync5, statSync } from "fs";
2805
- import { extname, resolve as resolve3, basename, sep } from "path";
3251
+ import { readFileSync as readFileSync5, existsSync as existsSync7, statSync as statSync2 } from "fs";
3252
+ import { extname, resolve as resolve3, basename as basename2, sep } from "path";
2806
3253
  import { homedir as homedir2 } from "os";
2807
3254
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
2808
3255
  function getSensitiveWarning(normalizedPath) {
2809
3256
  const home = homedir2();
2810
3257
  const p = normalizedPath.toLowerCase();
2811
- const base = basename(normalizedPath).toLowerCase();
3258
+ const base = basename2(normalizedPath).toLowerCase();
2812
3259
  if (normalizedPath.startsWith(home) && p.includes(".aicli") && base === "config.json") {
2813
3260
  return "[\u26A0 Security Warning: This file contains API keys. Be careful not to share this content.]\n\n";
2814
3261
  }
@@ -2894,8 +3341,8 @@ var readFileTool = {
2894
3341
  const encoding = args["encoding"] ?? "utf-8";
2895
3342
  if (!filePath) throw new Error("path is required");
2896
3343
  const normalizedPath = resolve3(filePath);
2897
- if (!existsSync5(normalizedPath)) throw new Error(`File not found: ${filePath}`);
2898
- const { size } = statSync(normalizedPath);
3344
+ if (!existsSync7(normalizedPath)) throw new Error(`File not found: ${filePath}`);
3345
+ const { size } = statSync2(normalizedPath);
2899
3346
  if (size > MAX_FILE_BYTES) {
2900
3347
  const mb = (size / 1024 / 1024).toFixed(1);
2901
3348
  return `[File too large: ${filePath} (${mb} MB)]
@@ -2915,7 +3362,7 @@ var readFileTool = {
2915
3362
  2. \u4F7F\u7528 bash \u5DE5\u5177\u8C03\u7528\u5916\u90E8\u8F6C\u6362\u7A0B\u5E8F\uFF08\u5982 pdftotext\u3001pandoc \u7B49\uFF09
2916
3363
  3. \u82E5\u6709\u5BF9\u5E94\u7684\u7EAF\u6587\u672C\u7248\u672C\uFF08.md / .txt\uFF09\uFF0C\u8BF7\u76F4\u63A5\u8BFB\u53D6\u90A3\u4E2A\u6587\u4EF6`;
2917
3364
  }
2918
- const buf = readFileSync4(normalizedPath);
3365
+ const buf = readFileSync5(normalizedPath);
2919
3366
  if (encoding === "base64") {
2920
3367
  return `[File: ${filePath} | base64]
2921
3368
 
@@ -2989,7 +3436,7 @@ var writeFileTool = {
2989
3436
  };
2990
3437
 
2991
3438
  // src/tools/builtin/edit-file.ts
2992
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync6 } from "fs";
3439
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
2993
3440
  var editFileTool = {
2994
3441
  definition: {
2995
3442
  name: "edit_file",
@@ -3047,8 +3494,8 @@ var editFileTool = {
3047
3494
  const filePath = String(args["path"] ?? "");
3048
3495
  const encoding = args["encoding"] ?? "utf-8";
3049
3496
  if (!filePath) throw new Error("path is required");
3050
- if (!existsSync6(filePath)) throw new Error(`File not found: ${filePath}`);
3051
- const original = readFileSync5(filePath, encoding);
3497
+ if (!existsSync8(filePath)) throw new Error(`File not found: ${filePath}`);
3498
+ const original = readFileSync6(filePath, encoding);
3052
3499
  if (args["old_str"] !== void 0) {
3053
3500
  const oldStr = String(args["old_str"]);
3054
3501
  const newStr = String(args["new_str"] ?? "");
@@ -3113,8 +3560,8 @@ function truncatePreview(str, maxLen = 80) {
3113
3560
  }
3114
3561
 
3115
3562
  // src/tools/builtin/list-dir.ts
3116
- import { readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync7 } from "fs";
3117
- import { join as join3 } from "path";
3563
+ import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync9 } from "fs";
3564
+ import { join as join5 } from "path";
3118
3565
  var listDirTool = {
3119
3566
  definition: {
3120
3567
  name: "list_dir",
@@ -3136,7 +3583,7 @@ var listDirTool = {
3136
3583
  async execute(args) {
3137
3584
  const dirPath = String(args["path"] ?? process.cwd());
3138
3585
  const recursive = Boolean(args["recursive"] ?? false);
3139
- if (!existsSync7(dirPath)) throw new Error(`Directory not found: ${dirPath}`);
3586
+ if (!existsSync9(dirPath)) throw new Error(`Directory not found: ${dirPath}`);
3140
3587
  const lines = [`Directory: ${dirPath}
3141
3588
  `];
3142
3589
  listRecursive(dirPath, "", recursive, lines);
@@ -3146,7 +3593,7 @@ var listDirTool = {
3146
3593
  function listRecursive(basePath, indent, recursive, lines) {
3147
3594
  let entries;
3148
3595
  try {
3149
- entries = readdirSync2(basePath, { withFileTypes: true });
3596
+ entries = readdirSync3(basePath, { withFileTypes: true });
3150
3597
  } catch {
3151
3598
  lines.push(`${indent}(permission denied)`);
3152
3599
  return;
@@ -3165,11 +3612,11 @@ function listRecursive(basePath, indent, recursive, lines) {
3165
3612
  if (entry.isDirectory()) {
3166
3613
  lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
3167
3614
  if (recursive) {
3168
- listRecursive(join3(basePath, entry.name), indent + " ", true, lines);
3615
+ listRecursive(join5(basePath, entry.name), indent + " ", true, lines);
3169
3616
  }
3170
3617
  } else {
3171
3618
  try {
3172
- const stat = statSync2(join3(basePath, entry.name));
3619
+ const stat = statSync3(join5(basePath, entry.name));
3173
3620
  const size = formatSize(stat.size);
3174
3621
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
3175
3622
  } catch {
@@ -3185,8 +3632,8 @@ function formatSize(bytes) {
3185
3632
  }
3186
3633
 
3187
3634
  // src/tools/builtin/grep-files.ts
3188
- import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3, existsSync as existsSync8 } from "fs";
3189
- import { join as join4, relative } from "path";
3635
+ import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4, existsSync as existsSync10 } from "fs";
3636
+ import { join as join6, relative } from "path";
3190
3637
  var grepFilesTool = {
3191
3638
  definition: {
3192
3639
  name: "grep_files",
@@ -3237,7 +3684,7 @@ var grepFilesTool = {
3237
3684
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
3238
3685
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
3239
3686
  if (!pattern) throw new Error("pattern is required");
3240
- if (!existsSync8(rootPath)) throw new Error(`Path not found: ${rootPath}`);
3687
+ if (!existsSync10(rootPath)) throw new Error(`Path not found: ${rootPath}`);
3241
3688
  let regex;
3242
3689
  try {
3243
3690
  regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
@@ -3246,7 +3693,7 @@ var grepFilesTool = {
3246
3693
  regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
3247
3694
  }
3248
3695
  const results = [];
3249
- const stat = statSync3(rootPath);
3696
+ const stat = statSync4(rootPath);
3250
3697
  if (stat.isFile()) {
3251
3698
  searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
3252
3699
  } else {
@@ -3302,7 +3749,7 @@ function collectFiles(dirPath, filePattern, results, regex, contextLines, maxRes
3302
3749
  if (results.length >= maxResults) return;
3303
3750
  let entries;
3304
3751
  try {
3305
- entries = readdirSync3(dirPath, { withFileTypes: true });
3752
+ entries = readdirSync4(dirPath, { withFileTypes: true });
3306
3753
  } catch {
3307
3754
  return;
3308
3755
  }
@@ -3310,11 +3757,11 @@ function collectFiles(dirPath, filePattern, results, regex, contextLines, maxRes
3310
3757
  if (results.length >= maxResults) return;
3311
3758
  if (entry.isDirectory()) {
3312
3759
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
3313
- collectFiles(join4(dirPath, entry.name), filePattern, results, regex, contextLines, maxResults, rootPath);
3760
+ collectFiles(join6(dirPath, entry.name), filePattern, results, regex, contextLines, maxResults, rootPath);
3314
3761
  } else if (entry.isFile()) {
3315
3762
  if (isBinary(entry.name)) continue;
3316
3763
  if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
3317
- const fullPath = join4(dirPath, entry.name);
3764
+ const fullPath = join6(dirPath, entry.name);
3318
3765
  const relPath = relative(rootPath, fullPath);
3319
3766
  searchInFile(fullPath, relPath, regex, contextLines, maxResults, results);
3320
3767
  }
@@ -3323,7 +3770,7 @@ function collectFiles(dirPath, filePattern, results, regex, contextLines, maxRes
3323
3770
  function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, results) {
3324
3771
  let content;
3325
3772
  try {
3326
- content = readFileSync6(fullPath, "utf-8");
3773
+ content = readFileSync7(fullPath, "utf-8");
3327
3774
  } catch {
3328
3775
  return;
3329
3776
  }
@@ -3355,8 +3802,8 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
3355
3802
  }
3356
3803
 
3357
3804
  // src/tools/builtin/glob-files.ts
3358
- import { readdirSync as readdirSync4, statSync as statSync4, existsSync as existsSync9 } from "fs";
3359
- import { join as join5, relative as relative2, basename as basename2 } from "path";
3805
+ import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync11 } from "fs";
3806
+ import { join as join7, relative as relative2, basename as basename3 } from "path";
3360
3807
  var globFilesTool = {
3361
3808
  definition: {
3362
3809
  name: "glob_files",
@@ -3389,7 +3836,7 @@ var globFilesTool = {
3389
3836
  const rootPath = String(args["path"] ?? process.cwd());
3390
3837
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
3391
3838
  if (!pattern) throw new Error("pattern is required");
3392
- if (!existsSync9(rootPath)) throw new Error(`Path not found: ${rootPath}`);
3839
+ if (!existsSync11(rootPath)) throw new Error(`Path not found: ${rootPath}`);
3393
3840
  const regex = globToRegex(pattern);
3394
3841
  const matches = [];
3395
3842
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -3460,21 +3907,21 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
3460
3907
  if (results.length >= maxResults) return;
3461
3908
  let entries;
3462
3909
  try {
3463
- entries = readdirSync4(dirPath, { withFileTypes: true });
3910
+ entries = readdirSync5(dirPath, { withFileTypes: true });
3464
3911
  } catch {
3465
3912
  return;
3466
3913
  }
3467
3914
  for (const entry of entries) {
3468
3915
  if (results.length >= maxResults) break;
3469
- const fullPath = join5(dirPath, entry.name);
3916
+ const fullPath = join7(dirPath, entry.name);
3470
3917
  if (entry.isDirectory()) {
3471
3918
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
3472
3919
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
3473
3920
  } else if (entry.isFile()) {
3474
3921
  const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
3475
- if (regex.test(relPath) || regex.test(basename2(relPath))) {
3922
+ if (regex.test(relPath) || regex.test(basename3(relPath))) {
3476
3923
  try {
3477
- const stat = statSync4(fullPath);
3924
+ const stat = statSync5(fullPath);
3478
3925
  results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
3479
3926
  } catch {
3480
3927
  results.push({ relPath, absPath: fullPath, mtime: 0 });
@@ -3486,8 +3933,8 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
3486
3933
 
3487
3934
  // src/tools/builtin/run-interactive.ts
3488
3935
  import { spawn } from "child_process";
3489
- import { platform as platform2 } from "os";
3490
- var IS_WINDOWS2 = platform2() === "win32";
3936
+ import { platform as platform3 } from "os";
3937
+ var IS_WINDOWS2 = platform3() === "win32";
3491
3938
  var runInteractiveTool = {
3492
3939
  definition: {
3493
3940
  name: "run_interactive",
@@ -3818,11 +4265,11 @@ var saveLastResponseTool = {
3818
4265
  };
3819
4266
 
3820
4267
  // src/tools/builtin/save-memory.ts
3821
- import { existsSync as existsSync10, readFileSync as readFileSync7, appendFileSync as appendFileSync2, mkdirSync as mkdirSync7 } from "fs";
3822
- import { join as join6 } from "path";
4268
+ import { existsSync as existsSync12, readFileSync as readFileSync8, appendFileSync as appendFileSync2, mkdirSync as mkdirSync7 } from "fs";
4269
+ import { join as join8 } from "path";
3823
4270
  import { homedir as homedir3 } from "os";
3824
4271
  function getMemoryFilePath() {
3825
- return join6(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
4272
+ return join8(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
3826
4273
  }
3827
4274
  function formatTimestamp() {
3828
4275
  const now = /* @__PURE__ */ new Date();
@@ -3846,8 +4293,8 @@ var saveMemoryTool = {
3846
4293
  const content = String(args["content"] ?? "").trim();
3847
4294
  if (!content) throw new Error("content is required");
3848
4295
  const memoryPath = getMemoryFilePath();
3849
- const configDir = join6(homedir3(), CONFIG_DIR_NAME);
3850
- if (!existsSync10(configDir)) {
4296
+ const configDir = join8(homedir3(), CONFIG_DIR_NAME);
4297
+ if (!existsSync12(configDir)) {
3851
4298
  mkdirSync7(configDir, { recursive: true });
3852
4299
  }
3853
4300
  const timestamp = formatTimestamp();
@@ -3856,7 +4303,7 @@ var saveMemoryTool = {
3856
4303
  ${content}
3857
4304
  `;
3858
4305
  appendFileSync2(memoryPath, entry, "utf-8");
3859
- const fullContent = readFileSync7(memoryPath, "utf-8");
4306
+ const fullContent = readFileSync8(memoryPath, "utf-8");
3860
4307
  const entryCount = (fullContent.match(/^## \d{4}-\d{2}-\d{2}/gm) ?? []).length;
3861
4308
  const byteSize = Buffer.byteLength(fullContent, "utf-8");
3862
4309
  return `Memory saved successfully. Total: ${entryCount} entries, ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
@@ -4137,6 +4584,9 @@ function formatResults(query, data, requested) {
4137
4584
  import chalk6 from "chalk";
4138
4585
 
4139
4586
  // src/tools/types.ts
4587
+ function isFileWriteTool(name) {
4588
+ return name === "write_file" || name === "edit_file";
4589
+ }
4140
4590
  function getDangerLevel(toolName, args) {
4141
4591
  if (toolName.startsWith("mcp__")) return "safe";
4142
4592
  if (toolName === "bash") {
@@ -4434,8 +4884,8 @@ var spawnAgentTool = {
4434
4884
 
4435
4885
  // src/tools/registry.ts
4436
4886
  import { pathToFileURL } from "url";
4437
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync5 } from "fs";
4438
- import { join as join7 } from "path";
4887
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync6 } from "fs";
4888
+ import { join as join9 } from "path";
4439
4889
  var ToolRegistry = class {
4440
4890
  tools = /* @__PURE__ */ new Map();
4441
4891
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -4507,7 +4957,7 @@ var ToolRegistry = class {
4507
4957
  * Returns the number of successfully loaded plugins.
4508
4958
  */
4509
4959
  async loadPlugins(pluginsDir, allowPlugins = false) {
4510
- if (!existsSync11(pluginsDir)) {
4960
+ if (!existsSync13(pluginsDir)) {
4511
4961
  try {
4512
4962
  mkdirSync8(pluginsDir, { recursive: true });
4513
4963
  } catch {
@@ -4516,7 +4966,7 @@ var ToolRegistry = class {
4516
4966
  }
4517
4967
  let files;
4518
4968
  try {
4519
- files = readdirSync5(pluginsDir).filter((f) => f.endsWith(".js"));
4969
+ files = readdirSync6(pluginsDir).filter((f) => f.endsWith(".js"));
4520
4970
  } catch {
4521
4971
  return 0;
4522
4972
  }
@@ -4533,12 +4983,12 @@ var ToolRegistry = class {
4533
4983
  process.stderr.write(
4534
4984
  `
4535
4985
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
4536
- ` + files.map((f) => ` + ${join7(pluginsDir, f)}`).join("\n") + "\n\n"
4986
+ ` + files.map((f) => ` + ${join9(pluginsDir, f)}`).join("\n") + "\n\n"
4537
4987
  );
4538
4988
  let loaded = 0;
4539
4989
  for (const file of files) {
4540
4990
  try {
4541
- const fileUrl = pathToFileURL(join7(pluginsDir, file)).href;
4991
+ const fileUrl = pathToFileURL(join9(pluginsDir, file)).href;
4542
4992
  const mod = await import(fileUrl);
4543
4993
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
4544
4994
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -4567,7 +5017,7 @@ var ToolRegistry = class {
4567
5017
 
4568
5018
  // src/tools/executor.ts
4569
5019
  import chalk8 from "chalk";
4570
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
5020
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
4571
5021
 
4572
5022
  // src/tools/diff-utils.ts
4573
5023
  import chalk7 from "chalk";
@@ -4821,12 +5271,130 @@ var ToolExecutor = class {
4821
5271
  }
4822
5272
  }
4823
5273
  async executeAll(calls) {
5274
+ const safeCalls = [];
5275
+ const fileWriteCalls = [];
5276
+ const otherCalls = [];
5277
+ for (let i = 0; i < calls.length; i++) {
5278
+ const call = calls[i];
5279
+ const level = getDangerLevel(call.name, call.arguments);
5280
+ if (level === "safe") {
5281
+ safeCalls.push({ idx: i, call });
5282
+ } else if (isFileWriteTool(call.name) && level === "write") {
5283
+ fileWriteCalls.push({ idx: i, call });
5284
+ } else {
5285
+ otherCalls.push({ idx: i, call });
5286
+ }
5287
+ }
5288
+ const results = new Array(calls.length);
5289
+ for (const { idx, call } of safeCalls) {
5290
+ results[idx] = await this.execute(call);
5291
+ }
5292
+ if (fileWriteCalls.length === 1) {
5293
+ const { idx, call } = fileWriteCalls[0];
5294
+ results[idx] = await this.execute(call);
5295
+ } else if (fileWriteCalls.length >= 2) {
5296
+ const batchResults = await this.executeBatchFileWrites(fileWriteCalls.map((f) => f.call));
5297
+ for (let i = 0; i < fileWriteCalls.length; i++) {
5298
+ results[fileWriteCalls[i].idx] = batchResults[i];
5299
+ }
5300
+ }
5301
+ for (const { idx, call } of otherCalls) {
5302
+ results[idx] = await this.execute(call);
5303
+ }
5304
+ return results;
5305
+ }
5306
+ /**
5307
+ * 批量文件写入:展示所有文件的编号列表 + diff 预览,
5308
+ * 然后让用户 approve all / reject all / 选择性 approve。
5309
+ */
5310
+ async executeBatchFileWrites(calls) {
5311
+ console.log();
5312
+ console.log(chalk8.bold.yellow(`\u270E Batch file writes (${calls.length} files):`));
5313
+ console.log(chalk8.dim("\u2500".repeat(50)));
5314
+ for (let i = 0; i < calls.length; i++) {
5315
+ const call = calls[i];
5316
+ const filePath = String(call.arguments["path"] ?? "");
5317
+ console.log(chalk8.yellow(` [${i + 1}] `) + chalk8.white(call.name) + chalk8.gray(": ") + chalk8.cyan(filePath));
5318
+ this.printDiffPreview(call);
5319
+ }
5320
+ console.log(chalk8.dim("\u2500".repeat(50)));
5321
+ const decision = await this.batchConfirm(calls.length);
4824
5322
  const results = [];
4825
- for (const call of calls) {
4826
- results.push(await this.execute(call));
5323
+ for (let i = 0; i < calls.length; i++) {
5324
+ const call = calls[i];
5325
+ const approved = decision === "all" || decision !== "none" && decision.has(i + 1);
5326
+ if (approved) {
5327
+ const tool = this.registry.get(call.name);
5328
+ if (!tool) {
5329
+ results.push({ callId: call.id, content: `Unknown tool: ${call.name}`, isError: true });
5330
+ continue;
5331
+ }
5332
+ try {
5333
+ const rawContent = await tool.execute(call.arguments);
5334
+ const content = truncateOutput2(rawContent, call.name);
5335
+ const wasTruncated = content !== rawContent;
5336
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
5337
+ results.push({ callId: call.id, content, isError: false });
5338
+ } catch (err) {
5339
+ const message = err instanceof Error ? err.message : String(err);
5340
+ this.printToolResult(call.name, message, true, false);
5341
+ results.push({ callId: call.id, content: message, isError: true });
5342
+ }
5343
+ } else {
5344
+ console.log(chalk8.gray(` [${i + 1}] `) + chalk8.dim("rejected"));
5345
+ results.push({ callId: call.id, content: "User rejected the operation.", isError: false });
5346
+ }
4827
5347
  }
4828
5348
  return results;
4829
5349
  }
5350
+ /**
5351
+ * 批量确认:让用户选择 approve all / reject all / 指定编号。
5352
+ * 返回 'all' | 'none' | Set<number>(1-based 编号)
5353
+ */
5354
+ batchConfirm(count) {
5355
+ const prompt = chalk8.yellow(` [a]pprove all [r]eject all [1,${count > 1 ? count : "2"},..] approve specific: `);
5356
+ if (!this.rl) {
5357
+ process.stdout.write(chalk8.yellow("No readline: auto-rejected.\n"));
5358
+ return Promise.resolve("none");
5359
+ }
5360
+ const rl = this.rl;
5361
+ const rlAny = rl;
5362
+ const savedOutput = rlAny.output;
5363
+ rlAny.output = process.stdout;
5364
+ rl.resume();
5365
+ process.stdout.write(prompt);
5366
+ this.confirming = true;
5367
+ return new Promise((resolve5) => {
5368
+ const cleanup = (result) => {
5369
+ rl.removeListener("line", onLine);
5370
+ this.cancelConfirmFn = null;
5371
+ rl.pause();
5372
+ rlAny.output = savedOutput;
5373
+ this.confirming = false;
5374
+ resolve5(result);
5375
+ };
5376
+ const onLine = (line) => {
5377
+ const input2 = line.trim().toLowerCase();
5378
+ if (input2 === "a" || input2 === "all" || input2 === "y") {
5379
+ cleanup("all");
5380
+ } else if (input2 === "r" || input2 === "reject" || input2 === "n" || input2 === "") {
5381
+ cleanup("none");
5382
+ } else {
5383
+ const nums = input2.split(/[,\s]+/).map(Number).filter((n) => !isNaN(n) && n >= 1 && n <= count);
5384
+ if (nums.length > 0) {
5385
+ cleanup(new Set(nums));
5386
+ } else {
5387
+ cleanup("none");
5388
+ }
5389
+ }
5390
+ };
5391
+ this.cancelConfirmFn = () => {
5392
+ process.stdout.write(chalk8.gray("\n(cancelled)\n"));
5393
+ cleanup("none");
5394
+ };
5395
+ rl.once("line", onLine);
5396
+ });
5397
+ }
4830
5398
  printToolCall(call) {
4831
5399
  const dangerLevel = getDangerLevel(call.name, call.arguments);
4832
5400
  console.log();
@@ -4857,10 +5425,10 @@ var ToolExecutor = class {
4857
5425
  const filePath = String(call.arguments["path"] ?? "");
4858
5426
  const newContent = String(call.arguments["content"] ?? "");
4859
5427
  if (!filePath) return;
4860
- if (existsSync12(filePath)) {
5428
+ if (existsSync14(filePath)) {
4861
5429
  let oldContent;
4862
5430
  try {
4863
- oldContent = readFileSync8(filePath, "utf-8");
5431
+ oldContent = readFileSync9(filePath, "utf-8");
4864
5432
  } catch {
4865
5433
  return;
4866
5434
  }
@@ -4883,7 +5451,7 @@ var ToolExecutor = class {
4883
5451
  }
4884
5452
  } else if (call.name === "edit_file") {
4885
5453
  const filePath = String(call.arguments["path"] ?? "");
4886
- if (!filePath || !existsSync12(filePath)) return;
5454
+ if (!filePath || !existsSync14(filePath)) return;
4887
5455
  const oldStr = call.arguments["old_str"];
4888
5456
  const newStr = call.arguments["new_str"];
4889
5457
  if (oldStr !== void 0) {
@@ -4910,7 +5478,7 @@ var ToolExecutor = class {
4910
5478
  const to = Number(call.arguments["delete_to_line"] ?? from);
4911
5479
  let fileContent;
4912
5480
  try {
4913
- fileContent = readFileSync8(filePath, "utf-8");
5481
+ fileContent = readFileSync9(filePath, "utf-8");
4914
5482
  } catch {
4915
5483
  return;
4916
5484
  }
@@ -5216,8 +5784,8 @@ Managing ${displayName} API Key`);
5216
5784
  };
5217
5785
 
5218
5786
  // src/repl/dev-state.ts
5219
- import { existsSync as existsSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2, mkdirSync as mkdirSync9 } from "fs";
5220
- import { join as join8 } from "path";
5787
+ import { existsSync as existsSync15, readFileSync as readFileSync10, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2, mkdirSync as mkdirSync9 } from "fs";
5788
+ import { join as join10 } from "path";
5221
5789
  import { homedir as homedir4 } from "os";
5222
5790
  var DEV_STATE_MAX_CHARS = 4e3;
5223
5791
  var SNAPSHOT_PROMPT = `You are about to be replaced by a different AI model. Please generate a structured development state snapshot so the next model can continue seamlessly.
@@ -5256,11 +5824,11 @@ function sessionHasMeaningfulContent(messages) {
5256
5824
  return hasUser && hasAssistant;
5257
5825
  }
5258
5826
  function getDevStatePath() {
5259
- return join8(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
5827
+ return join10(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
5260
5828
  }
5261
5829
  function saveDevState(content) {
5262
- const configDir = join8(homedir4(), CONFIG_DIR_NAME);
5263
- if (!existsSync13(configDir)) {
5830
+ const configDir = join10(homedir4(), CONFIG_DIR_NAME);
5831
+ if (!existsSync15(configDir)) {
5264
5832
  mkdirSync9(configDir, { recursive: true });
5265
5833
  }
5266
5834
  let trimmed = content.trim();
@@ -5276,13 +5844,13 @@ function saveDevState(content) {
5276
5844
  }
5277
5845
  function loadDevState() {
5278
5846
  const path = getDevStatePath();
5279
- if (!existsSync13(path)) return null;
5280
- const content = readFileSync9(path, "utf-8").trim();
5847
+ if (!existsSync15(path)) return null;
5848
+ const content = readFileSync10(path, "utf-8").trim();
5281
5849
  return content || null;
5282
5850
  }
5283
5851
  function clearDevState() {
5284
5852
  const path = getDevStatePath();
5285
- if (existsSync13(path)) {
5853
+ if (existsSync15(path)) {
5286
5854
  try {
5287
5855
  unlinkSync2(path);
5288
5856
  } catch {
@@ -5290,96 +5858,6 @@ function clearDevState() {
5290
5858
  }
5291
5859
  }
5292
5860
 
5293
- // src/tools/git-context.ts
5294
- import { execSync as execSync2 } from "child_process";
5295
- import { existsSync as existsSync14 } from "fs";
5296
- import { join as join9 } from "path";
5297
- function runGit(cmd, cwd) {
5298
- try {
5299
- return execSync2(`git ${cmd}`, {
5300
- cwd,
5301
- encoding: "utf-8",
5302
- stdio: ["pipe", "pipe", "pipe"],
5303
- timeout: 5e3
5304
- }).trim();
5305
- } catch {
5306
- return null;
5307
- }
5308
- }
5309
- function getGitRoot(cwd = process.cwd()) {
5310
- return runGit("rev-parse --show-toplevel", cwd);
5311
- }
5312
- function getGitContext(cwd = process.cwd()) {
5313
- if (!existsSync14(join9(cwd, ".git"))) {
5314
- const result = runGit("rev-parse --git-dir", cwd);
5315
- if (!result) return null;
5316
- }
5317
- const branch = runGit("rev-parse --abbrev-ref HEAD", cwd);
5318
- if (!branch) return null;
5319
- const statusOutput = runGit("status --porcelain", cwd) ?? "";
5320
- const statusLines = statusOutput ? statusOutput.split("\n").filter(Boolean) : [];
5321
- const stagedFiles = [];
5322
- const changedFiles = [];
5323
- for (const line of statusLines) {
5324
- const xy = line.slice(0, 2);
5325
- const file = line.slice(3).trim();
5326
- const indexStatus = xy[0];
5327
- const workStatus = xy[1];
5328
- if (indexStatus && indexStatus !== " " && indexStatus !== "?") {
5329
- stagedFiles.push(`${indexStatus} ${file}`);
5330
- }
5331
- if (workStatus && workStatus !== " ") {
5332
- changedFiles.push(`${workStatus} ${file}`);
5333
- }
5334
- }
5335
- const logOutput = runGit("log --oneline -3", cwd) ?? "";
5336
- const recentCommits = logOutput ? logOutput.split("\n").filter(Boolean) : [];
5337
- const unpushedOutput = runGit("log @{u}..HEAD --oneline 2>/dev/null", cwd);
5338
- const hasUnpushed = unpushedOutput !== null && unpushedOutput.trim().length > 0;
5339
- return {
5340
- branch,
5341
- changedFiles,
5342
- stagedFiles,
5343
- recentCommits,
5344
- hasUnpushed
5345
- };
5346
- }
5347
- function formatGitContextForPrompt(ctx) {
5348
- const lines = ["# Git Repository Status", ""];
5349
- lines.push(`- **Branch**: \`${ctx.branch}\``);
5350
- if (ctx.stagedFiles.length > 0) {
5351
- lines.push(`- **Staged** (${ctx.stagedFiles.length} files):`);
5352
- for (const f of ctx.stagedFiles.slice(0, 10)) {
5353
- lines.push(` - ${f}`);
5354
- }
5355
- if (ctx.stagedFiles.length > 10) {
5356
- lines.push(` - ... and ${ctx.stagedFiles.length - 10} more`);
5357
- }
5358
- }
5359
- if (ctx.changedFiles.length > 0) {
5360
- lines.push(`- **Modified** (${ctx.changedFiles.length} files):`);
5361
- for (const f of ctx.changedFiles.slice(0, 10)) {
5362
- lines.push(` - ${f}`);
5363
- }
5364
- if (ctx.changedFiles.length > 10) {
5365
- lines.push(` - ... and ${ctx.changedFiles.length - 10} more`);
5366
- }
5367
- }
5368
- if (ctx.stagedFiles.length === 0 && ctx.changedFiles.length === 0) {
5369
- lines.push("- **Working tree**: clean");
5370
- }
5371
- if (ctx.recentCommits.length > 0) {
5372
- lines.push("- **Recent commits**:");
5373
- for (const c of ctx.recentCommits) {
5374
- lines.push(` - ${c}`);
5375
- }
5376
- }
5377
- if (ctx.hasUnpushed) {
5378
- lines.push("- \u26A0\uFE0F Has unpushed commits");
5379
- }
5380
- return lines.join("\n");
5381
- }
5382
-
5383
5861
  // src/mcp/client.ts
5384
5862
  import { spawn as spawn2 } from "child_process";
5385
5863
  var McpClient = class {
@@ -5781,6 +6259,136 @@ var McpManager = class {
5781
6259
  }
5782
6260
  };
5783
6261
 
6262
+ // src/skills/manager.ts
6263
+ import { existsSync as existsSync16, readdirSync as readdirSync7, mkdirSync as mkdirSync10 } from "fs";
6264
+ import { join as join11 } from "path";
6265
+
6266
+ // src/skills/types.ts
6267
+ import { readFileSync as readFileSync11 } from "fs";
6268
+ import { basename as basename4 } from "path";
6269
+ function parseSimpleYaml(yaml) {
6270
+ const result = {};
6271
+ for (const line of yaml.split("\n")) {
6272
+ const match = line.match(/^(\w+)\s*:\s*(.+)$/);
6273
+ if (match) {
6274
+ result[match[1]] = match[2].trim();
6275
+ }
6276
+ }
6277
+ return result;
6278
+ }
6279
+ function parseYamlArray(value) {
6280
+ const bracketMatch = value.match(/^\[(.+)]$/);
6281
+ if (bracketMatch) {
6282
+ return bracketMatch[1].split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, ""));
6283
+ }
6284
+ return [value.trim()];
6285
+ }
6286
+ function parseSkillFile(filePath) {
6287
+ let raw;
6288
+ try {
6289
+ raw = readFileSync11(filePath, "utf-8");
6290
+ } catch {
6291
+ return null;
6292
+ }
6293
+ const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
6294
+ if (!frontmatterMatch) {
6295
+ return {
6296
+ meta: {
6297
+ name: basename4(filePath, ".md"),
6298
+ description: ""
6299
+ },
6300
+ content: raw.trim(),
6301
+ filePath
6302
+ };
6303
+ }
6304
+ const [, yaml, content] = frontmatterMatch;
6305
+ const parsed = parseSimpleYaml(yaml);
6306
+ return {
6307
+ meta: {
6308
+ name: parsed["name"] ?? basename4(filePath, ".md"),
6309
+ description: parsed["description"] ?? "",
6310
+ tools: parsed["tools"] ? parseYamlArray(parsed["tools"]) : void 0
6311
+ },
6312
+ content: content.trim(),
6313
+ filePath
6314
+ };
6315
+ }
6316
+
6317
+ // src/skills/manager.ts
6318
+ var SKILL_CONTENT_WARN_CHARS = 5e3;
6319
+ var SkillManager = class {
6320
+ skills = /* @__PURE__ */ new Map();
6321
+ activeSkill = null;
6322
+ skillsDir;
6323
+ constructor(skillsDir) {
6324
+ this.skillsDir = skillsDir;
6325
+ }
6326
+ /** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
6327
+ loadSkills() {
6328
+ this.skills.clear();
6329
+ if (!existsSync16(this.skillsDir)) {
6330
+ try {
6331
+ mkdirSync10(this.skillsDir, { recursive: true });
6332
+ } catch {
6333
+ }
6334
+ return 0;
6335
+ }
6336
+ let entries;
6337
+ try {
6338
+ entries = readdirSync7(this.skillsDir);
6339
+ } catch {
6340
+ return 0;
6341
+ }
6342
+ for (const entry of entries) {
6343
+ if (!entry.endsWith(".md")) continue;
6344
+ const filePath = join11(this.skillsDir, entry);
6345
+ const skill = parseSkillFile(filePath);
6346
+ if (skill) {
6347
+ this.skills.set(skill.meta.name, skill);
6348
+ if (skill.content.length > SKILL_CONTENT_WARN_CHARS) {
6349
+ process.stderr.write(
6350
+ `\u26A0 Skill '${skill.meta.name}' is ${skill.content.length} chars (>${SKILL_CONTENT_WARN_CHARS}), may consume significant context.
6351
+ `
6352
+ );
6353
+ }
6354
+ }
6355
+ }
6356
+ return this.skills.size;
6357
+ }
6358
+ /** 列出所有已加载的技能 */
6359
+ listSkills() {
6360
+ return [...this.skills.values()];
6361
+ }
6362
+ /** 按 name 激活技能。返回被激活的 Skill,未找到返回 null。 */
6363
+ activate(name) {
6364
+ const skill = this.skills.get(name);
6365
+ if (!skill) return null;
6366
+ this.activeSkill = skill;
6367
+ return skill;
6368
+ }
6369
+ /** 停用当前技能 */
6370
+ deactivate() {
6371
+ this.activeSkill = null;
6372
+ }
6373
+ /** 获取当前激活的技能(null 表示无激活技能) */
6374
+ getActive() {
6375
+ return this.activeSkill;
6376
+ }
6377
+ /** 获取激活技能的 prompt 内容(null 表示无激活技能) */
6378
+ getActivePromptContent() {
6379
+ return this.activeSkill?.content ?? null;
6380
+ }
6381
+ /** 获取激活技能的工具白名单 Set(null 表示无限制) */
6382
+ getActiveToolFilter() {
6383
+ if (!this.activeSkill?.meta.tools) return null;
6384
+ return new Set(this.activeSkill.meta.tools);
6385
+ }
6386
+ /** 获取技能目录路径 */
6387
+ getSkillsDir() {
6388
+ return this.skillsDir;
6389
+ }
6390
+ };
6391
+
5784
6392
  // src/repl/repl.ts
5785
6393
  var IMAGE_MIME = {
5786
6394
  ".png": "image/png",
@@ -5800,12 +6408,12 @@ function parseAtReferences(input2, cwd) {
5800
6408
  const absPath = resolve4(cwd, rawPath);
5801
6409
  const ext = extname3(rawPath).toLowerCase();
5802
6410
  const mime = IMAGE_MIME[ext];
5803
- if (!existsSync15(absPath)) {
6411
+ if (!existsSync17(absPath)) {
5804
6412
  refs.push({ path: rawPath, type: "notfound" });
5805
6413
  continue;
5806
6414
  }
5807
6415
  if (mime) {
5808
- const data = readFileSync10(absPath).toString("base64");
6416
+ const data = readFileSync12(absPath).toString("base64");
5809
6417
  imageParts.push({
5810
6418
  type: "image_url",
5811
6419
  image_url: { url: `data:${mime};base64,${data}` }
@@ -5813,7 +6421,7 @@ function parseAtReferences(input2, cwd) {
5813
6421
  refs.push({ path: rawPath, type: "image" });
5814
6422
  textBody = textBody.replace(match[0], "").trim();
5815
6423
  } else {
5816
- const content = readFileSync10(absPath, "utf-8");
6424
+ const content = readFileSync12(absPath, "utf-8");
5817
6425
  const inlined = `
5818
6426
 
5819
6427
  [File: ${rawPath}]
@@ -5883,6 +6491,8 @@ var Repl = class {
5883
6491
  * true 时:工具集过滤为只读工具,system prompt 注入规划说明,提示符加 [PLAN] 标记。
5884
6492
  */
5885
6493
  planMode = false;
6494
+ /** 技能管理器 */
6495
+ skillManager = null;
5886
6496
  /**
5887
6497
  * 交互式列表选择器进行中标志。
5888
6498
  * 与 toolExecutor.confirming 类似:主循环 line handler 在此为 true 时忽略 line 事件,
@@ -5895,9 +6505,9 @@ var Repl = class {
5895
6505
  */
5896
6506
  findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
5897
6507
  for (const candidate of candidates) {
5898
- const fullPath = join10(dir, candidate);
5899
- if (existsSync15(fullPath)) {
5900
- const content = readFileSync10(fullPath, "utf-8").trim();
6508
+ const fullPath = join12(dir, candidate);
6509
+ if (existsSync17(fullPath)) {
6510
+ const content = readFileSync12(fullPath, "utf-8").trim();
5901
6511
  if (content) return { filePath: fullPath, content };
5902
6512
  }
5903
6513
  }
@@ -5921,9 +6531,9 @@ var Repl = class {
5921
6531
  if (setting === false) return { layers: [], mergedContent: "" };
5922
6532
  const cwd = process.cwd();
5923
6533
  if (setting !== "auto") {
5924
- const fullPath = join10(cwd, setting);
5925
- if (existsSync15(fullPath)) {
5926
- const content = readFileSync10(fullPath, "utf-8").trim();
6534
+ const fullPath = join12(cwd, setting);
6535
+ if (existsSync17(fullPath)) {
6536
+ const content = readFileSync12(fullPath, "utf-8").trim();
5927
6537
  if (content) {
5928
6538
  const layer = {
5929
6539
  level: "project",
@@ -5980,9 +6590,9 @@ var Repl = class {
5980
6590
  * 超过 MEMORY_MAX_CHARS 时只取末尾最新部分。
5981
6591
  */
5982
6592
  loadMemoryContent() {
5983
- const memoryPath = join10(this.config.getConfigDir(), MEMORY_FILE_NAME);
5984
- if (!existsSync15(memoryPath)) return null;
5985
- let content = readFileSync10(memoryPath, "utf-8").trim();
6593
+ const memoryPath = join12(this.config.getConfigDir(), MEMORY_FILE_NAME);
6594
+ if (!existsSync17(memoryPath)) return null;
6595
+ let content = readFileSync12(memoryPath, "utf-8").trim();
5986
6596
  if (!content) return null;
5987
6597
  if (content.length > MEMORY_MAX_CHARS) {
5988
6598
  content = content.slice(-MEMORY_MAX_CHARS);
@@ -6036,6 +6646,13 @@ ${memory.content}`);
6036
6646
  if (this.activeSystemPrompt) {
6037
6647
  parts.push(this.activeSystemPrompt);
6038
6648
  }
6649
+ const skillContent = this.skillManager?.getActivePromptContent();
6650
+ if (skillContent) {
6651
+ const skillName = this.skillManager.getActive().meta.name;
6652
+ parts.push(`# Active Skill: ${skillName}
6653
+
6654
+ ${skillContent}`);
6655
+ }
6039
6656
  if (this.planMode) {
6040
6657
  parts.push(PLAN_MODE_SYSTEM_ADDON);
6041
6658
  }
@@ -6178,7 +6795,10 @@ ${response.content.trim()}
6178
6795
  }
6179
6796
  }
6180
6797
  refreshPrompt() {
6181
- const promptStr = this.planMode ? chalk10.green(`[${this.currentProvider}]`) + chalk10.yellow("[PLAN]") + chalk10.white(" > ") : chalk10.green(`[${this.currentProvider}]`) + chalk10.white(" > ");
6798
+ const activeSkill = this.skillManager?.getActive();
6799
+ const skillTag = activeSkill ? chalk10.magenta(`[${activeSkill.meta.name}]`) : "";
6800
+ const planTag = this.planMode ? chalk10.yellow("[PLAN]") : "";
6801
+ const promptStr = chalk10.green(`[${this.currentProvider}]`) + skillTag + planTag + chalk10.white(" > ");
6182
6802
  this.rl.setPrompt(promptStr);
6183
6803
  }
6184
6804
  showPrompt() {
@@ -6246,6 +6866,13 @@ ${response.content.trim()}
6246
6866
  }
6247
6867
  if (pluginCount > 0) {
6248
6868
  process.stdout.write(chalk10.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
6869
+ `));
6870
+ }
6871
+ const skillsDir = join12(this.config.getConfigDir(), SKILLS_DIR_NAME);
6872
+ this.skillManager = new SkillManager(skillsDir);
6873
+ const skillCount = this.skillManager.loadSkills();
6874
+ if (skillCount > 0) {
6875
+ process.stdout.write(chalk10.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
6249
6876
  `));
6250
6877
  }
6251
6878
  const mcpServers = this.config.get("mcpServers");
@@ -6474,7 +7101,17 @@ ${response.content.trim()}
6474
7101
  }
6475
7102
  async handleChatWithTools(provider, messages) {
6476
7103
  const session = this.sessions.current;
6477
- const toolDefs = this.planMode ? this.toolRegistry.getDefinitions().filter((t) => PLAN_MODE_READONLY_TOOLS.has(t.name)) : this.toolRegistry.getDefinitions();
7104
+ let toolDefs;
7105
+ if (this.planMode) {
7106
+ toolDefs = this.toolRegistry.getDefinitions().filter((t) => PLAN_MODE_READONLY_TOOLS.has(t.name));
7107
+ } else {
7108
+ const skillFilter = this.skillManager?.getActiveToolFilter();
7109
+ if (skillFilter) {
7110
+ toolDefs = this.toolRegistry.getDefinitions().filter((t) => skillFilter.has(t.name));
7111
+ } else {
7112
+ toolDefs = this.toolRegistry.getDefinitions();
7113
+ }
7114
+ }
6478
7115
  const apiMessages = [...messages];
6479
7116
  const extraMessages = [];
6480
7117
  const systemPrompt = this.buildCurrentSystemPrompt();
@@ -6666,6 +7303,7 @@ ${response.content.trim()}
6666
7303
  this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
6667
7304
  },
6668
7305
  getGitBranch: () => this.gitBranch,
7306
+ getLastResponse: () => lastResponseStore.content,
6669
7307
  runSetupWizard: async () => {
6670
7308
  const rlAny = this.rl;
6671
7309
  rlAny.output = process.stdout;
@@ -6687,6 +7325,22 @@ ${response.content.trim()}
6687
7325
  this.refreshPrompt();
6688
7326
  },
6689
7327
  getMcpManager: () => this.mcpManager,
7328
+ chatOnce: async (prompt, options) => {
7329
+ const provider = this.providers.get(this.currentProvider);
7330
+ const modelParams = this.getModelParams();
7331
+ const response = await provider.chat({
7332
+ messages: [{ role: "user", content: prompt, timestamp: /* @__PURE__ */ new Date() }],
7333
+ model: this.currentModel,
7334
+ systemPrompt: this.buildCurrentSystemPrompt(),
7335
+ stream: false,
7336
+ temperature: options?.temperature ?? 0.3,
7337
+ maxTokens: options?.maxTokens ?? modelParams.maxTokens ?? 4096,
7338
+ timeout: modelParams.timeout ?? 6e4
7339
+ });
7340
+ return response.content;
7341
+ },
7342
+ refreshPrompt: () => this.refreshPrompt(),
7343
+ getSkillManager: () => this.skillManager ?? null,
6690
7344
  exit: () => this.handleExit()
6691
7345
  };
6692
7346
  await cmd.execute(args, ctx);