diff --git a/codex/makers-agents.md b/codex/makers-agents.md index 474e539..98648ab 100644 --- a/codex/makers-agents.md +++ b/codex/makers-agents.md @@ -15,6 +15,15 @@ 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/** +validate: + - pattern: "process\\.env|os\\.environ" + message: "Read env via context.env inside agents/ and cloud-functions/, never process.env or os.environ (Critical Rule 3)." + - pattern: "headers\\s*\\.\\s*get\\s*\\(" + message: "Headers are plain objects here: context.request.headers['x-name'], not .get('x-name') (Critical Rule 4)." + - pattern: "langgraphStore\\s*\\?\\?\\s*store" + message: "Never write `store?.langgraphStore ?? store` — in cloud-function context it falls back to a store with no .get and crashes (Critical Rule 12)." metadata: author: edgeone version: "1.0.0" diff --git a/codex/makers-cloud-functions.md b/codex/makers-cloud-functions.md index 7b8ce09..86832a1 100644 --- a/codex/makers-cloud-functions.md +++ b/codex/makers-cloud-functions.md @@ -3,6 +3,11 @@ name: edgeone-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/** +validate: + - pattern: "process\\.env|os\\.environ" + message: "Read env via context.env inside cloud-functions/, never process.env or os.environ." metadata: author: edgeone version: "1.0.0" diff --git a/codex/makers-deploy.md b/codex/makers-deploy.md index dc2dff9..c83ec64 100644 --- a/codex/makers-deploy.md +++ b/codex/makers-deploy.md @@ -12,6 +12,12 @@ 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: + - "*.sh" + - .github/workflows/** +validate: + - pattern: "whoami[^\\n]*\\s-t\\s" + message: "edgeone whoami does not accept -t. Check the exit code instead: 0 = logged in, 1 = not." metadata: author: edgeone version: "2.2.0" diff --git a/codex/makers-edge-functions.md b/codex/makers-edge-functions.md index b7d0502..98bcd31 100644 --- a/codex/makers-edge-functions.md +++ b/codex/makers-edge-functions.md @@ -13,6 +13,8 @@ validate: message: "Use plain object headers for this runtime surface." - pattern: "fs\\.writeFile" message: "Edge Functions do not support filesystem writes." + - pattern: "Response\\.json\\s*\\(" + message: "Response.json() is not available in this V8 runtime — use new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })." metadata: author: edgeone version: "1.0.0" diff --git a/codex/makers-env-adaption.md b/codex/makers-env-adaption.md index 495ae8a..6921ae9 100644 --- a/codex/makers-env-adaption.md +++ b/codex/makers-env-adaption.md @@ -7,6 +7,14 @@ description: >- Covers: non-interactive CLI flags, network isolation workarounds, login in sandbox, proxy bypass, file preview constraints (MUST use http:// via dev server, NEVER file://, NEVER python -m http.server / npx serve), dev server requirements. +pathPatterns: + - "*.sh" + - package.json +validate: + - pattern: "python\\s+-m\\s+http\\.server|npx\\s+(serve|http-server)" + message: "Use `edgeone makers dev` — self-hosted static servers skip Blob credentials, Cloud Functions routing, Edge Functions and middleware." + - pattern: "localhost:80(88|89)" + message: "Use 127.0.0.1, not localhost — in the sandbox localhost resolves to ::1 and yields false 404s." metadata: author: edgeone version: "1.1.1" diff --git a/codex/makers-middleware.md b/codex/makers-middleware.md index 751cc3d..c1d580e 100644 --- a/codex/makers-middleware.md +++ b/codex/makers-middleware.md @@ -3,6 +3,12 @@ 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.js + - middleware.ts +validate: + - pattern: "NextRequest|NextResponse|next/server" + message: "Framework projects must use the framework's own middleware. This platform format takes a context object with next/redirect/rewrite — not NextRequest/NextResponse." metadata: author: edgeone version: "1.0.0" diff --git a/codex/makers-migration.md b/codex/makers-migration.md index afe05be..94826dc 100644 --- a/codex/makers-migration.md +++ b/codex/makers-migration.md @@ -7,6 +7,12 @@ description: >- convert Express/Next.js API routes to Makers handlers, or add platform capabilities (context.tools, context.sandbox, context.store). Do NOT trigger for new agent projects (use makers-agents instead). +pathPatterns: + - agents/** + - cloud-functions/** +validate: + - pattern: "process\\.env|os\\.environ" + message: "Migration checklist: replace process.env / os.environ with context.env (TS) or ctx env access (Python)." metadata: author: edgeone version: "1.0.0" diff --git a/codex/makers-storage.md b/codex/makers-storage.md index 304f586..514a977 100644 --- a/codex/makers-storage.md +++ b/codex/makers-storage.md @@ -3,6 +3,11 @@ name: edgeone-makers-storage description: >- KV and Blob storage services on EdgeOne Makers. KV for edge key-value pairs, Blob for file/object storage in Cloud Functions. Covers SDK usage, setup, and troubleshooting. +pathPatterns: + - edge-functions/** +validate: + - pattern: "context\\.env\\.[A-Za-z_]*[Kk][Vv]" + message: "KV is a console-bound global variable, not on context.env — call my_kv.get(...) directly. See makers-storage/references/kv.md." metadata: author: edgeone version: "1.0.0" diff --git a/cursor/rules/makers-agents.mdc b/cursor/rules/makers-agents.mdc index 474e539..98648ab 100644 --- a/cursor/rules/makers-agents.mdc +++ b/cursor/rules/makers-agents.mdc @@ -15,6 +15,15 @@ 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/** +validate: + - pattern: "process\\.env|os\\.environ" + message: "Read env via context.env inside agents/ and cloud-functions/, never process.env or os.environ (Critical Rule 3)." + - pattern: "headers\\s*\\.\\s*get\\s*\\(" + message: "Headers are plain objects here: context.request.headers['x-name'], not .get('x-name') (Critical Rule 4)." + - pattern: "langgraphStore\\s*\\?\\?\\s*store" + message: "Never write `store?.langgraphStore ?? store` — in cloud-function context it falls back to a store with no .get and crashes (Critical Rule 12)." metadata: author: edgeone version: "1.0.0" diff --git a/cursor/rules/makers-cloud-functions.mdc b/cursor/rules/makers-cloud-functions.mdc index 7b8ce09..86832a1 100644 --- a/cursor/rules/makers-cloud-functions.mdc +++ b/cursor/rules/makers-cloud-functions.mdc @@ -3,6 +3,11 @@ name: edgeone-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/** +validate: + - pattern: "process\\.env|os\\.environ" + message: "Read env via context.env inside cloud-functions/, never process.env or os.environ." metadata: author: edgeone version: "1.0.0" diff --git a/cursor/rules/makers-deploy.mdc b/cursor/rules/makers-deploy.mdc index dc2dff9..c83ec64 100644 --- a/cursor/rules/makers-deploy.mdc +++ b/cursor/rules/makers-deploy.mdc @@ -12,6 +12,12 @@ 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: + - "*.sh" + - .github/workflows/** +validate: + - pattern: "whoami[^\\n]*\\s-t\\s" + message: "edgeone whoami does not accept -t. Check the exit code instead: 0 = logged in, 1 = not." metadata: author: edgeone version: "2.2.0" diff --git a/cursor/rules/makers-edge-functions.mdc b/cursor/rules/makers-edge-functions.mdc index b7d0502..98bcd31 100644 --- a/cursor/rules/makers-edge-functions.mdc +++ b/cursor/rules/makers-edge-functions.mdc @@ -13,6 +13,8 @@ validate: message: "Use plain object headers for this runtime surface." - pattern: "fs\\.writeFile" message: "Edge Functions do not support filesystem writes." + - pattern: "Response\\.json\\s*\\(" + message: "Response.json() is not available in this V8 runtime — use new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })." metadata: author: edgeone version: "1.0.0" diff --git a/cursor/rules/makers-env-adaption.mdc b/cursor/rules/makers-env-adaption.mdc index 495ae8a..6921ae9 100644 --- a/cursor/rules/makers-env-adaption.mdc +++ b/cursor/rules/makers-env-adaption.mdc @@ -7,6 +7,14 @@ description: >- Covers: non-interactive CLI flags, network isolation workarounds, login in sandbox, proxy bypass, file preview constraints (MUST use http:// via dev server, NEVER file://, NEVER python -m http.server / npx serve), dev server requirements. +pathPatterns: + - "*.sh" + - package.json +validate: + - pattern: "python\\s+-m\\s+http\\.server|npx\\s+(serve|http-server)" + message: "Use `edgeone makers dev` — self-hosted static servers skip Blob credentials, Cloud Functions routing, Edge Functions and middleware." + - pattern: "localhost:80(88|89)" + message: "Use 127.0.0.1, not localhost — in the sandbox localhost resolves to ::1 and yields false 404s." metadata: author: edgeone version: "1.1.1" diff --git a/cursor/rules/makers-middleware.mdc b/cursor/rules/makers-middleware.mdc index 751cc3d..c1d580e 100644 --- a/cursor/rules/makers-middleware.mdc +++ b/cursor/rules/makers-middleware.mdc @@ -3,6 +3,12 @@ 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.js + - middleware.ts +validate: + - pattern: "NextRequest|NextResponse|next/server" + message: "Framework projects must use the framework's own middleware. This platform format takes a context object with next/redirect/rewrite — not NextRequest/NextResponse." metadata: author: edgeone version: "1.0.0" diff --git a/cursor/rules/makers-migration.mdc b/cursor/rules/makers-migration.mdc index afe05be..94826dc 100644 --- a/cursor/rules/makers-migration.mdc +++ b/cursor/rules/makers-migration.mdc @@ -7,6 +7,12 @@ description: >- convert Express/Next.js API routes to Makers handlers, or add platform capabilities (context.tools, context.sandbox, context.store). Do NOT trigger for new agent projects (use makers-agents instead). +pathPatterns: + - agents/** + - cloud-functions/** +validate: + - pattern: "process\\.env|os\\.environ" + message: "Migration checklist: replace process.env / os.environ with context.env (TS) or ctx env access (Python)." metadata: author: edgeone version: "1.0.0" diff --git a/cursor/rules/makers-storage.mdc b/cursor/rules/makers-storage.mdc index 304f586..514a977 100644 --- a/cursor/rules/makers-storage.mdc +++ b/cursor/rules/makers-storage.mdc @@ -3,6 +3,11 @@ name: edgeone-makers-storage description: >- KV and Blob storage services on EdgeOne Makers. KV for edge key-value pairs, Blob for file/object storage in Cloud Functions. Covers SDK usage, setup, and troubleshooting. +pathPatterns: + - edge-functions/** +validate: + - pattern: "context\\.env\\.[A-Za-z_]*[Kk][Vv]" + message: "KV is a console-bound global variable, not on context.env — call my_kv.get(...) directly. See makers-storage/references/kv.md." metadata: author: edgeone version: "1.0.0" diff --git a/hooks/validate-write.mjs b/hooks/validate-write.mjs index 95f2ed2..201403f 100644 --- a/hooks/validate-write.mjs +++ b/hooks/validate-write.mjs @@ -114,12 +114,32 @@ function parseSkillValidateRule(skillPath) { }; } +/** + * 读取各能力声明的 validate 规则。 + * + * 读不到就返回空数组,绝不抛错:本函数跑在 PreToolUse 钩子里, + * 模型每写一个文件都会经过它。references/ 不存在(部分安装、 + * CLAUDE_PLUGIN_ROOT 解析错位)时若抛 ENOENT,用户每次写文件都会看到一次报错。 + * 校验器失效的正确表现是「不提醒」,而不是「报错」。 + */ export function loadSkillValidateRules(skillsDir = DEFAULT_SKILLS_DIR) { if (skillsDir === DEFAULT_SKILLS_DIR && cachedRules) return cachedRules; - const rules = readdirSync(skillsDir, { withFileTypes: true }) + let entries; + try { + entries = readdirSync(skillsDir, { withFileTypes: true }); + } catch { + return []; + } + const rules = entries .filter((entry) => entry.isDirectory()) .map((entry) => join(skillsDir, entry.name, 'SKILL.md')) - .map((skillPath) => parseSkillValidateRule(skillPath)) + .map((skillPath) => { + try { + return parseSkillValidateRule(skillPath); + } catch { + return null; + } + }) .filter(Boolean); if (skillsDir === DEFAULT_SKILLS_DIR) cachedRules = rules; return rules; @@ -147,21 +167,33 @@ function getToolWriteContent(payload) { 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; +/** + * 返回所有 pathPatterns 命中该路径的规则。 + * + * 不能只取第一条:规则按目录字母序加载,而 `agents/**` 与 + * `cloud-functions/**` 这类前缀天然会重叠。只取首条等于让「哪条铁律生效」 + * 由目录名的字母序偶然决定,多个能力共管同一路径时会静默丢提醒。 + */ +function findSkillsForPath(filePath, rules) { + if (!filePath) return []; + return rules.filter((rule) => + rule.pathPatterns.some((pattern) => globToRegExp(pattern).test(filePath)), + ); } -function selectValidationMatches(content, rule) { +/** + * 收集全部命中的校验项,message 去重并保留首次出现顺序。 + * 每项带上来源 skill,供 signal log 归因。 + */ +function selectValidationMatches(content, matchedRules) { const seen = new Set(); const matches = []; - for (const item of rule.validate) { - if (new RegExp(item.pattern).test(content) && !seen.has(item.message)) { + for (const rule of matchedRules) { + for (const item of rule.validate) { + if (!new RegExp(item.pattern).test(content)) continue; + if (seen.has(item.message)) continue; seen.add(item.message); - matches.push(item); + matches.push({ ...item, skill: rule.skill }); } } return matches; @@ -176,13 +208,13 @@ export function buildValidateWriteOutput(payload, options = {}) { const content = getToolWriteContent(payload); if (!content) return null; - const rule = findSkillForPath( + const matchedRules = findSkillsForPath( getToolPath(getToolInput(payload)), options.rules || loadSkillValidateRules(), ); - if (!rule) return null; + if (matchedRules.length === 0) return null; - const matches = selectValidationMatches(content, rule); + const matches = selectValidationMatches(content, matchedRules); if (matches.length === 0) return null; if (shouldWriteSignalLog(options)) { @@ -191,7 +223,7 @@ export function buildValidateWriteOutput(payload, options = {}) { { hook: 'PreToolUse', trigger: 'validate', - matchedSkill: rule.skill, + matchedSkill: match.skill, reason: match.message, toolName: getToolName(payload), }, @@ -216,17 +248,33 @@ async function readStdin() { return input; } +/** + * 钩子入口。任何异常都吞掉并静默返回: + * 这段代码挡在模型每一次 Edit/Write 前面,宁可漏一次提醒, + * 也不能因为自身出错(stdin 不是合法 JSON、规则读不到等) + * 让用户每写一个文件都看到一次报错。 + */ export async function main() { - const rawInput = await readStdin(); - const payload = rawInput.trim() ? JSON.parse(rawInput) : {}; - const output = buildValidateWriteOutput(payload, { enableSignalLog: true }); + let payload; + try { + const rawInput = await readStdin(); + payload = rawInput.trim() ? JSON.parse(rawInput) : {}; + } catch { + return; + } + + let output; + try { + output = buildValidateWriteOutput(payload, { enableSignalLog: true }); + } catch { + return; + } + 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); - }); + // main() 内部已兜住所有异常;这里再兜一层,保证退出码始终是 0。 + main().catch(() => {}); } diff --git a/hooks/validate-write.test.mjs b/hooks/validate-write.test.mjs index 3c6bfe0..3bb678c 100644 --- a/hooks/validate-write.test.mjs +++ b/hooks/validate-write.test.mjs @@ -10,21 +10,23 @@ test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.1 loads validate ru const rules = await loadSkillValidateRules(); const edgeFunctions = rules.find((rule) => rule.skill === 'edgeone-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.', - }, - ]); + assert.ok(edgeFunctions, 'edge-functions capability should declare validate rules'); + assert.deepEqual(edgeFunctions.pathPatterns, ['edge-functions/**', 'functions/**']); + + // 断言不变量而不是冻结的数组:每加一条规则都让测试失败,只会逼着人改断言。 + assert.ok(edgeFunctions.validate.length >= 3); + for (const item of edgeFunctions.validate) { + assert.equal(typeof item.pattern, 'string'); + assert.equal(typeof item.message, 'string'); + assert.ok(item.message.length > 0); + assert.doesNotThrow(() => new RegExp(item.pattern)); + } + + // 三条原始红线必须始终在场,新增规则不得把它们挤掉。 + const messages = edgeFunctions.validate.map((item) => item.message); + assert.ok(messages.includes('Use context.env in EdgeOne Makers runtime code.')); + assert.ok(messages.includes('Use plain object headers for this runtime surface.')); + assert.ok(messages.includes('Edge Functions do not support filesystem writes.')); }); test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.2 warns on Edit content without blocking writes', () => { @@ -200,3 +202,125 @@ test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs validate matches await rm(tmp, { recursive: true, force: true }); } }); +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.7 aggregates matches from every skill whose pathPatterns match', () => { + const rules = [ + { + skill: 'skill-alpha', + pathPatterns: ['functions/**'], + validate: [{ pattern: 'process\\.env', message: 'Alpha says use context.env.' }], + }, + { + skill: 'skill-beta', + pathPatterns: ['functions/**'], + validate: [{ pattern: 'process\\.env', message: 'Beta says the same thing differently.' }], + }, + ]; + + const output = buildValidateWriteOutput( + { + tool_name: 'Write', + tool_input: { file_path: 'functions/index.ts', content: 'process.env.KEY' }, + }, + { rules }, + ); + + assert.deepEqual(output, { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: + 'Validation reminder:\n- Alpha says use context.env.\n- Beta says the same thing differently.', + }, + }); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.7 deduplicates an identical message from two skills', () => { + const rules = [ + { + skill: 'skill-alpha', + pathPatterns: ['functions/**'], + validate: [{ pattern: 'process\\.env', message: 'Use context.env.' }], + }, + { + skill: 'skill-beta', + pathPatterns: ['functions/**'], + validate: [{ pattern: 'process\\.env', message: 'Use context.env.' }], + }, + ]; + + const output = buildValidateWriteOutput( + { + tool_name: 'Write', + tool_input: { file_path: 'functions/index.ts', content: 'process.env.KEY' }, + }, + { rules }, + ); + + assert.equal( + output.hookSpecificOutput.additionalContext, + 'Validation reminder:\n- Use context.env.', + ); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.7 ignores a skill whose pathPatterns do not match', () => { + const rules = [ + { + skill: 'skill-alpha', + pathPatterns: ['edge-functions/**'], + validate: [{ pattern: 'process\\.env', message: 'Alpha.' }], + }, + { + skill: 'skill-beta', + pathPatterns: ['agents/**'], + validate: [{ pattern: 'process\\.env', message: 'Beta.' }], + }, + ]; + + const output = buildValidateWriteOutput( + { + tool_name: 'Write', + tool_input: { file_path: 'agents/chat/index.ts', content: 'process.env.KEY' }, + }, + { rules }, + ); + + assert.equal(output.hookSpecificOutput.additionalContext, 'Validation reminder:\n- Beta.'); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.8 every declared rule has usable patterns and paths', async () => { + const rules = await loadSkillValidateRules(); + assert.ok(rules.length >= 8, `expected at least 8 skills with validate, got ${rules.length}`); + + for (const rule of rules) { + assert.ok(rule.pathPatterns.length > 0, `${rule.skill} declares validate but no pathPatterns`); + assert.ok(rule.validate.length > 0, `${rule.skill} has an empty validate list`); + for (const item of rule.validate) { + assert.doesNotThrow( + () => new RegExp(item.pattern), + `${rule.skill}: invalid regex ${item.pattern}`, + ); + assert.ok(item.message.length > 10, `${rule.skill}: message too short to be actionable`); + } + } +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.9 returns no rules when the skills directory is missing', () => { + // 部分安装 / CLAUDE_PLUGIN_ROOT 解析错位时,skills/ 可能不存在。 + // 校验器失效应当是「不提醒」,不能变成每次写文件都抛 ENOENT。 + assert.deepEqual(loadSkillValidateRules('/nonexistent-skills-dir-for-test'), []); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.9 returns no rules when the skills path is a file', () => { + assert.deepEqual(loadSkillValidateRules('hooks/validate-write.mjs'), []); +}); + +test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.9 yields no output instead of throwing when rules cannot be loaded', () => { + const output = buildValidateWriteOutput( + { + tool_name: 'Write', + tool_input: { file_path: 'agents/chat/index.ts', content: 'process.env.KEY' }, + }, + { rules: loadSkillValidateRules('/nonexistent-skills-dir-for-test') }, + ); + + assert.equal(output, null); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..11c67e3 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "edgeone-makers-tools", + "version": "1.2.0", + "private": true, + "type": "module", + "description": "EdgeOne Makers platform development skills.", + "scripts": { + "test": "node --test hooks/*.test.mjs scripts/*.test.mjs scripts/lib/*.test.mjs", + "doctor": "node scripts/doctor.mjs", + "build": "node scripts/build.mjs" + } +} diff --git a/scripts/add-toc.mjs b/scripts/add-toc.mjs new file mode 100644 index 0000000..886a13c --- /dev/null +++ b/scripts/add-toc.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node +/** + * 一次性工具:为超 100 行、且开头没有锚点目录的 reference 插入目录。 + * 目录由文件自身的标题生成,插在 H1 之后、第一段正文之前。 + * + * 工作清单直接取自 findMissingTocs,保证生成器与验收闸门用同一套判定, + * 不会出现「脚本认为加过了、doctor 仍然报缺」的分歧。 + * + * 幂等:已有目录的文件不会出现在 findMissingTocs 结果里,重跑即零改动。 + */ +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { findMissingTocs } from './lib/skill-graph.mjs'; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const SKILLS_DIR = join(REPO_ROOT, 'skills'); + +/** GitHub 风格锚点:小写、去标点、空格转连字符。保留 CJK。 */ +export function slugify(heading) { + return heading + .trim() + .toLowerCase() + .replace(/[`*_~]/g, '') + .replace(/[^\w一-龥\s-]/g, '') + .trim() + .replace(/\s+/g, '-'); +} + +/** + * 提取指定层级的标题,跳过代码块内的 #。 + * level 为 2 取 `## `,为 3 取 `### `。 + */ +export function extractHeadings(lines, level = 2) { + const pattern = new RegExp(`^#{${level}}\\s+(.+?)\\s*$`); + const headings = []; + let inFence = false; + lines.forEach((line, index) => { + if (/^```/.test(line)) { + inFence = !inFence; + return; + } + if (inFence) return; + const match = pattern.exec(line); + if (match) headings.push({ text: match[1], line: index }); + }); + return headings; +} + +/** + * 返回插入目录后的完整文本;两个层级都凑不出 2 条标题时原样返回。 + * + * `##` 不足 2 条时回退到 `###`:capabilities/tools.md 就是这种结构 + * (1 个 `##` + 9 个 `###`),不回退的话它永远留在 findMissingTocs 里, + * doctor 也就永远转不了绿。只回退、不混层,免得目录深浅不一。 + */ +export function withToc(content) { + const lines = content.split(/\r?\n/); + let headings = extractHeadings(lines, 2); + if (headings.length < 2) headings = extractHeadings(lines, 3); + if (headings.length < 2) return content; + + const toc = ['## Contents', '', ...headings.map((h) => `- [${h.text}](#${slugify(h.text)})`), '']; + + // 插入点:H1 之后的首个非空行之前;没有 H1 就插到文件开头。 + let insertAt = 0; + if (/^#\s+/.test(lines[0])) { + insertAt = 1; + while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt += 1; + } + return [...lines.slice(0, insertAt), ...toc, ...lines.slice(insertAt)].join('\n'); +} + +function main() { + const targets = findMissingTocs(SKILLS_DIR); + let changed = 0; + for (const { file } of targets) { + const abs = join(SKILLS_DIR, file); + const before = readFileSync(abs, 'utf8'); + const after = withToc(before); + if (after !== before) { + writeFileSync(abs, after); + changed += 1; + console.log(` + ${file}`); + } else { + console.log(` ! 跳过(两个层级都不足 2 条标题)${file}`); + } + } + console.log(`\n${changed} / ${targets.length} 个文件已加目录`); +} + +main(); diff --git a/scripts/doctor.mjs b/scripts/doctor.mjs new file mode 100644 index 0000000..4088355 --- /dev/null +++ b/scripts/doctor.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * Skills 自检入口。六项检查: + * 断链 / 悬空 skill 名 / 二级引用 / reference 目录 / 行数上限 / 发布清单一致性 + * 退出码 0 = 全绿,1 = 有失败项。 + * + * 分层:skill-graph.mjs 只认「给一个 root,返回纯数据」,不知道仓库根在哪; + * 认识 _meta.json 与 skills/ 位置的是这一层。 + */ +import { readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { + checkFileManifest, + findBrokenLinks, + findDanglingSkillNames, + findDeepReferenceLinks, + findMissingTocs, + findOversizedFiles, +} from './lib/skill-graph.mjs'; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); + +const CHECKS = [ + { key: 'brokenLinks', check: 'broken-links', label: '断链' }, + { key: 'danglingNames', check: 'dangling-skill-names', label: '悬空 skill 名' }, + { key: 'deepLinks', check: 'deep-reference-links', label: '二级引用' }, + { key: 'missingTocs', check: 'missing-toc', label: '缺目录的长 reference' }, + { key: 'oversized', check: 'oversized-file', label: '超行数上限' }, +]; + +export function collect(skillsDir, metaPath = join(REPO_ROOT, '_meta.json')) { + const meta = JSON.parse(readFileSync(metaPath, 'utf8')); + return { + brokenLinks: findBrokenLinks(skillsDir), + danglingNames: findDanglingSkillNames(skillsDir), + deepLinks: findDeepReferenceLinks(skillsDir), + missingTocs: findMissingTocs(skillsDir), + oversized: findOversizedFiles(skillsDir), + manifest: checkFileManifest(skillsDir, meta.files), + }; +} + +export function summarize(results) { + const failures = []; + for (const { key, check, label } of CHECKS) { + const items = results[key] || []; + if (items.length > 0) failures.push({ check, label, count: items.length, items }); + } + const manifest = results.manifest || { missing: [], extra: [] }; + if (manifest.missing.length > 0 || manifest.extra.length > 0) { + failures.push({ + check: 'manifest-parity', + label: '_meta.json 与磁盘不一致', + count: 1, + items: [manifest], + }); + } + return { ok: failures.length === 0, failures }; +} + +function describeItem(item) { + // 读不到的文件是 findBrokenLinks 混在 broken 数组里的另一种条目 + // ({ file, line: 0, target: null, error }),跟「链接坏了」不是一回事,必须先单独认出来: + // 它的 line 是 0,会让下面 `if (item.line)` 落空,最该说清楚的那条反而退化成一坨裸 JSON。 + if (typeof item.error === 'string') return `无法读取 ${item.file}:${item.error}`; + if (Array.isArray(item.missing)) { + const parts = []; + if (item.missing.length) parts.push(`未声明 ${item.missing.length} 个:${item.missing.join(', ')}`); + if (item.extra.length) parts.push(`多声明 ${item.extra.length} 个:${item.extra.join(', ')}`); + return parts.join(';'); + } + if (item.line) return `${item.file}:${item.line} → ${item.target || item.name}`; + if (item.lines) return `${item.file}(${item.lines} 行)`; + return JSON.stringify(item); +} + +export function formatReport(summary) { + if (summary.ok) return '✅ doctor:六项检查全部通过'; + const lines = ['❌ doctor:发现问题']; + for (const failure of summary.failures) { + lines.push(`\n[${failure.check}] ${failure.label} — ${failure.count} 处`); + for (const item of failure.items) lines.push(` - ${describeItem(item)}`); + } + return lines.join('\n'); +} + +/** + * 扫描根固定为 skills/,而不是单个 skill 的 references/ 目录。 + * 这样 checkFileManifest 拼出的 `skills/...` 前缀与 _meta.json 的 files 一致, + * findMissingTocs 的 SKILL.md 豁免也能同时覆盖路由页与各 capability 的 SKILL.md。 + */ +export function main(skillsDir = join(REPO_ROOT, 'skills')) { + const summary = summarize(collect(skillsDir)); + console.log(formatReport(summary)); + return summary.ok ? 0 : 1; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(main()); +} diff --git a/scripts/doctor.test.mjs b/scripts/doctor.test.mjs new file mode 100644 index 0000000..a31d440 --- /dev/null +++ b/scripts/doctor.test.mjs @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { formatReport, summarize } from './doctor.mjs'; + +const EMPTY = { + brokenLinks: [], + danglingNames: [], + deepLinks: [], + missingTocs: [], + oversized: [], + manifest: { missing: [], extra: [] }, +}; + +test('doctor.summarize reports ok when every check is empty', () => { + const summary = summarize(EMPTY); + assert.equal(summary.ok, true); + assert.equal(summary.failures.length, 0); +}); + +test('doctor.summarize collects one failure per non-empty check', () => { + const summary = summarize({ + ...EMPTY, + brokenLinks: [{ file: 'a/SKILL.md', line: 1, target: 'x.md' }], + missingTocs: [{ file: 'a/references/b.md', lines: 200 }], + }); + assert.equal(summary.ok, false); + assert.deepEqual( + summary.failures.map((failure) => failure.check), + ['broken-links', 'missing-toc'], + ); + assert.equal(summary.failures[0].count, 1); +}); + +test('doctor.summarize treats a manifest gap as a single failure', () => { + const summary = summarize({ + ...EMPTY, + manifest: { missing: ['skills/makers-migration/SKILL.md'], extra: [] }, + }); + assert.equal(summary.ok, false); + assert.deepEqual(summary.failures.map((f) => f.check), ['manifest-parity']); + assert.equal(summary.failures[0].count, 1); +}); + +test('doctor.formatReport names each failing check and its locations', () => { + const text = formatReport( + summarize({ ...EMPTY, brokenLinks: [{ file: 'a/SKILL.md', line: 9, target: 'x.md' }] }), + ); + assert.match(text, /broken-links/); + assert.match(text, /a\/SKILL\.md:9/); +}); + +test('doctor.formatReport says everything passed when ok', () => { + assert.match(formatReport(summarize(EMPTY)), /通过/); +}); + +test('doctor.formatReport renders an unreadable file as its own message, not raw JSON', () => { + const text = formatReport( + summarize({ + ...EMPTY, + brokenLinks: [ + { + file: 'a/references/locked.md', + line: 0, + target: null, + error: "EACCES: permission denied, open 'a/references/locked.md'", + }, + ], + }), + ); + assert.match(text, /无法读取/); + assert.match(text, /a\/references\/locked\.md/); + assert.match(text, /EACCES/); + // line: 0 会让 `if (item.line)` 落空,退化成 JSON.stringify 的裸对象。 + assert.ok(!text.includes('{"'), `报告里出现了裸 JSON 片段:\n${text}`); +}); diff --git a/scripts/lib/skill-graph.mjs b/scripts/lib/skill-graph.mjs new file mode 100644 index 0000000..c8f5de2 --- /dev/null +++ b/scripts/lib/skill-graph.mjs @@ -0,0 +1,315 @@ +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; + +const MD_LINK = /\[[^\]]*\]\(([^)\s]+)\)/g; + +/** 统一的换行切分,兼容 CRLF;无 g 标志,可安全共享。 */ +const LINE_BREAK = /\r?\n/; + +/** + * 非本地链接:任意 URL scheme(http:/https:/mailto:/tel:/data: ……)与纯锚点。 + * 用通用 scheme 匹配而不是逐个枚举,免得以后漏掉一种就误报。 + */ +const NON_LOCAL_LINK = /^([a-z][a-z0-9+.-]*:|#)/i; + +/** root 必须是存在的目录,否则 readdirSync 只会抛 ENOENT/ENOTDIR 裸栈。 */ +function assertDirectory(root) { + if (!existsSync(root)) { + throw new Error(`skill-graph: root directory not found: ${root}`); + } + if (!statSync(root).isDirectory()) { + throw new Error(`skill-graph: root is not a directory: ${root}`); + } +} + +/** 递归列出所有 .md,返回相对 root 的 posix 路径,已排序。 */ +export function listMarkdownFiles(root) { + assertDirectory(root); + const out = []; + function walk(dir) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name.startsWith('.')) continue; + const full = join(dir, entry.name); + if (entry.isDirectory()) walk(full); + else if (entry.name.endsWith('.md')) out.push(relative(root, full).split('\\').join('/')); + } + } + walk(root); + // 逐目录排序 + 深度优先并不等于全局有序(目录 a/ 与文件 a-b.md 会乱序), + // 所以统一在这里排一次,让上面那句“已排序”成立。 + return out.sort(); +} + +/** skills/ 的直接子目录名,已排序。 */ +export function listSkillDirs(root) { + assertDirectory(root); + return readdirSync(root, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.')) + .map((entry) => entry.name) + .sort(); +} + +/** + * 逐行遍历 root 下所有 markdown,对每行调用 visit(file, line, lineNumber)。 + * file 是相对 root 的 posix 路径,lineNumber 从 1 开始。 + * + * 单个文件读失败(权限等)不该让整轮扫描失效——记进 unreadable 后跳过, + * 其余文件照常检查。返回读不到的文件清单。 + */ +export function forEachMarkdownLine(root, visit) { + const unreadable = []; + for (const file of listMarkdownFiles(root)) { + let text; + try { + text = readFileSync(join(root, file), 'utf8'); + } catch (error) { + unreadable.push({ file, error: error instanceof Error ? error.message : String(error) }); + continue; + } + text.split(LINE_BREAK).forEach((line, index) => visit(file, line, index + 1)); + } + return unreadable; +} + +/** + * 读单个 markdown 全文,读不到返回 null。 + * + * 本模块唯一一处“带守卫的读”,isFile() 这道守卫挡掉三类东西: + * - 不存在的路径 + * - 目录(SKILL.md 是个目录时裸读会抛 EISDIR) + * - 管道/设备等非普通文件——树里放一个命名 FIFO 叫 *.md,裸读会**永久阻塞** + * (实测 5s 未返回,只能 SIGKILL)。CI 里挂死比抛栈更难查,所以先 stat 再读。 + * try/catch 另外挡掉 EACCES 这类权限错。 + * + * 调用方把 null 当成“跳过这个文件”而不是抛——doctor 调 collect() 时没有 + * try/catch,一个权限异常的文件不该把整份六项报告换成裸栈。 + * + * forEachMarkdownLine 故意不走这里:它要把错误信息本身交给 findBrokenLinks + * 记成 unreadable 条目,而这里的契约是静默跳过,两种契约不该合并。 + */ +function readMarkdownText(root, file) { + const full = join(root, file); + if (!existsSync(full) || !statSync(full).isFile()) return null; + try { + return readFileSync(full, 'utf8'); + } catch { + return null; + } +} + +/** + * 读单个 markdown 并按行切好,读不到返回 null。 + * + * 拆成两个函数纯粹是为了各取所需:listDeclaredSkillNames 要整段 text 跑正则, + * 两个新检测要行数组(数行数、看开头 N 行)。包一层不花钱,也免得前者 + * 多做一次无意义的 split。只关心逐行内容的检测继续用 forEachMarkdownLine。 + */ +function readMarkdownLines(root, file) { + const text = readMarkdownText(root, file); + return text === null ? null : text.split(LINE_BREAK); +} + +/** + * 逐个取出一行里的本地链接目标,已剥掉 #anchor 与 ?query。 + * + * 注意:不跟踪 ``` 围栏状态,所以代码块里的链接同样会被取出来。 + * 目前真实仓库里没有“围栏内的坏链接”,不算问题;若以后新增示范用的 + * 错误链接片段(例如讲解断链时贴个反例),这里会误报,届时再加围栏开关。 + */ +export function* localLinkTargets(line) { + for (const match of line.matchAll(MD_LINK)) { + const raw = match[1]; + if (NON_LOCAL_LINK.test(raw)) continue; + const target = raw.split('#')[0].split('?')[0]; + if (target) yield target; + } +} + +/** + * 找出指向不存在文件的相对 markdown 链接。 + * + * 关键:以链接所在文件的目录为基准解析(模型就是这样读的), + * 而不是以仓库根目录为基准(作者往往这样心算)。 + * + * 返回项形如 { file, line, target };读不到的文件另记一条 + * { file, line: 0, target: null, error },让上层能区分“链接坏了”和“文件没读到”, + * 而不是把权限问题静默当成“没有断链”。 + */ +export function findBrokenLinks(root) { + const broken = []; + const unreadable = forEachMarkdownLine(root, (file, line, lineNumber) => { + for (const target of localLinkTargets(line)) { + if (!target.endsWith('.md')) continue; + const resolved = resolve(dirname(join(root, file)), target); + if (!existsSync(resolved) || !statSync(resolved).isFile()) { + broken.push({ file, line: lineNumber, target }); + } + } + }); + for (const entry of unreadable) { + broken.push({ file: entry.file, line: 0, target: null, error: entry.error }); + } + return broken; +} + +/** + * skill 名 token。 + * + * 只认 `edgeone-` 前缀是刻意保守:仓库里裸的 `makers-*` 大量撞车 + * (`makers-conversation-id` 是个 HTTP 头,还有 `makers-ai-voice-chat` 这类项目名), + * 放宽会把误报刷成噪音。 + * + * 代价是有盲区:`makers-cli` / `makers-cloud-functions` / `makers-migration` + * 这三个 skill 的 frontmatter 声明的就是裸名,正文里引用它们时本函数看不见。 + * 所以返回的条数是**下界**,别当成全覆盖。 + */ +const SKILL_NAME_TOKEN = /\bedgeone-(?:makers|pages)-[a-z0-9-]+/g; + +/** marketplace / plugin 的产品 slug,不是 skill 名,不参与悬空判定。 */ +const NON_SKILL_SLUGS = new Set(['edgeone-makers-tools']); + +/** markdown 链接里的锚点目标 `](#…)`,扫描 skill 名前先剥掉。 */ +const ANCHOR_LINK_TARGET = /\]\(#[^)]*\)/g; + +/** + * 各 SKILL.md frontmatter 声明的 name 集合。 + * + * 读不到就跳过而不是抛:这函数是 doctor 六项检查之一的输入, + * 一个权限异常的文件不该把整份报告换成裸栈。漏读的文件由 + * findBrokenLinks 那条 unreadable 记录负责报出来。 + */ +/** + * 收集所有 SKILL.md frontmatter 声明的 name。 + * + * 不能只看 root 的直接子目录:单 skill 路由结构下, + * skills/edgeone-makers-tools/SKILL.md 是路由页, + * 各能力的 SKILL.md 藏在 references// 里, + * 只扫一层会把 10 个真实存在的名字全判成悬空。 + * 改为遍历任意深度的 SKILL.md,兼容扁平与嵌套两种布局。 + */ +export function listDeclaredSkillNames(root) { + const names = new Set(); + for (const file of listMarkdownFiles(root)) { + if (file !== 'SKILL.md' && !file.endsWith('/SKILL.md')) continue; + const text = readMarkdownText(root, file); + if (text === null) continue; + const match = /^name:\s*(.+)$/m.exec(text); + if (match) names.add(match[1].trim().replace(/^["']|["']$/g, '')); + } + return names; +} + +/** + * 找出正文/description 里出现、但没有任何 skill 声明的 skill 名。 + * 模型会尝试加载这种名字,失败后重试成环。 + * + * 读不到的文件不在这里单独记账:findBrokenLinks 走的是同一批文件, + * 已经会把它们报出来,doctor 那层不需要同一个问题听三遍。 + * + * 锚点链接里的名字不算引用:`- [… EdgeOne Makers style](#…-edgeone-makers-style)` + * 这种目录条目,slug 会把散文标题压成看着像 skill 名的形状,但它指向的是 + * 本文件的小节,不是要加载的 skill。剥掉 `](#…)` 再扫,避免误报。 + */ +export function findDanglingSkillNames(root) { + const declared = listDeclaredSkillNames(root); + const dangling = []; + forEachMarkdownLine(root, (file, line, lineNumber) => { + const scannable = line.replace(ANCHOR_LINK_TARGET, '](#)'); + for (const match of scannable.matchAll(SKILL_NAME_TOKEN)) { + const name = match[0]; + if (declared.has(name) || NON_SKILL_SLUGS.has(name)) continue; + dangling.push({ file, line: lineNumber, name }); + } + }); + return dangling; +} + +/** + * 爬升两级以上的相对路径。 + * `(^|\/)` 这道前置守卫是为了别把 `..../../` 这类含 `....` 目录名的路径误判成两级爬升。 + */ +const DEEP_PARENT_LINK = /(^|\/)\.\.\/\.\.\//; + +/** + * 找出爬升两级以上的相对链接。 + * Anthropic 要求 reference 只下沉一层;`../../` 让模型在目录间反复横跳。 + * + * 复用 localLinkTargets:它剥的是 #anchor / ?query,不动 `../` 前缀, + * 所以判定与上报的 target 都保留原样的爬升层数,且与 findBrokenLinks + * 的 target 归一化方式一致。 + */ +export function findDeepReferenceLinks(root) { + const deep = []; + forEachMarkdownLine(root, (file, line, lineNumber) => { + for (const target of localLinkTargets(line)) { + if (DEEP_PARENT_LINK.test(target)) deep.push({ file, line: lineNumber, target }); + } + }); + return deep; +} + +export const TOC_LINE_THRESHOLD = 100; +export const MAX_FILE_LINES = 500; +const TOC_SCAN_LINES = 25; +const ANCHOR_LIST_ITEM = /^\s*(?:[-*]|\d+\.)\s*\[[^\]]+\]\(#/; + +/** + * 超 100 行、且开头 25 行内没有锚点目录的 reference。 + * 依据:Claude 可能只 head -100 部分读取,没有目录就拿不到全貌。 + * + * SKILL.md 不在此列:它是入口,模型总是整份读,且有 frontmatter 而非目录。 + * 用 `=== 'SKILL.md' || endsWith('/SKILL.md')` 而不是 endsWith('SKILL.md'): + * 后者会把 references/MY-SKILL.md 这种也一并豁免,从此永远拿不到目录且无人报错。 + * + * lines 的计法见 findOversizedFiles 的说明(是 split 段数,非 wc -l)。 + */ +export function findMissingTocs(root) { + const missing = []; + for (const file of listMarkdownFiles(root)) { + if (file === 'SKILL.md' || file.endsWith('/SKILL.md')) continue; + const lines = readMarkdownLines(root, file); + if (lines === null) continue; + if (lines.length <= TOC_LINE_THRESHOLD) continue; + if (lines.slice(0, TOC_SCAN_LINES).some((line) => ANCHOR_LIST_ITEM.test(line))) continue; + missing.push({ file, lines: lines.length }); + } + return missing; +} + +/** + * 超过 500 行的 md 文件(SKILL.md 与 reference 同一上限)。 + * + * 注意 lines 是 split(/\r?\n/) 的**段数**,不是 wc -l:以换行结尾的文件 + * 末尾会多出一个空串,所以 lines === wc -l + 1 + * (crewai.md:wc -l 601,这里报 602)。于是对这类文件实际卡的是 499 行正文。 + * 不改:602 这个基线已被 Task 3/11/16 三处断言,动它会连带失配。 + */ +export function findOversizedFiles(root) { + const over = []; + for (const file of listMarkdownFiles(root)) { + const lines = readMarkdownLines(root, file); + if (lines === null) continue; + if (lines.length > MAX_FILE_LINES) { + over.push({ file, lines: lines.length, cap: MAX_FILE_LINES }); + } + } + return over; +} + +/** + * _meta.json 的 files 数组 ↔ skills/ 下真实 md 文件。 + * missing = 磁盘有但没声明(不会发布到 SkillHub) + * extra = 声明了但磁盘没有(发布时会缺文件) + * 只比较 skills/ 前缀项,根级 SKILL.md / CLAUDE.md 不在管辖范围。 + * + * files 由调用方传入而不是在这里读 _meta.json:本模块的契约是 + * “给一个 root,返回纯数据”,不认识仓库根在哪。读文件是 doctor 那层的事。 + */ +export function checkFileManifest(root, files) { + const disk = new Set(listMarkdownFiles(root).map((file) => `skills/${file}`)); + const declared = new Set((files || []).filter((file) => String(file).startsWith('skills/'))); + return { + missing: [...disk].filter((file) => !declared.has(file)).sort(), + extra: [...declared].filter((file) => !disk.has(file)).sort(), + }; +} diff --git a/scripts/lib/skill-graph.test.mjs b/scripts/lib/skill-graph.test.mjs new file mode 100644 index 0000000..054a6e9 --- /dev/null +++ b/scripts/lib/skill-graph.test.mjs @@ -0,0 +1,560 @@ +import assert from 'node:assert/strict'; +import { chmod, mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import test from 'node:test'; + +import { + listDeclaredSkillNames, + checkFileManifest, + findBrokenLinks, + findDanglingSkillNames, + findDeepReferenceLinks, + findMissingTocs, + findOversizedFiles, + listMarkdownFiles, +} from './skill-graph.mjs'; + +/** 在临时目录里造一棵假的 skills 树,键是相对路径。 */ +async function makeSkills(tree) { + const root = await mkdtemp(join(tmpdir(), 'skill-graph-')); + for (const [relativePath, content] of Object.entries(tree)) { + const full = join(root, relativePath); + await mkdir(join(full, '..'), { recursive: true }); + await writeFile(full, content, 'utf8'); + } + return root; +} + +test('skill-graph.findBrokenLinks reports a link whose target does not exist', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': '---\nname: a\n---\n\nSee [kv](kv-storage.md) here.\n', + }); + try { + assert.deepEqual(findBrokenLinks(root), [ + { file: 'makers-a/SKILL.md', line: 5, target: 'kv-storage.md' }, + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findBrokenLinks accepts a link that resolves relative to its own file', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': '---\nname: a\n---\n\nSee [kv](references/kv.md).\n', + 'makers-a/references/kv.md': '# KV\n', + }); + try { + assert.deepEqual(findBrokenLinks(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findBrokenLinks resolves a one-level cross-skill link', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': '---\nname: a\n---\n\n[x](../makers-b/references/x.md)\n', + 'makers-b/references/x.md': '# X\n', + }); + try { + assert.deepEqual(findBrokenLinks(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findBrokenLinks flags the extra skills/ level bug', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': '---\nname: a\n---\n\n[x](../skills/makers-b/references/x.md)\n', + 'makers-b/references/x.md': '# X\n', + }); + try { + const broken = findBrokenLinks(root); + assert.equal(broken.length, 1); + assert.equal(broken[0].target, '../skills/makers-b/references/x.md'); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findBrokenLinks ignores http links and strips anchors', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': + '---\nname: a\n---\n\n[web](https://example.com/a.md) [anchor](references/kv.md#section)\n', + 'makers-a/references/kv.md': '# KV\n', + }); + try { + assert.deepEqual(findBrokenLinks(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findBrokenLinks fails with a readable message on a bad root', async () => { + const root = await makeSkills({ 'makers-a/SKILL.md': '---\nname: a\n---\n' }); + try { + assert.throws(() => findBrokenLinks(join(root, 'no-such-dir')), { + message: /root directory not found/, + }); + // 传文件而不是目录:原来会抛 ENOTDIR 裸栈。 + assert.throws(() => findBrokenLinks(join(root, 'makers-a/SKILL.md')), { + message: /not a directory/, + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findBrokenLinks keeps scanning when one file is unreadable', async () => { + const root = await makeSkills({ + 'makers-a/locked.md': '# locked\n', + 'makers-b/SKILL.md': '---\nname: b\n---\n\n[gone](missing.md)\n', + }); + const locked = join(root, 'makers-a/locked.md'); + try { + await chmod(locked, 0o000); + const broken = findBrokenLinks(root); + + // 后面那个文件的断链仍被发现,没有被前面的权限错误带走整轮扫描。 + assert.ok( + broken.some((item) => item.file === 'makers-b/SKILL.md' && item.target === 'missing.md'), + ); + // 读不到的文件被单独记录,而不是静默当成“没有断链”。 + const unreadable = broken.find((item) => item.file === 'makers-a/locked.md'); + assert.equal(unreadable.line, 0); + assert.equal(unreadable.target, null); + assert.match(unreadable.error, /EACCES/); + } finally { + // 先恢复权限,否则 rm 清不掉这棵树。 + await chmod(locked, 0o644); + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.listMarkdownFiles returns globally sorted paths', async () => { + const root = await makeSkills({ + 'a/z.md': '# z\n', + 'a-b.md': '# a-b\n', + 'b/c.md': '# c\n', + }); + try { + assert.deepEqual(listMarkdownFiles(root), ['a-b.md', 'a/z.md', 'b/c.md']); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDanglingSkillNames flags a name no skill declares', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': '---\nname: edgeone-makers-a\n---\n\nUse edgeone-pages-dev instead.\n', + }); + try { + assert.deepEqual(findDanglingSkillNames(root), [ + { file: 'makers-a/SKILL.md', line: 5, name: 'edgeone-pages-dev' }, + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDanglingSkillNames catches a dangling name inside description', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': + '---\nname: edgeone-makers-a\ndescription: >-\n Do NOT trigger for X (use edgeone-makers-dev instead).\n---\n\nBody.\n', + }); + try { + const dangling = findDanglingSkillNames(root); + assert.equal(dangling.length, 1); + assert.equal(dangling[0].name, 'edgeone-makers-dev'); + assert.equal(dangling[0].line, 4); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDanglingSkillNames accepts declared names and the marketplace slug', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': + '---\nname: edgeone-makers-a\n---\n\nSee edgeone-makers-b and edgeone-makers-tools.\n', + 'makers-b/SKILL.md': '---\nname: edgeone-makers-b\n---\n\nHi.\n', + }); + try { + assert.deepEqual(findDanglingSkillNames(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDanglingSkillNames ignores a name that only appears inside an anchor slug', async () => { + const root = await makeSkills({ + 'makers-a/references/long.md': + '# Long\n\n- [Remediation: from Vercel style to EdgeOne Makers style](#remediation-from-vercel-style-to-edgeone-makers-style)\n\n## Remediation: from Vercel style to EdgeOne Makers style\n', + }); + try { + assert.deepEqual(findDanglingSkillNames(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDanglingSkillNames still flags a real name on a line that also has an anchor link', async () => { + const root = await makeSkills({ + 'makers-a/references/long.md': + '# Long\n\nUse edgeone-pages-dev, see [Setup](#setup-edgeone-makers-style).\n\n## Setup edgeone makers style\n', + }); + try { + const dangling = findDanglingSkillNames(root); + assert.equal(dangling.length, 1); + assert.equal(dangling[0].name, 'edgeone-pages-dev'); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDeepReferenceLinks flags links that climb two levels', async () => { + const root = await makeSkills({ + 'makers-a/references/x.md': 'See [y](../../makers-b/references/y.md).\n', + 'makers-b/references/y.md': '# Y\n', + }); + try { + assert.deepEqual(findDeepReferenceLinks(root), [ + { file: 'makers-a/references/x.md', line: 1, target: '../../makers-b/references/y.md' }, + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDeepReferenceLinks allows single-level parent links', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': '---\nname: a\n---\n\n[y](../makers-b/references/y.md)\n', + 'makers-b/references/y.md': '# Y\n', + }); + try { + assert.deepEqual(findDeepReferenceLinks(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDeepReferenceLinks ignores a dotted dir name that only looks like two levels', async () => { + const root = await makeSkills({ + // `..../` 是个普通目录名,不是爬升两级;DEEP_PARENT_LINK 的 (^|/) 守卫就为这个。 + 'makers-a/SKILL.md': '---\nname: a\n---\n\n[x](..../../a.md)\n', + }); + try { + assert.deepEqual(findDeepReferenceLinks(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDanglingSkillNames survives an unreadable or non-file SKILL.md', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': '---\nname: edgeone-makers-a\n---\n', + 'makers-b/SKILL.md': '---\nname: edgeone-makers-b\n---\n\nUse edgeone-pages-dev.\n', + }); + const locked = join(root, 'makers-a/SKILL.md'); + try { + await chmod(locked, 0o000); + // SKILL.md 是个目录:原来会抛 EISDIR。 + await mkdir(join(root, 'makers-c/SKILL.md'), { recursive: true }); + + // 不抛——doctor 的六项检查不该被一个权限异常的文件换成裸栈。 + const dangling = findDanglingSkillNames(root); + + // 其余文件照常检查,没有被前面的权限错误带走整轮扫描。 + assert.deepEqual(dangling, [ + { file: 'makers-b/SKILL.md', line: 5, name: 'edgeone-pages-dev' }, + ]); + } finally { + // 先恢复权限,否则 rm 清不掉这棵树。 + await chmod(locked, 0o644); + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs flags a long reference without a table of contents', async () => { + const body = Array.from({ length: 120 }, (_, i) => `line ${i}`).join('\n'); + const root = await makeSkills({ 'makers-a/references/long.md': `# Long\n\n${body}\n` }); + try { + assert.deepEqual(findMissingTocs(root), [ + { file: 'makers-a/references/long.md', lines: 123 }, + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs accepts a long reference that opens with anchor links', async () => { + const body = Array.from({ length: 120 }, (_, i) => `line ${i}`).join('\n'); + const root = await makeSkills({ + 'makers-a/references/long.md': `# Long\n\n- [One](#one)\n- [Two](#two)\n\n${body}\n`, + }); + try { + assert.deepEqual(findMissingTocs(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs ignores short references and SKILL.md', async () => { + const body = Array.from({ length: 120 }, (_, i) => `line ${i}`).join('\n'); + const root = await makeSkills({ + 'makers-a/references/short.md': '# Short\n\nonly a few lines\n', + 'makers-a/SKILL.md': `---\nname: a\n---\n\n${body}\n`, + }); + try { + assert.deepEqual(findMissingTocs(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findOversizedFiles flags files above the 500-line cap', async () => { + const skillBody = Array.from({ length: 520 }, (_, i) => `s ${i}`).join('\n'); + const refBody = Array.from({ length: 520 }, (_, i) => `r ${i}`).join('\n'); + const root = await makeSkills({ + 'makers-a/SKILL.md': `---\nname: a\n---\n${skillBody}\n`, + 'makers-a/references/big.md': `# Big\n${refBody}\n`, + }); + try { + const over = findOversizedFiles(root); + assert.deepEqual(over.map((x) => x.file).sort(), [ + 'makers-a/SKILL.md', + 'makers-a/references/big.md', + ]); + assert.ok(over.every((x) => x.lines > 500 && x.cap === 500)); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs and findOversizedFiles survive an unreadable file', async () => { + const body = Array.from({ length: 600 }, (_, i) => `line ${i}`).join('\n'); + const root = await makeSkills({ + // 这个读不到:既够长会被目录检测盯上,也超 500 行会被行数检测盯上。 + 'makers-a/references/locked.md': `# Locked\n\n${body}\n`, + 'makers-b/references/plain.md': `# Plain\n\n${body}\n`, + }); + const locked = join(root, 'makers-a/references/locked.md'); + try { + await chmod(locked, 0o000); + + // 不抛——doctor 调 collect() 没有 try/catch,一个权限异常的文件 + // 不该把整份六项报告换成裸栈。 + const missing = findMissingTocs(root); + const over = findOversizedFiles(root); + + // 读不到的文件被跳过,不凭空报一条。 + assert.deepEqual( + missing.map((x) => x.file), + ['makers-b/references/plain.md'], + ); + assert.deepEqual( + over.map((x) => x.file), + ['makers-b/references/plain.md'], + ); + } finally { + // 先恢复权限,否则 rm 清不掉这棵树。 + await chmod(locked, 0o644); + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.checkFileManifest reports files on disk but absent from the manifest', async () => { + const root = await makeSkills({ + 'makers-a/SKILL.md': '---\nname: a\n---\n', + 'makers-a/references/x.md': '# X\n', + }); + try { + assert.deepEqual(checkFileManifest(root, ['skills/makers-a/SKILL.md']), { + missing: ['skills/makers-a/references/x.md'], + extra: [], + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.checkFileManifest reports manifest entries with no file on disk', async () => { + const root = await makeSkills({ 'makers-a/SKILL.md': '---\nname: a\n---\n' }); + try { + assert.deepEqual( + checkFileManifest(root, ['skills/makers-a/SKILL.md', 'skills/makers-ghost/SKILL.md']), + { missing: [], extra: ['skills/makers-ghost/SKILL.md'] }, + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.checkFileManifest ignores manifest entries outside skills/', async () => { + const root = await makeSkills({ 'makers-a/SKILL.md': '---\nname: a\n---\n' }); + try { + assert.deepEqual( + checkFileManifest(root, ['SKILL.md', 'CLAUDE.md', 'skills/makers-a/SKILL.md']), + { missing: [], extra: [] }, + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +/** 造一个正文足够长(必然超过目录阈值)的 reference,可选地在开头插几行。 */ +function longReference(head = '') { + const body = Array.from({ length: 120 }, (_, i) => `line ${i}`).join('\n'); + return `# Long\n\n${head}${body}\n`; +} + +test('skill-graph.findMissingTocs accepts an ordered-list table of contents', async () => { + // `1.` 这一支单独钉住:去掉 ANCHOR_LIST_ITEM 里的 \d+\. 分支后本用例才会红。 + const root = await makeSkills({ + 'makers-a/references/long.md': longReference('1. [One](#one)\n2. [Two](#two)\n\n'), + }); + try { + assert.deepEqual(findMissingTocs(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs accepts a star-bulleted table of contents', async () => { + // `*` 这一支单独钉住:去掉 [-*] 里的 * 后本用例才会红。 + const root = await makeSkills({ + 'makers-a/references/long.md': longReference('* [One](#one)\n* [Two](#two)\n\n'), + }); + try { + assert.deepEqual(findMissingTocs(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs still flags a long file whose prose merely links an anchor', async () => { + // 最危险的方向:漏报意味着 Task 15 永远不会给它补目录。 + // 正文里的行内锚点链接不是目录——它不在行首,ANCHOR_LIST_ITEM 的 ^ 与列表符号要求就为这个。 + const root = await makeSkills({ + 'makers-a/references/long.md': longReference('See [below](#tail) for details.\n\n'), + }); + try { + assert.deepEqual( + findMissingTocs(root).map((x) => x.file), + ['makers-a/references/long.md'], + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs still flags a long file whose only list link is not an anchor', async () => { + // 列表项里放的是普通文件链接而非 #锚点,不算目录:钉住 ANCHOR_LIST_ITEM 结尾的 \(# 要求。 + const root = await makeSkills({ + 'makers-a/references/long.md': longReference('- [One](one.md)\n- [Two](two.md)\n\n'), + 'makers-a/references/one.md': '# One\n', + 'makers-a/references/two.md': '# Two\n', + }); + try { + assert.deepEqual( + findMissingTocs(root).map((x) => x.file), + ['makers-a/references/long.md'], + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs still flags a long file whose mid-line dash precedes an anchor link', async () => { + // 钉住 ANCHOR_LIST_ITEM 的 ^:没有 ^ 时,这行里的「 - [Two](#two)」会被当成行首列表项, + // 于是一段普通散文被误认成目录(漏报),Task 15 就永远不会给它补目录。 + const root = await makeSkills({ + 'makers-a/references/long.md': longReference('Compare A - [Two](#two) is inline prose.\n\n'), + }); + try { + assert.deepEqual( + findMissingTocs(root).map((x) => x.file), + ['makers-a/references/long.md'], + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs treats 100 lines as short and 101 as long', async () => { + // 钉住 <= TOC_LINE_THRESHOLD 这道边界(而非 < )。 + // 行数是 split 段数:99 个 \n + 结尾换行 = 100 段。 + const root = await makeSkills({ + 'makers-a/references/at-100.md': `${Array.from({ length: 99 }, (_, i) => `a ${i}`).join('\n')}\n`, + 'makers-b/references/at-101.md': `${Array.from({ length: 100 }, (_, i) => `b ${i}`).join('\n')}\n`, + }); + try { + assert.deepEqual(findMissingTocs(root), [ + { file: 'makers-b/references/at-101.md', lines: 101 }, + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findOversizedFiles treats 500 lines as fine and 501 as oversized', async () => { + // 钉住 > MAX_FILE_LINES 这道边界(而非 >= )。 + const root = await makeSkills({ + 'makers-a/references/at-500.md': `${Array.from({ length: 499 }, (_, i) => `a ${i}`).join('\n')}\n`, + 'makers-b/references/at-501.md': `${Array.from({ length: 500 }, (_, i) => `b ${i}`).join('\n')}\n`, + }); + try { + assert.deepEqual(findOversizedFiles(root), [ + { file: 'makers-b/references/at-501.md', lines: 501, cap: 500 }, + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findMissingTocs only exempts a real SKILL.md, not a lookalike filename', async () => { + // endsWith('SKILL.md') 会把 MY-SKILL.md 一起豁免,而 Task 15 拿这个检测当验收门, + // 于是这种文件永远补不上目录也没人报。 + const root = await makeSkills({ + 'makers-a/references/MY-SKILL.md': longReference(), + 'makers-a/SKILL.md': `---\nname: a\n---\n\n${longReference()}`, + }); + try { + assert.deepEqual( + findMissingTocs(root).map((x) => x.file), + ['makers-a/references/MY-SKILL.md'], + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.listDeclaredSkillNames collects names from nested SKILL.md files', async () => { + // 单 skill 路由布局:路由页在顶层,各能力的 SKILL.md 藏在 references/ 下。 + const root = await makeSkills({ + 'tools/SKILL.md': '---\nname: edgeone-makers-tools\n---\n', + 'tools/references/cap-a/SKILL.md': '---\nname: edgeone-makers-a\n---\n', + 'tools/references/cap-b/SKILL.md': '---\nname: edgeone-makers-b\n---\n', + }); + try { + assert.deepEqual( + [...listDeclaredSkillNames(root)].sort(), + ['edgeone-makers-a', 'edgeone-makers-b', 'edgeone-makers-tools'], + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('skill-graph.findDanglingSkillNames accepts nested declarations in a single-router layout', async () => { + const root = await makeSkills({ + 'tools/SKILL.md': '---\nname: edgeone-makers-tools\n---\n\nSee edgeone-makers-a.\n', + 'tools/references/cap-a/SKILL.md': '---\nname: edgeone-makers-a\n---\n\nUse edgeone-pages-ghost.\n', + }); + try { + const dangling = findDanglingSkillNames(root); + assert.equal(dangling.length, 1); + assert.equal(dangling[0].name, 'edgeone-pages-ghost'); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/skills/edgeone-makers-tools/references/makers-agents/SKILL.md b/skills/edgeone-makers-tools/references/makers-agents/SKILL.md index 474e539..98648ab 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/SKILL.md +++ b/skills/edgeone-makers-tools/references/makers-agents/SKILL.md @@ -15,6 +15,15 @@ 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/** +validate: + - pattern: "process\\.env|os\\.environ" + message: "Read env via context.env inside agents/ and cloud-functions/, never process.env or os.environ (Critical Rule 3)." + - pattern: "headers\\s*\\.\\s*get\\s*\\(" + message: "Headers are plain objects here: context.request.headers['x-name'], not .get('x-name') (Critical Rule 4)." + - pattern: "langgraphStore\\s*\\?\\?\\s*store" + message: "Never write `store?.langgraphStore ?? store` — in cloud-function context it falls back to a store with no .get and crashes (Critical Rule 12)." metadata: author: edgeone version: "1.0.0" diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/capabilities/store.md b/skills/edgeone-makers-tools/references/makers-agents/references/capabilities/store.md index 24abab1..92036db 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/capabilities/store.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/capabilities/store.md @@ -1,5 +1,17 @@ # Memory / Store Cheat Sheet (Five Frameworks → context.store Adapters) +## Contents + +- [0. One-Sentence Mental Model](#0-one-sentence-mental-model) +- [1. Two Entry Points (First Decide Which Directory the Endpoint Lives In)](#1-two-entry-points-first-decide-which-directory-the-endpoint-lives-in) +- [2. Five-Framework Adapter Matrix (Core Cheat Sheet)](#2-five-framework-adapter-matrix-core-cheat-sheet) +- [3. API Signature Essentials (**single-object input — do not use two-arg form**)](#3-api-signature-essentials-single-object-input-do-not-use-two-arg-form) +- [4. Copy-Paste Snippets](#4-copy-paste-snippets) +- [5. Limits Cheat Sheet](#5-limits-cheat-sheet) +- [6. Choosing the Right Storage](#6-choosing-the-right-storage) +- [Python Store API (Route E and future Python routes)](#python-store-api-route-e-and-future-python-routes) +- [7. Review Red Lines (Spot Issues in 5 Seconds)](#7-review-red-lines-spot-issues-in-5-seconds) + > One-page reference: on EdgeOne Makers, which store entry point each Agent framework should use, how short-term/long-term memory is wired up, and how cloud-functions read it. --- diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/capabilities/tools.md b/skills/edgeone-makers-tools/references/makers-agents/references/capabilities/tools.md index 334827e..1dbf615 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/capabilities/tools.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/capabilities/tools.md @@ -1,5 +1,17 @@ # Tools Registry (context.tools) +## Contents + +- [2.1 ToolsContext Interface (@edgeone/pages-agent-toolkit)](#21-toolscontext-interface-edgeonepages-agent-toolkit) +- [2.2 Three Types of Tool Access](#22-three-types-of-tool-access) +- [2.3 Framework-Specific Tool Wiring](#23-framework-specific-tool-wiring) +- [2.4 Code Examples Per Framework](#24-code-examples-per-framework) +- [2.5 Getting a Single Tool / Group](#25-getting-a-single-tool-group) +- [⭐ web_search Configuration Requirements](#websearch-configuration-requirements) +- [web_search Return Value](#websearch-return-value) +- [web_search Input Parameters](#websearch-input-parameters) +- [web_search vs browser_* — when to use which](#websearch-vs-browser-when-to-use-which) + > Covers: ToolsContext interface, agents.framework-driven shape, 5-framework integration, built-in tools inventory. --- diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/framework-native-patterns.md b/skills/edgeone-makers-tools/references/makers-agents/references/framework-native-patterns.md index 90ab651..24a8369 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/framework-native-patterns.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/framework-native-patterns.md @@ -1,5 +1,15 @@ # Native Code Patterns Across Five Frameworks (Migration Reference) +## Contents + +- [0. Framework Positioning and Officially Recommended Path](#0-framework-positioning-and-officially-recommended-path) +- [1. LangGraph](#1-langgraph) +- [2. OpenAI Agents SDK](#2-openai-agents-sdk) +- [3. CrewAI](#3-crewai) +- [4. DeepAgents / LangChain createAgent](#4-deepagents-langchain-createagent) +- [5. Claude Agent SDK](#5-claude-agent-sdk) +- [6. Native → Makers Injection Cheat Sheet](#6-native-makers-injection-cheat-sheet) + > ⚠️ **Purpose**: This document shows the **official native patterns** for each Agent framework (i.e. what they look like *without* EdgeOne Makers injection). > **Do NOT copy these patterns into Makers templates** — on Makers, models go through the `context.env` gateway, tools come from `context.tools`, and storage comes from `context.store`. > Use this file to: ① understand the native shape of each framework, ② see by contrast what Makers injection saves you, ③ help teammates migrate from native usage to Makers. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/claude-sdk.md b/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/claude-sdk.md index 9b92bfb..b45db71 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/claude-sdk.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/claude-sdk.md @@ -1,5 +1,14 @@ # Route B: Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) +## Contents + +- [Dependencies](#dependencies) +- [When to Use Route B](#when-to-use-route-b) +- [Core Pattern Walkthrough](#core-pattern-walkthrough) +- [Route B Review Checklist](#route-b-review-checklist) +- [Frontend Call Example (chat + stop + file upload)](#frontend-call-example-chat-stop-file-upload) +- [See Also](#see-also) + > Use when: multi-step agentic flows, sandbox code execution, file processing, session memory. > Core pattern: `query()` + dual MCP servers (sandbox + custom tools) + session binding + SSE side channel. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/deepagents.md b/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/deepagents.md index 1a6ef55..6fdc425 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/deepagents.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/deepagents.md @@ -1,5 +1,13 @@ # DeepAgents (Node) +## Contents + +- [Dependencies](#dependencies) +- [When to Pick DeepAgents](#when-to-pick-deepagents) +- [Core Pattern](#core-pattern) +- [Memory](#memory) +- [Review Checklist](#review-checklist) + > Use when: long-running tasks with automatic context compression, sub-agent orchestration, middleware (retry/call-limit). > Core pattern: `createDeepAgent({ model, systemPrompt, tools, middleware })` + `agent.stream({ messages }, { streamMode })`. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/langgraph.md b/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/langgraph.md index 7ed328e..e0220b0 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/langgraph.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/langgraph.md @@ -1,5 +1,15 @@ # LangGraph (Node) +## Contents + +- [Dependencies](#dependencies) +- [When to Pick LangGraph](#when-to-pick-langgraph) +- [Core Pattern](#core-pattern) +- [Memory](#memory) +- [Stream Modes](#stream-modes) +- [Human-in-the-Loop](#human-in-the-loop) +- [Review Checklist](#review-checklist) + > Use when: fine-grained graph orchestration, custom node/edge control, human-in-the-loop (interrupt/resume), persistent thread state. > Core pattern: `StateGraph` + `compile({ checkpointer, store })` + `graph.stream()` → SSE. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/openai-agents.md b/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/openai-agents.md index 2aeb6e6..43da657 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/openai-agents.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/node-frameworks/openai-agents.md @@ -1,5 +1,14 @@ # Route C: OpenAI Agents SDK (`@openai/agents`) +## Contents + +- [Dependencies](#dependencies) +- [When to use Route C](#when-to-use-route-c) +- [Core pattern breakdown](#core-pattern-breakdown) +- [Route C review checklist](#route-c-review-checklist) +- [Frontend call examples](#frontend-call-examples) +- [Quick diff vs. other frameworks](#quick-diff-vs-other-frameworks) + > Use when: multi-agent collaboration (`handoff`), `guardrails`, or scenarios that need `Session` to auto-prepend history. > Core pattern: `Agent` + `run()` streaming + `context.store.openaiSession()` + event-to-SSE mapping. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/platform/node-entry.md b/skills/edgeone-makers-tools/references/makers-agents/references/platform/node-entry.md index ab11bc8..cfa0cd3 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/platform/node-entry.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/platform/node-entry.md @@ -1,5 +1,11 @@ # File Routing + onRequest Entry Convention +## Contents + +- [1. File Routing Convention](#1-file-routing-convention) +- [2. `onRequest` Entry Convention](#2-onrequest-entry-convention) +- [`externalNodeModules` (build config, usually not needed)](#externalnodemodules-build-config-usually-not-needed) + > Covers: file-based routing rules, `onRequest` signature, context fields, environment variable iron rule. --- diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/platform/python-entry.md b/skills/edgeone-makers-tools/references/makers-agents/references/platform/python-entry.md index 0b05389..d55e195 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/platform/python-entry.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/platform/python-entry.md @@ -1,5 +1,19 @@ # Python Agent Runtime Convention +## Contents + +- [Prerequisites](#prerequisites) +- [1. Entry Signature](#1-entry-signature) +- [2. Context Object (`ctx`)](#2-context-object-ctx) +- [3. SSE Streaming](#3-sse-streaming) +- [4. Return Values](#4-return-values) +- [5. Memory / Store API](#5-memory-store-api) +- [6. Abort / Stop Convention](#6-abort-stop-convention) +- [7. Node ↔ Python Naming Mapping](#7-node-python-naming-mapping) +- [8. Blocking Code (Critical for Python)](#8-blocking-code-critical-for-python) +- [9. File Routing (same as Node)](#9-file-routing-same-as-node) +- [See Also](#see-also) + > The Python agent runtime is an ASGI application (uvicorn). It shares the same platform conventions as the Node runtime (file-based routing, `makers-conversation-id` header contract, SSE protocol, etc.), but uses Python idioms. > Applies to: Route E (CrewAI) and any future Python-based routes (LangGraph Python, DeepAgents Python, etc.). diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/claude-sdk.md b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/claude-sdk.md index 27defce..e7c4b76 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/claude-sdk.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/claude-sdk.md @@ -1,5 +1,14 @@ # Route B (Python): Claude Agent SDK +## Contents + +- [Dependencies](#dependencies) +- [When to Use This Route (Python)](#when-to-use-this-route-python) +- [Core Pattern Breakdown](#core-pattern-breakdown) +- [Key Differences from Node Route B](#key-differences-from-node-route-b) +- [Review Checklist (Python Claude SDK)](#review-checklist-python-claude-sdk) +- [See Also](#see-also) + > Use when: multi-step agentic flows, sandbox code execution, file processing, session memory. > Core pattern: `claude_agent_sdk.query()` + MCP servers + session binding + SSE streaming. > Python runtime — see [../platform/python-entry.md](./../platform/python-entry.md) for entry signature, ctx object, and SSE conventions. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/crewai.md b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/crewai.md index 1052270..8d40ade 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/crewai.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/crewai.md @@ -1,5 +1,17 @@ # Route E: CrewAI (Python-only) +## Contents + +- [When to use Route E](#when-to-use-route-e) +- [⚠️ Key differences between Python and TS routes (in one shot)](#key-differences-between-python-and-ts-routes-in-one-shot) +- [Python Runtime Conventions (applies to all Python routes)](#python-runtime-conventions-applies-to-all-python-routes) +- [Core pattern breakdown](#core-pattern-breakdown) +- [Tool integration (context.tools)](#tool-integration-contexttools) +- [Route E review checklist](#route-e-review-checklist) +- [Frontend call example (frontend is TS, identical to other routes)](#frontend-call-example-frontend-is-ts-identical-to-other-routes) +- [Quick comparison vs. A/B/C/D](#quick-comparison-vs-abcd) +- [Common pitfalls](#common-pitfalls) + > Use when: multi-agent collaboration (role split + Sequential/Hierarchical Process), YAML-configured Agent/Task is desired, or you want to leverage CrewAI's built-in skills / event_bus capabilities. > Core pattern: `Crew(agents, tasks, process)` + `crew.kickoff()` + bridging events to SSE. > ⚠️ **CrewAI has no official JS SDK** — this is the **only route among the five that requires the Python runtime**. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/deepagents.md b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/deepagents.md index 2bc43cb..401a615 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/deepagents.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/deepagents.md @@ -1,5 +1,13 @@ # DeepAgents (Python) +## Contents + +- [Dependencies](#dependencies) +- [When to Pick DeepAgents (Python)](#when-to-pick-deepagents-python) +- [Core Pattern](#core-pattern) +- [Memory](#memory) +- [Review Checklist](#review-checklist) + > Use when: long-running tasks with automatic context compression, sub-agent orchestration, middleware. > Core pattern: `create_deep_agent()` + `agent.astream()` → SSE. > Python runtime — see [../platform/python-entry.md](./../platform/python-entry.md) for entry signature, ctx object, and SSE conventions. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/langgraph.md b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/langgraph.md index cea2975..6665e5b 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/langgraph.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/langgraph.md @@ -1,5 +1,15 @@ # LangGraph (Python) +## Contents + +- [Dependencies](#dependencies) +- [When to Pick LangGraph (Python)](#when-to-pick-langgraph-python) +- [Core Pattern](#core-pattern) +- [Memory](#memory) +- [Stream Modes](#stream-modes) +- [Human-in-the-Loop](#human-in-the-loop) +- [Review Checklist](#review-checklist) + > Use when: fine-grained graph orchestration, custom node/edge control, human-in-the-loop (interrupt/resume), persistent thread state. > Core pattern: `StateGraph` + `compile(checkpointer=..., store=...)` + `graph.astream()` → SSE. > Python runtime — see [../platform/python-entry.md](./../platform/python-entry.md) for entry signature, ctx object, and SSE conventions. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/openai-agents.md b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/openai-agents.md index 7679e76..b93bf80 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/openai-agents.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/python-frameworks/openai-agents.md @@ -1,5 +1,14 @@ # Route C (Python): OpenAI Agents SDK +## Contents + +- [Dependencies](#dependencies) +- [When to Use This Route (Python)](#when-to-use-this-route-python) +- [Core Pattern Breakdown](#core-pattern-breakdown) +- [Key Differences from Node Route C](#key-differences-from-node-route-c) +- [Review Checklist (Python OpenAI Agents)](#review-checklist-python-openai-agents) +- [See Also](#see-also) + > Use when: multi-agent collaboration (`handoff`), `guardrails`, or scenarios that need `Session` to auto-prepend history. > Core pattern: `Agent` + `Runner.run()` streaming + session + event-to-SSE mapping. > Python runtime — see [../platform/python-entry.md](./../platform/python-entry.md) for entry signature, ctx object, and SSE conventions. diff --git a/skills/edgeone-makers-tools/references/makers-agents/references/review-checklist.md b/skills/edgeone-makers-tools/references/makers-agents/references/review-checklist.md index fa34554..0da7a22 100644 --- a/skills/edgeone-makers-tools/references/makers-agents/references/review-checklist.md +++ b/skills/edgeone-makers-tools/references/makers-agents/references/review-checklist.md @@ -1,5 +1,20 @@ # EdgeOne Makers Agent Review Checklist +## Contents + +- [A. Directory Structure](#a-directory-structure) +- [B. Entry Point & Signature](#b-entry-point-signature) +- [C. Environment Variables & Models](#c-environment-variables-models) +- [D. SSE Protocol (where consistency tends to slip)](#d-sse-protocol-where-consistency-tends-to-slip) +- [E. Conversation ID & the /stop Dual Channel](#e-conversation-id-the-stop-dual-channel) +- [F. Robustness](#f-robustness) +- [G. Sandbox / Tools](#g-sandbox-tools) +- [H. Memory / Persistence (built on the official context.store API)](#h-memory-persistence-built-on-the-official-contextstore-api) +- [I. Frontend Integration (app/)](#i-frontend-integration-app) +- [Remediation Table: from "generic Vercel style" → "EdgeOne Makers style"](#remediation-table-from-generic-vercel-style-edgeone-makers-style) +- [J. Python Routes (Route E and future Python routes)](#j-python-routes-route-e-and-future-python-routes) +- [Bulk Refactor Workflow (SOP for teammates)](#bulk-refactor-workflow-sop-for-teammates) + > Purpose: when bulk-auditing or refactoring existing templates, walk this list top-to-bottom. Items marked ⚠️ are common foot-guns. --- diff --git a/skills/edgeone-makers-tools/references/makers-cloud-functions/SKILL.md b/skills/edgeone-makers-tools/references/makers-cloud-functions/SKILL.md index 7b8ce09..86832a1 100644 --- a/skills/edgeone-makers-tools/references/makers-cloud-functions/SKILL.md +++ b/skills/edgeone-makers-tools/references/makers-cloud-functions/SKILL.md @@ -3,6 +3,11 @@ name: edgeone-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/** +validate: + - pattern: "process\\.env|os\\.environ" + message: "Read env via context.env inside cloud-functions/, never process.env or os.environ." metadata: author: edgeone version: "1.0.0" diff --git a/skills/edgeone-makers-tools/references/makers-cloud-functions/references/go-functions.md b/skills/edgeone-makers-tools/references/makers-cloud-functions/references/go-functions.md index 1bf66ab..f2803af 100644 --- a/skills/edgeone-makers-tools/references/makers-cloud-functions/references/go-functions.md +++ b/skills/edgeone-makers-tools/references/makers-cloud-functions/references/go-functions.md @@ -1,5 +1,18 @@ # Go Functions +## Contents + +- [Development Modes](#development-modes) +- [Handler Mode](#handler-mode) +- [Framework Mode (Gin example)](#framework-mode-gin-example) +- [Framework Mode — Echo example](#framework-mode-echo-example) +- [File-system Routing (Handler mode)](#file-system-routing-handler-mode) +- [Dynamic Routes](#dynamic-routes) +- [Supported Frameworks](#supported-frameworks) +- [Local Development](#local-development) +- [Limits](#limits) +- [Template Projects](#template-projects) + Go runtime functions under `cloud-functions/`. High-performance compiled language with low memory footprint and fast cold start. Supports Handler mode (file-system routing) and Framework mode (Gin, Echo, Fiber, Chi). > **Runtime:** Go 1.26+ — cross-compiled automatically by the platform. No manual build configuration needed. diff --git a/skills/edgeone-makers-tools/references/makers-cloud-functions/references/node-functions.md b/skills/edgeone-makers-tools/references/makers-cloud-functions/references/node-functions.md index e0be788..b9f08da 100644 --- a/skills/edgeone-makers-tools/references/makers-cloud-functions/references/node-functions.md +++ b/skills/edgeone-makers-tools/references/makers-cloud-functions/references/node-functions.md @@ -1,5 +1,20 @@ # Node.js Functions +## Contents + +- [Basic function](#basic-function) +- [Handler methods](#handler-methods) +- [EventContext object](#eventcontext-object) +- [Using npm packages](#using-npm-packages) +- [Express integration](#express-integration) +- [Koa integration](#koa-integration) +- [File-system Routing](#file-system-routing) +- [Dynamic Routes](#dynamic-routes) +- [WebSocket](#websocket) +- [Local Development](#local-development) +- [Limits](#limits) +- [Template Projects](#template-projects) + Node.js v20.x runtime functions under `cloud-functions/`. Full npm ecosystem support. Ideal for complex backend logic, database access, Express/Koa frameworks, and WebSocket. > **Runtime:** Node.js v20.x — supports ES modules, full npm ecosystem, and WebSocket. diff --git a/skills/edgeone-makers-tools/references/makers-cloud-functions/references/python-functions.md b/skills/edgeone-makers-tools/references/makers-cloud-functions/references/python-functions.md index d09ccdc..0a4a115 100644 --- a/skills/edgeone-makers-tools/references/makers-cloud-functions/references/python-functions.md +++ b/skills/edgeone-makers-tools/references/makers-cloud-functions/references/python-functions.md @@ -1,5 +1,18 @@ # Python Functions +## Contents + +- [Development Modes](#development-modes) +- [Handler Mode](#handler-mode) +- [Flask Framework (WSGI)](#flask-framework-wsgi) +- [FastAPI Framework (ASGI)](#fastapi-framework-asgi) +- [File-system Routing](#file-system-routing) +- [Dynamic Route Parameters](#dynamic-route-parameters) +- [Dependency Management](#dependency-management) +- [Local Development](#local-development) +- [Limits](#limits) +- [Template Projects](#template-projects) + Python 3.10 runtime functions under `cloud-functions/`. Supports Handler class, WSGI (Flask/Django), and ASGI (FastAPI/Sanic) modes with automatic dependency detection. > **Runtime:** Python 3.10 — auto-detects framework, auto-installs dependencies, no manual configuration needed. diff --git a/skills/edgeone-makers-tools/references/makers-cloud-functions/references/troubleshooting.md b/skills/edgeone-makers-tools/references/makers-cloud-functions/references/troubleshooting.md index 139bb15..f61e82b 100644 --- a/skills/edgeone-makers-tools/references/makers-cloud-functions/references/troubleshooting.md +++ b/skills/edgeone-makers-tools/references/makers-cloud-functions/references/troubleshooting.md @@ -1,5 +1,15 @@ # Debugging & Troubleshooting +## Contents + +- [Local Preview / Dev Server Verification](#local-preview-dev-server-verification) +- [General Issues](#general-issues) +- [Edge Functions](#edge-functions) +- [KV Storage](#kv-storage) +- [Cloud Functions — Node.js](#cloud-functions-nodejs) +- [Cloud Functions — Go](#cloud-functions-go) +- [Cloud Functions — Python](#cloud-functions-python) + ## Local Preview / Dev Server Verification > **Scope: WorkBuddy only.** The symptoms and conclusions below were verified by hands-on testing in the **WorkBuddy** environment. diff --git a/skills/edgeone-makers-tools/references/makers-deploy/SKILL.md b/skills/edgeone-makers-tools/references/makers-deploy/SKILL.md index dc2dff9..c83ec64 100644 --- a/skills/edgeone-makers-tools/references/makers-deploy/SKILL.md +++ b/skills/edgeone-makers-tools/references/makers-deploy/SKILL.md @@ -12,6 +12,12 @@ 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: + - "*.sh" + - .github/workflows/** +validate: + - pattern: "whoami[^\\n]*\\s-t\\s" + message: "edgeone whoami does not accept -t. Check the exit code instead: 0 = logged in, 1 = not." metadata: author: edgeone version: "2.2.0" diff --git a/skills/edgeone-makers-tools/references/makers-edge-functions/SKILL.md b/skills/edgeone-makers-tools/references/makers-edge-functions/SKILL.md index b7d0502..98bcd31 100644 --- a/skills/edgeone-makers-tools/references/makers-edge-functions/SKILL.md +++ b/skills/edgeone-makers-tools/references/makers-edge-functions/SKILL.md @@ -13,6 +13,8 @@ validate: message: "Use plain object headers for this runtime surface." - pattern: "fs\\.writeFile" message: "Edge Functions do not support filesystem writes." + - pattern: "Response\\.json\\s*\\(" + message: "Response.json() is not available in this V8 runtime — use new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })." metadata: author: edgeone version: "1.0.0" diff --git a/skills/edgeone-makers-tools/references/makers-env-adaption/SKILL.md b/skills/edgeone-makers-tools/references/makers-env-adaption/SKILL.md index 495ae8a..6921ae9 100644 --- a/skills/edgeone-makers-tools/references/makers-env-adaption/SKILL.md +++ b/skills/edgeone-makers-tools/references/makers-env-adaption/SKILL.md @@ -7,6 +7,14 @@ description: >- Covers: non-interactive CLI flags, network isolation workarounds, login in sandbox, proxy bypass, file preview constraints (MUST use http:// via dev server, NEVER file://, NEVER python -m http.server / npx serve), dev server requirements. +pathPatterns: + - "*.sh" + - package.json +validate: + - pattern: "python\\s+-m\\s+http\\.server|npx\\s+(serve|http-server)" + message: "Use `edgeone makers dev` — self-hosted static servers skip Blob credentials, Cloud Functions routing, Edge Functions and middleware." + - pattern: "localhost:80(88|89)" + message: "Use 127.0.0.1, not localhost — in the sandbox localhost resolves to ::1 and yields false 404s." metadata: author: edgeone version: "1.1.1" diff --git a/skills/edgeone-makers-tools/references/makers-middleware/SKILL.md b/skills/edgeone-makers-tools/references/makers-middleware/SKILL.md index 751cc3d..c1d580e 100644 --- a/skills/edgeone-makers-tools/references/makers-middleware/SKILL.md +++ b/skills/edgeone-makers-tools/references/makers-middleware/SKILL.md @@ -3,6 +3,12 @@ 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.js + - middleware.ts +validate: + - pattern: "NextRequest|NextResponse|next/server" + message: "Framework projects must use the framework's own middleware. This platform format takes a context object with next/redirect/rewrite — not NextRequest/NextResponse." metadata: author: edgeone version: "1.0.0" diff --git a/skills/edgeone-makers-tools/references/makers-migration/SKILL.md b/skills/edgeone-makers-tools/references/makers-migration/SKILL.md index afe05be..94826dc 100644 --- a/skills/edgeone-makers-tools/references/makers-migration/SKILL.md +++ b/skills/edgeone-makers-tools/references/makers-migration/SKILL.md @@ -7,6 +7,12 @@ description: >- convert Express/Next.js API routes to Makers handlers, or add platform capabilities (context.tools, context.sandbox, context.store). Do NOT trigger for new agent projects (use makers-agents instead). +pathPatterns: + - agents/** + - cloud-functions/** +validate: + - pattern: "process\\.env|os\\.environ" + message: "Migration checklist: replace process.env / os.environ with context.env (TS) or ctx env access (Python)." metadata: author: edgeone version: "1.0.0" diff --git a/skills/edgeone-makers-tools/references/makers-migration/references/api-route-to-makers.md b/skills/edgeone-makers-tools/references/makers-migration/references/api-route-to-makers.md index 5cd5b30..890add0 100644 --- a/skills/edgeone-makers-tools/references/makers-migration/references/api-route-to-makers.md +++ b/skills/edgeone-makers-tools/references/makers-migration/references/api-route-to-makers.md @@ -1,5 +1,11 @@ # Migration: Generic API Route → EdgeOne Makers (no-framework path) +## Contents + +- [Example 1: Next.js API Route (app router)](#example-1-nextjs-api-route-app-router) +- [Example 2: Express Router](#example-2-express-router) +- [Conversion Checklist](#conversion-checklist) + > This is the **framework-less fallback**. Use it when your project does **not** use any of the five > agent frameworks (LangGraph, DeepAgents, OpenAI Agents SDK, Claude Agent SDK, CrewAI) — e.g. a custom > agent loop wrapped in Express, a Next.js API route, or a plain HTTP endpoint. diff --git a/skills/edgeone-makers-tools/references/makers-migration/references/claude-agent-sdk-to-makers.md b/skills/edgeone-makers-tools/references/makers-migration/references/claude-agent-sdk-to-makers.md index 989d4a7..18d896a 100644 --- a/skills/edgeone-makers-tools/references/makers-migration/references/claude-agent-sdk-to-makers.md +++ b/skills/edgeone-makers-tools/references/makers-migration/references/claude-agent-sdk-to-makers.md @@ -1,5 +1,11 @@ # Migration: Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) → EdgeOne Makers +## Contents + +- [Node](#node) +- [Python equivalent](#python-equivalent) +- [Conversion Checklist](#conversion-checklist) + Claude Agent SDK runs a Claude Code subprocess, so the biggest differences are: (1) route the model through AI Gateway by mapping `AI_GATEWAY_*` → `ANTHROPIC_*`, (2) provide a writable config/temp dir, (3) wire platform tools via `toClaudeMcpServer`, and (4) bind session memory through `claudeSessionStore`. --- diff --git a/skills/edgeone-makers-tools/references/makers-migration/references/crewai-to-makers.md b/skills/edgeone-makers-tools/references/makers-migration/references/crewai-to-makers.md index 29441c4..2637e84 100644 --- a/skills/edgeone-makers-tools/references/makers-migration/references/crewai-to-makers.md +++ b/skills/edgeone-makers-tools/references/makers-migration/references/crewai-to-makers.md @@ -1,5 +1,11 @@ # Migration: CrewAI (Python-only) → EdgeOne Makers +## Contents + +- [❌ Before — native CrewAI (Flask + direct OpenAI + LiteLLM)](#before-native-crewai-flask-direct-openai-litellm) +- [✅ After — Makers agent handler](#after-makers-agent-handler) +- [Conversion Checklist](#conversion-checklist) + CrewAI has no JS SDK, so this route is Python-only. The migration is about: routing the LLM through AI Gateway (`provider="openai"` to bypass LiteLLM), dropping `memory=True`/`verbose=True` for platform conventions, wrapping `crew.kickoff()` in a thread, and bridging the event_bus to Makers SSE. --- diff --git a/skills/edgeone-makers-tools/references/makers-migration/references/deepagents-to-makers.md b/skills/edgeone-makers-tools/references/makers-migration/references/deepagents-to-makers.md index c95ff40..d0c293c 100644 --- a/skills/edgeone-makers-tools/references/makers-migration/references/deepagents-to-makers.md +++ b/skills/edgeone-makers-tools/references/makers-migration/references/deepagents-to-makers.md @@ -1,5 +1,11 @@ # Migration: DeepAgents (Node + Python) → EdgeOne Makers +## Contents + +- [Node](#node) +- [Python equivalent](#python-equivalent) +- [Conversion Checklist](#conversion-checklist) + DeepAgents is a thin layer over LangGraph, so the migration is almost identical to LangGraph: swap the model endpoint, drop custom tools for `context.tools`, and replace the HTTP server with an `onRequest` handler. Memory reuses the LangGraph adapters. --- diff --git a/skills/edgeone-makers-tools/references/makers-migration/references/langgraph-to-makers.md b/skills/edgeone-makers-tools/references/makers-migration/references/langgraph-to-makers.md index 9ec8b92..ef6fd64 100644 --- a/skills/edgeone-makers-tools/references/makers-migration/references/langgraph-to-makers.md +++ b/skills/edgeone-makers-tools/references/makers-migration/references/langgraph-to-makers.md @@ -1,5 +1,11 @@ # Migration: LangGraph (Node + Python) → EdgeOne Makers +## Contents + +- [Node](#node) +- [Python equivalent](#python-equivalent) +- [Conversion Checklist](#conversion-checklist) + LangGraph's graph/state API is unchanged on Makers — you only swap three things: model endpoint, checkpointer/store backend, and the HTTP server. This file shows the native format and the exact changes. --- diff --git a/skills/edgeone-makers-tools/references/makers-migration/references/openai-agents-to-makers.md b/skills/edgeone-makers-tools/references/makers-migration/references/openai-agents-to-makers.md index 41da189..beb999d 100644 --- a/skills/edgeone-makers-tools/references/makers-migration/references/openai-agents-to-makers.md +++ b/skills/edgeone-makers-tools/references/makers-migration/references/openai-agents-to-makers.md @@ -1,5 +1,11 @@ # Migration: OpenAI Agents SDK (`@openai/agents`) → EdgeOne Makers +## Contents + +- [Node](#node) +- [Python equivalent](#python-equivalent) +- [Conversion Checklist](#conversion-checklist) + OpenAI Agents SDK has no special server format — it runs anywhere Node runs. The migration is about: routing the model through AI Gateway, replacing hand-built tools with `context.tools.all()`, swapping hand-managed history for `openaiSession`, and mapping SDK stream events to Makers SSE. --- diff --git a/skills/edgeone-makers-tools/references/makers-storage/SKILL.md b/skills/edgeone-makers-tools/references/makers-storage/SKILL.md index 304f586..514a977 100644 --- a/skills/edgeone-makers-tools/references/makers-storage/SKILL.md +++ b/skills/edgeone-makers-tools/references/makers-storage/SKILL.md @@ -3,6 +3,11 @@ name: edgeone-makers-storage description: >- KV and Blob storage services on EdgeOne Makers. KV for edge key-value pairs, Blob for file/object storage in Cloud Functions. Covers SDK usage, setup, and troubleshooting. +pathPatterns: + - edge-functions/** +validate: + - pattern: "context\\.env\\.[A-Za-z_]*[Kk][Vv]" + message: "KV is a console-bound global variable, not on context.env — call my_kv.get(...) directly. See makers-storage/references/kv.md." metadata: author: edgeone version: "1.0.0" diff --git a/skills/edgeone-makers-tools/references/makers-storage/references/blob.md b/skills/edgeone-makers-tools/references/makers-storage/references/blob.md index 5951485..03997f2 100644 --- a/skills/edgeone-makers-tools/references/makers-storage/references/blob.md +++ b/skills/edgeone-makers-tools/references/makers-storage/references/blob.md @@ -1,5 +1,16 @@ # Blob Storage +## Contents + +- [Quick Start](#quick-start) +- [Blob as your backend (there is no database)](#blob-as-your-backend-there-is-no-database) +- [Consistency Model](#consistency-model) +- [API Reference](#api-reference) +- [Examples](#examples) +- [Limits](#limits) +- [Common Errors](#common-errors) +- [Best Practices](#best-practices) + EdgeOne Makers Blob is a distributed **object storage** service for Makers Functions. Suitable for storing images, documents, user uploads, AI-generated content, and structured data sets. > ⚠️ Blob is for **Makers Functions (Cloud Functions)** — uses the `@edgeone/pages-blob` npm SDK (NOT a global variable like KV). diff --git a/skills/edgeone-makers-tools/references/makers-storage/references/kv.md b/skills/edgeone-makers-tools/references/makers-storage/references/kv.md index 65cf61d..e59447f 100644 --- a/skills/edgeone-makers-tools/references/makers-storage/references/kv.md +++ b/skills/edgeone-makers-tools/references/makers-storage/references/kv.md @@ -1,5 +1,17 @@ # KV Storage +## Contents + +- [Prerequisites (MUST complete before using KV)](#prerequisites-must-complete-before-using-kv) +- [Core Concept: KV is a Global Variable](#core-concept-kv-is-a-global-variable) +- [API Reference](#api-reference) +- [Examples](#examples) +- [Common Errors](#common-errors) +- [Limits](#limits) +- [Best Practices](#best-practices) +- [Local Development](#local-development) +- [Production Deployment](#production-deployment) + EdgeOne Makers KV is a globally distributed **key-value persistent storage** service deployed across multiple edge nodes. Data follows an eventual consistency model and synchronizes globally within **60 seconds**. > ⚠️ KV Storage is **only available in Edge Functions** — NOT supported in Node Functions.