@clawos-dev/clawd 0.2.514 → 0.2.516
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.
- package/dist/app-builder-plugin/mcp-server.cjs +197 -280
- package/dist/cli.cjs +8 -9
- package/dist/dispatch/mcp-server.cjs +2 -2
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/SKILL.md +77 -0
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +116 -0
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +33 -0
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +49 -0
- package/dist/persona-defaults/persona-app-builder/.mcp.json +8 -1
- package/dist/persona-defaults/persona-app-builder/CLAUDE.md +27 -24
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/SKILL.md +77 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +116 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +33 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +49 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.mcp.json +10 -2
- package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +15 -13
- package/dist/share-ui/assets/{guest--nOZRIff.js → guest-CGrg1A9a.js} +1 -1
- package/dist/share-ui/guest.html +1 -1
- package/package.json +1 -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
|
|
9
|
+
一个 app = 当前用户工作区下的 `projects/<name>/`,配一个 GitHub 组织 `ottin4ttc` 下的私有仓库(`<name>-<slug>`),仓库由你经 **`github` MCP** 建和提交,本机不需要 `gh`、项目里不需要 git。会话不绑项目:一个会话里可以改多个 app,隔几天新开会话改旧 app 用 skill 的 `list-projects.mjs` 找目录。没有右栏预览、没有 dev server 概念,验证靠发布后的公网链接。
|
|
10
10
|
|
|
11
|
-
|
|
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.
|
|
18
|
-
4. `
|
|
19
|
-
5.
|
|
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`
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
`
|
|
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
|
|
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` / `
|
|
67
|
+
- persona 目录只读;项目路径以 `createProject` / `list-projects.mjs` 返回的 `projectDir` 为准;不在用户工作区之外建项目。
|
|
68
|
+
- GitHub 只走 `github` MCP:不调 `gh`、不 `git push`、不把 GitHub token 写进任何文件或命令行。
|