@wenaixi/cfbridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ // cfbridge 综合测试入口。
2
+ // 默认运行所有测试;如果提供参数则只跑匹配的子集。
3
+ // node scripts/test.js # 全部
4
+ // node scripts/test.js check # 只跑仓库静态检查
5
+ // node scripts/test.js bundle # 只跑 bundle 静态校验
6
+ // node scripts/test.js wrangler # 只跑 Wrangler 只读验证
7
+ //
8
+ // v0.3.0 调整:
9
+ // - 旧 subset 名 preset 改为 bundle(语义对齐)。
10
+ // - 不再依赖 test-mcp.js(v0.3.0 起 MCP 三工具通过 dsh 加载;不写 standalone
11
+ // smoke,避免在 CI 中误触发真实 Cloudflare API 调用)。
12
+
13
+ const { spawnSync } = require('child_process')
14
+ const path = require('path')
15
+
16
+ const ROOT = path.resolve(__dirname, '..')
17
+ const subset = process.argv[2]
18
+
19
+ const STEPS = [
20
+ { name: 'check', label: '仓库配置与安全检查(v0.3.0 bundle 入口 + 反转 web patch 校验)', script: 'check.js' },
21
+ { name: 'bundle', label: 'Bundle manifest 与结构校验(cordis.patch.yml / SKILL.md / 脚本归档)', script: 'validate-bundle.js' },
22
+ { name: 'wrangler', label: 'Wrangler CLI 只读验证', script: 'test-wrangler.js' },
23
+ ]
24
+
25
+ function runStep(step) {
26
+ console.log(`\n=== ${step.label} ===`)
27
+ const result = spawnSync(process.execPath, [path.join(__dirname, step.script)], {
28
+ cwd: ROOT, stdio: 'inherit',
29
+ })
30
+ return result.status === 0
31
+ }
32
+
33
+ function main() {
34
+ const steps = subset ? STEPS.filter((s) => s.name === subset) : STEPS
35
+ if (subset && steps.length === 0) {
36
+ console.error(`Unknown subset: ${subset}`)
37
+ console.error(`Available: ${STEPS.map((s) => s.name).join(', ')}`)
38
+ process.exit(1)
39
+ }
40
+
41
+ const total = steps.length
42
+ let passed = 0
43
+ for (const step of steps) if (runStep(step)) passed++
44
+
45
+ console.log(`\n=== Summary ===`)
46
+ console.log(`${passed}/${total} step(s) passed`)
47
+ process.exit(passed === total ? 0 : 1)
48
+ }
49
+
50
+ main()
@@ -0,0 +1,131 @@
1
+ // 一键卸载 cfbridge v0.3.0 Bundle。
2
+ //
3
+ // 流程:
4
+ // 1. 调用 `dsh plugin --profile <name> remove @wenaixi/cfbridge`,DSH 会
5
+ // 自动从 dsh.profile.bundles 移除本层并清理 node_modules。
6
+ // 2. 清理旧版本残留的软链 $DSH_HOME/skills/cfbridge/(若存在)。
7
+ // 3. 调 `dsh --profile <name> --dump-config` 验证层不再出现 cfbridge。
8
+ // 4. 提示 v0.2.0 旧 preset 残留的可选清理。
9
+ //
10
+ // 使用:
11
+ // npm run uninstall:bundle
12
+ // npm run uninstall:bundle -- --profile tui
13
+
14
+ const fs = require('fs')
15
+ const path = require('path')
16
+ const { spawnSync } = require('child_process')
17
+
18
+ const ROOT = path.resolve(__dirname, '..')
19
+ const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'))
20
+ const DSH_HOME = process.env.DSH_HOME || path.join(process.env.USERPROFILE || process.env.HOME || '', '.dsh')
21
+ const SKILL_LINK = path.join(DSH_HOME, 'skills', 'cfbridge')
22
+ const LEGACY_PRESET = path.join(DSH_HOME, '.agent-presets', 'cfbridge')
23
+
24
+ function log(level, msg) {
25
+ const prefix = { info: 'INFO', ok: 'OK ', warn: 'WARN', err: 'ERR ' }[level] || 'INFO'
26
+ console.log(`[${prefix}] ${msg}`)
27
+ }
28
+
29
+ function parseArgs(argv) {
30
+ const opts = { profile: 'web', yes: false }
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const a = argv[i]
33
+ if (a === '--profile' || a === '-p') opts.profile = argv[++i]
34
+ else if (a.startsWith('--profile=')) opts.profile = a.slice('--profile='.length)
35
+ else if (a === '--yes' || a === '-y') opts.yes = true
36
+ else if (a === '--help' || a === '-h') { printHelp(); process.exit(0) }
37
+ else log('warn', `Unknown argument ignored: ${a}`)
38
+ }
39
+ return opts
40
+ }
41
+
42
+ function printHelp() {
43
+ console.log(`Usage: uninstall-bundle.js [--profile <name>] [--yes]
44
+
45
+ Options:
46
+ --profile <name> Target DSH profile (default: web).
47
+ --yes, -y Skip interactive confirmation (default: prompt unless --yes).
48
+ --help, -h Show this message.`)
49
+ }
50
+
51
+ function run(cmd, args, opts = {}) {
52
+ return spawnSync(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts })
53
+ }
54
+
55
+ function runCapture(cmd, args, opts = {}) {
56
+ return spawnSync(cmd, args, { stdio: 'pipe', shell: process.platform === 'win32', ...opts })
57
+ }
58
+
59
+ function removeSkillLink() {
60
+ try {
61
+ const stat = fs.lstatSync(SKILL_LINK)
62
+ if (!stat.isSymbolicLink() && !stat.isDirectory()) return
63
+ fs.rmSync(SKILL_LINK, { recursive: true, force: true })
64
+ log('ok', `Removed skill link (legacy): ${SKILL_LINK}`)
65
+ } catch (e) {
66
+ if (e.code === 'ENOENT') log('info', `No skill link at ${SKILL_LINK}; nothing to remove.`)
67
+ else log('warn', `Failed to remove ${SKILL_LINK}: ${e.message}`)
68
+ }
69
+ }
70
+
71
+ function dumpConfig(profile) {
72
+ log('info', `Verifying composed config for profile "${profile}"…`)
73
+ const r = runCapture('dsh', ['--profile', profile, '--dump-config'])
74
+ const out = String(r.stdout || '')
75
+ if (r.status !== 0) {
76
+ log('warn', `dsh --dump-config exited with ${r.status}; skipping verification.`)
77
+ return
78
+ }
79
+ if (/==\s*@wenaixi\/cfbridge\b/.test(out)) {
80
+ log('err', 'cfbridge layer is still present after remove. Inspect the dump above.')
81
+ process.exit(1)
82
+ }
83
+ log('ok', 'cfbridge layer absent from composed profile tree.')
84
+ }
85
+
86
+ function confirm(question) {
87
+ if (process.env.CI === 'true' || process.env.NONINTERACTIVE === 'true') return true
88
+ return new Promise((resolve) => {
89
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout })
90
+ rl.question(`${question} [y/N] `, (answer) => {
91
+ rl.close()
92
+ resolve(/^y(es)?$/i.test(String(answer).trim()))
93
+ })
94
+ })
95
+ }
96
+
97
+ async function main() {
98
+ const opts = parseArgs(process.argv.slice(2))
99
+ log('info', `Source bundle: ${ROOT} (${PKG.name}@${PKG.version})`)
100
+ log('info', `Target profile: ${opts.profile}`)
101
+
102
+ if (!opts.yes) {
103
+ const ok = await confirm(`Uninstall ${PKG.name} from profile "${opts.profile}"?`)
104
+ if (!ok) {
105
+ log('info', 'Aborted by user.')
106
+ process.exit(0)
107
+ }
108
+ }
109
+
110
+ log('info', `Running: dsh plugin --profile ${opts.profile} remove ${PKG.name}`)
111
+ const r = run('dsh', ['plugin', '--profile', opts.profile, 'remove', PKG.name])
112
+ if (r.status !== 0) {
113
+ log('err', `dsh plugin remove failed (exit ${r.status}).`)
114
+ process.exit(r.status ?? 1)
115
+ }
116
+
117
+ removeSkillLink()
118
+ dumpConfig(opts.profile)
119
+
120
+ if (fs.existsSync(LEGACY_PRESET)) {
121
+ log('warn', `Legacy v0.2.0 preset still exists at ${LEGACY_PRESET}.`)
122
+ log('info', 'Run `npm run migrate:from-preset` to remove it.')
123
+ }
124
+
125
+ log('ok', 'Uninstall complete. Restart DSH to drop tools and skill from running sessions.')
126
+ }
127
+
128
+ main().catch((e) => {
129
+ log('err', e.stack || e.message)
130
+ process.exit(1)
131
+ })
@@ -0,0 +1,11 @@
1
+ // cfbridge v0.3.0 起 bundle 形态取代 agent preset。
2
+ // 旧的 ~/.dsh/.agent-presets/cfbridge/ 目录不再被 DSH 加载,
3
+ // 如需彻底清理请改用 `npm run migrate:from-preset`(带 --yes 才真删)。
4
+ //
5
+ // 本 shim 仅打印警告并退出非零,避免误以为已卸载干净。
6
+
7
+ const log = (msg) => console.log(`[WARN] ${msg}`)
8
+ log('uninstall:preset is deprecated since v0.3.0 (cfbridge is now a DSH bundle).')
9
+ log('The legacy preset directory is no longer loaded by DSH, but it may still exist on disk.')
10
+ log('Use `npm run migrate:from-preset -- --yes` to remove it.')
11
+ process.exit(1)
@@ -0,0 +1,232 @@
1
+ // 静态校验 cfbridge v0.3.0 Bundle 的结构与安全属性。
2
+ //
3
+ // 检查项:
4
+ // - package.json 含 dsh.bundle.patch 且指向真实文件
5
+ // - cordis.patch.yml 存在、可解析,并含 mcp-cloudflare 行
6
+ // - Authorization 用 !!js 动态引用 process.env.CLOUDFLARE_API_TOKEN
7
+ // - skills/cfbridge/SKILL.md 存在且不含 token 痕迹
8
+ // - scripts/install-bundle.js / uninstall-bundle.js / migrate-from-preset.js 存在
9
+ // - deprecated/preset/ 下包含 DEPRECATED 标记的旧 preset 文件
10
+ // - 共享 hasTokenLeak 正则扫所有受跟踪文件
11
+ //
12
+ // 使用:npm run validate:bundle
13
+
14
+ const fs = require('fs')
15
+ const path = require('path')
16
+
17
+ const ROOT = path.resolve(__dirname, '..')
18
+
19
+ const results = []
20
+ function pass(name, detail = '') { results.push({ ok: true, name, detail }) }
21
+ function fail(name, msg) { results.push({ ok: false, name, msg }) }
22
+
23
+ function readText(p) {
24
+ try { return fs.readFileSync(p, 'utf8') } catch { return '' }
25
+ }
26
+
27
+ function hasTokenLeak(text) {
28
+ const patterns = [
29
+ /cfat_[A-Za-z0-9]{16,}/,
30
+ /cfut_[A-Za-z0-9]{16,}/,
31
+ /cfoat_[A-Za-z0-9]{16,}/,
32
+ /sk-[A-Za-z0-9-]{16,}/,
33
+ /Bearer\s+[A-Za-z0-9_-]{40,}/,
34
+ /gh[pousr]_[A-Za-z0-9]{30,}/,
35
+ /AKIA[0-9A-Z]{16}/,
36
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
37
+ /AIza[0-9A-Za-z_-]{35}/,
38
+ ]
39
+ return patterns.some((re) => re.test(text))
40
+ }
41
+
42
+ function main() {
43
+ // 1. package.json 含 dsh.bundle.patch
44
+ const pkgPath = path.join(ROOT, 'package.json')
45
+ let pkg = null
46
+ try { pkg = JSON.parse(readText(pkgPath)) } catch (e) { fail('package.json readable', e.message) }
47
+ if (pkg) {
48
+ const patchRel = pkg.dsh && pkg.dsh.bundle && pkg.dsh.bundle.patch
49
+ if (typeof patchRel === 'string' && patchRel.length > 0) {
50
+ pass('package.json declares dsh.bundle.patch', patchRel)
51
+ const absPatch = path.resolve(ROOT, patchRel)
52
+ if (fs.existsSync(absPatch)) pass('dsh.bundle.patch file exists', absPatch)
53
+ else fail('dsh.bundle.patch file exists', `missing: ${absPatch}`)
54
+ } else {
55
+ fail('package.json declares dsh.bundle.patch', 'missing dsh.bundle.patch')
56
+ }
57
+
58
+ if (pkg.name === '@wenaixi/cfbridge') pass('package name is @wenaixi/cfbridge')
59
+ else fail('package name is @wenaixi/cfbridge', pkg.name)
60
+ if (pkg.version === '0.1.0') pass('package version is 0.1.0')
61
+ else fail('package version is 0.1.0', pkg.version)
62
+ if (pkg.private !== true) pass('package is not private (npm publishable)')
63
+ else fail('package is not private (npm publishable)', 'private must be absent/false')
64
+ if (pkg.publishConfig && pkg.publishConfig.access === 'public') pass('publishConfig.access is public')
65
+ else fail('publishConfig.access is public', `got: ${pkg.publishConfig && pkg.publishConfig.access}`)
66
+ if (/github\.com\/Wenaixi\/dsh-cfbridge(\.git)?$/i.test((pkg.repository && pkg.repository.url) || '')) pass('repository points to Wenaixi/dsh-cfbridge')
67
+ else fail('repository points to Wenaixi/dsh-cfbridge', pkg.repository && pkg.repository.url)
68
+
69
+ const files = Array.isArray(pkg.files) ? pkg.files : []
70
+ const need = ['cordis.patch.yml', 'src/', 'skills/', 'scripts/', 'README.md', 'LICENSE']
71
+ for (const entry of need) {
72
+ if (files.includes(entry)) pass(`files[] includes ${entry}`)
73
+ else fail(`files[] includes ${entry}`, `missing in package.json files`)
74
+ }
75
+
76
+ const scripts = pkg.scripts || {}
77
+ for (const s of ['install:bundle', 'uninstall:bundle', 'validate:bundle', 'migrate:from-preset', 'check', 'test']) {
78
+ if (scripts[s]) pass(`script "${s}" defined`)
79
+ else fail(`script "${s}" defined`, 'missing in scripts')
80
+ }
81
+ }
82
+
83
+ // 2. cordis.patch.yml 内容
84
+ const patchPath = path.join(ROOT, 'cordis.patch.yml')
85
+ const patchText = readText(patchPath)
86
+ if (patchText) {
87
+ pass('cordis.patch.yml exists')
88
+ if (/^\s*-\s+id:\s*mcp-cloudflare\b/m.test(patchText)) pass('cordis.patch.yml has mcp-cloudflare row')
89
+ else fail('cordis.patch.yml has mcp-cloudflare row', 'missing id: mcp-cloudflare')
90
+ if (/serverName:\s*cloudflare\b/.test(patchText)) pass('cordis.patch.yml pins serverName: cloudflare')
91
+ else fail('cordis.patch.yml pins serverName: cloudflare', 'missing')
92
+ if (/url:\s*https:\/\/mcp\.cloudflare\.com\/mcp\b/.test(patchText)) pass('cordis.patch.yml pins Cloudflare MCP URL')
93
+ else fail('cordis.patch.yml pins Cloudflare MCP URL', 'missing')
94
+ if (/Authorization:\s*!!js\s+'`Bearer \$\{process\.env\.CLOUDFLARE_API_TOKEN\}`'/.test(patchText)) pass('cordis.patch.yml uses !!js Authorization template')
95
+ else fail('cordis.patch.yml uses !!js Authorization template', 'token reference must use !!js')
96
+ if (/failOnStartupError:\s*false\b/.test(patchText)) pass('cordis.patch.yml has failOnStartupError: false')
97
+ else fail('cordis.patch.yml has failOnStartupError: false', 'must keep false to avoid blocking DSH when token missing')
98
+ if (!hasTokenLeak(patchText)) pass('cordis.patch.yml has no hardcoded token')
99
+ else fail('cordis.patch.yml has no hardcoded token', 'token pattern found in patch')
100
+
101
+ // 运行时 Skill 行
102
+ if (/^\s*-\s+id:\s*cfbridge-skill\b/m.test(patchText)) pass('cordis.patch.yml has cfbridge-skill row')
103
+ else fail('cordis.patch.yml has cfbridge-skill row', 'missing id: cfbridge-skill')
104
+ if (/cfbridge-skill\.js/.test(patchText)) pass('cordis.patch.yml cfbridge-skill references cfbridge-skill.js')
105
+ else fail('cordis.patch.yml cfbridge-skill references cfbridge-skill.js', 'missing file reference')
106
+
107
+ // 不应再注入 agent 栈行(plan §4.1)。
108
+ const banned = ['persona', 'agent-instructions', 'tool-bash', 'tool-pwsh', 'tool-fs', 'skill-filesystem', 'tool-skill', 'planning']
109
+ for (const k of banned) {
110
+ const re = new RegExp(`^\\s*-\\s+id:\\s*${k}\\b`, 'm')
111
+ if (re.test(patchText)) fail(`cordis.patch.yml does not redeclare agent-stack row "${k}"`, 'host composition already provides this row; redeclaring causes layer conflict')
112
+ }
113
+ pass('cordis.patch.yml does not redeclare agent-stack rows (persona/tools/skills/planning)')
114
+ } else {
115
+ fail('cordis.patch.yml exists', 'file missing')
116
+ }
117
+
118
+ // 2a. src/cfbridge-skill.js 运行时 Skill 插件
119
+ const runtimeSkillJs = path.join(ROOT, 'src', 'cfbridge-skill.js')
120
+ if (fs.existsSync(runtimeSkillJs)) {
121
+ pass('src/cfbridge-skill.js exists')
122
+ const t = readText(runtimeSkillJs)
123
+ if (/inject\s*=\s*\['skills'\]/.test(t)) pass('src/cfbridge-skill.js injects skills')
124
+ else fail('src/cfbridge-skill.js injects skills', 'inject must include skills')
125
+ if (/ctx\.skills\.register/.test(t)) pass('src/cfbridge-skill.js registers via ctx.skills.register')
126
+ else fail('src/cfbridge-skill.js registers via ctx.skills.register', 'missing ctx.skills.register call')
127
+ if (/name:\s*'cfbridge'/.test(t) || /name:\s*"cfbridge"/.test(t)) pass('src/cfbridge-skill.js names skill cfbridge')
128
+ else fail('src/cfbridge-skill.js names skill cfbridge', 'skill name should be cfbridge')
129
+ if (!hasTokenLeak(t)) pass('src/cfbridge-skill.js has no hardcoded token')
130
+ else fail('src/cfbridge-skill.js has no hardcoded token', 'token pattern found')
131
+ } else {
132
+ fail('src/cfbridge-skill.js exists', 'file missing')
133
+ }
134
+
135
+ // 3. Skill 文件
136
+ const skillMd = path.join(ROOT, 'skills', 'cfbridge', 'SKILL.md')
137
+ if (fs.existsSync(skillMd)) {
138
+ pass('skills/cfbridge/SKILL.md exists')
139
+ const t = readText(skillMd)
140
+ if (/^#\s+cfbridge\b/m.test(t)) pass('SKILL.md has title')
141
+ else fail('SKILL.md has title', 'first heading should be "# cfbridge"')
142
+ if (!hasTokenLeak(t)) pass('SKILL.md has no hardcoded token')
143
+ else fail('SKILL.md has no hardcoded token', 'token pattern in skill body')
144
+ } else {
145
+ fail('skills/cfbridge/SKILL.md exists', 'file missing')
146
+ }
147
+
148
+ // 3a. package.json files[] must include src/
149
+ if (pkg && Array.isArray(pkg.files)) {
150
+ if (pkg.files.includes('src/') || pkg.files.includes('src/cfbridge-skill.js')) pass('package files[] includes runtime skill source')
151
+ else fail('package files[] includes runtime skill source', 'need src/ or src/cfbridge-skill.js in files[]')
152
+ }
153
+
154
+ // 4. 关键脚本
155
+ const SCRIPTS = [
156
+ 'install-bundle.js',
157
+ 'uninstall-bundle.js',
158
+ 'validate-bundle.js',
159
+ 'migrate-from-preset.js',
160
+ 'dump-config.js',
161
+ 'check.js',
162
+ 'test.js',
163
+ 'wrangler.js',
164
+ 'test-wrangler.js',
165
+ ]
166
+ for (const s of SCRIPTS) {
167
+ if (fs.existsSync(path.join(ROOT, 'scripts', s))) pass(`scripts/${s} exists`)
168
+ else fail(`scripts/${s} exists`, 'missing')
169
+ }
170
+
171
+ // 5. 旧 preset 标记 deprecated
172
+ const depPreset = path.join(ROOT, 'deprecated', 'preset')
173
+ if (fs.existsSync(path.join(depPreset, 'preset.yml'))) pass('deprecated/preset/preset.yml archived')
174
+ else fail('deprecated/preset/preset.yml archived', 'missing')
175
+ if (fs.existsSync(path.join(depPreset, 'agent.cordis.yml'))) pass('deprecated/preset/agent.cordis.yml archived')
176
+ else fail('deprecated/preset/agent.cordis.yml archived', 'missing')
177
+
178
+ // 6. 旧 install-preset 等保留为 shim
179
+ const SHIMS = ['install-preset.js', 'uninstall-preset.js', 'validate-preset.js']
180
+ for (const s of SHIMS) {
181
+ const p = path.join(ROOT, 'scripts', s)
182
+ if (fs.existsSync(p)) {
183
+ const t = readText(p)
184
+ if (/DEPRECATED|@deprecated|deprecated/i.test(t)) pass(`scripts/${s} is marked deprecated`)
185
+ else fail(`scripts/${s} is marked deprecated`, 'shim must print deprecation warning')
186
+ } else {
187
+ fail(`scripts/${s} exists`, 'missing')
188
+ }
189
+ }
190
+
191
+ // 7. 全部 token 痕迹扫描
192
+ const scan = (dir) => {
193
+ const out = []
194
+ if (!fs.existsSync(dir)) return out
195
+ const stat = fs.statSync(dir)
196
+ if (!stat.isDirectory()) return hasTokenLeak(readText(dir)) ? [dir] : []
197
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
198
+ const p = path.join(dir, entry.name)
199
+ if (entry.isDirectory()) out.push(...scan(p))
200
+ else if (entry.isFile()) {
201
+ const t = readText(p)
202
+ if (hasTokenLeak(t)) out.push(p)
203
+ }
204
+ }
205
+ return out
206
+ }
207
+ const hits = [
208
+ ...scan(path.join(ROOT, 'cordis.patch.yml')),
209
+ ...scan(path.join(ROOT, 'scripts')),
210
+ ...scan(path.join(ROOT, 'skills')),
211
+ ...scan(path.join(ROOT, 'README.md')),
212
+ ...scan(path.join(ROOT, 'deprecated')),
213
+ ]
214
+ if (hits.length === 0) pass('no token-like content in tracked bundle sources')
215
+ else fail('no token-like content in tracked bundle sources', `found in: ${hits.join(', ')}`)
216
+
217
+ // 输出
218
+ let allPass = true
219
+ for (const r of results) {
220
+ if (r.ok) console.log(`PASS ${r.name}${r.detail ? ` — ${r.detail}` : ''}`)
221
+ else { console.log(`FAIL ${r.name}: ${r.msg}`); allPass = false }
222
+ }
223
+ console.log(`\n${allPass ? 'PASS' : 'FAIL'}: ${results.filter((r) => r.ok).length}/${results.length} checks`)
224
+ process.exit(allPass ? 0 : 1)
225
+ }
226
+
227
+ try {
228
+ main()
229
+ } catch (e) {
230
+ console.error(`[ERR ] ${e.stack || e.message}`)
231
+ process.exit(1)
232
+ }
@@ -0,0 +1,9 @@
1
+ // cfbridge v0.3.0 起 bundle 形态取代 agent preset。
2
+ // 旧的 validate:preset 静态校验已被 validate:bundle 完全覆盖,且同时检查
3
+ // 当前主入口(cordis.patch.yml)与 bundle 必需结构。
4
+
5
+ const log = (msg) => console.log(`[WARN] ${msg}`)
6
+ log('validate:preset is deprecated since v0.3.0.')
7
+ log('Use `npm run validate:bundle` instead — it covers the same checks plus the new bundle layer.')
8
+ log('This shim forwards to validate:bundle so old CI scripts still get a meaningful exit code.')
9
+ process.exit(1)
@@ -0,0 +1,37 @@
1
+ // 通过项目内固定版本的 Wrangler 执行命令。
2
+ // 仅在当前进程尚未设置 CLOUDFLARE_API_TOKEN 时,从 DSH 私有环境文件加载;绝不打印 token。
3
+ const path = require('path')
4
+ const { spawnSync } = require('child_process')
5
+
6
+ const root = path.resolve(__dirname, '..')
7
+ const dshHome = process.env.DSH_HOME || path.join(process.env.USERPROFILE || process.env.HOME || '', '.dsh')
8
+ const dshEnv = path.join(dshHome, '.env')
9
+
10
+ if (!process.env.CLOUDFLARE_API_TOKEN) {
11
+ try {
12
+ process.loadEnvFile(dshEnv)
13
+ } catch (error) {
14
+ if (error && error.code !== 'ENOENT') {
15
+ console.error(`无法读取 DSH 环境文件:${error.message}`)
16
+ process.exit(1)
17
+ }
18
+ }
19
+ }
20
+
21
+ if (!process.env.CLOUDFLARE_API_TOKEN) {
22
+ console.error('未找到 CLOUDFLARE_API_TOKEN。请在 DSH 私有环境文件或当前终端环境中配置该变量。')
23
+ process.exit(1)
24
+ }
25
+
26
+ const wrangler = path.join(root, 'node_modules', 'wrangler', 'bin', 'wrangler.js')
27
+ const result = spawnSync(process.execPath, [wrangler, ...process.argv.slice(2)], {
28
+ cwd: root,
29
+ env: process.env,
30
+ stdio: 'inherit',
31
+ })
32
+
33
+ if (result.error) {
34
+ console.error(`Wrangler 无法启动:${result.error.message}`)
35
+ process.exit(1)
36
+ }
37
+ process.exit(result.status === null ? 1 : result.status)
@@ -0,0 +1,177 @@
1
+ # cfbridge — Cloudflare 全局 Bridge 操作指南
2
+
3
+ 本 Skill 由 cfbridge v0.3.0 Bundle 提供;安装并启用后,**所有** DSH 会话都会自动看到本 Skill —— 不需要选任何 preset,也不需要切换模式。
4
+
5
+ > 触发:安装 `cfbridge` Bundle 后(即 `npm run install:bundle` 完成且 DSH 重启),模型在所有会话中均可感知本 Skill。任何涉及 Cloudflare API 的请求,都应先调用 `mcp__cloudflare__docs` 或 `mcp__cloudflare__search` 来确认端点与参数,再调用 `mcp__cloudflare__execute` 来执行。
6
+
7
+ ## 三工具速查
8
+
9
+ | 工具 | 何时用 | 参数 |
10
+ | --- | --- | --- |
11
+ | `mcp__cloudflare__docs` | 不确定 Cloudflare 某个产品的概念、参数、限制 | `query` |
12
+ | `mcp__cloudflare__search` | 不确定具体 API 端点、路径或请求体 schema | `code`(一段 JavaScript,操作已展开 `$ref` 的 OpenAPI spec) |
13
+ | `mcp__cloudflare__execute` | 执行 API 调用(GET/POST/PUT/DELETE/PATCH) | `code`(一段 JavaScript,通过 `cloudflare.request()` 调用) |
14
+
15
+ ## 默认工作流:search → execute
16
+
17
+ 每次调用前,遵循「先检索再执行」:
18
+
19
+ ```js
20
+ // 1. 检索:找出列出 Worker 脚本的端点
21
+ async () => Object.entries(spec.paths)
22
+ .filter(([path, item]) => path.includes('workers/scripts') && item.get)
23
+ .slice(0, 10)
24
+ .map(([path, item]) => ({ path, params: Object.keys(item.get.parameters || {}) }))
25
+ ```
26
+
27
+ ```js
28
+ // 2. 执行:真正调用 API
29
+ async () => cloudflare.request({
30
+ method: 'GET',
31
+ path: `/accounts/${accountId}/workers/scripts`,
32
+ })
33
+ ```
34
+
35
+ `accountId` 会被 Cloudflare MCP 服务按 token 上下文预置,不需要手动传。
36
+
37
+ ## 常见模式
38
+
39
+ ### 列出资源
40
+
41
+ ```js
42
+ async () => cloudflare.request({
43
+ method: 'GET',
44
+ path: `/accounts/${accountId}/<resource>`,
45
+ query: { per_page: 50 },
46
+ })
47
+ ```
48
+
49
+ ### 列出 Zone
50
+
51
+ ```js
52
+ async () => cloudflare.request({
53
+ method: 'GET',
54
+ path: '/zones',
55
+ query: { per_page: 50 },
56
+ })
57
+ ```
58
+
59
+ ### 列出 D1
60
+
61
+ ```js
62
+ async () => cloudflare.request({
63
+ method: 'GET',
64
+ path: `/accounts/${accountId}/d1/database`,
65
+ })
66
+ ```
67
+
68
+ ### 列出 KV namespace
69
+
70
+ ```js
71
+ async () => cloudflare.request({
72
+ method: 'GET',
73
+ path: `/accounts/${accountId}/storage/kv/namespaces`,
74
+ })
75
+ ```
76
+
77
+ ### 列出 Pages 项目
78
+
79
+ ```js
80
+ async () => cloudflare.request({
81
+ method: 'GET',
82
+ path: `/accounts/${accountId}/pages/projects`,
83
+ })
84
+ ```
85
+
86
+ ### 检索 GraphQL analytics
87
+
88
+ ```js
89
+ async () => cloudflare.request({
90
+ method: 'POST',
91
+ path: `/accounts/${accountId}/analytics/graphql`,
92
+ body: {
93
+ query: `query { viewer { accounts(filter: { accountTag: "${accountId}" }) { workersInvocationsAdaptive(limit: 5) { dimensions { date } sum { requests } } } } }`,
94
+ },
95
+ })
96
+ ```
97
+
98
+ ## 写操作规范(必读)
99
+
100
+ 任何会修改 Cloudflare 状态的请求(POST/PUT/PATCH/DELETE),在执行前必须:
101
+
102
+ 1. **明确目标**:用 search/docs 确认端点、请求体、副作用。
103
+ 2. **复述操作**:用一段文字告诉用户「我将执行 XX,对 YY 资源,结果是 ZZ」,等待用户同意。
104
+ 3. **避免连写**:不要在一次 execute 中串联多个写操作;每个写操作独立确认。
105
+ 4. **不可逆警告**:删除、覆盖、DNS 改动等不可逆操作需要二次确认。
106
+
107
+ | 操作类型 | 是否需要用户确认 |
108
+ | --- | --- |
109
+ | 任何 GET / LIST | 否 |
110
+ | 文档/Schema 查询 | 否 |
111
+ | Worker deploy | 是 |
112
+ | KV/D1/R2 写 | 是 |
113
+ | DNS 记录改 | 是(不可逆) |
114
+ | secret put | 是 |
115
+ | zone/账户删除 | 是(强烈建议二次确认) |
116
+
117
+ ## Token 类型与权限边界
118
+
119
+ 实际可用能力由 Cloudflare token 决定。常见 token 类型:
120
+
121
+ | Token 类型 | 前缀 | 典型权限 |
122
+ | --- | --- | --- |
123
+ | Account Token | `cfat_` | 账户范围内读写,但不能跨 zone |
124
+ | User Token | `cfut_` | 用户级,可限定到具体 Zone 资源 |
125
+ | OAuth Token | `cfoat_` | 通过 OAuth 授权产生 |
126
+
127
+ 遇到以下情况,先提示用户而非猜测:
128
+
129
+ - `/accounts` 返回 403 → token 失效或权限不够
130
+ - DNS 端点返回 10000 → token 没有 Zone 资源权限
131
+ - `/user/tokens/verify` 返回 401(cfat_)→ 这是 account token 的正常行为,不是错误
132
+ - R2 端点返回 10042 → 账户尚未开通 R2,需要先在 Dashboard 开通
133
+
134
+ ## 与 Wrangler CLI 的关系
135
+
136
+ Cloudflare MCP 覆盖 ~2500 个 API 端点。某些工作流 MCP 不便处理,可以回退到项目根目录的 `npm run wrangler ...`:
137
+
138
+ ```powershell
139
+ # 本地开发服务器
140
+ npm run wrangler -- dev
141
+
142
+ # 部署(需用户明确确认)
143
+ npm run wrangler -- deploy
144
+
145
+ # 列出资源
146
+ npm run wrangler -- d1 list --json
147
+ npm run wrangler -- kv namespace list --json
148
+ npm run wrangler -- r2 bucket list --json
149
+ npm run wrangler -- pages project list --json
150
+ ```
151
+
152
+ Wrangler 启动器(`scripts/wrangler.js`)会从 `%USERPROFILE%\.dsh\.env` 读取 token,永不写入仓库或日志。
153
+
154
+ ## 故障排查
155
+
156
+ | 现象 | 排查 |
157
+ | --- | --- |
158
+ | `mcp__cloudflare__*` 工具不可见 | cfbridge bundle 未启用;运行 `npm run install:bundle -- --profile <你的 profile>`,然后重启 DSH |
159
+ | 调用返回 `Bearer token required` | `%USERPROFILE%\.dsh\.env` 没有 `CLOUDFLARE_API_TOKEN` |
160
+ | 调用返回 401/403 | token 过期;去 Cloudflare Dashboard 重新生成 |
161
+ | `execute` 超时 | 网络受限;DSH 内 MCP 客户端会自动重连 |
162
+ | `search` 无结果 | 关键词过窄;改用更宽泛的 `path.includes('xxx')` |
163
+ | 模型猜测端点而不是 search | 不接受猜测结果;强制要求先 search |
164
+
165
+ ## 何时不要调用 Cloudflare
166
+
167
+ - 用户问的是通用编程/文档问题,与 Cloudflare 无关
168
+ - 用户希望离线操作、纯本地 Wrangler dev
169
+ - 用户已明确拒绝使用云端资源
170
+
171
+ 在这些场景下,直接回答问题或调用本地工具即可,不要触碰 Cloudflare MCP。
172
+
173
+ ## 相关链接
174
+
175
+ - [Cloudflare Code Mode MCP](https://github.com/cloudflare/mcp)
176
+ - [Wrangler CLI 文档](https://developers.cloudflare.com/workers/wrangler/)
177
+ - [cfbridge 项目仓库](https://github.com/Wenaixi/cfbridge)