@luckychen1993/pm-mcp 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -14
- package/dist/agents-md.js +51 -0
- package/dist/audit.js +25 -21
- package/dist/budget.js +36 -3
- package/dist/cli.js +32 -0
- package/dist/dashboard.js +6 -4
- package/dist/doctor.js +148 -0
- package/dist/git-state.js +75 -0
- package/dist/governance-tools.js +12 -2
- package/dist/index-db.js +40 -0
- package/dist/index-store.js +35 -44
- package/dist/index.js +29 -9
- package/dist/init.js +3 -0
- package/dist/knowledge-tools.js +6 -15
- package/dist/language-adapters.js +11 -10
- package/dist/license.js +28 -2
- package/dist/paths.js +1 -1
- package/dist/probe.js +141 -0
- package/dist/project-fingerprint.js +9 -5
- package/dist/project-tools.js +35 -3
- package/dist/quality-evidence.js +56 -0
- package/dist/quality-store.js +5 -25
- package/dist/registry.js +28 -2
- package/dist/scan-policy.js +34 -0
- package/dist/scan.js +5 -25
- package/dist/search.js +5 -1
- package/dist/security.js +66 -5
- package/dist/semantic-evidence-store.js +22 -0
- package/dist/semantic-parsers.js +18 -0
- package/dist/session-log.js +14 -0
- package/dist/setup-project.js +146 -0
- package/dist/setup.js +136 -18
- package/dist/task-evidence.js +110 -0
- package/dist/task-pagination.js +64 -0
- package/dist/task-tools.js +47 -37
- package/dist/tool-base.js +70 -9
- package/dist/types.js +5 -0
- package/dist/usage-log.js +278 -0
- package/dist/version.js +10 -0
- package/package.json +7 -4
package/dist/tool-base.js
CHANGED
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
import { loadProject } from "./store.js";
|
|
2
2
|
import { IdempotencyKeySchema, runCoalescedRead, runIdempotentWrite, runIdempotentWriteSync } from "./idempotency.js";
|
|
3
|
+
import { drainFoldSavings } from "./budget.js";
|
|
4
|
+
import { estimateTokens, logRuntime, logUsage } from "./usage-log.js";
|
|
5
|
+
/** 工具/资源调用结束后写使用日志(含本次折叠省下的 token 估算);绝不抛错。 */
|
|
6
|
+
export function recordUsage(root, tool, kind, started, outcome) {
|
|
7
|
+
try {
|
|
8
|
+
const folded = drainFoldSavings();
|
|
9
|
+
logUsage(root, {
|
|
10
|
+
ts: new Date().toISOString(),
|
|
11
|
+
tool,
|
|
12
|
+
kind,
|
|
13
|
+
ok: outcome.ok,
|
|
14
|
+
ms: Date.now() - started,
|
|
15
|
+
...(outcome.text !== undefined ? { out_chars: outcome.text.length, est_tokens: estimateTokens(outcome.text) } : {}),
|
|
16
|
+
...(folded ? { folded_chars: folded.chars, folded_tokens: folded.tokens } : {}),
|
|
17
|
+
...(outcome.replayed ? { replayed: true } : {}),
|
|
18
|
+
...(outcome.pending ? { pending: true } : {}),
|
|
19
|
+
...(outcome.uncertain ? { uncertain: true } : {}),
|
|
20
|
+
...(outcome.error ? { error: outcome.error.slice(0, 300) } : {}),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* 使用日志绝不影响工具调用 */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
3
27
|
/** 只读工具:跨 Agent/进程合并完全相同的并行请求,短窗复用结果。 */
|
|
4
|
-
export function toolR(server, root, name, description, schema, fn) {
|
|
5
|
-
server.registerTool(name, { description, inputSchema: schema, annotations: { readOnlyHint: true } }, (async (args) => {
|
|
28
|
+
export function toolR(server, root, name, description, schema, fn, presentation) {
|
|
29
|
+
server.registerTool(name, { description, inputSchema: schema, ...(presentation ? { outputSchema: presentation.outputSchema } : {}), annotations: { readOnlyHint: true } }, (async (args) => {
|
|
30
|
+
const started = Date.now();
|
|
6
31
|
try {
|
|
7
|
-
const result = await runCoalescedRead(root, name, args, fn);
|
|
8
|
-
|
|
32
|
+
const result = await runCoalescedRead(root, presentation ? `${name}:${presentation.cacheVersion ?? "structured-v1"}` : name, args, fn);
|
|
33
|
+
const decoded = presentation?.decode(result.text);
|
|
34
|
+
const text = decoded?.text ?? result.text;
|
|
35
|
+
recordUsage(root, name, "read", started, {
|
|
36
|
+
ok: !result.pending,
|
|
37
|
+
text: decoded ? text + JSON.stringify(decoded.data) : text,
|
|
38
|
+
replayed: result.replayed,
|
|
39
|
+
pending: result.pending,
|
|
40
|
+
uncertain: result.uncertain,
|
|
41
|
+
});
|
|
42
|
+
return { content: [{ type: "text", text }], ...(decoded ? { structuredContent: decoded.data } : {}) };
|
|
9
43
|
}
|
|
10
44
|
catch (error) {
|
|
11
|
-
|
|
45
|
+
const message = error.message;
|
|
46
|
+
recordUsage(root, name, "read", started, { ok: false, error: message });
|
|
47
|
+
logRuntime(root, "error", "tool.error", `${name}: ${message.slice(0, 200)}`);
|
|
48
|
+
return { content: [{ type: "text", text: `错误: ${message}` }], isError: true };
|
|
12
49
|
}
|
|
13
50
|
}));
|
|
14
51
|
}
|
|
@@ -22,26 +59,50 @@ function writeSchema(schema) {
|
|
|
22
59
|
/** 同步写工具:业务幂等占位 + 跨进程账本锁覆盖完整读-改-写。 */
|
|
23
60
|
export function toolW(server, root, name, description, schema, fn, prepare) {
|
|
24
61
|
server.registerTool(name, { description: `${description} ${idempotencyDescription}`, inputSchema: writeSchema(schema), annotations: { readOnlyHint: false } }, ((args) => {
|
|
62
|
+
const started = Date.now();
|
|
25
63
|
try {
|
|
26
64
|
const result = runIdempotentWriteSync(root, name, args, fn, prepare);
|
|
27
65
|
const prefix = result.replayed ? `↩️ 幂等复用 ${result.key}\n` : result.pending ? `⏳ 幂等占位 ${result.key}\n` : "";
|
|
28
|
-
|
|
66
|
+
const text = prefix + result.text;
|
|
67
|
+
recordUsage(root, name, "write", started, {
|
|
68
|
+
ok: !result.pending,
|
|
69
|
+
text,
|
|
70
|
+
replayed: result.replayed,
|
|
71
|
+
pending: result.pending,
|
|
72
|
+
uncertain: result.uncertain,
|
|
73
|
+
});
|
|
74
|
+
return { content: [{ type: "text", text }], ...(result.pending ? { isError: true } : {}) };
|
|
29
75
|
}
|
|
30
76
|
catch (error) {
|
|
31
|
-
|
|
77
|
+
const message = error.message;
|
|
78
|
+
recordUsage(root, name, "write", started, { ok: false, error: message });
|
|
79
|
+
logRuntime(root, "error", "tool.error", `${name}: ${message.slice(0, 200)}`);
|
|
80
|
+
return { content: [{ type: "text", text: `错误: ${message}` }], isError: true };
|
|
32
81
|
}
|
|
33
82
|
}));
|
|
34
83
|
}
|
|
35
84
|
/** 已自带内部锁或异步执行的写工具:先跨进程占位,完成后缓存结果。 */
|
|
36
85
|
export function toolI(server, root, name, description, schema, fn) {
|
|
37
86
|
server.registerTool(name, { description: `${description} ${idempotencyDescription}`, inputSchema: writeSchema(schema), annotations: { readOnlyHint: false } }, (async (args) => {
|
|
87
|
+
const started = Date.now();
|
|
38
88
|
try {
|
|
39
89
|
const result = await runIdempotentWrite(root, name, args, fn);
|
|
40
90
|
const prefix = result.replayed ? `↩️ 幂等复用 ${result.key}\n` : result.pending ? `⏳ 幂等占位 ${result.key}\n` : "";
|
|
41
|
-
|
|
91
|
+
const text = prefix + result.text;
|
|
92
|
+
recordUsage(root, name, "write-async", started, {
|
|
93
|
+
ok: !result.pending,
|
|
94
|
+
text,
|
|
95
|
+
replayed: result.replayed,
|
|
96
|
+
pending: result.pending,
|
|
97
|
+
uncertain: result.uncertain,
|
|
98
|
+
});
|
|
99
|
+
return { content: [{ type: "text", text }], ...(result.pending ? { isError: true } : {}) };
|
|
42
100
|
}
|
|
43
101
|
catch (error) {
|
|
44
|
-
|
|
102
|
+
const message = error.message;
|
|
103
|
+
recordUsage(root, name, "write-async", started, { ok: false, error: message });
|
|
104
|
+
logRuntime(root, "error", "tool.error", `${name}: ${message.slice(0, 200)}`);
|
|
105
|
+
return { content: [{ type: "text", text: `错误: ${message}` }], isError: true };
|
|
45
106
|
}
|
|
46
107
|
}));
|
|
47
108
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { normSep } from "./budget.js";
|
|
3
|
+
import { ScanIgnoreSchema } from "./scan-policy.js";
|
|
4
|
+
import { CompletionEvidenceSchema } from "./quality-evidence.js";
|
|
3
5
|
export const now = () => new Date().toISOString();
|
|
4
6
|
/** 文件清单字段:统一 / 分隔(Windows 反斜杠入库即规范化),缺省空数组 */
|
|
5
7
|
const filesField = () => z.array(z.string()).default([]).transform((list) => list.map(normSep));
|
|
@@ -24,6 +26,8 @@ export const ProjectSchema = z.object({
|
|
|
24
26
|
phase: z.string().default(""),
|
|
25
27
|
/** 登记的模块名(用于复杂度账的"未登记目录"检测) */
|
|
26
28
|
modules: z.array(z.string()).default([]),
|
|
29
|
+
scan_ignore: ScanIgnoreSchema.optional(),
|
|
30
|
+
completion_evidence: CompletionEvidenceSchema.optional(),
|
|
27
31
|
/** 暴露面:local=本机工具 / network=内网服务 / public=公网服务,影响安全告警力度 */
|
|
28
32
|
exposure: ExposureSchema.default("local"),
|
|
29
33
|
/** 项目许可证(SPDX 或名称),用于依赖兼容性对账 */
|
|
@@ -118,6 +122,7 @@ export const SessionSchema = z.object({
|
|
|
118
122
|
author: z.string().default(""),
|
|
119
123
|
summary: z.string().trim().min(1),
|
|
120
124
|
files: filesField(),
|
|
125
|
+
file_hashes: z.record(z.string().regex(/^[a-f0-9]{64}$/)).optional(),
|
|
121
126
|
next_steps: z.array(z.string()).default([]),
|
|
122
127
|
});
|
|
123
128
|
export const SessionsFileSchema = z.object({
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { isInitialized, pmPath } from "./paths.js";
|
|
3
|
+
/**
|
|
4
|
+
* 使用日志 + 运行日志(运行时数据,非账本):
|
|
5
|
+
* - usage-log.jsonl:每次 MCP 工具/资源调用一条(工具名、成败、耗时、输出与折叠省下的 token 估算)。
|
|
6
|
+
* 只记计量不记参数内容,避免敏感信息与大参数进日志。
|
|
7
|
+
* - runtime-log.jsonl:服务级事件(server 就绪、watcher 状态、工具报错、轮转)。
|
|
8
|
+
* 均为追加式 JSONL,超限自动轮转(当前 + .1 共两份),绝不抛错——日志永远不能影响工具调用本身。
|
|
9
|
+
*/
|
|
10
|
+
export const USAGE_LOG = "usage-log.jsonl";
|
|
11
|
+
export const RUNTIME_LOG = "runtime-log.jsonl";
|
|
12
|
+
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
13
|
+
const ROTATE_CHECK_EVERY = 32;
|
|
14
|
+
const rotateCheckCounter = new Map();
|
|
15
|
+
function maxLogBytes() {
|
|
16
|
+
const override = Number(process.env.PM_TEST_LOG_MAX_BYTES);
|
|
17
|
+
return Number.isFinite(override) && override > 0 ? override : DEFAULT_MAX_BYTES;
|
|
18
|
+
}
|
|
19
|
+
function rotatedFile(file) {
|
|
20
|
+
return file.replace(/\.jsonl$/, ".1.jsonl");
|
|
21
|
+
}
|
|
22
|
+
function appendLine(root, file, line) {
|
|
23
|
+
try {
|
|
24
|
+
if (!isInitialized(root))
|
|
25
|
+
return;
|
|
26
|
+
fs.appendFileSync(file, line + "\n", "utf8");
|
|
27
|
+
// 每 N 次写查一次大小(测试覆盖了轮转,此时每次都查)
|
|
28
|
+
const every = process.env.PM_TEST_LOG_MAX_BYTES ? 1 : ROTATE_CHECK_EVERY;
|
|
29
|
+
const n = (rotateCheckCounter.get(file) ?? 0) + 1;
|
|
30
|
+
rotateCheckCounter.set(file, n);
|
|
31
|
+
if (n % every !== 0)
|
|
32
|
+
return;
|
|
33
|
+
if (fs.statSync(file).size <= maxLogBytes())
|
|
34
|
+
return;
|
|
35
|
+
const rotated = rotatedFile(file);
|
|
36
|
+
try {
|
|
37
|
+
fs.rmSync(rotated, { force: true });
|
|
38
|
+
fs.renameSync(file, rotated);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// 多进程并发轮转竞争失败可接受:当前文件继续追加,下次再试
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* 日志绝不影响工具调用 */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function logUsage(root, entry) {
|
|
49
|
+
appendLine(root, pmPath(root, USAGE_LOG), JSON.stringify(entry));
|
|
50
|
+
}
|
|
51
|
+
export function logRuntime(root, level, event, detail) {
|
|
52
|
+
const entry = { ts: new Date().toISOString(), level, event, ...(detail ? { detail } : {}) };
|
|
53
|
+
appendLine(root, pmPath(root, RUNTIME_LOG), JSON.stringify(entry));
|
|
54
|
+
}
|
|
55
|
+
/** 粗估 token 数:CJK 每字≈1 token,其余≈4字符/token(混合中英文经验值,够做趋势判断) */
|
|
56
|
+
export function estimateTokens(text) {
|
|
57
|
+
let cjk = 0;
|
|
58
|
+
for (const ch of text) {
|
|
59
|
+
const c = ch.codePointAt(0);
|
|
60
|
+
if ((c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf) ||
|
|
61
|
+
(c >= 0x3000 && c <= 0x303f) || (c >= 0xff00 && c <= 0xffef))
|
|
62
|
+
cjk += 1;
|
|
63
|
+
}
|
|
64
|
+
return cjk + Math.ceil((text.length - cjk) / 4);
|
|
65
|
+
}
|
|
66
|
+
function readJsonl(root, file, coerce) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const target of [rotatedFile(file), file]) {
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = fs.readFileSync(target, "utf8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const line of raw.split("\n")) {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
if (!trimmed)
|
|
79
|
+
continue;
|
|
80
|
+
try {
|
|
81
|
+
const parsed = coerce(JSON.parse(trimmed));
|
|
82
|
+
if (parsed !== null)
|
|
83
|
+
out.push(parsed);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* 坏行跳过(轮转竞争/手工编辑残留) */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function asString(v) {
|
|
93
|
+
return typeof v === "string" ? v : undefined;
|
|
94
|
+
}
|
|
95
|
+
function coerceUsage(raw) {
|
|
96
|
+
const tool = asString(raw.tool);
|
|
97
|
+
const ts = asString(raw.ts);
|
|
98
|
+
if (!tool || !ts)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
ts,
|
|
102
|
+
tool,
|
|
103
|
+
kind: raw.kind === "write" || raw.kind === "write-async" ? raw.kind : "read",
|
|
104
|
+
ok: raw.ok === true,
|
|
105
|
+
ms: typeof raw.ms === "number" && Number.isFinite(raw.ms) ? raw.ms : 0,
|
|
106
|
+
...(typeof raw.out_chars === "number" ? { out_chars: raw.out_chars } : {}),
|
|
107
|
+
...(typeof raw.est_tokens === "number" ? { est_tokens: raw.est_tokens } : {}),
|
|
108
|
+
...(typeof raw.folded_chars === "number" ? { folded_chars: raw.folded_chars } : {}),
|
|
109
|
+
...(typeof raw.folded_tokens === "number" ? { folded_tokens: raw.folded_tokens } : {}),
|
|
110
|
+
...(raw.replayed === true ? { replayed: true } : {}),
|
|
111
|
+
...(raw.pending === true ? { pending: true } : {}),
|
|
112
|
+
...(raw.uncertain === true ? { uncertain: true } : {}),
|
|
113
|
+
...(asString(raw.error) ? { error: asString(raw.error) } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function coerceRuntime(raw) {
|
|
117
|
+
const ts = asString(raw.ts);
|
|
118
|
+
const event = asString(raw.event);
|
|
119
|
+
const level = raw.level === "info" || raw.level === "warn" || raw.level === "error" ? raw.level : null;
|
|
120
|
+
if (!ts || !event || !level)
|
|
121
|
+
return null;
|
|
122
|
+
return { ts, level, event, ...(asString(raw.detail) ? { detail: asString(raw.detail) } : {}) };
|
|
123
|
+
}
|
|
124
|
+
/** 读全部保留的使用日志(轮转份 + 当前份,按时间序) */
|
|
125
|
+
export function loadUsageEntries(root) {
|
|
126
|
+
return readJsonl(root, pmPath(root, USAGE_LOG), coerceUsage);
|
|
127
|
+
}
|
|
128
|
+
export function loadRuntimeEntries(root) {
|
|
129
|
+
return readJsonl(root, pmPath(root, RUNTIME_LOG), coerceRuntime);
|
|
130
|
+
}
|
|
131
|
+
export function usageSummary(entries) {
|
|
132
|
+
const calls = entries.length;
|
|
133
|
+
const errors = entries.filter((e) => !e.ok).length;
|
|
134
|
+
const totalMs = entries.reduce((sum, e) => sum + e.ms, 0);
|
|
135
|
+
const slowest = entries.reduce((best, e) => (best === null || e.ms > best.ms ? e : best), null);
|
|
136
|
+
const estTokensOut = entries.reduce((sum, e) => sum + (e.est_tokens ?? 0), 0);
|
|
137
|
+
const estTokensSaved = entries.reduce((sum, e) => sum + (e.folded_tokens ?? 0), 0);
|
|
138
|
+
const byTool = new Map();
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
const bucket = byTool.get(e.tool) ?? { calls: 0, errors: 0, ms: 0 };
|
|
141
|
+
bucket.calls += 1;
|
|
142
|
+
if (!e.ok)
|
|
143
|
+
bucket.errors += 1;
|
|
144
|
+
bucket.ms += e.ms;
|
|
145
|
+
byTool.set(e.tool, bucket);
|
|
146
|
+
}
|
|
147
|
+
const topTools = [...byTool.entries()]
|
|
148
|
+
.map(([tool, b]) => ({ tool, calls: b.calls, errors: b.errors, avgMs: Math.round(b.ms / Math.max(1, b.calls)) }))
|
|
149
|
+
.sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool))
|
|
150
|
+
.slice(0, 8);
|
|
151
|
+
return {
|
|
152
|
+
calls,
|
|
153
|
+
errors,
|
|
154
|
+
errorRate: calls === 0 ? 0 : errors / calls,
|
|
155
|
+
avgMs: calls === 0 ? 0 : Math.round(totalMs / calls),
|
|
156
|
+
maxMs: slowest?.ms ?? 0,
|
|
157
|
+
slowestTool: slowest?.tool ?? "",
|
|
158
|
+
estTokensOut,
|
|
159
|
+
estTokensSaved,
|
|
160
|
+
savePct: estTokensOut + estTokensSaved === 0 ? 0 : estTokensSaved / (estTokensOut + estTokensSaved),
|
|
161
|
+
replayed: entries.filter((e) => e.replayed).length,
|
|
162
|
+
pending: entries.filter((e) => e.pending).length,
|
|
163
|
+
uncertain: entries.filter((e) => e.uncertain).length,
|
|
164
|
+
folds: entries.filter((e) => (e.folded_chars ?? 0) > 0).length,
|
|
165
|
+
topTools,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** 过滤出明细(汇总始终基于全部保留日志,明细才吃 filter) */
|
|
169
|
+
export function filterUsage(entries, filter) {
|
|
170
|
+
let list = entries;
|
|
171
|
+
if (filter.tool)
|
|
172
|
+
list = list.filter((e) => e.tool === filter.tool);
|
|
173
|
+
if (filter.ok !== undefined)
|
|
174
|
+
list = list.filter((e) => e.ok === filter.ok);
|
|
175
|
+
if (filter.since)
|
|
176
|
+
list = list.filter((e) => e.ts >= filter.since);
|
|
177
|
+
const last = filter.last ?? 20;
|
|
178
|
+
return list.slice(-last).reverse();
|
|
179
|
+
}
|
|
180
|
+
export function filterRuntime(entries, filter) {
|
|
181
|
+
let list = entries;
|
|
182
|
+
if (filter.level)
|
|
183
|
+
list = list.filter((e) => e.level === filter.level);
|
|
184
|
+
if (filter.event)
|
|
185
|
+
list = list.filter((e) => e.event === filter.event);
|
|
186
|
+
if (filter.since)
|
|
187
|
+
list = list.filter((e) => e.ts >= filter.since);
|
|
188
|
+
const last = filter.last ?? 20;
|
|
189
|
+
return list.slice(-last).reverse();
|
|
190
|
+
}
|
|
191
|
+
/* ------------------------------ 渲染(供工具层) ------------------------------ */
|
|
192
|
+
function fmtK(n) {
|
|
193
|
+
return n >= 10000 ? `${(n / 1000).toFixed(0)}k` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
194
|
+
}
|
|
195
|
+
function fmtPct(rate) {
|
|
196
|
+
return `${(rate * 100).toFixed(rate < 0.1 && rate > 0 ? 1 : 0)}%`;
|
|
197
|
+
}
|
|
198
|
+
function outcomeMark(e) {
|
|
199
|
+
if (!e.ok)
|
|
200
|
+
return "❌";
|
|
201
|
+
if (e.uncertain)
|
|
202
|
+
return "⚠️";
|
|
203
|
+
if (e.replayed)
|
|
204
|
+
return "↩️";
|
|
205
|
+
if (e.pending)
|
|
206
|
+
return "⏳";
|
|
207
|
+
return "✅";
|
|
208
|
+
}
|
|
209
|
+
export function renderUsageLogLines(root, filter) {
|
|
210
|
+
const entries = loadUsageEntries(root);
|
|
211
|
+
if (entries.length === 0) {
|
|
212
|
+
return [
|
|
213
|
+
"(使用日志为空:还没有记录到工具调用;日志在项目初始化后自动开始记录。)",
|
|
214
|
+
"提示: 本日志只记工具名与计量(耗时/输出量),不记录参数内容。",
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
const summary = usageSummary(entries);
|
|
218
|
+
const list = filterUsage(entries, filter);
|
|
219
|
+
const conditions = [
|
|
220
|
+
filter.tool ? `工具=${filter.tool}` : "",
|
|
221
|
+
filter.ok !== undefined ? `结果=${filter.ok ? "成功" : "失败"}` : "",
|
|
222
|
+
filter.since ? `since=${filter.since}` : "",
|
|
223
|
+
].filter(Boolean).join(" · ");
|
|
224
|
+
const L = [];
|
|
225
|
+
L.push(`## 使用日志汇总(保留 ${entries.length} 次调用)`);
|
|
226
|
+
L.push(`- 调用 ${summary.calls} 次 · 失败 ${summary.errors} 次(${fmtPct(summary.errorRate)})· 平均 ${summary.avgMs}ms · 最慢 ${summary.maxMs}ms(${summary.slowestTool})`);
|
|
227
|
+
L.push(`- 幂等复用 ${summary.replayed} 次 · 等待占位 ${summary.pending} 次 · 不确定 ${summary.uncertain} 次 · 发生折叠 ${summary.folds} 次`);
|
|
228
|
+
L.push(`- 估算输出 tokens ≈ ${fmtK(summary.estTokensOut)} · 折叠省下 ≈ ${fmtK(summary.estTokensSaved)}(占总产出 ${fmtPct(summary.savePct)})——省 token 主要来自输出折叠与过滤引导`);
|
|
229
|
+
if (summary.topTools.length > 0) {
|
|
230
|
+
L.push(`- 热门工具: ${summary.topTools.map((t) => `${t.tool}×${t.calls}${t.errors > 0 ? `(${t.errors}败)` : ""}`).join(" · ")}`);
|
|
231
|
+
}
|
|
232
|
+
L.push("");
|
|
233
|
+
L.push(`## 明细(最近 ${list.length} 条${conditions ? `,过滤: ${conditions}` : ""})`);
|
|
234
|
+
if (list.length === 0)
|
|
235
|
+
L.push("(无匹配条目,放宽过滤条件试试。)");
|
|
236
|
+
for (const e of list) {
|
|
237
|
+
const parts = [
|
|
238
|
+
e.ts.slice(0, 19),
|
|
239
|
+
`${outcomeMark(e)} ${e.tool}`,
|
|
240
|
+
`${e.ms}ms`,
|
|
241
|
+
e.out_chars !== undefined ? `输出 ${fmtK(e.out_chars)} 字/tok≈${fmtK(e.est_tokens ?? 0)}` : "",
|
|
242
|
+
(e.folded_tokens ?? 0) > 0 ? `折叠省 tok≈${fmtK(e.folded_tokens)}` : "",
|
|
243
|
+
e.error ? `错误: ${e.error.slice(0, 160)}` : "",
|
|
244
|
+
].filter(Boolean);
|
|
245
|
+
L.push(parts.join(" "));
|
|
246
|
+
}
|
|
247
|
+
return L;
|
|
248
|
+
}
|
|
249
|
+
export function renderRuntimeLogLines(root, filter) {
|
|
250
|
+
const entries = loadRuntimeEntries(root);
|
|
251
|
+
if (entries.length === 0) {
|
|
252
|
+
return ["(运行日志为空:服务级事件(启动、watcher、工具报错)会在项目初始化后自动记录。)"];
|
|
253
|
+
}
|
|
254
|
+
const counts = { info: 0, warn: 0, error: 0 };
|
|
255
|
+
for (const e of entries)
|
|
256
|
+
counts[e.level] += 1;
|
|
257
|
+
const list = filterRuntime(entries, filter);
|
|
258
|
+
const conditions = [
|
|
259
|
+
filter.level ? `level=${filter.level}` : "",
|
|
260
|
+
filter.event ? `event=${filter.event}` : "",
|
|
261
|
+
filter.since ? `since=${filter.since}` : "",
|
|
262
|
+
].filter(Boolean).join(" · ");
|
|
263
|
+
const L = [];
|
|
264
|
+
L.push(`## 运行日志(保留 ${entries.length} 条:info ${counts.info} · warn ${counts.warn} · error ${counts.error})`);
|
|
265
|
+
if (counts.error > 0) {
|
|
266
|
+
const lastError = [...entries].reverse().find((e) => e.level === "error");
|
|
267
|
+
if (lastError)
|
|
268
|
+
L.push(`- 最近 error: ${lastError.ts.slice(0, 19)} ${lastError.event} ${lastError.detail ?? ""}`.trimEnd());
|
|
269
|
+
}
|
|
270
|
+
L.push("");
|
|
271
|
+
L.push(`## 明细(最近 ${list.length} 条${conditions ? `,过滤: ${conditions}` : ""})`);
|
|
272
|
+
if (list.length === 0)
|
|
273
|
+
L.push("(无匹配条目,放宽过滤条件试试。)");
|
|
274
|
+
for (const e of list) {
|
|
275
|
+
L.push([e.ts.slice(0, 19), `[${e.level}]`, e.event, e.detail ?? ""].filter(Boolean).join(" ").trimEnd());
|
|
276
|
+
}
|
|
277
|
+
return L;
|
|
278
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
// src/ 与发布后的 dist/ 共用包根,避免服务端与安装器各自硬编码版本。
|
|
3
|
+
const metadata = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
4
|
+
export const SERVER_NAME = "pm-mcp";
|
|
5
|
+
export const VERSION = metadata.version;
|
|
6
|
+
export const PACKAGE_SPEC = `${metadata.name}@${VERSION}`;
|
|
7
|
+
export const RUNTIME_CONTRACT = "pagination-evidence-v1";
|
|
8
|
+
// 服务器工具目录契约(doctor 与独立巡检 health-check 都以此核对)。
|
|
9
|
+
// 新增/移除 MCP 工具时必须同步更新这里,并用 `pm-mcp probe` 实测复核。
|
|
10
|
+
export const TOOL_CATALOG_SIZE = 49;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luckychen1993/pm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI 编码项目的单一事实来源 + 健康台账 MCP 服务(事实来源 / 路线图 / 防幻觉 / 七本账 / 断点续做 / token 经济)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -32,12 +32,15 @@
|
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=22.18.0"
|
|
34
34
|
},
|
|
35
|
+
"pm_mcp": {
|
|
36
|
+
"acceptance_baseline": { "id": "pm-mcp-local-release", "version": "1.1.0" }
|
|
37
|
+
},
|
|
35
38
|
"scripts": {
|
|
36
39
|
"build": "tsc -p tsconfig.json",
|
|
37
40
|
"prepare": "npm run build",
|
|
38
41
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.scripts.json",
|
|
39
|
-
"test": "node --test \"test/*.test.ts\"",
|
|
40
|
-
"coverage": "node --experimental-test-coverage --test-coverage-include=\"src/**/*.ts\" --test-coverage-lines=90 --test-coverage-branches=85 --test-coverage-functions=90 --test \"test/*.test.ts\"",
|
|
42
|
+
"test": "node --test-concurrency=2 --test \"test/*.test.ts\"",
|
|
43
|
+
"coverage": "node --experimental-test-coverage --test-coverage-include=\"src/**/*.ts\" --test-coverage-lines=90 --test-coverage-branches=85 --test-coverage-functions=90 --test-concurrency=2 --test \"test/*.test.ts\"",
|
|
41
44
|
"audit": "node scripts/health-check.mts",
|
|
42
45
|
"benchmark": "node scripts/benchmark.mts",
|
|
43
46
|
"benchmark:volume": "node scripts/benchmark-volume.mts",
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
"quality:plan": "node scripts/quality-gate.mts --plan",
|
|
46
49
|
"acceptance:evidence": "node scripts/collect-acceptance-evidence.mts --root .",
|
|
47
50
|
"acceptance:prepare": "node scripts/create-pm-acceptance-evaluation.mts --root .",
|
|
48
|
-
"acceptance": "node scripts/acceptance-
|
|
51
|
+
"acceptance": "node scripts/acceptance-cycle.mts --root .",
|
|
49
52
|
"acceptance:cycle": "node scripts/acceptance-cycle.mts --root .",
|
|
50
53
|
"gate": "npm run audit -- --root . && npm run quality && npm run acceptance:cycle",
|
|
51
54
|
"start": "node dist/index.js",
|