feat: add OpenClaw target surface parser

Add reusable OpenClaw target checkout parsing for compatibility records, hooks, registrars, SDK exports, and manifest type fields.
This commit is contained in:
Vincent Koc 2026-04-26 20:46:04 -07:00 committed by GitHub
parent 22cb50fdbd
commit e7747f74ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 321 additions and 0 deletions

View File

@ -51,6 +51,7 @@ import {
createCaptureApi,
inspectFixtureSet,
loadInspectorConfig,
readOpenClawTargetSurface,
renderColdImportReadinessMarkdown,
renderContractCaptureMarkdown,
renderPlatformProbesMarkdown,
@ -75,6 +76,8 @@ await writeContractCapture(capture);
const readiness = buildColdImportReadiness({ report });
await writeColdImportReadiness(readiness);
const target = await readOpenClawTargetSurface({ manifest: config });
const platformProbes = buildPlatformProbes({ plan: existingWorkspacePlan });
await writePlatformProbes(platformProbes);
```
@ -84,6 +87,8 @@ await writePlatformProbes(platformProbes);
Default inspection is offline and credential-free. It reads manifests, package
metadata, and source files, then reports observed `api.on(...)`,
`api.register*`, `define*`, SDK imports, and manifest contracts.
OpenClaw target checkout parsing is limited to public compatibility registries,
SDK package exports, manifest types, hooks, and captured registrar metadata.
Cold import capture and synthetic contract probes are explicit opt-in modes.
Live lanes will stay credential-gated and must never run in default CI.

View File

@ -25,6 +25,7 @@
"./capture-api": "./src/capture-api.js",
"./cold-import-readiness": "./src/cold-import-readiness.js",
"./contract-capture": "./src/contract-capture.js",
"./openclaw-target": "./src/openclaw-target.js",
"./platform-probes": "./src/platform-probes.js"
},
"files": [

View File

@ -46,6 +46,15 @@ export {
knownIssueCodes,
summarizeIssueClasses,
} from "./issues.js";
export {
defaultOpenClawCheckoutPaths,
openClawTargetPathCandidates,
parseCompatRecordEntries,
parseExportedStringArray,
parsePluginSdkExports,
parseTypeFields,
readOpenClawTargetSurface,
} from "./openclaw-target.js";
export {
captureEntrypoint,
inspectFixtureSet,

179
src/openclaw-target.js Normal file
View File

@ -0,0 +1,179 @@
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import path from "node:path";
export const defaultOpenClawCheckoutPaths = ["./openclaw", "../openclaw"];
export async function readOpenClawTargetSurface(options = {}) {
const rootDir = path.resolve(options.rootDir ?? process.cwd());
const configuredPath = options.configuredPath;
if (configuredPath === false) {
return emptyTargetSurface({ configuredPath: null, status: "disabled" });
}
const requestedPaths = openClawTargetPathCandidates(options.manifest, configuredPath);
if (requestedPaths.length === 0) {
return emptyTargetSurface({ configuredPath: null, status: "not-configured" });
}
const match = findTargetCheckout(rootDir, requestedPaths);
if (!match) {
return emptyTargetSurface({
configuredPath: requestedPaths[0],
searchedPaths: requestedPaths,
status: "missing",
});
}
const { requestedPath, resolvedPath, registryPath } = match;
const hookTypesPath = path.join(resolvedPath, "src/plugins/hook-types.ts");
const apiBuilderPath = path.join(resolvedPath, "src/plugins/api-builder.ts");
const capturedRegistrationPath = path.join(resolvedPath, "src/plugins/captured-registration.ts");
const manifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
const packagePath = path.join(resolvedPath, "package.json");
const registrySource = await readFile(registryPath, "utf8");
const compatRecordEntries = parseCompatRecordEntries(registrySource);
const hookTypesSource = existsSync(hookTypesPath) ? await readFile(hookTypesPath, "utf8") : "";
const hookNames = hookTypesSource ? parseExportedStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
const apiBuilderSource = existsSync(apiBuilderPath) ? await readFile(apiBuilderPath, "utf8") : "";
const apiRegistrars = apiBuilderSource ? parseApiRegistrars(apiBuilderSource) : [];
const manifestTypesSource = existsSync(manifestTypesPath) ? await readFile(manifestTypesPath, "utf8") : "";
const manifestFields = manifestTypesSource ? parseTypeFields(manifestTypesSource, "PluginManifest") : [];
const manifestContractFields = manifestTypesSource ? parseTypeFields(manifestTypesSource, "PluginManifestContracts") : [];
const capturedRegistrars = existsSync(capturedRegistrationPath)
? parseCapturedRegistrars(await readFile(capturedRegistrationPath, "utf8"))
: [];
const sdkExports = existsSync(packagePath)
? parsePluginSdkExports(JSON.parse(await readFile(packagePath, "utf8")))
: [];
return {
configuredPath: requestedPath,
searchedPaths: requestedPaths,
status: "ok",
compatRegistryPath: relativePath(rootDir, registryPath),
compatRecordCount: compatRecordEntries.length,
compatRecords: compatRecordEntries.map((record) => record.code).sort(),
compatRecordStatuses: Object.fromEntries(compatRecordEntries.map((record) => [record.code, record.status])),
hookTypesPath: existsSync(hookTypesPath) ? relativePath(rootDir, hookTypesPath) : null,
hookNameCount: hookNames.length,
hookNames,
apiBuilderPath: existsSync(apiBuilderPath) ? relativePath(rootDir, apiBuilderPath) : null,
apiRegistrarCount: apiRegistrars.length,
apiRegistrars,
capturedRegistrationPath: existsSync(capturedRegistrationPath) ? relativePath(rootDir, capturedRegistrationPath) : null,
capturedRegistrarCount: capturedRegistrars.length,
capturedRegistrars,
packagePath: existsSync(packagePath) ? relativePath(rootDir, packagePath) : null,
sdkExportCount: sdkExports.length,
sdkExports,
manifestTypesPath: existsSync(manifestTypesPath) ? relativePath(rootDir, manifestTypesPath) : null,
manifestFieldCount: manifestFields.length,
manifestFields,
manifestContractFieldCount: manifestContractFields.length,
manifestContractFields,
};
}
export function openClawTargetPathCandidates(manifest, configuredPath) {
if (typeof configuredPath === "string") {
return [configuredPath];
}
return unique([manifest?.openclaw?.defaultCheckoutPath, ...defaultOpenClawCheckoutPaths].filter(Boolean));
}
export function parseCompatRecordEntries(source) {
const entries = [];
for (const match of source.matchAll(/\{[\s\S]*?\bcode:\s*["'`]([^"'`]+)["'`][\s\S]*?\bstatus:\s*["'`]([^"'`]+)["'`][\s\S]*?\}/g)) {
entries.push({ code: match[1], status: match[2] });
}
return dedupeBy(entries, (entry) => entry.code).sort((left, right) => left.code.localeCompare(right.code));
}
export function parsePluginSdkExports(packageJson) {
return Object.keys(packageJson.exports ?? {})
.filter((specifier) => specifier === "./plugin-sdk" || specifier.startsWith("./plugin-sdk/"))
.map((specifier) => `openclaw/${specifier.slice(2)}`)
.sort();
}
export function parseExportedStringArray(source, exportName) {
const match = source.match(new RegExp(`export\\s+const\\s+${exportName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s+as\\s+const`));
if (!match) {
return [];
}
return unique([...match[1].matchAll(/["'`]([^"'`]+)["'`]/g)].map((item) => item[1])).sort();
}
export function parseTypeFields(source, typeName) {
const marker = `export type ${typeName} = {`;
const start = source.indexOf(marker);
if (start === -1) {
return [];
}
const bodyStart = start + marker.length;
const end = source.indexOf("\n};", bodyStart);
if (end === -1) {
return [];
}
const body = source.slice(bodyStart, end);
return unique(
[...body.matchAll(/^\s*([A-Za-z][A-Za-z0-9]*)\??:/gm)]
.map((match) => match[1])
.filter((field) => !field.startsWith("PluginManifest")),
).sort();
}
function findTargetCheckout(rootDir, requestedPaths) {
for (const requestedPath of requestedPaths) {
const resolvedPath = path.resolve(rootDir, requestedPath);
const registryPath = path.join(resolvedPath, "src/plugins/compat/registry.ts");
if (existsSync(registryPath)) {
return { requestedPath, resolvedPath, registryPath };
}
}
return null;
}
function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status }) {
return {
configuredPath,
searchedPaths,
status,
compatRecords: [],
compatRecordStatuses: {},
hookNames: [],
apiRegistrars: [],
capturedRegistrars: [],
sdkExports: [],
manifestFields: [],
manifestContractFields: [],
};
}
function parseCapturedRegistrars(source) {
return unique([...source.matchAll(/^\s*(register[A-Za-z0-9]+)\s*\(/gm)].map((match) => match[1])).sort();
}
function parseApiRegistrars(source) {
return unique([...source.matchAll(/\b(register[A-Za-z0-9]+)\b/g)].map((match) => match[1])).sort();
}
function relativePath(rootDir, filePath) {
return path.relative(rootDir, filePath).replaceAll("\\", "/");
}
function dedupeBy(values, keyForValue) {
const byKey = new Map();
for (const value of values) {
byKey.set(keyForValue(value), value);
}
return [...byKey.values()];
}
function unique(values) {
return [...new Set(values)];
}

View File

@ -0,0 +1,127 @@
import assert from "node:assert/strict";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import {
openClawTargetPathCandidates,
parseCompatRecordEntries,
parsePluginSdkExports,
parseTypeFields,
readOpenClawTargetSurface,
} from "../src/index.js";
test("OpenClaw target parser reads public target surface facts", async (t) => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-openclaw-target-"));
t.after(() => rm(rootDir, { recursive: true, force: true }));
const targetRoot = path.join(rootDir, "openclaw");
await mkdir(path.join(targetRoot, "src/plugins/compat"), { recursive: true });
await writeFile(
path.join(targetRoot, "src/plugins/compat/registry.ts"),
`export const records = [
{ code: "sdk.import.root-barrel-cold-import", status: "deprecated" },
{ code: "hook.before_tool_call.terminal-block-approval", status: "supported" },
];\n`,
"utf8",
);
await writeFile(
path.join(targetRoot, "src/plugins/hook-types.ts"),
`export const PLUGIN_HOOK_NAMES = ["before_tool_call", "llm_input"] as const;\n`,
"utf8",
);
await writeFile(
path.join(targetRoot, "src/plugins/api-builder.ts"),
`api.registerTool(tool); api.registerService(service); api.registerTool(other);\n`,
"utf8",
);
await writeFile(
path.join(targetRoot, "src/plugins/captured-registration.ts"),
`export function createApi() {
return {
registerTool(tool) {},
registerService(service) {},
};
}\n`,
"utf8",
);
await writeFile(
path.join(targetRoot, "src/plugins/manifest.ts"),
`export type PluginManifest = {
id: string;
PluginManifestCompat?: never;
contracts?: PluginManifestContracts;
};
export type PluginManifestContracts = {
tools?: unknown;
channels?: unknown;
};\n`,
"utf8",
);
await writeFile(
path.join(targetRoot, "package.json"),
JSON.stringify({
exports: {
"./plugin-sdk": "./dist/plugin-sdk.js",
"./plugin-sdk/channels": "./dist/channels.js",
".": "./dist/index.js",
},
}),
"utf8",
);
const target = await readOpenClawTargetSurface({
rootDir,
manifest: { openclaw: { defaultCheckoutPath: "./openclaw" } },
});
assert.equal(target.status, "ok");
assert.equal(target.configuredPath, "./openclaw");
assert.deepEqual(target.compatRecords, [
"hook.before_tool_call.terminal-block-approval",
"sdk.import.root-barrel-cold-import",
]);
assert.deepEqual(target.compatRecordStatuses, {
"hook.before_tool_call.terminal-block-approval": "supported",
"sdk.import.root-barrel-cold-import": "deprecated",
});
assert.deepEqual(target.hookNames, ["before_tool_call", "llm_input"]);
assert.deepEqual(target.apiRegistrars, ["registerService", "registerTool"]);
assert.deepEqual(target.capturedRegistrars, ["registerService", "registerTool"]);
assert.deepEqual(target.sdkExports, ["openclaw/plugin-sdk", "openclaw/plugin-sdk/channels"]);
assert.deepEqual(target.manifestFields, ["contracts", "id"]);
assert.deepEqual(target.manifestContractFields, ["channels", "tools"]);
assert.equal(target.compatRegistryPath, "openclaw/src/plugins/compat/registry.ts");
});
test("OpenClaw target parser reports disabled and missing targets", async () => {
assert.equal((await readOpenClawTargetSurface({ configuredPath: false })).status, "disabled");
const missing = await readOpenClawTargetSurface({
rootDir: "/tmp",
configuredPath: "./missing-openclaw",
});
assert.equal(missing.status, "missing");
assert.deepEqual(missing.searchedPaths, ["./missing-openclaw"]);
});
test("OpenClaw target parsing helpers stay deterministic", () => {
assert.deepEqual(openClawTargetPathCandidates({ openclaw: { defaultCheckoutPath: "../target" } }), [
"../target",
"./openclaw",
"../openclaw",
]);
assert.deepEqual(parsePluginSdkExports({ exports: { "./plugin-sdk": "", "./plugin-sdk/tools": "", ".": "" } }), [
"openclaw/plugin-sdk",
"openclaw/plugin-sdk/tools",
]);
assert.deepEqual(
parseCompatRecordEntries(`
{ code: "b", status: "supported" }
{ code: "a", status: "deprecated" }
{ code: "b", status: "supported" }
`).map((entry) => entry.code),
["a", "b"],
);
assert.deepEqual(parseTypeFields("export type PluginManifest = {\n id?: string;\n};", "PluginManifest"), ["id"]);
});