jinzd-ai-cli 0.4.217 → 0.4.219

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 (30) hide show
  1. package/README.md +5 -2
  2. package/dist/{batch-YCOVKTXD.js → batch-CHAPYRVM.js} +2 -2
  3. package/dist/chunk-3I4RZYYO.js +638 -0
  4. package/dist/{chunk-KOPUCJXM.js → chunk-4ZN6L6K5.js} +3 -3
  5. package/dist/{chunk-VTH7BLXK.js → chunk-64NUW3WL.js} +1 -1
  6. package/dist/{chunk-QAYOI57M.js → chunk-EGMORH5W.js} +1 -1
  7. package/dist/{chunk-MGBMNCHG.js → chunk-GEN4O5QH.js} +94 -270
  8. package/dist/chunk-GGKQHPB3.js +604 -0
  9. package/dist/{chunk-WKOQ5CYC.js → chunk-KIEZBTVD.js} +1 -1
  10. package/dist/{chunk-OUAZOE5U.js → chunk-QUFYBR6Q.js} +47 -206
  11. package/dist/{chunk-XJGEQIYS.js → chunk-U7KVU25H.js} +1 -1
  12. package/dist/{chunk-WZ3VKLF3.js → chunk-VADHBW7T.js} +1 -1
  13. package/dist/{ci-52RZIYWB.js → ci-QPP66T53.js} +4 -4
  14. package/dist/{ci-format-73UXKE65.js → ci-format-5S3EEYRK.js} +2 -2
  15. package/dist/{constants-DIXAD35W.js → constants-FJOLBABC.js} +1 -1
  16. package/dist/doctor-cli-KB2LUDF3.js +17 -0
  17. package/dist/electron-server.js +921 -274
  18. package/dist/{hub-GIGBITZN.js → hub-DIM7SKKY.js} +1 -1
  19. package/dist/index.js +192 -200
  20. package/dist/{pr-KPQ5RPKB.js → pr-YQGH72N6.js} +4 -4
  21. package/dist/{run-tests-ZDSA3QES.js → run-tests-GEZSSNJM.js} +2 -2
  22. package/dist/{run-tests-K7QR5QN4.js → run-tests-O76JIBCW.js} +1 -1
  23. package/dist/{server-QT3SC2KI.js → server-5E2AIXVX.js} +108 -80
  24. package/dist/{server-22YF3U34.js → server-Z6O3G2LY.js} +9 -9
  25. package/dist/{task-orchestrator-BHQQCVTY.js → task-orchestrator-5HBW4O64.js} +9 -9
  26. package/dist/{usage-WZZFSFLM.js → usage-BMK6M5U3.js} +2 -2
  27. package/package.json +1 -1
  28. package/dist/chunk-524WZOKS.js +0 -166
  29. package/dist/chunk-SNJAOXFT.js +0 -129
  30. package/dist/doctor-cli-7GOQPULZ.js +0 -226
