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.
- package/README.md +5 -2
- package/dist/{batch-YCOVKTXD.js → batch-CHAPYRVM.js} +2 -2
- package/dist/chunk-3I4RZYYO.js +638 -0
- package/dist/{chunk-KOPUCJXM.js → chunk-4ZN6L6K5.js} +3 -3
- package/dist/{chunk-VTH7BLXK.js → chunk-64NUW3WL.js} +1 -1
- package/dist/{chunk-QAYOI57M.js → chunk-EGMORH5W.js} +1 -1
- package/dist/{chunk-MGBMNCHG.js → chunk-GEN4O5QH.js} +94 -270
- package/dist/chunk-GGKQHPB3.js +604 -0
- package/dist/{chunk-WKOQ5CYC.js → chunk-KIEZBTVD.js} +1 -1
- package/dist/{chunk-OUAZOE5U.js → chunk-QUFYBR6Q.js} +47 -206
- package/dist/{chunk-XJGEQIYS.js → chunk-U7KVU25H.js} +1 -1
- package/dist/{chunk-WZ3VKLF3.js → chunk-VADHBW7T.js} +1 -1
- package/dist/{ci-52RZIYWB.js → ci-QPP66T53.js} +4 -4
- package/dist/{ci-format-73UXKE65.js → ci-format-5S3EEYRK.js} +2 -2
- package/dist/{constants-DIXAD35W.js → constants-FJOLBABC.js} +1 -1
- package/dist/doctor-cli-KB2LUDF3.js +17 -0
- package/dist/electron-server.js +921 -274
- package/dist/{hub-GIGBITZN.js → hub-DIM7SKKY.js} +1 -1
- package/dist/index.js +192 -200
- package/dist/{pr-KPQ5RPKB.js → pr-YQGH72N6.js} +4 -4
- package/dist/{run-tests-ZDSA3QES.js → run-tests-GEZSSNJM.js} +2 -2
- package/dist/{run-tests-K7QR5QN4.js → run-tests-O76JIBCW.js} +1 -1
- package/dist/{server-QT3SC2KI.js → server-5E2AIXVX.js} +108 -80
- package/dist/{server-22YF3U34.js → server-Z6O3G2LY.js} +9 -9
- package/dist/{task-orchestrator-BHQQCVTY.js → task-orchestrator-5HBW4O64.js} +9 -9
- package/dist/{usage-WZZFSFLM.js → usage-BMK6M5U3.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-524WZOKS.js +0 -166
- package/dist/chunk-SNJAOXFT.js +0 -129
- package/dist/doctor-cli-7GOQPULZ.js +0 -226
package/README.md
CHANGED
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
- **MCP Server Mode** *(v0.4.84+)* — `aicli mcp-serve` reverses ai-cli into an MCP server (JSON-RPC 2.0 over stdio), exposing its 28 built-in tools (incl. `find_symbol` / `search_code` / `run_tests`) to Claude Desktop / Cursor / any MCP client. Opt-in destructive-tool allow, `--tools` whitelist, `--cwd` override
|
|
35
35
|
- **Session Sensitive-Data Redaction** *(v0.4.88+)* — unified redactor scrubs `password=` / `api_key` / bearer tokens / OpenAI-style keys from every message **before it hits disk**. Query text is redacted too, so secrets never reach embeddings or logs. `/security status` + `/security scan` to audit
|
|
36
36
|
- **Human-like Long-Term Memory** *(v0.4.89+, B4)* — semantic index over every past chat session + `recall_memory` AI tool + `/memory rebuild|refresh|status|recall` commands. AI is prompted to auto-recall when it sees "last time" / "之前" / ambiguous references. Reuses the same MiniLM embedder as semantic code search
|
|
37
|
-
- **Governed Persistent Memory** *(v0.4.217+)* — `save_memory` and `/memory add` write auditable `memory.jsonl` entries with id/scope/source/sensitivity/approval/expiry; low-risk entries auto-approve, sensitive entries stay pending until `/memory approve <id>`, and project-scoped memories only inject inside the same project
|
|
37
|
+
- **Governed Persistent Memory** *(v0.4.217+)* — `save_memory` and `/memory add` write auditable `memory.jsonl` entries with id/scope/source/sensitivity/approval/expiry; low-risk entries auto-approve, sensitive entries stay pending until `/memory approve <id>`, and project-scoped memories only inject inside the same project
|
|
38
|
+
- **Package Plugin Ecosystem** *(v0.4.218+)* — install shareable .aicli-plugin/plugin.json packages with skills, hooks, commands, MCP servers, agents, and permission hints; only trusted+enabled plugins load, hooks still require hook trust, and MCP/tools remain under permission profiles/network policy
|
|
38
39
|
- **Web UI Memory Panel** *(v0.4.90+, B4)* — new 🧠 Memory sidebar tab with semantic search across past chats; each hit has **➕ Inject** (quotes the snippet into the chat input as a markdown blockquote so you can review/edit before sending — no silent context injection) and **↗ Load** (jumps to source session). Bulk "Inject top 3" for recall bundles
|
|
39
40
|
- **Streaming Tool Use** — Real-time streaming of AI reasoning and tool calls as they happen
|
|
40
41
|
- **Sub-Agents** — Delegate complex subtasks to isolated child agents with independent tool loops
|
|
@@ -218,7 +219,8 @@ AI autonomously invokes these 27 tools during conversations:
|
|
|
218
219
|
| `/export` | Export session as Markdown or JSON |
|
|
219
220
|
| `/profile` | View/edit your identity (AI knows who you are across all providers) |
|
|
220
221
|
| `/config` | Open configuration wizard |
|
|
221
|
-
|
|
|
222
|
+
| /help | Show all available commands |
|
|
223
|
+
| /plugin | Install, trust, enable, disable, and inspect package plugins |
|
|
222
224
|
|
|
223
225
|
**Multi-line input**: Use `\` at end of line for continuation, or paste multi-line content directly (auto-detected and merged).
|
|
224
226
|
|
|
@@ -561,6 +563,7 @@ node scripts/release.mjs patch -m "..." --dry-run
|
|
|
561
563
|
- [`docs/USAGE.md`](docs/USAGE.md) — Complete reference manual (commands, tools, config)
|
|
562
564
|
- [`docs/TUTORIAL.md`](docs/TUTORIAL.md) — Hands-on tutorial, zero to fluent in an hour
|
|
563
565
|
- [`docs/ADVANCED.md`](docs/ADVANCED.md) — Architecture and internals (for developers)
|
|
566
|
+
- [`docs/MIGRATION.md`](docs/MIGRATION.md) — Migration guide from Claude Code, Codex, Claude Desktop, and Cursor
|
|
564
567
|
- [`docs/RECIPES.md`](docs/RECIPES.md) — Practical recipes by scenario
|
|
565
568
|
- [`docs/SECURITY.md`](docs/SECURITY.md) — **Security model, deployment checklist, audit history.** Read before exposing `aicli web` to a network.
|
|
566
569
|
- [`CHANGELOG.md`](CHANGELOG.md) — Per-version change log
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ConfigManager
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KIEZBTVD.js";
|
|
5
5
|
import "./chunk-TZQHYZKT.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-U7KVU25H.js";
|
|
7
7
|
import {
|
|
8
8
|
atomicWriteFileSync
|
|
9
9
|
} from "./chunk-IW3Q7AE5.js";
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ProviderRegistry
|
|
4
|
+
} from "./chunk-QMXC327F.js";
|
|
5
|
+
import {
|
|
6
|
+
getActivePluginAssets,
|
|
7
|
+
getPendingHookTrust,
|
|
8
|
+
getStatsSnapshot,
|
|
9
|
+
getTopFailingTools,
|
|
10
|
+
getTopUsedTools,
|
|
11
|
+
listHooks,
|
|
12
|
+
listInstalledPlugins,
|
|
13
|
+
resetStats
|
|
14
|
+
} from "./chunk-GGKQHPB3.js";
|
|
15
|
+
import {
|
|
16
|
+
ConfigManager
|
|
17
|
+
} from "./chunk-KIEZBTVD.js";
|
|
18
|
+
import {
|
|
19
|
+
CONFIG_DIR_NAME,
|
|
20
|
+
CONTEXT_FILE_CANDIDATES,
|
|
21
|
+
CONTEXT_FILE_MAX_BYTES,
|
|
22
|
+
DEV_STATE_FILE_NAME,
|
|
23
|
+
MCP_PROJECT_CONFIG_NAME,
|
|
24
|
+
MEMORY_FILE_NAME,
|
|
25
|
+
VERSION
|
|
26
|
+
} from "./chunk-U7KVU25H.js";
|
|
27
|
+
import {
|
|
28
|
+
getGitRoot
|
|
29
|
+
} from "./chunk-HOSJZMQS.js";
|
|
30
|
+
|
|
31
|
+
// src/diagnostics/doctor-report.ts
|
|
32
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
33
|
+
import { join as join3 } from "path";
|
|
34
|
+
import { arch as arch2, platform as platform2, release as release2 } from "os";
|
|
35
|
+
|
|
36
|
+
// src/core/context-files.ts
|
|
37
|
+
import { existsSync, readFileSync } from "fs";
|
|
38
|
+
import { join, relative, resolve } from "path";
|
|
39
|
+
function uniqueNonEmpty(values) {
|
|
40
|
+
const seen = /* @__PURE__ */ new Set();
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const value of values) {
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
45
|
+
seen.add(trimmed);
|
|
46
|
+
out.push(trimmed);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function buildContextFileCandidates(fallbackFilenames = []) {
|
|
51
|
+
return uniqueNonEmpty([...CONTEXT_FILE_CANDIDATES, ...fallbackFilenames]);
|
|
52
|
+
}
|
|
53
|
+
function displayPath(filePath, cwd) {
|
|
54
|
+
const rel = relative(cwd, filePath);
|
|
55
|
+
if (rel && !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\")) {
|
|
56
|
+
return rel;
|
|
57
|
+
}
|
|
58
|
+
return filePath;
|
|
59
|
+
}
|
|
60
|
+
function isInsideOrEqual(parent, child) {
|
|
61
|
+
const rel = relative(resolve(parent), resolve(child));
|
|
62
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\");
|
|
63
|
+
}
|
|
64
|
+
function readContextFile(level, filePath, cwd, maxBytes) {
|
|
65
|
+
const name = filePath.split(/[\\/]/).pop() ?? filePath;
|
|
66
|
+
const shown = displayPath(filePath, cwd);
|
|
67
|
+
try {
|
|
68
|
+
const raw = readFileSync(filePath);
|
|
69
|
+
const byteCount = raw.byteLength;
|
|
70
|
+
if (byteCount === 0) {
|
|
71
|
+
return {
|
|
72
|
+
layer: null,
|
|
73
|
+
skipped: { level, filePath, displayPath: shown, fileName: name, reason: "empty" }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const truncated = byteCount > maxBytes;
|
|
77
|
+
const bytes = truncated ? raw.subarray(0, maxBytes) : raw;
|
|
78
|
+
const content = bytes.toString("utf-8").trim();
|
|
79
|
+
if (!content) {
|
|
80
|
+
return {
|
|
81
|
+
layer: null,
|
|
82
|
+
skipped: { level, filePath, displayPath: shown, fileName: name, reason: "empty" }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
layer: {
|
|
87
|
+
level: level === "single" ? "project" : level,
|
|
88
|
+
filePath,
|
|
89
|
+
displayPath: shown,
|
|
90
|
+
fileName: name,
|
|
91
|
+
content,
|
|
92
|
+
charCount: content.length,
|
|
93
|
+
byteCount,
|
|
94
|
+
truncated
|
|
95
|
+
},
|
|
96
|
+
skipped: truncated ? { level, filePath, displayPath: shown, fileName: name, reason: "too-large", message: `truncated to ${maxBytes} bytes` } : null
|
|
97
|
+
};
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
layer: null,
|
|
101
|
+
skipped: {
|
|
102
|
+
level,
|
|
103
|
+
filePath,
|
|
104
|
+
displayPath: shown,
|
|
105
|
+
fileName: name,
|
|
106
|
+
reason: "read-error",
|
|
107
|
+
message: err instanceof Error ? err.message : String(err)
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
|
|
113
|
+
const skipped = [];
|
|
114
|
+
for (const candidate of candidates) {
|
|
115
|
+
const filePath = join(dir, candidate);
|
|
116
|
+
if (!existsSync(filePath)) continue;
|
|
117
|
+
const result = readContextFile(level, filePath, cwd, maxBytes);
|
|
118
|
+
if (result.skipped) skipped.push(result.skipped);
|
|
119
|
+
if (result.layer) return { layer: result.layer, skipped };
|
|
120
|
+
}
|
|
121
|
+
return { layer: null, skipped };
|
|
122
|
+
}
|
|
123
|
+
function loadContextFiles(options) {
|
|
124
|
+
const cwd = resolve(options.cwd);
|
|
125
|
+
const maxBytes = options.maxBytes ?? CONTEXT_FILE_MAX_BYTES;
|
|
126
|
+
const candidates = options.candidates ?? buildContextFileCandidates(options.fallbackFilenames);
|
|
127
|
+
const skipped = [];
|
|
128
|
+
if (options.setting === false) {
|
|
129
|
+
return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
|
|
130
|
+
}
|
|
131
|
+
if (options.setting && options.setting !== "auto") {
|
|
132
|
+
const filePath = resolve(cwd, String(options.setting));
|
|
133
|
+
if (!isInsideOrEqual(cwd, filePath)) {
|
|
134
|
+
skipped.push({
|
|
135
|
+
level: "single",
|
|
136
|
+
filePath,
|
|
137
|
+
displayPath: filePath,
|
|
138
|
+
fileName: String(options.setting),
|
|
139
|
+
reason: "outside-cwd"
|
|
140
|
+
});
|
|
141
|
+
return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
|
|
142
|
+
}
|
|
143
|
+
if (!existsSync(filePath)) {
|
|
144
|
+
skipped.push({
|
|
145
|
+
level: "single",
|
|
146
|
+
filePath,
|
|
147
|
+
displayPath: displayPath(filePath, cwd),
|
|
148
|
+
fileName: String(options.setting),
|
|
149
|
+
reason: "missing"
|
|
150
|
+
});
|
|
151
|
+
return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
|
|
152
|
+
}
|
|
153
|
+
const result = readContextFile("single", filePath, cwd, maxBytes);
|
|
154
|
+
if (result.skipped) skipped.push(result.skipped);
|
|
155
|
+
const layers2 = result.layer ? [result.layer] : [];
|
|
156
|
+
const mergedContent2 = layers2.map((l) => l.content).join("\n\n---\n\n");
|
|
157
|
+
return {
|
|
158
|
+
layers: layers2,
|
|
159
|
+
mergedContent: mergedContent2,
|
|
160
|
+
skipped,
|
|
161
|
+
totalChars: layers2.reduce((sum, l) => sum + l.charCount, 0),
|
|
162
|
+
totalBytes: layers2.reduce((sum, l) => sum + l.byteCount, 0),
|
|
163
|
+
maxBytes,
|
|
164
|
+
candidates
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const layers = [];
|
|
168
|
+
for (const [level, dir] of [
|
|
169
|
+
["global", options.configDir],
|
|
170
|
+
["project", options.projectRoot]
|
|
171
|
+
]) {
|
|
172
|
+
const result = findFirstContextFile(level, dir, cwd, candidates, maxBytes);
|
|
173
|
+
skipped.push(...result.skipped);
|
|
174
|
+
if (result.layer) layers.push(result.layer);
|
|
175
|
+
}
|
|
176
|
+
if (resolve(options.cwd) !== resolve(options.projectRoot)) {
|
|
177
|
+
const result = findFirstContextFile("local", options.cwd, cwd, candidates, maxBytes);
|
|
178
|
+
skipped.push(...result.skipped);
|
|
179
|
+
if (result.layer) layers.push(result.layer);
|
|
180
|
+
}
|
|
181
|
+
const mergedContent = layers.map((l) => l.content).join("\n\n---\n\n");
|
|
182
|
+
return {
|
|
183
|
+
layers,
|
|
184
|
+
mergedContent,
|
|
185
|
+
skipped,
|
|
186
|
+
totalChars: layers.reduce((sum, l) => sum + l.charCount, 0),
|
|
187
|
+
totalBytes: layers.reduce((sum, l) => sum + l.byteCount, 0),
|
|
188
|
+
maxBytes,
|
|
189
|
+
candidates
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/diagnostics/crash-log.ts
|
|
194
|
+
import {
|
|
195
|
+
existsSync as existsSync2,
|
|
196
|
+
mkdirSync,
|
|
197
|
+
readdirSync,
|
|
198
|
+
readFileSync as readFileSync2,
|
|
199
|
+
statSync,
|
|
200
|
+
unlinkSync,
|
|
201
|
+
writeFileSync
|
|
202
|
+
} from "fs";
|
|
203
|
+
import { join as join2 } from "path";
|
|
204
|
+
import { homedir, platform, release, arch } from "os";
|
|
205
|
+
var LOGS_DIR_NAME = "logs";
|
|
206
|
+
var CRASH_LOG_PREFIX = "crash-";
|
|
207
|
+
var MAX_CRASH_FILES = 20;
|
|
208
|
+
function getLogsDir(configDir) {
|
|
209
|
+
const base = configDir ?? join2(homedir(), CONFIG_DIR_NAME);
|
|
210
|
+
return join2(base, LOGS_DIR_NAME);
|
|
211
|
+
}
|
|
212
|
+
function ensureLogsDir(configDir) {
|
|
213
|
+
const dir = getLogsDir(configDir);
|
|
214
|
+
if (!existsSync2(dir)) {
|
|
215
|
+
mkdirSync(dir, { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
return dir;
|
|
218
|
+
}
|
|
219
|
+
function isoTimestampForFilename(d = /* @__PURE__ */ new Date()) {
|
|
220
|
+
return d.toISOString().replace(/[:.]/g, "-");
|
|
221
|
+
}
|
|
222
|
+
function writeCrashLog(err, context, configDir) {
|
|
223
|
+
try {
|
|
224
|
+
const dir = ensureLogsDir(configDir);
|
|
225
|
+
const ts = /* @__PURE__ */ new Date();
|
|
226
|
+
const file = join2(dir, `${CRASH_LOG_PREFIX}${isoTimestampForFilename(ts)}.log`);
|
|
227
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
228
|
+
const errStack = err instanceof Error ? err.stack ?? "(no stack)" : "(non-Error thrown)";
|
|
229
|
+
const lines = [
|
|
230
|
+
`# ai-cli crash log`,
|
|
231
|
+
`timestamp: ${ts.toISOString()}`,
|
|
232
|
+
`kind: ${context.kind}`,
|
|
233
|
+
`version: ${VERSION}`,
|
|
234
|
+
`node: ${process.version}`,
|
|
235
|
+
`platform: ${platform()} ${release()} (${arch()})`,
|
|
236
|
+
`pid: ${process.pid}`,
|
|
237
|
+
context.provider ? `provider: ${context.provider}` : "",
|
|
238
|
+
context.model ? `model: ${context.model}` : "",
|
|
239
|
+
context.lastAction ? `lastAction: ${truncate(context.lastAction, 500)}` : "",
|
|
240
|
+
"",
|
|
241
|
+
`## message`,
|
|
242
|
+
errMessage,
|
|
243
|
+
"",
|
|
244
|
+
`## stack`,
|
|
245
|
+
errStack,
|
|
246
|
+
""
|
|
247
|
+
].filter(Boolean);
|
|
248
|
+
writeFileSync(file, lines.join("\n"), "utf-8");
|
|
249
|
+
pruneOldCrashes(configDir);
|
|
250
|
+
return file;
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function truncate(s, n) {
|
|
256
|
+
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
257
|
+
}
|
|
258
|
+
function pruneOldCrashes(configDir) {
|
|
259
|
+
try {
|
|
260
|
+
const dir = getLogsDir(configDir);
|
|
261
|
+
if (!existsSync2(dir)) return;
|
|
262
|
+
const entries = readdirSync(dir).filter((f) => f.startsWith(CRASH_LOG_PREFIX) && f.endsWith(".log")).map((f) => {
|
|
263
|
+
const full = join2(dir, f);
|
|
264
|
+
let mtime = 0;
|
|
265
|
+
try {
|
|
266
|
+
mtime = statSync(full).mtimeMs;
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
return { file: full, mtime };
|
|
270
|
+
}).sort((a, b) => b.mtime - a.mtime);
|
|
271
|
+
for (const e of entries.slice(MAX_CRASH_FILES)) {
|
|
272
|
+
try {
|
|
273
|
+
unlinkSync(e.file);
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function listRecentCrashes(limit = 10, configDir) {
|
|
281
|
+
const dir = getLogsDir(configDir);
|
|
282
|
+
if (!existsSync2(dir)) return [];
|
|
283
|
+
const files = [];
|
|
284
|
+
let names;
|
|
285
|
+
try {
|
|
286
|
+
names = readdirSync(dir);
|
|
287
|
+
} catch {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
for (const name of names) {
|
|
291
|
+
if (!name.startsWith(CRASH_LOG_PREFIX) || !name.endsWith(".log")) continue;
|
|
292
|
+
const full = join2(dir, name);
|
|
293
|
+
try {
|
|
294
|
+
const st = statSync(full);
|
|
295
|
+
const head = readFileSync2(full, "utf-8").split("\n").slice(0, 20);
|
|
296
|
+
const tsLine = head.find((l) => l.startsWith("timestamp:"));
|
|
297
|
+
const kindLine = head.find((l) => l.startsWith("kind:"));
|
|
298
|
+
const msgIdx = head.findIndex((l) => l.trim() === "## message");
|
|
299
|
+
const message = msgIdx >= 0 && head[msgIdx + 1] ? head[msgIdx + 1] : "";
|
|
300
|
+
files.push({
|
|
301
|
+
file: full,
|
|
302
|
+
timestamp: tsLine ? tsLine.replace("timestamp:", "").trim() : st.mtime.toISOString(),
|
|
303
|
+
kind: kindLine ? kindLine.replace("kind:", "").trim() : "unknown",
|
|
304
|
+
message: truncate(message, 200),
|
|
305
|
+
sizeBytes: st.size
|
|
306
|
+
});
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
|
|
311
|
+
return files.slice(0, limit);
|
|
312
|
+
}
|
|
313
|
+
function getConfigDirUsage(configDir) {
|
|
314
|
+
const base = configDir ?? join2(homedir(), CONFIG_DIR_NAME);
|
|
315
|
+
if (!existsSync2(base)) return { totalBytes: 0, entries: [] };
|
|
316
|
+
const entries = [];
|
|
317
|
+
let total = 0;
|
|
318
|
+
let names;
|
|
319
|
+
try {
|
|
320
|
+
names = readdirSync(base);
|
|
321
|
+
} catch {
|
|
322
|
+
return { totalBytes: 0, entries: [] };
|
|
323
|
+
}
|
|
324
|
+
for (const name of names) {
|
|
325
|
+
const full = join2(base, name);
|
|
326
|
+
const bytes = dirSize(full);
|
|
327
|
+
entries.push({ name, bytes });
|
|
328
|
+
total += bytes;
|
|
329
|
+
}
|
|
330
|
+
entries.sort((a, b) => b.bytes - a.bytes);
|
|
331
|
+
return { totalBytes: total, entries };
|
|
332
|
+
}
|
|
333
|
+
function dirSize(path) {
|
|
334
|
+
try {
|
|
335
|
+
const st = statSync(path);
|
|
336
|
+
if (st.isFile()) return st.size;
|
|
337
|
+
if (!st.isDirectory()) return 0;
|
|
338
|
+
let sum = 0;
|
|
339
|
+
for (const name of readdirSync(path)) {
|
|
340
|
+
sum += dirSize(join2(path, name));
|
|
341
|
+
}
|
|
342
|
+
return sum;
|
|
343
|
+
} catch {
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/diagnostics/doctor-report.ts
|
|
349
|
+
function checkFile(label, path) {
|
|
350
|
+
const exists = existsSync3(path);
|
|
351
|
+
let sizeBytes = null;
|
|
352
|
+
if (exists) {
|
|
353
|
+
try {
|
|
354
|
+
sizeBytes = statSync2(path).size;
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return { label, path, exists, sizeBytes };
|
|
359
|
+
}
|
|
360
|
+
function buildDoctorReport(options) {
|
|
361
|
+
const config = options.config;
|
|
362
|
+
const cwd = options.cwd ?? process.cwd();
|
|
363
|
+
const configDir = config.getConfigDir();
|
|
364
|
+
const projectRoot = getGitRoot(cwd) ?? cwd;
|
|
365
|
+
const contextCfg = config.get("context");
|
|
366
|
+
const contextSetting = config.get("contextFile");
|
|
367
|
+
const context = loadContextFiles({
|
|
368
|
+
cwd,
|
|
369
|
+
configDir,
|
|
370
|
+
projectRoot,
|
|
371
|
+
setting: contextSetting,
|
|
372
|
+
maxBytes: contextCfg.projectDocMaxBytes,
|
|
373
|
+
fallbackFilenames: contextCfg.projectDocFallbackFilenames
|
|
374
|
+
});
|
|
375
|
+
const hooksConfig = config.get("hooks");
|
|
376
|
+
const hooks = listHooks(hooksConfig, configDir);
|
|
377
|
+
const pendingHooks = getPendingHookTrust(hooksConfig, configDir);
|
|
378
|
+
const plugins = listInstalledPlugins(configDir);
|
|
379
|
+
const activeAssets = getActivePluginAssets(configDir);
|
|
380
|
+
const network = config.get("networkPolicy");
|
|
381
|
+
const mcpServers = config.get("mcpServers");
|
|
382
|
+
const allStats = getStatsSnapshot();
|
|
383
|
+
return {
|
|
384
|
+
version: VERSION,
|
|
385
|
+
npmCheck: options.npmCheck,
|
|
386
|
+
node: process.version,
|
|
387
|
+
platform: `${platform2()} ${release2()} (${arch2()})`,
|
|
388
|
+
cwd,
|
|
389
|
+
projectRoot,
|
|
390
|
+
configDir,
|
|
391
|
+
providers: options.providers.listAll().map((p) => ({ id: p.id, displayName: p.displayName, configured: p.configured })),
|
|
392
|
+
files: [
|
|
393
|
+
checkFile("config.json", join3(configDir, "config.json")),
|
|
394
|
+
checkFile("memory.md", join3(configDir, MEMORY_FILE_NAME)),
|
|
395
|
+
checkFile("memory.jsonl", join3(configDir, "memory.jsonl")),
|
|
396
|
+
checkFile("dev-state.md", join3(configDir, DEV_STATE_FILE_NAME)),
|
|
397
|
+
checkFile("hooks-trust.json", join3(configDir, "hooks-trust.json")),
|
|
398
|
+
checkFile("plugin-state.json", join3(configDir, "plugins", "plugin-state.json"))
|
|
399
|
+
],
|
|
400
|
+
context: {
|
|
401
|
+
enabled: contextSetting !== false,
|
|
402
|
+
setting: contextSetting,
|
|
403
|
+
candidates: context.candidates,
|
|
404
|
+
maxBytes: context.maxBytes,
|
|
405
|
+
layers: context.layers.map((l) => ({ level: l.level, fileName: l.fileName, displayPath: l.displayPath, bytes: l.byteCount, chars: l.charCount, truncated: l.truncated })),
|
|
406
|
+
skipped: context.skipped.map((s) => ({ level: s.level, fileName: s.fileName, displayPath: s.displayPath, reason: s.reason, message: s.message }))
|
|
407
|
+
},
|
|
408
|
+
permissions: {
|
|
409
|
+
defaultProfile: config.get("defaultPermissionProfile"),
|
|
410
|
+
allowedProfiles: config.get("allowedPermissionProfiles"),
|
|
411
|
+
customProfileCount: Object.keys(config.get("permissionProfiles")).length,
|
|
412
|
+
legacyDefaultAction: config.get("defaultPermission"),
|
|
413
|
+
ruleCount: config.get("permissionRules").length
|
|
414
|
+
},
|
|
415
|
+
networkPolicy: {
|
|
416
|
+
enabled: network.enabled,
|
|
417
|
+
defaultAction: network.defaultAction,
|
|
418
|
+
allowDomains: network.allowDomains.length,
|
|
419
|
+
denyDomains: network.denyDomains.length,
|
|
420
|
+
allowPorts: network.allowPorts.length,
|
|
421
|
+
denyPorts: network.denyPorts.length,
|
|
422
|
+
allowPrivateNetwork: network.allowPrivateNetwork,
|
|
423
|
+
toolOverrides: Object.fromEntries(Object.entries(network.tools).filter(([, v]) => v !== void 0))
|
|
424
|
+
},
|
|
425
|
+
hooks: {
|
|
426
|
+
enabled: hooksConfig?.enabled !== false,
|
|
427
|
+
total: hooks.length,
|
|
428
|
+
project: hooks.filter((h) => h.source === "project").length,
|
|
429
|
+
pendingTrust: pendingHooks.length,
|
|
430
|
+
disabled: hooks.filter((h) => h.disabled).length
|
|
431
|
+
},
|
|
432
|
+
plugins: {
|
|
433
|
+
root: join3(configDir, "plugins"),
|
|
434
|
+
installed: plugins.length,
|
|
435
|
+
active: activeAssets.plugins.length,
|
|
436
|
+
invalid: plugins.filter((p) => !p.valid).length,
|
|
437
|
+
untrusted: plugins.filter((p) => p.valid && !p.trusted).length,
|
|
438
|
+
disabled: plugins.filter((p) => p.valid && !p.enabled).length,
|
|
439
|
+
mcpServers: Object.keys(activeAssets.mcpServers).length,
|
|
440
|
+
skillDirs: activeAssets.skillDirs.length,
|
|
441
|
+
commandDirs: activeAssets.commandDirs.length,
|
|
442
|
+
agentDirs: activeAssets.agentDirs.length,
|
|
443
|
+
entries: plugins.map((p) => ({ name: p.name, version: p.manifest.version, enabled: p.enabled, trusted: p.trusted, valid: p.valid, error: p.error }))
|
|
444
|
+
},
|
|
445
|
+
mcp: {
|
|
446
|
+
enabled: config.get("mcpEnabled"),
|
|
447
|
+
configured: Object.keys(mcpServers).length,
|
|
448
|
+
projectConfigExists: existsSync3(join3(projectRoot, MCP_PROJECT_CONFIG_NAME)),
|
|
449
|
+
statuses: options.mcpStatuses ?? []
|
|
450
|
+
},
|
|
451
|
+
recentCrashes: listRecentCrashes(5, configDir),
|
|
452
|
+
diskUsage: getConfigDirUsage(configDir),
|
|
453
|
+
toolStats: {
|
|
454
|
+
totalCalls: allStats.reduce((a, b) => a + b.calls, 0),
|
|
455
|
+
totalFailures: allStats.reduce((a, b) => a + b.failures, 0),
|
|
456
|
+
topUsed: getTopUsedTools(5),
|
|
457
|
+
topFailing: getTopFailingTools(5)
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/diagnostics/doctor-cli.ts
|
|
463
|
+
function compareVersion(a, b) {
|
|
464
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
465
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
466
|
+
const len = Math.max(pa.length, pb.length);
|
|
467
|
+
for (let i = 0; i < len; i++) {
|
|
468
|
+
const da = pa[i] ?? 0;
|
|
469
|
+
const db = pb[i] ?? 0;
|
|
470
|
+
if (da !== db) return da - db;
|
|
471
|
+
}
|
|
472
|
+
return 0;
|
|
473
|
+
}
|
|
474
|
+
async function checkNpmLatest() {
|
|
475
|
+
const controller = new AbortController();
|
|
476
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
477
|
+
try {
|
|
478
|
+
const res = await fetch("https://registry.npmjs.org/jinzd-ai-cli/latest", {
|
|
479
|
+
signal: controller.signal,
|
|
480
|
+
headers: { accept: "application/json" }
|
|
481
|
+
});
|
|
482
|
+
if (!res.ok) return { latest: null, status: "skipped", reason: `HTTP ${res.status}` };
|
|
483
|
+
const data = await res.json();
|
|
484
|
+
const latest = typeof data.version === "string" ? data.version : null;
|
|
485
|
+
if (!latest) return { latest: null, status: "skipped", reason: "no version in response" };
|
|
486
|
+
return { latest, status: compareVersion(latest, VERSION) > 0 ? "outdated" : "up-to-date" };
|
|
487
|
+
} catch (err) {
|
|
488
|
+
const reason = err?.name === "AbortError" ? "timeout (3s)" : err?.message ?? "network error";
|
|
489
|
+
return { latest: null, status: "skipped", reason };
|
|
490
|
+
} finally {
|
|
491
|
+
clearTimeout(timer);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function collect() {
|
|
495
|
+
const config = new ConfigManager();
|
|
496
|
+
const registry = new ProviderRegistry();
|
|
497
|
+
await registry.initialize(
|
|
498
|
+
(id) => config.getApiKey(id),
|
|
499
|
+
(id) => ({ baseUrl: config.get("customBaseUrls")[id], timeout: config.get("timeouts")[id] }),
|
|
500
|
+
config.get("customProviders")
|
|
501
|
+
);
|
|
502
|
+
return buildDoctorReport({ config, providers: registry, npmCheck: await checkNpmLatest() });
|
|
503
|
+
}
|
|
504
|
+
function formatBytes(n) {
|
|
505
|
+
if (n < 1024) return `${n} B`;
|
|
506
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
507
|
+
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
508
|
+
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
509
|
+
}
|
|
510
|
+
function formatDoctorReport(report, ansi = true) {
|
|
511
|
+
const wrap = (code, s) => ansi ? `${code}${s}\x1B[0m` : s;
|
|
512
|
+
const B = (s) => wrap("\x1B[1m", s);
|
|
513
|
+
const D = (s) => wrap("\x1B[2m", s);
|
|
514
|
+
const G = (s) => wrap("\x1B[32m", s);
|
|
515
|
+
const Y = (s) => wrap("\x1B[33m", s);
|
|
516
|
+
const R = (s) => wrap("\x1B[31m", s);
|
|
517
|
+
const out = [];
|
|
518
|
+
out.push("");
|
|
519
|
+
out.push(B("\u{1FA7A} AI-CLI Health Check"));
|
|
520
|
+
out.push("");
|
|
521
|
+
const nc = report.npmCheck;
|
|
522
|
+
let versionLine = ` version: ${report.version}`;
|
|
523
|
+
if (nc?.status === "up-to-date") versionLine += ` ${D("(latest on npm)")}`;
|
|
524
|
+
else if (nc?.status === "outdated" && nc.latest) versionLine += ` ${Y(`\u2192 ${nc.latest} available`)} ${D("(npm i -g jinzd-ai-cli@latest)")}`;
|
|
525
|
+
else if (nc?.status === "skipped") versionLine += ` ${D(`(npm check skipped: ${nc.reason ?? "unknown"})`)}`;
|
|
526
|
+
out.push(versionLine);
|
|
527
|
+
out.push(` node: ${report.node}`);
|
|
528
|
+
out.push(` platform: ${report.platform}`);
|
|
529
|
+
out.push(` cwd: ${report.cwd}`);
|
|
530
|
+
out.push(` project: ${report.projectRoot}`);
|
|
531
|
+
out.push(` config: ${report.configDir}`);
|
|
532
|
+
out.push("");
|
|
533
|
+
out.push(B("API Keys:"));
|
|
534
|
+
for (const p of report.providers) {
|
|
535
|
+
out.push(` ${p.configured ? G("\u2713") : D("\u25CB")} ${p.id.padEnd(14)} ${p.configured ? G("configured") : D("not configured")}`);
|
|
536
|
+
}
|
|
537
|
+
out.push("");
|
|
538
|
+
out.push(B("Context Files:"));
|
|
539
|
+
out.push(` status: ${report.context.enabled ? G("enabled") : D("disabled")} \xB7 max ${formatBytes(report.context.maxBytes)}`);
|
|
540
|
+
if (report.context.layers.length === 0) out.push(` ${D("(no context files loaded)")}`);
|
|
541
|
+
for (const l of report.context.layers) {
|
|
542
|
+
out.push(` ${G("\u2713")} ${l.level.padEnd(7)} ${l.displayPath} ${D(`(${formatBytes(l.bytes)}${l.truncated ? ", truncated" : ""})`)}`);
|
|
543
|
+
}
|
|
544
|
+
if (report.context.skipped.length > 0) {
|
|
545
|
+
out.push(` ${D("skipped:")}`);
|
|
546
|
+
for (const s of report.context.skipped.slice(0, 5)) {
|
|
547
|
+
out.push(` ${Y("!")} ${s.level}:${s.fileName} ${s.reason}${s.message ? ` \xB7 ${s.message}` : ""}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
out.push("");
|
|
551
|
+
out.push(B("Permissions / Network:"));
|
|
552
|
+
out.push(` profile: ${report.permissions.defaultProfile} \xB7 allowed ${report.permissions.allowedProfiles.join(", ")} \xB7 rules ${report.permissions.ruleCount} \xB7 custom profiles ${report.permissions.customProfileCount}`);
|
|
553
|
+
const np = report.networkPolicy;
|
|
554
|
+
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"}`);
|
|
555
|
+
const overrides = Object.entries(np.toolOverrides).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
556
|
+
if (overrides) out.push(` tool overrides: ${overrides}`);
|
|
557
|
+
out.push("");
|
|
558
|
+
out.push(B("Hooks / Plugins:"));
|
|
559
|
+
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}`);
|
|
560
|
+
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}`);
|
|
561
|
+
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)`);
|
|
562
|
+
if (report.plugins.entries.length > 0) {
|
|
563
|
+
for (const p of report.plugins.entries.slice(0, 5)) {
|
|
564
|
+
const state = p.valid ? `${p.enabled ? "enabled" : "disabled"} \xB7 ${p.trusted ? "trusted" : "untrusted"}` : `invalid \xB7 ${p.error ?? ""}`;
|
|
565
|
+
out.push(` ${p.valid ? G("\u2713") : R("\u2717")} ${p.name}@${p.version} ${D(state)}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
out.push("");
|
|
569
|
+
out.push(B("MCP Servers:"));
|
|
570
|
+
out.push(` global configured: ${report.mcp.configured} \xB7 project .mcp.json: ${report.mcp.projectConfigExists ? "yes" : "no"} \xB7 mcpEnabled: ${report.mcp.enabled ? "true" : "false"}`);
|
|
571
|
+
if (report.mcp.statuses.length === 0) out.push(` ${D("(no connected manager status in this mode)")}`);
|
|
572
|
+
for (const s of report.mcp.statuses) {
|
|
573
|
+
const state = s.connected ? `${G("connected")} \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `${R("disconnected")}${s.error ? D(` \xB7 ${s.error}`) : ""}`;
|
|
574
|
+
out.push(` ${s.connected ? G("\u2713") : R("\u2717")} ${s.serverId.padEnd(16)} ${state}`);
|
|
575
|
+
}
|
|
576
|
+
out.push("");
|
|
577
|
+
out.push(B("Config Files:"));
|
|
578
|
+
for (const f of report.files) {
|
|
579
|
+
const extra = f.exists && f.sizeBytes !== null ? ` ${D(`(${formatBytes(f.sizeBytes)})`)}` : "";
|
|
580
|
+
out.push(` ${f.exists ? G("\u2713") : D("\u2013")} ${f.label.padEnd(16)} ${f.exists ? f.path : D("(not found)")}${extra}`);
|
|
581
|
+
}
|
|
582
|
+
out.push("");
|
|
583
|
+
out.push(B("Recent Crashes (last 5):"));
|
|
584
|
+
if (report.recentCrashes.length === 0) out.push(` ${D("(none - clean!)")}`);
|
|
585
|
+
for (const c of report.recentCrashes) {
|
|
586
|
+
out.push(` ${R("\u2717")} ${c.timestamp} ${Y(c.kind)}`);
|
|
587
|
+
out.push(` ${D(c.message)}`);
|
|
588
|
+
out.push(` ${D(c.file)}`);
|
|
589
|
+
}
|
|
590
|
+
out.push("");
|
|
591
|
+
const ts = report.toolStats;
|
|
592
|
+
out.push(B("Tool Usage:"));
|
|
593
|
+
out.push(` total: ${ts.totalCalls} calls \xB7 ${ts.totalFailures} failures (${ts.totalCalls ? (ts.totalFailures * 100 / ts.totalCalls).toFixed(1) : "0.0"}%)`);
|
|
594
|
+
if (ts.topUsed.length > 0) {
|
|
595
|
+
out.push(` ${D("top used:")}`);
|
|
596
|
+
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`);
|
|
597
|
+
}
|
|
598
|
+
if (ts.topFailing.length > 0) {
|
|
599
|
+
out.push(` ${D("top failing:")}`);
|
|
600
|
+
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));
|
|
601
|
+
}
|
|
602
|
+
out.push("");
|
|
603
|
+
const dirAnnotations = {
|
|
604
|
+
models: "semantic search model -> /index semantic-clear",
|
|
605
|
+
index: "symbol index -> /index clear",
|
|
606
|
+
history: "conversation history",
|
|
607
|
+
logs: "crash logs",
|
|
608
|
+
"memory-index": "chat memory index -> /memory index-clear",
|
|
609
|
+
plugins: "installed package plugins"
|
|
610
|
+
};
|
|
611
|
+
out.push(B("Disk Usage (~/.aicli):"));
|
|
612
|
+
out.push(` total: ${formatBytes(report.diskUsage.totalBytes)}`);
|
|
613
|
+
for (const e of report.diskUsage.entries.slice(0, 10)) {
|
|
614
|
+
const note = dirAnnotations[e.name];
|
|
615
|
+
out.push(` ${e.name.padEnd(18)} ${formatBytes(e.bytes)}${note ? ` ${D(`\xB7 ${note}`)}` : ""}`);
|
|
616
|
+
}
|
|
617
|
+
out.push("");
|
|
618
|
+
out.push(G("\u2713 Health check complete"));
|
|
619
|
+
out.push("");
|
|
620
|
+
return out.join("\n");
|
|
621
|
+
}
|
|
622
|
+
async function runDoctorCli(options = {}) {
|
|
623
|
+
if (options.resetStats) {
|
|
624
|
+
resetStats();
|
|
625
|
+
process.stdout.write("Tool stats reset.\n");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const report = await collect();
|
|
629
|
+
process.stdout.write(options.json ? JSON.stringify(report, null, 2) + "\n" : formatDoctorReport(report, true));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export {
|
|
633
|
+
writeCrashLog,
|
|
634
|
+
loadContextFiles,
|
|
635
|
+
buildDoctorReport,
|
|
636
|
+
formatDoctorReport,
|
|
637
|
+
runDoctorCli
|
|
638
|
+
};
|