mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
8e24d0694a
Add a chainTo frontmatter field to skills that triggers follow-up skill injection when PostToolUse file contents match regex patterns. Add upgradeToSkill/upgradeWhy fields to validation rules so validate errors can recommend loading a specific skill. Register posttooluse-bash-chain.mjs in hooks.json. Add ChainToRule interface to patterns.mts and skill-map-frontmatter.mts, with parseChainToRules() parser. Add duplicate-key detection to the YAML parser. Reset dedup claim dir and session file on clear/compact events in session-start-seen-skills so skills re-inject into fresh context windows. Add chainTo rules to: agent-browser-verify, agent-browser, ai-elements, ai-gateway, ai-generation-persistence, ai-sdk, auth, bootstrap, chat-sdk, cms, cron-jobs, deployments-cicd, email, env-vars, geist, investigation-mode, json-render, marketplace, micro, ncc, next-forge, nextjs, observability, payments, react-best-practices, routing-middleware, runtime-cache, satori, shadcn, sign-in-with-vercel, swr, turbopack, turborepo, v0-dev, vercel-agent, vercel-api, vercel-cli, vercel-firewall, vercel-flags, vercel-functions, vercel-queues, vercel-sandbox, vercel-storage, verification, workflow. Add upgradeToSkill to ai-elements and ai-sdk validate rules. Expand ai-sdk validate messages with Run Skill() hints. Update nextjs, vercel-storage, runtime-cache, workflow, turborepo skill bodies. Add new skills: geistdocs (Geist design system docs), zzz-test-meta-name-mask (test fixture). Add skills/_chain-audit.md chain coverage audit doc. Delete .claude-plugin/marketplace.json, .claude-plugin/plugin.json (deprecated), skills/edge-runtime/SKILL.md (consolidated into vercel-functions). Add tests: posttooluse-chain.test.ts (4699 lines, chain injection e2e), ai-sdk-companion.test.ts (181 lines). Expand build-skill-map.test.ts (+335 lines), validate-rules.test.ts (+936 lines), session-start-seen-skills.test.ts (+74 lines), skill-map-frontmatter.test.ts (+50 lines), verification-skill.test.ts (+20 lines). Update build-manifest.ts to emit chainTo rules and upgradeToSkill fields. Rebuild generated/skill-manifest.json, generated/skill-catalog.md, generated/build-from-skills.manifest.json. Rebuild all compiled hooks/*.mjs. Update CLAUDE.md lexical prompt default to on. Update vercel.md ecosystem graph, docs, and cli-reference.
156 lines
5.9 KiB
JavaScript
Executable File
156 lines
5.9 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
// hooks/src/posttooluse-verification-observe.mts
|
|
import { readFileSync, realpathSync } from "fs";
|
|
import { resolve } from "path";
|
|
import { fileURLToPath } from "url";
|
|
import { generateVerificationId } from "./hook-env.mjs";
|
|
import { createLogger } from "./logger.mjs";
|
|
function isVerificationReport(value) {
|
|
if (typeof value !== "object" || value === null) return false;
|
|
const obj = value;
|
|
return obj.type === "verification.report/v1" && typeof obj.verificationId === "string" && Array.isArray(obj.boundaries) && obj.boundaries.every(
|
|
(b) => typeof b === "object" && b !== null && b.event === "verification.boundary_observed"
|
|
);
|
|
}
|
|
var BOUNDARY_PATTERNS = [
|
|
// uiRender: browser/screenshot/playwright/puppeteer commands
|
|
{ boundary: "uiRender", pattern: /\b(open|launch|browse|screenshot|puppeteer|playwright|chromium|firefox|webkit)\b/i, label: "browser-tool" },
|
|
{ boundary: "uiRender", pattern: /\bopen\s+https?:/i, label: "open-url" },
|
|
{ boundary: "uiRender", pattern: /\bnpx\s+playwright\b/i, label: "playwright-cli" },
|
|
// clientRequest: curl, fetch, wget, httpie
|
|
{ boundary: "clientRequest", pattern: /\b(curl|wget|http|httpie)\b/i, label: "http-client" },
|
|
{ boundary: "clientRequest", pattern: /\bfetch\s*\(/i, label: "fetch-call" },
|
|
{ boundary: "clientRequest", pattern: /\bnpx\s+undici\b/i, label: "undici-cli" },
|
|
// serverHandler: log tailing, server process inspection
|
|
{ boundary: "serverHandler", pattern: /\b(tail|less|cat)\b.*\.(log|out|err)\b/i, label: "log-tail" },
|
|
{ boundary: "serverHandler", pattern: /\b(tail\s+-f|journalctl\s+-f)\b/i, label: "log-follow" },
|
|
{ boundary: "serverHandler", pattern: /\blog(s)?\s/i, label: "log-command" },
|
|
{ boundary: "serverHandler", pattern: /\b(vercel\s+logs|vercel\s+inspect)\b/i, label: "vercel-logs" },
|
|
{ boundary: "serverHandler", pattern: /\b(lsof|netstat|ss)\s.*:(3000|3001|4000|5173|8080)\b/i, label: "port-inspect" },
|
|
// environment: env reads, config inspection
|
|
{ boundary: "environment", pattern: /\b(printenv|env\b|echo\s+\$)/i, label: "env-read" },
|
|
{ boundary: "environment", pattern: /\bvercel\s+env\b/i, label: "vercel-env" },
|
|
{ boundary: "environment", pattern: /\bcat\b.*\.env\b/i, label: "dotenv-read" },
|
|
{ boundary: "environment", pattern: /\bnode\s+-e\b.*process\.env\b/i, label: "node-env" }
|
|
];
|
|
function classifyBoundary(command) {
|
|
for (const bp of BOUNDARY_PATTERNS) {
|
|
if (bp.pattern.test(command)) {
|
|
return { boundary: bp.boundary, matchedPattern: bp.label };
|
|
}
|
|
}
|
|
return { boundary: "unknown", matchedPattern: "none" };
|
|
}
|
|
var ROUTE_REGEX = /\b(?:app|pages|src\/pages|src\/app)\/([\w[\].-]+(?:\/[\w[\].-]+)*)/;
|
|
var URL_ROUTE_REGEX = /https?:\/\/[^/\s]+(\/([\w-]+(?:\/[\w-]+)*))/;
|
|
function inferRoute(command, recentEdits) {
|
|
if (recentEdits) {
|
|
const paths = recentEdits.split(",").map((p) => p.trim()).filter(Boolean);
|
|
for (const p of paths) {
|
|
const match = ROUTE_REGEX.exec(p);
|
|
if (match) {
|
|
const route = "/" + match[1].replace(/\/page\.\w+$/, "").replace(/\/route\.\w+$/, "").replace(/\/layout\.\w+$/, "").replace(/\/loading\.\w+$/, "").replace(/\/error\.\w+$/, "").replace(/\[([^\]]+)\]/g, ":$1");
|
|
return route === "/" ? "/" : route.replace(/\/$/, "");
|
|
}
|
|
}
|
|
}
|
|
const urlMatch = URL_ROUTE_REGEX.exec(command);
|
|
if (urlMatch && urlMatch[1]) {
|
|
return urlMatch[1];
|
|
}
|
|
return null;
|
|
}
|
|
function parseInput(raw, logger) {
|
|
const trimmed = (raw || "").trim();
|
|
if (!trimmed) return null;
|
|
let input;
|
|
try {
|
|
input = JSON.parse(trimmed);
|
|
} catch {
|
|
return null;
|
|
}
|
|
const toolName = input.tool_name || "";
|
|
if (toolName !== "Bash") return null;
|
|
const toolInput = input.tool_input || {};
|
|
const command = toolInput.command || "";
|
|
if (!command) return null;
|
|
const sessionId = input.session_id || null;
|
|
const cwdCandidate = input.cwd ?? input.working_directory;
|
|
const cwd = typeof cwdCandidate === "string" && cwdCandidate.trim() !== "" ? cwdCandidate : null;
|
|
return { command, sessionId, cwd };
|
|
}
|
|
function run(rawInput) {
|
|
const log = createLogger();
|
|
let raw;
|
|
if (rawInput !== void 0) {
|
|
raw = rawInput;
|
|
} else {
|
|
try {
|
|
raw = readFileSync(0, "utf-8");
|
|
} catch {
|
|
return "{}";
|
|
}
|
|
}
|
|
const parsed = parseInput(raw, log);
|
|
if (!parsed) {
|
|
log.debug("verification-observe-skip", { reason: "no_bash_input" });
|
|
return "{}";
|
|
}
|
|
const { command, sessionId } = parsed;
|
|
const { boundary, matchedPattern } = classifyBoundary(command);
|
|
if (boundary === "unknown") {
|
|
log.trace("verification-observe-skip", { reason: "no_boundary_match", command: command.slice(0, 120) });
|
|
return "{}";
|
|
}
|
|
const verificationId = generateVerificationId();
|
|
const recentEdits = process.env.VERCEL_PLUGIN_RECENT_EDITS || "";
|
|
const inferredRoute = inferRoute(command, recentEdits);
|
|
const boundaryEvent = {
|
|
event: "verification.boundary_observed",
|
|
boundary,
|
|
verificationId,
|
|
command: command.slice(0, 200),
|
|
matchedPattern,
|
|
inferredRoute,
|
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
};
|
|
log.summary("verification.boundary_observed", boundaryEvent);
|
|
log.complete("verification-observe-done", {
|
|
matchedCount: 1,
|
|
injectedCount: 0
|
|
});
|
|
return "{}";
|
|
}
|
|
function isMainModule() {
|
|
try {
|
|
const scriptPath = realpathSync(resolve(process.argv[1] || ""));
|
|
const modulePath = realpathSync(fileURLToPath(import.meta.url));
|
|
return scriptPath === modulePath;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
if (isMainModule()) {
|
|
try {
|
|
const output = run();
|
|
process.stdout.write(output);
|
|
} catch (err) {
|
|
const entry = [
|
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] CRASH in posttooluse-verification-observe.mts`,
|
|
` error: ${err?.message || String(err)}`,
|
|
` stack: ${err?.stack || "(no stack)"}`,
|
|
""
|
|
].join("\n");
|
|
process.stderr.write(entry);
|
|
process.stdout.write("{}");
|
|
}
|
|
}
|
|
export {
|
|
classifyBoundary,
|
|
inferRoute,
|
|
isVerificationReport,
|
|
parseInput,
|
|
run
|
|
};
|