@@ -38,7 +38,7 @@ import {
38
38
  VERSION,
39
39
  buildUserIdentityPrompt,
40
40
  runTestsTool
41
- } from "./chunk-QAYOI57M.js";
41
+ } from "./chunk-EGMORH5W.js";
42
42
  import {
43
43
  hasSemanticIndex,
44
44
  semanticSearch
@@ -64,8 +64,8 @@ import {
64
64
  import express from "express";
65
65
  import { createServer } from "http";
66
66
  import { WebSocketServer } from "ws";
67
- import { join as join22, dirname as dirname7, resolve as resolve10, relative as relative4, sep as sep3 } from "path";
68
- import { existsSync as existsSync27, readFileSync as readFileSync21, readdirSync as readdirSync13, statSync as statSync9, realpathSync } from "fs";
67
+ import { join as join25, dirname as dirname8, resolve as resolve11, relative as relative4, sep as sep3 } from "path";
68
+ import { existsSync as existsSync30, readFileSync as readFileSync23, readdirSync as readdirSync15, statSync as statSync12, realpathSync } from "fs";
69
69
  import { networkInterfaces } from "os";
70
70
 
71
71
  // src/config/config-manager.ts
@@ -1847,12 +1847,12 @@ function findPhantomClaims(content, extraMessages) {
1847
1847
  const claimed = extractClaimedFilePaths(content);
1848
1848
  if (claimed.length === 0) return [];
1849
1849
  const normalize = (p) => p.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
1850
- const basename6 = (p) => {
1850
+ const basename7 = (p) => {
1851
1851
  const parts = normalize(p).split("/");
1852
1852
  return parts[parts.length - 1] ?? "";
1853
1853
  };
1854
1854
  const written = extractWrittenFilePaths(extraMessages).map(normalize);
1855
- const writtenBases = new Set(written.map(basename6));
1855
+ const writtenBases = new Set(written.map(basename7));
1856
1856
  const writtenFull = new Set(written);
1857
1857
  const bashText = extractBashCommands(extraMessages).map((c) => c.replace(/\\/g, "/").toLowerCase()).join("\n");
1858
1858
  return claimed.filter((raw) => {
@@ -1861,8 +1861,8 @@ function findPhantomClaims(content, extraMessages) {
1861
1861
  for (const w of writtenFull) {
1862
1862
  if (w.endsWith("/" + norm2) || norm2.endsWith("/" + w)) return false;
1863
1863
  }
1864
- if (writtenBases.has(basename6(norm2))) return false;
1865
- const base = basename6(norm2);
1864
+ if (writtenBases.has(basename7(norm2))) return false;
1865
+ const base = basename7(norm2);
1866
1866
  if (base && bashText.includes(base)) return false;
1867
1867
  return true;
1868
1868
  });
@@ -6629,6 +6629,21 @@ function flush() {
6629
6629
  } catch {
6630
6630
  }
6631
6631
  }
6632
+ function getStatsSnapshot() {
6633
+ const s = ensureLoaded();
6634
+ return Object.values(s.entries).map((e) => ({ ...e }));
6635
+ }
6636
+ function getTopFailingTools(limit = 5) {
6637
+ return getStatsSnapshot().filter((e) => e.failures > 0).sort((a, b) => {
6638
+ const rateA = a.failures / Math.max(1, a.calls);
6639
+ const rateB = b.failures / Math.max(1, b.calls);
6640
+ if (rateB !== rateA) return rateB - rateA;
6641
+ return b.failures - a.failures;
6642
+ }).slice(0, limit);
6643
+ }
6644
+ function getTopUsedTools(limit = 5) {
6645
+ return getStatsSnapshot().sort((a, b) => b.calls - a.calls).slice(0, limit);
6646
+ }
6632
6647
 
6633
6648
  // src/tools/action-classifier.ts
6634
6649
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
@@ -7182,7 +7197,7 @@ var ToolExecutor = class {
7182
7197
  rl.resume();
7183
7198
  process.stdout.write(prompt);
7184
7199
  this.confirming = true;
7185
- return new Promise((resolve11) => {
7200
+ return new Promise((resolve12) => {
7186
7201
  let completed = false;
7187
7202
  const cleanup = (result) => {
7188
7203
  if (completed) return;
@@ -7192,7 +7207,7 @@ var ToolExecutor = class {
7192
7207
  rl.pause();
7193
7208
  rlAny.output = savedOutput;
7194
7209
  this.confirming = false;
7195
- resolve11(result);
7210
+ resolve12(result);
7196
7211
  };
7197
7212
  const onLine = (line) => {
7198
7213
  const trimmed = line.trim();
@@ -7366,7 +7381,7 @@ var ToolExecutor = class {
7366
7381
  rl.resume();
7367
7382
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
7368
7383
  this.confirming = true;
7369
- return new Promise((resolve11) => {
7384
+ return new Promise((resolve12) => {
7370
7385
  let completed = false;
7371
7386
  const cleanup = (answer) => {
7372
7387
  if (completed) return;
@@ -7376,7 +7391,7 @@ var ToolExecutor = class {
7376
7391
  rl.pause();
7377
7392
  rlAny.output = savedOutput;
7378
7393
  this.confirming = false;
7379
- resolve11(answer === "y");
7394
+ resolve12(answer === "y");
7380
7395
  };
7381
7396
  const onLine = (line) => {
7382
7397
  const trimmed = line.trim();
@@ -8625,7 +8640,7 @@ var runInteractiveTool = {
8625
8640
  PYTHONDONTWRITEBYTECODE: "1"
8626
8641
  };
8627
8642
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
8628
- return new Promise((resolve11) => {
8643
+ return new Promise((resolve12) => {
8629
8644
  const child = spawn2(executable, cmdArgs.map(String), {
8630
8645
  cwd: process.cwd(),
8631
8646
  env,
@@ -8658,22 +8673,22 @@ var runInteractiveTool = {
8658
8673
  setTimeout(writeNextLine, 400);
8659
8674
  const timer = setTimeout(() => {
8660
8675
  child.kill();
8661
- resolve11(`${prefixWarnings}[Timeout after ${timeout}ms]
8676
+ resolve12(`${prefixWarnings}[Timeout after ${timeout}ms]
8662
8677
  ${buildOutput(stdout, stderr)}`);
8663
8678
  }, timeout);
8664
8679
  child.on("close", (code) => {
8665
8680
  clearTimeout(timer);
8666
8681
  const output = buildOutput(stdout, stderr);
8667
8682
  if (code !== 0 && code !== null) {
8668
- resolve11(`${prefixWarnings}Exit code ${code}:
8683
+ resolve12(`${prefixWarnings}Exit code ${code}:
8669
8684
  ${output}`);
8670
8685
  } else {
8671
- resolve11(`${prefixWarnings}${output || "(no output)"}`);
8686
+ resolve12(`${prefixWarnings}${output || "(no output)"}`);
8672
8687
  }
8673
8688
  });
8674
8689
  child.on("error", (err) => {
8675
8690
  clearTimeout(timer);
8676
- resolve11(
8691
+ resolve12(
8677
8692
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
8678
8693
  Hint: On Windows, use the full path to the executable, e.g.:
8679
8694
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -9723,7 +9738,7 @@ function promptUser(rl, question) {
9723
9738
  console.log();
9724
9739
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
9725
9740
  process.stdout.write(chalk4.cyan("> "));
9726
- return new Promise((resolve11) => {
9741
+ return new Promise((resolve12) => {
9727
9742
  let completed = false;
9728
9743
  const cleanup = (answer) => {
9729
9744
  if (completed) return;
@@ -9733,7 +9748,7 @@ function promptUser(rl, question) {
9733
9748
  rl.pause();
9734
9749
  rlAny.output = savedOutput;
9735
9750
  askUserContext.prompting = false;
9736
- resolve11(answer);
9751
+ resolve12(answer);
9737
9752
  };
9738
9753
  const onLine = (line) => {
9739
9754
  cleanup(line);
@@ -9965,9 +9980,292 @@ function formatResults2(query, data, _requested) {
9965
9980
  }
9966
9981
 
9967
9982
  // src/agents/agent-config.ts
9968
- import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync12 } from "fs";
9969
- import { join as join12 } from "path";
9983
+ import { existsSync as existsSync17, readdirSync as readdirSync8, readFileSync as readFileSync13 } from "fs";
9984
+ import { join as join13 } from "path";
9970
9985
  import { homedir as homedir6 } from "os";
9986
+
9987
+ // src/plugins/plugin-manager.ts
9988
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync12, cpSync, rmSync, statSync as statSync6 } from "fs";
9989
+ import { basename as basename5, dirname as dirname6, join as join12, resolve as resolve7 } from "path";
9990
+ import { createHash as createHash3 } from "crypto";
9991
+ import { z as z2 } from "zod";
9992
+ var MANIFEST_RELATIVE = ".aicli-plugin/plugin.json";
9993
+ var STATE_FILE = "plugin-state.json";
9994
+ var HookCommandSchema = z2.union([
9995
+ z2.string(),
9996
+ z2.object({
9997
+ command: z2.string(),
9998
+ source: z2.enum(["user", "project", "managed"]).optional(),
9999
+ description: z2.string().optional(),
10000
+ required: z2.boolean().optional(),
10001
+ timeoutMs: z2.number().int().min(100).max(3e4).optional(),
10002
+ disabled: z2.boolean().optional()
10003
+ }),
10004
+ z2.array(z2.union([
10005
+ z2.string(),
10006
+ z2.object({
10007
+ command: z2.string(),
10008
+ source: z2.enum(["user", "project", "managed"]).optional(),
10009
+ description: z2.string().optional(),
10010
+ required: z2.boolean().optional(),
10011
+ timeoutMs: z2.number().int().min(100).max(3e4).optional(),
10012
+ disabled: z2.boolean().optional()
10013
+ })
10014
+ ]))
10015
+ ]);
10016
+ var HookEventNameSchema = z2.enum([
10017
+ "SessionStart",
10018
+ "UserPromptSubmit",
10019
+ "PreToolUse",
10020
+ "PermissionRequest",
10021
+ "PostToolUse",
10022
+ "PreCompact",
10023
+ "PostCompact",
10024
+ "Stop",
10025
+ "SubagentStart",
10026
+ "SubagentStop"
10027
+ ]);
10028
+ var McpServerSchema = z2.object({
10029
+ command: z2.string(),
10030
+ args: z2.array(z2.string()).default([]),
10031
+ env: z2.record(z2.string()).optional(),
10032
+ timeout: z2.number().default(3e4)
10033
+ });
10034
+ var PluginManifestSchema = z2.object({
10035
+ name: z2.string().regex(/^[a-zA-Z0-9._-]{1,80}$/),
10036
+ version: z2.string().min(1),
10037
+ description: z2.string().optional(),
10038
+ skills: z2.array(z2.string()).default([]),
10039
+ hooks: z2.object({
10040
+ enabled: z2.boolean().optional(),
10041
+ events: z2.record(HookEventNameSchema, HookCommandSchema).default({})
10042
+ }).default({ events: {} }),
10043
+ commands: z2.array(z2.string()).default([]),
10044
+ mcpServers: z2.record(McpServerSchema).default({}),
10045
+ agents: z2.array(z2.string()).default([]),
10046
+ permissionHints: z2.array(z2.string()).default([])
10047
+ }).strict();
10048
+ function pluginRoot(configDir) {
10049
+ return join12(configDir, PLUGINS_DIR_NAME);
10050
+ }
10051
+ function pluginManifestPath(pluginDir) {
10052
+ return join12(pluginDir, MANIFEST_RELATIVE);
10053
+ }
10054
+ function statePath(configDir) {
10055
+ return join12(pluginRoot(configDir), STATE_FILE);
10056
+ }
10057
+ function loadState(configDir) {
10058
+ const file = statePath(configDir);
10059
+ if (!existsSync16(file)) return { version: 1, plugins: [] };
10060
+ try {
10061
+ const parsed = JSON.parse(readFileSync12(file, "utf-8"));
10062
+ return { version: 1, plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [] };
10063
+ } catch {
10064
+ return { version: 1, plugins: [] };
10065
+ }
10066
+ }
10067
+ function saveState(configDir, state2) {
10068
+ mkdirSync8(pluginRoot(configDir), { recursive: true });
10069
+ atomicWriteFileSync(statePath(configDir), JSON.stringify(state2, null, 2));
10070
+ }
10071
+ function hashPluginManifest(manifestPath) {
10072
+ return createHash3("sha256").update(readFileSync12(manifestPath)).digest("hex");
10073
+ }
10074
+ function readPluginManifest(manifestPath) {
10075
+ return PluginManifestSchema.parse(JSON.parse(readFileSync12(manifestPath, "utf-8")));
10076
+ }
10077
+ function findPluginSourceDir(inputPath) {
10078
+ const abs = resolve7(inputPath);
10079
+ const direct = statSync6(abs).isDirectory() ? abs : dirname6(abs);
10080
+ if (existsSync16(pluginManifestPath(direct))) return direct;
10081
+ const nested = join12(direct, ".aicli-plugin");
10082
+ if (existsSync16(join12(nested, "plugin.json"))) return direct;
10083
+ throw new Error(`plugin manifest not found: ${MANIFEST_RELATIVE}`);
10084
+ }
10085
+ function installPlugin(configDir, inputPath) {
10086
+ const sourceDir = findPluginSourceDir(inputPath);
10087
+ const manifest = readPluginManifest(pluginManifestPath(sourceDir));
10088
+ const targetDir = join12(pluginRoot(configDir), manifest.name);
10089
+ mkdirSync8(pluginRoot(configDir), { recursive: true });
10090
+ if (existsSync16(targetDir)) rmSync(targetDir, { recursive: true, force: true });
10091
+ cpSync(sourceDir, targetDir, { recursive: true });
10092
+ const manifestPath = pluginManifestPath(targetDir);
10093
+ const hash = hashPluginManifest(manifestPath);
10094
+ const state2 = loadState(configDir);
10095
+ const previous = state2.plugins.find((p) => p.name === manifest.name);
10096
+ const next = {
10097
+ name: manifest.name,
10098
+ enabled: previous?.enabled ?? false,
10099
+ trusted: false,
10100
+ hash,
10101
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
10102
+ };
10103
+ state2.plugins = [...state2.plugins.filter((p) => p.name !== manifest.name), next];
10104
+ saveState(configDir, state2);
10105
+ return { name: manifest.name, dir: targetDir, manifestPath, manifest, hash, enabled: next.enabled, trusted: false, valid: true };
10106
+ }
10107
+ function setPluginEnabled(configDir, name, enabled) {
10108
+ const plugin = getInstalledPlugin(configDir, name);
10109
+ if (!plugin.valid) throw new Error(plugin.error ?? `invalid plugin: ${name}`);
10110
+ const state2 = loadState(configDir);
10111
+ const current = state2.plugins.find((p) => p.name === plugin.name) ?? {
10112
+ name: plugin.name,
10113
+ enabled: false,
10114
+ trusted: false,
10115
+ hash: plugin.hash,
10116
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
10117
+ };
10118
+ const next = { ...current, enabled, hash: plugin.hash, enabledAt: enabled ? (/* @__PURE__ */ new Date()).toISOString() : current.enabledAt };
10119
+ state2.plugins = [...state2.plugins.filter((p) => p.name !== plugin.name), next];
10120
+ saveState(configDir, state2);
10121
+ return next;
10122
+ }
10123
+ function trustPlugin(configDir, name) {
10124
+ const plugin = getInstalledPlugin(configDir, name);
10125
+ if (!plugin.valid) throw new Error(plugin.error ?? `invalid plugin: ${name}`);
10126
+ const state2 = loadState(configDir);
10127
+ const current = state2.plugins.find((p) => p.name === plugin.name) ?? {
10128
+ name: plugin.name,
10129
+ enabled: false,
10130
+ trusted: false,
10131
+ hash: plugin.hash,
10132
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
10133
+ };
10134
+ const next = { ...current, trusted: true, hash: plugin.hash, trustedAt: (/* @__PURE__ */ new Date()).toISOString() };
10135
+ state2.plugins = [...state2.plugins.filter((p) => p.name !== plugin.name), next];
10136
+ saveState(configDir, state2);
10137
+ return next;
10138
+ }
10139
+ function getInstalledPlugin(configDir, name) {
10140
+ const plugins = listInstalledPlugins(configDir);
10141
+ const matches = plugins.filter((p) => p.name === name || p.name.startsWith(name));
10142
+ if (matches.length === 0) throw new Error(`plugin not found: ${name}`);
10143
+ if (matches.length > 1) throw new Error(`plugin name is ambiguous: ${name}`);
10144
+ return matches[0];
10145
+ }
10146
+ function listInstalledPlugins(configDir) {
10147
+ const root = pluginRoot(configDir);
10148
+ if (!existsSync16(root)) return [];
10149
+ const state2 = loadState(configDir);
10150
+ const out = [];
10151
+ for (const entry of readdirSync7(root)) {
10152
+ const dir = join12(root, entry);
10153
+ try {
10154
+ if (!statSync6(dir).isDirectory()) continue;
10155
+ } catch {
10156
+ continue;
10157
+ }
10158
+ const manifestPath = pluginManifestPath(dir);
10159
+ if (!existsSync16(manifestPath)) continue;
10160
+ try {
10161
+ const manifest = readPluginManifest(manifestPath);
10162
+ const hash = hashPluginManifest(manifestPath);
10163
+ const st = state2.plugins.find((p) => p.name === manifest.name);
10164
+ const trusted = st?.trusted === true && st.hash === hash;
10165
+ out.push({
10166
+ name: manifest.name,
10167
+ dir,
10168
+ manifestPath,
10169
+ manifest,
10170
+ hash,
10171
+ enabled: st?.enabled === true,
10172
+ trusted,
10173
+ valid: true
10174
+ });
10175
+ } catch (err) {
10176
+ out.push({
10177
+ name: entry,
10178
+ dir,
10179
+ manifestPath,
10180
+ manifest: { name: entry, version: "invalid", skills: [], commands: [], agents: [], hooks: { events: {} }, mcpServers: {}, permissionHints: [] },
10181
+ hash: "",
10182
+ enabled: false,
10183
+ trusted: false,
10184
+ valid: false,
10185
+ error: err instanceof Error ? err.message : String(err)
10186
+ });
10187
+ }
10188
+ }
10189
+ return out.sort((a, b) => a.name.localeCompare(b.name));
10190
+ }
10191
+ function activePlugins(configDir) {
10192
+ return listInstalledPlugins(configDir).filter((p) => p.valid && p.enabled && p.trusted);
10193
+ }
10194
+ function uniqueDirs(plugin, rels) {
10195
+ const dirs = /* @__PURE__ */ new Set();
10196
+ for (const rel of rels) {
10197
+ const abs = resolve7(plugin.dir, rel);
10198
+ if (!abs.startsWith(resolve7(plugin.dir))) continue;
10199
+ if (existsSync16(abs)) dirs.add(statSync6(abs).isDirectory() ? abs : dirname6(abs));
10200
+ }
10201
+ return [...dirs];
10202
+ }
10203
+ function prefixCommand(plugin, command) {
10204
+ return command.replaceAll("{pluginDir}", plugin.dir).replaceAll("$PLUGIN_DIR", plugin.dir);
10205
+ }
10206
+ function pluginHooks(plugin) {
10207
+ const events = plugin.manifest.hooks?.events ?? {};
10208
+ const out = {};
10209
+ for (const [event, raw] of Object.entries(events)) {
10210
+ const normalize = (item) => {
10211
+ if (typeof item === "string") return { command: prefixCommand(plugin, item), source: "project", description: `plugin:${plugin.name}` };
10212
+ if (item && typeof item === "object") {
10213
+ const obj = item;
10214
+ return { ...obj, command: prefixCommand(plugin, String(obj.command ?? "")), source: "project", description: obj.description ?? `plugin:${plugin.name}` };
10215
+ }
10216
+ return item;
10217
+ };
10218
+ out[event] = Array.isArray(raw) ? raw.map(normalize) : normalize(raw);
10219
+ }
10220
+ return Object.keys(out).length > 0 ? { enabled: plugin.manifest.hooks?.enabled ?? true, events: out } : void 0;
10221
+ }
10222
+ function mergePluginHooks(base, configDir) {
10223
+ const plugins = activePlugins(configDir);
10224
+ if (plugins.length === 0) return base;
10225
+ const merged = { ...base ?? {}, enabled: base?.enabled ?? true, events: { ...base?.events ?? {} } };
10226
+ for (const plugin of plugins) {
10227
+ const hooks = pluginHooks(plugin);
10228
+ if (!hooks?.events) continue;
10229
+ for (const [event, entry] of Object.entries(hooks.events)) {
10230
+ const key = event;
10231
+ const existing = merged.events?.[key];
10232
+ const existingItems = existing === void 0 ? [] : Array.isArray(existing) ? existing : [existing];
10233
+ const newItems = Array.isArray(entry) ? entry : [entry];
10234
+ merged.events[key] = [...existingItems, ...newItems];
10235
+ }
10236
+ }
10237
+ return merged;
10238
+ }
10239
+ function getActivePluginAssets(configDir) {
10240
+ const plugins = activePlugins(configDir);
10241
+ const mcpServers = {};
10242
+ const skillDirs = [];
10243
+ const commandDirs = [];
10244
+ const agentDirs = [];
10245
+ for (const plugin of plugins) {
10246
+ skillDirs.push(...uniqueDirs(plugin, plugin.manifest.skills));
10247
+ commandDirs.push(...uniqueDirs(plugin, plugin.manifest.commands));
10248
+ agentDirs.push(...uniqueDirs(plugin, plugin.manifest.agents));
10249
+ for (const [serverName, server] of Object.entries(plugin.manifest.mcpServers)) {
10250
+ mcpServers[`plugin_${plugin.name}_${serverName}`] = server;
10251
+ }
10252
+ }
10253
+ return { plugins, skillDirs, commandDirs, agentDirs, hooks: mergePluginHooks(void 0, configDir), mcpServers };
10254
+ }
10255
+ function describePlugin(plugin) {
10256
+ const m = plugin.manifest;
10257
+ return [
10258
+ `${plugin.name}@${m.version}`,
10259
+ ` status: ${plugin.enabled ? "enabled" : "disabled"} \xB7 ${plugin.trusted ? "trusted" : "untrusted"}${plugin.valid ? "" : " \xB7 invalid"}`,
10260
+ ` dir: ${plugin.dir}`,
10261
+ m.description ? ` description: ${m.description}` : void 0,
10262
+ ` assets: ${m.skills.length} skill(s), ${m.commands.length} command(s), ${m.agents.length} agent(s), ${Object.keys(m.mcpServers).length} MCP server(s), ${Object.keys(m.hooks?.events ?? {}).length} hook event(s)`,
10263
+ m.permissionHints.length ? ` permission hints: ${m.permissionHints.join("; ")}` : void 0,
10264
+ plugin.error ? ` error: ${plugin.error}` : void 0
10265
+ ].filter((line) => Boolean(line));
10266
+ }
10267
+
10268
+ // src/agents/agent-config.ts
9971
10269
  var READ_ONLY_TOOLS2 = /* @__PURE__ */ new Set([
9972
10270
  "read_file",
9973
10271
  "list_dir",
@@ -10061,13 +10359,13 @@ function parseAgentConfig(raw, source, path3) {
10061
10359
  return cfg;
10062
10360
  }
10063
10361
  function loadAgentDir(dir, source) {
10064
- if (!existsSync16(dir)) return [];
10362
+ if (!existsSync17(dir)) return [];
10065
10363
  const out = [];
10066
- for (const file of readdirSync7(dir)) {
10364
+ for (const file of readdirSync8(dir)) {
10067
10365
  if (!file.endsWith(".json")) continue;
10068
- const path3 = join12(dir, file);
10366
+ const path3 = join13(dir, file);
10069
10367
  try {
10070
- const parsed = JSON.parse(readFileSync12(path3, "utf-8"));
10368
+ const parsed = JSON.parse(readFileSync13(path3, "utf-8"));
10071
10369
  const cfg = parseAgentConfig(parsed, source, path3);
10072
10370
  if (cfg) out.push(cfg);
10073
10371
  } catch {
@@ -10077,8 +10375,8 @@ function loadAgentDir(dir, source) {
10077
10375
  }
10078
10376
  function getAgentDirs(configDir, cwd = process.cwd()) {
10079
10377
  return {
10080
- user: join12(configDir ?? join12(homedir6(), CONFIG_DIR_NAME), "agents"),
10081
- project: join12(cwd, CONFIG_DIR_NAME, "agents")
10378
+ user: join13(configDir ?? join13(homedir6(), CONFIG_DIR_NAME), "agents"),
10379
+ project: join13(cwd, CONFIG_DIR_NAME, "agents")
10082
10380
  };
10083
10381
  }
10084
10382
  function listAgentConfigs(configDir, cwd = process.cwd()) {
@@ -10086,6 +10384,11 @@ function listAgentConfigs(configDir, cwd = process.cwd()) {
10086
10384
  const byName = /* @__PURE__ */ new Map();
10087
10385
  for (const cfg of BUILTIN_AGENTS) byName.set(normalizeName(cfg.name), { ...cfg });
10088
10386
  for (const cfg of loadAgentDir(dirs.user, "user")) byName.set(normalizeName(cfg.name), cfg);
10387
+ if (configDir) {
10388
+ for (const dir of getActivePluginAssets(configDir).agentDirs) {
10389
+ for (const cfg of loadAgentDir(dir, "plugin")) byName.set(normalizeName(cfg.name), cfg);
10390
+ }
10391
+ }
10089
10392
  for (const cfg of loadAgentDir(dirs.project, "project")) byName.set(normalizeName(cfg.name), cfg);
10090
10393
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
10091
10394
  }
@@ -10788,14 +11091,14 @@ var taskStopTool = {
10788
11091
 
10789
11092
  // src/tools/builtin/git-tools.ts
10790
11093
  import { execFileSync as execFileSync2 } from "child_process";
10791
- import { existsSync as existsSync17 } from "fs";
10792
- import { join as join13 } from "path";
11094
+ import { existsSync as existsSync18 } from "fs";
11095
+ import { join as join14 } from "path";
10793
11096
  function assertGitRepo(cwd) {
10794
11097
  let dir = cwd;
10795
11098
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
10796
11099
  while (dir && dir !== root) {
10797
- if (existsSync17(join13(dir, ".git"))) return;
10798
- const parent = join13(dir, "..");
11100
+ if (existsSync18(join14(dir, ".git"))) return;
11101
+ const parent = join14(dir, "..");
10799
11102
  if (parent === dir) break;
10800
11103
  dir = parent;
10801
11104
  }
@@ -11060,9 +11363,9 @@ ${commitOutput.trim()}`;
11060
11363
  };
11061
11364
 
11062
11365
  // src/tools/builtin/notebook-edit.ts
11063
- import { readFileSync as readFileSync13, existsSync as existsSync18 } from "fs";
11366
+ import { readFileSync as readFileSync14, existsSync as existsSync19 } from "fs";
11064
11367
  import { writeFile } from "fs/promises";
11065
- import { resolve as resolve7, extname as extname2 } from "path";
11368
+ import { resolve as resolve8, extname as extname2 } from "path";
11066
11369
  var notebookEditTool = {
11067
11370
  definition: {
11068
11371
  name: "notebook_edit",
@@ -11111,14 +11414,14 @@ var notebookEditTool = {
11111
11414
  if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
11112
11415
  throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
11113
11416
  }
11114
- const absPath = resolve7(filePath);
11417
+ const absPath = resolve8(filePath);
11115
11418
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
11116
11419
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
11117
11420
  }
11118
- if (!existsSync18(absPath)) {
11421
+ if (!existsSync19(absPath)) {
11119
11422
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
11120
11423
  }
11121
- const raw = readFileSync13(absPath, "utf-8");
11424
+ const raw = readFileSync14(absPath, "utf-8");
11122
11425
  const nb = parseNotebook(raw);
11123
11426
  const cellIdx0 = cellIndexRaw - 1;
11124
11427
  undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
@@ -11535,8 +11838,8 @@ function estimateToolDefinitionTokens(def) {
11535
11838
 
11536
11839
  // src/tools/registry.ts
11537
11840
  import { pathToFileURL } from "url";
11538
- import { existsSync as existsSync19, mkdirSync as mkdirSync8, readdirSync as readdirSync8 } from "fs";
11539
- import { join as join14 } from "path";
11841
+ import { existsSync as existsSync20, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
11842
+ import { join as join15 } from "path";
11540
11843
  var ToolRegistry = class {
11541
11844
  tools = /* @__PURE__ */ new Map();
11542
11845
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -11697,16 +12000,16 @@ var ToolRegistry = class {
11697
12000
  * Returns the number of successfully loaded plugins.
11698
12001
  */
11699
12002
  async loadPlugins(pluginsDir, allowPlugins = false) {
11700
- if (!existsSync19(pluginsDir)) {
12003
+ if (!existsSync20(pluginsDir)) {
11701
12004
  try {
11702
- mkdirSync8(pluginsDir, { recursive: true });
12005
+ mkdirSync9(pluginsDir, { recursive: true });
11703
12006
  } catch {
11704
12007
  }
11705
12008
  return 0;
11706
12009
  }
11707
12010
  let files;
11708
12011
  try {
11709
- files = readdirSync8(pluginsDir).filter((f) => f.endsWith(".js"));
12012
+ files = readdirSync9(pluginsDir).filter((f) => f.endsWith(".js"));
11710
12013
  } catch {
11711
12014
  return 0;
11712
12015
  }
@@ -11723,12 +12026,12 @@ var ToolRegistry = class {
11723
12026
  process.stderr.write(
11724
12027
  `
11725
12028
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
11726
- ` + files.map((f) => ` + ${join14(pluginsDir, f)}`).join("\n") + "\n\n"
12029
+ ` + files.map((f) => ` + ${join15(pluginsDir, f)}`).join("\n") + "\n\n"
11727
12030
  );
11728
12031
  let loaded = 0;
11729
12032
  for (const file of files) {
11730
12033
  try {
11731
- const fileUrl = pathToFileURL(join14(pluginsDir, file)).href;
12034
+ const fileUrl = pathToFileURL(join15(pluginsDir, file)).href;
11732
12035
  const mod = await import(fileUrl);
11733
12036
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
11734
12037
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -11890,7 +12193,7 @@ var McpClient = class {
11890
12193
  // 内部方法:JSON-RPC 通信
11891
12194
  // ══════════════════════════════════════════════════════════════════
11892
12195
  sendRequest(method, params) {
11893
- return new Promise((resolve11, reject) => {
12196
+ return new Promise((resolve12, reject) => {
11894
12197
  if (!this.process?.stdin?.writable) {
11895
12198
  return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
11896
12199
  }
@@ -11914,7 +12217,7 @@ var McpClient = class {
11914
12217
  this.pendingRequests.set(id, {
11915
12218
  resolve: (result) => {
11916
12219
  cleanup();
11917
- resolve11(result);
12220
+ resolve12(result);
11918
12221
  },
11919
12222
  reject: (error) => {
11920
12223
  cleanup();
@@ -11991,13 +12294,13 @@ var McpClient = class {
11991
12294
  }
11992
12295
  /** Promise 超时包装 */
11993
12296
  withTimeout(promise, ms, label) {
11994
- return new Promise((resolve11, reject) => {
12297
+ return new Promise((resolve12, reject) => {
11995
12298
  const timer = setTimeout(() => {
11996
12299
  reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
11997
12300
  }, ms);
11998
12301
  promise.then((val) => {
11999
12302
  clearTimeout(timer);
12000
- resolve11(val);
12303
+ resolve12(val);
12001
12304
  }).catch((err) => {
12002
12305
  clearTimeout(timer);
12003
12306
  reject(err);
@@ -12274,12 +12577,12 @@ var McpManager = class {
12274
12577
  };
12275
12578
 
12276
12579
  // src/skills/manager.ts
12277
- import { existsSync as existsSync20, readdirSync as readdirSync9, mkdirSync as mkdirSync9, statSync as statSync6 } from "fs";
12278
- import { join as join15 } from "path";
12580
+ import { existsSync as existsSync21, readdirSync as readdirSync10, mkdirSync as mkdirSync10, statSync as statSync7 } from "fs";
12581
+ import { join as join16 } from "path";
12279
12582
 
12280
12583
  // src/skills/types.ts
12281
- import { readFileSync as readFileSync14 } from "fs";
12282
- import { basename as basename5 } from "path";
12584
+ import { readFileSync as readFileSync15 } from "fs";
12585
+ import { basename as basename6 } from "path";
12283
12586
  function parseSimpleYaml(yaml) {
12284
12587
  const result = {};
12285
12588
  for (const line of yaml.split("\n")) {
@@ -12300,7 +12603,7 @@ function parseYamlArray(value) {
12300
12603
  function parseSkillFile(filePath) {
12301
12604
  let raw;
12302
12605
  try {
12303
- raw = readFileSync14(filePath, "utf-8");
12606
+ raw = readFileSync15(filePath, "utf-8");
12304
12607
  } catch {
12305
12608
  return null;
12306
12609
  }
@@ -12308,7 +12611,7 @@ function parseSkillFile(filePath) {
12308
12611
  if (!frontmatterMatch) {
12309
12612
  return {
12310
12613
  meta: {
12311
- name: basename5(filePath, ".md"),
12614
+ name: basename6(filePath, ".md"),
12312
12615
  description: ""
12313
12616
  },
12314
12617
  content: raw.trim(),
@@ -12319,7 +12622,7 @@ function parseSkillFile(filePath) {
12319
12622
  const parsed = parseSimpleYaml(yaml);
12320
12623
  return {
12321
12624
  meta: {
12322
- name: parsed["name"] ?? basename5(filePath, ".md"),
12625
+ name: parsed["name"] ?? basename6(filePath, ".md"),
12323
12626
  description: parsed["description"] ?? "",
12324
12627
  tools: parsed["tools"] ? parseYamlArray(parsed["tools"]) : void 0
12325
12628
  },
@@ -12340,36 +12643,40 @@ var SkillManager = class {
12340
12643
  this.skillsDir = skillsDir;
12341
12644
  this.warnThreshold = warnThreshold ?? SKILL_CONTENT_WARN_CHARS_DEFAULT;
12342
12645
  }
12343
- /** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
12344
- loadSkills() {
12646
+ /** 发现并加载 skillsDir 以及额外插件目录下的技能,返回加载数量 */
12647
+ loadSkills(extraDirs = []) {
12345
12648
  this.skills.clear();
12346
- if (!existsSync20(this.skillsDir)) {
12347
- try {
12348
- mkdirSync9(this.skillsDir, { recursive: true });
12349
- } catch {
12649
+ this.loadSkillDir(this.skillsDir, true);
12650
+ for (const dir of extraDirs) this.loadSkillDir(dir, false);
12651
+ return this.skills.size;
12652
+ }
12653
+ loadSkillDir(dir, createIfMissing) {
12654
+ if (!existsSync21(dir)) {
12655
+ if (createIfMissing) {
12656
+ try {
12657
+ mkdirSync10(dir, { recursive: true });
12658
+ } catch {
12659
+ }
12350
12660
  }
12351
- return 0;
12661
+ return;
12352
12662
  }
12353
12663
  let entries;
12354
12664
  try {
12355
- entries = readdirSync9(this.skillsDir);
12665
+ entries = readdirSync10(dir);
12356
12666
  } catch {
12357
- return 0;
12667
+ return;
12358
12668
  }
12359
12669
  for (const entry of entries) {
12360
12670
  let filePath;
12361
- const fullPath = join15(this.skillsDir, entry);
12671
+ const fullPath = join16(dir, entry);
12362
12672
  if (entry.endsWith(".md")) {
12363
12673
  filePath = fullPath;
12364
12674
  } else {
12365
12675
  try {
12366
- if (statSync6(fullPath).isDirectory()) {
12367
- const skillMd = join15(fullPath, "SKILL.md");
12368
- if (existsSync20(skillMd)) {
12369
- filePath = skillMd;
12370
- } else {
12371
- continue;
12372
- }
12676
+ if (statSync7(fullPath).isDirectory()) {
12677
+ const skillMd = join16(fullPath, "SKILL.md");
12678
+ if (existsSync21(skillMd)) filePath = skillMd;
12679
+ else continue;
12373
12680
  } else {
12374
12681
  continue;
12375
12682
  }
@@ -12379,13 +12686,10 @@ var SkillManager = class {
12379
12686
  }
12380
12687
  const skill = parseSkillFile(filePath);
12381
12688
  if (skill) {
12382
- if (skill.meta.name === "SKILL" && !entry.endsWith(".md")) {
12383
- skill.meta.name = entry;
12384
- }
12385
- this.skills.set(skill.meta.name, skill);
12689
+ if (skill.meta.name === "SKILL" && !entry.endsWith(".md")) skill.meta.name = entry;
12690
+ if (!this.skills.has(skill.meta.name)) this.skills.set(skill.meta.name, skill);
12386
12691
  }
12387
12692
  }
12388
- return this.skills.size;
12389
12693
  }
12390
12694
  /** 列出所有已加载的技能 */
12391
12695
  listSkills() {
@@ -12430,7 +12734,7 @@ var SkillManager = class {
12430
12734
  // src/web/tool-executor-web.ts
12431
12735
  import { randomUUID as randomUUID3 } from "crypto";
12432
12736
  import { tmpdir as tmpdir2 } from "os";
12433
- import { existsSync as existsSync21, readFileSync as readFileSync15 } from "fs";
12737
+ import { existsSync as existsSync22, readFileSync as readFileSync16 } from "fs";
12434
12738
  var ToolExecutorWeb = class _ToolExecutorWeb {
12435
12739
  constructor(registry, ws) {
12436
12740
  this.registry = registry;
@@ -12500,33 +12804,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12500
12804
  }
12501
12805
  /** Resolve a pending confirm from client response */
12502
12806
  resolveConfirm(requestId, approved) {
12503
- const resolve11 = this.pendingConfirms.get(requestId);
12504
- if (resolve11) {
12807
+ const resolve12 = this.pendingConfirms.get(requestId);
12808
+ if (resolve12) {
12505
12809
  this.clearPendingTimer(requestId);
12506
12810
  this.pendingConfirms.delete(requestId);
12507
12811
  this.confirming = false;
12508
- resolve11(approved);
12812
+ resolve12(approved);
12509
12813
  }
12510
12814
  }
12511
12815
  /** Resolve a pending batch confirm from client response */
12512
12816
  resolveBatchConfirm(requestId, decision) {
12513
- const resolve11 = this.pendingBatchConfirms.get(requestId);
12514
- if (resolve11) {
12817
+ const resolve12 = this.pendingBatchConfirms.get(requestId);
12818
+ if (resolve12) {
12515
12819
  this.clearPendingTimer(requestId);
12516
12820
  this.pendingBatchConfirms.delete(requestId);
12517
12821
  this.confirming = false;
12518
12822
  if (decision === "all" || decision === "none") {
12519
- resolve11(decision);
12823
+ resolve12(decision);
12520
12824
  } else {
12521
- resolve11(new Set(decision));
12825
+ resolve12(new Set(decision));
12522
12826
  }
12523
12827
  }
12524
12828
  }
12525
12829
  /** Cancel all pending confirms (e.g., on disconnect) */
12526
12830
  cancelAll() {
12527
- for (const resolve11 of this.pendingConfirms.values()) resolve11(false);
12831
+ for (const resolve12 of this.pendingConfirms.values()) resolve12(false);
12528
12832
  this.pendingConfirms.clear();
12529
- for (const resolve11 of this.pendingBatchConfirms.values()) resolve11("none");
12833
+ for (const resolve12 of this.pendingBatchConfirms.values()) resolve12("none");
12530
12834
  this.pendingBatchConfirms.clear();
12531
12835
  this.confirming = false;
12532
12836
  }
@@ -12569,9 +12873,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12569
12873
  if (call.name === "write_file") {
12570
12874
  const filePath = String(call.arguments["path"] ?? "");
12571
12875
  const newContent = String(call.arguments["content"] ?? "");
12572
- if (filePath && existsSync21(filePath)) {
12876
+ if (filePath && existsSync22(filePath)) {
12573
12877
  try {
12574
- const old = readFileSync15(filePath, "utf-8");
12878
+ const old = readFileSync16(filePath, "utf-8");
12575
12879
  return renderDiff(old, newContent, { filePath });
12576
12880
  } catch {
12577
12881
  }
@@ -12602,8 +12906,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12602
12906
  diff: this.getDiffPreview(call)
12603
12907
  };
12604
12908
  this.send(msg);
12605
- return new Promise((resolve11) => {
12606
- this.pendingConfirms.set(requestId, resolve11);
12909
+ return new Promise((resolve12) => {
12910
+ this.pendingConfirms.set(requestId, resolve12);
12607
12911
  this.pendingTimers.set(requestId, setTimeout(() => {
12608
12912
  if (this.pendingConfirms.has(requestId)) {
12609
12913
  this.resolveConfirm(requestId, false);
@@ -12627,8 +12931,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12627
12931
  files
12628
12932
  };
12629
12933
  this.send(msg);
12630
- return new Promise((resolve11) => {
12631
- this.pendingBatchConfirms.set(requestId, resolve11);
12934
+ return new Promise((resolve12) => {
12935
+ this.pendingBatchConfirms.set(requestId, resolve12);
12632
12936
  this.pendingTimers.set(requestId, setTimeout(() => {
12633
12937
  if (this.pendingBatchConfirms.has(requestId)) {
12634
12938
  this.resolveBatchConfirm(requestId, "none");
@@ -12874,16 +13178,16 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
12874
13178
  };
12875
13179
 
12876
13180
  // src/repl/dev-state.ts
12877
- import { existsSync as existsSync22, readFileSync as readFileSync16, unlinkSync as unlinkSync4, mkdirSync as mkdirSync10 } from "fs";
12878
- import { join as join16 } from "path";
13181
+ import { existsSync as existsSync23, readFileSync as readFileSync17, unlinkSync as unlinkSync4, mkdirSync as mkdirSync11 } from "fs";
13182
+ import { join as join17 } from "path";
12879
13183
  import { homedir as homedir7 } from "os";
12880
13184
  function getDevStatePath() {
12881
- return join16(homedir7(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
13185
+ return join17(homedir7(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
12882
13186
  }
12883
13187
  function loadDevState() {
12884
13188
  const path3 = getDevStatePath();
12885
- if (!existsSync22(path3)) return null;
12886
- const content = readFileSync16(path3, "utf-8").trim();
13189
+ if (!existsSync23(path3)) return null;
13190
+ const content = readFileSync17(path3, "utf-8").trim();
12887
13191
  return content || null;
12888
13192
  }
12889
13193
 
@@ -13140,8 +13444,8 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
13140
13444
  }
13141
13445
 
13142
13446
  // src/core/context-files.ts
13143
- import { existsSync as existsSync23, readFileSync as readFileSync17 } from "fs";
13144
- import { join as join17, relative as relative3, resolve as resolve8 } from "path";
13447
+ import { existsSync as existsSync24, readFileSync as readFileSync18 } from "fs";
13448
+ import { join as join18, relative as relative3, resolve as resolve9 } from "path";
13145
13449
  function uniqueNonEmpty(values) {
13146
13450
  const seen = /* @__PURE__ */ new Set();
13147
13451
  const out = [];
@@ -13164,14 +13468,14 @@ function displayPath(filePath, cwd) {
13164
13468
  return filePath;
13165
13469
  }
13166
13470
  function isInsideOrEqual(parent, child) {
13167
- const rel = relative3(resolve8(parent), resolve8(child));
13471
+ const rel = relative3(resolve9(parent), resolve9(child));
13168
13472
  return rel === "" || !!rel && !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\");
13169
13473
  }
13170
13474
  function readContextFile(level, filePath, cwd, maxBytes) {
13171
13475
  const name = filePath.split(/[\\/]/).pop() ?? filePath;
13172
13476
  const shown = displayPath(filePath, cwd);
13173
13477
  try {
13174
- const raw = readFileSync17(filePath);
13478
+ const raw = readFileSync18(filePath);
13175
13479
  const byteCount = raw.byteLength;
13176
13480
  if (byteCount === 0) {
13177
13481
  return {
@@ -13218,8 +13522,8 @@ function readContextFile(level, filePath, cwd, maxBytes) {
13218
13522
  function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
13219
13523
  const skipped = [];
13220
13524
  for (const candidate of candidates) {
13221
- const filePath = join17(dir, candidate);
13222
- if (!existsSync23(filePath)) continue;
13525
+ const filePath = join18(dir, candidate);
13526
+ if (!existsSync24(filePath)) continue;
13223
13527
  const result = readContextFile(level, filePath, cwd, maxBytes);
13224
13528
  if (result.skipped) skipped.push(result.skipped);
13225
13529
  if (result.layer) return { layer: result.layer, skipped };
@@ -13227,7 +13531,7 @@ function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
13227
13531
  return { layer: null, skipped };
13228
13532
  }
13229
13533
  function loadContextFiles(options) {
13230
- const cwd = resolve8(options.cwd);
13534
+ const cwd = resolve9(options.cwd);
13231
13535
  const maxBytes = options.maxBytes ?? CONTEXT_FILE_MAX_BYTES;
13232
13536
  const candidates = options.candidates ?? buildContextFileCandidates(options.fallbackFilenames);
13233
13537
  const skipped = [];
@@ -13235,7 +13539,7 @@ function loadContextFiles(options) {
13235
13539
  return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
13236
13540
  }
13237
13541
  if (options.setting && options.setting !== "auto") {
13238
- const filePath = resolve8(cwd, String(options.setting));
13542
+ const filePath = resolve9(cwd, String(options.setting));
13239
13543
  if (!isInsideOrEqual(cwd, filePath)) {
13240
13544
  skipped.push({
13241
13545
  level: "single",
@@ -13246,7 +13550,7 @@ function loadContextFiles(options) {
13246
13550
  });
13247
13551
  return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
13248
13552
  }
13249
- if (!existsSync23(filePath)) {
13553
+ if (!existsSync24(filePath)) {
13250
13554
  skipped.push({
13251
13555
  level: "single",
13252
13556
  filePath,
@@ -13279,7 +13583,7 @@ function loadContextFiles(options) {
13279
13583
  skipped.push(...result.skipped);
13280
13584
  if (result.layer) layers.push(result.layer);
13281
13585
  }
13282
- if (resolve8(options.cwd) !== resolve8(options.projectRoot)) {
13586
+ if (resolve9(options.cwd) !== resolve9(options.projectRoot)) {
13283
13587
  const result = findFirstContextFile("local", options.cwd, cwd, candidates, maxBytes);
13284
13588
  skipped.push(...result.skipped);
13285
13589
  if (result.layer) layers.push(result.layer);
@@ -13297,8 +13601,8 @@ function loadContextFiles(options) {
13297
13601
  }
13298
13602
 
13299
13603
  // src/web/session-handler.ts
13300
- import { existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync2, mkdirSync as mkdirSync11, readdirSync as readdirSync11, statSync as statSync8, createWriteStream } from "fs";
13301
- import { join as join20, resolve as resolve9, dirname as dirname6 } from "path";
13604
+ import { existsSync as existsSync28, readFileSync as readFileSync21, writeFileSync as writeFileSync3, mkdirSync as mkdirSync13, readdirSync as readdirSync13, statSync as statSync11, createWriteStream } from "fs";
13605
+ import { join as join23, resolve as resolve10, dirname as dirname7 } from "path";
13302
13606
 
13303
13607
  // src/cli/review-prompts.ts
13304
13608
  function buildReviewPrompt(diff, gitContextStr, detailed) {
@@ -13359,8 +13663,8 @@ If no security issues found, state "\u2705 No security vulnerabilities detected"
13359
13663
  }
13360
13664
 
13361
13665
  // src/repl/commands/project-init.ts
13362
- import { existsSync as existsSync24, readFileSync as readFileSync18, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
13363
- import { join as join18 } from "path";
13666
+ import { existsSync as existsSync25, readFileSync as readFileSync19, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
13667
+ import { join as join19 } from "path";
13364
13668
  var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
13365
13669
  "node_modules",
13366
13670
  ".git",
@@ -13396,7 +13700,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
13396
13700
  if (depth > maxDepth || count >= maxEntries) return;
13397
13701
  let entries;
13398
13702
  try {
13399
- entries = readdirSync10(d);
13703
+ entries = readdirSync11(d);
13400
13704
  } catch {
13401
13705
  return;
13402
13706
  }
@@ -13404,11 +13708,11 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
13404
13708
  const sorted = filtered.sort((a, b) => {
13405
13709
  let aIsDir = false, bIsDir = false;
13406
13710
  try {
13407
- aIsDir = statSync7(join18(d, a)).isDirectory();
13711
+ aIsDir = statSync8(join19(d, a)).isDirectory();
13408
13712
  } catch {
13409
13713
  }
13410
13714
  try {
13411
- bIsDir = statSync7(join18(d, b)).isDirectory();
13715
+ bIsDir = statSync8(join19(d, b)).isDirectory();
13412
13716
  } catch {
13413
13717
  }
13414
13718
  if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
@@ -13416,12 +13720,12 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
13416
13720
  });
13417
13721
  for (let i = 0; i < sorted.length && count < maxEntries; i++) {
13418
13722
  const name = sorted[i];
13419
- const fullPath = join18(d, name);
13723
+ const fullPath = join19(d, name);
13420
13724
  const isLast = i === sorted.length - 1;
13421
13725
  const connector = isLast ? "+-- " : "|-- ";
13422
13726
  let isDir;
13423
13727
  try {
13424
- isDir = statSync7(fullPath).isDirectory();
13728
+ isDir = statSync8(fullPath).isDirectory();
13425
13729
  } catch {
13426
13730
  continue;
13427
13731
  }
@@ -13443,7 +13747,7 @@ function scanProject(cwd) {
13443
13747
  configFiles: [],
13444
13748
  directoryStructure: ""
13445
13749
  };
13446
- const check = (file) => existsSync24(join18(cwd, file));
13750
+ const check = (file) => existsSync25(join19(cwd, file));
13447
13751
  const configCandidates = [
13448
13752
  "package.json",
13449
13753
  "tsconfig.json",
@@ -13470,7 +13774,7 @@ function scanProject(cwd) {
13470
13774
  info.type = "node";
13471
13775
  info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
13472
13776
  try {
13473
- const pkg = JSON.parse(readFileSync18(join18(cwd, "package.json"), "utf-8"));
13777
+ const pkg = JSON.parse(readFileSync19(join19(cwd, "package.json"), "utf-8"));
13474
13778
  const scripts = pkg.scripts ?? {};
13475
13779
  info.buildCommand = scripts.build ? "npm run build" : void 0;
13476
13780
  info.testCommand = scripts.test ? "npm test" : void 0;
@@ -13943,6 +14247,334 @@ var DiscussionOrchestrator = class {
13943
14247
  }
13944
14248
  };
13945
14249
 
14250
+ // src/diagnostics/doctor-report.ts
14251
+ import { existsSync as existsSync27, statSync as statSync10 } from "fs";
14252
+ import { join as join21 } from "path";
14253
+ import { arch as arch2, platform as platform6, release as release2 } from "os";
14254
+
14255
+ // src/diagnostics/crash-log.ts
14256
+ import {
14257
+ existsSync as existsSync26,
14258
+ mkdirSync as mkdirSync12,
14259
+ readdirSync as readdirSync12,
14260
+ readFileSync as readFileSync20,
14261
+ statSync as statSync9,
14262
+ unlinkSync as unlinkSync5,
14263
+ writeFileSync as writeFileSync2
14264
+ } from "fs";
14265
+ import { join as join20 } from "path";
14266
+ import { homedir as homedir8, platform as platform5, release, arch } from "os";
14267
+ var LOGS_DIR_NAME = "logs";
14268
+ var CRASH_LOG_PREFIX = "crash-";
14269
+ function getLogsDir(configDir) {
14270
+ const base = configDir ?? join20(homedir8(), CONFIG_DIR_NAME);
14271
+ return join20(base, LOGS_DIR_NAME);
14272
+ }
14273
+ function truncate(s, n) {
14274
+ return s.length <= n ? s : s.slice(0, n) + "\u2026";
14275
+ }
14276
+ function listRecentCrashes(limit = 10, configDir) {
14277
+ const dir = getLogsDir(configDir);
14278
+ if (!existsSync26(dir)) return [];
14279
+ const files = [];
14280
+ let names;
14281
+ try {
14282
+ names = readdirSync12(dir);
14283
+ } catch {
14284
+ return [];
14285
+ }
14286
+ for (const name of names) {
14287
+ if (!name.startsWith(CRASH_LOG_PREFIX) || !name.endsWith(".log")) continue;
14288
+ const full = join20(dir, name);
14289
+ try {
14290
+ const st = statSync9(full);
14291
+ const head = readFileSync20(full, "utf-8").split("\n").slice(0, 20);
14292
+ const tsLine = head.find((l) => l.startsWith("timestamp:"));
14293
+ const kindLine = head.find((l) => l.startsWith("kind:"));
14294
+ const msgIdx = head.findIndex((l) => l.trim() === "## message");
14295
+ const message = msgIdx >= 0 && head[msgIdx + 1] ? head[msgIdx + 1] : "";
14296
+ files.push({
14297
+ file: full,
14298
+ timestamp: tsLine ? tsLine.replace("timestamp:", "").trim() : st.mtime.toISOString(),
14299
+ kind: kindLine ? kindLine.replace("kind:", "").trim() : "unknown",
14300
+ message: truncate(message, 200),
14301
+ sizeBytes: st.size
14302
+ });
14303
+ } catch {
14304
+ }
14305
+ }
14306
+ files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
14307
+ return files.slice(0, limit);
14308
+ }
14309
+ function getConfigDirUsage(configDir) {
14310
+ const base = configDir ?? join20(homedir8(), CONFIG_DIR_NAME);
14311
+ if (!existsSync26(base)) return { totalBytes: 0, entries: [] };
14312
+ const entries = [];
14313
+ let total = 0;
14314
+ let names;
14315
+ try {
14316
+ names = readdirSync12(base);
14317
+ } catch {
14318
+ return { totalBytes: 0, entries: [] };
14319
+ }
14320
+ for (const name of names) {
14321
+ const full = join20(base, name);
14322
+ const bytes = dirSize(full);
14323
+ entries.push({ name, bytes });
14324
+ total += bytes;
14325
+ }
14326
+ entries.sort((a, b) => b.bytes - a.bytes);
14327
+ return { totalBytes: total, entries };
14328
+ }
14329
+ function dirSize(path3) {
14330
+ try {
14331
+ const st = statSync9(path3);
14332
+ if (st.isFile()) return st.size;
14333
+ if (!st.isDirectory()) return 0;
14334
+ let sum = 0;
14335
+ for (const name of readdirSync12(path3)) {
14336
+ sum += dirSize(join20(path3, name));
14337
+ }
14338
+ return sum;
14339
+ } catch {
14340
+ return 0;
14341
+ }
14342
+ }
14343
+
14344
+ // src/diagnostics/doctor-report.ts
14345
+ function checkFile(label, path3) {
14346
+ const exists = existsSync27(path3);
14347
+ let sizeBytes = null;
14348
+ if (exists) {
14349
+ try {
14350
+ sizeBytes = statSync10(path3).size;
14351
+ } catch {
14352
+ }
14353
+ }
14354
+ return { label, path: path3, exists, sizeBytes };
14355
+ }
14356
+ function buildDoctorReport(options) {
14357
+ const config = options.config;
14358
+ const cwd = options.cwd ?? process.cwd();
14359
+ const configDir = config.getConfigDir();
14360
+ const projectRoot = getGitRoot(cwd) ?? cwd;
14361
+ const contextCfg = config.get("context");
14362
+ const contextSetting = config.get("contextFile");
14363
+ const context = loadContextFiles({
14364
+ cwd,
14365
+ configDir,
14366
+ projectRoot,
14367
+ setting: contextSetting,
14368
+ maxBytes: contextCfg.projectDocMaxBytes,
14369
+ fallbackFilenames: contextCfg.projectDocFallbackFilenames
14370
+ });
14371
+ const hooksConfig = config.get("hooks");
14372
+ const hooks = listHooks(hooksConfig, configDir);
14373
+ const pendingHooks = getPendingHookTrust(hooksConfig, configDir);
14374
+ const plugins = listInstalledPlugins(configDir);
14375
+ const activeAssets = getActivePluginAssets(configDir);
14376
+ const network = config.get("networkPolicy");
14377
+ const mcpServers = config.get("mcpServers");
14378
+ const allStats = getStatsSnapshot();
14379
+ return {
14380
+ version: VERSION,
14381
+ npmCheck: options.npmCheck,
14382
+ node: process.version,
14383
+ platform: `${platform6()} ${release2()} (${arch2()})`,
14384
+ cwd,
14385
+ projectRoot,
14386
+ configDir,
14387
+ providers: options.providers.listAll().map((p) => ({ id: p.id, displayName: p.displayName, configured: p.configured })),
14388
+ files: [
14389
+ checkFile("config.json", join21(configDir, "config.json")),
14390
+ checkFile("memory.md", join21(configDir, MEMORY_FILE_NAME)),
14391
+ checkFile("memory.jsonl", join21(configDir, "memory.jsonl")),
14392
+ checkFile("dev-state.md", join21(configDir, DEV_STATE_FILE_NAME)),
14393
+ checkFile("hooks-trust.json", join21(configDir, "hooks-trust.json")),
14394
+ checkFile("plugin-state.json", join21(configDir, "plugins", "plugin-state.json"))
14395
+ ],
14396
+ context: {
14397
+ enabled: contextSetting !== false,
14398
+ setting: contextSetting,
14399
+ candidates: context.candidates,
14400
+ maxBytes: context.maxBytes,
14401
+ layers: context.layers.map((l) => ({ level: l.level, fileName: l.fileName, displayPath: l.displayPath, bytes: l.byteCount, chars: l.charCount, truncated: l.truncated })),
14402
+ skipped: context.skipped.map((s) => ({ level: s.level, fileName: s.fileName, displayPath: s.displayPath, reason: s.reason, message: s.message }))
14403
+ },
14404
+ permissions: {
14405
+ defaultProfile: config.get("defaultPermissionProfile"),
14406
+ allowedProfiles: config.get("allowedPermissionProfiles"),
14407
+ customProfileCount: Object.keys(config.get("permissionProfiles")).length,
14408
+ legacyDefaultAction: config.get("defaultPermission"),
14409
+ ruleCount: config.get("permissionRules").length
14410
+ },
14411
+ networkPolicy: {
14412
+ enabled: network.enabled,
14413
+ defaultAction: network.defaultAction,
14414
+ allowDomains: network.allowDomains.length,
14415
+ denyDomains: network.denyDomains.length,
14416
+ allowPorts: network.allowPorts.length,
14417
+ denyPorts: network.denyPorts.length,
14418
+ allowPrivateNetwork: network.allowPrivateNetwork,
14419
+ toolOverrides: Object.fromEntries(Object.entries(network.tools).filter(([, v]) => v !== void 0))
14420
+ },
14421
+ hooks: {
14422
+ enabled: hooksConfig?.enabled !== false,
14423
+ total: hooks.length,
14424
+ project: hooks.filter((h) => h.source === "project").length,
14425
+ pendingTrust: pendingHooks.length,
14426
+ disabled: hooks.filter((h) => h.disabled).length
14427
+ },
14428
+ plugins: {
14429
+ root: join21(configDir, "plugins"),
14430
+ installed: plugins.length,
14431
+ active: activeAssets.plugins.length,
14432
+ invalid: plugins.filter((p) => !p.valid).length,
14433
+ untrusted: plugins.filter((p) => p.valid && !p.trusted).length,
14434
+ disabled: plugins.filter((p) => p.valid && !p.enabled).length,
14435
+ mcpServers: Object.keys(activeAssets.mcpServers).length,
14436
+ skillDirs: activeAssets.skillDirs.length,
14437
+ commandDirs: activeAssets.commandDirs.length,
14438
+ agentDirs: activeAssets.agentDirs.length,
14439
+ entries: plugins.map((p) => ({ name: p.name, version: p.manifest.version, enabled: p.enabled, trusted: p.trusted, valid: p.valid, error: p.error }))
14440
+ },
14441
+ mcp: {
14442
+ enabled: config.get("mcpEnabled"),
14443
+ configured: Object.keys(mcpServers).length,
14444
+ projectConfigExists: existsSync27(join21(projectRoot, MCP_PROJECT_CONFIG_NAME)),
14445
+ statuses: options.mcpStatuses ?? []
14446
+ },
14447
+ recentCrashes: listRecentCrashes(5, configDir),
14448
+ diskUsage: getConfigDirUsage(configDir),
14449
+ toolStats: {
14450
+ totalCalls: allStats.reduce((a, b) => a + b.calls, 0),
14451
+ totalFailures: allStats.reduce((a, b) => a + b.failures, 0),
14452
+ topUsed: getTopUsedTools(5),
14453
+ topFailing: getTopFailingTools(5)
14454
+ }
14455
+ };
14456
+ }
14457
+
14458
+ // src/diagnostics/doctor-cli.ts
14459
+ function formatBytes(n) {
14460
+ if (n < 1024) return `${n} B`;
14461
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
14462
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
14463
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
14464
+ }
14465
+ function formatDoctorReport(report, ansi = true) {
14466
+ const wrap = (code, s) => ansi ? `${code}${s}\x1B[0m` : s;
14467
+ const B = (s) => wrap("\x1B[1m", s);
14468
+ const D = (s) => wrap("\x1B[2m", s);
14469
+ const G = (s) => wrap("\x1B[32m", s);
14470
+ const Y = (s) => wrap("\x1B[33m", s);
14471
+ const R = (s) => wrap("\x1B[31m", s);
14472
+ const out = [];
14473
+ out.push("");
14474
+ out.push(B("\u{1FA7A} AI-CLI Health Check"));
14475
+ out.push("");
14476
+ const nc = report.npmCheck;
14477
+ let versionLine = ` version: ${report.version}`;
14478
+ if (nc?.status === "up-to-date") versionLine += ` ${D("(latest on npm)")}`;
14479
+ else if (nc?.status === "outdated" && nc.latest) versionLine += ` ${Y(`\u2192 ${nc.latest} available`)} ${D("(npm i -g jinzd-ai-cli@latest)")}`;
14480
+ else if (nc?.status === "skipped") versionLine += ` ${D(`(npm check skipped: ${nc.reason ?? "unknown"})`)}`;
14481
+ out.push(versionLine);
14482
+ out.push(` node: ${report.node}`);
14483
+ out.push(` platform: ${report.platform}`);
14484
+ out.push(` cwd: ${report.cwd}`);
14485
+ out.push(` project: ${report.projectRoot}`);
14486
+ out.push(` config: ${report.configDir}`);
14487
+ out.push("");
14488
+ out.push(B("API Keys:"));
14489
+ for (const p of report.providers) {
14490
+ out.push(` ${p.configured ? G("\u2713") : D("\u25CB")} ${p.id.padEnd(14)} ${p.configured ? G("configured") : D("not configured")}`);
14491
+ }
14492
+ out.push("");
14493
+ out.push(B("Context Files:"));
14494
+ out.push(` status: ${report.context.enabled ? G("enabled") : D("disabled")} \xB7 max ${formatBytes(report.context.maxBytes)}`);
14495
+ if (report.context.layers.length === 0) out.push(` ${D("(no context files loaded)")}`);
14496
+ for (const l of report.context.layers) {
14497
+ out.push(` ${G("\u2713")} ${l.level.padEnd(7)} ${l.displayPath} ${D(`(${formatBytes(l.bytes)}${l.truncated ? ", truncated" : ""})`)}`);
14498
+ }
14499
+ if (report.context.skipped.length > 0) {
14500
+ out.push(` ${D("skipped:")}`);
14501
+ for (const s of report.context.skipped.slice(0, 5)) {
14502
+ out.push(` ${Y("!")} ${s.level}:${s.fileName} ${s.reason}${s.message ? ` \xB7 ${s.message}` : ""}`);
14503
+ }
14504
+ }
14505
+ out.push("");
14506
+ out.push(B("Permissions / Network:"));
14507
+ out.push(` profile: ${report.permissions.defaultProfile} \xB7 allowed ${report.permissions.allowedProfiles.join(", ")} \xB7 rules ${report.permissions.ruleCount} \xB7 custom profiles ${report.permissions.customProfileCount}`);
14508
+ const np = report.networkPolicy;
14509
+ out.push(` networkPolicy: ${np.enabled ? G("enabled") : D("disabled")} \xB7 default ${np.defaultAction} \xB7 allow ${np.allowDomains} domain(s)/${np.allowPorts} port(s) \xB7 deny ${np.denyDomains} domain(s)/${np.denyPorts} port(s) \xB7 private ${np.allowPrivateNetwork ? "allow" : "block/confirm"}`);
14510
+ const overrides = Object.entries(np.toolOverrides).map(([k, v]) => `${k}=${v}`).join(", ");
14511
+ if (overrides) out.push(` tool overrides: ${overrides}`);
14512
+ out.push("");
14513
+ out.push(B("Hooks / Plugins:"));
14514
+ out.push(` hooks: ${report.hooks.enabled ? G("enabled") : D("disabled")} \xB7 total ${report.hooks.total} \xB7 project ${report.hooks.project} \xB7 pending trust ${report.hooks.pendingTrust} \xB7 disabled ${report.hooks.disabled}`);
14515
+ out.push(` plugins: installed ${report.plugins.installed} \xB7 active ${report.plugins.active} \xB7 disabled ${report.plugins.disabled} \xB7 untrusted ${report.plugins.untrusted} \xB7 invalid ${report.plugins.invalid}`);
14516
+ out.push(` plugin assets: ${report.plugins.skillDirs} skill dir(s), ${report.plugins.commandDirs} command dir(s), ${report.plugins.agentDirs} agent dir(s), ${report.plugins.mcpServers} MCP server(s)`);
14517
+ if (report.plugins.entries.length > 0) {
14518
+ for (const p of report.plugins.entries.slice(0, 5)) {
14519
+ const state2 = p.valid ? `${p.enabled ? "enabled" : "disabled"} \xB7 ${p.trusted ? "trusted" : "untrusted"}` : `invalid \xB7 ${p.error ?? ""}`;
14520
+ out.push(` ${p.valid ? G("\u2713") : R("\u2717")} ${p.name}@${p.version} ${D(state2)}`);
14521
+ }
14522
+ }
14523
+ out.push("");
14524
+ out.push(B("MCP Servers:"));
14525
+ out.push(` global configured: ${report.mcp.configured} \xB7 project .mcp.json: ${report.mcp.projectConfigExists ? "yes" : "no"} \xB7 mcpEnabled: ${report.mcp.enabled ? "true" : "false"}`);
14526
+ if (report.mcp.statuses.length === 0) out.push(` ${D("(no connected manager status in this mode)")}`);
14527
+ for (const s of report.mcp.statuses) {
14528
+ const state2 = s.connected ? `${G("connected")} \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `${R("disconnected")}${s.error ? D(` \xB7 ${s.error}`) : ""}`;
14529
+ out.push(` ${s.connected ? G("\u2713") : R("\u2717")} ${s.serverId.padEnd(16)} ${state2}`);
14530
+ }
14531
+ out.push("");
14532
+ out.push(B("Config Files:"));
14533
+ for (const f of report.files) {
14534
+ const extra = f.exists && f.sizeBytes !== null ? ` ${D(`(${formatBytes(f.sizeBytes)})`)}` : "";
14535
+ out.push(` ${f.exists ? G("\u2713") : D("\u2013")} ${f.label.padEnd(16)} ${f.exists ? f.path : D("(not found)")}${extra}`);
14536
+ }
14537
+ out.push("");
14538
+ out.push(B("Recent Crashes (last 5):"));
14539
+ if (report.recentCrashes.length === 0) out.push(` ${D("(none - clean!)")}`);
14540
+ for (const c of report.recentCrashes) {
14541
+ out.push(` ${R("\u2717")} ${c.timestamp} ${Y(c.kind)}`);
14542
+ out.push(` ${D(c.message)}`);
14543
+ out.push(` ${D(c.file)}`);
14544
+ }
14545
+ out.push("");
14546
+ const ts = report.toolStats;
14547
+ out.push(B("Tool Usage:"));
14548
+ out.push(` total: ${ts.totalCalls} calls \xB7 ${ts.totalFailures} failures (${ts.totalCalls ? (ts.totalFailures * 100 / ts.totalCalls).toFixed(1) : "0.0"}%)`);
14549
+ if (ts.topUsed.length > 0) {
14550
+ out.push(` ${D("top used:")}`);
14551
+ for (const t of ts.topUsed) out.push(` ${t.name.padEnd(20)} ${String(t.calls).padStart(6)} calls \xB7 avg ${t.calls ? (t.totalDurationMs / t.calls).toFixed(0) : "0"}ms`);
14552
+ }
14553
+ if (ts.topFailing.length > 0) {
14554
+ out.push(` ${D("top failing:")}`);
14555
+ for (const t of ts.topFailing) out.push(` ${R(t.name.padEnd(20))} ${t.failures}/${t.calls} (${(t.failures * 100 / Math.max(1, t.calls)).toFixed(1)}%) ${D(t.lastFailureMessage ?? "")}`.slice(0, 200));
14556
+ }
14557
+ out.push("");
14558
+ const dirAnnotations = {
14559
+ models: "semantic search model -> /index semantic-clear",
14560
+ index: "symbol index -> /index clear",
14561
+ history: "conversation history",
14562
+ logs: "crash logs",
14563
+ "memory-index": "chat memory index -> /memory index-clear",
14564
+ plugins: "installed package plugins"
14565
+ };
14566
+ out.push(B("Disk Usage (~/.aicli):"));
14567
+ out.push(` total: ${formatBytes(report.diskUsage.totalBytes)}`);
14568
+ for (const e of report.diskUsage.entries.slice(0, 10)) {
14569
+ const note = dirAnnotations[e.name];
14570
+ out.push(` ${e.name.padEnd(18)} ${formatBytes(e.bytes)}${note ? ` ${D(`\xB7 ${note}`)}` : ""}`);
14571
+ }
14572
+ out.push("");
14573
+ out.push(G("\u2713 Health check complete"));
14574
+ out.push("");
14575
+ return out.join("\n");
14576
+ }
14577
+
13946
14578
  // src/hub/presets.ts
13947
14579
  var PRESETS = [
13948
14580
  {
@@ -14130,7 +14762,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
14130
14762
  }
14131
14763
 
14132
14764
  // src/hub/persist.ts
14133
- import { join as join19 } from "path";
14765
+ import { join as join22 } from "path";
14134
14766
  function discussionToMessages(state2) {
14135
14767
  const out = [];
14136
14768
  const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
@@ -14168,7 +14800,7 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
14168
14800
  session.title = `[Hub] ${state2.topic.slice(0, 48)}`.replace(/\n/g, " ");
14169
14801
  session.titleAiGenerated = true;
14170
14802
  await sm.save();
14171
- return { id: session.id, path: join19(config.getHistoryDir(), `${session.id}.json`) };
14803
+ return { id: session.id, path: join22(config.getHistoryDir(), `${session.id}.json`) };
14172
14804
  }
14173
14805
 
14174
14806
  // src/web/session-handler.ts
@@ -14264,7 +14896,7 @@ var SessionHandler = class {
14264
14896
  }
14265
14897
  this.updateContextWindow();
14266
14898
  this.activeSystemPrompt = this.loadContextFiles();
14267
- const hooks = this.config.get("hooks");
14899
+ const hooks = mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir());
14268
14900
  const permissionRules = this.config.get("permissionRules");
14269
14901
  const defaultPermission = this.config.get("defaultPermission");
14270
14902
  const permissionProfileName = this.config.get("defaultPermissionProfile") ?? "legacy";
@@ -14364,10 +14996,10 @@ var SessionHandler = class {
14364
14996
  return;
14365
14997
  }
14366
14998
  case "ask_user_response": {
14367
- const resolve11 = this.pendingAskUser.get(msg.requestId);
14368
- if (resolve11) {
14999
+ const resolve12 = this.pendingAskUser.get(msg.requestId);
15000
+ if (resolve12) {
14369
15001
  this.pendingAskUser.delete(msg.requestId);
14370
- resolve11(msg.answer);
15002
+ resolve12(msg.answer);
14371
15003
  }
14372
15004
  return;
14373
15005
  }
@@ -14380,10 +15012,10 @@ var SessionHandler = class {
14380
15012
  case "memory_manage":
14381
15013
  return this.handleMemoryManage(msg.action, msg.id, msg.expiresAt);
14382
15014
  case "auto_pause_response": {
14383
- const resolve11 = this.pendingAutoPause.get(msg.requestId);
14384
- if (resolve11) {
15015
+ const resolve12 = this.pendingAutoPause.get(msg.requestId);
15016
+ if (resolve12) {
14385
15017
  this.pendingAutoPause.delete(msg.requestId);
14386
- resolve11({ action: msg.action, message: msg.message });
15018
+ resolve12({ action: msg.action, message: msg.message });
14387
15019
  }
14388
15020
  return;
14389
15021
  }
@@ -14398,10 +15030,10 @@ var SessionHandler = class {
14398
15030
  this.hubOrchestrator?.abort();
14399
15031
  return;
14400
15032
  case "hub_steer": {
14401
- const resolve11 = this.pendingHubReview.get(msg.requestId);
14402
- if (resolve11) {
15033
+ const resolve12 = this.pendingHubReview.get(msg.requestId);
15034
+ if (resolve12) {
14403
15035
  this.pendingHubReview.delete(msg.requestId);
14404
- resolve11({ action: msg.action, message: msg.message });
15036
+ resolve12({ action: msg.action, message: msg.message });
14405
15037
  }
14406
15038
  return;
14407
15039
  }
@@ -14411,12 +15043,12 @@ var SessionHandler = class {
14411
15043
  }
14412
15044
  }
14413
15045
  onDisconnect() {
14414
- runLifecycleHooks(this.config.get("hooks") ?? void 0, "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
15046
+ runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
14415
15047
  this.toolExecutor.cancelAll();
14416
15048
  if (this.abortController) this.abortController.abort();
14417
- for (const resolve11 of this.pendingAskUser.values()) resolve11(null);
15049
+ for (const resolve12 of this.pendingAskUser.values()) resolve12(null);
14418
15050
  this.pendingAskUser.clear();
14419
- for (const resolve11 of this.pendingAutoPause.values()) resolve11({ action: "stop" });
15051
+ for (const resolve12 of this.pendingAutoPause.values()) resolve12({ action: "stop" });
14420
15052
  this.pendingAutoPause.clear();
14421
15053
  this.saveIfNeeded();
14422
15054
  }
@@ -14550,9 +15182,9 @@ var SessionHandler = class {
14550
15182
  this.hubOrchestrator = orchestrator;
14551
15183
  orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
14552
15184
  if (config.humanSteer) {
14553
- orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve11) => {
15185
+ orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve12) => {
14554
15186
  const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
14555
- this.pendingHubReview.set(requestId, resolve11);
15187
+ this.pendingHubReview.set(requestId, resolve12);
14556
15188
  this.send({ type: "hub_review", requestId, round, maxRounds });
14557
15189
  });
14558
15190
  }
@@ -14590,7 +15222,7 @@ var SessionHandler = class {
14590
15222
  this.ensureSession();
14591
15223
  const session = this.sessions.current;
14592
15224
  const promptDecisions = runLifecycleHooks(
14593
- this.config.get("hooks") ?? void 0,
15225
+ mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()),
14594
15226
  "UserPromptSubmit",
14595
15227
  { sessionId: session.id, prompt: content },
14596
15228
  { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
@@ -14885,7 +15517,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
14885
15517
  this.send({ type: "response_done", content, usage });
14886
15518
  if (pendingTeeSave && isCleanDocumentBody(content)) {
14887
15519
  try {
14888
- mkdirSync11(dirname6(pendingTeeSave), { recursive: true });
15520
+ mkdirSync13(dirname7(pendingTeeSave), { recursive: true });
14889
15521
  const bodyToSave = stripOuterCodeFence(content);
14890
15522
  atomicWriteFileSync(pendingTeeSave, bodyToSave);
14891
15523
  undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
@@ -14910,8 +15542,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
14910
15542
  onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
14911
15543
  requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
14912
15544
  const requestId = `pause_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
14913
- const pauseResp = await new Promise((resolve11) => {
14914
- this.pendingAutoPause.set(requestId, resolve11);
15545
+ const pauseResp = await new Promise((resolve12) => {
15546
+ this.pendingAutoPause.set(requestId, resolve12);
14915
15547
  this.send({
14916
15548
  type: "auto_pause_request",
14917
15549
  requestId,
@@ -15061,7 +15693,7 @@ ${summaryContent}`,
15061
15693
  let isError = false;
15062
15694
  let summary;
15063
15695
  try {
15064
- mkdirSync11(dirname6(saveToFile), { recursive: true });
15696
+ mkdirSync13(dirname7(saveToFile), { recursive: true });
15065
15697
  fileStream = createWriteStream(saveToFile);
15066
15698
  const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
15067
15699
  const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
@@ -15090,8 +15722,8 @@ ${summaryContent}`,
15090
15722
  }
15091
15723
  if (chunk.done) break;
15092
15724
  }
15093
- await new Promise((resolve11, reject) => {
15094
- fileStream.end((err) => err ? reject(err) : resolve11());
15725
+ await new Promise((resolve12, reject) => {
15726
+ fileStream.end((err) => err ? reject(err) : resolve12());
15095
15727
  });
15096
15728
  const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
15097
15729
  if (verdict.kind === "reject") {
@@ -15117,7 +15749,7 @@ ${summaryContent}`,
15117
15749
  } catch (err) {
15118
15750
  if (fileStream) {
15119
15751
  try {
15120
- await new Promise((resolve11) => fileStream.end(() => resolve11()));
15752
+ await new Promise((resolve12) => fileStream.end(() => resolve12()));
15121
15753
  } catch {
15122
15754
  }
15123
15755
  }
@@ -15432,6 +16064,7 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
15432
16064
  " /add-dir [path|remove] \u2014 Add/remove directory from AI context",
15433
16065
  " /mcp [reconnect] \u2014 Show/manage MCP servers",
15434
16066
  " /plugins \u2014 Show loaded plugins",
16067
+ " /plugin <cmd> \u2014 Manage package plugins",
15435
16068
  " /commands \u2014 List custom commands",
15436
16069
  " /doctor \u2014 Health check (API keys, config, MCP)",
15437
16070
  " /bug \u2014 Generate bug report template",
@@ -15530,7 +16163,7 @@ ${lines.join("\n")}` });
15530
16163
  this.sendToolsList();
15531
16164
  this.sendStatus();
15532
16165
  } else if (sub === "reload") {
15533
- this.skillManager.loadSkills();
16166
+ this.skillManager.loadSkills(getActivePluginAssets(this.config.getConfigDir()).skillDirs);
15534
16167
  this.send({ type: "info", message: `\u{1F504} Reloaded ${this.skillManager.listSkills().length} skill(s).` });
15535
16168
  this.sendToolsList();
15536
16169
  } else {
@@ -15749,9 +16382,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15749
16382
  let modifiedFiles = 0;
15750
16383
  const diffLines2 = [];
15751
16384
  for (const [filePath, { earliest }] of fileMap) {
15752
- const currentContent = existsSync25(filePath) ? (() => {
16385
+ const currentContent = existsSync28(filePath) ? (() => {
15753
16386
  try {
15754
- return readFileSync19(filePath, "utf-8");
16387
+ return readFileSync21(filePath, "utf-8");
15755
16388
  } catch {
15756
16389
  return null;
15757
16390
  }
@@ -15852,7 +16485,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15852
16485
  break;
15853
16486
  }
15854
16487
  const sub = args[0]?.toLowerCase();
15855
- const resolve11 = (ref) => {
16488
+ const resolve12 = (ref) => {
15856
16489
  const r = session.resolveBranchRef(ref);
15857
16490
  if (r.ok) return r.id;
15858
16491
  if (r.reason === "ambiguous") {
@@ -15906,7 +16539,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15906
16539
  this.send({ type: "error", message: "Usage: /branch switch <id|title>" });
15907
16540
  break;
15908
16541
  }
15909
- const id = resolve11(ref);
16542
+ const id = resolve12(ref);
15910
16543
  if (!id) break;
15911
16544
  const ok = session.switchBranch(id);
15912
16545
  if (ok) {
@@ -15926,7 +16559,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15926
16559
  this.send({ type: "error", message: "Usage: /branch delete <id|title>" });
15927
16560
  break;
15928
16561
  }
15929
- const id = resolve11(ref);
16562
+ const id = resolve12(ref);
15930
16563
  if (!id) break;
15931
16564
  const ok = session.deleteBranch(id);
15932
16565
  if (ok) {
@@ -15945,7 +16578,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15945
16578
  this.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
15946
16579
  break;
15947
16580
  }
15948
- const id = resolve11(ref);
16581
+ const id = resolve12(ref);
15949
16582
  if (!id) break;
15950
16583
  const ok = session.renameBranch(id, title);
15951
16584
  if (ok) {
@@ -15963,7 +16596,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15963
16596
  this.send({ type: "error", message: "Usage: /branch diff <id|title>" });
15964
16597
  break;
15965
16598
  }
15966
- const id = resolve11(ref);
16599
+ const id = resolve12(ref);
15967
16600
  if (!id) break;
15968
16601
  const d = session.diffBranches(id);
15969
16602
  if (!d) {
@@ -16005,7 +16638,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
16005
16638
  this.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
16006
16639
  break;
16007
16640
  }
16008
- const id = resolve11(ref);
16641
+ const id = resolve12(ref);
16009
16642
  if (!id) break;
16010
16643
  const idx = parseInt(idxStr, 10);
16011
16644
  if (Number.isNaN(idx)) {
@@ -16267,7 +16900,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
16267
16900
  case "test": {
16268
16901
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
16269
16902
  try {
16270
- const { executeTests } = await import("./run-tests-K7QR5QN4.js");
16903
+ const { executeTests } = await import("./run-tests-O76JIBCW.js");
16271
16904
  const argStr = args.join(" ").trim();
16272
16905
  let testArgs = {};
16273
16906
  if (argStr) {
@@ -16284,9 +16917,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
16284
16917
  // ── /init ───────────────────────────────────────────────────────
16285
16918
  case "init": {
16286
16919
  const cwd = process.cwd();
16287
- const targetPath = join20(cwd, "AICLI.md");
16920
+ const targetPath = join23(cwd, "AICLI.md");
16288
16921
  const force = args.includes("--force");
16289
- if (existsSync25(targetPath) && !force) {
16922
+ if (existsSync28(targetPath) && !force) {
16290
16923
  this.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
16291
16924
  Use /init --force to overwrite.` });
16292
16925
  break;
@@ -16296,7 +16929,7 @@ Use /init --force to overwrite.` });
16296
16929
  const projectInfo = scanProject(cwd);
16297
16930
  const prompt = buildInitPrompt(projectInfo, cwd);
16298
16931
  const content = await this.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
16299
- writeFileSync2(targetPath, content, "utf-8");
16932
+ writeFileSync3(targetPath, content, "utf-8");
16300
16933
  this.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
16301
16934
  Use /context reload to load it.` });
16302
16935
  } catch (err) {
@@ -16306,53 +16939,13 @@ Use /context reload to load it.` });
16306
16939
  }
16307
16940
  // ── /doctor ─────────────────────────────────────────────────────
16308
16941
  case "doctor": {
16309
- const lines = ["\u{1FA7A} **AI-CLI Health Check**", ""];
16310
- lines.push("**API Keys:**");
16311
- const providersList = this.providers.listAll();
16312
- for (const p of providersList) {
16313
- const icon = p.configured ? "\u2713" : "\u25CB";
16314
- const status = p.configured ? "configured" : "not configured";
16315
- lines.push(` ${icon} ${p.id.padEnd(14)} ${status}`);
16316
- }
16317
- lines.push("");
16318
- const configDir = this.config.getConfigDir();
16319
- lines.push("**Config Files:**");
16320
- lines.push(` Dir: ${configDir}`);
16321
- const checkFile = (label, filePath) => {
16322
- const exists = existsSync25(filePath);
16323
- let extra = "";
16324
- if (exists) {
16325
- try {
16326
- extra = ` (${statSync8(filePath).size} bytes)`;
16327
- } catch {
16328
- }
16329
- }
16330
- lines.push(` ${exists ? "\u2713" : "\u2013"} ${label.padEnd(14)} ${exists ? filePath + extra : "(not found)"}`);
16331
- };
16332
- checkFile("config.json", join20(configDir, "config.json"));
16333
- checkFile("memory.md", join20(configDir, MEMORY_FILE_NAME));
16334
- checkFile("dev-state.md", join20(configDir, "dev-state.md"));
16335
- lines.push("");
16336
- if (this.mcpManager) {
16337
- lines.push("**MCP Servers:**");
16338
- const statuses = this.mcpManager.getStatus();
16339
- if (statuses.length === 0) {
16340
- lines.push(" (no servers configured)");
16341
- } else {
16342
- for (const s of statuses) {
16343
- const state2 = s.connected ? `connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
16344
- lines.push(` ${s.connected ? "\u2713" : "\u2717"} ${s.serverId.padEnd(16)} ${state2}`);
16345
- }
16346
- }
16347
- lines.push("");
16348
- }
16349
- lines.push("**Current Session:**");
16350
- lines.push(` Provider: ${this.currentProvider}`);
16351
- lines.push(` Model: ${this.currentModel}`);
16352
- lines.push(` Version: ${VERSION}`);
16353
- lines.push("");
16354
- lines.push("\u2713 Health check complete");
16355
- this.send({ type: "info", message: lines.join("\n") });
16942
+ const report = buildDoctorReport({
16943
+ config: this.config,
16944
+ providers: this.providers,
16945
+ cwd: process.cwd(),
16946
+ mcpStatuses: this.mcpManager?.getStatus() ?? []
16947
+ });
16948
+ this.send({ type: "info", message: formatDoctorReport(report, false) });
16356
16949
  break;
16357
16950
  }
16358
16951
  // ── /about ──────────────────────────────────────────────────────
@@ -16434,7 +17027,7 @@ ${this.config.toFormattedJSON()}
16434
17027
  const result = this.contextLoadResult;
16435
17028
  const layers = ["\u{1F4DA} **Context Layers:**", ""];
16436
17029
  if (result && result.layers.length > 0) {
16437
- const labels = { global: "Global ", project: "Project", local: "Local " };
17030
+ const labels = { global: "Global ", project: "Project", plugin: "Plugin ", local: "Local " };
16438
17031
  for (const layer of result.layers) {
16439
17032
  layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
16440
17033
  }
@@ -16518,12 +17111,12 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
16518
17111
  this.send({ type: "info", message: "\u2713 All added directories removed from context." });
16519
17112
  break;
16520
17113
  }
16521
- const dirPath = resolve9(sub);
16522
- if (!existsSync25(dirPath)) {
17114
+ const dirPath = resolve10(sub);
17115
+ if (!existsSync28(dirPath)) {
16523
17116
  this.send({ type: "error", message: `Directory not found: ${dirPath}` });
16524
17117
  break;
16525
17118
  }
16526
- if (!statSync8(dirPath).isDirectory()) {
17119
+ if (!statSync11(dirPath).isDirectory()) {
16527
17120
  this.send({ type: "error", message: `Not a directory: ${dirPath}` });
16528
17121
  break;
16529
17122
  }
@@ -16535,14 +17128,14 @@ It will be included in AI context for subsequent messages.` });
16535
17128
  // ── /commands ───────────────────────────────────────────────────
16536
17129
  case "commands": {
16537
17130
  const configDir = this.config.getConfigDir();
16538
- const commandsDir = join20(configDir, CUSTOM_COMMANDS_DIR_NAME);
16539
- if (!existsSync25(commandsDir)) {
17131
+ const commandsDir = join23(configDir, CUSTOM_COMMANDS_DIR_NAME);
17132
+ if (!existsSync28(commandsDir)) {
16540
17133
  this.send({ type: "info", message: `No custom commands directory.
16541
17134
  Create: ${commandsDir}/ with .md files.` });
16542
17135
  break;
16543
17136
  }
16544
17137
  try {
16545
- const files = readdirSync11(commandsDir).filter((f) => f.endsWith(".md"));
17138
+ const files = readdirSync13(commandsDir).filter((f) => f.endsWith(".md"));
16546
17139
  if (files.length === 0) {
16547
17140
  this.send({ type: "info", message: `No custom commands found in ${commandsDir}
16548
17141
  Add .md files to create commands.` });
@@ -16559,23 +17152,75 @@ Add .md files to create commands.` });
16559
17152
  }
16560
17153
  break;
16561
17154
  }
16562
- // ── /plugins ────────────────────────────────────────────────────
17155
+ // ── /plugins / /plugin ──────────────────────────────────────────
16563
17156
  case "plugins": {
16564
17157
  const configDir = this.config.getConfigDir();
16565
- const pluginsDir = join20(configDir, PLUGINS_DIR_NAME);
17158
+ const pluginsDir = join23(configDir, PLUGINS_DIR_NAME);
16566
17159
  const pluginTools = this.toolRegistry.listPluginTools();
17160
+ const pluginPackages = listInstalledPlugins(configDir);
16567
17161
  const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
16568
- if (pluginTools.length === 0) {
17162
+ if (pluginTools.length === 0 && pluginPackages.length === 0) {
16569
17163
  lines.push("No plugins loaded.");
16570
- lines.push("Add .js files to the plugins directory and set allowPlugins:true in config.");
17164
+ lines.push("Use /plugin install <path>, /plugin trust <name>, then /plugin enable <name>.");
16571
17165
  } else {
16572
- for (const t of pluginTools) {
16573
- lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
17166
+ if (pluginPackages.length > 0) {
17167
+ lines.push(`Package plugins (${pluginPackages.length}):`);
17168
+ for (const p of pluginPackages) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
17169
+ lines.push("");
17170
+ }
17171
+ if (pluginTools.length > 0) {
17172
+ lines.push(`Legacy JS plugin tools (${pluginTools.length}):`);
17173
+ for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
16574
17174
  }
16575
17175
  }
16576
17176
  this.send({ type: "info", message: lines.join("\n") });
16577
17177
  break;
16578
17178
  }
17179
+ case "plugin": {
17180
+ const configDir = this.config.getConfigDir();
17181
+ const sub = args[0] ?? "list";
17182
+ try {
17183
+ if (sub === "list") {
17184
+ const plugins = listInstalledPlugins(configDir);
17185
+ const lines = [`\u{1F50C} **Package Plugins**`, `Dir: ${pluginRoot(configDir)}`, ""];
17186
+ if (plugins.length === 0) lines.push("No package plugins installed.");
17187
+ for (const p of plugins) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
17188
+ this.send({ type: "info", message: lines.join("\n") });
17189
+ break;
17190
+ }
17191
+ if (sub === "install") {
17192
+ const input = args[1];
17193
+ if (!input) throw new Error("Usage: /plugin install <path>");
17194
+ const plugin = installPlugin(configDir, input);
17195
+ this.send({ type: "info", message: `Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.` });
17196
+ break;
17197
+ }
17198
+ if (sub === "trust") {
17199
+ const name2 = args[1];
17200
+ if (!name2) throw new Error("Usage: /plugin trust <name>");
17201
+ const state2 = trustPlugin(configDir, name2);
17202
+ this.send({ type: "info", message: `Trusted plugin ${state2.name}.` });
17203
+ break;
17204
+ }
17205
+ if (sub === "enable" || sub === "disable") {
17206
+ const name2 = args[1];
17207
+ if (!name2) throw new Error(`Usage: /plugin ${sub} <name>`);
17208
+ const state2 = setPluginEnabled(configDir, name2, sub === "enable");
17209
+ this.send({ type: "info", message: `${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state2.name}. Restart Web UI for changes to take effect.` });
17210
+ break;
17211
+ }
17212
+ if (sub === "inspect") {
17213
+ const name2 = args[1];
17214
+ if (!name2) throw new Error("Usage: /plugin inspect <name>");
17215
+ this.send({ type: "info", message: describePlugin(getInstalledPlugin(configDir, name2)).join("\n") });
17216
+ break;
17217
+ }
17218
+ throw new Error("Usage: /plugin [list|install|enable|disable|inspect|trust]");
17219
+ } catch (err) {
17220
+ this.send({ type: "error", message: err instanceof Error ? err.message : String(err) });
17221
+ }
17222
+ break;
17223
+ }
16579
17224
  // ── /bug ────────────────────────────────────────────────────────
16580
17225
  case "bug": {
16581
17226
  const session = this.sessions.current;
@@ -16618,7 +17263,7 @@ Add .md files to create commands.` });
16618
17263
  return;
16619
17264
  }
16620
17265
  const preDecisions = runLifecycleHooks(
16621
- this.config.get("hooks") ?? void 0,
17266
+ mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()),
16622
17267
  "PreCompact",
16623
17268
  { sessionId: session.id, messageCount: session.messages.length, instruction },
16624
17269
  { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
@@ -16655,7 +17300,7 @@ Add .md files to create commands.` });
16655
17300
  const removedCount = session.compact(summaryMsg, ackMsg, keepLast);
16656
17301
  const afterCount = session.messages.length;
16657
17302
  runLifecycleHooks(
16658
- this.config.get("hooks") ?? void 0,
17303
+ mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()),
16659
17304
  "PostCompact",
16660
17305
  { sessionId: session.id, beforeCount, afterCount, removedCount, keepLast, instruction },
16661
17306
  { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
@@ -16792,7 +17437,7 @@ ${entry.content}`;
16792
17437
  const configDir = this.config.getConfigDir();
16793
17438
  try {
16794
17439
  atomicWriteFileSync(memoryStorePath(configDir), "");
16795
- atomicWriteFileSync(join20(configDir, MEMORY_FILE_NAME), "");
17440
+ atomicWriteFileSync(join23(configDir, MEMORY_FILE_NAME), "");
16796
17441
  this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
16797
17442
  this.sendMemoryEntries();
16798
17443
  } catch (err) {
@@ -17074,8 +17719,8 @@ async function setupProxy(configProxy) {
17074
17719
  }
17075
17720
 
17076
17721
  // src/web/auth.ts
17077
- import { existsSync as existsSync26, readFileSync as readFileSync20, writeFileSync as writeFileSync3, mkdirSync as mkdirSync12, readdirSync as readdirSync12, copyFileSync } from "fs";
17078
- import { join as join21 } from "path";
17722
+ import { existsSync as existsSync29, readFileSync as readFileSync22, writeFileSync as writeFileSync4, mkdirSync as mkdirSync14, readdirSync as readdirSync14, copyFileSync } from "fs";
17723
+ import { join as join24 } from "path";
17079
17724
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
17080
17725
  var USERS_FILE = "users.json";
17081
17726
  var TOKEN_EXPIRY_HOURS = 24;
@@ -17090,7 +17735,7 @@ var AuthManager = class {
17090
17735
  db;
17091
17736
  constructor(baseDir) {
17092
17737
  this.baseDir = baseDir;
17093
- this.usersFile = join21(baseDir, USERS_FILE);
17738
+ this.usersFile = join24(baseDir, USERS_FILE);
17094
17739
  this.db = this.loadOrCreate();
17095
17740
  }
17096
17741
  // ── Public API ─────────────────────────────────────────────────
@@ -17119,9 +17764,9 @@ var AuthManager = class {
17119
17764
  }
17120
17765
  const salt = randomBytes(16).toString("hex");
17121
17766
  const passwordHash = this.hashPassword(password, salt);
17122
- const dataDir = join21(USERS_DIR, username);
17123
- const fullDataDir = join21(this.baseDir, dataDir);
17124
- mkdirSync12(join21(fullDataDir, "history"), { recursive: true });
17767
+ const dataDir = join24(USERS_DIR, username);
17768
+ const fullDataDir = join24(this.baseDir, dataDir);
17769
+ mkdirSync14(join24(fullDataDir, "history"), { recursive: true });
17125
17770
  const user = {
17126
17771
  username,
17127
17772
  passwordHash,
@@ -17236,7 +17881,7 @@ var AuthManager = class {
17236
17881
  getUserDataDir(username) {
17237
17882
  const user = this.db.users.find((u) => u.username === username);
17238
17883
  if (!user) throw new Error(`User not found: ${username}`);
17239
- return join21(this.baseDir, user.dataDir);
17884
+ return join24(this.baseDir, user.dataDir);
17240
17885
  }
17241
17886
  /** List all usernames */
17242
17887
  listUsers() {
@@ -17272,30 +17917,30 @@ var AuthManager = class {
17272
17917
  const err = this.register(username, password);
17273
17918
  if (err) return err;
17274
17919
  const userDir = this.getUserDataDir(username);
17275
- const globalConfig = join21(this.baseDir, "config.json");
17276
- if (existsSync26(globalConfig)) {
17920
+ const globalConfig = join24(this.baseDir, "config.json");
17921
+ if (existsSync29(globalConfig)) {
17277
17922
  try {
17278
- const content = readFileSync20(globalConfig, "utf-8");
17279
- writeFileSync3(join21(userDir, "config.json"), content, "utf-8");
17923
+ const content = readFileSync22(globalConfig, "utf-8");
17924
+ writeFileSync4(join24(userDir, "config.json"), content, "utf-8");
17280
17925
  } catch {
17281
17926
  }
17282
17927
  }
17283
- const globalMemory = join21(this.baseDir, "memory.md");
17284
- if (existsSync26(globalMemory)) {
17928
+ const globalMemory = join24(this.baseDir, "memory.md");
17929
+ if (existsSync29(globalMemory)) {
17285
17930
  try {
17286
- const content = readFileSync20(globalMemory, "utf-8");
17287
- writeFileSync3(join21(userDir, "memory.md"), content, "utf-8");
17931
+ const content = readFileSync22(globalMemory, "utf-8");
17932
+ writeFileSync4(join24(userDir, "memory.md"), content, "utf-8");
17288
17933
  } catch {
17289
17934
  }
17290
17935
  }
17291
- const globalHistory = join21(this.baseDir, "history");
17292
- if (existsSync26(globalHistory)) {
17936
+ const globalHistory = join24(this.baseDir, "history");
17937
+ if (existsSync29(globalHistory)) {
17293
17938
  try {
17294
- const files = readdirSync12(globalHistory).filter((f) => f.endsWith(".json"));
17295
- const userHistory = join21(userDir, "history");
17939
+ const files = readdirSync14(globalHistory).filter((f) => f.endsWith(".json"));
17940
+ const userHistory = join24(userDir, "history");
17296
17941
  for (const f of files) {
17297
17942
  try {
17298
- copyFileSync(join21(globalHistory, f), join21(userHistory, f));
17943
+ copyFileSync(join24(globalHistory, f), join24(userHistory, f));
17299
17944
  } catch {
17300
17945
  }
17301
17946
  }
@@ -17306,9 +17951,9 @@ var AuthManager = class {
17306
17951
  }
17307
17952
  // ── Private methods ────────────────────────────────────────────
17308
17953
  loadOrCreate() {
17309
- if (existsSync26(this.usersFile)) {
17954
+ if (existsSync29(this.usersFile)) {
17310
17955
  try {
17311
- return JSON.parse(readFileSync20(this.usersFile, "utf-8"));
17956
+ return JSON.parse(readFileSync22(this.usersFile, "utf-8"));
17312
17957
  } catch {
17313
17958
  }
17314
17959
  }
@@ -17324,7 +17969,7 @@ var AuthManager = class {
17324
17969
  this.saveDB(this.db);
17325
17970
  }
17326
17971
  saveDB(db) {
17327
- mkdirSync12(this.baseDir, { recursive: true });
17972
+ mkdirSync14(this.baseDir, { recursive: true });
17328
17973
  atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
17329
17974
  }
17330
17975
  /** Legacy hash — kept only for migrating old users (v0.2.x) */
@@ -17356,11 +18001,11 @@ function getModuleDir() {
17356
18001
  if (typeof import.meta?.url === "string") {
17357
18002
  const url = new URL(import.meta.url);
17358
18003
  const filePath = url.pathname.replace(/^\/([A-Z]:)/i, "$1");
17359
- return dirname7(filePath);
18004
+ return dirname8(filePath);
17360
18005
  }
17361
18006
  } catch {
17362
18007
  }
17363
- return resolve10(".");
18008
+ return resolve11(".");
17364
18009
  }
17365
18010
  async function startWebServer(options = {}) {
17366
18011
  const port = options.port ?? 3e3;
@@ -17395,6 +18040,8 @@ async function startWebServer(options = {}) {
17395
18040
  let mcpManager = null;
17396
18041
  const mcpEnabled = config.get("mcpEnabled") !== false;
17397
18042
  const globalMcpServers = mcpEnabled ? config.get("mcpServers") ?? {} : {};
18043
+ const pluginAssets = getActivePluginAssets(config.getConfigDir());
18044
+ const pluginMcpServers = mcpEnabled ? pluginAssets.mcpServers : {};
17398
18045
  const projectMcpResolved = mcpEnabled ? resolveProjectMcpPath() : null;
17399
18046
  let projectMcpServers = {};
17400
18047
  if (projectMcpResolved) {
@@ -17407,7 +18054,7 @@ async function startWebServer(options = {}) {
17407
18054
  console.log(` Skipped to prevent unintended RCE. Run "aicli" then "/mcp trust-project" to approve.`);
17408
18055
  }
17409
18056
  }
17410
- const mergedMcpServers = { ...globalMcpServers, ...projectMcpServers };
18057
+ const mergedMcpServers = { ...globalMcpServers, ...pluginMcpServers, ...projectMcpServers };
17411
18058
  if (!mcpEnabled) {
17412
18059
  console.log(" \u{1F50C} MCP: disabled (config.mcpEnabled=false)");
17413
18060
  }
@@ -17425,10 +18072,10 @@ async function startWebServer(options = {}) {
17425
18072
  }
17426
18073
  }
17427
18074
  let skillManager = null;
17428
- const skillsDir = join22(config.getConfigDir(), SKILLS_DIR_NAME);
17429
- if (existsSync27(skillsDir)) {
18075
+ const skillsDir = join25(config.getConfigDir(), SKILLS_DIR_NAME);
18076
+ if (existsSync30(skillsDir)) {
17430
18077
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
17431
- skillManager.loadSkills();
18078
+ skillManager.loadSkills(pluginAssets.skillDirs);
17432
18079
  const count = skillManager.listSkills().length;
17433
18080
  if (count > 0) {
17434
18081
  console.log(` Skills: ${count} available`);
@@ -17497,18 +18144,18 @@ async function startWebServer(options = {}) {
17497
18144
  next();
17498
18145
  };
17499
18146
  const moduleDir = getModuleDir();
17500
- let clientDir = join22(moduleDir, "web", "client");
17501
- if (!existsSync27(clientDir)) {
17502
- clientDir = join22(moduleDir, "client");
18147
+ let clientDir = join25(moduleDir, "web", "client");
18148
+ if (!existsSync30(clientDir)) {
18149
+ clientDir = join25(moduleDir, "client");
17503
18150
  }
17504
- if (!existsSync27(clientDir)) {
17505
- clientDir = join22(moduleDir, "..", "..", "src", "web", "client");
18151
+ if (!existsSync30(clientDir)) {
18152
+ clientDir = join25(moduleDir, "..", "..", "src", "web", "client");
17506
18153
  }
17507
- if (!existsSync27(clientDir)) {
17508
- clientDir = join22(process.cwd(), "src", "web", "client");
18154
+ if (!existsSync30(clientDir)) {
18155
+ clientDir = join25(process.cwd(), "src", "web", "client");
17509
18156
  }
17510
18157
  console.log(` Static files: ${clientDir}`);
17511
- app.use("/vendor", express.static(join22(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
18158
+ app.use("/vendor", express.static(join25(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
17512
18159
  app.use(express.static(clientDir));
17513
18160
  app.get("/api/status", (_req, res) => {
17514
18161
  res.json({
@@ -17575,10 +18222,10 @@ async function startWebServer(options = {}) {
17575
18222
  app.get("/api/files", requireAuth, (req, res) => {
17576
18223
  const cwd = process.cwd();
17577
18224
  const prefix = req.query.prefix || "";
17578
- const targetDir = join22(cwd, prefix);
18225
+ const targetDir = join25(cwd, prefix);
17579
18226
  try {
17580
- const canonicalTarget = realpathSync(resolve10(targetDir));
17581
- const canonicalCwd = realpathSync(resolve10(cwd));
18227
+ const canonicalTarget = realpathSync(resolve11(targetDir));
18228
+ const canonicalCwd = realpathSync(resolve11(cwd));
17582
18229
  if (!canonicalTarget.startsWith(canonicalCwd + sep3) && canonicalTarget !== canonicalCwd) {
17583
18230
  res.json({ files: [] });
17584
18231
  return;
@@ -17589,10 +18236,10 @@ async function startWebServer(options = {}) {
17589
18236
  }
17590
18237
  try {
17591
18238
  const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
17592
- const entries = readdirSync13(targetDir, { withFileTypes: true });
18239
+ const entries = readdirSync15(targetDir, { withFileTypes: true });
17593
18240
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
17594
18241
  name: e.name,
17595
- path: relative4(cwd, join22(targetDir, e.name)).replace(/\\/g, "/"),
18242
+ path: relative4(cwd, join25(targetDir, e.name)).replace(/\\/g, "/"),
17596
18243
  isDir: e.isDirectory()
17597
18244
  }));
17598
18245
  res.json({ files });
@@ -17628,8 +18275,8 @@ async function startWebServer(options = {}) {
17628
18275
  try {
17629
18276
  const authUser = req._authUser;
17630
18277
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
17631
- const filePath = join22(histDir, `${id}.json`);
17632
- if (!existsSync27(filePath)) {
18278
+ const filePath = join25(histDir, `${id}.json`);
18279
+ if (!existsSync30(filePath)) {
17633
18280
  res.status(404).json({ error: "Session not found" });
17634
18281
  return;
17635
18282
  }
@@ -17644,7 +18291,7 @@ async function startWebServer(options = {}) {
17644
18291
  res.status(404).json({ error: "Session not found" });
17645
18292
  return;
17646
18293
  }
17647
- const data = JSON.parse(readFileSync21(filePath, "utf-8"));
18294
+ const data = JSON.parse(readFileSync23(filePath, "utf-8"));
17648
18295
  res.json({ session: data });
17649
18296
  } catch (err) {
17650
18297
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
@@ -17657,10 +18304,10 @@ async function startWebServer(options = {}) {
17657
18304
  return;
17658
18305
  }
17659
18306
  const cwd = process.cwd();
17660
- const fullPath = resolve10(join22(cwd, filePath));
18307
+ const fullPath = resolve11(join25(cwd, filePath));
17661
18308
  try {
17662
18309
  const canonicalFull = realpathSync(fullPath);
17663
- const canonicalCwd = realpathSync(resolve10(cwd));
18310
+ const canonicalCwd = realpathSync(resolve11(cwd));
17664
18311
  if (!canonicalFull.startsWith(canonicalCwd + sep3) && canonicalFull !== canonicalCwd) {
17665
18312
  res.json({ error: "Access denied" });
17666
18313
  return;
@@ -17670,12 +18317,12 @@ async function startWebServer(options = {}) {
17670
18317
  return;
17671
18318
  }
17672
18319
  try {
17673
- const stat = statSync9(fullPath);
18320
+ const stat = statSync12(fullPath);
17674
18321
  if (stat.size > 512 * 1024) {
17675
18322
  res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
17676
18323
  return;
17677
18324
  }
17678
- const content = readFileSync21(fullPath, "utf-8");
18325
+ const content = readFileSync23(fullPath, "utf-8");
17679
18326
  res.json({ content, size: stat.size });
17680
18327
  } catch {
17681
18328
  res.json({ error: "Cannot read file" });
@@ -17879,7 +18526,7 @@ async function startWebServer(options = {}) {
17879
18526
  });
17880
18527
  const MAX_PORT_ATTEMPTS = 10;
17881
18528
  let actualPort = port;
17882
- const result = await new Promise((resolve11, reject) => {
18529
+ const result = await new Promise((resolve12, reject) => {
17883
18530
  const tryListen = (attempt) => {
17884
18531
  server.once("error", (err) => {
17885
18532
  if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
@@ -17910,7 +18557,7 @@ async function startWebServer(options = {}) {
17910
18557
  }
17911
18558
  console.log(` Press Ctrl+C to stop
17912
18559
  `);
17913
- resolve11({ port: actualPort, host, url });
18560
+ resolve12({ port: actualPort, host, url });
17914
18561
  });
17915
18562
  };
17916
18563
  tryListen(1);
@@ -17930,17 +18577,17 @@ function resolveProjectMcpPath() {
17930
18577
  const cwd = process.cwd();
17931
18578
  const gitRoot = getGitRoot(cwd);
17932
18579
  const projectRoot = gitRoot ?? cwd;
17933
- const configPath = join22(projectRoot, MCP_PROJECT_CONFIG_NAME);
17934
- return existsSync27(configPath) ? configPath : null;
18580
+ const configPath = join25(projectRoot, MCP_PROJECT_CONFIG_NAME);
18581
+ return existsSync30(configPath) ? configPath : null;
17935
18582
  }
17936
18583
  function loadProjectMcpConfig() {
17937
18584
  const cwd = process.cwd();
17938
18585
  const gitRoot = getGitRoot(cwd);
17939
18586
  const projectRoot = gitRoot ?? cwd;
17940
- const configPath = join22(projectRoot, MCP_PROJECT_CONFIG_NAME);
17941
- if (!existsSync27(configPath)) return null;
18587
+ const configPath = join25(projectRoot, MCP_PROJECT_CONFIG_NAME);
18588
+ if (!existsSync30(configPath)) return null;
17942
18589
  try {
17943
- const raw = JSON.parse(readFileSync21(configPath, "utf-8"));
18590
+ const raw = JSON.parse(readFileSync23(configPath, "utf-8"));
17944
18591
  return raw.mcpServers ?? raw;
17945
18592
  } catch {
17946
18593
  return null;