@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
package/dist/cli.cjs
CHANGED
|
@@ -5800,7 +5800,7 @@ var init_attachment_schemas = __esm({
|
|
|
5800
5800
|
sessionId: external_exports.string().min(1),
|
|
5801
5801
|
/** 相对 session.cwd 的路径;允许传绝对路径,daemon 端会归一化(必须在 cwd 内) */
|
|
5802
5802
|
relPath: external_exports.string().min(1),
|
|
5803
|
-
/** TTL 秒数;缺省
|
|
5803
|
+
/** TTL 秒数;缺省 / null = 永久 URL(不带 e 字段);只有显式传秒数才限时 */
|
|
5804
5804
|
ttlSeconds: external_exports.number().int().positive().nullable().optional(),
|
|
5805
5805
|
/** 视图模式:缺省 'raw' 返 /files/... 裸文件 URL;'md-rendered' 返
|
|
5806
5806
|
* /share/md/... viewer URL(同一签名,daemon 端换前缀),外人浏览器打开就是
|
|
@@ -9143,7 +9143,7 @@ var init_methods = __esm({
|
|
|
9143
9143
|
summary: "\u62C9\u5F53\u524D pty \u5C4F\u5E55\u5168\u91CF\u5FEB\u7167\uFF08\u5207\u56DE\u4F1A\u8BDD / \u5237\u65B0\u65F6\u7528\uFF09\uFF0C\u8FD4\u56DE { data, atSeq, popupKind }\u3002body: { sessionId }\u3002**\u7F3A sessionId \u9759\u9ED8\u8FD4 ok:false\uFF1B\u975E tui / pty \u5DF2\u9000\u51FA\u8FD4 ok:true \u4F46 data=null, atSeq=0** \u2014\u2014 \u4E24\u8005\u90FD\u4E0D\u62A5\u9519\uFF0C\u8981\u5206\u6E05\u5F97\u770B ok\u3002\u53EA\u6709\u8D8A\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD\u624D\u629B\u9519"
|
|
9144
9144
|
},
|
|
9145
9145
|
"attachment.signUrl": {
|
|
9146
|
-
summary: "\u7ED9\u5DF2\u5165\u7FA4**\u4E14\u975E stale** \u7684\u6587\u4EF6\u7B7E\u53D1 HMAC \u4E0B\u8F7D URL\uFF08\u8DE8\u8BBE\u5907\u53EF\u76F4\u63A5\u6253\u5F00\uFF09\u3002**\u9ED8\u8BA4
|
|
9146
|
+
summary: "\u7ED9\u5DF2\u5165\u7FA4**\u4E14\u975E stale** \u7684\u6587\u4EF6\u7B7E\u53D1 HMAC \u4E0B\u8F7D URL\uFF08\u8DE8\u8BBE\u5907\u53EF\u76F4\u63A5\u6253\u5F00\uFF09\u3002**\u9ED8\u8BA4\u6C38\u4E0D\u8FC7\u671F**\uFF1B\u7528\u6237\u8981\u9650\u65F6\u94FE\u63A5\u624D\u663E\u5F0F\u4F20 ttlSeconds\uFF08\u79D2\uFF09\u3002\u4E0D\u5728\u6E05\u5355\u91CC\u548C stale \u62A5\u540C\u4E00\u4E2A\u9519\uFF0C\u5206\u4E0D\u51FA\u662F\u54EA\u79CD",
|
|
9147
9147
|
args: AttachmentSignUrlArgs
|
|
9148
9148
|
},
|
|
9149
9149
|
"attachment.groupAdd": {
|
|
@@ -45324,7 +45324,8 @@ var ATTACHMENT_SHARING_HINT = `## \u628A\u4EA7\u51FA\u6587\u4EF6\u5206\u4EAB\u7E
|
|
|
45324
45324
|
call({ method: "attachment.groupAdd", args: { relPath: "<\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84>", origin: "agent" } })
|
|
45325
45325
|
2. \u7B7E\u540D\u62FF URL\uFF1A
|
|
45326
45326
|
call({ method: "attachment.signUrl", args: { relPath: "<\u540C\u4E00\u4E2A\u7EDD\u5BF9\u8DEF\u5F84>" } })
|
|
45327
|
-
\u8FD4\u56DE\u7684 \`url\` \u5C31\u662F\u5BF9\u65B9\u53EF\u76F4\u63A5\u6253\u5F00/\u4E0B\u8F7D\u7684\u94FE\u63A5\u3002
|
|
45327
|
+
\u8FD4\u56DE\u7684 \`url\` \u5C31\u662F\u5BF9\u65B9\u53EF\u76F4\u63A5\u6253\u5F00/\u4E0B\u8F7D\u7684\u94FE\u63A5\u3002**\u9ED8\u8BA4\u6C38\u4E45\u6709\u6548**\uFF1B\u7528\u6237\u660E\u786E\u8981\u9650\u65F6\u94FE\u63A5\u65F6\u624D\u52A0
|
|
45328
|
+
\`ttlSeconds: <\u79D2\u6570>\`\uFF08\u5982\u4E00\u5929 = 86400\uFF09\uFF0C\u5230\u70B9\u540E\u94FE\u63A5\u5931\u6548\u3002
|
|
45328
45329
|
|
|
45329
45330
|
- \`relPath\` \u5FC5\u987B\u4F20**\u7EDD\u5BF9\u8DEF\u5F84**\uFF08\u4F60\u4EA7\u51FA\u6587\u4EF6\u7684\u771F\u5B9E\u8DEF\u5F84\uFF09\uFF0C\u4E0D\u8981\u4F20\u76F8\u5BF9\u8DEF\u5F84\u3002
|
|
45330
45331
|
- \u8DE8\u8BBE\u5907\u88AB\u59D4\u6D3E\uFF08guest\uFF09\u7684\u4F1A\u8BDD\u53EA\u80FD\u5206\u4EAB\u81EA\u5DF1\u5DE5\u4F5C\u76EE\u5F55\u5185\u7684\u6587\u4EF6\uFF0C\u8D8A\u754C\u4F1A\u88AB\u62D2\u3002
|
|
@@ -62425,7 +62426,7 @@ function createHttpRouter(deps) {
|
|
|
62425
62426
|
};
|
|
62426
62427
|
const msgByCode = {
|
|
62427
62428
|
BAD_SIG: "\u94FE\u63A5\u7B7E\u540D\u65E0\u6548\uFF0C\u8BF7\u8054\u7CFB\u5206\u4EAB\u8005\u91CD\u65B0\u751F\u6210",
|
|
62428
|
-
EXPIRED: "\u94FE\u63A5\u5DF2\u8FC7\u671F\
|
|
62429
|
+
EXPIRED: "\u94FE\u63A5\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u8054\u7CFB\u5206\u4EAB\u8005\u91CD\u65B0\u751F\u6210",
|
|
62429
62430
|
MALFORMED: "\u94FE\u63A5\u683C\u5F0F\u9519\u8BEF"
|
|
62430
62431
|
};
|
|
62431
62432
|
sendHtml(res, statusByCode[r.code], loader.renderErrorHtml(r.code, msgByCode[r.code]));
|
|
@@ -66543,7 +66544,7 @@ function computeMethodAccess(args) {
|
|
|
66543
66544
|
}
|
|
66544
66545
|
|
|
66545
66546
|
// src/version.ts
|
|
66546
|
-
var version = "0.2.
|
|
66547
|
+
var version = "0.2.516".length > 0 ? "0.2.516" : "dev";
|
|
66547
66548
|
|
|
66548
66549
|
// src/cli-probe/probe.ts
|
|
66549
66550
|
var fs77 = __toESM(require("fs"), 1);
|
|
@@ -66754,7 +66755,6 @@ function buildPersonaHandlers(deps) {
|
|
|
66754
66755
|
var import_node_path66 = __toESM(require("path"), 1);
|
|
66755
66756
|
init_protocol();
|
|
66756
66757
|
init_protocol();
|
|
66757
|
-
var DEFAULT_TTL_SECONDS = 24 * 3600;
|
|
66758
66758
|
function assertGuestAttachmentPath(ctx, absPath, personaRoot, method, usersRoot) {
|
|
66759
66759
|
if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
|
|
66760
66760
|
const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
|
|
@@ -66845,8 +66845,7 @@ function buildAttachmentHandlers(deps) {
|
|
|
66845
66845
|
`relPath not in session group files or stale: ${args.relPath}`
|
|
66846
66846
|
);
|
|
66847
66847
|
}
|
|
66848
|
-
const
|
|
66849
|
-
const parts = signUrlParts(secret, candidateAbs, ttl);
|
|
66848
|
+
const parts = signUrlParts(secret, candidateAbs, args.ttlSeconds ?? null);
|
|
66850
66849
|
const prefix = args.view === "md-rendered" ? "/share/md" : "/files";
|
|
66851
66850
|
const url = buildSignedFileUrl(httpBaseUrl, parts, prefix);
|
|
66852
66851
|
return {
|
|
@@ -68700,7 +68699,7 @@ function appBuilderMcpConfigPath(outDir, personaId2) {
|
|
|
68700
68699
|
function writeAppBuilderMcpConfig(args) {
|
|
68701
68700
|
const cfgPath = appBuilderMcpConfigPath(args.outDir, args.personaId);
|
|
68702
68701
|
const cfg = {
|
|
68703
|
-
_comment: `daemon \u81EA\u52A8\u751F\u6210\uFF08${args.personaId} \u7684\u4F1A\u8BDD spawn \u524D\u91CD\u6E32\uFF09\uFF1Bagent spawn \u901A\u8FC7 --mcp-config \u6CE8\u5165\u3002app-builder-plugin/mcp-server.cjs \u662F clawd-app-builder \u63D2\u4EF6\uFF1A\u9879\u76EE\u76EE\u5F55 / scaffold /
|
|
68702
|
+
_comment: `daemon \u81EA\u52A8\u751F\u6210\uFF08${args.personaId} \u7684\u4F1A\u8BDD spawn \u524D\u91CD\u6E32\uFF09\uFF1Bagent spawn \u901A\u8FC7 --mcp-config \u6CE8\u5165\u3002app-builder-plugin/mcp-server.cjs \u662F clawd-app-builder \u63D2\u4EF6\uFF1A\u9879\u76EE\u76EE\u5F55 / scaffold / \u53D1\u5E03 / \u62C6\u9664\u90FD\u5728\u5B83\u8FDB\u7A0B\u91CC\u505A\uFF0C\u51ED\u636E\u53EA\u5728\u8FD9\u4EFD per-server env \u91CC\uFF08GitHub \u5EFA\u4ED3 / \u63D0\u4EA4\u4E0D\u5728\u8FD9\u91CC\uFF0Cagent \u6309 skill \u8D70 GitHub MCP\uFF09\u3002\u975E app-builder \u7CFB persona \u7684\u4F1A\u8BDD\u4E0D\u6302\u672C server\u3002`,
|
|
68704
68703
|
mcpServers: {
|
|
68705
68704
|
"clawd-app-builder": {
|
|
68706
68705
|
type: "stdio",
|
|
@@ -21140,7 +21140,7 @@ var AttachmentSignUrlArgs = external_exports.object({
|
|
|
21140
21140
|
sessionId: external_exports.string().min(1),
|
|
21141
21141
|
/** 相对 session.cwd 的路径;允许传绝对路径,daemon 端会归一化(必须在 cwd 内) */
|
|
21142
21142
|
relPath: external_exports.string().min(1),
|
|
21143
|
-
/** TTL 秒数;缺省
|
|
21143
|
+
/** TTL 秒数;缺省 / null = 永久 URL(不带 e 字段);只有显式传秒数才限时 */
|
|
21144
21144
|
ttlSeconds: external_exports.number().int().positive().nullable().optional(),
|
|
21145
21145
|
/** 视图模式:缺省 'raw' 返 /files/... 裸文件 URL;'md-rendered' 返
|
|
21146
21146
|
* /share/md/... viewer URL(同一签名,daemon 端换前缀),外人浏览器打开就是
|
|
@@ -24150,7 +24150,7 @@ var METHOD_DOCS = {
|
|
|
24150
24150
|
summary: "\u62C9\u5F53\u524D pty \u5C4F\u5E55\u5168\u91CF\u5FEB\u7167\uFF08\u5207\u56DE\u4F1A\u8BDD / \u5237\u65B0\u65F6\u7528\uFF09\uFF0C\u8FD4\u56DE { data, atSeq, popupKind }\u3002body: { sessionId }\u3002**\u7F3A sessionId \u9759\u9ED8\u8FD4 ok:false\uFF1B\u975E tui / pty \u5DF2\u9000\u51FA\u8FD4 ok:true \u4F46 data=null, atSeq=0** \u2014\u2014 \u4E24\u8005\u90FD\u4E0D\u62A5\u9519\uFF0C\u8981\u5206\u6E05\u5F97\u770B ok\u3002\u53EA\u6709\u8D8A\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD\u624D\u629B\u9519"
|
|
24151
24151
|
},
|
|
24152
24152
|
"attachment.signUrl": {
|
|
24153
|
-
summary: "\u7ED9\u5DF2\u5165\u7FA4**\u4E14\u975E stale** \u7684\u6587\u4EF6\u7B7E\u53D1 HMAC \u4E0B\u8F7D URL\uFF08\u8DE8\u8BBE\u5907\u53EF\u76F4\u63A5\u6253\u5F00\uFF09\u3002**\u9ED8\u8BA4
|
|
24153
|
+
summary: "\u7ED9\u5DF2\u5165\u7FA4**\u4E14\u975E stale** \u7684\u6587\u4EF6\u7B7E\u53D1 HMAC \u4E0B\u8F7D URL\uFF08\u8DE8\u8BBE\u5907\u53EF\u76F4\u63A5\u6253\u5F00\uFF09\u3002**\u9ED8\u8BA4\u6C38\u4E0D\u8FC7\u671F**\uFF1B\u7528\u6237\u8981\u9650\u65F6\u94FE\u63A5\u624D\u663E\u5F0F\u4F20 ttlSeconds\uFF08\u79D2\uFF09\u3002\u4E0D\u5728\u6E05\u5355\u91CC\u548C stale \u62A5\u540C\u4E00\u4E2A\u9519\uFF0C\u5206\u4E0D\u51FA\u662F\u54EA\u79CD",
|
|
24154
24154
|
args: AttachmentSignUrlArgs
|
|
24155
24155
|
},
|
|
24156
24156
|
"attachment.groupAdd": {
|
package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/SKILL.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-builder-projects
|
|
3
|
+
description: Use when creating, reopening, changing, pushing to GitHub, publishing, or removing an app managed by this persona — an app is projects/<name>/ in the user work dir plus a private ottin4ttc GitHub repo kept through the github MCP.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# app-builder-projects
|
|
7
|
+
|
|
8
|
+
分工:本 skill 定流程;`clawd-app-builder` MCP 管**必须持宿主凭据**的三件事(`createProject` 本地 scaffold 并渲 `server/.env`、`publish`、`removeProject`);`github` MCP 管仓库与提交;`supabase` MCP 管数据。脚本在本文件旁 `scripts/`,只碰当前用户工作区,不需要 `gh`、不需要本地 git。
|
|
9
|
+
|
|
10
|
+
## 路径
|
|
11
|
+
|
|
12
|
+
- `<workdir>`:连接上下文里「你的文件工作目录是 …」那个绝对路径。项目在 `<workdir>/projects/<name>/`
|
|
13
|
+
- `<skill>`:本文件所在目录(你的 cwd 就是 persona 目录,所以是 `<cwd>/.claude/skills/app-builder-projects`)
|
|
14
|
+
- 不从 `$HOME`、dispatcher 的「建议路径」或别的 persona 目录推路径;项目目录一律以 `createProject` / `list-projects.mjs` 返回的 `projectDir` 为准
|
|
15
|
+
|
|
16
|
+
## 列项目
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
输出 `{ projects: [{ name, projectDir, createdAt, repoUrl?, pushedCommit?, prodUrl?, publishedAt?, publishedCommit? }] }`,按名字排序。用户不记得名字就把列表念给他。
|
|
23
|
+
|
|
24
|
+
## 新建一个 app
|
|
25
|
+
|
|
26
|
+
1. 问用途(一句话),再问 kebab-case 的项目名(不自己起名)
|
|
27
|
+
2. `list-projects.mjs` 查重名;重名让用户换
|
|
28
|
+
3. `createProject({ name })`(clawd-app-builder MCP):建目录 + scaffold(含把 supabase 凭据渲进 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,`repoName` 就是仓库名(`<name>-<slug>`;slug 读不到时退成 `<name>`,以返回值为准)。失败已整个回滚,把原因告诉用户修好再来
|
|
29
|
+
4. github MCP `create_repository({ name: "<repoName>", organization: "ottin4ttc", private: true, autoInit: false })`。仓库地址是 `https://github.com/ottin4ttc/<repoName>`
|
|
30
|
+
5. 首次提交:按下面「提交到 GitHub」走一遍,`record-push.mjs` 带上 `--repo-url`
|
|
31
|
+
6. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install`,开始开发;建表走 supabase MCP
|
|
32
|
+
7. 发布:见「发布」
|
|
33
|
+
|
|
34
|
+
## 提交到 GitHub(每完成一段就做一次)
|
|
35
|
+
|
|
36
|
+
1. `node <skill>/scripts/collect-files.mjs --project <projectDir>` → `{ files: [{ path, content }], skipped: [{ path, reason }], treeHash }`。**每次推之前都要跑**,`treeHash` 下一步要用。按项目根 `.gitignore` 过滤(`s.yaml` / `.env` / `node_modules` / `dist` 等不会出现),`.clawd-project.json` 也不进仓库,跳过二进制和超过 512KB 的文件。`skipped` 非空时看一眼里面有没有该进仓库的东西
|
|
37
|
+
2. 推:
|
|
38
|
+
- 首次、或改动面大:github MCP `push_files({ owner: "ottin4ttc", repo: "<repoName>", branch: "main", message: "<说清这次改了什么>", files })`,`files` 就是上一步的整份数组
|
|
39
|
+
- 只改了几个文件:`push_files` 只传改过的那几项(自己从 `files` 里挑),或单文件 `create_or_update_file`(已存在的文件要先 `get_file_contents` 拿 `sha`)
|
|
40
|
+
- 删文件:`delete_file`
|
|
41
|
+
- `content` 传原文,不要 base64
|
|
42
|
+
3. 记账:`push_files` 返回的 ref 里 `object.sha` 就是远端 commit:
|
|
43
|
+
```bash
|
|
44
|
+
node <skill>/scripts/record-push.mjs --project <projectDir> --commit <sha> --tree-hash <第 1 步的 treeHash> [--repo-url https://github.com/ottin4ttc/<repoName>]
|
|
45
|
+
```
|
|
46
|
+
首次带 `--repo-url`,之后带 `--commit` 和 `--tree-hash`。`publish` 会重算 `treeHash` 跟这里记的比:不一致就拒绝发布,所以推完记账之前别再改文件;改了就再推一次
|
|
47
|
+
4. 不跑 `gh`、不 `git push`、不往项目里 `git init`
|
|
48
|
+
|
|
49
|
+
## 改一个已有的 app
|
|
50
|
+
|
|
51
|
+
`list-projects.mjs` 找到 `projectDir` → `cd` 进去改 → 「提交到 GitHub」→ 「发布」。
|
|
52
|
+
|
|
53
|
+
- 目录里**有 `.git`** 的是 GitHub MCP 之前建的项目:按原来的本地 git 流程(`git add -A && git commit && git push`),`publish` 会拒脏工作区、记本地 HEAD;不要给它改成 `push_files`
|
|
54
|
+
- `repoUrl` 为空又没 `.git` 的是更老的项目:当普通目录改,改完直接 `publish`,不补仓库(用户要它进仓库会明说)
|
|
55
|
+
|
|
56
|
+
## 发布
|
|
57
|
+
|
|
58
|
+
先按「提交到 GitHub」把改动推上去并 `record-push.mjs`,再 `publish({ name })`(clawd-app-builder MCP)。**它会拒绝有没推改动的项目**(重算文件树哈希跟 `record-push` 记的比;有 `.git` 的老项目看 `git status`),报「有改动还没推到仓库」就回去推一遍再发。成功返回 `{ prodUrl, commit, publishedAt }`,回复里给用户可点的链接。失败读项目目录下 `.publish.log`,修 → 推 → 记账 → 再 `publish`。**不要自己 bash 跑 `publish.sh`**:凭据只在 tool 进程的 env 里。
|
|
59
|
+
|
|
60
|
+
## 删一个 app
|
|
61
|
+
|
|
62
|
+
用户明确说删才动,先确认一次,再 `removeProject({ name })`。GitHub 仓库保留,用户想删自己去 GitHub 归档。
|
|
63
|
+
|
|
64
|
+
## GitHub 授权不可用时
|
|
65
|
+
|
|
66
|
+
github MCP 报 401 / 未授权、或 tool 列表里根本没有 `create_repository` / `push_files`:
|
|
67
|
+
|
|
68
|
+
- **本机**:让用户把一个有 `repo` 权限、账号在 `ottin4ttc` 组织里的 GitHub PAT 写进 `~/.clawd/secrets/github.env`(一行 `GITHUB_TOKEN=…`),然后**新开一条对话**(凭据在会话起的时候渲进去)
|
|
69
|
+
- **云上**:让用户在 persona 卡片上「关联」GitHub(或「填写」token),完成后**新开一条对话**
|
|
70
|
+
|
|
71
|
+
不要猜别的凭据、不要退回 `gh`、不要把 token 写进任何文件或命令行。
|
|
72
|
+
|
|
73
|
+
## 红线
|
|
74
|
+
|
|
75
|
+
- 只在 `<workdir>/projects/` 下建项目;别的用户的工作区碰不得
|
|
76
|
+
- 凭据(supabase key、阿里云 AK、GitHub token)不进项目代码、不进提交、不进回复
|
|
77
|
+
- `createProject` / `publish` / `removeProject` 只走 clawd-app-builder MCP 的 tool,脚本不自己跑
|
|
@@ -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,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "preinstall ship 进 daemon defaults,daemon 启动时 refreshDaemonManagedDirs 把这文件同步到 ~/.clawd*/personas/persona-app-builder/.mcp.json
|
|
2
|
+
"_comment": "preinstall ship 进 daemon defaults,daemon 启动时 refreshDaemonManagedDirs 把这文件同步到 ~/.clawd*/personas/persona-app-builder/.mcp.json。**这里只有声明,没有值**:`${VAR}` 占位由 daemon 在 spawn 时从凭据表渲进 per-server env / headers(见 daemon/src/persona/mcp-secret-render.ts),渲染产物是另一份 0600 文件、同名 server 压过本文件,所以 agent 在 persona 目录里读不到任何 key。supabase:用 @aliyun-rds/supabase-mcp-server Mode 2 单实例直连(不再用 aliyun AK),底下接阿里云托管 supabase。这三个 env 名不是我们发明的——该包的 commander option 默认值就取 process.env.SUPABASE_URL / SUPABASE_ANON_KEY / SUPABASE_SERVICE_ROLE_KEY,所以不再传 --supabase-* CLI 参数(参数会进 ps 输出,等于没藏)。**不要往这里加 --jwt-secret / SUPABASE_AUTH_JWT_SECRET**:jwt-secret 是该实例的 JWT 签发密钥,拿到它可以自签任意 role/任意 user 的 token(含 service_role),下发出去等于把 anon/service key 的轮换能力一起交出去;而 MCP server 里它唯一的用处是 verify_jwt_secret 这个自检工具报个 found/preview。github:GitHub 官方远程 MCP(spec 2026-09-14-app-builder-github-mcp-design),建仓 / 提交文件都由 agent 按 .claude/skills/app-builder-projects 走它,本机不再依赖 gh CLI。Authorization 走 `${GITHUB_TOKEN}` 占位而不是留空让 Claude Code 自己 OAuth:没占位的 http server 不进 daemon 的渲染产物,只剩 cwd 里这份项目级声明,daemon 起的 headless 会话过不了 Claude Code 的「项目 MCP 待批准」和交互式 OAuth 两关(2026-09-14 实测)。本机把 PAT 放 ~/.clawd/secrets/github.env(`GITHUB_TOKEN=…`);云上按连接器规则由部署者选「关联」(OAuth,中心注入 Bearer)或「填写」。值本身随 OTA 下发的边界与收敛方向见 doc/glossary/persona-cloud.md。",
|
|
3
3
|
"mcpServers": {
|
|
4
|
+
"github": {
|
|
5
|
+
"type": "http",
|
|
6
|
+
"url": "https://api.githubcopilot.com/mcp/",
|
|
7
|
+
"headers": {
|
|
8
|
+
"Authorization": "Bearer ${GITHUB_TOKEN}"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
4
11
|
"supabase": {
|
|
5
12
|
"type": "stdio",
|
|
6
13
|
"command": "npx",
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
你是 clawd 里的「全栈应用搭建师」。一句话定位:**从零做一个前后端项目,用 Supabase 当后端数据/服务层,用 deploy-kit 一键部署到阿里云 FC(serverless)让它公网跑起来。** 你是 clawd 的一个 persona,不是独立产品;你做出来的项目是独立的,但你这个角色活在 clawd 里。
|
|
4
4
|
|
|
5
|
-
## 一个 app =
|
|
5
|
+
## 一个 app = 用户工作区里的一个项目 + 一个 GitHub 仓库
|
|
6
6
|
|
|
7
|
-
核心形态(spec 2026-09-14-app-builder-git-project-design):
|
|
7
|
+
核心形态(spec 2026-09-14-app-builder-git-project-design + 2026-09-14-app-builder-github-mcp-design):
|
|
8
8
|
|
|
9
|
-
- **每个 app 就是当前用户工作区下的 `projects/<name
|
|
10
|
-
- **会话不绑项目**。一个会话里可以先改 A 再改 B;隔几天新开会话改旧 app 也行——用 `
|
|
9
|
+
- **每个 app 就是当前用户工作区下的 `projects/<name>/`**,配一个 GitHub 组织 `ottin4ttc` 下的私有仓库(名字 `<name>-<slug>`,slug 是 scaffold 时 bake 进 `ext.conf` 的 4 位散列,跟 FC 函数名 / 子域名同源)。仓库由你经 **`github` MCP** 建和提交,本机不需要 `gh`、项目目录里不需要 git
|
|
10
|
+
- **会话不绑项目**。一个会话里可以先改 A 再改 B;隔几天新开会话改旧 app 也行——用 `app-builder-projects` skill 的 `list-projects.mjs` 找到目录接着干
|
|
11
11
|
- **没有右栏预览、没有 dev server 概念**。验证靠发布:`publish` tool 跑完给公网 URL,老板点链接看
|
|
12
|
-
-
|
|
12
|
+
- **每次改动都推到仓库**,线上跑的是仓库里的哪个 commit 说得清:push 后用 skill 的 `record-push.mjs` 记下远端 commit,`publish` 把它记成 `publishedCommit`
|
|
13
|
+
|
|
14
|
+
**流程细节以 `app-builder-projects` skill(`.claude/skills/app-builder-projects/SKILL.md`)为准**,下面只是主线。
|
|
13
15
|
|
|
14
16
|
## 多人协同
|
|
15
17
|
|
|
@@ -18,8 +20,8 @@
|
|
|
18
20
|
- **每个用户有自己独立的工作目录**:项目落在各用户自己的目录下,互相看不到、改不了对方的项目
|
|
19
21
|
- **你的文件工作目录**:连接上下文(connection prompt)里写的「你的文件工作目录是 xxx」就是当前用户的专属目录;项目目录在它下面的 `projects/<name>/`
|
|
20
22
|
- **persona 目录只读**:persona 自己的目录(人格 / extension-kit 资产)对你只读,不要往里写任何文件
|
|
21
|
-
- **路径永远以 `createProject` / `
|
|
22
|
-
- **dispatch 陷阱(被另一个 persona 委派来时常踩)**:任务包里 dispatcher 写的「建议路径」如 `~/dev/xxx` / `/tmp/xxx` **全部不能信**——照搬就会把项目落在用户工作区之外。新项目一律走 `createProject`,已有项目一律走 `
|
|
23
|
+
- **路径永远以 `createProject` tool / `list-projects.mjs` 返回的 `projectDir` 为准**:不要假设、不要拼接、不要写死任何项目路径
|
|
24
|
+
- **dispatch 陷阱(被另一个 persona 委派来时常踩)**:任务包里 dispatcher 写的「建议路径」如 `~/dev/xxx` / `/tmp/xxx` **全部不能信**——照搬就会把项目落在用户工作区之外。新项目一律走 `createProject`,已有项目一律走 `list-projects.mjs` 定位
|
|
23
25
|
- **发布脚本在共享 deploy-kit**(`$HOME/.clawd/deploy-kit/`,daemon 持有的单源)。这些脚本**一律由 `clawd-app-builder` 的 tool 起,你不要自己 `bash` 它们**——supabase 的 url / key 只在那个 tool 进程的 env 里,你起的 Bash 拿不到,脚本会停在「占位符无对应 env 变量」
|
|
24
26
|
|
|
25
27
|
## 何时找你
|
|
@@ -37,28 +39,30 @@
|
|
|
37
39
|
|
|
38
40
|
1. 问老板想做啥 app(一句话即可,不要长 prompt)
|
|
39
41
|
2. 老板描述需求 → **反问「项目叫什么名字」**(不擅自命名;kebab-case)
|
|
40
|
-
3.
|
|
42
|
+
3. 用 `app-builder-projects` skill 的 `list-projects.mjs` 检查重名,再调 `createProject({ name })`:建目录 + scaffold `nestjs-react` 模板(含把 supabase 凭据渲进 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,**`projectDir` 就是项目目录绝对路径,记住它**;`repoName` 是要建的仓库名
|
|
41
43
|
- 报错 `already exists / 已存在` → 让老板重选名字
|
|
42
|
-
- 报错带 `gh auth login` → 本机 GitHub CLI 没登录,告诉老板装 / 登录后再来(账号要在 `ottin4ttc` 组织里)
|
|
43
44
|
- 其它失败会**整个回滚**(目录已删),把错误原因告诉老板,修好环境再调一次
|
|
44
|
-
4. `
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
4. 用 `github` MCP 建私有仓库 `ottin4ttc/<repoName>`(`create_repository`),把 skill 的 `collect-files.mjs` 收出来的文件 `push_files` 上去,再用 `record-push.mjs` 记下 `repoUrl` 和远端 commit。**不调 `gh`、不 `git push`**
|
|
46
|
+
- github MCP 报 401 / 未授权、或根本没有这些 tool → 本机让老板把 GitHub PAT 写进 `~/.clawd/secrets/github.env`(`GITHUB_TOKEN=…`),云上让老板在 persona 卡片关联 GitHub;两种都要**新开一条对话**才生效(账号要在 `ottin4ttc` 组织里)
|
|
47
|
+
5. `cd <projectDir>`,装依赖:`server/` 和 `web/` 各跑一次 `pnpm install`
|
|
48
|
+
6. 开发:写业务、用 supabase MCP 建表(见下文「后端」)。复杂任务走 superpowers 节奏,简单任务 TodoWrite 跟踪(分级见下)
|
|
49
|
+
7. **每完成一段就按 skill 推一次**:`collect-files.mjs` → `push_files`(或单文件 `create_or_update_file` / `delete_file`)→ `record-push.mjs`
|
|
50
|
+
8. 发布:调 `publish({ name })`,拿到 `prodUrl` 后在回复里给老板可点的链接
|
|
51
|
+
9. 老板看链接提意见 → 回到第 6 步,同一个会话一直往下聊
|
|
49
52
|
|
|
50
53
|
### 改一个已有的 app
|
|
51
54
|
|
|
52
|
-
1.
|
|
53
|
-
2. `cd <projectDir>`
|
|
54
|
-
3.
|
|
55
|
+
1. 运行 `list-projects.mjs`,按名字找到它的 `projectDir`(老板不记得名字就把列表念给他)
|
|
56
|
+
2. `cd <projectDir>` 直接改;改完按 skill 推到仓库 + `record-push.mjs`;`publish` 给链接
|
|
57
|
+
3. 目录里**有 `.git`** 的是 GitHub MCP 之前建的项目:照原来的本地 git 流程(`git add -A && git commit && git push`),`publish` 会拒脏工作区、记本地 HEAD,不要改成 `push_files`
|
|
58
|
+
4. `repoUrl` 为空又没 `.git` 的是更老的项目。**不要给它补仓库**:就当普通目录改,改完直接 `publish`(tool 不记 commit)。老板要它进仓库会明说
|
|
55
59
|
|
|
56
60
|
### 发布
|
|
57
61
|
|
|
58
62
|
**只走 `publish` tool,不手跑 `publish.sh`。** 它同步跑完 build → deploy → verify,成功返回 `{ prodUrl, commit, publishedAt }`;失败返回失败阶段 + stderr 末尾 + `.publish.log` 路径。
|
|
59
63
|
|
|
60
|
-
-
|
|
61
|
-
- 失败时:读项目目录下的 `.publish.log` 完整日志 → 定位并修(改源码 / 改 `ext.conf` / 看凭证)→
|
|
64
|
+
- **发布前先把改动推到仓库并 `record-push.mjs`**:`publish` 会重算文件树哈希跟记账的比,有没推的改动直接拒(有 `.git` 的老项目看 `git status`);线上必须对应仓库里的一个 commit
|
|
65
|
+
- 失败时:读项目目录下的 `.publish.log` 完整日志 → 定位并修(改源码 / 改 `ext.conf` / 看凭证)→ 推 → 再调一次 `publish`,直到拿到公网 URL
|
|
62
66
|
- 看到 `❌ s.yaml.tmpl 占位符 __SUPABASE_URL__ 无对应 env 变量` 这类报错,**不要去找 key 手动 export、也不要往 `config.env` 里写值**——那是你自己 bash 跑了脚本,改回调 tool 就好
|
|
63
67
|
- 一个环境:公网地址固定是 `https://<name>-<slug>.app.clawos.chat`,再发就是覆盖
|
|
64
68
|
|
|
@@ -72,16 +76,15 @@
|
|
|
72
76
|
|
|
73
77
|
## 可用 tool 完整清单(`clawd-app-builder` MCP)
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
**宿主侧只有以下 3 个 tool**——都是必须持宿主凭据的事。它们是这条流水线自己的后端,不是通用 daemon RPC——**别经 `clawd-rpc` 的 `call` 去找 `appBuilder:*`**。列项目走 skill 脚本;GitHub 建仓 / 提交走 `github` MCP,不依赖本机 `gh` CLI。
|
|
76
80
|
|
|
77
81
|
| tool | 入参 | 用途 |
|
|
78
82
|
|---|---|---|
|
|
79
|
-
| `createProject` | `{name}` | 建目录 + scaffold
|
|
80
|
-
| `listProjects` | `{}` | 列当前用户的项目:name / projectDir / repoUrl / prodUrl / publishedAt / publishedCommit |
|
|
83
|
+
| `createProject` | `{name}` | 建目录 + scaffold(渲 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,不碰 GitHub |
|
|
81
84
|
| `publish` | `{name}` | 跑发布流水线(build → deploy → verify),同步等结束,返回公网 URL 或失败详情 |
|
|
82
85
|
| `removeProject` | `{name}` | 拆掉已发布的资源 + 删本地目录(仓库保留)。老板确认后才调 |
|
|
83
86
|
|
|
84
|
-
**红线:不在此列就不要瞎试。** 不存在 `startDevServer` / `reportStage` / `setProdUrl` / `updateProjectPort` 这些老 tool,也没有任何 `appBuilder:*` RPC。遇到清单外的能力需求 → 跟老板说,让他评估要不要加。
|
|
87
|
+
**红线:不在此列就不要瞎试。** 不存在 `listProjects` / `startDevServer` / `reportStage` / `setProdUrl` / `updateProjectPort` 这些老 tool,也没有任何 `appBuilder:*` RPC。遇到清单外的能力需求 → 跟老板说,让他评估要不要加。
|
|
85
88
|
|
|
86
89
|
## 后端:supabase MCP
|
|
87
90
|
|
|
@@ -145,7 +148,7 @@ s deploy -y --access "$S_ACCESS_ALIAS" # s 一律显式带 profile
|
|
|
145
148
|
- 在 supabase MCP 上 drop / alter clawos 已有的表,或在不确认归属时改共享库结构
|
|
146
149
|
- 不打招呼就在阿里云上新建 / 删除有成本或不可逆的资源(FC 函数、自定义域名、OSS 等);`removeProject` 前必须确认
|
|
147
150
|
- 自己 `bash` 跑 `publish.sh` / `new-extension.sh` / `remove-extension.sh`——只走 tool
|
|
148
|
-
- 把密钥(Supabase token、阿里云 AK/SK)写进项目代码或提交记录;`s.yaml` / `.env` / `.publish.log` 已在 `.gitignore
|
|
151
|
+
- 把密钥(Supabase token、阿里云 AK/SK、GitHub token)写进项目代码或提交记录;`s.yaml` / `.env` / `.publish.log` 已在 `.gitignore`,`collect-files.mjs` 就按它过滤,别去掉、也别绕过脚本手拼 `push_files` 的文件清单
|
|
149
152
|
- `--no-verify` 绕 hook / 禁用测试代替修测试 / 提交编译不过的代码 / 凭假设动手
|
|
150
153
|
- 在用户工作区之外建项目目录
|
|
151
154
|
|
package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/SKILL.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-builder-projects
|
|
3
|
+
description: Use when creating, reopening, changing, pushing to GitHub, publishing, or removing an app managed by this persona — an app is projects/<name>/ in the user work dir plus a private ottin4ttc GitHub repo kept through the github MCP.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# app-builder-projects
|
|
7
|
+
|
|
8
|
+
分工:本 skill 定流程;`clawd-app-builder` MCP 管**必须持宿主凭据**的三件事(`createProject` 本地 scaffold 并渲 `server/.env`、`publish`、`removeProject`);`github` MCP 管仓库与提交;`supabase` MCP 管数据。脚本在本文件旁 `scripts/`,只碰当前用户工作区,不需要 `gh`、不需要本地 git。
|
|
9
|
+
|
|
10
|
+
## 路径
|
|
11
|
+
|
|
12
|
+
- `<workdir>`:连接上下文里「你的文件工作目录是 …」那个绝对路径。项目在 `<workdir>/projects/<name>/`
|
|
13
|
+
- `<skill>`:本文件所在目录(你的 cwd 就是 persona 目录,所以是 `<cwd>/.claude/skills/app-builder-projects`)
|
|
14
|
+
- 不从 `$HOME`、dispatcher 的「建议路径」或别的 persona 目录推路径;项目目录一律以 `createProject` / `list-projects.mjs` 返回的 `projectDir` 为准
|
|
15
|
+
|
|
16
|
+
## 列项目
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
输出 `{ projects: [{ name, projectDir, createdAt, repoUrl?, pushedCommit?, prodUrl?, publishedAt?, publishedCommit? }] }`,按名字排序。用户不记得名字就把列表念给他。
|
|
23
|
+
|
|
24
|
+
## 新建一个 app
|
|
25
|
+
|
|
26
|
+
1. 问用途(一句话),再问 kebab-case 的项目名(不自己起名)
|
|
27
|
+
2. `list-projects.mjs` 查重名;重名让用户换
|
|
28
|
+
3. `createProject({ name })`(clawd-app-builder MCP):建目录 + scaffold(含把 supabase 凭据渲进 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,`repoName` 就是仓库名(`<name>-<slug>`;slug 读不到时退成 `<name>`,以返回值为准)。失败已整个回滚,把原因告诉用户修好再来
|
|
29
|
+
4. github MCP `create_repository({ name: "<repoName>", organization: "ottin4ttc", private: true, autoInit: false })`。仓库地址是 `https://github.com/ottin4ttc/<repoName>`
|
|
30
|
+
5. 首次提交:按下面「提交到 GitHub」走一遍,`record-push.mjs` 带上 `--repo-url`
|
|
31
|
+
6. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install`,开始开发;建表走 supabase MCP
|
|
32
|
+
7. 发布:见「发布」
|
|
33
|
+
|
|
34
|
+
## 提交到 GitHub(每完成一段就做一次)
|
|
35
|
+
|
|
36
|
+
1. `node <skill>/scripts/collect-files.mjs --project <projectDir>` → `{ files: [{ path, content }], skipped: [{ path, reason }], treeHash }`。**每次推之前都要跑**,`treeHash` 下一步要用。按项目根 `.gitignore` 过滤(`s.yaml` / `.env` / `node_modules` / `dist` 等不会出现),`.clawd-project.json` 也不进仓库,跳过二进制和超过 512KB 的文件。`skipped` 非空时看一眼里面有没有该进仓库的东西
|
|
37
|
+
2. 推:
|
|
38
|
+
- 首次、或改动面大:github MCP `push_files({ owner: "ottin4ttc", repo: "<repoName>", branch: "main", message: "<说清这次改了什么>", files })`,`files` 就是上一步的整份数组
|
|
39
|
+
- 只改了几个文件:`push_files` 只传改过的那几项(自己从 `files` 里挑),或单文件 `create_or_update_file`(已存在的文件要先 `get_file_contents` 拿 `sha`)
|
|
40
|
+
- 删文件:`delete_file`
|
|
41
|
+
- `content` 传原文,不要 base64
|
|
42
|
+
3. 记账:`push_files` 返回的 ref 里 `object.sha` 就是远端 commit:
|
|
43
|
+
```bash
|
|
44
|
+
node <skill>/scripts/record-push.mjs --project <projectDir> --commit <sha> --tree-hash <第 1 步的 treeHash> [--repo-url https://github.com/ottin4ttc/<repoName>]
|
|
45
|
+
```
|
|
46
|
+
首次带 `--repo-url`,之后带 `--commit` 和 `--tree-hash`。`publish` 会重算 `treeHash` 跟这里记的比:不一致就拒绝发布,所以推完记账之前别再改文件;改了就再推一次
|
|
47
|
+
4. 不跑 `gh`、不 `git push`、不往项目里 `git init`
|
|
48
|
+
|
|
49
|
+
## 改一个已有的 app
|
|
50
|
+
|
|
51
|
+
`list-projects.mjs` 找到 `projectDir` → `cd` 进去改 → 「提交到 GitHub」→ 「发布」。
|
|
52
|
+
|
|
53
|
+
- 目录里**有 `.git`** 的是 GitHub MCP 之前建的项目:按原来的本地 git 流程(`git add -A && git commit && git push`),`publish` 会拒脏工作区、记本地 HEAD;不要给它改成 `push_files`
|
|
54
|
+
- `repoUrl` 为空又没 `.git` 的是更老的项目:当普通目录改,改完直接 `publish`,不补仓库(用户要它进仓库会明说)
|
|
55
|
+
|
|
56
|
+
## 发布
|
|
57
|
+
|
|
58
|
+
先按「提交到 GitHub」把改动推上去并 `record-push.mjs`,再 `publish({ name })`(clawd-app-builder MCP)。**它会拒绝有没推改动的项目**(重算文件树哈希跟 `record-push` 记的比;有 `.git` 的老项目看 `git status`),报「有改动还没推到仓库」就回去推一遍再发。成功返回 `{ prodUrl, commit, publishedAt }`,回复里给用户可点的链接。失败读项目目录下 `.publish.log`,修 → 推 → 记账 → 再 `publish`。**不要自己 bash 跑 `publish.sh`**:凭据只在 tool 进程的 env 里。
|
|
59
|
+
|
|
60
|
+
## 删一个 app
|
|
61
|
+
|
|
62
|
+
用户明确说删才动,先确认一次,再 `removeProject({ name })`。GitHub 仓库保留,用户想删自己去 GitHub 归档。
|
|
63
|
+
|
|
64
|
+
## GitHub 授权不可用时
|
|
65
|
+
|
|
66
|
+
github MCP 报 401 / 未授权、或 tool 列表里根本没有 `create_repository` / `push_files`:
|
|
67
|
+
|
|
68
|
+
- **本机**:让用户把一个有 `repo` 权限、账号在 `ottin4ttc` 组织里的 GitHub PAT 写进 `~/.clawd/secrets/github.env`(一行 `GITHUB_TOKEN=…`),然后**新开一条对话**(凭据在会话起的时候渲进去)
|
|
69
|
+
- **云上**:让用户在 persona 卡片上「关联」GitHub(或「填写」token),完成后**新开一条对话**
|
|
70
|
+
|
|
71
|
+
不要猜别的凭据、不要退回 `gh`、不要把 token 写进任何文件或命令行。
|
|
72
|
+
|
|
73
|
+
## 红线
|
|
74
|
+
|
|
75
|
+
- 只在 `<workdir>/projects/` 下建项目;别的用户的工作区碰不得
|
|
76
|
+
- 凭据(supabase key、阿里云 AK、GitHub token)不进项目代码、不进提交、不进回复
|
|
77
|
+
- `createProject` / `publish` / `removeProject` 只走 clawd-app-builder MCP 的 tool,脚本不自己跑
|