mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
reduce-vercel-injection-at-start
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"generatedAt": "2026-04-01T17:44:37.555Z",
|
||||
"generatedAt": "2026-04-01T19:46:10.445Z",
|
||||
"templates": [
|
||||
{
|
||||
"template": "agents/ai-architect.md.tmpl",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2026-04-01T17:44:37.524Z",
|
||||
"generatedAt": "2026-04-01T19:46:11.138Z",
|
||||
"version": 2,
|
||||
"skills": {
|
||||
"vercel-agent": {
|
||||
|
||||
@@ -55,9 +55,10 @@ function stripFrontmatter(content) {
|
||||
function main() {
|
||||
const input = parseInjectClaudeMdInput(readFileSync(0, "utf8"));
|
||||
const platform = detectInjectClaudeMdPlatform(input);
|
||||
const thinSessionContext = safeReadFile(join(pluginRoot(), "vercel-session.md"));
|
||||
const knowledgeUpdateRaw = safeReadFile(join(pluginRoot(), "skills", "knowledge-update", "SKILL.md"));
|
||||
const knowledgeUpdate = knowledgeUpdateRaw !== null ? stripFrontmatter(knowledgeUpdateRaw) : null;
|
||||
const parts = buildInjectClaudeMdParts(safeReadFile(join(pluginRoot(), "vercel.md")), process.env, knowledgeUpdate);
|
||||
const parts = buildInjectClaudeMdParts(thinSessionContext, process.env, knowledgeUpdate);
|
||||
if (parts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import { resolveVercelJsonSkills, isVercelJsonPath, VERCEL_JSON_SKILLS } from "./vercel-config.mjs";
|
||||
import { createLogger, logDecision } from "./logger.mjs";
|
||||
import { trackBaseEvents } from "./telemetry.mjs";
|
||||
import { selectManagedContextChunk } from "./vercel-context.mjs";
|
||||
var MAX_SKILLS = 3;
|
||||
var DEFAULT_INJECTION_BUDGET_BYTES = 18e3;
|
||||
var SETUP_MODE_BOOTSTRAP_SKILL = "bootstrap";
|
||||
@@ -553,6 +554,7 @@ function formatOutput({
|
||||
parts,
|
||||
matched,
|
||||
injectedSkills,
|
||||
contextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget,
|
||||
@@ -573,6 +575,7 @@ function formatOutput({
|
||||
toolTarget: toolName === "Bash" ? redactCommand(toolTarget) : toolTarget,
|
||||
matchedSkills: [...matched],
|
||||
injectedSkills,
|
||||
contextChunks: contextChunks || [],
|
||||
summaryOnly: summaryOnly || [],
|
||||
droppedByBudget: droppedByBudget || []
|
||||
};
|
||||
@@ -722,6 +725,22 @@ function run() {
|
||||
parts.push(VERCEL_ENV_HELP);
|
||||
log.debug("vercel-env-help-appended", { subcommand: vercelEnvHelp.subcommand || "" });
|
||||
}
|
||||
const injectedContextChunks = [];
|
||||
if (!scopeId) {
|
||||
const chunk = selectManagedContextChunk(loaded, {
|
||||
pluginRoot: PLUGIN_ROOT,
|
||||
sessionId
|
||||
});
|
||||
if (chunk) {
|
||||
parts.push(chunk.wrapped);
|
||||
injectedContextChunks.push(chunk.chunkId);
|
||||
log.debug("managed-context-chunk-injected", {
|
||||
chunkId: chunk.chunkId,
|
||||
skill: chunk.skill,
|
||||
bytes: chunk.bytes
|
||||
});
|
||||
}
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
if (log.active) timing.total = log.elapsed();
|
||||
log.complete("no_matches", {
|
||||
@@ -764,6 +783,7 @@ function run() {
|
||||
parts,
|
||||
matched,
|
||||
injectedSkills: loaded,
|
||||
contextChunks: injectedContextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget,
|
||||
@@ -782,6 +802,7 @@ function run() {
|
||||
toolTarget: toolName === "Bash" ? redactCommand(toolTarget) : toolTarget,
|
||||
matchedSkills: [...matched],
|
||||
injectedSkills: loaded,
|
||||
contextChunks: injectedContextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* SessionStart hook: inject vercel.md as additional context.
|
||||
* SessionStart hook: inject a thin Vercel session context.
|
||||
* Claude Code receives plain-text stdout.
|
||||
* Cursor receives `{ additional_context: "..." }` JSON on stdout.
|
||||
*/
|
||||
@@ -86,9 +86,10 @@ function stripFrontmatter(content: string): string {
|
||||
function main(): void {
|
||||
const input = parseInjectClaudeMdInput(readFileSync(0, "utf8"));
|
||||
const platform = detectInjectClaudeMdPlatform(input);
|
||||
const thinSessionContext = safeReadFile(join(pluginRoot(), "vercel-session.md"));
|
||||
const knowledgeUpdateRaw = safeReadFile(join(pluginRoot(), "skills", "knowledge-update", "SKILL.md"));
|
||||
const knowledgeUpdate = knowledgeUpdateRaw !== null ? stripFrontmatter(knowledgeUpdateRaw) : null;
|
||||
const parts = buildInjectClaudeMdParts(safeReadFile(join(pluginRoot(), "vercel.md")), process.env, knowledgeUpdate);
|
||||
const parts = buildInjectClaudeMdParts(thinSessionContext, process.env, knowledgeUpdate);
|
||||
|
||||
if (parts.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -59,6 +59,7 @@ import type { VercelJsonRouting } from "./vercel-config.mjs";
|
||||
import { createLogger, logDecision } from "./logger.mjs";
|
||||
import type { Logger } from "./logger.mjs";
|
||||
import { trackBaseEvents } from "./telemetry.mjs";
|
||||
import { selectManagedContextChunk } from "./vercel-context.mjs";
|
||||
|
||||
const MAX_SKILLS = 3;
|
||||
const DEFAULT_INJECTION_BUDGET_BYTES = 18_000;
|
||||
@@ -815,6 +816,7 @@ export interface FormatOutputParams {
|
||||
parts: string[];
|
||||
matched: Set<string>;
|
||||
injectedSkills: string[];
|
||||
contextChunks?: string[];
|
||||
summaryOnly?: string[];
|
||||
droppedByCap: string[];
|
||||
droppedByBudget?: string[];
|
||||
@@ -895,6 +897,7 @@ export function formatOutput({
|
||||
parts,
|
||||
matched,
|
||||
injectedSkills,
|
||||
contextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget,
|
||||
@@ -916,6 +919,7 @@ export function formatOutput({
|
||||
toolTarget: toolName === "Bash" ? redactCommand(toolTarget) : toolTarget,
|
||||
matchedSkills: [...matched],
|
||||
injectedSkills,
|
||||
contextChunks: contextChunks || [],
|
||||
summaryOnly: summaryOnly || [],
|
||||
droppedByBudget: droppedByBudget || [],
|
||||
};
|
||||
@@ -1107,6 +1111,23 @@ function run(): string {
|
||||
log.debug("vercel-env-help-appended", { subcommand: vercelEnvHelp.subcommand || "" });
|
||||
}
|
||||
|
||||
const injectedContextChunks: string[] = [];
|
||||
if (!scopeId) {
|
||||
const chunk = selectManagedContextChunk(loaded, {
|
||||
pluginRoot: PLUGIN_ROOT,
|
||||
sessionId,
|
||||
});
|
||||
if (chunk) {
|
||||
parts.push(chunk.wrapped);
|
||||
injectedContextChunks.push(chunk.chunkId);
|
||||
log.debug("managed-context-chunk-injected", {
|
||||
chunkId: chunk.chunkId,
|
||||
skill: chunk.skill,
|
||||
bytes: chunk.bytes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
if (log.active) timing.total = log.elapsed();
|
||||
log.complete("no_matches", {
|
||||
@@ -1155,6 +1176,7 @@ function run(): string {
|
||||
parts,
|
||||
matched,
|
||||
injectedSkills: loaded,
|
||||
contextChunks: injectedContextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget,
|
||||
@@ -1174,6 +1196,7 @@ function run(): string {
|
||||
toolTarget: toolName === "Bash" ? redactCommand(toolTarget) : toolTarget,
|
||||
matchedSkills: [...matched],
|
||||
injectedSkills: loaded,
|
||||
contextChunks: injectedContextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget,
|
||||
|
||||
@@ -48,6 +48,7 @@ import type { PromptAnalysisReport } from "./prompt-analysis.mjs";
|
||||
import { createLogger, logDecision } from "./logger.mjs";
|
||||
import type { Logger } from "./logger.mjs";
|
||||
import { trackBaseEvents } from "./telemetry.mjs";
|
||||
import { selectManagedContextChunk } from "./vercel-context.mjs";
|
||||
|
||||
const MAX_SKILLS = 2;
|
||||
const DEFAULT_INJECTION_BUDGET_BYTES = 8_000;
|
||||
@@ -750,6 +751,7 @@ export function formatOutput(
|
||||
parts: string[],
|
||||
matchedSkills: string[],
|
||||
injectedSkills: string[],
|
||||
contextChunks: string[],
|
||||
summaryOnly: string[],
|
||||
droppedByCap: string[],
|
||||
droppedByBudget: string[],
|
||||
@@ -767,6 +769,7 @@ export function formatOutput(
|
||||
hookEvent: "UserPromptSubmit",
|
||||
matchedSkills,
|
||||
injectedSkills,
|
||||
contextChunks,
|
||||
summaryOnly,
|
||||
droppedByBudget,
|
||||
};
|
||||
@@ -997,6 +1000,20 @@ export function run(): string {
|
||||
if (log.active) timing.inject = Math.round(log.now() - tInject);
|
||||
|
||||
const { parts, loaded, summaryOnly } = injectResult;
|
||||
const injectedContextChunks: string[] = [];
|
||||
const chunk = selectManagedContextChunk(loaded, {
|
||||
pluginRoot: PLUGIN_ROOT,
|
||||
sessionId,
|
||||
});
|
||||
if (chunk) {
|
||||
parts.push(chunk.wrapped);
|
||||
injectedContextChunks.push(chunk.chunkId);
|
||||
log.debug("managed-context-chunk-injected", {
|
||||
chunkId: chunk.chunkId,
|
||||
skill: chunk.skill,
|
||||
bytes: chunk.bytes,
|
||||
});
|
||||
}
|
||||
let syncedSeenSkills = seenState;
|
||||
if (hasFileDedup) {
|
||||
syncedSeenSkills = syncPromptSeenSkillClaims(sessionId as string, loaded);
|
||||
@@ -1028,6 +1045,7 @@ export function run(): string {
|
||||
hookEvent: "UserPromptSubmit",
|
||||
matchedSkills,
|
||||
injectedSkills: loaded,
|
||||
contextChunks: injectedContextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget,
|
||||
@@ -1070,6 +1088,7 @@ export function run(): string {
|
||||
parts,
|
||||
matchedSkills,
|
||||
loaded,
|
||||
injectedContextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
pluginRoot as resolvePluginRoot,
|
||||
safeReadFile,
|
||||
syncSessionFileFromClaims,
|
||||
tryClaimSessionKey,
|
||||
} from "./hook-env.mjs";
|
||||
|
||||
const PLUGIN_ROOT = resolvePluginRoot();
|
||||
const DEFAULT_CONTEXT_CHUNK_BUDGET_BYTES = 1_800;
|
||||
const CONTEXT_CHUNK_KIND = "seen-context-chunks";
|
||||
|
||||
interface ChunkSectionMapping {
|
||||
chunkId: string;
|
||||
heading: string;
|
||||
}
|
||||
|
||||
export interface ManagedContextChunk {
|
||||
chunkId: string;
|
||||
heading: string;
|
||||
skill: string;
|
||||
content: string;
|
||||
wrapped: string;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
interface ManagedContextChunkOptions {
|
||||
pluginRoot?: string;
|
||||
sessionId?: string | null;
|
||||
budgetBytes?: number;
|
||||
}
|
||||
|
||||
const SKILL_TO_CHUNK: Record<string, ChunkSectionMapping> = {
|
||||
"nextjs": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"next-cache-components": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"next-upgrade": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"turbopack": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"next-forge": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"ai-sdk": { chunkId: "ai-stack", heading: "AI Stack" },
|
||||
"ai-gateway": { chunkId: "ai-stack", heading: "AI Stack" },
|
||||
"chat-sdk": { chunkId: "ai-stack", heading: "AI Stack" },
|
||||
"vercel-functions": { chunkId: "compute-routing", heading: "Compute and Routing" },
|
||||
"routing-middleware": { chunkId: "compute-routing", heading: "Compute and Routing" },
|
||||
"runtime-cache": { chunkId: "compute-routing", heading: "Compute and Routing" },
|
||||
"vercel-sandbox": { chunkId: "compute-routing", heading: "Compute and Routing" },
|
||||
"vercel-cli": { chunkId: "deploy-operations", heading: "Deploy and Operations" },
|
||||
"deployments-cicd": { chunkId: "deploy-operations", heading: "Deploy and Operations" },
|
||||
"env-vars": { chunkId: "deploy-operations", heading: "Deploy and Operations" },
|
||||
"marketplace": { chunkId: "deploy-operations", heading: "Deploy and Operations" },
|
||||
"vercel-storage": { chunkId: "storage-data", heading: "Storage and Data" },
|
||||
"workflow": { chunkId: "workflow-durable", heading: "Workflow and Durability" },
|
||||
};
|
||||
|
||||
function parseHeadingSpec(spec: string): { level: number | null; text: string } {
|
||||
const match = spec.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (match) {
|
||||
return { level: match[1].length, text: match[2].trim().toLowerCase() };
|
||||
}
|
||||
return { level: null, text: spec.trim().toLowerCase() };
|
||||
}
|
||||
|
||||
function extractDirectSection(markdown: string, headingSpec: string): string {
|
||||
const { level: specLevel, text: specText } = parseHeadingSpec(headingSpec);
|
||||
const lines = markdown.split("\n");
|
||||
let startLine = -1;
|
||||
let headingLevel = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const headingMatch = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
||||
if (!headingMatch) continue;
|
||||
|
||||
const lineLevel = headingMatch[1].length;
|
||||
const lineText = headingMatch[2].trim().toLowerCase();
|
||||
if (lineText === specText && (specLevel === null || lineLevel === specLevel)) {
|
||||
startLine = i;
|
||||
headingLevel = lineLevel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startLine === -1) return "";
|
||||
|
||||
const contentLines: string[] = [];
|
||||
for (let i = startLine + 1; i < lines.length; i += 1) {
|
||||
const headingMatch = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
||||
if (headingMatch && headingMatch[1].length <= headingLevel) {
|
||||
break;
|
||||
}
|
||||
contentLines.push(lines[i]);
|
||||
}
|
||||
|
||||
return contentLines.join("\n").trim();
|
||||
}
|
||||
|
||||
export function getManagedContextChunkForSkill(
|
||||
skill: string,
|
||||
options?: ManagedContextChunkOptions,
|
||||
): ManagedContextChunk | null {
|
||||
const mapping = SKILL_TO_CHUNK[skill];
|
||||
if (!mapping) return null;
|
||||
|
||||
const root = options?.pluginRoot ?? PLUGIN_ROOT;
|
||||
const raw = safeReadFile(join(root, "vercel.md"));
|
||||
if (raw === null) return null;
|
||||
|
||||
const content = extractDirectSection(raw, mapping.heading);
|
||||
if (!content) return null;
|
||||
|
||||
const wrapped = `<!-- vercel-context-chunk:${mapping.chunkId} -->\n${content}\n<!-- /vercel-context-chunk:${mapping.chunkId} -->`;
|
||||
const bytes = Buffer.byteLength(wrapped, "utf8");
|
||||
const budget = options?.budgetBytes ?? DEFAULT_CONTEXT_CHUNK_BUDGET_BYTES;
|
||||
if (bytes > budget) return null;
|
||||
|
||||
return {
|
||||
chunkId: mapping.chunkId,
|
||||
heading: mapping.heading,
|
||||
skill,
|
||||
content,
|
||||
wrapped,
|
||||
bytes,
|
||||
};
|
||||
}
|
||||
|
||||
export function claimManagedContextChunk(
|
||||
chunkId: string,
|
||||
sessionId?: string | null,
|
||||
): boolean {
|
||||
if (!sessionId) return true;
|
||||
const claimed = tryClaimSessionKey(sessionId, CONTEXT_CHUNK_KIND, chunkId);
|
||||
if (claimed) {
|
||||
syncSessionFileFromClaims(sessionId, CONTEXT_CHUNK_KIND);
|
||||
}
|
||||
return claimed;
|
||||
}
|
||||
|
||||
export function selectManagedContextChunk(
|
||||
orderedSkills: string[],
|
||||
options?: ManagedContextChunkOptions,
|
||||
): ManagedContextChunk | null {
|
||||
if (orderedSkills.length === 0) return null;
|
||||
|
||||
const topSkill = orderedSkills[0];
|
||||
const chunk = getManagedContextChunkForSkill(topSkill, options);
|
||||
if (!chunk) return null;
|
||||
|
||||
return claimManagedContextChunk(chunk.chunkId, options?.sessionId) ? chunk : null;
|
||||
}
|
||||
|
||||
export { DEFAULT_CONTEXT_CHUNK_BUDGET_BYTES };
|
||||
@@ -26,6 +26,7 @@ import { searchSkills, initializeLexicalIndex } from "./lexical-index.mjs";
|
||||
import { analyzePrompt } from "./prompt-analysis.mjs";
|
||||
import { createLogger, logDecision } from "./logger.mjs";
|
||||
import { trackBaseEvents } from "./telemetry.mjs";
|
||||
import { selectManagedContextChunk } from "./vercel-context.mjs";
|
||||
var MAX_SKILLS = 2;
|
||||
var DEFAULT_INJECTION_BUDGET_BYTES = 8e3;
|
||||
var MIN_PROMPT_LENGTH = 10;
|
||||
@@ -471,7 +472,7 @@ function deduplicateAndInject(matches, skills, logger, platform) {
|
||||
matchedSkills: allMatched
|
||||
};
|
||||
}
|
||||
function formatOutput(parts, matchedSkills, injectedSkills, summaryOnly, droppedByCap, droppedByBudget, promptMatchReasons, skillMap, platform = "claude-code", env) {
|
||||
function formatOutput(parts, matchedSkills, injectedSkills, contextChunks, summaryOnly, droppedByCap, droppedByBudget, promptMatchReasons, skillMap, platform = "claude-code", env) {
|
||||
if (parts.length === 0) {
|
||||
return formatEmptyOutput(platform, env);
|
||||
}
|
||||
@@ -480,6 +481,7 @@ function formatOutput(parts, matchedSkills, injectedSkills, summaryOnly, dropped
|
||||
hookEvent: "UserPromptSubmit",
|
||||
matchedSkills,
|
||||
injectedSkills,
|
||||
contextChunks,
|
||||
summaryOnly,
|
||||
droppedByBudget
|
||||
};
|
||||
@@ -658,6 +660,20 @@ function run() {
|
||||
});
|
||||
if (log.active) timing.inject = Math.round(log.now() - tInject);
|
||||
const { parts, loaded, summaryOnly } = injectResult;
|
||||
const injectedContextChunks = [];
|
||||
const chunk = selectManagedContextChunk(loaded, {
|
||||
pluginRoot: PLUGIN_ROOT,
|
||||
sessionId
|
||||
});
|
||||
if (chunk) {
|
||||
parts.push(chunk.wrapped);
|
||||
injectedContextChunks.push(chunk.chunkId);
|
||||
log.debug("managed-context-chunk-injected", {
|
||||
chunkId: chunk.chunkId,
|
||||
skill: chunk.skill,
|
||||
bytes: chunk.bytes
|
||||
});
|
||||
}
|
||||
let syncedSeenSkills = seenState;
|
||||
if (hasFileDedup) {
|
||||
syncedSeenSkills = syncPromptSeenSkillClaims(sessionId, loaded);
|
||||
@@ -685,6 +701,7 @@ function run() {
|
||||
hookEvent: "UserPromptSubmit",
|
||||
matchedSkills,
|
||||
injectedSkills: loaded,
|
||||
contextChunks: injectedContextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget
|
||||
@@ -723,6 +740,7 @@ function run() {
|
||||
parts,
|
||||
matchedSkills,
|
||||
loaded,
|
||||
injectedContextChunks,
|
||||
summaryOnly,
|
||||
droppedByCap,
|
||||
droppedByBudget,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// hooks/src/vercel-context.mts
|
||||
import { join } from "path";
|
||||
import {
|
||||
pluginRoot as resolvePluginRoot,
|
||||
safeReadFile,
|
||||
syncSessionFileFromClaims,
|
||||
tryClaimSessionKey
|
||||
} from "./hook-env.mjs";
|
||||
var PLUGIN_ROOT = resolvePluginRoot();
|
||||
var DEFAULT_CONTEXT_CHUNK_BUDGET_BYTES = 1800;
|
||||
var CONTEXT_CHUNK_KIND = "seen-context-chunks";
|
||||
var SKILL_TO_CHUNK = {
|
||||
"nextjs": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"next-cache-components": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"next-upgrade": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"turbopack": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"next-forge": { chunkId: "nextjs-platform", heading: "Next.js and Rendering" },
|
||||
"ai-sdk": { chunkId: "ai-stack", heading: "AI Stack" },
|
||||
"ai-gateway": { chunkId: "ai-stack", heading: "AI Stack" },
|
||||
"chat-sdk": { chunkId: "ai-stack", heading: "AI Stack" },
|
||||
"vercel-functions": { chunkId: "compute-routing", heading: "Compute and Routing" },
|
||||
"routing-middleware": { chunkId: "compute-routing", heading: "Compute and Routing" },
|
||||
"runtime-cache": { chunkId: "compute-routing", heading: "Compute and Routing" },
|
||||
"vercel-sandbox": { chunkId: "compute-routing", heading: "Compute and Routing" },
|
||||
"vercel-cli": { chunkId: "deploy-operations", heading: "Deploy and Operations" },
|
||||
"deployments-cicd": { chunkId: "deploy-operations", heading: "Deploy and Operations" },
|
||||
"env-vars": { chunkId: "deploy-operations", heading: "Deploy and Operations" },
|
||||
"marketplace": { chunkId: "deploy-operations", heading: "Deploy and Operations" },
|
||||
"vercel-storage": { chunkId: "storage-data", heading: "Storage and Data" },
|
||||
"workflow": { chunkId: "workflow-durable", heading: "Workflow and Durability" }
|
||||
};
|
||||
function parseHeadingSpec(spec) {
|
||||
const match = spec.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (match) {
|
||||
return { level: match[1].length, text: match[2].trim().toLowerCase() };
|
||||
}
|
||||
return { level: null, text: spec.trim().toLowerCase() };
|
||||
}
|
||||
function extractDirectSection(markdown, headingSpec) {
|
||||
const { level: specLevel, text: specText } = parseHeadingSpec(headingSpec);
|
||||
const lines = markdown.split("\n");
|
||||
let startLine = -1;
|
||||
let headingLevel = 0;
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const headingMatch = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
||||
if (!headingMatch) continue;
|
||||
const lineLevel = headingMatch[1].length;
|
||||
const lineText = headingMatch[2].trim().toLowerCase();
|
||||
if (lineText === specText && (specLevel === null || lineLevel === specLevel)) {
|
||||
startLine = i;
|
||||
headingLevel = lineLevel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (startLine === -1) return "";
|
||||
const contentLines = [];
|
||||
for (let i = startLine + 1; i < lines.length; i += 1) {
|
||||
const headingMatch = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
||||
if (headingMatch && headingMatch[1].length <= headingLevel) {
|
||||
break;
|
||||
}
|
||||
contentLines.push(lines[i]);
|
||||
}
|
||||
return contentLines.join("\n").trim();
|
||||
}
|
||||
function getManagedContextChunkForSkill(skill, options) {
|
||||
const mapping = SKILL_TO_CHUNK[skill];
|
||||
if (!mapping) return null;
|
||||
const root = options?.pluginRoot ?? PLUGIN_ROOT;
|
||||
const raw = safeReadFile(join(root, "vercel.md"));
|
||||
if (raw === null) return null;
|
||||
const content = extractDirectSection(raw, mapping.heading);
|
||||
if (!content) return null;
|
||||
const wrapped = `<!-- vercel-context-chunk:${mapping.chunkId} -->
|
||||
${content}
|
||||
<!-- /vercel-context-chunk:${mapping.chunkId} -->`;
|
||||
const bytes = Buffer.byteLength(wrapped, "utf8");
|
||||
const budget = options?.budgetBytes ?? DEFAULT_CONTEXT_CHUNK_BUDGET_BYTES;
|
||||
if (bytes > budget) return null;
|
||||
return {
|
||||
chunkId: mapping.chunkId,
|
||||
heading: mapping.heading,
|
||||
skill,
|
||||
content,
|
||||
wrapped,
|
||||
bytes
|
||||
};
|
||||
}
|
||||
function claimManagedContextChunk(chunkId, sessionId) {
|
||||
if (!sessionId) return true;
|
||||
const claimed = tryClaimSessionKey(sessionId, CONTEXT_CHUNK_KIND, chunkId);
|
||||
if (claimed) {
|
||||
syncSessionFileFromClaims(sessionId, CONTEXT_CHUNK_KIND);
|
||||
}
|
||||
return claimed;
|
||||
}
|
||||
function selectManagedContextChunk(orderedSkills, options) {
|
||||
if (orderedSkills.length === 0) return null;
|
||||
const topSkill = orderedSkills[0];
|
||||
const chunk = getManagedContextChunkForSkill(topSkill, options);
|
||||
if (!chunk) return null;
|
||||
return claimManagedContextChunk(chunk.chunkId, options?.sessionId) ? chunk : null;
|
||||
}
|
||||
export {
|
||||
DEFAULT_CONTEXT_CHUNK_BUDGET_BYTES,
|
||||
claimManagedContextChunk,
|
||||
getManagedContextChunkForSkill,
|
||||
selectManagedContextChunk
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, "..");
|
||||
const HOOK_SCRIPT = join(ROOT, "hooks", "inject-claude-md.mjs");
|
||||
|
||||
async function runHook(
|
||||
payload: Record<string, unknown>,
|
||||
env?: Record<string, string | undefined>,
|
||||
): Promise<{ code: number; stdout: string; stderr: string }> {
|
||||
const mergedEnv: Record<string, string> = { ...(process.env as Record<string, string>) };
|
||||
for (const [key, value] of Object.entries(env || {})) {
|
||||
if (value === undefined) {
|
||||
delete mergedEnv[key];
|
||||
} else {
|
||||
mergedEnv[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const proc = Bun.spawn(["node", HOOK_SCRIPT], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: mergedEnv,
|
||||
});
|
||||
proc.stdin.write(JSON.stringify(payload));
|
||||
proc.stdin.end();
|
||||
|
||||
const code = await proc.exited;
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
return { code, stdout, stderr };
|
||||
}
|
||||
|
||||
describe("inject-claude-md", () => {
|
||||
test("injects thin session context instead of the full vercel ecosystem graph", async () => {
|
||||
const { code, stdout } = await runHook({ session_id: "inject-thin-session" });
|
||||
expect(code).toBe(0);
|
||||
expect(stdout).toContain("Vercel Plugin Session Context");
|
||||
expect(stdout).toContain("Vercel Knowledge Updates");
|
||||
expect(stdout).not.toContain("Vercel Ecosystem — Relational Knowledge Graph");
|
||||
});
|
||||
|
||||
test("appends greenfield guidance when VERCEL_PLUGIN_GREENFIELD=true", async () => {
|
||||
const { code, stdout } = await runHook(
|
||||
{ session_id: "inject-thin-greenfield" },
|
||||
{ VERCEL_PLUGIN_GREENFIELD: "true" },
|
||||
);
|
||||
expect(code).toBe(0);
|
||||
expect(stdout).toContain("Greenfield execution mode");
|
||||
});
|
||||
|
||||
test("cursor payload returns flat JSON with thin additional context", async () => {
|
||||
const { code, stdout } = await runHook({
|
||||
conversation_id: "inject-thin-cursor",
|
||||
cursor_version: "1.0.0",
|
||||
workspace_roots: [ROOT],
|
||||
});
|
||||
expect(code).toBe(0);
|
||||
const result = JSON.parse(stdout);
|
||||
expect(result.additional_context).toContain("Vercel Plugin Session Context");
|
||||
expect(result.additional_context).not.toContain("Vercel Ecosystem — Relational Knowledge Graph");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import {
|
||||
getManagedContextChunkForSkill,
|
||||
selectManagedContextChunk,
|
||||
} from "../hooks/src/vercel-context.mts";
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, "..");
|
||||
const PRETOOL_HOOK = join(ROOT, "hooks", "pretooluse-skill-inject.mjs");
|
||||
const PROMPT_HOOK = join(ROOT, "hooks", "user-prompt-submit-skill-inject.mjs");
|
||||
|
||||
function cleanupSessionArtifacts(sessionId: string): void {
|
||||
const prefix = `vercel-plugin-${sessionId}-`;
|
||||
try {
|
||||
for (const entry of readdirSync(tmpdir())) {
|
||||
if (entry.startsWith(prefix)) {
|
||||
rmSync(join(tmpdir(), entry), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// best-effort cleanup for tests
|
||||
}
|
||||
}
|
||||
|
||||
async function runPretoolHook(
|
||||
input: object,
|
||||
sessionId: string,
|
||||
): Promise<{ code: number; stdout: string; stderr: string }> {
|
||||
const proc = Bun.spawn(["node", PRETOOL_HOOK], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: { ...(process.env as Record<string, string>) },
|
||||
});
|
||||
proc.stdin.write(JSON.stringify({ ...input, session_id: sessionId }));
|
||||
proc.stdin.end();
|
||||
const code = await proc.exited;
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
return { code, stdout, stderr };
|
||||
}
|
||||
|
||||
async function runPromptHook(
|
||||
prompt: string,
|
||||
sessionId: string,
|
||||
envOverrides?: Record<string, string>,
|
||||
): Promise<{ code: number; stdout: string; stderr: string }> {
|
||||
const proc = Bun.spawn(["node", PROMPT_HOOK], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: { ...(process.env as Record<string, string>), ...(envOverrides || {}) },
|
||||
});
|
||||
proc.stdin.write(JSON.stringify({
|
||||
prompt,
|
||||
session_id: sessionId,
|
||||
cwd: ROOT,
|
||||
hook_event_name: "UserPromptSubmit",
|
||||
}));
|
||||
proc.stdin.end();
|
||||
const code = await proc.exited;
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
return { code, stdout, stderr };
|
||||
}
|
||||
|
||||
function createTestSession(): string {
|
||||
return `vercel-context-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
describe("managed vercel context chunks", () => {
|
||||
test("extracts a small nextjs chunk from vercel.md", () => {
|
||||
const chunk = getManagedContextChunkForSkill("nextjs", { pluginRoot: ROOT });
|
||||
expect(chunk).not.toBeNull();
|
||||
expect(chunk?.chunkId).toBe("nextjs-platform");
|
||||
expect(chunk?.wrapped).toContain("Default to Next.js App Router");
|
||||
expect(chunk?.wrapped).toContain("vercel-context-chunk:nextjs-platform");
|
||||
});
|
||||
|
||||
test("returns null for unmapped skills", () => {
|
||||
expect(getManagedContextChunkForSkill("shadcn", { pluginRoot: ROOT })).toBeNull();
|
||||
});
|
||||
|
||||
test("deduplicates chunk claims per session", () => {
|
||||
const testSession = createTestSession();
|
||||
const first = selectManagedContextChunk(["nextjs"], {
|
||||
pluginRoot: ROOT,
|
||||
sessionId: testSession,
|
||||
});
|
||||
const second = selectManagedContextChunk(["nextjs"], {
|
||||
pluginRoot: ROOT,
|
||||
sessionId: testSession,
|
||||
});
|
||||
|
||||
expect(first).not.toBeNull();
|
||||
expect(second).toBeNull();
|
||||
cleanupSessionArtifacts(testSession);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on-demand context injection", () => {
|
||||
test("pretooluse appends a nextjs chunk after skill injection", async () => {
|
||||
const testSession = createTestSession();
|
||||
try {
|
||||
const { code, stdout } = await runPretoolHook({
|
||||
tool_name: "Read",
|
||||
tool_input: { file_path: "/Users/me/project/next.config.ts" },
|
||||
}, testSession);
|
||||
|
||||
expect(code).toBe(0);
|
||||
const parsed = JSON.parse(stdout);
|
||||
const ctx = parsed.hookSpecificOutput.additionalContext as string;
|
||||
expect(ctx).toContain("Skill(");
|
||||
expect(ctx).toContain("<!-- vercel-context-chunk:nextjs-platform -->");
|
||||
expect(ctx).toContain("Default to Next.js App Router");
|
||||
} finally {
|
||||
cleanupSessionArtifacts(testSession);
|
||||
}
|
||||
});
|
||||
|
||||
test("prompt hook appends ai chunk once and dedups it across later prompts", async () => {
|
||||
const testSession = createTestSession();
|
||||
try {
|
||||
const first = await runPromptHook(
|
||||
"I need to use the AI SDK to add streaming text generation to this endpoint",
|
||||
testSession,
|
||||
);
|
||||
expect(first.code).toBe(0);
|
||||
const firstParsed = JSON.parse(first.stdout);
|
||||
const firstCtx = firstParsed.hookSpecificOutput.additionalContext as string;
|
||||
expect(firstCtx).toContain("<!-- vercel-context-chunk:ai-stack -->");
|
||||
|
||||
const second = await runPromptHook(
|
||||
"Build a conversational interface for a Discord bot that responds to mentions",
|
||||
testSession,
|
||||
);
|
||||
expect(second.code).toBe(0);
|
||||
const secondParsed = JSON.parse(second.stdout);
|
||||
const secondCtx = secondParsed.hookSpecificOutput.additionalContext as string;
|
||||
expect(secondCtx).toContain("Skill(chat-sdk)");
|
||||
expect(secondCtx).not.toContain("<!-- vercel-context-chunk:ai-stack -->");
|
||||
} finally {
|
||||
cleanupSessionArtifacts(testSession);
|
||||
}
|
||||
});
|
||||
|
||||
test("prompt audit log records context chunk ids", async () => {
|
||||
const testSession = createTestSession();
|
||||
const auditLogPath = join(tmpdir(), `${testSession}-audit.jsonl`);
|
||||
try {
|
||||
const result = await runPromptHook(
|
||||
"I need to use the AI SDK to add streaming text generation to this endpoint",
|
||||
testSession,
|
||||
{ VERCEL_PLUGIN_AUDIT_LOG_FILE: auditLogPath },
|
||||
);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(existsSync(auditLogPath)).toBe(true);
|
||||
|
||||
const entries = readFileSync(auditLogPath, "utf8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line));
|
||||
const promptEntry = entries.find((entry) => entry.event === "prompt-skill-injection");
|
||||
|
||||
expect(promptEntry).toBeDefined();
|
||||
expect(promptEntry.injectedSkills).toContain("ai-sdk");
|
||||
expect(promptEntry.contextChunks).toEqual(["ai-stack"]);
|
||||
} finally {
|
||||
cleanupSessionArtifacts(testSession);
|
||||
rmSync(auditLogPath, { force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
# Vercel Plugin Session Context
|
||||
|
||||
Use Vercel guidance only when the current repo, prompt, or tool call makes it relevant.
|
||||
|
||||
- Prefer matched skills and official docs over memorized APIs.
|
||||
- Default Vercel assumptions: Next.js App Router, AI SDK v6, `vercel env` for environment management, and platform-native integrations before custom infrastructure.
|
||||
- Do not push broad Vercel migrations or product recommendations unless they directly help the current task.
|
||||
- The full ecosystem graph stays in `vercel.md`; runtime hooks now load only thin, topic-sized chunks on demand.
|
||||
@@ -808,6 +808,50 @@ Git Push → CI Pipeline → vercel build → vercel deploy --prebuilt
|
||||
- Use `vercel deploy --prebuilt` in CI for fastest deploys (separate build from deploy).
|
||||
- For monorepos, use Turborepo with remote caching and `--affected` for efficient CI.
|
||||
|
||||
## Session Chunks
|
||||
|
||||
### Next.js and Rendering
|
||||
|
||||
- Default to Next.js App Router and Server Components; add `'use client'` only where interactivity is required.
|
||||
- Use `proxy.ts` instead of `middleware.ts` in Next.js 16 and keep proxy logic focused on interception, auth gates, rewrites, and redirects.
|
||||
- Prefer Cache Components, `next/image`, and `next/font` over custom rendering/caching/font-loading workarounds.
|
||||
- Treat `@vercel/postgres` and `@vercel/kv` as sunset packages; use modern storage integrations instead.
|
||||
|
||||
### AI Stack
|
||||
|
||||
- Default to AI SDK v6 plus AI Gateway model strings (`provider/model`) rather than direct provider SDK wiring.
|
||||
- Use `@ai-sdk/react` for React hooks and keep user-facing AI experiences streaming-first.
|
||||
- Prefer `toUIMessageStreamResponse()`, `DefaultChatTransport`, and modern tool definitions (`inputSchema`, `outputSchema`) over v5-era patterns.
|
||||
- Keep AI recommendations scoped to the current task; do not force broad AI stack migrations unless the code already points there.
|
||||
|
||||
### Compute and Routing
|
||||
|
||||
- Prefer Vercel Functions, Route Handlers, and proxy/routing primitives over custom servers when the task fits platform-native execution.
|
||||
- Use Fluid Compute, `waitUntil`/`after`, and Runtime Cache for long-running work, background tasks, and shared caching.
|
||||
- Keep routing guidance focused on the matched boundary: request interception, rewrites/redirects, function behavior, or cache invalidation.
|
||||
- Do not recommend Edge-only patterns by default when regular Node.js execution solves the problem cleanly.
|
||||
|
||||
### Deploy and Operations
|
||||
|
||||
- Use `vercel env`, linked projects, and Marketplace-managed provisioning instead of hardcoded secrets or manual config drift.
|
||||
- For deploy workflows, prefer `vercel deploy`, `--prebuilt` CI flows, and platform-native preview/production promotion patterns.
|
||||
- Keep environment and deployment advice narrow to the current repo state rather than reciting the whole platform.
|
||||
- Only surface Marketplace or CLI recommendations when the prompt, files, or commands already imply those workflows.
|
||||
|
||||
### Storage and Data
|
||||
|
||||
- Prefer current Vercel data integrations such as Neon, Upstash, Blob, and Edge Config over sunset packages.
|
||||
- Match storage advice to the active need: relational data, cache/queue-style access, blob assets, or low-latency config reads.
|
||||
- Avoid recommending data migrations unless the codebase is actually using deprecated Vercel storage packages.
|
||||
- Keep data-layer guidance practical: client choice, env setup, and runtime-fit over product catalog detail.
|
||||
|
||||
### Workflow and Durability
|
||||
|
||||
- Use Workflow DevKit and DurableAgent when the task needs retries, resumability, crash recovery, or long-lived orchestration.
|
||||
- Prefer workflow steps over ad-hoc retry loops, timers, and manual state persistence in request handlers.
|
||||
- Keep workflow recommendations limited to durable execution problems; do not route ordinary request/response code into workflow patterns by default.
|
||||
- When workflow context is injected, emphasize survival of crashes, retries, and async callback orchestration.
|
||||
|
||||
---
|
||||
|
||||
## Plugin Mechanics
|
||||
@@ -816,7 +860,9 @@ This document is part of the **Vercel plugin for Claude Code**. The plugin uses
|
||||
|
||||
### SessionStart — Baseline injection
|
||||
|
||||
On every session event (`startup`, `resume`, `clear`, `compact`), the `inject-claude-md.mjs` hook runs and feeds the entire contents of `vercel.md` (this file) into the conversation as foundational context. This ensures every session starts with the full Vercel ecosystem knowledge graph.
|
||||
On every session event (`startup`, `resume`, `clear`, `compact`), the `inject-claude-md.mjs` hook injects a thin Vercel session context plus the `knowledge-update` skill. The full ecosystem graph in `vercel.md` is no longer injected wholesale at session start.
|
||||
|
||||
Deeper `vercel.md` guidance is now loaded later in small topic chunks when prompt or tool-time skill matching shows it is relevant. Chunking is deduped per session so the same Vercel topic is not repeated on every prompt.
|
||||
|
||||
**Hook config** (`hooks/hooks.json`):
|
||||
```json
|
||||
|
||||
Reference in New Issue
Block a user