@clawos-dev/clawd 0.2.515 → 0.2.517

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.
Files changed (18) hide show
  1. package/dist/app-builder-plugin/mcp-server.cjs +228 -284
  2. package/dist/cli.cjs +10871 -7110
  3. package/dist/dispatch/mcp-server.cjs +28 -4
  4. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/SKILL.md +77 -0
  5. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +116 -0
  6. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +33 -0
  7. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +49 -0
  8. package/dist/persona-defaults/persona-app-builder/.mcp.json +8 -1
  9. package/dist/persona-defaults/persona-app-builder/CLAUDE.md +27 -24
  10. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/SKILL.md +77 -0
  11. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +116 -0
  12. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +33 -0
  13. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +49 -0
  14. package/dist/persona-defaults/persona-dataclaw-builder/.mcp.json +10 -2
  15. package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +15 -13
  16. package/dist/share-ui/assets/{guest-CGrg1A9a.js → guest-fV-R3Vmz.js} +125 -125
  17. package/dist/share-ui/guest.html +1 -1
  18. package/package.json +3 -1
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // 把项目目录里该进仓库的文件收成 GitHub MCP `push_files` 要的 `[{ path, content }]`。
3
+ //
4
+ // 过滤规则 = 项目根 `.gitignore`(模板那份:node_modules/ dist/ .env .publish.log s.yaml server/bootstrap .s/ …)
5
+ // + 永远排掉 `.git` 和 `.clawd-project.json`(插件的记账文件:publish / record-push 都会改它,进了仓库或哈希就会
6
+ // 「每推一次都变一次」,而且里面的 prodUrl / pushedCommit 也不是源码)。**不要**在这里另写一份排除表:publish.sh 渲进项目目录的 s.yaml 含 supabase key,
7
+ // 它进不进仓库只该由 .gitignore 一处决定。
8
+ // 二进制(前 8KB 含 NUL)和超过 512KB 的文件跳过并列进 `skipped`——push_files 只收文本,
9
+ // 而且每个字节都要经模型上下文走一遍;agent 看到 skipped 里有该进仓库的东西要自己判断。
10
+ //
11
+ // 用法:collect-files.mjs --project <项目目录绝对路径>
12
+ // 输出:{ files: [{ path, content }], skipped: [{ path, reason }], treeHash }
13
+ // treeHash = files 按 path 排序后 sha256(path\0content\0…)。record-push.mjs 把它记进元数据,
14
+ // publish tool 再跑一次本脚本比对——不一致就是「改了没推」,拒绝发布。skipped 里的文件不进哈希。
15
+ import { createHash } from 'node:crypto'
16
+ import { readdir, readFile, stat } from 'node:fs/promises'
17
+ import { join, relative, sep } from 'node:path'
18
+
19
+ const MAX_BYTES = 512 * 1024
20
+ const PROBE_BYTES = 8 * 1024
21
+
22
+ function arg(name) {
23
+ const i = process.argv.indexOf(name)
24
+ return i >= 0 ? process.argv[i + 1] : undefined
25
+ }
26
+
27
+ const root = arg('--project')
28
+ if (!root) {
29
+ process.stderr.write('usage: collect-files.mjs --project <project-dir>\n')
30
+ process.exit(2)
31
+ }
32
+
33
+ /**
34
+ * 极简 .gitignore:够模板那份用。支持
35
+ * - `name`:任一路径段等于 name(`*` / `?` 单段通配)
36
+ * - `name/`:只匹配目录(及其下所有文件)
37
+ * - 含 `/` 的:相对项目根的路径前缀(`server/bootstrap`、`/dist`)
38
+ * 不支持 `!` 取反和 `**`——模板没用到;真要用先加测试。
39
+ */
40
+ function parseGitignore(text) {
41
+ const rules = []
42
+ for (const raw of text.split('\n')) {
43
+ let line = raw.trim()
44
+ if (!line || line.startsWith('#')) continue
45
+ const dirOnly = line.endsWith('/')
46
+ if (dirOnly) line = line.slice(0, -1)
47
+ const anchored = line.includes('/')
48
+ if (anchored && line.startsWith('/')) line = line.slice(1)
49
+ if (!line) continue
50
+ const re = new RegExp(`^${line.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]')}$`)
51
+ rules.push({ re, dirOnly, anchored })
52
+ }
53
+ return rules
54
+ }
55
+
56
+ /** rel 用 `/` 分隔;isDir 说 rel 本身是不是目录。 */
57
+ function ignored(rel, isDir, rules) {
58
+ const segments = rel.split('/')
59
+ for (const rule of rules) {
60
+ if (rule.anchored) {
61
+ // 前缀匹配:rel 等于它、或在它下面
62
+ for (let i = 1; i <= segments.length; i++) {
63
+ const head = segments.slice(0, i).join('/')
64
+ if (rule.re.test(head) && (!rule.dirOnly || isDir || i < segments.length)) return true
65
+ }
66
+ continue
67
+ }
68
+ for (let i = 0; i < segments.length; i++) {
69
+ if (!rule.re.test(segments[i])) continue
70
+ // dirOnly:命中的这一段得是目录——要么它不是最后一段(后面还有东西),要么 rel 本身就是目录
71
+ if (!rule.dirOnly || i < segments.length - 1 || isDir) return true
72
+ }
73
+ }
74
+ return false
75
+ }
76
+
77
+ let rules = []
78
+ try {
79
+ rules = parseGitignore(await readFile(join(root, '.gitignore'), 'utf8'))
80
+ } catch {
81
+ // 没有 .gitignore:只排 .git
82
+ }
83
+
84
+ const files = []
85
+ const skipped = []
86
+
87
+ async function walk(dir) {
88
+ for (const name of (await readdir(dir)).sort()) {
89
+ if (name === '.git' || (dir === root && name === '.clawd-project.json')) continue
90
+ const abs = join(dir, name)
91
+ const rel = relative(root, abs).split(sep).join('/')
92
+ const info = await stat(abs)
93
+ if (info.isDirectory()) {
94
+ if (!ignored(rel, true, rules)) await walk(abs)
95
+ continue
96
+ }
97
+ if (!info.isFile() || ignored(rel, false, rules)) continue
98
+ if (info.size > MAX_BYTES) {
99
+ skipped.push({ path: rel, reason: `too large (${info.size} bytes > ${MAX_BYTES})` })
100
+ continue
101
+ }
102
+ const buf = await readFile(abs)
103
+ if (buf.subarray(0, PROBE_BYTES).includes(0)) {
104
+ skipped.push({ path: rel, reason: 'binary' })
105
+ continue
106
+ }
107
+ files.push({ path: rel, content: buf.toString('utf8') })
108
+ }
109
+ }
110
+
111
+ await walk(root)
112
+ // 按字节序排,不用 localeCompare:不同机器 locale 不同会让同一棵树排出两种顺序
113
+ files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
114
+ const hash = createHash('sha256')
115
+ for (const f of files) hash.update(f.path).update('\0').update(f.content).update('\0')
116
+ process.stdout.write(`${JSON.stringify({ files, skipped, treeHash: hash.digest('hex') })}\n`)
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { readdir, readFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+
5
+ function arg(name) {
6
+ const i = process.argv.indexOf(name)
7
+ return i >= 0 ? process.argv[i + 1] : undefined
8
+ }
9
+
10
+ const root = arg('--root')
11
+ if (!root) {
12
+ process.stderr.write('usage: list-projects.mjs --root <user-workdir>\n')
13
+ process.exit(2)
14
+ }
15
+
16
+ const projectsRoot = join(root, 'projects')
17
+ let names = []
18
+ try { names = await readdir(projectsRoot) } catch (error) {
19
+ if (error?.code !== 'ENOENT') throw error
20
+ }
21
+
22
+ const projects = []
23
+ for (const dirName of names) {
24
+ try {
25
+ const value = JSON.parse(await readFile(join(projectsRoot, dirName, '.clawd-project.json'), 'utf8'))
26
+ if (value?.name !== dirName || typeof value.createdAt !== 'string') continue
27
+ projects.push({ ...value, projectDir: join(projectsRoot, dirName) })
28
+ } catch {
29
+ // User-owned directories without valid app-builder metadata are not projects.
30
+ }
31
+ }
32
+ projects.sort((a, b) => a.name.localeCompare(b.name))
33
+ process.stdout.write(`${JSON.stringify({ projects })}\n`)
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ // 经 GitHub MCP 建仓 / push 之后记账:把 repoUrl、远端 commit sha、push 那一刻的文件树哈希写进项目的 .clawd-project.json。
3
+ // publish tool 对没有本地 git 的项目:重算文件树哈希与 pushedTreeHash 比,不一致拒绝发布;一致就记 pushedCommit
4
+ // 当 publishedCommit(「线上 = 仓库里的哪个 commit」)。
5
+ //
6
+ // 用法:record-push.mjs --project <项目目录绝对路径> --commit <push_files 返回的 object.sha> --tree-hash <collect-files.mjs 输出的 treeHash> [--repo-url <https://github.com/ottin4ttc/...>]
7
+ // 输出:写回后的元数据 JSON
8
+ import { readFile, writeFile } from 'node:fs/promises'
9
+ import { join } from 'node:path'
10
+
11
+ function arg(name) {
12
+ const i = process.argv.indexOf(name)
13
+ return i >= 0 ? process.argv[i + 1] : undefined
14
+ }
15
+
16
+ const project = arg('--project')
17
+ const commit = arg('--commit')
18
+ const treeHash = arg('--tree-hash')
19
+ const repoUrl = arg('--repo-url')
20
+ if (!project || !commit || !treeHash) {
21
+ process.stderr.write('usage: record-push.mjs --project <project-dir> --commit <sha> --tree-hash <sha256 from collect-files.mjs> [--repo-url <url>]\n')
22
+ process.exit(2)
23
+ }
24
+ if (!/^[0-9a-f]{64}$/i.test(treeHash)) {
25
+ process.stderr.write(`invalid tree hash "${treeHash}": expected the 64-hex treeHash printed by collect-files.mjs\n`)
26
+ process.exit(2)
27
+ }
28
+ if (!/^[0-9a-f]{7,64}$/i.test(commit)) {
29
+ process.stderr.write(`invalid commit sha "${commit}": expected hex, 7-64 chars (push_files result → object.sha)\n`)
30
+ process.exit(2)
31
+ }
32
+ if (repoUrl !== undefined && !/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+$/.test(repoUrl)) {
33
+ process.stderr.write(`invalid repo url "${repoUrl}": expected https://github.com/<owner>/<repo>\n`)
34
+ process.exit(2)
35
+ }
36
+
37
+ const metaPath = join(project, '.clawd-project.json')
38
+ let meta
39
+ try {
40
+ meta = JSON.parse(await readFile(metaPath, 'utf8'))
41
+ } catch (error) {
42
+ process.stderr.write(`not an app-builder project (cannot read ${metaPath}): ${error instanceof Error ? error.message : String(error)}\n`)
43
+ process.exit(1)
44
+ }
45
+ if (repoUrl) meta.repoUrl = repoUrl
46
+ meta.pushedCommit = commit.toLowerCase()
47
+ meta.pushedTreeHash = treeHash.toLowerCase()
48
+ await writeFile(metaPath, `${JSON.stringify(meta, null, 2)}\n`)
49
+ process.stdout.write(`${JSON.stringify(meta)}\n`)
@@ -1,4 +1,12 @@
1
1
  {
2
- "_comment": "数据查询搭建师:通路 A 直调 dataclaw-service HTTP,不挂任何数据 MCP。daemon refreshDaemonManagedDirs 会把本文件同步到 ~/.clawd/personas/persona-dataclaw-builder/.mcp.json。",
3
- "mcpServers": {}
2
+ "_comment": "数据查询搭建师:通路 A 直调 dataclaw-service HTTP,不挂任何数据 MCP。daemon refreshDaemonManagedDirs 会把本文件同步到 ~/.clawd/personas/persona-dataclaw-builder/.mcp.json。github:GitHub 官方远程 MCP,建仓 / 提交文件由 agent 按 .claude/skills/app-builder-projects 走它(与 persona-app-builder 同一份 skill),本机不依赖 gh CLI。`${GITHUB_TOKEN}` 占位由 daemon 从本机凭据表(~/.clawd/secrets/github.env)渲进 0600 产物的 headers,云上由部署者选「关联」或「填写」;为什么必须是占位见 persona-app-builder/.mcp.json 的说明。",
3
+ "mcpServers": {
4
+ "github": {
5
+ "type": "http",
6
+ "url": "https://api.githubcopilot.com/mcp/",
7
+ "headers": {
8
+ "Authorization": "Bearer ${GITHUB_TOKEN}"
9
+ }
10
+ }
11
+ }
4
12
  }
@@ -2,21 +2,22 @@
2
2
 
3
3
  你是 clawd 里的「数据查询搭建师」。一句话定位:**帮 TTC 业务用户把特定的数据查询条件,做成一个只读的 GUI 查询页面,背后复用 dataclaw 的查询能力。** 你做出来的页面是独立 app,用 app-builder 流水线起、发阿里云 FC、老板点链接看。
4
4
 
5
- ## 工作流(clawd-app-builder MCP)
5
+ ## 工作流(app-builder-projects skill + clawd-app-builder MCP + github MCP)
6
6
 
7
- 你复用的是 app-builder 那条项目流水线(persona 无关),**不是** app-builder persona 的磁盘资产——你摸不到也不需要它。下面是你要做的全部,自包含照做即可。
7
+ 你复用的是 app-builder 那条项目流水线(persona 无关),**不是** app-builder persona 的磁盘资产——你摸不到也不需要它。下面是主线;步骤细节以你自己目录里的 `.claude/skills/app-builder-projects/SKILL.md` 为准。
8
8
 
9
- 一个 app = 当前用户工作区下的 `projects/<name>/`,同时是 GitHub 组织 `ottin4ttc` 下的一个私有仓库(`<name>-<slug>`)。会话不绑项目:一个会话里可以改多个 app,隔几天新开会话改旧 app 用 `listProjects` 找目录。没有右栏预览、没有 dev server 概念,验证靠发布后的公网链接。
9
+ 一个 app = 当前用户工作区下的 `projects/<name>/`,配一个 GitHub 组织 `ottin4ttc` 下的私有仓库(`<name>-<slug>`),仓库由你经 **`github` MCP** 建和提交,本机不需要 `gh`、项目里不需要 git。会话不绑项目:一个会话里可以改多个 app,隔几天新开会话改旧 app 用 skill 的 `list-projects.mjs` 找目录。没有右栏预览、没有 dev server 概念,验证靠发布后的公网链接。
10
10
 
11
- 项目类动作全走 **`clawd-app-builder` MCP** 的 4 个 tool:`createProject({name})` / `listProjects({})` / `publish({name})` / `removeProject({name})`。没有别的 tool,也没有任何 `appBuilder:*` RPC。
11
+ 宿主侧 **`clawd-app-builder` MCP** 只有 3 个 tool:`createProject({name})` / `publish({name})` / `removeProject({name})`。没有 `listProjects`(走脚本),没有任何 `appBuilder:*` RPC。
12
12
 
13
13
  ### 新建
14
14
 
15
15
  1. 主动问「老板想看什么数据的页?」(一句话,别长 prompt)。顺带提醒:稍后需要老板给一把 dataclaw apikey(向刘浩索取;只要冒号后的 secret 段)
16
16
  2. 老板描述需求 → **反问「项目叫什么名字」**(不擅自命名,kebab-case)
17
- 3. 拿到名字后 `createProject({ name })`:建目录 + scaffold(dataclaw 起手 app)+ `git init` + 首个 commit + 建仓推送。返回的 `projectDir` 就是项目目录绝对路径,**记住它**。重名报 `already exists` → 让老板换名;带 `gh auth login` 的报错 = 本机 GitHub CLI 没登录,告诉老板处理
18
- 4. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install`
19
- 5. 写入 apikey:向老板要 dataclaw apikey(只要冒号后的 secret 段),写进 `<projectDir>/server/.env`:
17
+ 3. `list-projects.mjs` 查重名,再 `createProject({ name })`:建目录 + scaffold(dataclaw 起手 app)。返回 `{ name, projectDir, slug, repoName }`,**`projectDir` 就是项目目录绝对路径,记住它**。重名报 `already exists` → 让老板换名
18
+ 4. `github` MCP `create_repository` 建 `ottin4ttc/<repoName>` 私有仓库 → `collect-files.mjs` → `push_files` → `record-push.mjs --repo-url …`。github MCP 报未授权 / 没这些 tool → 本机让老板把 PAT 写进 `~/.clawd/secrets/github.env`(`GITHUB_TOKEN=…`),云上让老板在 persona 卡片关联 GitHub,然后**新开一条对话**
19
+ 5. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install`
20
+ 6. 写入 apikey:向老板要 dataclaw apikey(只要冒号后的 secret 段),写进 `<projectDir>/server/.env`:
20
21
  ```bash
21
22
  cat > "<projectDir>/server/.env" <<EOF
22
23
  DATACLAW_SERVICE_URL=https://120.48.21.36
@@ -24,17 +25,17 @@
24
25
  PORT=3000
25
26
  EOF
26
27
  ```
27
- key 只存这里:dev 由 dotenv 加载、发布 FC 时 pre-deploy 自动从这读出注入 FC(`.env` 被 `.fcignore` / `.gitignore` 排除,不进部署包也不进 git)。老板要换 key → 重写本文件
28
- 6. 按需求改数据集 / 端点 / 展示;每完成一段 `git add -A && git commit && git push`
29
- 7. `publish({ name })` 拿到 `prodUrl`,回复里给老板可点的链接;老板看链接提意见,回到第 6 步
28
+ key 只存这里:dev 由 dotenv 加载、发布 FC 时 pre-deploy 自动从这读出注入 FC(`.env` 被 `.fcignore` / `.gitignore` 排除,不进部署包、`collect-files.mjs` 也不会把它推上 GitHub)。老板要换 key → 重写本文件
29
+ 7. 按需求改数据集 / 端点 / 展示;每完成一段按 skill 推一次:`collect-files.mjs` → `push_files` → `record-push.mjs`。不调 `gh`、不 `git push`
30
+ 8. `publish({ name })` 拿到 `prodUrl`,回复里给老板可点的链接;老板看链接提意见,回到第 7 步
30
31
 
31
32
  ### 改旧页面
32
33
 
33
- `listProjects({})` 找到 `projectDir` → 改 → commit + push → `publish` → 链接。`repoUrl` 为空的是老项目,**不要给它补仓库**:当普通目录改,改完直接 `publish`(不是 git 仓库时 tool 跳过脏检查、不记 commit)。
34
+ `list-projects.mjs` 找到 `projectDir` → 改 → 按 skill 推到仓库 + `record-push.mjs` → `publish` → 链接。目录里有 `.git` 的是 GitHub MCP 之前建的项目,照原来的本地 git 流程(commit + `git push`,`publish` 拒脏工作区);`repoUrl` 为空又没 `.git` 的是更老的项目,**不要给它补仓库**:当普通目录改,改完直接 `publish`(tool 不记 commit)。
34
35
 
35
36
  ### 发布
36
37
 
37
- **只走 `publish` tool,不手跑 `publish.sh`**(凭据只在 tool 进程的 env 里)。发布前工作区必须干净(tool 会拒绝脏工作区)。失败时读项目目录下的 `.publish.log`,修好 → commit → 再 `publish`。本地想先看一眼:`cd server && pnpm dev` + `cd web && pnpm dev`(`/api` 代理到 3000),看完停掉。
38
+ **只走 `publish` tool,不手跑 `publish.sh`**(凭据只在 tool 进程的 env 里)。发布前先把改动推到仓库并 `record-push.mjs`——`publish` 会重算文件树哈希跟记账的比,有没推的改动直接拒。失败时读项目目录下的 `.publish.log`,修好 → 推 → 再 `publish`。本地想先看一眼:`cd server && pnpm dev` + `cd web && pnpm dev`(`/api` 代理到 3000),看完停掉。
38
39
 
39
40
  ### 删页面
40
41
 
@@ -63,4 +64,5 @@
63
64
  - 只读:绝不让生成的 app 往 dataclaw 写数据 / 不接 Supabase 写库。
64
65
  - apikey 只存 `server/.env`、只在后端用,**绝不下发到前端 / 不硬编码进代码 / 不写进会被打包或提交的文件**。
65
66
  - 自己 `bash` 跑 `publish.sh` / `new-extension.sh` / `remove-extension.sh`——只走 tool。
66
- - persona 目录只读;项目路径以 `createProject` / `listProjects` 返回的 `projectDir` 为准;不在用户工作区之外建项目。
67
+ - persona 目录只读;项目路径以 `createProject` / `list-projects.mjs` 返回的 `projectDir` 为准;不在用户工作区之外建项目。
68
+ - GitHub 只走 `github` MCP:不调 `gh`、不 `git push`、不把 GitHub token 写进任何文件或命令行。