From 3b1a3aba2f49a1328a1add350a64276be968a6de Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Tue, 23 Jun 2026 20:17:00 +0800 Subject: [PATCH 1/9] docs(skills): clarify Makers dev script usage Document that package scripts should keep frontend dev servers separate from the EdgeOne Makers wrapper to avoid recursive local dev startup. Co-authored-by: Cursor --- skills/makers-agents/SKILL.md | 18 +++++++++++- .../references/node-frameworks/claude-sdk.md | 2 ++ .../references/node-frameworks/deepagents.md | 1 + .../references/node-frameworks/langgraph.md | 2 ++ .../node-frameworks/openai-agents.md | 2 ++ .../python-frameworks/claude-sdk.md | 2 ++ .../references/python-frameworks/crewai.md | 2 ++ .../python-frameworks/deepagents.md | 1 + .../references/python-frameworks/langgraph.md | 2 ++ .../python-frameworks/openai-agents.md | 2 ++ .../references/review-checklist.md | 4 +++ skills/makers-cli/SKILL.md | 29 +++++++++++++++++-- skills/makers-recipes/SKILL.md | 18 ++++++++++++ 13 files changed, 81 insertions(+), 4 deletions(-) diff --git a/skills/makers-agents/SKILL.md b/skills/makers-agents/SKILL.md index 3262b2c..8602153 100644 --- a/skills/makers-agents/SKILL.md +++ b/skills/makers-agents/SKILL.md @@ -292,6 +292,21 @@ CLI will automatically serve `public/` as static files during `edgeone makers de - `edgeone makers build` — builds agents + frontend into `.edgeone/` output - `edgeone makers deploy` — builds and deploys to EdgeOne Makers +### Local development + +```bash +# 1. Link to remote project (pulls project ID + env vars) +PAGES_SOURCE=skills edgeone makers link + +# 2. Pull remote environment variables to local .env +PAGES_SOURCE=skills edgeone makers env pull + +# 3. Start local dev server (agent runtime + frontend) +npm run makers:dev +``` + +`npm run makers:dev` starts `edgeone makers dev`, which starts both the agent runtime (Node or Python, auto-detected from `agents/` file extensions) and the frontend dev server. Test through the Makers entry URL printed by the CLI. Agent endpoints are available through that Makers proxy, not through the raw frontend dev-server port. + ### Environment variables for deployment **AI Gateway variables** (`AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`) are **auto-provisioned** by the CLI during deployment — no manual setup needed, as long as `.env.example` declares them: @@ -353,7 +368,8 @@ edgeone makers env pull 2. Copy the skeleton from the matching framework reference doc. 3. Configure `edgeone.json`: set `agents.framework` correctly. 4. Frontend: `getOrCreateConversationId` + `fetch` with `makers-conversation-id` header. -5. Get it running → self-check against the Critical Rules → run through `references/review-checklist.md`. +5. Package scripts: keep `dev` as the frontend dev server, and put the Makers wrapper in `makers:dev`. +6. Get it running → self-check against the Critical Rules → run through `references/review-checklist.md`. ### Pre-Deploy SOP (⚠️ MUST execute before `edgeone makers deploy`) diff --git a/skills/makers-agents/references/node-frameworks/claude-sdk.md b/skills/makers-agents/references/node-frameworks/claude-sdk.md index 9b92bfb..635570f 100644 --- a/skills/makers-agents/references/node-frameworks/claude-sdk.md +++ b/skills/makers-agents/references/node-frameworks/claude-sdk.md @@ -22,6 +22,8 @@ npm install @anthropic-ai/claude-agent-sdk zod > `@anthropic-ai/claude-agent-sdk` is **auto-externalized** by the CLI — no manual `externalNodeModules` config needed. +> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). + --- ## When to Use Route B diff --git a/skills/makers-agents/references/node-frameworks/deepagents.md b/skills/makers-agents/references/node-frameworks/deepagents.md index fd09a18..18877ed 100644 --- a/skills/makers-agents/references/node-frameworks/deepagents.md +++ b/skills/makers-agents/references/node-frameworks/deepagents.md @@ -11,6 +11,7 @@ npm install deepagents@^1.9.0 @langchain/openai @langchain/core zod ``` +> **Note**: `deepagents` is a platform-provided package bundled with the EdgeOne Makers agent runtime. It is automatically available in the deployed environment. For local development, use `edgeone makers dev` which sets up the runtime with all platform packages. If adding package scripts, put this wrapper in `makers:dev`; keep `dev` for the frontend dev server only. `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/node-frameworks/langgraph.md b/skills/makers-agents/references/node-frameworks/langgraph.md index 7ed328e..74a24bd 100644 --- a/skills/makers-agents/references/node-frameworks/langgraph.md +++ b/skills/makers-agents/references/node-frameworks/langgraph.md @@ -22,6 +22,8 @@ npm install @langchain/langgraph @langchain/openai @langchain/core zod > All `@langchain/*` packages are **auto-externalized** by the CLI — no manual `externalNodeModules` config needed. +> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). + --- ## When to Pick LangGraph diff --git a/skills/makers-agents/references/node-frameworks/openai-agents.md b/skills/makers-agents/references/node-frameworks/openai-agents.md index 2aeb6e6..dad493a 100644 --- a/skills/makers-agents/references/node-frameworks/openai-agents.md +++ b/skills/makers-agents/references/node-frameworks/openai-agents.md @@ -22,6 +22,8 @@ npm install @openai/agents openai zod > If you encounter build errors like `Dynamic require` or `Cannot find module`, add `"externalNodeModules": ["openai", "@openai/agents"]` to the `agents` config. Unlike `deepagents` / `@langchain/*` / `claude-agent-sdk`, these are not auto-externalized. +> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). + --- ## When to use Route C diff --git a/skills/makers-agents/references/python-frameworks/claude-sdk.md b/skills/makers-agents/references/python-frameworks/claude-sdk.md index 27defce..25ead28 100644 --- a/skills/makers-agents/references/python-frameworks/claude-sdk.md +++ b/skills/makers-agents/references/python-frameworks/claude-sdk.md @@ -17,6 +17,8 @@ claude-agent-sdk>=0.1.0 pip install -r requirements.txt ``` +> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). + `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/python-frameworks/crewai.md b/skills/makers-agents/references/python-frameworks/crewai.md index 554d033..2c2e026 100644 --- a/skills/makers-agents/references/python-frameworks/crewai.md +++ b/skills/makers-agents/references/python-frameworks/crewai.md @@ -157,6 +157,8 @@ Install dependencies locally for development: pip install -r requirements.txt ``` +> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). + ```txt # CrewAI core (version aligned with the platform's bundled lib: .edgeone/agent-python/lib/) crewai>=1.14.5 diff --git a/skills/makers-agents/references/python-frameworks/deepagents.md b/skills/makers-agents/references/python-frameworks/deepagents.md index df450df..048febc 100644 --- a/skills/makers-agents/references/python-frameworks/deepagents.md +++ b/skills/makers-agents/references/python-frameworks/deepagents.md @@ -20,6 +20,7 @@ pydantic>=2.0.0 pip install -r requirements.txt ``` +> **Note**: `deepagents` is a platform-provided package bundled with the EdgeOne Makers agent runtime. It is automatically available in the deployed environment. For local development, use `edgeone makers dev`. If adding package scripts, put this wrapper in `makers:dev`; keep `dev` for the frontend dev server only. `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/python-frameworks/langgraph.md b/skills/makers-agents/references/python-frameworks/langgraph.md index cea2975..25b5abb 100644 --- a/skills/makers-agents/references/python-frameworks/langgraph.md +++ b/skills/makers-agents/references/python-frameworks/langgraph.md @@ -21,6 +21,8 @@ pydantic>=2.0.0 pip install -r requirements.txt ``` +> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). + `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/python-frameworks/openai-agents.md b/skills/makers-agents/references/python-frameworks/openai-agents.md index 7679e76..faaa9ae 100644 --- a/skills/makers-agents/references/python-frameworks/openai-agents.md +++ b/skills/makers-agents/references/python-frameworks/openai-agents.md @@ -19,6 +19,8 @@ pydantic>=2.0.0 pip install -r requirements.txt ``` +> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). + `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/review-checklist.md b/skills/makers-agents/references/review-checklist.md index 02d8309..e47cfb8 100644 --- a/skills/makers-agents/references/review-checklist.md +++ b/skills/makers-agents/references/review-checklist.md @@ -24,6 +24,8 @@ - [ ] `edgeone.json` sets `agents.framework` (`claude-agent-sdk` / `openai-agents-sdk` / `langgraph` / `crewai` / `deepagents` — **no `basic`**, the schema enum does not include it) — **required for console icon display** +- [ ] ⚠️ `package.json` keeps `dev` as the frontend dev server command only, and puts `edgeone makers dev` in a separate wrapper such as `makers:dev`; never set `dev` to `PAGES_SOURCE=skills npx --yes edgeone makers dev` + - [ ] Frontend lives under `app/`, components under `app/components/`, global utilities under root `lib/` --- @@ -194,6 +196,8 @@ - [ ] `process.env` is allowed on the frontend (consistent with frontend frameworks), but **do not** expose backend secrets like `AI_GATEWAY_API_KEY` to the browser +- [ ] Local testing is done through the Makers proxy URL printed by `edgeone makers dev` / `npm run makers:dev`; the raw frontend dev-server port does not serve `agents/` routes + --- ## Remediation Table: from "generic Vercel style" → "EdgeOne Makers style" diff --git a/skills/makers-cli/SKILL.md b/skills/makers-cli/SKILL.md index 4a1dcc7..7e50492 100644 --- a/skills/makers-cli/SKILL.md +++ b/skills/makers-cli/SKILL.md @@ -48,15 +48,38 @@ export PAGES_SOURCE=skills Or inline: `PAGES_SOURCE=skills edgeone makers dev` +## Package Scripts For Agent Projects + +For projects that contain `agents/` plus a frontend, do not put `edgeone makers dev` in `package.json`'s `dev` script. The Makers CLI uses `npm run dev -- --port ` as the frontend dev server command, so `dev` must be a real frontend server: + +```json +{ + "scripts": { + "dev": "vite --host 127.0.0.1", + "makers:dev": "PAGES_SOURCE=skills npx --yes edgeone makers dev", + "makers:build": "PAGES_SOURCE=skills npx --yes edgeone makers build", + "deploy": "PAGES_SOURCE=skills npx --yes edgeone makers deploy" + } +} +``` + +Run `npm run makers:dev` to start Makers local development, then test through the Makers URL it prints (usually `http://localhost:8088`). Do not test Agent endpoints through the raw frontend port; that server only serves frontend assets. + ## Common Workflows ### First-time setup ```bash npm install -g edgeone edgeone login -edgeone makers link -edgeone makers env pull -edgeone makers dev +PAGES_SOURCE=skills edgeone makers link +PAGES_SOURCE=skills edgeone makers env pull +PAGES_SOURCE=skills edgeone makers dev +``` + +### Project script workflow +```bash +npm install +npm run makers:dev ``` ### Deploy diff --git a/skills/makers-recipes/SKILL.md b/skills/makers-recipes/SKILL.md index aaed490..fb3832a 100644 --- a/skills/makers-recipes/SKILL.md +++ b/skills/makers-recipes/SKILL.md @@ -14,6 +14,24 @@ metadata: Project structure templates for typical EdgeOne Makers applications. +## Agent project package scripts + +For any recipe that creates an Agent project with a frontend, keep the frontend dev script and Makers wrapper script separate: + +```json +{ + "scripts": { + "dev": "vite --host 127.0.0.1", + "makers:dev": "PAGES_SOURCE=skills npx --yes edgeone makers dev", + "build": "npm run typecheck", + "makers:build": "PAGES_SOURCE=skills npx --yes edgeone makers build", + "deploy": "PAGES_SOURCE=skills npx --yes edgeone makers deploy" + } +} +``` + +`dev` is reserved for the frontend dev server because `edgeone makers dev` invokes it behind the Makers proxy with a chosen port. Put the EdgeOne Makers CLI command in `makers:dev` and tell users to open the Makers proxy URL, not the raw frontend port, when testing `agents/` endpoints. + ## Full-stack app — Node.js (static + API) ``` From f589934ea1d6466d21eecbc4e0ff6becaa5d96d3 Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Fri, 26 Jun 2026 14:48:32 +0800 Subject: [PATCH 2/9] skill hooks --- .gitignore | 6 +- hooks/hooks.json | 20 +- hooks/on-prompt.sh | 18 +- hooks/on-session-start.sh | 9 + hooks/pretooluse-skill-inject.mjs | 457 +++++++++++++++++ hooks/pretooluse-skill-inject.test.mjs | 536 ++++++++++++++++++++ hooks/sessionstart-minimal-context.mjs | 31 ++ hooks/sessionstart-minimal-context.test.mjs | 34 ++ hooks/signal-log.mjs | 43 ++ hooks/signal-log.test.mjs | 52 ++ hooks/userprompt-skill-inject.mjs | 172 +++++++ hooks/userprompt-skill-inject.test.mjs | 100 ++++ skills/makers-agents/SKILL.md | 6 + skills/makers-cli/SKILL.md | 2 + skills/makers-cloud-functions/SKILL.md | 2 + skills/makers-deploy/SKILL.md | 5 + skills/makers-edge-functions/SKILL.md | 23 +- skills/makers-middleware/SKILL.md | 13 +- 18 files changed, 1484 insertions(+), 45 deletions(-) mode change 100644 => 100755 hooks/on-prompt.sh create mode 100755 hooks/on-session-start.sh create mode 100644 hooks/pretooluse-skill-inject.mjs create mode 100644 hooks/pretooluse-skill-inject.test.mjs create mode 100644 hooks/sessionstart-minimal-context.mjs create mode 100644 hooks/sessionstart-minimal-context.test.mjs create mode 100644 hooks/signal-log.mjs create mode 100644 hooks/signal-log.test.mjs create mode 100644 hooks/userprompt-skill-inject.mjs create mode 100644 hooks/userprompt-skill-inject.test.mjs diff --git a/.gitignore b/.gitignore index 02b9779..7ee7282 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,8 @@ docs/ # Export script and output scripts/export-clean.sh -edgeone-makers-tools-clean/ \ No newline at end of file +edgeone-makers-tools-clean/ + +# EdgeOne Makers hook signal log and state +.edgeone/signal-log.jsonl +.edgeone/pretooluse-injected-skills.json diff --git a/hooks/hooks.json b/hooks/hooks.json index 3b009c8..36ded82 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,36 +1,36 @@ { "hooks": { - "UserPromptSubmit": [ + "SessionStart": [ { "matcher": "", "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/on-prompt.sh", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/on-session-start.sh", "timeout": 5 } ] } ], - "PreToolUse": [ + "UserPromptSubmit": [ { - "matcher": "Write|Edit", + "matcher": "", "hooks": [ { "type": "command", - "if": "Write(agents/*)|Edit(agents/*)", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/on-agent-write.sh", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/on-prompt.sh", "timeout": 5 } ] - }, + } + ], + "PreToolUse": [ { - "matcher": "Bash", + "matcher": "Read|Edit|Write|Bash|read_file|replace_in_file|write_to_file|execute_command", "hooks": [ { "type": "command", - "if": "Bash(edgeone *)", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/on-edgeone-cmd.sh", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse-skill-inject.mjs\"", "timeout": 5 } ] diff --git a/hooks/on-prompt.sh b/hooks/on-prompt.sh old mode 100644 new mode 100755 index 45f9509..6ba208b --- a/hooks/on-prompt.sh +++ b/hooks/on-prompt.sh @@ -1,18 +1,8 @@ #!/bin/bash -# UserPromptSubmit hook: detect agent-related prompts and inject skill routing context +# UserPromptSubmit hook: route prompts to Skill loading instructions. set -euo pipefail -INPUT=$(cat) -PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""') +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLUGIN_ROOT="${EDGEONE_MAKERS_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}}" -# Check if the prompt mentions agent/AI development keywords -if echo "$PROMPT" | grep -qiE 'agent|makers|deepagent|langgraph|crewai|openai.?agents|claude.?sdk|context\.store|context\.tools|context\.sandbox|edgeone.*deploy|SSE.*stream'; then - jq -n '{ - hookSpecificOutput: { - hookEventName: "UserPromptSubmit", - additionalContext: "This project uses EdgeOne Makers for AI Agent development. Read skills/makers-agents/SKILL.md for platform conventions, then read the matching framework reference based on the decision tree." - } - }' -else - exit 0 -fi +node "$PLUGIN_ROOT/hooks/userprompt-skill-inject.mjs" diff --git a/hooks/on-session-start.sh b/hooks/on-session-start.sh new file mode 100755 index 0000000..6abdb12 --- /dev/null +++ b/hooks/on-session-start.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# SessionStart hook: inject compact EdgeOne Makers routing principles. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLUGIN_ROOT="${EDGEONE_MAKERS_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}}" + +node "$PLUGIN_ROOT/hooks/sessionstart-minimal-context.mjs" + diff --git a/hooks/pretooluse-skill-inject.mjs b/hooks/pretooluse-skill-inject.mjs new file mode 100644 index 0000000..06181ee --- /dev/null +++ b/hooks/pretooluse-skill-inject.mjs @@ -0,0 +1,457 @@ +#!/usr/bin/env node +import { readFileSync, readdirSync } from 'node:fs'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { shouldWriteSignalLog, writeSignalLog } from './signal-log.mjs'; +import { detectPlatform, renderSkillInstruction } from './userprompt-skill-inject.mjs'; + +const HOOKS_DIR = dirname(fileURLToPath(import.meta.url)); +const DEFAULT_SKILLS_DIR = join(HOOKS_DIR, '..', 'skills'); +const BASH_TOOL_NAMES = new Set(['Bash', 'execute_command']); +const PATH_TOOL_NAMES = new Set(['Read', 'Edit', 'Write', 'read_file', 'replace_in_file', 'write_to_file']); +const WRITE_TOOL_NAMES = new Set(['Edit', 'Write', 'replace_in_file', 'write_to_file']); + +let cachedRules = null; + +function escapeRegExp(value) { + return value.replace(/[|\\{}()[\]^$+?.*]/g, '\\$&'); +} + +function globToRegExp(pattern) { + const source = String(pattern) + .replace(/\\/g, '/') + .split('/') + .map((segment) => { + if (segment === '**') return '.*'; + return escapeRegExp(segment).replace(/\\\*/g, '[^/]*'); + }) + .join('/'); + + return new RegExp(`(^|/)${source}$`); +} + +function parseFrontmatter(content) { + const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content); + return match ? match[1] : ''; +} + +function parseYamlScalar(value) { + const trimmed = String(value || '').trim(); + if (!trimmed) return ''; + + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + try { + return JSON.parse(trimmed); + } catch { + return trimmed.slice(1, -1); + } + } + + if (trimmed.startsWith("'") && trimmed.endsWith("'")) { + return trimmed.slice(1, -1).replace(/''/g, "'"); + } + + return trimmed; +} + +function parseFrontmatterString(frontmatter, key) { + const pattern = new RegExp(`^${key}:\\s*(.+)$`, 'm'); + const match = pattern.exec(frontmatter); + return match ? parseYamlScalar(match[1]) : ''; +} + +function parseFrontmatterList(frontmatter, key) { + const lines = frontmatter.split(/\r?\n/); + const values = []; + let inList = false; + + for (const line of lines) { + if (!inList) { + inList = new RegExp(`^${key}:\\s*$`).test(line); + continue; + } + + if (!line.trim()) continue; + if (/^\S/.test(line)) break; + + const item = /^\s+-\s*(.+?)\s*$/.exec(line); + if (item) values.push(parseYamlScalar(item[1])); + } + + return values; +} + +function assignObjectField(object, text) { + const field = /^([A-Za-z][\w-]*):\s*(.*?)\s*$/.exec(text); + if (!field) return; + object[field[1]] = parseYamlScalar(field[2]); +} + +function parseFrontmatterObjectList(frontmatter, key, requiredFields = ['pattern', 'message']) { + const lines = frontmatter.split(/\r?\n/); + const values = []; + let current = null; + let inList = false; + + for (const line of lines) { + if (!inList) { + inList = new RegExp(`^${key}:\\s*$`).test(line); + continue; + } + + if (!line.trim()) continue; + if (/^\S/.test(line)) break; + + const item = /^\s+-\s*(.*?)\s*$/.exec(line); + if (item) { + current = {}; + values.push(current); + if (item[1]) assignObjectField(current, item[1]); + continue; + } + + if (current) assignObjectField(current, line.trim()); + } + + return values.filter((value) => requiredFields.every((field) => value[field])); +} + +function parseSkillTriggerRule(skillPath) { + const frontmatter = parseFrontmatter(readFileSync(skillPath, 'utf8')); + const skill = parseFrontmatterString(frontmatter, 'name'); + if (!skill) return null; + + return { + skill, + pathPatterns: parseFrontmatterList(frontmatter, 'pathPatterns'), + bashPatterns: parseFrontmatterList(frontmatter, 'bashPatterns'), + validate: parseFrontmatterObjectList(frontmatter, 'validate'), + chainTo: parseFrontmatterObjectList(frontmatter, 'chainTo', ['pattern', 'skill', 'reason']), + }; +} + +export function loadSkillTriggerRules(skillsDir = DEFAULT_SKILLS_DIR) { + if (skillsDir === DEFAULT_SKILLS_DIR && cachedRules) return cachedRules; + + const rules = readdirSync(skillsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => join(skillsDir, entry.name, 'SKILL.md')) + .map((skillPath) => parseSkillTriggerRule(skillPath)) + .filter( + (rule) => + rule && + (rule.pathPatterns.length > 0 || + rule.bashPatterns.length > 0 || + rule.validate.length > 0 || + rule.chainTo.length > 0), + ); + + if (skillsDir === DEFAULT_SKILLS_DIR) cachedRules = rules; + return rules; +} + +function normalizePath(filePath) { + return String(filePath || '').replace(/\\/g, '/'); +} + +function getToolName(payload) { + return String(payload?.tool_name || payload?.toolName || '').trim(); +} + +function getToolInput(payload) { + return payload?.tool_input || payload?.toolInput || {}; +} + +function getToolPath(toolInput) { + return normalizePath( + toolInput.file_path || toolInput.filePath || toolInput.path || toolInput.target_file || '', + ); +} + +function getToolCommand(toolInput) { + return String(toolInput.command || '').trim(); +} + +function pickMostSpecificMatch(matches) { + if (matches.length === 0) return null; + return matches.sort((left, right) => right.specificity - left.specificity)[0]; +} + +function matchPathRule(filePath, rules) { + if (!filePath) return null; + + const matches = []; + for (const rule of rules) { + for (const pattern of rule.pathPatterns || []) { + if (globToRegExp(pattern).test(filePath)) { + matches.push({ + skill: rule.skill, + trigger: 'pathPatterns', + reason: `${filePath} matched ${pattern}`, + validate: rule.validate || [], + chainTo: rule.chainTo || [], + specificity: pattern.length, + }); + } + } + } + + return pickMostSpecificMatch(matches); +} + +function matchBashRule(command, rules) { + if (!command) return null; + + const matches = []; + for (const rule of rules) { + for (const pattern of rule.bashPatterns || []) { + const regex = new RegExp(pattern, 'i'); + if (regex.test(command)) { + matches.push({ + skill: rule.skill, + trigger: 'bashPatterns', + reason: `${command} matched ${pattern}`, + validate: [], + chainTo: [], + specificity: pattern.length, + }); + } + } + } + + return pickMostSpecificMatch(matches); +} + +export function selectSkillForToolUse(payload, rules = loadSkillTriggerRules()) { + const toolName = getToolName(payload); + const toolInput = getToolInput(payload); + + if (BASH_TOOL_NAMES.has(toolName)) { + return matchBashRule(getToolCommand(toolInput), rules); + } + + if (PATH_TOOL_NAMES.has(toolName)) { + return matchPathRule(getToolPath(toolInput), rules); + } + + return null; +} + +function getToolWriteContent(payload) { + const toolName = getToolName(payload); + if (!WRITE_TOOL_NAMES.has(toolName)) return ''; + + const toolInput = getToolInput(payload); + const candidateKeys = ['content', 'new_string', 'new_str', 'newString', 'text']; + for (const key of candidateKeys) { + if (typeof toolInput[key] === 'string') return toolInput[key]; + } + + return ''; +} + +function selectValidationMatches(payload, match) { + const content = getToolWriteContent(payload); + if (!content) return []; + + const matches = []; + for (const rule of match.validate || []) { + const regex = new RegExp(rule.pattern); + if (regex.test(content)) { + matches.push({ + pattern: rule.pattern, + message: rule.message, + }); + } + } + + return matches.filter( + (matchItem, index, list) => + list.findIndex((candidate) => candidate.message === matchItem.message) === index, + ); +} + +function selectChainToMatches(payload, match) { + const content = getToolWriteContent(payload); + if (!content) return []; + + const matches = []; + for (const rule of match.chainTo || []) { + const regex = new RegExp(rule.pattern); + if (regex.test(content)) { + matches.push({ + pattern: rule.pattern, + skill: rule.skill, + reason: rule.reason, + }); + } + } + + return matches.filter( + (matchItem, index, list) => + list.findIndex( + (candidate) => candidate.skill === matchItem.skill && candidate.reason === matchItem.reason, + ) === index, + ); +} + +function renderValidationReminder(messages) { + return `Validation reminder:\n${messages.map((message) => `- ${message}`).join('\n')}`; +} + +function maybeWritePreToolUseSignal(match, payload, platform, options) { + if (!shouldWriteSignalLog(options)) return; + + writeSignalLog( + { + hook: 'PreToolUse', + trigger: match.trigger, + matchedSkill: match.skill, + reason: match.reason, + platform, + toolName: getToolName(payload), + }, + options, + ); +} + +function maybeWriteValidationSignals(validationMatches, match, payload, platform, options) { + if (!shouldWriteSignalLog(options)) return; + + for (const validationMatch of validationMatches) { + writeSignalLog( + { + hook: 'PreToolUse', + trigger: 'validate', + matchedSkill: match.skill, + reason: validationMatch.message, + platform, + toolName: getToolName(payload), + }, + options, + ); + } +} + +function maybeWriteChainToSignals(chainToMatches, payload, platform, options) { + if (!shouldWriteSignalLog(options)) return; + + for (const chainToMatch of chainToMatches) { + writeSignalLog( + { + hook: 'PreToolUse', + trigger: 'chainTo', + matchedSkill: chainToMatch.skill, + reason: chainToMatch.reason, + platform, + toolName: getToolName(payload), + }, + options, + ); + } +} + +function defaultStatePath() { + return process.env.EDGEONE_MAKERS_PRETOOLUSE_STATE || join(process.cwd(), '.edgeone', 'pretooluse-injected-skills.json'); +} + +async function readInjectedSkills(statePath) { + try { + const raw = await readFile(statePath, 'utf8'); + const parsed = JSON.parse(raw); + return new Set(Array.isArray(parsed.injectedSkills) ? parsed.injectedSkills : []); + } catch { + return new Set(); + } +} + +async function writeInjectedSkills(statePath, injectedSkills) { + await mkdir(dirname(statePath), { recursive: true }); + await writeFile( + statePath, + `${JSON.stringify({ injectedSkills: [...injectedSkills].sort() }, null, 2)}\n`, + ); +} + +async function getInjectedSkills(options) { + if (options.injectedSkills) return options.injectedSkills; + return readInjectedSkills(options.statePath || defaultStatePath()); +} + +async function persistInjectedSkills(injectedSkills, options) { + if (options.injectedSkills) return; + await writeInjectedSkills(options.statePath || defaultStatePath(), injectedSkills); +} + +export async function buildPreToolUseOutput(payload, platform = 'claude-code', options = {}) { + const match = selectSkillForToolUse(payload); + if (!match) return null; + + const additionalContext = []; + const validationMatches = selectValidationMatches(payload, match); + const chainToMatches = selectChainToMatches(payload, match); + maybeWritePreToolUseSignal(match, payload, platform, options); + maybeWriteValidationSignals(validationMatches, match, payload, platform, options); + maybeWriteChainToSignals(chainToMatches, payload, platform, options); + + const injectedSkills = await getInjectedSkills(options); + let injectedSkillsChanged = false; + if (!injectedSkills.has(match.skill)) { + injectedSkills.add(match.skill); + injectedSkillsChanged = true; + additionalContext.push(renderSkillInstruction(match.skill, platform)); + } + + for (const chainToMatch of chainToMatches) { + if (injectedSkills.has(chainToMatch.skill)) continue; + + injectedSkills.add(chainToMatch.skill); + injectedSkillsChanged = true; + additionalContext.push(renderSkillInstruction(chainToMatch.skill, platform)); + } + + if (injectedSkillsChanged) { + await persistInjectedSkills(injectedSkills, options); + } + + const validationMessages = validationMatches.map((validationMatch) => validationMatch.message); + if (validationMessages.length > 0) { + additionalContext.push(renderValidationReminder(validationMessages)); + } + + if (additionalContext.length === 0) return null; + + return { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: additionalContext.join('\n\n'), + }, + }; +} + +async function readStdin() { + let input = ''; + for await (const chunk of process.stdin) { + input += chunk; + } + return input; +} + +export async function main() { + const rawInput = await readStdin(); + const payload = rawInput.trim() ? JSON.parse(rawInput) : {}; + const output = await buildPreToolUseOutput(payload, detectPlatform(), { enableSignalLog: true }); + + if (!output) return; + process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + }); +} + diff --git a/hooks/pretooluse-skill-inject.test.mjs b/hooks/pretooluse-skill-inject.test.mjs new file mode 100644 index 0000000..81aff8b --- /dev/null +++ b/hooks/pretooluse-skill-inject.test.mjs @@ -0,0 +1,536 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import test from 'node:test'; + +import { + buildPreToolUseOutput, + loadSkillTriggerRules, + selectSkillForToolUse, +} from './pretooluse-skill-inject.mjs'; + +test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.7 selects makers-edge-functions for Read functions/index.ts', () => { + assert.equal( + selectSkillForToolUse({ + tool_name: 'Read', + tool_input: { file_path: 'functions/index.ts' }, + })?.skill, + 'makers-edge-functions', + ); +}); + +test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 selects makers-edge-functions for CodeBuddy read_file filePath', () => { + assert.equal( + selectSkillForToolUse({ + tool_name: 'read_file', + tool_input: { filePath: 'functions/index.ts' }, + })?.skill, + 'makers-edge-functions', + ); +}); + +test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.8 selects makers-deploy for edgeone pages deploy', () => { + assert.equal( + selectSkillForToolUse({ + tool_name: 'Bash', + tool_input: { command: 'PAGES_SOURCE=skills edgeone pages deploy' }, + })?.skill, + 'makers-deploy', + ); +}); + +test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 selects makers-deploy for CodeBuddy execute_command', () => { + assert.equal( + selectSkillForToolUse({ + tool_name: 'execute_command', + tool_input: { command: 'PAGES_SOURCE=skills edgeone pages deploy' }, + })?.skill, + 'makers-deploy', + ); +}); + +test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.5 renders Cursor Skill loading instruction', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'Edit', + tool_input: { file_path: '/project/agents/chat.ts' }, + }, + 'cursor', + { injectedSkills: new Set() }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Load the /makers-agents skill.', + }, + }); +}); + +test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.6 deduplicates injected Skills with state file', async () => { + const tmp = await mkdtemp(join(tmpdir(), 'makers-pretooluse-')); + const statePath = join(tmp, 'injected-skills.json'); + + try { + const payload = { + tool_name: 'Read', + tool_input: { file_path: '/project/functions/index.ts' }, + }; + + assert.deepEqual(await buildPreToolUseOutput(payload, 'claude-code', { statePath }), { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'You must run the Skill(makers-edge-functions) tool.', + }, + }); + assert.equal(await buildPreToolUseOutput(payload, 'claude-code', { statePath }), null); + } finally { + await rm(tmp, { recursive: true, force: true }); + } +}); + +test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.3 returns null for unmatched tool use', async () => { + assert.equal( + await buildPreToolUseOutput( + { + tool_name: 'Read', + tool_input: { file_path: 'src/components/Button.tsx' }, + }, + 'claude-code', + { injectedSkills: new Set() }, + ), + null, + ); +}); + +test('plugin-skill-injection-optimization.MULTI_SIGNAL_MATCHING.1 loads pathPatterns from Skill frontmatter', async () => { + const rules = await loadSkillTriggerRules(); + const edgeFunctions = rules.find((rule) => rule.skill === 'makers-edge-functions'); + + assert.ok(edgeFunctions); + assert.deepEqual(edgeFunctions.pathPatterns, ['functions/**']); +}); + +test('plugin-skill-injection-optimization.MULTI_SIGNAL_MATCHING.2 loads bashPatterns from Skill frontmatter', async () => { + const rules = await loadSkillTriggerRules(); + const deploy = rules.find((rule) => rule.skill === 'makers-deploy'); + + assert.ok(deploy); + assert.ok(deploy.bashPatterns.includes('\\bedgeone\\s+pages\\s+deploy\\b')); +}); + +test('plugin-skill-injection-optimization.MULTI_SIGNAL_MATCHING.10 matches configured path patterns', () => { + const cases = [ + ['edgeone.json', 'makers-deploy'], + ['functions/index.ts', 'makers-edge-functions'], + ['cloud-functions/api/index.ts', 'makers-cloud-functions'], + ['agents/chat.ts', 'makers-agents'], + ['middleware.ts', 'makers-middleware'], + ]; + + for (const [filePath, expectedSkill] of cases) { + assert.equal( + selectSkillForToolUse({ + tool_name: 'Read', + tool_input: { file_path: filePath }, + })?.skill, + expectedSkill, + ); + } +}); + +test('plugin-skill-injection-optimization.MULTI_SIGNAL_MATCHING.10 matches configured bash patterns by specificity', () => { + assert.equal( + selectSkillForToolUse({ + tool_name: 'Bash', + tool_input: { command: 'PAGES_SOURCE=skills edgeone pages deploy' }, + })?.skill, + 'makers-deploy', + ); + + assert.equal( + selectSkillForToolUse({ + tool_name: 'Bash', + tool_input: { command: 'edgeone makers env ls' }, + })?.skill, + 'makers-cli', + ); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.1 loads validate rules from Skill frontmatter', async () => { + const rules = await loadSkillTriggerRules(); + const edgeFunctions = rules.find((rule) => rule.skill === 'makers-edge-functions'); + + assert.ok(edgeFunctions); + assert.deepEqual(edgeFunctions.validate, [ + { + pattern: 'process\\.env', + message: 'Use context.env in EdgeOne Makers runtime code.', + }, + { + pattern: 'new\\s+Headers\\s*\\(', + message: 'Use plain object headers for this runtime surface.', + }, + { + pattern: 'fs\\.writeFile', + message: 'Edge Functions do not support filesystem writes.', + }, + ]); +}); + +test('plugin-skill-injection-optimization.CHAIN_TO_LOADING.1 loads chainTo rules from Skill frontmatter', async () => { + const rules = await loadSkillTriggerRules(); + const edgeFunctions = rules.find((rule) => rule.skill === 'makers-edge-functions'); + + assert.ok(edgeFunctions); + assert.deepEqual(edgeFunctions.chainTo, [ + { + pattern: '\\bKV\\b|context\\.store', + skill: 'makers-storage', + reason: 'Code references KV or store APIs.', + }, + ]); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.2 warns on Edit content without blocking writes', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'Edit', + tool_input: { + file_path: 'functions/index.ts', + new_string: 'export default () => process.env.API_KEY;', + }, + }, + 'claude-code', + { injectedSkills: new Set() }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: + 'You must run the Skill(makers-edge-functions) tool.\n\nValidation reminder:\n- Use context.env in EdgeOne Makers runtime code.', + }, + }); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.4 warns on Write content using new Headers', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'Write', + tool_input: { + file_path: 'functions/index.ts', + content: 'return new Response(body, { headers: new Headers() });', + }, + }, + 'cursor', + { injectedSkills: new Set(['makers-edge-functions']) }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Validation reminder:\n- Use plain object headers for this runtime surface.', + }, + }); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.5 warns on Edge Function filesystem writes', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'Write', + tool_input: { + file_path: 'functions/index.ts', + content: 'fs.writeFile("/tmp/out.txt", "data", () => {});', + }, + }, + 'claude-code', + { injectedSkills: new Set(['makers-edge-functions']) }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Validation reminder:\n- Edge Functions do not support filesystem writes.', + }, + }); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 does not warn for read-only tool use', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'Read', + tool_input: { + file_path: 'functions/index.ts', + content: 'process.env.API_KEY', + }, + }, + 'claude-code', + { injectedSkills: new Set(['makers-edge-functions']) }, + ); + + assert.equal(output, null); +}); + +test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs PreToolUse path pattern matches', async () => { + const tmp = await mkdtemp(join(tmpdir(), 'makers-pretooluse-log-')); + const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); + + try { + await buildPreToolUseOutput( + { + tool_name: 'Read', + tool_input: { file_path: 'functions/index.ts' }, + }, + 'claude-code', + { + injectedSkills: new Set(), + signalLogPath, + now: new Date('2026-06-24T00:00:00.000Z'), + }, + ); + + const [line] = (await readFile(signalLogPath, 'utf8')).trim().split('\n'); + assert.deepEqual(JSON.parse(line), { + timestamp: '2026-06-24T00:00:00.000Z', + hook: 'PreToolUse', + trigger: 'pathPatterns', + matchedSkill: 'makers-edge-functions', + reason: 'functions/index.ts matched functions/**', + platform: 'claude-code', + toolName: 'Read', + }); + } finally { + await rm(tmp, { recursive: true, force: true }); + } +}); + +test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs PreToolUse bash pattern matches', async () => { + const tmp = await mkdtemp(join(tmpdir(), 'makers-pretooluse-log-')); + const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); + + try { + await buildPreToolUseOutput( + { + tool_name: 'Bash', + tool_input: { command: 'PAGES_SOURCE=skills edgeone pages deploy' }, + }, + 'cursor', + { + injectedSkills: new Set(), + signalLogPath, + now: new Date('2026-06-24T00:00:00.000Z'), + }, + ); + + const [line] = (await readFile(signalLogPath, 'utf8')).trim().split('\n'); + assert.deepEqual(JSON.parse(line), { + timestamp: '2026-06-24T00:00:00.000Z', + hook: 'PreToolUse', + trigger: 'bashPatterns', + matchedSkill: 'makers-deploy', + reason: 'PAGES_SOURCE=skills edgeone pages deploy matched \\bedgeone\\s+pages\\s+deploy\\b', + platform: 'cursor', + toolName: 'Bash', + }); + } finally { + await rm(tmp, { recursive: true, force: true }); + } +}); + +test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs validate matches with readable reasons', async () => { + const tmp = await mkdtemp(join(tmpdir(), 'makers-pretooluse-log-')); + const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); + + try { + await buildPreToolUseOutput( + { + tool_name: 'Write', + tool_input: { + file_path: 'functions/index.ts', + content: 'export default () => process.env.API_KEY;', + }, + }, + 'claude-code', + { + injectedSkills: new Set(['makers-edge-functions']), + signalLogPath, + now: new Date('2026-06-24T00:00:00.000Z'), + }, + ); + + const lines = (await readFile(signalLogPath, 'utf8')) + .trim() + .split('\n') + .map((line) => JSON.parse(line)); + + assert.deepEqual( + lines.map((line) => line.trigger), + ['pathPatterns', 'validate'], + ); + assert.deepEqual(lines[1], { + timestamp: '2026-06-24T00:00:00.000Z', + hook: 'PreToolUse', + trigger: 'validate', + matchedSkill: 'makers-edge-functions', + reason: 'Use context.env in EdgeOne Makers runtime code.', + platform: 'claude-code', + toolName: 'Write', + }); + } finally { + await rm(tmp, { recursive: true, force: true }); + } +}); + +test('plugin-skill-injection-optimization.CHAIN_TO_LOADING.3 injects chained storage Skill for KV code', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'Write', + tool_input: { + file_path: 'functions/index.ts', + content: 'const value = await KV.get("counter");', + }, + }, + 'claude-code', + { injectedSkills: new Set() }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: + 'You must run the Skill(makers-edge-functions) tool.\n\nYou must run the Skill(makers-storage) tool.', + }, + }); +}); + +test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 supports CodeBuddy write_to_file chainTo with filePath', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'write_to_file', + tool_input: { + filePath: 'functions/index.ts', + content: 'const value = await KV.get("counter");', + }, + }, + 'codebuddy', + { injectedSkills: new Set() }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: + 'Load the /makers-edge-functions skill.\n\nLoad the /makers-storage skill.', + }, + }); +}); + +test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 supports CodeBuddy replace_in_file validate with filePath', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'replace_in_file', + tool_input: { + filePath: 'functions/index.ts', + new_str: 'export default () => process.env.API_KEY;', + }, + }, + 'codebuddy', + { injectedSkills: new Set(['makers-edge-functions']) }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Validation reminder:\n- Use context.env in EdgeOne Makers runtime code.', + }, + }); +}); + +test('plugin-skill-injection-optimization.CHAIN_TO_LOADING.4 does not repeat chained Skills already injected', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'Write', + tool_input: { + file_path: 'functions/index.ts', + content: 'const value = await KV.get("counter");', + }, + }, + 'claude-code', + { injectedSkills: new Set(['makers-storage']) }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'You must run the Skill(makers-edge-functions) tool.', + }, + }); +}); + +test('plugin-skill-injection-optimization.CHAIN_TO_LOADING.5 injects storage Skill for context.store code', async () => { + const output = await buildPreToolUseOutput( + { + tool_name: 'Edit', + tool_input: { + file_path: 'agents/chat.ts', + new_string: 'const session = context.store.openaiSession(context.conversation_id);', + }, + }, + 'cursor', + { injectedSkills: new Set(['makers-agents']) }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Load the /makers-storage skill.', + }, + }); +}); + +test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs chainTo matches with readable reasons', async () => { + const tmp = await mkdtemp(join(tmpdir(), 'makers-chain-log-')); + const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); + + try { + await buildPreToolUseOutput( + { + tool_name: 'Write', + tool_input: { + file_path: 'functions/index.ts', + content: 'const value = await KV.get("counter");', + }, + }, + 'claude-code', + { + injectedSkills: new Set(['makers-edge-functions', 'makers-storage']), + signalLogPath, + now: new Date('2026-06-24T00:00:00.000Z'), + }, + ); + + const lines = (await readFile(signalLogPath, 'utf8')) + .trim() + .split('\n') + .map((line) => JSON.parse(line)); + + assert.deepEqual( + lines.map((line) => line.trigger), + ['pathPatterns', 'chainTo'], + ); + assert.deepEqual(lines[1], { + timestamp: '2026-06-24T00:00:00.000Z', + hook: 'PreToolUse', + trigger: 'chainTo', + matchedSkill: 'makers-storage', + reason: 'Code references KV or store APIs.', + platform: 'claude-code', + toolName: 'Write', + }); + } finally { + await rm(tmp, { recursive: true, force: true }); + } +}); + diff --git a/hooks/sessionstart-minimal-context.mjs b/hooks/sessionstart-minimal-context.mjs new file mode 100644 index 0000000..45deba2 --- /dev/null +++ b/hooks/sessionstart-minimal-context.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import { pathToFileURL } from 'node:url'; + +export const MINIMAL_SESSION_START_CONTEXT = [ + 'EdgeOne Makers Tools is installed.', + 'Use CLAUDE.md / AGENTS.md as the Skill route table.', + 'Load exactly the matching makers-* Skill before EdgeOne Makers work.', + 'Do not load all Skills at startup.', + 'Prefer Makers-specific Skills over generic EdgeOne Pages guidance.', +].join('\n'); + +export function buildSessionStartOutput() { + return { + hookSpecificOutput: { + hookEventName: 'SessionStart', + additionalContext: MINIMAL_SESSION_START_CONTEXT, + }, + }; +} + +export async function main() { + process.stdout.write(`${JSON.stringify(buildSessionStartOutput(), null, 2)}\n`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + }); +} + diff --git a/hooks/sessionstart-minimal-context.test.mjs b/hooks/sessionstart-minimal-context.test.mjs new file mode 100644 index 0000000..42620a8 --- /dev/null +++ b/hooks/sessionstart-minimal-context.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + MINIMAL_SESSION_START_CONTEXT, + buildSessionStartOutput, +} from './sessionstart-minimal-context.mjs'; + +test('plugin-skill-injection-optimization.MINIMAL_SESSION_START.1 injects compact startup principles', () => { + const lines = MINIMAL_SESSION_START_CONTEXT.trim().split('\n'); + + assert.ok(lines.length <= 8); + assert.match(MINIMAL_SESSION_START_CONTEXT, /CLAUDE\.md/); + assert.match(MINIMAL_SESSION_START_CONTEXT, /AGENTS\.md/); + assert.match(MINIMAL_SESSION_START_CONTEXT, /matching makers-\*/); + assert.match(MINIMAL_SESSION_START_CONTEXT, /Do not load all Skills/); +}); + +test('plugin-skill-injection-optimization.MINIMAL_SESSION_START.1 omits knowledge graph content', () => { + assert.doesNotMatch(MINIMAL_SESSION_START_CONTEXT, /DeepAgents/); + assert.doesNotMatch(MINIMAL_SESSION_START_CONTEXT, /context\.store/); + assert.doesNotMatch(MINIMAL_SESSION_START_CONTEXT, /PAGES_SOURCE/); + assert.doesNotMatch(MINIMAL_SESSION_START_CONTEXT, /profiler/i); +}); + +test('plugin-skill-injection-optimization.MINIMAL_SESSION_START.2 outputs SessionStart additionalContext', () => { + assert.deepEqual(buildSessionStartOutput(), { + hookSpecificOutput: { + hookEventName: 'SessionStart', + additionalContext: MINIMAL_SESSION_START_CONTEXT, + }, + }); +}); + diff --git a/hooks/signal-log.mjs b/hooks/signal-log.mjs new file mode 100644 index 0000000..d9e7210 --- /dev/null +++ b/hooks/signal-log.mjs @@ -0,0 +1,43 @@ +import { appendFileSync, mkdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +const REQUIRED_SIGNAL_FIELDS = ['hook', 'trigger', 'matchedSkill', 'reason']; + +export function defaultSignalLogPath(env = process.env, cwd = process.cwd()) { + return env.EDGEONE_MAKERS_SIGNAL_LOG || join(cwd, '.edgeone', 'signal-log.jsonl'); +} + +function normalizeSignalLogEntry(entry, now = new Date()) { + for (const field of REQUIRED_SIGNAL_FIELDS) { + if (!entry?.[field]) { + throw new Error(`Missing signal log field: ${field}`); + } + } + + const normalized = { + timestamp: now.toISOString(), + hook: entry.hook, + trigger: entry.trigger, + matchedSkill: entry.matchedSkill, + reason: entry.reason, + }; + + if (entry.platform) normalized.platform = entry.platform; + if (entry.toolName) normalized.toolName = entry.toolName; + + return normalized; +} + +export function shouldWriteSignalLog(options = {}) { + return Boolean(options.enableSignalLog || options.signalLogPath); +} + +export function writeSignalLog(entry, options = {}) { + const normalized = normalizeSignalLogEntry(entry, options.now); + const logPath = options.logPath || options.signalLogPath || defaultSignalLogPath(); + + mkdirSync(dirname(logPath), { recursive: true }); + appendFileSync(logPath, `${JSON.stringify(normalized)}\n`); + + return normalized; +} diff --git a/hooks/signal-log.test.mjs b/hooks/signal-log.test.mjs new file mode 100644 index 0000000..a5431c6 --- /dev/null +++ b/hooks/signal-log.test.mjs @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import { writeSignalLog } from './signal-log.mjs'; + +test('plugin-skill-injection-optimization.SIGNAL_LOGGING.1 appends signal entries to JSONL', async () => { + const tmp = await mkdtemp(join(tmpdir(), 'makers-signal-log-')); + const logPath = join(tmp, '.edgeone', 'signal-log.jsonl'); + + try { + writeSignalLog( + { + hook: 'PreToolUse', + trigger: 'pathPatterns', + matchedSkill: 'makers-edge-functions', + reason: 'functions/index.ts matched functions/**', + platform: 'claude-code', + toolName: 'Read', + }, + { logPath, now: new Date('2026-06-24T00:00:00.000Z') }, + ); + + const [line] = (await readFile(logPath, 'utf8')).trim().split('\n'); + + assert.deepEqual(JSON.parse(line), { + timestamp: '2026-06-24T00:00:00.000Z', + hook: 'PreToolUse', + trigger: 'pathPatterns', + matchedSkill: 'makers-edge-functions', + reason: 'functions/index.ts matched functions/**', + platform: 'claude-code', + toolName: 'Read', + }); + } finally { + await rm(tmp, { recursive: true, force: true }); + } +}); + +test('plugin-skill-injection-optimization.SIGNAL_LOGGING.2 requires the core signal fields', () => { + assert.throws( + () => + writeSignalLog({ + hook: 'PreToolUse', + trigger: 'pathPatterns', + matchedSkill: 'makers-edge-functions', + }), + /Missing signal log field: reason/, + ); +}); diff --git a/hooks/userprompt-skill-inject.mjs b/hooks/userprompt-skill-inject.mjs new file mode 100644 index 0000000..c7665a4 --- /dev/null +++ b/hooks/userprompt-skill-inject.mjs @@ -0,0 +1,172 @@ +#!/usr/bin/env node +import { pathToFileURL } from 'node:url'; + +import { shouldWriteSignalLog, writeSignalLog } from './signal-log.mjs'; + +const SKILL_RULES = [ + { + skill: 'makers-deploy', + patterns: [ + [/\bedgeone\s+pages\s+deploy\b/i, 8], + [/\bdeploy(?:ment)?\b/i, 5], + [/\bpublish\b|\brelease\b/i, 4], + [/上线|发布|部署/i, 5], + ], + }, + { + skill: 'makers-edge-functions', + patterns: [ + [/\bedge\s+functions?\b/i, 8], + [/\bfunctions\/[\w./-]*/i, 6], + [/\bV8\b/i, 3], + [/\bonRequest\b/i, 2], + ], + }, + { + skill: 'makers-cloud-functions', + patterns: [ + [/\bcloud[-\s]?functions?\b/i, 8], + [/\bcloud-functions\/[\w./-]*/i, 6], + [/\bexpress\b|\bkoa\b|\bgin\b/i, 4], + [/\bnode\.?js\b|\bgo\b|\bpython\b/i, 2], + ], + }, + { + skill: 'makers-agents', + patterns: [ + [/\bagents?\b/i, 5], + [/\bdeepagents?\b|\blanggraph\b|\bcrewai\b/i, 7], + [/\bopenai[-\s]?agents?\b|\bclaude[-\s]?sdk\b/i, 7], + [/\bcontext\.(store|tools|sandbox)\b/i, 5], + [/\bconversation[_-]?id\b|\bsse\b/i, 3], + [/智能体|代理开发/i, 5], + ], + }, + { + skill: 'makers-storage', + patterns: [ + [/\bkv\b|\bblob\b/i, 6], + [/\bstorage\b/i, 4], + [/\bcontext\.store\b/i, 4], + [/存储/i, 5], + ], + }, + { + skill: 'makers-middleware', + patterns: [ + [/\bmiddleware\b/i, 7], + [/\brewrite\b|\bredirect\b/i, 4], + [/\bauth\b.*\b(route|path|middleware)\b/i, 4], + [/中间件|重写|重定向/i, 5], + ], + }, + { + skill: 'makers-cli', + patterns: [ + [/\bedgeone\s+/i, 3], + [/\bcli\b|\bcommand\b/i, 3], + [/\bPAGES_SOURCE\b/i, 5], + ], + }, + { + skill: 'makers-recipes', + patterns: [ + [/\btemplate\b|\bscaffold\b|\brecipe\b/i, 5], + [/\bproject\s+structure\b/i, 4], + [/模板|脚手架|项目结构/i, 5], + ], + }, +]; + +function scorePrompt(prompt, rule) { + return rule.patterns.reduce((score, [pattern, weight]) => { + return pattern.test(prompt) ? score + weight : score; + }, 0); +} + +export function selectSkillForPrompt(prompt) { + const text = String(prompt || '').trim(); + if (!text) return null; + + let best = null; + for (const rule of SKILL_RULES) { + const score = scorePrompt(text, rule); + if (score > 0 && (!best || score > best.score)) { + best = { skill: rule.skill, score }; + } + } + return best; +} + +export function detectPlatform(env = process.env) { + const explicit = String(env.EDGEONE_MAKERS_PLATFORM || '').toLowerCase(); + if (explicit === 'claude' || explicit === 'claude-code') return 'claude-code'; + if (explicit === 'cursor') return 'cursor'; + if (explicit === 'codebuddy') return 'codebuddy'; + + if (env.CURSOR_PLUGIN_ROOT) return 'cursor'; + if (env.CODEBUDDY_PLUGIN_ROOT) return 'codebuddy'; + if (env.CLAUDE_PLUGIN_ROOT) return 'claude-code'; + return 'claude-code'; +} + +export function renderSkillInstruction(skill, platform = 'claude-code') { + if (platform === 'claude-code') { + return `You must run the Skill(${skill}) tool.`; + } + return `Load the /${skill} skill.`; +} + +function maybeWritePromptSignal(match, platform, options) { + if (!shouldWriteSignalLog(options)) return; + + writeSignalLog( + { + hook: 'UserPromptSubmit', + trigger: 'promptScore', + matchedSkill: match.skill, + reason: `prompt score ${match.score} selected ${match.skill}`, + platform, + }, + options, + ); +} + +export function buildHookOutput(prompt, platform = 'claude-code', options = {}) { + const match = selectSkillForPrompt(prompt); + if (!match) return null; + + maybeWritePromptSignal(match, platform, options); + + return { + hookSpecificOutput: { + hookEventName: 'UserPromptSubmit', + additionalContext: renderSkillInstruction(match.skill, platform), + }, + }; +} + +async function readStdin() { + let input = ''; + for await (const chunk of process.stdin) { + input += chunk; + } + return input; +} + +export async function main() { + const rawInput = await readStdin(); + const payload = rawInput.trim() ? JSON.parse(rawInput) : {}; + const output = buildHookOutput(payload.prompt, detectPlatform(), { enableSignalLog: true }); + + if (!output) return; + process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + }); +} + diff --git a/hooks/userprompt-skill-inject.test.mjs b/hooks/userprompt-skill-inject.test.mjs new file mode 100644 index 0000000..bc1ea6d --- /dev/null +++ b/hooks/userprompt-skill-inject.test.mjs @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import { + buildHookOutput, + detectPlatform, + renderSkillInstruction, + selectSkillForPrompt, +} from './userprompt-skill-inject.mjs'; + +test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.2 selects makers-agents for agent prompts', () => { + assert.equal( + selectSkillForPrompt('Create a LangGraph agent with context.store support')?.skill, + 'makers-agents', + ); +}); + +test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.2 selects makers-deploy for deploy prompts', () => { + assert.equal( + selectSkillForPrompt('Deploy this project with edgeone pages deploy')?.skill, + 'makers-deploy', + ); +}); + +test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.2 selects makers-edge-functions for edge function prompts', () => { + assert.equal( + selectSkillForPrompt('Write an Edge Function under functions/index.ts')?.skill, + 'makers-edge-functions', + ); +}); + +test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.3 renders Claude Code Skill tool instruction', () => { + assert.equal( + renderSkillInstruction('makers-edge-functions', 'claude-code'), + 'You must run the Skill(makers-edge-functions) tool.', + ); +}); + +test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.4 renders Cursor slash skill instruction', () => { + assert.equal( + renderSkillInstruction('makers-edge-functions', 'cursor'), + 'Load the /makers-edge-functions skill.', + ); +}); + +test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.6 outputs only the loading instruction', () => { + assert.deepEqual( + buildHookOutput('Deploy to EdgeOne', 'claude-code'), + { + hookSpecificOutput: { + hookEventName: 'UserPromptSubmit', + additionalContext: 'You must run the Skill(makers-deploy) tool.', + }, + }, + ); +}); + +test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.6 returns null when no skill matches', () => { + assert.equal(buildHookOutput('Explain how git bisect works', 'claude-code'), null); +}); + +test('detectPlatform prefers explicit platform override', () => { + assert.equal( + detectPlatform({ EDGEONE_MAKERS_PLATFORM: 'cursor', CLAUDE_PLUGIN_ROOT: '/tmp/plugin' }), + 'cursor', + ); +}); + +test('plugin-skill-injection-optimization.SIGNAL_LOGGING.4 logs prompt score matches', async () => { + const tmp = await mkdtemp(join(tmpdir(), 'makers-userprompt-log-')); + const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); + + try { + const output = buildHookOutput('Deploy this project with edgeone pages deploy', 'claude-code', { + signalLogPath, + now: new Date('2026-06-24T00:00:00.000Z'), + }); + + assert.equal( + output.hookSpecificOutput.additionalContext, + 'You must run the Skill(makers-deploy) tool.', + ); + + const [line] = (await readFile(signalLogPath, 'utf8')).trim().split('\n'); + assert.deepEqual(JSON.parse(line), { + timestamp: '2026-06-24T00:00:00.000Z', + hook: 'UserPromptSubmit', + trigger: 'promptScore', + matchedSkill: 'makers-deploy', + reason: 'prompt score 13 selected makers-deploy', + platform: 'claude-code', + }); + } finally { + await rm(tmp, { recursive: true, force: true }); + } +}); + diff --git a/skills/makers-agents/SKILL.md b/skills/makers-agents/SKILL.md index 8602153..261a7c2 100644 --- a/skills/makers-agents/SKILL.md +++ b/skills/makers-agents/SKILL.md @@ -15,6 +15,12 @@ description: >- Do NOT trigger for deployment workflows (use edgeone-pages-deploy). Do NOT trigger for generic AI framework development outside an EdgeOne Makers project. +pathPatterns: + - agents/** +chainTo: + - pattern: "\\bKV\\b|context\\.store" + skill: makers-storage + reason: "Code references KV or store APIs." metadata: author: edgeone version: "1.0.0" diff --git a/skills/makers-cli/SKILL.md b/skills/makers-cli/SKILL.md index 7e50492..45f3ea5 100644 --- a/skills/makers-cli/SKILL.md +++ b/skills/makers-cli/SKILL.md @@ -3,6 +3,8 @@ name: makers-cli description: >- EdgeOne Makers CLI command reference. Use when running edgeone CLI commands for dev, build, deploy, env management. +bashPatterns: + - "\\bedgeone\\s+" metadata: author: edgeone version: "1.0.0" diff --git a/skills/makers-cloud-functions/SKILL.md b/skills/makers-cloud-functions/SKILL.md index 26fac3d..f9f8533 100644 --- a/skills/makers-cloud-functions/SKILL.md +++ b/skills/makers-cloud-functions/SKILL.md @@ -3,6 +3,8 @@ name: makers-cloud-functions description: >- EdgeOne Makers Cloud Functions — Node.js, Go, and Python runtimes. Use when building server-side APIs, Express/Koa patterns, or backend logic. +pathPatterns: + - cloud-functions/** metadata: author: edgeone version: "1.0.0" diff --git a/skills/makers-deploy/SKILL.md b/skills/makers-deploy/SKILL.md index 890eda9..d4ebae5 100644 --- a/skills/makers-deploy/SKILL.md +++ b/skills/makers-deploy/SKILL.md @@ -12,6 +12,11 @@ description: >- commands — the skill contains critical rules for parsing deploy output and presenting access URLs. Do NOT trigger for post-deployment runtime errors (e.g. CORS issues, 500 errors after deploy — use edgeone-makers-dev for troubleshooting). +pathPatterns: + - edgeone.json +bashPatterns: + - "\\bedgeone\\s+pages\\s+deploy\\b" + - "\\bedgeone\\s+makers\\s+deploy\\b" metadata: author: edgeone version: "2.1.0" diff --git a/skills/makers-edge-functions/SKILL.md b/skills/makers-edge-functions/SKILL.md index 96020c4..2510c5f 100644 --- a/skills/makers-edge-functions/SKILL.md +++ b/skills/makers-edge-functions/SKILL.md @@ -3,16 +3,19 @@ name: edgeone-makers-edge-functions description: >- V8-based lightweight edge functions on EdgeOne Makers. Covers routing, KV storage access, request/response handling, and environment variables at the edge. -metadata: - author: edgeone - version: "1.0.0" ---- - ---- -name: edgeone-makers-edge-functions -description: >- - V8-based lightweight edge functions on EdgeOne Makers. Covers routing, KV storage access, - request/response handling, and environment variables at the edge. +pathPatterns: + - functions/** +validate: + - pattern: "process\\.env" + message: "Use context.env in EdgeOne Makers runtime code." + - pattern: "new\\s+Headers\\s*\\(" + message: "Use plain object headers for this runtime surface." + - pattern: "fs\\.writeFile" + message: "Edge Functions do not support filesystem writes." +chainTo: + - pattern: "\\bKV\\b|context\\.store" + skill: makers-storage + reason: "Code references KV or store APIs." metadata: author: edgeone version: "1.0.0" diff --git a/skills/makers-middleware/SKILL.md b/skills/makers-middleware/SKILL.md index c0a25a0..218f91d 100644 --- a/skills/makers-middleware/SKILL.md +++ b/skills/makers-middleware/SKILL.md @@ -3,16 +3,9 @@ name: edgeone-makers-middleware description: >- Edge middleware for EdgeOne Makers — request interception, redirects, rewrites, auth guards, A/B testing, and header injection at the edge (V8 runtime). -metadata: - author: edgeone - version: "1.0.0" ---- - ---- -name: edgeone-makers-middleware -description: >- - Edge middleware for EdgeOne Makers — request interception, redirects, rewrites, - auth guards, A/B testing, and header injection at the edge (V8 runtime). +pathPatterns: + - middleware.* + - middleware/** metadata: author: edgeone version: "1.0.0" From b4a31fa7f024c4459695666ad68c0b50d10501d5 Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Fri, 3 Jul 2026 16:10:27 +0800 Subject: [PATCH 3/9] refactor(hooks): slim runtime to validate-only per md plan - drop SessionStart/UserPromptSubmit/PreToolUse injection+dedup+chainTo - keep validate as standalone stateless validate-write.mjs - hooks.json: PreToolUse on write tools only - strip pathPatterns/bashPatterns/chainTo from SKILL.md (keep pathPatterns on edge-functions as validate scope) - move chainTo cross-refs into SKILL.md prose - mark #6/#7/#8/#9/#12 deprecated in feature.yaml - runtime ~700 -> ~150 lines, 3 hook events -> 1, drop .edgeone state Co-authored-by: Cursor --- .gitignore | 1 - docs/plugin-hooks-vs-context-and-tools.md | 292 ++++++++++ ...-skill-injection-optimization.feature.yaml | 129 +++++ hooks/hooks.json | 30 +- hooks/on-prompt.sh | 8 - hooks/on-session-start.sh | 9 - hooks/pretooluse-skill-inject.mjs | 457 --------------- hooks/pretooluse-skill-inject.test.mjs | 536 ------------------ hooks/sessionstart-minimal-context.mjs | 31 - hooks/sessionstart-minimal-context.test.mjs | 34 -- hooks/userprompt-skill-inject.mjs | 172 ------ hooks/userprompt-skill-inject.test.mjs | 100 ---- hooks/validate-write.mjs | 230 ++++++++ hooks/validate-write.test.mjs | 185 ++++++ skills/makers-agents/SKILL.md | 8 +- skills/makers-cli/SKILL.md | 2 - skills/makers-cloud-functions/SKILL.md | 2 - skills/makers-deploy/SKILL.md | 5 - skills/makers-edge-functions/SKILL.md | 6 +- skills/makers-middleware/SKILL.md | 3 - 20 files changed, 843 insertions(+), 1397 deletions(-) create mode 100644 docs/plugin-hooks-vs-context-and-tools.md create mode 100644 features/edgeone-makers-tools/plugin-skill-injection-optimization.feature.yaml delete mode 100755 hooks/on-prompt.sh delete mode 100755 hooks/on-session-start.sh delete mode 100644 hooks/pretooluse-skill-inject.mjs delete mode 100644 hooks/pretooluse-skill-inject.test.mjs delete mode 100644 hooks/sessionstart-minimal-context.mjs delete mode 100644 hooks/sessionstart-minimal-context.test.mjs delete mode 100644 hooks/userprompt-skill-inject.mjs delete mode 100644 hooks/userprompt-skill-inject.test.mjs create mode 100644 hooks/validate-write.mjs create mode 100644 hooks/validate-write.test.mjs diff --git a/.gitignore b/.gitignore index 7ee7282..5664071 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,3 @@ edgeone-makers-tools-clean/ # EdgeOne Makers hook signal log and state .edgeone/signal-log.jsonl -.edgeone/pretooluse-injected-skills.json diff --git a/docs/plugin-hooks-vs-context-and-tools.md b/docs/plugin-hooks-vs-context-and-tools.md new file mode 100644 index 0000000..7b46799 --- /dev/null +++ b/docs/plugin-hooks-vs-context-and-tools.md @@ -0,0 +1,292 @@ +# Plugin Hooks vs Context-and-Tools 对比分析 + +> 日期:2026-06-30 +> 对比项目:edgeone-makers-tools (当前) vs context-and-tools (Netlify 参考实现) + +--- + +## 一、两个项目的架构概览 + +### Netlify context-and-tools + +- **定位**:多平台 AI 技能分发系统(Claude Code / Cursor / Codex / Grok / Gemini) +- **核心理念**:纯静态内容 + AI 自主路由,零运行时代码 +- **Plugin manifest**:声明式 `plugin.json`,无 hooks 字段 +- **技能触发**:依赖 CLAUDE.md 路由表 → AI 自主选择加载哪个 SKILL.md +- **运行时代码**:无(只有构建脚本生成各平台格式) + +``` +skills/CLAUDE.md (路由表) → AI 阅读后自主选择 → 读取对应 SKILL.md +``` + +**关键文件:** +``` +.claude-plugin/ +├── plugin.json # 纯元信息:name, version, description, author, repository +└── marketplace.json # 市场注册配置 +skills/ +├── CLAUDE.md # 路由决策表(给 AI 读的) +└── netlify-*/SKILL.md # 13 个技能(YAML frontmatter 只有 name + description) +``` + +### EdgeOne edgeone-makers-tools (当前) + +- **定位**:类似定位,多平台技能分发 +- **核心理念**:Hooks 主动注入 + AI 被动执行,运行时代码辅助路由 +- **Plugin manifest**:`manifest.json` 中绑定 hooks +- **技能触发**:3 层 hooks 逐步注入(SessionStart → UserPromptSubmit → PreToolUse) +- **运行时代码**:~700 行 JS + 状态文件 + 信号日志 + +``` +hooks(拦截事件) → 正则匹配 → 强制注入 "You must run Skill(xxx)" → AI 被动执行 +``` + +**关键文件:** +``` +.claude-plugin/ +└── manifest.json # 含 "hooks": "../hooks/hooks.json" +hooks/ +├── hooks.json # 3 个事件 hook 定义 +├── sessionstart-minimal-context.mjs # SessionStart: 注入启动提示 +├── userprompt-skill-inject.mjs # UserPromptSubmit: 对 prompt 评分选 skill +├── pretooluse-skill-inject.mjs # PreToolUse: 路径/命令匹配 + validate + chainTo +└── signal-log.mjs # 诊断日志 +skills/ +├── makers-*/SKILL.md # 8 个技能(frontmatter 含 pathPatterns/bashPatterns/validate/chainTo) +``` + +--- + +## 二、核心差异对比表 + +| 维度 | context-and-tools (Netlify) | edgeone-makers-tools (当前) | +|------|------|------| +| Plugin manifest | 纯声明式,无 hooks 字段 | manifest 绑定 hooks.json | +| 技能触发方式 | AI 读 CLAUDE.md 自主选择 | Hooks 代码强制注入指令 | +| 运行时代码 | 0 行 | ~700 行 JS | +| 路由决策者 | AI 自身 | Hooks 代码 | +| 状态管理 | 无 | .edgeone/pretooluse-injected-skills.json | +| 验证/链式触发 | 无 | frontmatter validate/chainTo 字段 | +| SKILL.md frontmatter | 仅 name + description | name + description + pathPatterns + bashPatterns + validate + chainTo + metadata | +| 多平台分发 | 纯构建脚本 | 构建脚本 + 运行时 hooks | +| 维护成本 | 极低 | 较高(hooks 逻辑 + 测试 + 状态文件) | + +--- + +## 三、PreToolUse 阶段详细拆解 + +`pretooluse-skill-inject.mjs` 在 AI 每次调用 Read/Edit/Write/Bash 工具之前被触发,执行 4 段逻辑: + +### 逻辑 1:Skill 匹配(路由注入) + +**流程:** +``` +AI 要 Read("functions/api/hello.ts") +→ hooks 解析路径,匹配 pathPatterns: ["functions/**"] +→ 去重检查(是否已注入过) +→ 输出: "You must run the Skill(makers-edge-functions) tool." +``` + +**实现要素:** +- `matchPathRule()` — 路径 glob 匹配 +- `matchBashRule()` — 命令正则匹配 +- `pickMostSpecificMatch()` — 多匹配时选最长 pattern(最具体) +- 工具类型分发:Bash 类工具走 bash 匹配,Read/Edit/Write 走路径匹配 + +**价值判断:低 / 可删除** +- CLAUDE.md 路由表已经让 AI 有能力自主选择 +- AI 在写 `functions/` 下的代码时大概率已经加载过对应 skill +- Netlify 没有这层逻辑,完全靠 AI 自觉,验证了此路径可行 + +--- + +### 逻辑 2:去重状态管理 + +**流程:** +``` +已注入 Set(["makers-edge-functions"]) +→ 第二次触发同 skill 时跳过注入 +→ 持久化到 .edgeone/pretooluse-injected-skills.json +``` + +**实现要素:** +- `readInjectedSkills()` / `writeInjectedSkills()` — 文件 I/O +- `persistInjectedSkills()` — 仅在集合变化时写盘 +- JSON 格式:`{ "injectedSkills": ["makers-agents", "makers-edge-functions"] }` + +**价值判断:纯辅助逻辑** +- 如果去掉逻辑 1 的强制注入,去重也没有存在意义 + +--- + +### 逻辑 3:Validate(代码写入验证)⭐ + +**流程:** +``` +AI 要 Edit("functions/api/hello.ts", new_string: "process.env.API_KEY") +→ hooks 提取写入内容(new_string / content / text 等字段) +→ 对内容运行 validate 规则的正则匹配 +→ 输出追加: "Validation reminder:\n- Use context.env in EdgeOne Makers runtime code." +``` + +**当前定义的全部 validate 规则(仅 makers-edge-functions 有):** + +| Pattern | 提醒内容 | 场景 | +|---------|---------|------| +| `process\.env` | "Use context.env in EdgeOne Makers runtime code." | Edge Function 不能用 Node.js 环境变量 | +| `new\s+Headers\s*\(` | "Use plain object headers for this runtime surface." | Edge Function 的 Headers 构造方式不同 | +| `fs\.writeFile` | "Edge Functions do not support filesystem writes." | Edge Function 无文件系统写入能力 | + +**实现要素:** +- `getToolWriteContent()` — 从 payload 提取写入内容(兼容多种工具字段名) +- `selectValidationMatches()` — 对写入内容逐条运行 validate 正则 +- `renderValidationReminder()` — 格式化为 "Validation reminder:\n- ..." 输出 + +**价值判断:这是唯一有独特价值的逻辑** +- SKILL.md 里虽然写了 "不要用 process.env",但 AI 不一定每次都记得 +- 这是**写入时**的最后一道防线 — 静态文档是事前引导,这是事中拦截 +- 类似于 IDE 实时 lint 红线提醒 +- 触发时机精确:只在 Edit/Write 工具调用时触发,只检查写入内容 + +--- + +### 逻辑 4:ChainTo(跨 skill 关联触发) + +**流程:** +``` +AI 要 Edit("functions/api/store.ts", new_string: "context.store.get(...)") +→ hooks 检测写入内容匹配 chainTo pattern: "\\bKV\\b|context\\.store" +→ 如果 makers-storage 尚未注入: +→ 输出追加: "You must run the Skill(makers-storage) tool." +``` + +**当前定义的全部 chainTo 规则:** + +| 所属 Skill | 触发条件 | 链向 | 理由 | +|-----------|---------|------|------| +| makers-edge-functions | `\bKV\b\|context\.store` | makers-storage | Code references KV or store APIs. | +| makers-agents | `\bKV\b\|context\.store` | makers-storage | Code references KV or store APIs. | + +**价值判断:中等 / 可用静态方式替代** +- 在 SKILL.md 正文中写 "If using `context.store`, also read `makers-storage/SKILL.md`" 即可 +- AI 能理解交叉引用文本 + +--- + +## 四、"独立 lint hook" 的含义 + +当前 PreToolUse 是一个大杂烩 — 同一个 457 行的 JS 文件混合了: +- skill 路由注入(可删) +- 去重管理(可删) +- validate 验证(有价值) +- chainTo 关联(可用文档替代) + +"独立 lint hook" = **只保留 validate 逻辑,剥离成一个纯粹的代码质量检查 hook**,与 skill 注入完全解耦。 + +### 方案 A:极简 PreToolUse hook(推荐) + +保留一个 hook,但只做 validate: + +```json +// hooks/hooks.json +{ + "hooks": { + "PreToolUse": [{ + "matcher": "Edit|Write|replace_in_file|write_to_file", + "hooks": [{ + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/validate-write.mjs\"", + "timeout": 3 + }] + }] + } +} +``` + +`validate-write.mjs` 约 50 行,只做: +1. 从 stdin 读取 payload +2. 根据文件路径确定所属 skill +3. 对写入内容运行该 skill 的 validate 正则 +4. 如果匹配,输出 "Validation reminder: ..." +5. 无状态文件、无 skill 注入、无去重逻辑 + +**优势:** +- 保留事中拦截的独特价值 +- 代码量 457 行 → ~50 行 +- 无状态文件(`.edgeone/` 目录可完全删除) +- 只在写入工具触发,不拦截 Read / Bash + +### 方案 B:完全去掉 hooks,约束写入 SKILL.md 正文 + +在 `makers-edge-functions/SKILL.md` 正文中强调: + +```markdown +## Critical Constraints (NEVER violate) + +- `process.env` → use `context.env` instead +- `new Headers()` → use plain object `{ "Content-Type": "..." }` +- `fs.writeFile` or any filesystem write → Edge Functions have no writable FS +``` + +**优势:** 零运行时代码,完全对齐 Netlify 方案 +**风险:** AI 可能在长对话中遗忘约束,写出违规代码时没有拦截 + +--- + +## 五、迁移建议总结 + +| 当前逻辑 | 处理方式 | 理由 | +|---------|---------|------| +| SessionStart hook | 删除 | CLAUDE.md 已作为项目指令自动加载 | +| UserPromptSubmit hook | 删除 | AI 读 CLAUDE.md 路由表后能自主判断 | +| PreToolUse: skill 路由注入 | 删除 | CLAUDE.md 路由表已够用 | +| PreToolUse: 去重状态管理 | 删除 | 路由注入删了就没必要了 | +| **PreToolUse: validate** | **保留(精简为独立 hook)** | 事中拦截有独特价值 | +| PreToolUse: chainTo | 删除,移到 SKILL.md 正文 | 文本交叉引用足够 | +| signal-log.mjs | 可选保留 | validate hook 中可保留诊断日志 | + +**目标架构:** +``` +CLAUDE.md (路由表) → AI 主动选择 skill → [可选] validate hook 做写入检查 +``` + +**预期收益:** +- 运行时代码:~700 行 → ~50 行 +- 状态文件:删除 `.edgeone/` 目录 +- hooks 事件:3 个 → 1 个(仅 PreToolUse on write tools) +- SKILL.md frontmatter:可移除 pathPatterns / bashPatterns / chainTo(仅保留 name + description + validate) +- 与 Netlify 最佳实践对齐,降低维护成本 + +--- + +## 六、Netlify 关键实现参考 + +### plugin.json(无 hooks) +```json +{ + "name": "netlify-skills", + "version": "1.1.0", + "description": "Netlify platform skills for Claude Code", + "author": { "name": "Netlify" }, + "repository": "https://github.com/netlify/context-and-tools" +} +``` + +### SKILL.md frontmatter(仅 name + description) +```yaml +--- +name: netlify-functions +description: Guide for writing Netlify serverless functions. Use when creating API endpoints... +--- +``` + +### 路由方式(CLAUDE.md 纯文本引导) +```markdown +**Building API endpoints or server-side logic?** +Read `netlify-functions/SKILL.md` for modern function syntax... + +**Need low-latency middleware?** +Read `netlify-edge-functions/SKILL.md` for edge compute patterns. +``` + +无代码、无 hooks、无状态 — 完全信赖 AI 的理解和判断能力。 diff --git a/features/edgeone-makers-tools/plugin-skill-injection-optimization.feature.yaml b/features/edgeone-makers-tools/plugin-skill-injection-optimization.feature.yaml new file mode 100644 index 0000000..324d0d8 --- /dev/null +++ b/features/edgeone-makers-tools/plugin-skill-injection-optimization.feature.yaml @@ -0,0 +1,129 @@ +feature: + name: plugin-skill-injection-optimization + product: edgeone-makers-tools + description: 优化 EdgeOne Makers Tools 插件的 Skill 注入、Hook 路由、平台降级和信号观测,减少上下文占用并提高 Skill 触发准确性。【2026-07-03 决策:采纳 docs/plugin-hooks-vs-context-and-tools.md 方案A,废弃 #6 UserPromptSubmit/#7 SessionStart/#9 pathPatterns+bashPatterns/#12 chainTo 的运行时注入;#8 路由注入+去重废弃(validate 保留见#10);#11 路由降级废弃(写入工具名兼容保留);#10 validate 精简为独立 hook 保留;#13 signal-log 保留供诊断;AI 改由 CLAUDE.md 路由表自主选 skill】 + +components: + INSTRUCTION_INJECTION: + description: "#6 Plugin 改为指令注入模式。【DEPRECATED 2026-07-03:方案A,UserPromptSubmit hook 整体废弃,AI 改由 CLAUDE.md 路由表自主选 skill】" + requirements: + 1: 命中 Skill 后,插件只向 `additionalContext` 注入加载该 Skill 的短指令,不注入 `SKILL.md` 全文。 + 1-note: 2026-06-24 初查当前仓库时,`hooks/on-prompt.sh` 未读取 `SKILL.md` 全文,而是硬编码注入一段读取 `skills/makers-agents/SKILL.md` 的说明;实现前需要确认全文注入来自外部打包流程、旧版本脚本,还是待迁移设计。 + 2: 插件保留现有提示词评分和 Skill 选择机制,输出格式变化不改变命中结果。 + 2-note: 2026-06-24 讨论确认:#6 的目标定义为统一所有命中 Skill 的输出为平台化加载指令;旧版全文注入只作为背景假设保留。 + 2-note-implementation: 2026-06-24 实现选择:新增 `hooks/userprompt-skill-inject.mjs` 承载提示词评分、平台检测和指令生成;`hooks/on-prompt.sh` 仅定位插件根目录并转发 stdin,便于后续 #8/#9/#12 复用。 + 2-note-trial: 2026-06-24 试改已加入 `selectSkillForPrompt`、`detectPlatform`、`renderSkillInstruction` 和 `buildHookOutput`;当前评分规则仍是脚本内静态规则,后续 #9 可迁移为读取 Skill frontmatter。 + 3: Claude Code 平台的注入指令格式为 `You must run the Skill() tool.`。 + 4: Cursor 平台的注入指令格式为 `Load the / skill.`。 + 5: 指令注入路径不再执行 10KB 预算裁剪。 + 6: 验证用例必须证明触发 Skill 时上下文只包含加载指令,且 Agent 能继续加载正确 Skill 内容。 + 6-note-verified: 2026-06-24 已运行 `node --test hooks/userprompt-skill-inject.test.mjs`,退出码为 0;Hook 样例验证部署 prompt 输出 `You must run the Skill(makers-deploy) tool.`,Cursor Edge Function prompt 输出 `Load the /makers-edge-functions skill.`。 + 7: 不支持 Skill tool 的平台不在 #6 中降级注入全文,相关 Push 模式 fallback 由 #11 覆盖。 + + MINIMAL_SESSION_START: + description: "#7 SessionStart 极简化。【DEPRECATED 2026-07-03:方案A,SessionStart hook 删除,CLAUDE.md 已作为项目指令自动加载】" + requirements: + 1: `SessionStart` 只注入少量行为原则和 Skill 路由提示,不注入知识图谱全文。 + 2: 合并后的 `CLAUDE.md` 作为 Agent 可读取的 Skill 路由表。 + 3: + requirement: profiler 逻辑继续扫描项目设置并设置环境变量。 + deprecated: true + reason: 2026-06-24 用户确认 profiler 属于旧插件仓库,新仓库没有该逻辑;#7 按无 profiler 的新仓库实现。 + 4: 验证用例必须记录优化后新会话的上下文占用下降。 + 5: 新仓库的 `SessionStart` 实现不新增或模拟 profiler 入口。 + 5-note-implementation: 2026-06-24 已新增 `hooks/sessionstart-minimal-context.mjs`,并通过 `hooks/on-session-start.sh` 和 `hooks/hooks.json` 注册到 `SessionStart`;输出内容保持为 5 行路由原则,不读取 `SKILL.md`、`references/` 或 profiler 数据。 + 5-note-verified: 2026-06-24 已运行 `node --test hooks/sessionstart-minimal-context.test.mjs`,覆盖上下文行数、`CLAUDE.md` / `AGENTS.md` 路由提示、禁止启动时加载全部 Skills,以及不包含 DeepAgents、`context.store`、`PAGES_SOURCE`、profiler 等知识图谱内容。 + + PRETOOLUSE_HOOK: + description: "#8 新增 PreToolUse Hook。【2026-07-03 部分废弃:路由注入+去重删除;PreToolUse hook 精简为仅 validate,拆为独立 hooks/validate-write.mjs(见#10)】" + requirements: + 1: 插件在 `hooks.json` 中注册 `PreToolUse` hook。 + 2: 插件提供 `pretooluse-skill-inject.mjs`,并从 stdin 解析 `tool_name` 和 `tool_input`。 + 3: `Read`、`Edit`、`Write` 工具调用根据文件路径匹配 Skill 的 `pathPatterns`。 + 4: `Bash` 工具调用根据命令字符串匹配 Skill 的 `bashPatterns`。 + 5: 命中 Skill 后输出对应平台的 Skill 加载指令。 + 6: 同一会话中已经注入过的 Skill 不重复注入。 + 7: 验证用例必须覆盖读取 `functions/index.ts` 触发 `makers-edge-functions`。 + 8: 验证用例必须覆盖执行 `edgeone pages deploy` 触发 `makers-deploy`。 + 8-note-implementation: 2026-06-24 初始实现新增 `hooks/pretooluse-skill-inject.mjs` 和对应测试;路径与命令规则暂时内置在脚本中,后续 #9 再迁移到 Skill frontmatter 的 `pathPatterns` / `bashPatterns`。 + 8-note-verified: 2026-06-24 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,并通过 stdin smoke test 验证 `Read functions/index.ts` 输出 `makers-edge-functions` 加载指令、`Bash edgeone pages deploy` 输出 `makers-deploy` 加载指令,重复读取同一 Skill 时不再输出。 + + MULTI_SIGNAL_MATCHING: + description: "#9 增加 pathPatterns 和 bashPatterns 多维匹配。【DEPRECATED 2026-07-03:方案A,pathPatterns/bashPatterns 从 frontmatter 移除,路由交还 AI】" + requirements: + 1: Skill frontmatter 支持 `pathPatterns` 字段,用于声明文件路径触发规则。 + 2: Skill frontmatter 支持 `bashPatterns` 字段,用于声明命令触发正则。 + 3: `makers-deploy` 至少匹配 `edgeone.json` 和 `\bedgeone\s+pages\s+deploy\b`。 + 4: `makers-edge-functions` 至少匹配 `functions/**`。 + 5: `makers-cloud-functions` 至少匹配 `cloud-functions/**`。 + 6: `makers-agents` 至少匹配 `agents/**`。 + 7: `makers-middleware` 至少匹配 `middleware.*`。 + 8: `makers-cli` 至少匹配 `\bedgeone\s+`。 + 9: Hook 将 glob 编译为正则后执行路径匹配。 + 10: 验证用例必须覆盖每个新增 pattern 的正向命中。 + 10-note-decision: 2026-06-24 #9 实现按真实仓库 Skill 名称使用 `makers-cloud-functions`,不新增不存在的 `makers-node-functions`;`makers-storage` 暂不声明虚构目录 pathPatterns,KV import / `context.store` 触发留给 #12 `chainTo`。 + 10-note-priority: 2026-06-24 `edgeone pages deploy` 会同时匹配 `makers-deploy` 与更泛的 `makers-cli`,Hook 需要按规则具体度选择最具体命中,避免 CLI 泛规则抢占部署 Skill。 + 10-note-implementation: 2026-06-24 已在 `makers-deploy`、`makers-edge-functions`、`makers-cloud-functions`、`makers-agents`、`makers-middleware` 和 `makers-cli` 的 `SKILL.md` frontmatter 中增加 `pathPatterns` / `bashPatterns`;`hooks/pretooluse-skill-inject.mjs` 改为扫描真实 Skill frontmatter 生成触发规则。 + 10-note-verified: 2026-06-24 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,覆盖从 frontmatter 读取 path/bash patterns、`edgeone pages deploy` 具体度优先命中 `makers-deploy`、泛 `edgeone` 命令命中 `makers-cli`,以及所有新增路径 pattern 的正向命中。 + + VALIDATE_RED_LINES: + description: "#10 增加 validate 红线规则。【2026-07-03 保留并精简:validate 拆为独立 hook hooks/validate-write.mjs,仅做写入内容检查,无状态无注入】" + requirements: + 1: Skill frontmatter 支持 `validate` 字段,用于声明写入内容的违规 pattern 和纠错提示。 + 2: `PreToolUse` 在 `Edit` 和 `Write` 时检测拟写入内容。 + 3: 写入 `process.env` 时提示 EdgeOne Makers 运行时代码应使用 `context.env`。 + 4: 写入 `new Headers()` 时提示当前运行时 surface 使用普通对象 headers。 + 5: 写入 `fs.writeFile` 时提示 Edge Function 不支持文件系统写入。 + 6: 初始实现中 validate 命中只追加纠错提示,不阻断写入。 + 6-note: 是否对关键违规执行阻断,需要在实现前继续讨论。 + 6-note-implementation: 2026-06-24 已在 `makers-edge-functions` frontmatter 中增加 `validate` 规则,并让 `hooks/pretooluse-skill-inject.mjs` 在 `Edit` / `Write` 的拟写入内容中匹配规则后追加纠错提醒;Skill 已经去重注入过时仍会继续输出 validate 提醒。 + 6-note-verified: 2026-06-24 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,覆盖 validate frontmatter 解析、`process.env`、`new Headers()`、`fs.writeFile` 三条提醒,以及 `Read` 不触发 validate 的只读路径。 + + DOMESTIC_IDE_ADAPTATION: + description: "#11 CodeBuddy 等国内 IDE 适配。【2026-07-03 部分废弃:路由降级/Push 模式 fallback 删除;写入工具名兼容(read_file/write_to_file/replace_in_file/execute_command)保留在 validate-write.mjs】" + requirements: + 1: 插件记录各平台对 `SessionStart`、`UserPromptSubmit`、`PreToolUse` 的支持差异。 + 2: 插件记录各平台是否支持 Skill tool。 + 3: 不支持 `PreToolUse` 的平台降级为仅使用 `UserPromptSubmit`。 + 4: 不支持 Skill tool 的平台降级为 Push 模式,注入必要 Skill 内容。 + 5: 插件通过现有或扩展后的 `detectPlatform` 选择平台分支。 + 6: 验证用例必须覆盖 CodeBuddy 从开发到部署的完整流程。 + 6-note-implementation: 2026-06-25 CodeBuddy 回归发现其工具名为 `read_file`、`write_to_file`、`replace_in_file`、`execute_command`,路径字段为 `filePath`,增量替换内容字段为 `new_str`;已扩展 `hooks/hooks.json` 的 `PreToolUse` matcher,并在 `hooks/pretooluse-skill-inject.mjs` 中将这些工具名映射到既有 Read/Edit/Write/Bash 逻辑。 + 6-note-verified: 2026-06-25 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,覆盖 CodeBuddy `read_file` 路径触发、`execute_command` 命令触发、`write_to_file` 的 chainTo 触发,以及 `replace_in_file` 使用 `new_str` 的 validate 触发。 + + CHAIN_TO_LOADING: + description: "#12 增加 chainTo 链式加载。【DEPRECATED 2026-07-03:方案A,chainTo 从 frontmatter 移除,交叉引用迁入 SKILL.md 正文】" + requirements: + 1: Skill frontmatter 支持 `chainTo` 字段,用于声明代码 pattern 到关联 Skill 的加载规则。 + 2: `PreToolUse` 在 `Edit` 和 `Write` 时检测 `chainTo` pattern。 + 3: 命中 `chainTo` 后额外注入关联 Skill 的加载指令。 + 4: 链式加载使用与直接命中相同的去重机制。 + 5: 验证用例必须覆盖编辑包含 KV import 或 `context.store` 的代码时触发 `makers-storage`。 + 5-note-implementation: 2026-06-25 已在 `makers-edge-functions` 与 `makers-agents` frontmatter 中增加 `chainTo` 规则,并让 `hooks/pretooluse-skill-inject.mjs` 对 `Edit` / `Write` 拟写入内容执行一跳链式匹配;命中后追加关联 Skill 加载指令,复用直接命中的去重状态,并写入 `trigger: "chainTo"` 信号日志。 + 5-note-verified: 2026-06-25 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,覆盖 `chainTo` frontmatter 解析、KV 代码触发 `makers-storage`、`context.store` 触发 `makers-storage`、已注入 Skill 不重复输出,以及 chainTo 日志原因。 + + SIGNAL_LOGGING: + description: "#13 信号观测日志。【2026-07-03 保留:signal-log.mjs 供 validate-write.mjs 记录 validate 命中;UserPromptSubmit/PreToolUse 路由信号删除】" + requirements: + 1: 插件将 Skill 触发信号追加写入 `.edgeone/signal-log.jsonl`。 + 2: 每条日志至少包含 `timestamp`、`hook`、`trigger`、`matchedSkill` 和 `reason`。 + 3: `PreToolUse` 记录路径、命令、validate 和 chainTo 触发原因。 + 4: `UserPromptSubmit` 记录提示词评分命中原因。 + 5: 验证用例必须检查日志文件中存在正确的触发记录。 + 5-note-implementation: 2026-06-25 已新增 `hooks/signal-log.mjs`,支持向 `.edgeone/signal-log.jsonl` append JSONL,并在 `UserPromptSubmit` 命中提示词评分、`PreToolUse` 命中 `pathPatterns` / `bashPatterns` / `validate` 时记录信号;`chainTo` 的实际触发日志随 #12 链式加载实现接入。 + 5-note-verified: 2026-06-25 已运行 `node --test hooks/signal-log.test.mjs hooks/userprompt-skill-inject.test.mjs hooks/pretooluse-skill-inject.test.mjs`,覆盖必填字段、JSONL 写入、prompt score、路径、命令和 validate 信号日志;`.gitignore` 已忽略 `.edgeone/signal-log.jsonl`。 + +constraints: + CONTEXT_BUDGET: + description: 上下文预算和加载策略约束。 + requirements: + 1: 默认路径应优先使用指令加载模式,避免把大型 Skill 文档直接塞入上下文。 + 2: 只有平台不支持 Skill tool 或显式要求 Push 模式时,才允许注入 Skill 全文。 + + SPEC_MAINTENANCE: + description: 规格维护约束。 + requirements: + 1: 讨论中确认的新决策必须先更新本规格,再进入实现。 + 2: 实现、测试和文档中的关键行为应引用完整 ACID。 + 3: 已发布或已讨论确认的 requirement 不应重编号;需要变更时使用 note 或 deprecated 标记。 + diff --git a/hooks/hooks.json b/hooks/hooks.json index 36ded82..70fc38c 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,37 +1,13 @@ { "hooks": { - "SessionStart": [ - { - "matcher": "", - "hooks": [ - { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/on-session-start.sh", - "timeout": 5 - } - ] - } - ], - "UserPromptSubmit": [ - { - "matcher": "", - "hooks": [ - { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/on-prompt.sh", - "timeout": 5 - } - ] - } - ], "PreToolUse": [ { - "matcher": "Read|Edit|Write|Bash|read_file|replace_in_file|write_to_file|execute_command", + "matcher": "Edit|Write|replace_in_file|write_to_file", "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse-skill-inject.mjs\"", - "timeout": 5 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/validate-write.mjs\"", + "timeout": 3 } ] } diff --git a/hooks/on-prompt.sh b/hooks/on-prompt.sh deleted file mode 100755 index 6ba208b..0000000 --- a/hooks/on-prompt.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -# UserPromptSubmit hook: route prompts to Skill loading instructions. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PLUGIN_ROOT="${EDGEONE_MAKERS_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}}" - -node "$PLUGIN_ROOT/hooks/userprompt-skill-inject.mjs" diff --git a/hooks/on-session-start.sh b/hooks/on-session-start.sh deleted file mode 100755 index 6abdb12..0000000 --- a/hooks/on-session-start.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -# SessionStart hook: inject compact EdgeOne Makers routing principles. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PLUGIN_ROOT="${EDGEONE_MAKERS_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}}" - -node "$PLUGIN_ROOT/hooks/sessionstart-minimal-context.mjs" - diff --git a/hooks/pretooluse-skill-inject.mjs b/hooks/pretooluse-skill-inject.mjs deleted file mode 100644 index 06181ee..0000000 --- a/hooks/pretooluse-skill-inject.mjs +++ /dev/null @@ -1,457 +0,0 @@ -#!/usr/bin/env node -import { readFileSync, readdirSync } from 'node:fs'; -import { mkdir, readFile, writeFile } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; - -import { shouldWriteSignalLog, writeSignalLog } from './signal-log.mjs'; -import { detectPlatform, renderSkillInstruction } from './userprompt-skill-inject.mjs'; - -const HOOKS_DIR = dirname(fileURLToPath(import.meta.url)); -const DEFAULT_SKILLS_DIR = join(HOOKS_DIR, '..', 'skills'); -const BASH_TOOL_NAMES = new Set(['Bash', 'execute_command']); -const PATH_TOOL_NAMES = new Set(['Read', 'Edit', 'Write', 'read_file', 'replace_in_file', 'write_to_file']); -const WRITE_TOOL_NAMES = new Set(['Edit', 'Write', 'replace_in_file', 'write_to_file']); - -let cachedRules = null; - -function escapeRegExp(value) { - return value.replace(/[|\\{}()[\]^$+?.*]/g, '\\$&'); -} - -function globToRegExp(pattern) { - const source = String(pattern) - .replace(/\\/g, '/') - .split('/') - .map((segment) => { - if (segment === '**') return '.*'; - return escapeRegExp(segment).replace(/\\\*/g, '[^/]*'); - }) - .join('/'); - - return new RegExp(`(^|/)${source}$`); -} - -function parseFrontmatter(content) { - const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content); - return match ? match[1] : ''; -} - -function parseYamlScalar(value) { - const trimmed = String(value || '').trim(); - if (!trimmed) return ''; - - if (trimmed.startsWith('"') && trimmed.endsWith('"')) { - try { - return JSON.parse(trimmed); - } catch { - return trimmed.slice(1, -1); - } - } - - if (trimmed.startsWith("'") && trimmed.endsWith("'")) { - return trimmed.slice(1, -1).replace(/''/g, "'"); - } - - return trimmed; -} - -function parseFrontmatterString(frontmatter, key) { - const pattern = new RegExp(`^${key}:\\s*(.+)$`, 'm'); - const match = pattern.exec(frontmatter); - return match ? parseYamlScalar(match[1]) : ''; -} - -function parseFrontmatterList(frontmatter, key) { - const lines = frontmatter.split(/\r?\n/); - const values = []; - let inList = false; - - for (const line of lines) { - if (!inList) { - inList = new RegExp(`^${key}:\\s*$`).test(line); - continue; - } - - if (!line.trim()) continue; - if (/^\S/.test(line)) break; - - const item = /^\s+-\s*(.+?)\s*$/.exec(line); - if (item) values.push(parseYamlScalar(item[1])); - } - - return values; -} - -function assignObjectField(object, text) { - const field = /^([A-Za-z][\w-]*):\s*(.*?)\s*$/.exec(text); - if (!field) return; - object[field[1]] = parseYamlScalar(field[2]); -} - -function parseFrontmatterObjectList(frontmatter, key, requiredFields = ['pattern', 'message']) { - const lines = frontmatter.split(/\r?\n/); - const values = []; - let current = null; - let inList = false; - - for (const line of lines) { - if (!inList) { - inList = new RegExp(`^${key}:\\s*$`).test(line); - continue; - } - - if (!line.trim()) continue; - if (/^\S/.test(line)) break; - - const item = /^\s+-\s*(.*?)\s*$/.exec(line); - if (item) { - current = {}; - values.push(current); - if (item[1]) assignObjectField(current, item[1]); - continue; - } - - if (current) assignObjectField(current, line.trim()); - } - - return values.filter((value) => requiredFields.every((field) => value[field])); -} - -function parseSkillTriggerRule(skillPath) { - const frontmatter = parseFrontmatter(readFileSync(skillPath, 'utf8')); - const skill = parseFrontmatterString(frontmatter, 'name'); - if (!skill) return null; - - return { - skill, - pathPatterns: parseFrontmatterList(frontmatter, 'pathPatterns'), - bashPatterns: parseFrontmatterList(frontmatter, 'bashPatterns'), - validate: parseFrontmatterObjectList(frontmatter, 'validate'), - chainTo: parseFrontmatterObjectList(frontmatter, 'chainTo', ['pattern', 'skill', 'reason']), - }; -} - -export function loadSkillTriggerRules(skillsDir = DEFAULT_SKILLS_DIR) { - if (skillsDir === DEFAULT_SKILLS_DIR && cachedRules) return cachedRules; - - const rules = readdirSync(skillsDir, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => join(skillsDir, entry.name, 'SKILL.md')) - .map((skillPath) => parseSkillTriggerRule(skillPath)) - .filter( - (rule) => - rule && - (rule.pathPatterns.length > 0 || - rule.bashPatterns.length > 0 || - rule.validate.length > 0 || - rule.chainTo.length > 0), - ); - - if (skillsDir === DEFAULT_SKILLS_DIR) cachedRules = rules; - return rules; -} - -function normalizePath(filePath) { - return String(filePath || '').replace(/\\/g, '/'); -} - -function getToolName(payload) { - return String(payload?.tool_name || payload?.toolName || '').trim(); -} - -function getToolInput(payload) { - return payload?.tool_input || payload?.toolInput || {}; -} - -function getToolPath(toolInput) { - return normalizePath( - toolInput.file_path || toolInput.filePath || toolInput.path || toolInput.target_file || '', - ); -} - -function getToolCommand(toolInput) { - return String(toolInput.command || '').trim(); -} - -function pickMostSpecificMatch(matches) { - if (matches.length === 0) return null; - return matches.sort((left, right) => right.specificity - left.specificity)[0]; -} - -function matchPathRule(filePath, rules) { - if (!filePath) return null; - - const matches = []; - for (const rule of rules) { - for (const pattern of rule.pathPatterns || []) { - if (globToRegExp(pattern).test(filePath)) { - matches.push({ - skill: rule.skill, - trigger: 'pathPatterns', - reason: `${filePath} matched ${pattern}`, - validate: rule.validate || [], - chainTo: rule.chainTo || [], - specificity: pattern.length, - }); - } - } - } - - return pickMostSpecificMatch(matches); -} - -function matchBashRule(command, rules) { - if (!command) return null; - - const matches = []; - for (const rule of rules) { - for (const pattern of rule.bashPatterns || []) { - const regex = new RegExp(pattern, 'i'); - if (regex.test(command)) { - matches.push({ - skill: rule.skill, - trigger: 'bashPatterns', - reason: `${command} matched ${pattern}`, - validate: [], - chainTo: [], - specificity: pattern.length, - }); - } - } - } - - return pickMostSpecificMatch(matches); -} - -export function selectSkillForToolUse(payload, rules = loadSkillTriggerRules()) { - const toolName = getToolName(payload); - const toolInput = getToolInput(payload); - - if (BASH_TOOL_NAMES.has(toolName)) { - return matchBashRule(getToolCommand(toolInput), rules); - } - - if (PATH_TOOL_NAMES.has(toolName)) { - return matchPathRule(getToolPath(toolInput), rules); - } - - return null; -} - -function getToolWriteContent(payload) { - const toolName = getToolName(payload); - if (!WRITE_TOOL_NAMES.has(toolName)) return ''; - - const toolInput = getToolInput(payload); - const candidateKeys = ['content', 'new_string', 'new_str', 'newString', 'text']; - for (const key of candidateKeys) { - if (typeof toolInput[key] === 'string') return toolInput[key]; - } - - return ''; -} - -function selectValidationMatches(payload, match) { - const content = getToolWriteContent(payload); - if (!content) return []; - - const matches = []; - for (const rule of match.validate || []) { - const regex = new RegExp(rule.pattern); - if (regex.test(content)) { - matches.push({ - pattern: rule.pattern, - message: rule.message, - }); - } - } - - return matches.filter( - (matchItem, index, list) => - list.findIndex((candidate) => candidate.message === matchItem.message) === index, - ); -} - -function selectChainToMatches(payload, match) { - const content = getToolWriteContent(payload); - if (!content) return []; - - const matches = []; - for (const rule of match.chainTo || []) { - const regex = new RegExp(rule.pattern); - if (regex.test(content)) { - matches.push({ - pattern: rule.pattern, - skill: rule.skill, - reason: rule.reason, - }); - } - } - - return matches.filter( - (matchItem, index, list) => - list.findIndex( - (candidate) => candidate.skill === matchItem.skill && candidate.reason === matchItem.reason, - ) === index, - ); -} - -function renderValidationReminder(messages) { - return `Validation reminder:\n${messages.map((message) => `- ${message}`).join('\n')}`; -} - -function maybeWritePreToolUseSignal(match, payload, platform, options) { - if (!shouldWriteSignalLog(options)) return; - - writeSignalLog( - { - hook: 'PreToolUse', - trigger: match.trigger, - matchedSkill: match.skill, - reason: match.reason, - platform, - toolName: getToolName(payload), - }, - options, - ); -} - -function maybeWriteValidationSignals(validationMatches, match, payload, platform, options) { - if (!shouldWriteSignalLog(options)) return; - - for (const validationMatch of validationMatches) { - writeSignalLog( - { - hook: 'PreToolUse', - trigger: 'validate', - matchedSkill: match.skill, - reason: validationMatch.message, - platform, - toolName: getToolName(payload), - }, - options, - ); - } -} - -function maybeWriteChainToSignals(chainToMatches, payload, platform, options) { - if (!shouldWriteSignalLog(options)) return; - - for (const chainToMatch of chainToMatches) { - writeSignalLog( - { - hook: 'PreToolUse', - trigger: 'chainTo', - matchedSkill: chainToMatch.skill, - reason: chainToMatch.reason, - platform, - toolName: getToolName(payload), - }, - options, - ); - } -} - -function defaultStatePath() { - return process.env.EDGEONE_MAKERS_PRETOOLUSE_STATE || join(process.cwd(), '.edgeone', 'pretooluse-injected-skills.json'); -} - -async function readInjectedSkills(statePath) { - try { - const raw = await readFile(statePath, 'utf8'); - const parsed = JSON.parse(raw); - return new Set(Array.isArray(parsed.injectedSkills) ? parsed.injectedSkills : []); - } catch { - return new Set(); - } -} - -async function writeInjectedSkills(statePath, injectedSkills) { - await mkdir(dirname(statePath), { recursive: true }); - await writeFile( - statePath, - `${JSON.stringify({ injectedSkills: [...injectedSkills].sort() }, null, 2)}\n`, - ); -} - -async function getInjectedSkills(options) { - if (options.injectedSkills) return options.injectedSkills; - return readInjectedSkills(options.statePath || defaultStatePath()); -} - -async function persistInjectedSkills(injectedSkills, options) { - if (options.injectedSkills) return; - await writeInjectedSkills(options.statePath || defaultStatePath(), injectedSkills); -} - -export async function buildPreToolUseOutput(payload, platform = 'claude-code', options = {}) { - const match = selectSkillForToolUse(payload); - if (!match) return null; - - const additionalContext = []; - const validationMatches = selectValidationMatches(payload, match); - const chainToMatches = selectChainToMatches(payload, match); - maybeWritePreToolUseSignal(match, payload, platform, options); - maybeWriteValidationSignals(validationMatches, match, payload, platform, options); - maybeWriteChainToSignals(chainToMatches, payload, platform, options); - - const injectedSkills = await getInjectedSkills(options); - let injectedSkillsChanged = false; - if (!injectedSkills.has(match.skill)) { - injectedSkills.add(match.skill); - injectedSkillsChanged = true; - additionalContext.push(renderSkillInstruction(match.skill, platform)); - } - - for (const chainToMatch of chainToMatches) { - if (injectedSkills.has(chainToMatch.skill)) continue; - - injectedSkills.add(chainToMatch.skill); - injectedSkillsChanged = true; - additionalContext.push(renderSkillInstruction(chainToMatch.skill, platform)); - } - - if (injectedSkillsChanged) { - await persistInjectedSkills(injectedSkills, options); - } - - const validationMessages = validationMatches.map((validationMatch) => validationMatch.message); - if (validationMessages.length > 0) { - additionalContext.push(renderValidationReminder(validationMessages)); - } - - if (additionalContext.length === 0) return null; - - return { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: additionalContext.join('\n\n'), - }, - }; -} - -async function readStdin() { - let input = ''; - for await (const chunk of process.stdin) { - input += chunk; - } - return input; -} - -export async function main() { - const rawInput = await readStdin(); - const payload = rawInput.trim() ? JSON.parse(rawInput) : {}; - const output = await buildPreToolUseOutput(payload, detectPlatform(), { enableSignalLog: true }); - - if (!output) return; - process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - main().catch((error) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - }); -} - diff --git a/hooks/pretooluse-skill-inject.test.mjs b/hooks/pretooluse-skill-inject.test.mjs deleted file mode 100644 index 81aff8b..0000000 --- a/hooks/pretooluse-skill-inject.test.mjs +++ /dev/null @@ -1,536 +0,0 @@ -import assert from 'node:assert/strict'; -import { mkdtemp, readFile, rm } from 'node:fs/promises'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import test from 'node:test'; - -import { - buildPreToolUseOutput, - loadSkillTriggerRules, - selectSkillForToolUse, -} from './pretooluse-skill-inject.mjs'; - -test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.7 selects makers-edge-functions for Read functions/index.ts', () => { - assert.equal( - selectSkillForToolUse({ - tool_name: 'Read', - tool_input: { file_path: 'functions/index.ts' }, - })?.skill, - 'makers-edge-functions', - ); -}); - -test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 selects makers-edge-functions for CodeBuddy read_file filePath', () => { - assert.equal( - selectSkillForToolUse({ - tool_name: 'read_file', - tool_input: { filePath: 'functions/index.ts' }, - })?.skill, - 'makers-edge-functions', - ); -}); - -test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.8 selects makers-deploy for edgeone pages deploy', () => { - assert.equal( - selectSkillForToolUse({ - tool_name: 'Bash', - tool_input: { command: 'PAGES_SOURCE=skills edgeone pages deploy' }, - })?.skill, - 'makers-deploy', - ); -}); - -test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 selects makers-deploy for CodeBuddy execute_command', () => { - assert.equal( - selectSkillForToolUse({ - tool_name: 'execute_command', - tool_input: { command: 'PAGES_SOURCE=skills edgeone pages deploy' }, - })?.skill, - 'makers-deploy', - ); -}); - -test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.5 renders Cursor Skill loading instruction', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'Edit', - tool_input: { file_path: '/project/agents/chat.ts' }, - }, - 'cursor', - { injectedSkills: new Set() }, - ); - - assert.deepEqual(output, { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: 'Load the /makers-agents skill.', - }, - }); -}); - -test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.6 deduplicates injected Skills with state file', async () => { - const tmp = await mkdtemp(join(tmpdir(), 'makers-pretooluse-')); - const statePath = join(tmp, 'injected-skills.json'); - - try { - const payload = { - tool_name: 'Read', - tool_input: { file_path: '/project/functions/index.ts' }, - }; - - assert.deepEqual(await buildPreToolUseOutput(payload, 'claude-code', { statePath }), { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: 'You must run the Skill(makers-edge-functions) tool.', - }, - }); - assert.equal(await buildPreToolUseOutput(payload, 'claude-code', { statePath }), null); - } finally { - await rm(tmp, { recursive: true, force: true }); - } -}); - -test('plugin-skill-injection-optimization.PRETOOLUSE_HOOK.3 returns null for unmatched tool use', async () => { - assert.equal( - await buildPreToolUseOutput( - { - tool_name: 'Read', - tool_input: { file_path: 'src/components/Button.tsx' }, - }, - 'claude-code', - { injectedSkills: new Set() }, - ), - null, - ); -}); - -test('plugin-skill-injection-optimization.MULTI_SIGNAL_MATCHING.1 loads pathPatterns from Skill frontmatter', async () => { - const rules = await loadSkillTriggerRules(); - const edgeFunctions = rules.find((rule) => rule.skill === 'makers-edge-functions'); - - assert.ok(edgeFunctions); - assert.deepEqual(edgeFunctions.pathPatterns, ['functions/**']); -}); - -test('plugin-skill-injection-optimization.MULTI_SIGNAL_MATCHING.2 loads bashPatterns from Skill frontmatter', async () => { - const rules = await loadSkillTriggerRules(); - const deploy = rules.find((rule) => rule.skill === 'makers-deploy'); - - assert.ok(deploy); - assert.ok(deploy.bashPatterns.includes('\\bedgeone\\s+pages\\s+deploy\\b')); -}); - -test('plugin-skill-injection-optimization.MULTI_SIGNAL_MATCHING.10 matches configured path patterns', () => { - const cases = [ - ['edgeone.json', 'makers-deploy'], - ['functions/index.ts', 'makers-edge-functions'], - ['cloud-functions/api/index.ts', 'makers-cloud-functions'], - ['agents/chat.ts', 'makers-agents'], - ['middleware.ts', 'makers-middleware'], - ]; - - for (const [filePath, expectedSkill] of cases) { - assert.equal( - selectSkillForToolUse({ - tool_name: 'Read', - tool_input: { file_path: filePath }, - })?.skill, - expectedSkill, - ); - } -}); - -test('plugin-skill-injection-optimization.MULTI_SIGNAL_MATCHING.10 matches configured bash patterns by specificity', () => { - assert.equal( - selectSkillForToolUse({ - tool_name: 'Bash', - tool_input: { command: 'PAGES_SOURCE=skills edgeone pages deploy' }, - })?.skill, - 'makers-deploy', - ); - - assert.equal( - selectSkillForToolUse({ - tool_name: 'Bash', - tool_input: { command: 'edgeone makers env ls' }, - })?.skill, - 'makers-cli', - ); -}); - -test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.1 loads validate rules from Skill frontmatter', async () => { - const rules = await loadSkillTriggerRules(); - const edgeFunctions = rules.find((rule) => rule.skill === 'makers-edge-functions'); - - assert.ok(edgeFunctions); - assert.deepEqual(edgeFunctions.validate, [ - { - pattern: 'process\\.env', - message: 'Use context.env in EdgeOne Makers runtime code.', - }, - { - pattern: 'new\\s+Headers\\s*\\(', - message: 'Use plain object headers for this runtime surface.', - }, - { - pattern: 'fs\\.writeFile', - message: 'Edge Functions do not support filesystem writes.', - }, - ]); -}); - -test('plugin-skill-injection-optimization.CHAIN_TO_LOADING.1 loads chainTo rules from Skill frontmatter', async () => { - const rules = await loadSkillTriggerRules(); - const edgeFunctions = rules.find((rule) => rule.skill === 'makers-edge-functions'); - - assert.ok(edgeFunctions); - assert.deepEqual(edgeFunctions.chainTo, [ - { - pattern: '\\bKV\\b|context\\.store', - skill: 'makers-storage', - reason: 'Code references KV or store APIs.', - }, - ]); -}); - -test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.2 warns on Edit content without blocking writes', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'Edit', - tool_input: { - file_path: 'functions/index.ts', - new_string: 'export default () => process.env.API_KEY;', - }, - }, - 'claude-code', - { injectedSkills: new Set() }, - ); - - assert.deepEqual(output, { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: - 'You must run the Skill(makers-edge-functions) tool.\n\nValidation reminder:\n- Use context.env in EdgeOne Makers runtime code.', - }, - }); -}); - -test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.4 warns on Write content using new Headers', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'Write', - tool_input: { - file_path: 'functions/index.ts', - content: 'return new Response(body, { headers: new Headers() });', - }, - }, - 'cursor', - { injectedSkills: new Set(['makers-edge-functions']) }, - ); - - assert.deepEqual(output, { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: 'Validation reminder:\n- Use plain object headers for this runtime surface.', - }, - }); -}); - -test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.5 warns on Edge Function filesystem writes', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'Write', - tool_input: { - file_path: 'functions/index.ts', - content: 'fs.writeFile("/tmp/out.txt", "data", () => {});', - }, - }, - 'claude-code', - { injectedSkills: new Set(['makers-edge-functions']) }, - ); - - assert.deepEqual(output, { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: 'Validation reminder:\n- Edge Functions do not support filesystem writes.', - }, - }); -}); - -test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 does not warn for read-only tool use', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'Read', - tool_input: { - file_path: 'functions/index.ts', - content: 'process.env.API_KEY', - }, - }, - 'claude-code', - { injectedSkills: new Set(['makers-edge-functions']) }, - ); - - assert.equal(output, null); -}); - -test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs PreToolUse path pattern matches', async () => { - const tmp = await mkdtemp(join(tmpdir(), 'makers-pretooluse-log-')); - const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); - - try { - await buildPreToolUseOutput( - { - tool_name: 'Read', - tool_input: { file_path: 'functions/index.ts' }, - }, - 'claude-code', - { - injectedSkills: new Set(), - signalLogPath, - now: new Date('2026-06-24T00:00:00.000Z'), - }, - ); - - const [line] = (await readFile(signalLogPath, 'utf8')).trim().split('\n'); - assert.deepEqual(JSON.parse(line), { - timestamp: '2026-06-24T00:00:00.000Z', - hook: 'PreToolUse', - trigger: 'pathPatterns', - matchedSkill: 'makers-edge-functions', - reason: 'functions/index.ts matched functions/**', - platform: 'claude-code', - toolName: 'Read', - }); - } finally { - await rm(tmp, { recursive: true, force: true }); - } -}); - -test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs PreToolUse bash pattern matches', async () => { - const tmp = await mkdtemp(join(tmpdir(), 'makers-pretooluse-log-')); - const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); - - try { - await buildPreToolUseOutput( - { - tool_name: 'Bash', - tool_input: { command: 'PAGES_SOURCE=skills edgeone pages deploy' }, - }, - 'cursor', - { - injectedSkills: new Set(), - signalLogPath, - now: new Date('2026-06-24T00:00:00.000Z'), - }, - ); - - const [line] = (await readFile(signalLogPath, 'utf8')).trim().split('\n'); - assert.deepEqual(JSON.parse(line), { - timestamp: '2026-06-24T00:00:00.000Z', - hook: 'PreToolUse', - trigger: 'bashPatterns', - matchedSkill: 'makers-deploy', - reason: 'PAGES_SOURCE=skills edgeone pages deploy matched \\bedgeone\\s+pages\\s+deploy\\b', - platform: 'cursor', - toolName: 'Bash', - }); - } finally { - await rm(tmp, { recursive: true, force: true }); - } -}); - -test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs validate matches with readable reasons', async () => { - const tmp = await mkdtemp(join(tmpdir(), 'makers-pretooluse-log-')); - const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); - - try { - await buildPreToolUseOutput( - { - tool_name: 'Write', - tool_input: { - file_path: 'functions/index.ts', - content: 'export default () => process.env.API_KEY;', - }, - }, - 'claude-code', - { - injectedSkills: new Set(['makers-edge-functions']), - signalLogPath, - now: new Date('2026-06-24T00:00:00.000Z'), - }, - ); - - const lines = (await readFile(signalLogPath, 'utf8')) - .trim() - .split('\n') - .map((line) => JSON.parse(line)); - - assert.deepEqual( - lines.map((line) => line.trigger), - ['pathPatterns', 'validate'], - ); - assert.deepEqual(lines[1], { - timestamp: '2026-06-24T00:00:00.000Z', - hook: 'PreToolUse', - trigger: 'validate', - matchedSkill: 'makers-edge-functions', - reason: 'Use context.env in EdgeOne Makers runtime code.', - platform: 'claude-code', - toolName: 'Write', - }); - } finally { - await rm(tmp, { recursive: true, force: true }); - } -}); - -test('plugin-skill-injection-optimization.CHAIN_TO_LOADING.3 injects chained storage Skill for KV code', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'Write', - tool_input: { - file_path: 'functions/index.ts', - content: 'const value = await KV.get("counter");', - }, - }, - 'claude-code', - { injectedSkills: new Set() }, - ); - - assert.deepEqual(output, { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: - 'You must run the Skill(makers-edge-functions) tool.\n\nYou must run the Skill(makers-storage) tool.', - }, - }); -}); - -test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 supports CodeBuddy write_to_file chainTo with filePath', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'write_to_file', - tool_input: { - filePath: 'functions/index.ts', - content: 'const value = await KV.get("counter");', - }, - }, - 'codebuddy', - { injectedSkills: new Set() }, - ); - - assert.deepEqual(output, { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: - 'Load the /makers-edge-functions skill.\n\nLoad the /makers-storage skill.', - }, - }); -}); - -test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 supports CodeBuddy replace_in_file validate with filePath', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'replace_in_file', - tool_input: { - filePath: 'functions/index.ts', - new_str: 'export default () => process.env.API_KEY;', - }, - }, - 'codebuddy', - { injectedSkills: new Set(['makers-edge-functions']) }, - ); - - assert.deepEqual(output, { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: 'Validation reminder:\n- Use context.env in EdgeOne Makers runtime code.', - }, - }); -}); - -test('plugin-skill-injection-optimization.CHAIN_TO_LOADING.4 does not repeat chained Skills already injected', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'Write', - tool_input: { - file_path: 'functions/index.ts', - content: 'const value = await KV.get("counter");', - }, - }, - 'claude-code', - { injectedSkills: new Set(['makers-storage']) }, - ); - - assert.deepEqual(output, { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: 'You must run the Skill(makers-edge-functions) tool.', - }, - }); -}); - -test('plugin-skill-injection-optimization.CHAIN_TO_LOADING.5 injects storage Skill for context.store code', async () => { - const output = await buildPreToolUseOutput( - { - tool_name: 'Edit', - tool_input: { - file_path: 'agents/chat.ts', - new_string: 'const session = context.store.openaiSession(context.conversation_id);', - }, - }, - 'cursor', - { injectedSkills: new Set(['makers-agents']) }, - ); - - assert.deepEqual(output, { - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: 'Load the /makers-storage skill.', - }, - }); -}); - -test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs chainTo matches with readable reasons', async () => { - const tmp = await mkdtemp(join(tmpdir(), 'makers-chain-log-')); - const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); - - try { - await buildPreToolUseOutput( - { - tool_name: 'Write', - tool_input: { - file_path: 'functions/index.ts', - content: 'const value = await KV.get("counter");', - }, - }, - 'claude-code', - { - injectedSkills: new Set(['makers-edge-functions', 'makers-storage']), - signalLogPath, - now: new Date('2026-06-24T00:00:00.000Z'), - }, - ); - - const lines = (await readFile(signalLogPath, 'utf8')) - .trim() - .split('\n') - .map((line) => JSON.parse(line)); - - assert.deepEqual( - lines.map((line) => line.trigger), - ['pathPatterns', 'chainTo'], - ); - assert.deepEqual(lines[1], { - timestamp: '2026-06-24T00:00:00.000Z', - hook: 'PreToolUse', - trigger: 'chainTo', - matchedSkill: 'makers-storage', - reason: 'Code references KV or store APIs.', - platform: 'claude-code', - toolName: 'Write', - }); - } finally { - await rm(tmp, { recursive: true, force: true }); - } -}); - diff --git a/hooks/sessionstart-minimal-context.mjs b/hooks/sessionstart-minimal-context.mjs deleted file mode 100644 index 45deba2..0000000 --- a/hooks/sessionstart-minimal-context.mjs +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node -import { pathToFileURL } from 'node:url'; - -export const MINIMAL_SESSION_START_CONTEXT = [ - 'EdgeOne Makers Tools is installed.', - 'Use CLAUDE.md / AGENTS.md as the Skill route table.', - 'Load exactly the matching makers-* Skill before EdgeOne Makers work.', - 'Do not load all Skills at startup.', - 'Prefer Makers-specific Skills over generic EdgeOne Pages guidance.', -].join('\n'); - -export function buildSessionStartOutput() { - return { - hookSpecificOutput: { - hookEventName: 'SessionStart', - additionalContext: MINIMAL_SESSION_START_CONTEXT, - }, - }; -} - -export async function main() { - process.stdout.write(`${JSON.stringify(buildSessionStartOutput(), null, 2)}\n`); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - main().catch((error) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - }); -} - diff --git a/hooks/sessionstart-minimal-context.test.mjs b/hooks/sessionstart-minimal-context.test.mjs deleted file mode 100644 index 42620a8..0000000 --- a/hooks/sessionstart-minimal-context.test.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { - MINIMAL_SESSION_START_CONTEXT, - buildSessionStartOutput, -} from './sessionstart-minimal-context.mjs'; - -test('plugin-skill-injection-optimization.MINIMAL_SESSION_START.1 injects compact startup principles', () => { - const lines = MINIMAL_SESSION_START_CONTEXT.trim().split('\n'); - - assert.ok(lines.length <= 8); - assert.match(MINIMAL_SESSION_START_CONTEXT, /CLAUDE\.md/); - assert.match(MINIMAL_SESSION_START_CONTEXT, /AGENTS\.md/); - assert.match(MINIMAL_SESSION_START_CONTEXT, /matching makers-\*/); - assert.match(MINIMAL_SESSION_START_CONTEXT, /Do not load all Skills/); -}); - -test('plugin-skill-injection-optimization.MINIMAL_SESSION_START.1 omits knowledge graph content', () => { - assert.doesNotMatch(MINIMAL_SESSION_START_CONTEXT, /DeepAgents/); - assert.doesNotMatch(MINIMAL_SESSION_START_CONTEXT, /context\.store/); - assert.doesNotMatch(MINIMAL_SESSION_START_CONTEXT, /PAGES_SOURCE/); - assert.doesNotMatch(MINIMAL_SESSION_START_CONTEXT, /profiler/i); -}); - -test('plugin-skill-injection-optimization.MINIMAL_SESSION_START.2 outputs SessionStart additionalContext', () => { - assert.deepEqual(buildSessionStartOutput(), { - hookSpecificOutput: { - hookEventName: 'SessionStart', - additionalContext: MINIMAL_SESSION_START_CONTEXT, - }, - }); -}); - diff --git a/hooks/userprompt-skill-inject.mjs b/hooks/userprompt-skill-inject.mjs deleted file mode 100644 index c7665a4..0000000 --- a/hooks/userprompt-skill-inject.mjs +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env node -import { pathToFileURL } from 'node:url'; - -import { shouldWriteSignalLog, writeSignalLog } from './signal-log.mjs'; - -const SKILL_RULES = [ - { - skill: 'makers-deploy', - patterns: [ - [/\bedgeone\s+pages\s+deploy\b/i, 8], - [/\bdeploy(?:ment)?\b/i, 5], - [/\bpublish\b|\brelease\b/i, 4], - [/上线|发布|部署/i, 5], - ], - }, - { - skill: 'makers-edge-functions', - patterns: [ - [/\bedge\s+functions?\b/i, 8], - [/\bfunctions\/[\w./-]*/i, 6], - [/\bV8\b/i, 3], - [/\bonRequest\b/i, 2], - ], - }, - { - skill: 'makers-cloud-functions', - patterns: [ - [/\bcloud[-\s]?functions?\b/i, 8], - [/\bcloud-functions\/[\w./-]*/i, 6], - [/\bexpress\b|\bkoa\b|\bgin\b/i, 4], - [/\bnode\.?js\b|\bgo\b|\bpython\b/i, 2], - ], - }, - { - skill: 'makers-agents', - patterns: [ - [/\bagents?\b/i, 5], - [/\bdeepagents?\b|\blanggraph\b|\bcrewai\b/i, 7], - [/\bopenai[-\s]?agents?\b|\bclaude[-\s]?sdk\b/i, 7], - [/\bcontext\.(store|tools|sandbox)\b/i, 5], - [/\bconversation[_-]?id\b|\bsse\b/i, 3], - [/智能体|代理开发/i, 5], - ], - }, - { - skill: 'makers-storage', - patterns: [ - [/\bkv\b|\bblob\b/i, 6], - [/\bstorage\b/i, 4], - [/\bcontext\.store\b/i, 4], - [/存储/i, 5], - ], - }, - { - skill: 'makers-middleware', - patterns: [ - [/\bmiddleware\b/i, 7], - [/\brewrite\b|\bredirect\b/i, 4], - [/\bauth\b.*\b(route|path|middleware)\b/i, 4], - [/中间件|重写|重定向/i, 5], - ], - }, - { - skill: 'makers-cli', - patterns: [ - [/\bedgeone\s+/i, 3], - [/\bcli\b|\bcommand\b/i, 3], - [/\bPAGES_SOURCE\b/i, 5], - ], - }, - { - skill: 'makers-recipes', - patterns: [ - [/\btemplate\b|\bscaffold\b|\brecipe\b/i, 5], - [/\bproject\s+structure\b/i, 4], - [/模板|脚手架|项目结构/i, 5], - ], - }, -]; - -function scorePrompt(prompt, rule) { - return rule.patterns.reduce((score, [pattern, weight]) => { - return pattern.test(prompt) ? score + weight : score; - }, 0); -} - -export function selectSkillForPrompt(prompt) { - const text = String(prompt || '').trim(); - if (!text) return null; - - let best = null; - for (const rule of SKILL_RULES) { - const score = scorePrompt(text, rule); - if (score > 0 && (!best || score > best.score)) { - best = { skill: rule.skill, score }; - } - } - return best; -} - -export function detectPlatform(env = process.env) { - const explicit = String(env.EDGEONE_MAKERS_PLATFORM || '').toLowerCase(); - if (explicit === 'claude' || explicit === 'claude-code') return 'claude-code'; - if (explicit === 'cursor') return 'cursor'; - if (explicit === 'codebuddy') return 'codebuddy'; - - if (env.CURSOR_PLUGIN_ROOT) return 'cursor'; - if (env.CODEBUDDY_PLUGIN_ROOT) return 'codebuddy'; - if (env.CLAUDE_PLUGIN_ROOT) return 'claude-code'; - return 'claude-code'; -} - -export function renderSkillInstruction(skill, platform = 'claude-code') { - if (platform === 'claude-code') { - return `You must run the Skill(${skill}) tool.`; - } - return `Load the /${skill} skill.`; -} - -function maybeWritePromptSignal(match, platform, options) { - if (!shouldWriteSignalLog(options)) return; - - writeSignalLog( - { - hook: 'UserPromptSubmit', - trigger: 'promptScore', - matchedSkill: match.skill, - reason: `prompt score ${match.score} selected ${match.skill}`, - platform, - }, - options, - ); -} - -export function buildHookOutput(prompt, platform = 'claude-code', options = {}) { - const match = selectSkillForPrompt(prompt); - if (!match) return null; - - maybeWritePromptSignal(match, platform, options); - - return { - hookSpecificOutput: { - hookEventName: 'UserPromptSubmit', - additionalContext: renderSkillInstruction(match.skill, platform), - }, - }; -} - -async function readStdin() { - let input = ''; - for await (const chunk of process.stdin) { - input += chunk; - } - return input; -} - -export async function main() { - const rawInput = await readStdin(); - const payload = rawInput.trim() ? JSON.parse(rawInput) : {}; - const output = buildHookOutput(payload.prompt, detectPlatform(), { enableSignalLog: true }); - - if (!output) return; - process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - main().catch((error) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - }); -} - diff --git a/hooks/userprompt-skill-inject.test.mjs b/hooks/userprompt-skill-inject.test.mjs deleted file mode 100644 index bc1ea6d..0000000 --- a/hooks/userprompt-skill-inject.test.mjs +++ /dev/null @@ -1,100 +0,0 @@ -import assert from 'node:assert/strict'; -import { mkdtemp, readFile, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import test from 'node:test'; - -import { - buildHookOutput, - detectPlatform, - renderSkillInstruction, - selectSkillForPrompt, -} from './userprompt-skill-inject.mjs'; - -test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.2 selects makers-agents for agent prompts', () => { - assert.equal( - selectSkillForPrompt('Create a LangGraph agent with context.store support')?.skill, - 'makers-agents', - ); -}); - -test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.2 selects makers-deploy for deploy prompts', () => { - assert.equal( - selectSkillForPrompt('Deploy this project with edgeone pages deploy')?.skill, - 'makers-deploy', - ); -}); - -test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.2 selects makers-edge-functions for edge function prompts', () => { - assert.equal( - selectSkillForPrompt('Write an Edge Function under functions/index.ts')?.skill, - 'makers-edge-functions', - ); -}); - -test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.3 renders Claude Code Skill tool instruction', () => { - assert.equal( - renderSkillInstruction('makers-edge-functions', 'claude-code'), - 'You must run the Skill(makers-edge-functions) tool.', - ); -}); - -test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.4 renders Cursor slash skill instruction', () => { - assert.equal( - renderSkillInstruction('makers-edge-functions', 'cursor'), - 'Load the /makers-edge-functions skill.', - ); -}); - -test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.6 outputs only the loading instruction', () => { - assert.deepEqual( - buildHookOutput('Deploy to EdgeOne', 'claude-code'), - { - hookSpecificOutput: { - hookEventName: 'UserPromptSubmit', - additionalContext: 'You must run the Skill(makers-deploy) tool.', - }, - }, - ); -}); - -test('plugin-skill-injection-optimization.INSTRUCTION_INJECTION.6 returns null when no skill matches', () => { - assert.equal(buildHookOutput('Explain how git bisect works', 'claude-code'), null); -}); - -test('detectPlatform prefers explicit platform override', () => { - assert.equal( - detectPlatform({ EDGEONE_MAKERS_PLATFORM: 'cursor', CLAUDE_PLUGIN_ROOT: '/tmp/plugin' }), - 'cursor', - ); -}); - -test('plugin-skill-injection-optimization.SIGNAL_LOGGING.4 logs prompt score matches', async () => { - const tmp = await mkdtemp(join(tmpdir(), 'makers-userprompt-log-')); - const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); - - try { - const output = buildHookOutput('Deploy this project with edgeone pages deploy', 'claude-code', { - signalLogPath, - now: new Date('2026-06-24T00:00:00.000Z'), - }); - - assert.equal( - output.hookSpecificOutput.additionalContext, - 'You must run the Skill(makers-deploy) tool.', - ); - - const [line] = (await readFile(signalLogPath, 'utf8')).trim().split('\n'); - assert.deepEqual(JSON.parse(line), { - timestamp: '2026-06-24T00:00:00.000Z', - hook: 'UserPromptSubmit', - trigger: 'promptScore', - matchedSkill: 'makers-deploy', - reason: 'prompt score 13 selected makers-deploy', - platform: 'claude-code', - }); - } finally { - await rm(tmp, { recursive: true, force: true }); - } -}); - diff --git a/hooks/validate-write.mjs b/hooks/validate-write.mjs new file mode 100644 index 0000000..4df7b1d --- /dev/null +++ b/hooks/validate-write.mjs @@ -0,0 +1,230 @@ +#!/usr/bin/env node +import { readFileSync, readdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { shouldWriteSignalLog, writeSignalLog } from './signal-log.mjs'; + +const HOOKS_DIR = dirname(fileURLToPath(import.meta.url)); +const DEFAULT_SKILLS_DIR = join(HOOKS_DIR, '..', 'skills'); +const WRITE_TOOL_NAMES = new Set(['Edit', 'Write', 'replace_in_file', 'write_to_file']); +const WRITE_CONTENT_KEYS = ['content', 'new_string', 'new_str', 'newString', 'text']; +const PATH_KEYS = ['file_path', 'filePath', 'path', 'target_file']; + +let cachedRules = null; + +function escapeRegExp(value) { + return value.replace(/[|\\{}()[\]^$+?.*]/g, '\\$&'); +} + +function globToRegExp(pattern) { + const source = String(pattern) + .replace(/\\/g, '/') + .split('/') + .map((segment) => (segment === '**' ? '.*' : escapeRegExp(segment).replace(/\\\*/g, '[^/]*'))) + .join('/'); + return new RegExp(`(^|/)${source}$`); +} + +function parseFrontmatter(content) { + const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content); + return match ? match[1] : ''; +} + +function parseYamlScalar(value) { + const trimmed = String(value || '').trim(); + if (!trimmed) return ''; + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + try { + return JSON.parse(trimmed); + } catch { + return trimmed.slice(1, -1); + } + } + if (trimmed.startsWith("'") && trimmed.endsWith("'")) { + return trimmed.slice(1, -1).replace(/''/g, "'"); + } + return trimmed; +} + +function parseFrontmatterString(frontmatter, key) { + const match = new RegExp(`^${key}:\\s*(.+)$`, 'm').exec(frontmatter); + return match ? parseYamlScalar(match[1]) : ''; +} + +function parseFrontmatterList(frontmatter, key) { + const lines = frontmatter.split(/\r?\n/); + const values = []; + let inList = false; + for (const line of lines) { + if (!inList) { + inList = new RegExp(`^${key}:\\s*$`).test(line); + continue; + } + if (!line.trim()) continue; + if (/^\S/.test(line)) break; + const item = /^\s+-\s*(.+?)\s*$/.exec(line); + if (item) values.push(parseYamlScalar(item[1])); + } + return values; +} + +function assignObjectField(object, text) { + const field = /^([A-Za-z][\w-]*):\s*(.*?)\s*$/.exec(text); + if (!field) return; + object[field[1]] = parseYamlScalar(field[2]); +} + +function parseFrontmatterObjectList(frontmatter, key, requiredFields = ['pattern', 'message']) { + const lines = frontmatter.split(/\r?\n/); + const values = []; + let current = null; + let inList = false; + for (const line of lines) { + if (!inList) { + inList = new RegExp(`^${key}:\\s*$`).test(line); + continue; + } + if (!line.trim()) continue; + if (/^\S/.test(line)) break; + const item = /^\s+-\s*(.*?)\s*$/.exec(line); + if (item) { + current = {}; + values.push(current); + if (item[1]) assignObjectField(current, item[1]); + continue; + } + if (current) assignObjectField(current, line.trim()); + } + return values.filter((value) => requiredFields.every((field) => value[field])); +} + +function parseSkillValidateRule(skillPath) { + const frontmatter = parseFrontmatter(readFileSync(skillPath, 'utf8')); + const skill = parseFrontmatterString(frontmatter, 'name'); + if (!skill) return null; + const validate = parseFrontmatterObjectList(frontmatter, 'validate'); + if (validate.length === 0) return null; + return { + skill, + pathPatterns: parseFrontmatterList(frontmatter, 'pathPatterns'), + validate, + }; +} + +export function loadSkillValidateRules(skillsDir = DEFAULT_SKILLS_DIR) { + if (skillsDir === DEFAULT_SKILLS_DIR && cachedRules) return cachedRules; + const rules = readdirSync(skillsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => join(skillsDir, entry.name, 'SKILL.md')) + .map((skillPath) => parseSkillValidateRule(skillPath)) + .filter(Boolean); + if (skillsDir === DEFAULT_SKILLS_DIR) cachedRules = rules; + return rules; +} + +function getToolName(payload) { + return String(payload?.tool_name || payload?.toolName || '').trim(); +} + +function getToolInput(payload) { + return payload?.tool_input || payload?.toolInput || {}; +} + +function getToolPath(toolInput) { + const raw = PATH_KEYS.map((key) => toolInput[key]).find((value) => typeof value === 'string'); + return String(raw || '').replace(/\\/g, '/'); +} + +function getToolWriteContent(payload) { + if (!WRITE_TOOL_NAMES.has(getToolName(payload))) return ''; + const toolInput = getToolInput(payload); + for (const key of WRITE_CONTENT_KEYS) { + if (typeof toolInput[key] === 'string') return toolInput[key]; + } + return ''; +} + +function findSkillForPath(filePath, rules) { + if (!filePath) return null; + for (const rule of rules) { + if (rule.pathPatterns.some((pattern) => globToRegExp(pattern).test(filePath))) return rule; + } + return null; +} + +function selectValidationMatches(content, rule) { + const seen = new Set(); + const matches = []; + for (const item of rule.validate) { + if (new RegExp(item.pattern).test(content) && !seen.has(item.message)) { + seen.add(item.message); + matches.push(item); + } + } + return matches; +} + +function renderValidationReminder(messages) { + return `Validation reminder:\n${messages.map((message) => `- ${message}`).join('\n')}`; +} + +export function buildValidateWriteOutput(payload, options = {}) { + if (!WRITE_TOOL_NAMES.has(getToolName(payload))) return null; + const content = getToolWriteContent(payload); + if (!content) return null; + + const rule = findSkillForPath( + getToolPath(getToolInput(payload)), + options.rules || loadSkillValidateRules(), + ); + if (!rule) return null; + + const matches = selectValidationMatches(content, rule); + if (matches.length === 0) return null; + + if (shouldWriteSignalLog(options)) { + for (const match of matches) { + writeSignalLog( + { + hook: 'PreToolUse', + trigger: 'validate', + matchedSkill: rule.skill, + reason: match.message, + toolName: getToolName(payload), + }, + options, + ); + } + } + + return { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: renderValidationReminder(matches.map((match) => match.message)), + }, + }; +} + +async function readStdin() { + let input = ''; + for await (const chunk of process.stdin) { + input += chunk; + } + return input; +} + +export async function main() { + const rawInput = await readStdin(); + const payload = rawInput.trim() ? JSON.parse(rawInput) : {}; + const output = buildValidateWriteOutput(payload, { enableSignalLog: true }); + if (!output) return; + process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + }); +} diff --git a/hooks/validate-write.test.mjs b/hooks/validate-write.test.mjs new file mode 100644 index 0000000..a01aefa --- /dev/null +++ b/hooks/validate-write.test.mjs @@ -0,0 +1,185 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import test from 'node:test'; + +import { buildValidateWriteOutput, loadSkillValidateRules } from './validate-write.mjs'; + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.1 loads validate rules from Skill frontmatter', async () => { + const rules = await loadSkillValidateRules(); + const edgeFunctions = rules.find((rule) => rule.skill === 'makers-edge-functions'); + + assert.ok(edgeFunctions); + assert.deepEqual(edgeFunctions.validate, [ + { + pattern: 'process\\.env', + message: 'Use context.env in EdgeOne Makers runtime code.', + }, + { + pattern: 'new\\s+Headers\\s*\\(', + message: 'Use plain object headers for this runtime surface.', + }, + { + pattern: 'fs\\.writeFile', + message: 'Edge Functions do not support filesystem writes.', + }, + ]); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.2 warns on Edit content without blocking writes', () => { + const output = buildValidateWriteOutput({ + tool_name: 'Edit', + tool_input: { + file_path: 'functions/index.ts', + new_string: 'export default () => process.env.API_KEY;', + }, + }); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Validation reminder:\n- Use context.env in EdgeOne Makers runtime code.', + }, + }); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.4 warns on Write content using new Headers', () => { + const output = buildValidateWriteOutput({ + tool_name: 'Write', + tool_input: { + file_path: 'functions/index.ts', + content: 'return new Response(body, { headers: new Headers() });', + }, + }); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Validation reminder:\n- Use plain object headers for this runtime surface.', + }, + }); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.5 warns on Edge Function filesystem writes', () => { + const output = buildValidateWriteOutput({ + tool_name: 'Write', + tool_input: { + file_path: 'functions/index.ts', + content: 'fs.writeFile("/tmp/out.txt", "data", () => {});', + }, + }); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Validation reminder:\n- Edge Functions do not support filesystem writes.', + }, + }); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 does not warn for read-only tool use', () => { + assert.equal( + buildValidateWriteOutput({ + tool_name: 'Read', + tool_input: { + file_path: 'functions/index.ts', + content: 'process.env.API_KEY', + }, + }), + null, + ); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 does not warn for frontend paths outside validate scope', () => { + assert.equal( + buildValidateWriteOutput({ + tool_name: 'Edit', + tool_input: { + file_path: 'src/components/Button.tsx', + new_string: 'export default () => process.env.API_KEY;', + }, + }), + null, + ); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 does not warn for skills without validate rules', () => { + assert.equal( + buildValidateWriteOutput({ + tool_name: 'Edit', + tool_input: { + file_path: 'agents/chat.ts', + new_string: 'const session = context.store.openaiSession(context.conversation_id);', + }, + }), + null, + ); +}); + +test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 supports CodeBuddy replace_in_file validate with new_str', () => { + const output = buildValidateWriteOutput({ + tool_name: 'replace_in_file', + tool_input: { + filePath: 'functions/index.ts', + new_str: 'export default () => process.env.API_KEY;', + }, + }); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Validation reminder:\n- Use context.env in EdgeOne Makers runtime code.', + }, + }); +}); + +test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 supports CodeBuddy write_to_file validate with filePath', () => { + const output = buildValidateWriteOutput({ + tool_name: 'write_to_file', + tool_input: { + filePath: 'functions/index.ts', + content: 'return new Response(body, { headers: new Headers() });', + }, + }); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Validation reminder:\n- Use plain object headers for this runtime surface.', + }, + }); +}); + +test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs validate matches with readable reasons', async () => { + const tmp = await mkdtemp(join(tmpdir(), 'makers-validate-log-')); + const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl'); + + try { + buildValidateWriteOutput( + { + tool_name: 'Write', + tool_input: { + file_path: 'functions/index.ts', + content: 'export default () => process.env.API_KEY;', + }, + }, + { + signalLogPath, + now: new Date('2026-07-03T00:00:00.000Z'), + }, + ); + + const [line] = (await readFile(signalLogPath, 'utf8')).trim().split('\n'); + assert.deepEqual(JSON.parse(line), { + timestamp: '2026-07-03T00:00:00.000Z', + hook: 'PreToolUse', + trigger: 'validate', + matchedSkill: 'makers-edge-functions', + reason: 'Use context.env in EdgeOne Makers runtime code.', + toolName: 'Write', + }); + } finally { + await rm(tmp, { recursive: true, force: true }); + } +}); diff --git a/skills/makers-agents/SKILL.md b/skills/makers-agents/SKILL.md index 261a7c2..21beca4 100644 --- a/skills/makers-agents/SKILL.md +++ b/skills/makers-agents/SKILL.md @@ -15,12 +15,6 @@ description: >- Do NOT trigger for deployment workflows (use edgeone-pages-deploy). Do NOT trigger for generic AI framework development outside an EdgeOne Makers project. -pathPatterns: - - agents/** -chainTo: - - pattern: "\\bKV\\b|context\\.store" - skill: makers-storage - reason: "Code references KV or store APIs." metadata: author: edgeone version: "1.0.0" @@ -44,6 +38,8 @@ This skill covers five supported frameworks (DeepAgents, LangGraph, CrewAI, Open - Calling sandbox or platform tools via `context.sandbox` / `context.tools` - Splitting AI inference (`agents/`) from data CRUD (`cloud-functions/`) +> Cross-reference: if your code uses `context.store` or KV APIs, also read `skills/makers-storage/SKILL.md`. + **Do NOT use for:** - Plain Edge Functions / Cloud Functions / Middleware → use `edgeone-pages-dev` - Deployment workflows → use `edgeone-pages-deploy` diff --git a/skills/makers-cli/SKILL.md b/skills/makers-cli/SKILL.md index 45f3ea5..7e50492 100644 --- a/skills/makers-cli/SKILL.md +++ b/skills/makers-cli/SKILL.md @@ -3,8 +3,6 @@ name: makers-cli description: >- EdgeOne Makers CLI command reference. Use when running edgeone CLI commands for dev, build, deploy, env management. -bashPatterns: - - "\\bedgeone\\s+" metadata: author: edgeone version: "1.0.0" diff --git a/skills/makers-cloud-functions/SKILL.md b/skills/makers-cloud-functions/SKILL.md index f9f8533..26fac3d 100644 --- a/skills/makers-cloud-functions/SKILL.md +++ b/skills/makers-cloud-functions/SKILL.md @@ -3,8 +3,6 @@ name: makers-cloud-functions description: >- EdgeOne Makers Cloud Functions — Node.js, Go, and Python runtimes. Use when building server-side APIs, Express/Koa patterns, or backend logic. -pathPatterns: - - cloud-functions/** metadata: author: edgeone version: "1.0.0" diff --git a/skills/makers-deploy/SKILL.md b/skills/makers-deploy/SKILL.md index d4ebae5..890eda9 100644 --- a/skills/makers-deploy/SKILL.md +++ b/skills/makers-deploy/SKILL.md @@ -12,11 +12,6 @@ description: >- commands — the skill contains critical rules for parsing deploy output and presenting access URLs. Do NOT trigger for post-deployment runtime errors (e.g. CORS issues, 500 errors after deploy — use edgeone-makers-dev for troubleshooting). -pathPatterns: - - edgeone.json -bashPatterns: - - "\\bedgeone\\s+pages\\s+deploy\\b" - - "\\bedgeone\\s+makers\\s+deploy\\b" metadata: author: edgeone version: "2.1.0" diff --git a/skills/makers-edge-functions/SKILL.md b/skills/makers-edge-functions/SKILL.md index 2510c5f..633cebc 100644 --- a/skills/makers-edge-functions/SKILL.md +++ b/skills/makers-edge-functions/SKILL.md @@ -12,10 +12,6 @@ validate: message: "Use plain object headers for this runtime surface." - pattern: "fs\\.writeFile" message: "Edge Functions do not support filesystem writes." -chainTo: - - pattern: "\\bKV\\b|context\\.store" - skill: makers-storage - reason: "Code references KV or store APIs." metadata: author: edgeone version: "1.0.0" @@ -111,6 +107,8 @@ export function onRequest(context) { ## KV Storage (Edge Functions only) +> Cross-reference: if your code uses `context.store` or KV APIs, also read `skills/makers-storage/SKILL.md`. + ⚠️ **Prerequisites**: You must enable KV Storage in the EdgeOne Makers console, create a namespace, and bind it to your project before using KV. See [kv-storage.md](kv-storage.md) for full setup instructions (same directory). The KV namespace is a **global variable** (name is set when binding in the console) — it is **NOT** on `context.env`. diff --git a/skills/makers-middleware/SKILL.md b/skills/makers-middleware/SKILL.md index 218f91d..751cc3d 100644 --- a/skills/makers-middleware/SKILL.md +++ b/skills/makers-middleware/SKILL.md @@ -3,9 +3,6 @@ name: edgeone-makers-middleware description: >- Edge middleware for EdgeOne Makers — request interception, redirects, rewrites, auth guards, A/B testing, and header injection at the edge (V8 runtime). -pathPatterns: - - middleware.* - - middleware/** metadata: author: edgeone version: "1.0.0" From bd50148041f030514e952fae89863f3cb16222d2 Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Fri, 3 Jul 2026 16:27:10 +0800 Subject: [PATCH 4/9] fix(hooks): correct edge-functions pathPatterns + drop legacy sh - makers-edge-functions pathPatterns: add edge-functions/** (SKILL.md prose uses edge-functions/, old functions/** never matched) - delete unreferenced on-agent-write.sh, on-edgeone-cmd.sh (superseded by validate-write) - add edge-functions path test case Co-authored-by: Cursor --- hooks/on-agent-write.sh | 10 ---------- hooks/on-edgeone-cmd.sh | 18 ------------------ hooks/validate-write.test.mjs | 17 +++++++++++++++++ skills/makers-edge-functions/SKILL.md | 1 + 4 files changed, 18 insertions(+), 28 deletions(-) delete mode 100644 hooks/on-agent-write.sh delete mode 100644 hooks/on-edgeone-cmd.sh diff --git a/hooks/on-agent-write.sh b/hooks/on-agent-write.sh deleted file mode 100644 index db9f3df..0000000 --- a/hooks/on-agent-write.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# PreToolUse hook: when writing to agents/ directory, remind about platform rules -set -euo pipefail - -jq -n '{ - hookSpecificOutput: { - hookEventName: "PreToolUse", - additionalContext: "Writing to agents/ directory. Ensure: 1) Entry is `export async function onRequest(context)` (Node) or `async def handler(ctx)` (Python). 2) Use context.env, never process.env/os.environ. 3) Use context.conversation_id directly. 4) For tools: Claude SDK uses toClaudeMcpServer(), OpenAI Agents uses all(), LangGraph/DeepAgents uses toLangChainTools(tool), CrewAI uses toCrewAITools(BaseTool)." - } -}' diff --git a/hooks/on-edgeone-cmd.sh b/hooks/on-edgeone-cmd.sh deleted file mode 100644 index 5d6cc41..0000000 --- a/hooks/on-edgeone-cmd.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# PreToolUse hook: when running edgeone CLI commands, ensure PAGES_SOURCE is set -set -euo pipefail - -INPUT=$(cat) -COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""') - -# Check if PAGES_SOURCE is missing from the command -if echo "$COMMAND" | grep -q "edgeone" && ! echo "$COMMAND" | grep -q "PAGES_SOURCE"; then - jq -n '{ - hookSpecificOutput: { - hookEventName: "PreToolUse", - additionalContext: "Running edgeone CLI command. Ensure PAGES_SOURCE=skills is set (export PAGES_SOURCE=skills or prefix the command)." - } - }' -else - exit 0 -fi diff --git a/hooks/validate-write.test.mjs b/hooks/validate-write.test.mjs index a01aefa..0ec8567 100644 --- a/hooks/validate-write.test.mjs +++ b/hooks/validate-write.test.mjs @@ -117,6 +117,23 @@ test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 does not warn for ); }); +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 matches edge-functions path after pathPatterns fix', () => { + const output = buildValidateWriteOutput({ + tool_name: 'Write', + tool_input: { + file_path: 'edge-functions/api/hello.js', + content: 'export default () => process.env.API_KEY;', + }, + }); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: 'Validation reminder:\n- Use context.env in EdgeOne Makers runtime code.', + }, + }); +}); + test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 supports CodeBuddy replace_in_file validate with new_str', () => { const output = buildValidateWriteOutput({ tool_name: 'replace_in_file', diff --git a/skills/makers-edge-functions/SKILL.md b/skills/makers-edge-functions/SKILL.md index 633cebc..7345171 100644 --- a/skills/makers-edge-functions/SKILL.md +++ b/skills/makers-edge-functions/SKILL.md @@ -4,6 +4,7 @@ description: >- V8-based lightweight edge functions on EdgeOne Makers. Covers routing, KV storage access, request/response handling, and environment variables at the edge. pathPatterns: + - edge-functions/** - functions/** validate: - pattern: "process\\.env" From aab6ebefde10d1aaee6a642f46f5d92cc28fa4dd Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Fri, 3 Jul 2026 16:45:24 +0800 Subject: [PATCH 5/9] feat(plugin): add claude-code marketplace.json + plugin.json - add .claude-plugin/marketplace.json (name/owner/plugins, source: ./) - migrate manifest.json -> plugin.json (hooks: ./hooks/hooks.json, author object) - enables /plugin marketplace add + /plugin install edgeone-makers@edgeone-makers - README: document Claude Code marketplace install Co-authored-by: Cursor --- .claude-plugin/marketplace.json | 20 +++++++++++++++++++ .claude-plugin/{manifest.json => plugin.json} | 6 ++++-- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .claude-plugin/marketplace.json rename .claude-plugin/{manifest.json => plugin.json} (74%) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..35aed42 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "edgeone-makers", + "owner": { + "name": "EdgeOne" + }, + "description": "EdgeOne Makers platform development skills marketplace.", + "plugins": [ + { + "name": "edgeone-makers", + "source": "./", + "description": "EdgeOne Makers platform development skills — AI Agents, Cloud Functions, Edge Functions, Storage, Middleware, and Deployment.", + "version": "1.0.0", + "author": { + "name": "EdgeOne" + }, + "category": "edge-one", + "tags": ["edgeone", "makers", "edge-functions", "cloud-functions", "agents", "deployment"] + } + ] +} diff --git a/.claude-plugin/manifest.json b/.claude-plugin/plugin.json similarity index 74% rename from .claude-plugin/manifest.json rename to .claude-plugin/plugin.json index 39d81d2..a125659 100644 --- a/.claude-plugin/manifest.json +++ b/.claude-plugin/plugin.json @@ -2,6 +2,8 @@ "name": "edgeone-makers-tools", "description": "EdgeOne Makers platform development skills — AI Agents, Cloud Functions, Edge Functions, Storage, Middleware, and Deployment.", "version": "1.0.0", - "author": "EdgeOne", - "hooks": "../hooks/hooks.json" + "author": { + "name": "EdgeOne" + }, + "hooks": "./hooks/hooks.json" } From 18a1d0698debac13189de2214db68ef941635c97 Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Fri, 3 Jul 2026 18:05:32 +0800 Subject: [PATCH 6/9] feat(plugin): add codebuddy marketplace.json for distribution - .codebuddy-plugin/marketplace.json: standard name (edgeone-makers) for public install - codebuddy installs via plugin marketplace like Claude Code Co-authored-by: Cursor --- .codebuddy-plugin/marketplace.json | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .codebuddy-plugin/marketplace.json diff --git a/.codebuddy-plugin/marketplace.json b/.codebuddy-plugin/marketplace.json new file mode 100644 index 0000000..62fce61 --- /dev/null +++ b/.codebuddy-plugin/marketplace.json @@ -0,0 +1,30 @@ +{ + "name": "edgeone-makers", + "owner": { + "name": "EdgeOne" + }, + "description": "EdgeOne Makers platform development skills marketplace.", + "plugins": [ + { + "name": "edgeone-makers", + "source": "./", + "description": "EdgeOne Makers platform development skills — AI Agents, Cloud Functions, Edge Functions, Storage, Middleware, and Deployment.", + "version": "1.0.0", + "author": { + "name": "EdgeOne" + }, + "strict": false, + "skills": [ + "./skills/makers-agents", + "./skills/makers-deploy", + "./skills/makers-edge-functions", + "./skills/makers-cloud-functions", + "./skills/makers-storage", + "./skills/makers-middleware", + "./skills/makers-cli", + "./skills/makers-recipes" + ], + "hooks": "./hooks/hooks.json" + } + ] +} From b2973d5b9516d32e02f9f69ab57209954ac725bf Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Mon, 6 Jul 2026 11:08:16 +0800 Subject: [PATCH 7/9] feat(plugin): migrate .cursor-plugin manifest.json -> plugin.json - align with Cursor official plugin manifest (name/description/version/author object) - skills/hooks auto-discovered from repo root default dirs - drop explicit skills list (default scan skills/) Co-authored-by: Cursor --- .cursor-plugin/manifest.json | 16 ---------------- .cursor-plugin/plugin.json | 8 ++++++++ 2 files changed, 8 insertions(+), 16 deletions(-) delete mode 100644 .cursor-plugin/manifest.json create mode 100644 .cursor-plugin/plugin.json diff --git a/.cursor-plugin/manifest.json b/.cursor-plugin/manifest.json deleted file mode 100644 index 9e7ad70..0000000 --- a/.cursor-plugin/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "edgeone-makers-tools", - "description": "EdgeOne Makers platform development skills — AI Agents, Cloud Functions, Edge Functions, Storage, Middleware, and Deployment.", - "version": "1.0.0", - "author": "EdgeOne", - "skills": [ - "skills/makers-agents", - "skills/makers-deploy", - "skills/makers-edge-functions", - "skills/makers-cloud-functions", - "skills/makers-storage", - "skills/makers-middleware", - "skills/makers-cli", - "skills/makers-recipes" - ] -} diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json new file mode 100644 index 0000000..67296b5 --- /dev/null +++ b/.cursor-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "edgeone-makers-tools", + "description": "EdgeOne Makers platform development skills — AI Agents, Cloud Functions, Edge Functions, Storage, Middleware, and Deployment.", + "version": "1.0.0", + "author": { + "name": "EdgeOne" + } +} From 2f442900cd26abc915322a1076a78fdae289d96e Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Tue, 7 Jul 2026 17:06:00 +0800 Subject: [PATCH 8/9] fix: adapt to main rename (edgeone-makers-* skills, edgeone-makers-tools plugin) - validate-write.test: expect edgeone-makers-edge-functions (main renamed skills with edgeone- prefix) - marketplace.json (.claude/.codebuddy): plugins[].name edgeone-makers -> edgeone-makers-tools (match plugin.json name) Co-authored-by: Cursor --- .claude-plugin/marketplace.json | 2 +- .codebuddy-plugin/marketplace.json | 2 +- hooks/validate-write.test.mjs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 35aed42..d89207d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ "description": "EdgeOne Makers platform development skills marketplace.", "plugins": [ { - "name": "edgeone-makers", + "name": "edgeone-makers-tools", "source": "./", "description": "EdgeOne Makers platform development skills — AI Agents, Cloud Functions, Edge Functions, Storage, Middleware, and Deployment.", "version": "1.0.0", diff --git a/.codebuddy-plugin/marketplace.json b/.codebuddy-plugin/marketplace.json index 62fce61..b0d7768 100644 --- a/.codebuddy-plugin/marketplace.json +++ b/.codebuddy-plugin/marketplace.json @@ -6,7 +6,7 @@ "description": "EdgeOne Makers platform development skills marketplace.", "plugins": [ { - "name": "edgeone-makers", + "name": "edgeone-makers-tools", "source": "./", "description": "EdgeOne Makers platform development skills — AI Agents, Cloud Functions, Edge Functions, Storage, Middleware, and Deployment.", "version": "1.0.0", diff --git a/hooks/validate-write.test.mjs b/hooks/validate-write.test.mjs index 0ec8567..3c6bfe0 100644 --- a/hooks/validate-write.test.mjs +++ b/hooks/validate-write.test.mjs @@ -8,7 +8,7 @@ import { buildValidateWriteOutput, loadSkillValidateRules } from './validate-wri test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.1 loads validate rules from Skill frontmatter', async () => { const rules = await loadSkillValidateRules(); - const edgeFunctions = rules.find((rule) => rule.skill === 'makers-edge-functions'); + const edgeFunctions = rules.find((rule) => rule.skill === 'edgeone-makers-edge-functions'); assert.ok(edgeFunctions); assert.deepEqual(edgeFunctions.validate, [ @@ -192,7 +192,7 @@ test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs validate matches timestamp: '2026-07-03T00:00:00.000Z', hook: 'PreToolUse', trigger: 'validate', - matchedSkill: 'makers-edge-functions', + matchedSkill: 'edgeone-makers-edge-functions', reason: 'Use context.env in EdgeOne Makers runtime code.', toolName: 'Write', }); From 809ef8ce5629cb4908a25bd6963d77571fe28539 Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Wed, 8 Jul 2026 16:34:00 +0800 Subject: [PATCH 9/9] docs(skills): drop dev/package-scripts guidance; remove docs+features from PR review #5 feedback: - drop duplicated dev/package.json scripts guidance across skills (CLI handles compat) - remove vite examples that could mislead AI into wrapping static projects - keep CLI command refs (makers dev/build) and preview rules; untouched - restore Claude Code marketplace install option in README - remove docs/plugin-hooks-vs-context-and-tools.md and features/.../feature.yaml from PR (reviewer: docs should not be submitted) Co-authored-by: Cursor --- README.md | 7 + docs/plugin-hooks-vs-context-and-tools.md | 292 ------------------ ...-skill-injection-optimization.feature.yaml | 129 -------- skills/makers-agents/SKILL.md | 32 +- .../references/node-frameworks/claude-sdk.md | 2 - .../references/node-frameworks/deepagents.md | 2 +- .../references/node-frameworks/langgraph.md | 2 - .../node-frameworks/openai-agents.md | 2 - .../python-frameworks/claude-sdk.md | 2 - .../references/python-frameworks/crewai.md | 2 - .../python-frameworks/deepagents.md | 2 +- .../references/python-frameworks/langgraph.md | 2 - .../python-frameworks/openai-agents.md | 2 - .../references/review-checklist.md | 4 +- skills/makers-cli/SKILL.md | 23 -- skills/makers-recipes/SKILL.md | 18 -- 16 files changed, 11 insertions(+), 512 deletions(-) delete mode 100644 docs/plugin-hooks-vs-context-and-tools.md delete mode 100644 features/edgeone-makers-tools/plugin-skill-injection-optimization.feature.yaml diff --git a/README.md b/README.md index ec46f32..2808053 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,13 @@ internally). > [`BRANCH.md`](https://github.com/TencentEdgeOne/edgeone-makers-tools/blob/skillhub/BRANCH.md) > on the `skillhub` branch for the maintenance flow. +### Option C — Claude Code plugin marketplace + +```text +/plugin marketplace add TencentEdgeOne/edgeone-makers-tools +/plugin install edgeone-makers-tools@edgeone-makers +``` + After installation, your AI coding agent will automatically detect relevant tasks and load the right skill. ## Skills diff --git a/docs/plugin-hooks-vs-context-and-tools.md b/docs/plugin-hooks-vs-context-and-tools.md deleted file mode 100644 index 7b46799..0000000 --- a/docs/plugin-hooks-vs-context-and-tools.md +++ /dev/null @@ -1,292 +0,0 @@ -# Plugin Hooks vs Context-and-Tools 对比分析 - -> 日期:2026-06-30 -> 对比项目:edgeone-makers-tools (当前) vs context-and-tools (Netlify 参考实现) - ---- - -## 一、两个项目的架构概览 - -### Netlify context-and-tools - -- **定位**:多平台 AI 技能分发系统(Claude Code / Cursor / Codex / Grok / Gemini) -- **核心理念**:纯静态内容 + AI 自主路由,零运行时代码 -- **Plugin manifest**:声明式 `plugin.json`,无 hooks 字段 -- **技能触发**:依赖 CLAUDE.md 路由表 → AI 自主选择加载哪个 SKILL.md -- **运行时代码**:无(只有构建脚本生成各平台格式) - -``` -skills/CLAUDE.md (路由表) → AI 阅读后自主选择 → 读取对应 SKILL.md -``` - -**关键文件:** -``` -.claude-plugin/ -├── plugin.json # 纯元信息:name, version, description, author, repository -└── marketplace.json # 市场注册配置 -skills/ -├── CLAUDE.md # 路由决策表(给 AI 读的) -└── netlify-*/SKILL.md # 13 个技能(YAML frontmatter 只有 name + description) -``` - -### EdgeOne edgeone-makers-tools (当前) - -- **定位**:类似定位,多平台技能分发 -- **核心理念**:Hooks 主动注入 + AI 被动执行,运行时代码辅助路由 -- **Plugin manifest**:`manifest.json` 中绑定 hooks -- **技能触发**:3 层 hooks 逐步注入(SessionStart → UserPromptSubmit → PreToolUse) -- **运行时代码**:~700 行 JS + 状态文件 + 信号日志 - -``` -hooks(拦截事件) → 正则匹配 → 强制注入 "You must run Skill(xxx)" → AI 被动执行 -``` - -**关键文件:** -``` -.claude-plugin/ -└── manifest.json # 含 "hooks": "../hooks/hooks.json" -hooks/ -├── hooks.json # 3 个事件 hook 定义 -├── sessionstart-minimal-context.mjs # SessionStart: 注入启动提示 -├── userprompt-skill-inject.mjs # UserPromptSubmit: 对 prompt 评分选 skill -├── pretooluse-skill-inject.mjs # PreToolUse: 路径/命令匹配 + validate + chainTo -└── signal-log.mjs # 诊断日志 -skills/ -├── makers-*/SKILL.md # 8 个技能(frontmatter 含 pathPatterns/bashPatterns/validate/chainTo) -``` - ---- - -## 二、核心差异对比表 - -| 维度 | context-and-tools (Netlify) | edgeone-makers-tools (当前) | -|------|------|------| -| Plugin manifest | 纯声明式,无 hooks 字段 | manifest 绑定 hooks.json | -| 技能触发方式 | AI 读 CLAUDE.md 自主选择 | Hooks 代码强制注入指令 | -| 运行时代码 | 0 行 | ~700 行 JS | -| 路由决策者 | AI 自身 | Hooks 代码 | -| 状态管理 | 无 | .edgeone/pretooluse-injected-skills.json | -| 验证/链式触发 | 无 | frontmatter validate/chainTo 字段 | -| SKILL.md frontmatter | 仅 name + description | name + description + pathPatterns + bashPatterns + validate + chainTo + metadata | -| 多平台分发 | 纯构建脚本 | 构建脚本 + 运行时 hooks | -| 维护成本 | 极低 | 较高(hooks 逻辑 + 测试 + 状态文件) | - ---- - -## 三、PreToolUse 阶段详细拆解 - -`pretooluse-skill-inject.mjs` 在 AI 每次调用 Read/Edit/Write/Bash 工具之前被触发,执行 4 段逻辑: - -### 逻辑 1:Skill 匹配(路由注入) - -**流程:** -``` -AI 要 Read("functions/api/hello.ts") -→ hooks 解析路径,匹配 pathPatterns: ["functions/**"] -→ 去重检查(是否已注入过) -→ 输出: "You must run the Skill(makers-edge-functions) tool." -``` - -**实现要素:** -- `matchPathRule()` — 路径 glob 匹配 -- `matchBashRule()` — 命令正则匹配 -- `pickMostSpecificMatch()` — 多匹配时选最长 pattern(最具体) -- 工具类型分发:Bash 类工具走 bash 匹配,Read/Edit/Write 走路径匹配 - -**价值判断:低 / 可删除** -- CLAUDE.md 路由表已经让 AI 有能力自主选择 -- AI 在写 `functions/` 下的代码时大概率已经加载过对应 skill -- Netlify 没有这层逻辑,完全靠 AI 自觉,验证了此路径可行 - ---- - -### 逻辑 2:去重状态管理 - -**流程:** -``` -已注入 Set(["makers-edge-functions"]) -→ 第二次触发同 skill 时跳过注入 -→ 持久化到 .edgeone/pretooluse-injected-skills.json -``` - -**实现要素:** -- `readInjectedSkills()` / `writeInjectedSkills()` — 文件 I/O -- `persistInjectedSkills()` — 仅在集合变化时写盘 -- JSON 格式:`{ "injectedSkills": ["makers-agents", "makers-edge-functions"] }` - -**价值判断:纯辅助逻辑** -- 如果去掉逻辑 1 的强制注入,去重也没有存在意义 - ---- - -### 逻辑 3:Validate(代码写入验证)⭐ - -**流程:** -``` -AI 要 Edit("functions/api/hello.ts", new_string: "process.env.API_KEY") -→ hooks 提取写入内容(new_string / content / text 等字段) -→ 对内容运行 validate 规则的正则匹配 -→ 输出追加: "Validation reminder:\n- Use context.env in EdgeOne Makers runtime code." -``` - -**当前定义的全部 validate 规则(仅 makers-edge-functions 有):** - -| Pattern | 提醒内容 | 场景 | -|---------|---------|------| -| `process\.env` | "Use context.env in EdgeOne Makers runtime code." | Edge Function 不能用 Node.js 环境变量 | -| `new\s+Headers\s*\(` | "Use plain object headers for this runtime surface." | Edge Function 的 Headers 构造方式不同 | -| `fs\.writeFile` | "Edge Functions do not support filesystem writes." | Edge Function 无文件系统写入能力 | - -**实现要素:** -- `getToolWriteContent()` — 从 payload 提取写入内容(兼容多种工具字段名) -- `selectValidationMatches()` — 对写入内容逐条运行 validate 正则 -- `renderValidationReminder()` — 格式化为 "Validation reminder:\n- ..." 输出 - -**价值判断:这是唯一有独特价值的逻辑** -- SKILL.md 里虽然写了 "不要用 process.env",但 AI 不一定每次都记得 -- 这是**写入时**的最后一道防线 — 静态文档是事前引导,这是事中拦截 -- 类似于 IDE 实时 lint 红线提醒 -- 触发时机精确:只在 Edit/Write 工具调用时触发,只检查写入内容 - ---- - -### 逻辑 4:ChainTo(跨 skill 关联触发) - -**流程:** -``` -AI 要 Edit("functions/api/store.ts", new_string: "context.store.get(...)") -→ hooks 检测写入内容匹配 chainTo pattern: "\\bKV\\b|context\\.store" -→ 如果 makers-storage 尚未注入: -→ 输出追加: "You must run the Skill(makers-storage) tool." -``` - -**当前定义的全部 chainTo 规则:** - -| 所属 Skill | 触发条件 | 链向 | 理由 | -|-----------|---------|------|------| -| makers-edge-functions | `\bKV\b\|context\.store` | makers-storage | Code references KV or store APIs. | -| makers-agents | `\bKV\b\|context\.store` | makers-storage | Code references KV or store APIs. | - -**价值判断:中等 / 可用静态方式替代** -- 在 SKILL.md 正文中写 "If using `context.store`, also read `makers-storage/SKILL.md`" 即可 -- AI 能理解交叉引用文本 - ---- - -## 四、"独立 lint hook" 的含义 - -当前 PreToolUse 是一个大杂烩 — 同一个 457 行的 JS 文件混合了: -- skill 路由注入(可删) -- 去重管理(可删) -- validate 验证(有价值) -- chainTo 关联(可用文档替代) - -"独立 lint hook" = **只保留 validate 逻辑,剥离成一个纯粹的代码质量检查 hook**,与 skill 注入完全解耦。 - -### 方案 A:极简 PreToolUse hook(推荐) - -保留一个 hook,但只做 validate: - -```json -// hooks/hooks.json -{ - "hooks": { - "PreToolUse": [{ - "matcher": "Edit|Write|replace_in_file|write_to_file", - "hooks": [{ - "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/validate-write.mjs\"", - "timeout": 3 - }] - }] - } -} -``` - -`validate-write.mjs` 约 50 行,只做: -1. 从 stdin 读取 payload -2. 根据文件路径确定所属 skill -3. 对写入内容运行该 skill 的 validate 正则 -4. 如果匹配,输出 "Validation reminder: ..." -5. 无状态文件、无 skill 注入、无去重逻辑 - -**优势:** -- 保留事中拦截的独特价值 -- 代码量 457 行 → ~50 行 -- 无状态文件(`.edgeone/` 目录可完全删除) -- 只在写入工具触发,不拦截 Read / Bash - -### 方案 B:完全去掉 hooks,约束写入 SKILL.md 正文 - -在 `makers-edge-functions/SKILL.md` 正文中强调: - -```markdown -## Critical Constraints (NEVER violate) - -- `process.env` → use `context.env` instead -- `new Headers()` → use plain object `{ "Content-Type": "..." }` -- `fs.writeFile` or any filesystem write → Edge Functions have no writable FS -``` - -**优势:** 零运行时代码,完全对齐 Netlify 方案 -**风险:** AI 可能在长对话中遗忘约束,写出违规代码时没有拦截 - ---- - -## 五、迁移建议总结 - -| 当前逻辑 | 处理方式 | 理由 | -|---------|---------|------| -| SessionStart hook | 删除 | CLAUDE.md 已作为项目指令自动加载 | -| UserPromptSubmit hook | 删除 | AI 读 CLAUDE.md 路由表后能自主判断 | -| PreToolUse: skill 路由注入 | 删除 | CLAUDE.md 路由表已够用 | -| PreToolUse: 去重状态管理 | 删除 | 路由注入删了就没必要了 | -| **PreToolUse: validate** | **保留(精简为独立 hook)** | 事中拦截有独特价值 | -| PreToolUse: chainTo | 删除,移到 SKILL.md 正文 | 文本交叉引用足够 | -| signal-log.mjs | 可选保留 | validate hook 中可保留诊断日志 | - -**目标架构:** -``` -CLAUDE.md (路由表) → AI 主动选择 skill → [可选] validate hook 做写入检查 -``` - -**预期收益:** -- 运行时代码:~700 行 → ~50 行 -- 状态文件:删除 `.edgeone/` 目录 -- hooks 事件:3 个 → 1 个(仅 PreToolUse on write tools) -- SKILL.md frontmatter:可移除 pathPatterns / bashPatterns / chainTo(仅保留 name + description + validate) -- 与 Netlify 最佳实践对齐,降低维护成本 - ---- - -## 六、Netlify 关键实现参考 - -### plugin.json(无 hooks) -```json -{ - "name": "netlify-skills", - "version": "1.1.0", - "description": "Netlify platform skills for Claude Code", - "author": { "name": "Netlify" }, - "repository": "https://github.com/netlify/context-and-tools" -} -``` - -### SKILL.md frontmatter(仅 name + description) -```yaml ---- -name: netlify-functions -description: Guide for writing Netlify serverless functions. Use when creating API endpoints... ---- -``` - -### 路由方式(CLAUDE.md 纯文本引导) -```markdown -**Building API endpoints or server-side logic?** -Read `netlify-functions/SKILL.md` for modern function syntax... - -**Need low-latency middleware?** -Read `netlify-edge-functions/SKILL.md` for edge compute patterns. -``` - -无代码、无 hooks、无状态 — 完全信赖 AI 的理解和判断能力。 diff --git a/features/edgeone-makers-tools/plugin-skill-injection-optimization.feature.yaml b/features/edgeone-makers-tools/plugin-skill-injection-optimization.feature.yaml deleted file mode 100644 index 324d0d8..0000000 --- a/features/edgeone-makers-tools/plugin-skill-injection-optimization.feature.yaml +++ /dev/null @@ -1,129 +0,0 @@ -feature: - name: plugin-skill-injection-optimization - product: edgeone-makers-tools - description: 优化 EdgeOne Makers Tools 插件的 Skill 注入、Hook 路由、平台降级和信号观测,减少上下文占用并提高 Skill 触发准确性。【2026-07-03 决策:采纳 docs/plugin-hooks-vs-context-and-tools.md 方案A,废弃 #6 UserPromptSubmit/#7 SessionStart/#9 pathPatterns+bashPatterns/#12 chainTo 的运行时注入;#8 路由注入+去重废弃(validate 保留见#10);#11 路由降级废弃(写入工具名兼容保留);#10 validate 精简为独立 hook 保留;#13 signal-log 保留供诊断;AI 改由 CLAUDE.md 路由表自主选 skill】 - -components: - INSTRUCTION_INJECTION: - description: "#6 Plugin 改为指令注入模式。【DEPRECATED 2026-07-03:方案A,UserPromptSubmit hook 整体废弃,AI 改由 CLAUDE.md 路由表自主选 skill】" - requirements: - 1: 命中 Skill 后,插件只向 `additionalContext` 注入加载该 Skill 的短指令,不注入 `SKILL.md` 全文。 - 1-note: 2026-06-24 初查当前仓库时,`hooks/on-prompt.sh` 未读取 `SKILL.md` 全文,而是硬编码注入一段读取 `skills/makers-agents/SKILL.md` 的说明;实现前需要确认全文注入来自外部打包流程、旧版本脚本,还是待迁移设计。 - 2: 插件保留现有提示词评分和 Skill 选择机制,输出格式变化不改变命中结果。 - 2-note: 2026-06-24 讨论确认:#6 的目标定义为统一所有命中 Skill 的输出为平台化加载指令;旧版全文注入只作为背景假设保留。 - 2-note-implementation: 2026-06-24 实现选择:新增 `hooks/userprompt-skill-inject.mjs` 承载提示词评分、平台检测和指令生成;`hooks/on-prompt.sh` 仅定位插件根目录并转发 stdin,便于后续 #8/#9/#12 复用。 - 2-note-trial: 2026-06-24 试改已加入 `selectSkillForPrompt`、`detectPlatform`、`renderSkillInstruction` 和 `buildHookOutput`;当前评分规则仍是脚本内静态规则,后续 #9 可迁移为读取 Skill frontmatter。 - 3: Claude Code 平台的注入指令格式为 `You must run the Skill() tool.`。 - 4: Cursor 平台的注入指令格式为 `Load the / skill.`。 - 5: 指令注入路径不再执行 10KB 预算裁剪。 - 6: 验证用例必须证明触发 Skill 时上下文只包含加载指令,且 Agent 能继续加载正确 Skill 内容。 - 6-note-verified: 2026-06-24 已运行 `node --test hooks/userprompt-skill-inject.test.mjs`,退出码为 0;Hook 样例验证部署 prompt 输出 `You must run the Skill(makers-deploy) tool.`,Cursor Edge Function prompt 输出 `Load the /makers-edge-functions skill.`。 - 7: 不支持 Skill tool 的平台不在 #6 中降级注入全文,相关 Push 模式 fallback 由 #11 覆盖。 - - MINIMAL_SESSION_START: - description: "#7 SessionStart 极简化。【DEPRECATED 2026-07-03:方案A,SessionStart hook 删除,CLAUDE.md 已作为项目指令自动加载】" - requirements: - 1: `SessionStart` 只注入少量行为原则和 Skill 路由提示,不注入知识图谱全文。 - 2: 合并后的 `CLAUDE.md` 作为 Agent 可读取的 Skill 路由表。 - 3: - requirement: profiler 逻辑继续扫描项目设置并设置环境变量。 - deprecated: true - reason: 2026-06-24 用户确认 profiler 属于旧插件仓库,新仓库没有该逻辑;#7 按无 profiler 的新仓库实现。 - 4: 验证用例必须记录优化后新会话的上下文占用下降。 - 5: 新仓库的 `SessionStart` 实现不新增或模拟 profiler 入口。 - 5-note-implementation: 2026-06-24 已新增 `hooks/sessionstart-minimal-context.mjs`,并通过 `hooks/on-session-start.sh` 和 `hooks/hooks.json` 注册到 `SessionStart`;输出内容保持为 5 行路由原则,不读取 `SKILL.md`、`references/` 或 profiler 数据。 - 5-note-verified: 2026-06-24 已运行 `node --test hooks/sessionstart-minimal-context.test.mjs`,覆盖上下文行数、`CLAUDE.md` / `AGENTS.md` 路由提示、禁止启动时加载全部 Skills,以及不包含 DeepAgents、`context.store`、`PAGES_SOURCE`、profiler 等知识图谱内容。 - - PRETOOLUSE_HOOK: - description: "#8 新增 PreToolUse Hook。【2026-07-03 部分废弃:路由注入+去重删除;PreToolUse hook 精简为仅 validate,拆为独立 hooks/validate-write.mjs(见#10)】" - requirements: - 1: 插件在 `hooks.json` 中注册 `PreToolUse` hook。 - 2: 插件提供 `pretooluse-skill-inject.mjs`,并从 stdin 解析 `tool_name` 和 `tool_input`。 - 3: `Read`、`Edit`、`Write` 工具调用根据文件路径匹配 Skill 的 `pathPatterns`。 - 4: `Bash` 工具调用根据命令字符串匹配 Skill 的 `bashPatterns`。 - 5: 命中 Skill 后输出对应平台的 Skill 加载指令。 - 6: 同一会话中已经注入过的 Skill 不重复注入。 - 7: 验证用例必须覆盖读取 `functions/index.ts` 触发 `makers-edge-functions`。 - 8: 验证用例必须覆盖执行 `edgeone pages deploy` 触发 `makers-deploy`。 - 8-note-implementation: 2026-06-24 初始实现新增 `hooks/pretooluse-skill-inject.mjs` 和对应测试;路径与命令规则暂时内置在脚本中,后续 #9 再迁移到 Skill frontmatter 的 `pathPatterns` / `bashPatterns`。 - 8-note-verified: 2026-06-24 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,并通过 stdin smoke test 验证 `Read functions/index.ts` 输出 `makers-edge-functions` 加载指令、`Bash edgeone pages deploy` 输出 `makers-deploy` 加载指令,重复读取同一 Skill 时不再输出。 - - MULTI_SIGNAL_MATCHING: - description: "#9 增加 pathPatterns 和 bashPatterns 多维匹配。【DEPRECATED 2026-07-03:方案A,pathPatterns/bashPatterns 从 frontmatter 移除,路由交还 AI】" - requirements: - 1: Skill frontmatter 支持 `pathPatterns` 字段,用于声明文件路径触发规则。 - 2: Skill frontmatter 支持 `bashPatterns` 字段,用于声明命令触发正则。 - 3: `makers-deploy` 至少匹配 `edgeone.json` 和 `\bedgeone\s+pages\s+deploy\b`。 - 4: `makers-edge-functions` 至少匹配 `functions/**`。 - 5: `makers-cloud-functions` 至少匹配 `cloud-functions/**`。 - 6: `makers-agents` 至少匹配 `agents/**`。 - 7: `makers-middleware` 至少匹配 `middleware.*`。 - 8: `makers-cli` 至少匹配 `\bedgeone\s+`。 - 9: Hook 将 glob 编译为正则后执行路径匹配。 - 10: 验证用例必须覆盖每个新增 pattern 的正向命中。 - 10-note-decision: 2026-06-24 #9 实现按真实仓库 Skill 名称使用 `makers-cloud-functions`,不新增不存在的 `makers-node-functions`;`makers-storage` 暂不声明虚构目录 pathPatterns,KV import / `context.store` 触发留给 #12 `chainTo`。 - 10-note-priority: 2026-06-24 `edgeone pages deploy` 会同时匹配 `makers-deploy` 与更泛的 `makers-cli`,Hook 需要按规则具体度选择最具体命中,避免 CLI 泛规则抢占部署 Skill。 - 10-note-implementation: 2026-06-24 已在 `makers-deploy`、`makers-edge-functions`、`makers-cloud-functions`、`makers-agents`、`makers-middleware` 和 `makers-cli` 的 `SKILL.md` frontmatter 中增加 `pathPatterns` / `bashPatterns`;`hooks/pretooluse-skill-inject.mjs` 改为扫描真实 Skill frontmatter 生成触发规则。 - 10-note-verified: 2026-06-24 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,覆盖从 frontmatter 读取 path/bash patterns、`edgeone pages deploy` 具体度优先命中 `makers-deploy`、泛 `edgeone` 命令命中 `makers-cli`,以及所有新增路径 pattern 的正向命中。 - - VALIDATE_RED_LINES: - description: "#10 增加 validate 红线规则。【2026-07-03 保留并精简:validate 拆为独立 hook hooks/validate-write.mjs,仅做写入内容检查,无状态无注入】" - requirements: - 1: Skill frontmatter 支持 `validate` 字段,用于声明写入内容的违规 pattern 和纠错提示。 - 2: `PreToolUse` 在 `Edit` 和 `Write` 时检测拟写入内容。 - 3: 写入 `process.env` 时提示 EdgeOne Makers 运行时代码应使用 `context.env`。 - 4: 写入 `new Headers()` 时提示当前运行时 surface 使用普通对象 headers。 - 5: 写入 `fs.writeFile` 时提示 Edge Function 不支持文件系统写入。 - 6: 初始实现中 validate 命中只追加纠错提示,不阻断写入。 - 6-note: 是否对关键违规执行阻断,需要在实现前继续讨论。 - 6-note-implementation: 2026-06-24 已在 `makers-edge-functions` frontmatter 中增加 `validate` 规则,并让 `hooks/pretooluse-skill-inject.mjs` 在 `Edit` / `Write` 的拟写入内容中匹配规则后追加纠错提醒;Skill 已经去重注入过时仍会继续输出 validate 提醒。 - 6-note-verified: 2026-06-24 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,覆盖 validate frontmatter 解析、`process.env`、`new Headers()`、`fs.writeFile` 三条提醒,以及 `Read` 不触发 validate 的只读路径。 - - DOMESTIC_IDE_ADAPTATION: - description: "#11 CodeBuddy 等国内 IDE 适配。【2026-07-03 部分废弃:路由降级/Push 模式 fallback 删除;写入工具名兼容(read_file/write_to_file/replace_in_file/execute_command)保留在 validate-write.mjs】" - requirements: - 1: 插件记录各平台对 `SessionStart`、`UserPromptSubmit`、`PreToolUse` 的支持差异。 - 2: 插件记录各平台是否支持 Skill tool。 - 3: 不支持 `PreToolUse` 的平台降级为仅使用 `UserPromptSubmit`。 - 4: 不支持 Skill tool 的平台降级为 Push 模式,注入必要 Skill 内容。 - 5: 插件通过现有或扩展后的 `detectPlatform` 选择平台分支。 - 6: 验证用例必须覆盖 CodeBuddy 从开发到部署的完整流程。 - 6-note-implementation: 2026-06-25 CodeBuddy 回归发现其工具名为 `read_file`、`write_to_file`、`replace_in_file`、`execute_command`,路径字段为 `filePath`,增量替换内容字段为 `new_str`;已扩展 `hooks/hooks.json` 的 `PreToolUse` matcher,并在 `hooks/pretooluse-skill-inject.mjs` 中将这些工具名映射到既有 Read/Edit/Write/Bash 逻辑。 - 6-note-verified: 2026-06-25 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,覆盖 CodeBuddy `read_file` 路径触发、`execute_command` 命令触发、`write_to_file` 的 chainTo 触发,以及 `replace_in_file` 使用 `new_str` 的 validate 触发。 - - CHAIN_TO_LOADING: - description: "#12 增加 chainTo 链式加载。【DEPRECATED 2026-07-03:方案A,chainTo 从 frontmatter 移除,交叉引用迁入 SKILL.md 正文】" - requirements: - 1: Skill frontmatter 支持 `chainTo` 字段,用于声明代码 pattern 到关联 Skill 的加载规则。 - 2: `PreToolUse` 在 `Edit` 和 `Write` 时检测 `chainTo` pattern。 - 3: 命中 `chainTo` 后额外注入关联 Skill 的加载指令。 - 4: 链式加载使用与直接命中相同的去重机制。 - 5: 验证用例必须覆盖编辑包含 KV import 或 `context.store` 的代码时触发 `makers-storage`。 - 5-note-implementation: 2026-06-25 已在 `makers-edge-functions` 与 `makers-agents` frontmatter 中增加 `chainTo` 规则,并让 `hooks/pretooluse-skill-inject.mjs` 对 `Edit` / `Write` 拟写入内容执行一跳链式匹配;命中后追加关联 Skill 加载指令,复用直接命中的去重状态,并写入 `trigger: "chainTo"` 信号日志。 - 5-note-verified: 2026-06-25 已运行 `node --test hooks/pretooluse-skill-inject.test.mjs`,覆盖 `chainTo` frontmatter 解析、KV 代码触发 `makers-storage`、`context.store` 触发 `makers-storage`、已注入 Skill 不重复输出,以及 chainTo 日志原因。 - - SIGNAL_LOGGING: - description: "#13 信号观测日志。【2026-07-03 保留:signal-log.mjs 供 validate-write.mjs 记录 validate 命中;UserPromptSubmit/PreToolUse 路由信号删除】" - requirements: - 1: 插件将 Skill 触发信号追加写入 `.edgeone/signal-log.jsonl`。 - 2: 每条日志至少包含 `timestamp`、`hook`、`trigger`、`matchedSkill` 和 `reason`。 - 3: `PreToolUse` 记录路径、命令、validate 和 chainTo 触发原因。 - 4: `UserPromptSubmit` 记录提示词评分命中原因。 - 5: 验证用例必须检查日志文件中存在正确的触发记录。 - 5-note-implementation: 2026-06-25 已新增 `hooks/signal-log.mjs`,支持向 `.edgeone/signal-log.jsonl` append JSONL,并在 `UserPromptSubmit` 命中提示词评分、`PreToolUse` 命中 `pathPatterns` / `bashPatterns` / `validate` 时记录信号;`chainTo` 的实际触发日志随 #12 链式加载实现接入。 - 5-note-verified: 2026-06-25 已运行 `node --test hooks/signal-log.test.mjs hooks/userprompt-skill-inject.test.mjs hooks/pretooluse-skill-inject.test.mjs`,覆盖必填字段、JSONL 写入、prompt score、路径、命令和 validate 信号日志;`.gitignore` 已忽略 `.edgeone/signal-log.jsonl`。 - -constraints: - CONTEXT_BUDGET: - description: 上下文预算和加载策略约束。 - requirements: - 1: 默认路径应优先使用指令加载模式,避免把大型 Skill 文档直接塞入上下文。 - 2: 只有平台不支持 Skill tool 或显式要求 Push 模式时,才允许注入 Skill 全文。 - - SPEC_MAINTENANCE: - description: 规格维护约束。 - requirements: - 1: 讨论中确认的新决策必须先更新本规格,再进入实现。 - 2: 实现、测试和文档中的关键行为应引用完整 ACID。 - 3: 已发布或已讨论确认的 requirement 不应重编号;需要变更时使用 note 或 deprecated 标记。 - diff --git a/skills/makers-agents/SKILL.md b/skills/makers-agents/SKILL.md index 21beca4..748c38e 100644 --- a/skills/makers-agents/SKILL.md +++ b/skills/makers-agents/SKILL.md @@ -270,30 +270,6 @@ PAGES_SOURCE=skills edgeone makers dev This tells the platform that the command was triggered from an AI skill context. -### package.json scripts (⚠️ avoid dev script recursion) - -> ⛔ **NEVER set `"dev": "edgeone makers dev"` or `"dev": "edgeone makers dev"` in package.json** — this causes infinite recursion. -> When CLI starts, it reads `scripts.dev` to launch the frontend dev server. If that script is -> `edgeone makers dev` itself, it recurses. CLI detects this and skips the frontend server entirely, -> causing static files (e.g., `public/index.html`) to return 404. - -Correct pattern — **do not include a `dev` script**: - -```json -{ - "scripts": { - "build": "edgeone makers build", - "deploy": "edgeone makers deploy" - } -} -``` - -CLI will automatically serve `public/` as static files during `edgeone makers dev`. No `dev` script needed. - -- `edgeone makers dev` — starts agent runtime + detects & launches frontend dev server (reads `scripts.dev` or auto-serves `public/`) -- `edgeone makers build` — builds agents + frontend into `.edgeone/` output -- `edgeone makers deploy` — builds and deploys to EdgeOne Makers - ### Local development ```bash @@ -302,13 +278,8 @@ PAGES_SOURCE=skills edgeone makers link # 2. Pull remote environment variables to local .env PAGES_SOURCE=skills edgeone makers env pull - -# 3. Start local dev server (agent runtime + frontend) -npm run makers:dev ``` -`npm run makers:dev` starts `edgeone makers dev`, which starts both the agent runtime (Node or Python, auto-detected from `agents/` file extensions) and the frontend dev server. Test through the Makers entry URL printed by the CLI. Agent endpoints are available through that Makers proxy, not through the raw frontend dev-server port. - ### Environment variables for deployment **AI Gateway variables** (`AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`) are **auto-provisioned** by the CLI during deployment — no manual setup needed, as long as `.env.example` declares them: @@ -370,8 +341,7 @@ edgeone makers env pull 2. Copy the skeleton from the matching framework reference doc. 3. Configure `edgeone.json`: set `agents.framework` correctly. 4. Frontend: `getOrCreateConversationId` + `fetch` with `makers-conversation-id` header. -5. Package scripts: keep `dev` as the frontend dev server, and put the Makers wrapper in `makers:dev`. -6. Get it running → self-check against the Critical Rules → run through `references/review-checklist.md`. +5. Get it running → self-check against the Critical Rules → run through `references/review-checklist.md`. ### Pre-Deploy SOP (⚠️ MUST execute before `edgeone makers deploy`) diff --git a/skills/makers-agents/references/node-frameworks/claude-sdk.md b/skills/makers-agents/references/node-frameworks/claude-sdk.md index 635570f..9b92bfb 100644 --- a/skills/makers-agents/references/node-frameworks/claude-sdk.md +++ b/skills/makers-agents/references/node-frameworks/claude-sdk.md @@ -22,8 +22,6 @@ npm install @anthropic-ai/claude-agent-sdk zod > `@anthropic-ai/claude-agent-sdk` is **auto-externalized** by the CLI — no manual `externalNodeModules` config needed. -> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). - --- ## When to Use Route B diff --git a/skills/makers-agents/references/node-frameworks/deepagents.md b/skills/makers-agents/references/node-frameworks/deepagents.md index 18877ed..1a6ef55 100644 --- a/skills/makers-agents/references/node-frameworks/deepagents.md +++ b/skills/makers-agents/references/node-frameworks/deepagents.md @@ -11,7 +11,7 @@ npm install deepagents@^1.9.0 @langchain/openai @langchain/core zod ``` -> **Note**: `deepagents` is a platform-provided package bundled with the EdgeOne Makers agent runtime. It is automatically available in the deployed environment. For local development, use `edgeone makers dev` which sets up the runtime with all platform packages. If adding package scripts, put this wrapper in `makers:dev`; keep `dev` for the frontend dev server only. +> **Note**: `deepagents` is a platform-provided package bundled with the EdgeOne Makers agent runtime. It is automatically available in the deployed environment. `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/node-frameworks/langgraph.md b/skills/makers-agents/references/node-frameworks/langgraph.md index 74a24bd..7ed328e 100644 --- a/skills/makers-agents/references/node-frameworks/langgraph.md +++ b/skills/makers-agents/references/node-frameworks/langgraph.md @@ -22,8 +22,6 @@ npm install @langchain/langgraph @langchain/openai @langchain/core zod > All `@langchain/*` packages are **auto-externalized** by the CLI — no manual `externalNodeModules` config needed. -> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). - --- ## When to Pick LangGraph diff --git a/skills/makers-agents/references/node-frameworks/openai-agents.md b/skills/makers-agents/references/node-frameworks/openai-agents.md index dad493a..2aeb6e6 100644 --- a/skills/makers-agents/references/node-frameworks/openai-agents.md +++ b/skills/makers-agents/references/node-frameworks/openai-agents.md @@ -22,8 +22,6 @@ npm install @openai/agents openai zod > If you encounter build errors like `Dynamic require` or `Cannot find module`, add `"externalNodeModules": ["openai", "@openai/agents"]` to the `agents` config. Unlike `deepagents` / `@langchain/*` / `claude-agent-sdk`, these are not auto-externalized. -> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). - --- ## When to use Route C diff --git a/skills/makers-agents/references/python-frameworks/claude-sdk.md b/skills/makers-agents/references/python-frameworks/claude-sdk.md index 25ead28..27defce 100644 --- a/skills/makers-agents/references/python-frameworks/claude-sdk.md +++ b/skills/makers-agents/references/python-frameworks/claude-sdk.md @@ -17,8 +17,6 @@ claude-agent-sdk>=0.1.0 pip install -r requirements.txt ``` -> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). - `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/python-frameworks/crewai.md b/skills/makers-agents/references/python-frameworks/crewai.md index 2c2e026..554d033 100644 --- a/skills/makers-agents/references/python-frameworks/crewai.md +++ b/skills/makers-agents/references/python-frameworks/crewai.md @@ -157,8 +157,6 @@ Install dependencies locally for development: pip install -r requirements.txt ``` -> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). - ```txt # CrewAI core (version aligned with the platform's bundled lib: .edgeone/agent-python/lib/) crewai>=1.14.5 diff --git a/skills/makers-agents/references/python-frameworks/deepagents.md b/skills/makers-agents/references/python-frameworks/deepagents.md index 048febc..2bc43cb 100644 --- a/skills/makers-agents/references/python-frameworks/deepagents.md +++ b/skills/makers-agents/references/python-frameworks/deepagents.md @@ -20,7 +20,7 @@ pydantic>=2.0.0 pip install -r requirements.txt ``` -> **Note**: `deepagents` is a platform-provided package bundled with the EdgeOne Makers agent runtime. It is automatically available in the deployed environment. For local development, use `edgeone makers dev`. If adding package scripts, put this wrapper in `makers:dev`; keep `dev` for the frontend dev server only. +> **Note**: `deepagents` is a platform-provided package bundled with the EdgeOne Makers agent runtime. It is automatically available in the deployed environment. `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/python-frameworks/langgraph.md b/skills/makers-agents/references/python-frameworks/langgraph.md index 25b5abb..cea2975 100644 --- a/skills/makers-agents/references/python-frameworks/langgraph.md +++ b/skills/makers-agents/references/python-frameworks/langgraph.md @@ -21,8 +21,6 @@ pydantic>=2.0.0 pip install -r requirements.txt ``` -> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). - `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/python-frameworks/openai-agents.md b/skills/makers-agents/references/python-frameworks/openai-agents.md index faaa9ae..7679e76 100644 --- a/skills/makers-agents/references/python-frameworks/openai-agents.md +++ b/skills/makers-agents/references/python-frameworks/openai-agents.md @@ -19,8 +19,6 @@ pydantic>=2.0.0 pip install -r requirements.txt ``` -> **Package scripts note**: if the project has a frontend, keep `package.json`'s `dev` script for the frontend dev server only (for example `vite --host 127.0.0.1` or `next dev`). Put the Makers wrapper in `makers:dev` (`PAGES_SOURCE=skills npx --yes edgeone makers dev`). - `edgeone.json`: ```json { diff --git a/skills/makers-agents/references/review-checklist.md b/skills/makers-agents/references/review-checklist.md index e47cfb8..fa34554 100644 --- a/skills/makers-agents/references/review-checklist.md +++ b/skills/makers-agents/references/review-checklist.md @@ -24,8 +24,6 @@ - [ ] `edgeone.json` sets `agents.framework` (`claude-agent-sdk` / `openai-agents-sdk` / `langgraph` / `crewai` / `deepagents` — **no `basic`**, the schema enum does not include it) — **required for console icon display** -- [ ] ⚠️ `package.json` keeps `dev` as the frontend dev server command only, and puts `edgeone makers dev` in a separate wrapper such as `makers:dev`; never set `dev` to `PAGES_SOURCE=skills npx --yes edgeone makers dev` - - [ ] Frontend lives under `app/`, components under `app/components/`, global utilities under root `lib/` --- @@ -196,7 +194,7 @@ - [ ] `process.env` is allowed on the frontend (consistent with frontend frameworks), but **do not** expose backend secrets like `AI_GATEWAY_API_KEY` to the browser -- [ ] Local testing is done through the Makers proxy URL printed by `edgeone makers dev` / `npm run makers:dev`; the raw frontend dev-server port does not serve `agents/` routes +- [ ] Local testing is done through the Makers proxy URL printed by `edgeone makers dev`; the raw frontend dev-server port does not serve `agents/` routes --- diff --git a/skills/makers-cli/SKILL.md b/skills/makers-cli/SKILL.md index 7e50492..ea6e420 100644 --- a/skills/makers-cli/SKILL.md +++ b/skills/makers-cli/SKILL.md @@ -48,23 +48,6 @@ export PAGES_SOURCE=skills Or inline: `PAGES_SOURCE=skills edgeone makers dev` -## Package Scripts For Agent Projects - -For projects that contain `agents/` plus a frontend, do not put `edgeone makers dev` in `package.json`'s `dev` script. The Makers CLI uses `npm run dev -- --port ` as the frontend dev server command, so `dev` must be a real frontend server: - -```json -{ - "scripts": { - "dev": "vite --host 127.0.0.1", - "makers:dev": "PAGES_SOURCE=skills npx --yes edgeone makers dev", - "makers:build": "PAGES_SOURCE=skills npx --yes edgeone makers build", - "deploy": "PAGES_SOURCE=skills npx --yes edgeone makers deploy" - } -} -``` - -Run `npm run makers:dev` to start Makers local development, then test through the Makers URL it prints (usually `http://localhost:8088`). Do not test Agent endpoints through the raw frontend port; that server only serves frontend assets. - ## Common Workflows ### First-time setup @@ -76,12 +59,6 @@ PAGES_SOURCE=skills edgeone makers env pull PAGES_SOURCE=skills edgeone makers dev ``` -### Project script workflow -```bash -npm install -npm run makers:dev -``` - ### Deploy ```bash edgeone makers deploy diff --git a/skills/makers-recipes/SKILL.md b/skills/makers-recipes/SKILL.md index fb3832a..aaed490 100644 --- a/skills/makers-recipes/SKILL.md +++ b/skills/makers-recipes/SKILL.md @@ -14,24 +14,6 @@ metadata: Project structure templates for typical EdgeOne Makers applications. -## Agent project package scripts - -For any recipe that creates an Agent project with a frontend, keep the frontend dev script and Makers wrapper script separate: - -```json -{ - "scripts": { - "dev": "vite --host 127.0.0.1", - "makers:dev": "PAGES_SOURCE=skills npx --yes edgeone makers dev", - "build": "npm run typecheck", - "makers:build": "PAGES_SOURCE=skills npx --yes edgeone makers build", - "deploy": "PAGES_SOURCE=skills npx --yes edgeone makers deploy" - } -} -``` - -`dev` is reserved for the frontend dev server because `edgeone makers dev` invokes it behind the Makers proxy with a chosen port. Put the EdgeOne Makers CLI command in `makers:dev` and tell users to open the Makers proxy URL, not the raw frontend port, when testing `agents/` endpoints. - ## Full-stack app — Node.js (static + API) ```