feat(setup): verify installed skill catalog identity
This commit is contained in:
parent
8de25d0230
commit
b2d1f2affd
|
|
@ -10,11 +10,11 @@
|
|||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](#89-tools-for-investigation)
|
||||
[](#tool-catalog-for-investigation)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
|
||||
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [89 tools](#89-tools-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
||||
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
||||
|
||||
<br />
|
||||
|
||||
|
|
@ -320,7 +320,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
|
|||
- Analyze Swift and Objective-C metadata without manually untangling every mangled symbol.
|
||||
- Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other.
|
||||
|
||||
## 89 tools for investigation
|
||||
## Tool catalog for investigation
|
||||
|
||||
| Tool family | Count | Examples |
|
||||
| ------------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](#منصة-من-89-أداة)
|
||||
[](#منصة-أدوات-التحقيق)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [89 أداة](#منصة-من-89-أداة) · [كيف يعمل](#كيف-يعمل) · [الأسئلة الشائعة](#الأسئلة-الشائعة)
|
||||
[البدء السريع](#البدء-السريع) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [دليل الأدوات](#منصة-أدوات-التحقيق) · [كيف يعمل](#كيف-يعمل) · [الأسئلة الشائعة](#الأسئلة-الشائعة)
|
||||
|
||||
<br />
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ rea uninstall --purge-data
|
|||
|
||||
افحص التحقق من الإدخال، والتشفير، والصلاحيات، والتخزين، ومسارات الأخطاء من دون رفع الملف التنفيذي إلى خدمة بعيدة.
|
||||
|
||||
## منصة من 89 أداة
|
||||
## منصة أدوات التحقيق
|
||||
|
||||
| عائلة الأدوات | العدد | أمثلة |
|
||||
| --------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](#89-ツールのワークベンチ)
|
||||
[](#調査ツールカタログ)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
|
||||
[クイックスタート](#クイックスタート) · [バイナリから動作へ](#バイナリから動作へ) · [89 ツール](#89-ツールのワークベンチ) · [仕組み](#仕組み) · [FAQ](#faq)
|
||||
[クイックスタート](#クイックスタート) · [バイナリから動作へ](#バイナリから動作へ) · [ツールカタログ](#調査ツールカタログ) · [仕組み](#仕組み) · [FAQ](#faq)
|
||||
|
||||
<br />
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
|
|||
- Swift / Objective-C のメタデータを解析する。
|
||||
- Hopper に名前、コメント、ブックマークを残し、人間とエージェントの調査を共有する。
|
||||
|
||||
## 89 ツールのワークベンチ
|
||||
## 調査ツールカタログ
|
||||
|
||||
| ツール群 | 数 | 例 |
|
||||
| ---------------------- | --: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](#89개-도구로-구성된-워크벤치)
|
||||
[](#조사-도구-카탈로그)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
|
||||
[빠른 시작](#빠른-시작) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [89개 도구](#89개-도구로-구성된-워크벤치) · [작동 방식](#작동-방식) · [FAQ](#faq)
|
||||
[빠른 시작](#빠른-시작) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [도구 카탈로그](#조사-도구-카탈로그) · [작동 방식](#작동-방식) · [FAQ](#faq)
|
||||
|
||||
<br />
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
|
|||
- Swift 및 Objective-C 메타데이터를 분석합니다.
|
||||
- Hopper에 이름, 주석, 북마크를 남겨 사람과 에이전트의 분석을 연결합니다.
|
||||
|
||||
## 89개 도구로 구성된 워크벤치
|
||||
## 조사 도구 카탈로그
|
||||
|
||||
| 도구 그룹 | 수 | 예시 |
|
||||
| ------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](#89-个工具组成的工作台)
|
||||
[](#调查工具目录)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
|
||||
[快速开始](#快速开始) · [从二进制到行为](#从二进制到行为) · [89 个工具](#89-个工具组成的工作台) · [工作原理](#工作原理) · [常见问题](#常见问题)
|
||||
[快速开始](#快速开始) · [从二进制到行为](#从二进制到行为) · [工具目录](#调查工具目录) · [工作原理](#工作原理) · [常见问题](#常见问题)
|
||||
|
||||
<br />
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
|
|||
- 分析 Swift 和 Objective-C 元数据。
|
||||
- 在 Hopper 中留下名称、注释与书签,使人与智能体的分析互相增强。
|
||||
|
||||
## 89 个工具组成的工作台
|
||||
## 调查工具目录
|
||||
|
||||
| 工具类别 | 数量 | 示例 |
|
||||
| -------------- | ---: | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ graph TD
|
|||
end
|
||||
|
||||
subgraph "Contracts Layer"
|
||||
CONTRACTS["src/contracts/<br/>Caller-visible tool schemas<br/>86 analysis, managed, browser, Electron, application, and session tools"]
|
||||
CONTRACTS["src/contracts/<br/>Caller-visible schemas for analysis, managed, browser, Electron, application, and session tools"]
|
||||
end
|
||||
|
||||
subgraph "Domain Layer"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Guided MCP prompts and completion
|
||||
|
||||
REA exposes six provider-neutral investigation workflows through the MCP
|
||||
`prompts` capability. They complement the 89 tools; they do not add, remove, or
|
||||
`prompts` capability. They complement the generated tool catalog; they do not add, remove, or
|
||||
invoke tools by themselves.
|
||||
|
||||
## Prompt inventory
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
const arguments_ = new Set(process.argv.slice(2));
|
||||
for (const argument of arguments_)
|
||||
if (argument !== "--check")
|
||||
throw new Error(`Unknown skill metadata option: ${argument}`);
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const path = join(root, "skills/rea-analysis/SKILL.md");
|
||||
const current = await readFile(path, "utf8");
|
||||
const { CATALOG_IDENTITY } = await import(
|
||||
`${pathToFileURL(join(root, "dist/catalogIdentity.js")).href}?${String(Date.now())}`
|
||||
);
|
||||
const withCount = current.replace(
|
||||
/^\s{2}tool_count:\s*\d+\s*$/mu,
|
||||
` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`,
|
||||
);
|
||||
const digestLine = ` catalog_digest: "${CATALOG_IDENTITY.digests.combined_sha256}"`;
|
||||
const source = /^\s{2}catalog_digest:/mu.test(withCount)
|
||||
? withCount.replace(
|
||||
/^\s{2}catalog_digest:\s*"[a-f0-9]{64}"\s*$/mu,
|
||||
digestLine,
|
||||
)
|
||||
: withCount.replace(/^(\s{2}tool_count:\s*\d+\s*)$/mu, `$1\n${digestLine}`);
|
||||
|
||||
await ensureGeneratedFile({
|
||||
path,
|
||||
source,
|
||||
check: arguments_.has("--check"),
|
||||
generateCommand: "npm run docs:generate",
|
||||
});
|
||||
|
|
@ -10,26 +10,20 @@ const README_PATHS = [
|
|||
];
|
||||
const familyLabel = (id) => (id === "electron" ? "Electron" : id);
|
||||
|
||||
const tableCounts = (content, path, total) => {
|
||||
const tableCounts = (content, path, expectedCounts) => {
|
||||
const lines = content.split(/\r?\n/u);
|
||||
const headingIndex = lines.findIndex(
|
||||
(line) => line.startsWith("## ") && line.includes(String(total)),
|
||||
);
|
||||
if (headingIndex === -1) {
|
||||
throw new Error(`Missing ${total}-tool heading in ${path}`);
|
||||
}
|
||||
const counts = [];
|
||||
for (const line of lines.slice(headingIndex + 1)) {
|
||||
if (line.startsWith("## ")) break;
|
||||
if (!line.trim().startsWith("|") || /^\|\s*-/u.test(line)) continue;
|
||||
const count = Number(line.split("|")[2]?.trim());
|
||||
if (!Number.isInteger(count)) {
|
||||
if (counts.length === 0) continue;
|
||||
throw new Error(`Non-numeric tool count in ${path}`);
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (!/^\|\s*-/u.test(line)) continue;
|
||||
const counts = [];
|
||||
for (const row of lines.slice(index + 1)) {
|
||||
if (!row.trim().startsWith("|")) break;
|
||||
const count = Number(row.split("|")[2]?.trim());
|
||||
if (!Number.isInteger(count)) break;
|
||||
counts.push(count);
|
||||
}
|
||||
counts.push(count);
|
||||
if (counts.length === expectedCounts.length) return counts;
|
||||
}
|
||||
return counts;
|
||||
throw new Error(`Missing tool-family inventory table in ${path}`);
|
||||
};
|
||||
|
||||
const requireText = (issues, path, content, expected) => {
|
||||
|
|
@ -42,16 +36,11 @@ export const documentationFactIssues = async (root, catalog) => {
|
|||
const expectedCounts = catalog.tools.families.map(({ count }) => count);
|
||||
for (const path of README_PATHS) {
|
||||
const content = await readFile(join(root, path), "utf8");
|
||||
requireText(
|
||||
issues,
|
||||
path,
|
||||
content,
|
||||
`MCP_tools-${String(catalog.tools.total)}`,
|
||||
);
|
||||
requireText(issues, path, content, "MCP-tool_catalog");
|
||||
for (const client of catalog.setup_clients)
|
||||
requireText(issues, path, content, client.display_name);
|
||||
try {
|
||||
const actualCounts = tableCounts(content, path, catalog.tools.total);
|
||||
const actualCounts = tableCounts(content, path, expectedCounts);
|
||||
if (JSON.stringify(actualCounts) !== JSON.stringify(expectedCounts))
|
||||
issues.push(
|
||||
`${path}: tool family counts ${JSON.stringify(actualCounts)} do not match ${JSON.stringify(expectedCounts)}`,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ description: Reverse engineer native, Electron/JavaScript, and web applications
|
|||
metadata:
|
||||
version: "20"
|
||||
tool_count: 89
|
||||
catalog_digest: "59253c96793f162725d5dc08e611c1e0c19938b23de1e9490dcc1cc800dde955"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -77,10 +77,18 @@ export interface DoctorHost {
|
|||
providerInspections?(): Promise<readonly DoctorProviderInspection[]>;
|
||||
installationPaths?(): Promise<readonly string[]>;
|
||||
installedSkillVersion?(): Promise<string | undefined>;
|
||||
installedSkillIdentity?(): Promise<InstalledSkillIdentity | undefined>;
|
||||
clientRegistrations?(): Promise<readonly ClientRegistrationStatus[]>;
|
||||
javascriptReplayCheck?(): Promise<DoctorCheck>;
|
||||
}
|
||||
|
||||
/** Parsed identity committed by an installed REA skill. */
|
||||
interface InstalledSkillIdentity {
|
||||
readonly version: string | null;
|
||||
readonly toolCount: number | null;
|
||||
readonly catalogDigest: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check requirements and an optional target without mutating the host.
|
||||
* Every failed check includes remediation suitable for either the one-shot CLI
|
||||
|
|
@ -109,7 +117,9 @@ export const runDoctor = async (
|
|||
};
|
||||
readonly skill: {
|
||||
readonly installed_version: string | null;
|
||||
readonly state: "aligned" | "stale" | "unknown";
|
||||
readonly installed_tool_count: number | null;
|
||||
readonly installed_catalog_digest: string | null;
|
||||
readonly state: "aligned" | "stale" | "missing";
|
||||
readonly remediation: string | null;
|
||||
};
|
||||
readonly registrations: readonly ClientRegistrationStatus[];
|
||||
|
|
@ -216,7 +226,37 @@ export const runDoctor = async (
|
|||
}),
|
||||
);
|
||||
const installationPaths = (await host.installationPaths?.()) ?? [];
|
||||
const installedSkillVersion = await host.installedSkillVersion?.();
|
||||
const observedSkillIdentity = await host.installedSkillIdentity?.();
|
||||
const legacySkillVersion =
|
||||
observedSkillIdentity === undefined
|
||||
? await host.installedSkillVersion?.()
|
||||
: undefined;
|
||||
const installedSkillIdentity =
|
||||
observedSkillIdentity ??
|
||||
(legacySkillVersion === undefined
|
||||
? undefined
|
||||
: {
|
||||
version: legacySkillVersion,
|
||||
toolCount: null,
|
||||
catalogDigest: null,
|
||||
});
|
||||
const installedSkillVersion = installedSkillIdentity?.version ?? undefined;
|
||||
const skillAligned =
|
||||
installedSkillIdentity?.version === PRODUCT_IDENTITY.skillVersion &&
|
||||
installedSkillIdentity.toolCount === CATALOG_IDENTITY.counts.mcp_tools &&
|
||||
installedSkillIdentity.catalogDigest ===
|
||||
CATALOG_IDENTITY.digests.combined_sha256;
|
||||
if (!skillAligned)
|
||||
checks.push({
|
||||
name: "skill:identity",
|
||||
ok: false,
|
||||
classification: "config_drift",
|
||||
detail:
|
||||
installedSkillIdentity === undefined
|
||||
? "Installed REA skill identity is missing."
|
||||
: "Installed REA skill identity is stale.",
|
||||
remediation: "Run rea setup to update the installed REA skill.",
|
||||
});
|
||||
const registrations = (await host.clientRegistrations?.()) ?? [];
|
||||
for (const registration of registrations)
|
||||
if (registration.state !== "aligned")
|
||||
|
|
@ -263,17 +303,17 @@ export const runDoctor = async (
|
|||
},
|
||||
skill: {
|
||||
installed_version: installedSkillVersion ?? null,
|
||||
installed_tool_count: installedSkillIdentity?.toolCount ?? null,
|
||||
installed_catalog_digest: installedSkillIdentity?.catalogDigest ?? null,
|
||||
state:
|
||||
installedSkillVersion === undefined
|
||||
? "unknown"
|
||||
: installedSkillVersion === PRODUCT_IDENTITY.skillVersion
|
||||
installedSkillIdentity === undefined
|
||||
? "missing"
|
||||
: skillAligned
|
||||
? "aligned"
|
||||
: "stale",
|
||||
remediation:
|
||||
installedSkillVersion === undefined ||
|
||||
installedSkillVersion === PRODUCT_IDENTITY.skillVersion
|
||||
? null
|
||||
: "Run rea setup to update the installed REA skill.",
|
||||
remediation: skillAligned
|
||||
? null
|
||||
: "Run rea setup to update the installed REA skill.",
|
||||
},
|
||||
registrations,
|
||||
},
|
||||
|
|
@ -407,6 +447,33 @@ export const systemDoctorHost = (
|
|||
return undefined;
|
||||
}
|
||||
},
|
||||
async installedSkillIdentity() {
|
||||
try {
|
||||
const content = await readFile(
|
||||
join(
|
||||
homedir(),
|
||||
".agents/skills",
|
||||
PRODUCT_IDENTITY.skillName,
|
||||
"SKILL.md",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const version =
|
||||
/^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null;
|
||||
const countText = /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1];
|
||||
const catalogDigest =
|
||||
/^\s{2}catalog_digest:\s*"([a-f0-9]{64})"\s*$/mu.exec(content)?.[1] ??
|
||||
null;
|
||||
return {
|
||||
version,
|
||||
toolCount:
|
||||
countText === undefined ? null : Number.parseInt(countText, 10),
|
||||
catalogDigest,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
clientRegistrations: () => readClientRegistrationStatuses(homedir()),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { runDoctor, type DoctorHost } from "../src/application/Doctor.js";
|
||||
import { CATALOG_IDENTITY } from "../src/catalogIdentity.js";
|
||||
import { PRODUCT_IDENTITY } from "../src/identity.js";
|
||||
|
||||
const host = (overrides: Partial<DoctorHost> = {}): DoctorHost => ({
|
||||
platform: "darwin",
|
||||
|
|
@ -12,6 +14,12 @@ const host = (overrides: Partial<DoctorHost> = {}): DoctorHost => ({
|
|||
linuxDemoRuntimeReady: () => Promise.resolve(true),
|
||||
brewHopperPath: () => Promise.resolve(undefined),
|
||||
manualHopperPaths: () => Promise.resolve([]),
|
||||
installedSkillIdentity: () =>
|
||||
Promise.resolve({
|
||||
version: PRODUCT_IDENTITY.skillVersion,
|
||||
toolCount: CATALOG_IDENTITY.counts.mcp_tools,
|
||||
catalogDigest: CATALOG_IDENTITY.digests.combined_sha256,
|
||||
}),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
|
@ -85,7 +93,12 @@ describe("doctor", () => {
|
|||
host({
|
||||
installationPaths: () =>
|
||||
Promise.resolve(["/usr/local/bin/rea", "/home/user/bin/rea"]),
|
||||
installedSkillVersion: () => Promise.resolve("10"),
|
||||
installedSkillIdentity: () =>
|
||||
Promise.resolve({
|
||||
version: "10",
|
||||
toolCount: null,
|
||||
catalogDigest: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -99,6 +112,25 @@ describe("doctor", () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
it("reports a missing installed skill as unhealthy", async () => {
|
||||
const result = await runDoctor(
|
||||
undefined,
|
||||
host({ installedSkillIdentity: () => Promise.resolve(undefined) }),
|
||||
);
|
||||
|
||||
expect(result.healthy).toBe(false);
|
||||
expect(result.identity?.skill).toMatchObject({
|
||||
state: "missing",
|
||||
remediation: "Run rea setup to update the installed REA skill.",
|
||||
});
|
||||
expect(result.checks).toContainEqual({
|
||||
name: "skill:identity",
|
||||
ok: false,
|
||||
classification: "config_drift",
|
||||
detail: "Installed REA skill identity is missing.",
|
||||
remediation: "Run rea setup to update the installed REA skill.",
|
||||
});
|
||||
});
|
||||
it("reports stale client registration paths without claiming live state", async () => {
|
||||
const result = await runDoctor(
|
||||
undefined,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {
|
|||
type GhidraInstallationInspection,
|
||||
} from "../src/ghidra/GhidraInstallation.js";
|
||||
import { projectGhidraDoctorInspection } from "../src/ghidra/GhidraDoctor.js";
|
||||
import { CATALOG_IDENTITY } from "../src/catalogIdentity.js";
|
||||
import { PRODUCT_IDENTITY } from "../src/identity.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
|
||||
const installationHost = (
|
||||
|
|
@ -65,6 +67,12 @@ const doctorHost = (
|
|||
manualHopperPaths: () => Promise.resolve([]),
|
||||
providerInspections: () =>
|
||||
Promise.resolve([projectGhidraDoctorInspection(ghidra)]),
|
||||
installedSkillIdentity: () =>
|
||||
Promise.resolve({
|
||||
version: PRODUCT_IDENTITY.skillVersion,
|
||||
toolCount: CATALOG_IDENTITY.counts.mcp_tools,
|
||||
catalogDigest: CATALOG_IDENTITY.digests.combined_sha256,
|
||||
}),
|
||||
});
|
||||
|
||||
describe("Ghidra doctor integration", () => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ describe("package identity", () => {
|
|||
scripts: z.object({
|
||||
start: z.string(),
|
||||
prepare: z.string(),
|
||||
prepack: z.string().optional(),
|
||||
prepack: z.string(),
|
||||
prepublishOnly: z.string(),
|
||||
}),
|
||||
})
|
||||
.parse(
|
||||
|
|
@ -30,6 +31,11 @@ describe("package identity", () => {
|
|||
expect(PRODUCT_IDENTITY.mcpCommand).toBe(`npx -y ${packageJson.name} mcp`);
|
||||
expect(packageJson.scripts.start).toBe("node scripts/rea.mjs mcp");
|
||||
expect(packageJson.scripts.prepare).toBe("npm run build && husky");
|
||||
expect(packageJson.scripts.prepack).toBeUndefined();
|
||||
expect(packageJson.scripts.prepack).toBe(
|
||||
"npm run deps:check && node scripts/generate-skill-metadata.mjs --check",
|
||||
);
|
||||
expect(packageJson.scripts.prepublishOnly).toBe(
|
||||
"npm run deps:check && node scripts/generate-skill-metadata.mjs --check",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
ENHANCED_TOOL_CONTRACTS,
|
||||
OFFICIAL_TOOL_CONTRACTS,
|
||||
SESSION_TOOL_CONTRACTS,
|
||||
TOOL_CONTRACTS,
|
||||
} from "../src/contracts/toolContracts.js";
|
||||
import { NATIVE_TOOL_CONTRACTS } from "../src/contracts/nativeToolContracts.js";
|
||||
import { ARTIFACT_TOOL_CONTRACTS } from "../src/contracts/artifactToolContracts.js";
|
||||
|
|
@ -39,30 +38,18 @@ const expectedToolCounts = [
|
|||
|
||||
const toolCountsFromReadme = (content: string, path: string): number[] => {
|
||||
const lines = content.split(/\r?\n/u);
|
||||
const headingIndex = lines.findIndex((line) =>
|
||||
new RegExp(`^## .*${String(TOOL_CONTRACTS.length)}`, "u").test(line),
|
||||
);
|
||||
if (headingIndex === -1)
|
||||
throw new Error(
|
||||
`Missing ${String(TOOL_CONTRACTS.length)}-tool heading in ${path}`,
|
||||
);
|
||||
|
||||
const counts: number[] = [];
|
||||
for (const line of lines.slice(headingIndex + 1)) {
|
||||
if (line.startsWith("## ")) break;
|
||||
if (!line.trim().startsWith("|") || /^\|\s*-/.test(line)) continue;
|
||||
const cells = line.split("|").map((cell) => cell.trim());
|
||||
const countCell = cells[2];
|
||||
if (countCell === undefined)
|
||||
throw new Error(`Malformed table row in ${path}`);
|
||||
const count = Number(countCell);
|
||||
if (!Number.isInteger(count)) {
|
||||
if (counts.length === 0) continue;
|
||||
throw new Error(`Non-numeric tool count in ${path}`);
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (!/^\|\s*-/u.test(line)) continue;
|
||||
const counts: number[] = [];
|
||||
for (const row of lines.slice(index + 1)) {
|
||||
if (!row.trim().startsWith("|")) break;
|
||||
const count = Number(row.split("|")[2]?.trim());
|
||||
if (!Number.isInteger(count)) break;
|
||||
counts.push(count);
|
||||
}
|
||||
counts.push(count);
|
||||
if (counts.length === expectedToolCounts.length) return counts;
|
||||
}
|
||||
return counts;
|
||||
throw new Error(`Missing tool-family inventory table in ${path}`);
|
||||
};
|
||||
|
||||
describe("localized README product facts", () => {
|
||||
|
|
@ -87,7 +74,7 @@ describe("localized README product facts", () => {
|
|||
expect(content).toContain(client.displayName);
|
||||
if (path === "README_ar.md")
|
||||
expect(content).toContain("Windows غير مدعوم حاليًا");
|
||||
expect(content).toContain(`MCP_tools-${String(TOOL_CONTRACTS.length)}`);
|
||||
expect(content).toContain("MCP-tool_catalog");
|
||||
expect(toolCountsFromReadme(content, path)).toEqual(expectedToolCounts);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue