@clawos-dev/clawd 0.2.112-beta.217.80b79a2 → 0.2.112-beta.220.a78f1c0

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 (29) hide show
  1. package/dist/cli.cjs +1125 -1070
  2. package/dist/persona-defaults/persona-app-builder/CLAUDE.md +168 -0
  3. package/dist/persona-defaults/persona-app-builder/extension-kit/README.md +96 -0
  4. package/dist/persona-defaults/persona-app-builder/extension-kit/config.env +20 -0
  5. package/dist/persona-defaults/persona-app-builder/extension-kit/contract/bootstrap +22 -0
  6. package/dist/persona-defaults/persona-app-builder/extension-kit/contract/s.yaml.tmpl +54 -0
  7. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.env.example +3 -0
  8. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.fcignore +7 -0
  9. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/nest-cli.json +8 -0
  10. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/package-lock.json +4531 -0
  11. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/package.json +28 -0
  12. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +10 -0
  13. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +49 -0
  14. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.controller.ts +27 -0
  15. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.module.ts +9 -0
  16. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.service.ts +38 -0
  17. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +8 -0
  18. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/tsconfig.json +14 -0
  19. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/index.html +12 -0
  20. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/package-lock.json +1680 -0
  21. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/package.json +18 -0
  22. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +161 -0
  23. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/main.jsx +5 -0
  24. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/vite.config.js +30 -0
  25. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/new-extension.sh +49 -0
  26. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/publish.sh +78 -0
  27. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/remove-extension.sh +57 -0
  28. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/verify.sh +20 -0
  29. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -132,11 +132,15 @@ var init_methods = __esm({
132
132
  "attachment.groupRemove",
133
133
  "attachment.groupList",
134
134
  // ---- capability:* (capability platform 鉴权底座) ----
135
- // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal token / 邀请码全链路删除——
136
- // hub 已完整替代(中心 server 签发 per-hub connect token)。capability:list / capability:delete
137
- // 保留:调试 + whoami 反查 + 撤销级联 + 清旧 per-peer cap。personal-cap:get 已下线。
135
+ // global personal token 模型 (2026-05-25): UI 不再调 capability:issue —— daemon 启动时
136
+ // loadOrCreatePersonalCapability 自动创建 1 fixed-grants personal capability,
137
+ // 所有联系人共用 personal token。capability:list / capability:delete 保留 (供调试 /
138
+ // 老板手动清旧 cap);capability:issue / capability:bindPeer 已下线。
138
139
  "capability:list",
139
140
  "capability:delete",
141
+ // global personal token (2026-05-25): UI invite/accept dialog 挂载即调,拿 personal
142
+ // token + capability + shareBaseUrl 用于拼 inviteUrl / autoAttach 反向推。owner-only。
143
+ "personal-cap:get",
140
144
  // ---- inbox:* (capability platform Phase 3 跨用户通知 + Phase 4 DM) ----
141
145
  // owner 接 guest 的 cross-principal 消息事件 + DM 双向私聊.
142
146
  // inbox:list / markRead: admin-only (Phase 3); inbox:postMessage: DM 自带能力,
@@ -144,11 +148,14 @@ var init_methods = __esm({
144
148
  "inbox:list",
145
149
  "inbox:markRead",
146
150
  "inbox:postMessage",
147
- // ---- received-capability:* (联系人列表, 视角 B 双向授权模型 2026-05-23) ----
148
- // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): 邀请码 add / autoAttach 已删除——
149
- // hub:connect 落同一 store + 自动反向(决策 #11)替代。list / remove 保留作联系人列表读删。
151
+ // ---- received-capability:* (对方颁给我的 cap, 视角 B 双向授权模型, 2026-05-23) ----
152
+ // owner 入口: list / add / remove
153
+ // guest 端: autoAttach (B A token 后通过 cap channel ws 推 cap 给 A)
154
+ // 详见 received-capability.ts JSDoc + spec 2026-05-23-bidirectional-cap-design.md
150
155
  "received-capability:list",
156
+ "received-capability:add",
151
157
  "received-capability:remove",
158
+ "received-capability:autoAttach",
152
159
  // ---- whoami (v2 capability platform) ----
153
160
  // 任何 authed connection 都能调;返回 owner 显示名 + 当前 capability wire 形态 +
154
161
  // grants 对应的 persona 元数据 (id + displayName)。UI 端 AddRemotePersonaDialog
@@ -174,17 +181,16 @@ var init_methods = __esm({
174
181
  // 无 subscription 持久化:guest 端身份完全来自文件系统 + reconciled publishedExtensions.
175
182
  "extension.install-from-channel",
176
183
  "extension.update-from-channel",
177
- // ---- auth:* 飞书统一身份 Phase 1 (spec 2026-06-01-feishu-identity-design) ----
178
- // owner-only:daemon loopback 登录流程(auth:login:start state HTTP /auth/callback
179
- // 回调落盘 auth:login:done 事件)。中央字面量见 protocol/feishu-auth.ts FEISHU_AUTH_METHODS。
180
- "auth:login:start",
181
- "auth:getIdentity",
182
- "auth:logout",
183
- // ---- hub:* 飞书统一身份 Phase 2 (spec §10.2) ----
184
- // owner-only:公共 hub 目录 + per-hub connect token 连接(daemon 代理中心 server)。
185
- // 中央字面量见 protocol/feishu-auth.ts HUB_METHODS。
186
- "hub:list",
187
- "hub:connect"
184
+ // ---- app-builder Project picker (spec 2026-06-01-app-builder-project-picker-design) ----
185
+ // persona-app-builder project 管理;schemas: ProjectMetadataSchema + 4 args/result 对。
186
+ // listProjects: readdir projects/,按目录读 .clawd-project.json
187
+ // createProject: 校验名 + 从 6173-6182 段分配端口 + 写元数据 + 创建空目录
188
+ // deleteProject: 停 dev server + rm -rf 目录 + 连带删该 project 名下所有 session
189
+ // updateProjectPort: 校验新端口段内 + 未被别的 project 占 + 重启 dev server
190
+ "appBuilder:listProjects",
191
+ "appBuilder:createProject",
192
+ "appBuilder:deleteProject",
193
+ "appBuilder:updateProjectPort"
188
194
  ];
189
195
  }
190
196
  });
@@ -246,11 +252,7 @@ var init_errors = __esm({
246
252
  INTERNAL: "INTERNAL",
247
253
  UNAUTHORIZED: "UNAUTHORIZED",
248
254
  FORBIDDEN: "FORBIDDEN",
249
- INVALID_PARAM: "INVALID_PARAM",
250
- // 飞书统一身份(spec 2026-06-01 决策 #9):People/远程体系强制飞书登录。
251
- // daemon 进程身份未激活为飞书 unionId 时,FEISHU_GATED_METHODS 内的 RPC 一律返回此错误;
252
- // UI 据此显示登录引导(区别于 UNAUTHORIZED 的越权语义)。
253
- FEISHU_LOGIN_REQUIRED: "FEISHU_LOGIN_REQUIRED"
255
+ INVALID_PARAM: "INVALID_PARAM"
254
256
  };
255
257
  ClawdError = class extends Error {
256
258
  constructor(code, message) {
@@ -4530,7 +4532,7 @@ var init_persona_schemas = __esm({
4530
4532
  });
4531
4533
 
4532
4534
  // ../protocol/src/schemas.ts
4533
- var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, PeerSessionUpsertArgs, PeerSessionUpsertResponseSchema, PeerSessionRemoveArgs, PeerSessionRemoveResponseSchema, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema;
4535
+ var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, PeerSessionUpsertArgs, PeerSessionUpsertResponseSchema, PeerSessionRemoveArgs, PeerSessionRemoveResponseSchema, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelReadyEventSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema, PROJECT_PORT_MIN, PROJECT_PORT_MAX, projectNameRegex, DEFAULT_DEV_COMMAND, ProjectMetadataSchema, ProjectWithStatusSchema, ListProjectsArgsSchema, ListProjectsResultSchema, CreateProjectArgsSchema, CreateProjectResultSchema, DeleteProjectArgsSchema, DeleteProjectResultSchema, UpdateProjectPortArgsSchema, UpdateProjectPortResultSchema;
4534
4536
  var init_schemas = __esm({
4535
4537
  "../protocol/src/schemas.ts"() {
4536
4538
  "use strict";
@@ -4627,6 +4629,10 @@ var init_schemas = __esm({
4627
4629
  // owner-mode persona session 身份标识;UI 用它在 SessionList 过滤 + jump,
4628
4630
  // daemon 用它做 idempotent dedupe + spawn 时派生 ctx.personaMode='owner'
4629
4631
  ownerPersonaId: external_exports.string().min(1).optional(),
4632
+ // app-builder Project 反向索引(spec: superpowers/specs/2026-06-01-app-builder-project-picker-design.md)。
4633
+ // 当此 session 绑定到某个 build 中的 project 时填写;name 与 projects/<name>/ 子目录同名,
4634
+ // 受 projectNameSchema regex 约束。daemon session:resume 时校验该目录存在,不存在则拒。
4635
+ appBuilderProject: external_exports.string().regex(/^[a-z][a-z0-9-]{0,39}$/).optional(),
4630
4636
  // listener-mode sub-session 的原始 chatId(owner-mode 不写;listener-scope 强制写入)。
4631
4637
  // 派生 sessionId 是单向 hash `${personaId}-${tokenHash12}-${chatHash8}`,必须保留原始 chatId
4632
4638
  // 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
@@ -4899,7 +4905,12 @@ var init_schemas = __esm({
4899
4905
  forkedFromSessionId: external_exports.string().min(1).optional(),
4900
4906
  // owner-mode persona session:传此字段时 daemon 自行派生 cwd 和启动参数,
4901
4907
  // 且按 ownerPersonaId 做 idempotent dedupe(每 persona 永远只有一个 owner session)
4902
- ownerPersonaId: external_exports.string().min(1).optional()
4908
+ ownerPersonaId: external_exports.string().min(1).optional(),
4909
+ // app-builder Project 绑定(spec 2026-06-01-app-builder-project-picker-design)。
4910
+ // UI picker 上点 idle project / 新建 project 时传入;daemon 透传写入 SessionFile.appBuilderProject。
4911
+ // 跟 ownerPersonaId 同层但不互斥(典型场景两者都传:ownerPersonaId='persona-app-builder' +
4912
+ // appBuilderProject='<project-name>')。
4913
+ appBuilderProject: external_exports.string().regex(/^[a-z][a-z0-9-]{0,39}$/).optional()
4903
4914
  }).refine((args) => args.cwd != null || args.ownerPersonaId != null, {
4904
4915
  message: "cwd \u4E0E ownerPersonaId \u81F3\u5C11\u4F20\u4E00\u4E2A"
4905
4916
  });
@@ -4913,7 +4924,11 @@ var init_schemas = __esm({
4913
4924
  effort: external_exports.string().optional(),
4914
4925
  cwd: external_exports.string().optional(),
4915
4926
  // 用户在 Edit modal 选择的 icon 标识;UI 端做 enum 兜底
4916
- iconKey: external_exports.string().optional()
4927
+ iconKey: external_exports.string().optional(),
4928
+ // app-builder picker 在当前 session 内切 project 时写入(spec 2026-06-01-app-builder-project-picker-design)。
4929
+ // daemon session:update handler 检测此字段变化时停旧 project dev server + 起新 project dev server。
4930
+ // 设为空字符串 '' 表示解绑(picker 不暴露解绑入口,但 schema 允许)。
4931
+ appBuilderProject: external_exports.string().regex(/^[a-z][a-z0-9-]{0,39}$/).or(external_exports.literal("")).optional()
4917
4932
  })
4918
4933
  });
4919
4934
  SessionSendArgs = external_exports.object({
@@ -5110,12 +5125,6 @@ var init_schemas = __esm({
5110
5125
  * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
5111
5126
  * 把 cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
5112
5127
  * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
5113
- *
5114
- * 飞书统一身份 Phase 1 (2026-06-01):取值升级为飞书 union_id(owner 已飞书登录时;
5115
- * 来自 whoami.owner.id → daemon ownerPrincipalId → owner-identity.json unionId)。
5116
- * 未登录 daemon 仍为 auth.json 的 owner-<uuid>。daemon 端不验证该身份真实性
5117
- * (Phase 1 信任模型,见 spec §9)。Phase 2 将以 connect token 替代(introspect
5118
- * 返回身份,删除自报字段,见 spec §10)。
5119
5128
  */
5120
5129
  selfPrincipalId: external_exports.string().min(1).optional(),
5121
5130
  /**
@@ -5123,23 +5132,18 @@ var init_schemas = __esm({
5123
5132
  * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5124
5133
  * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5125
5134
  */
5126
- selfDisplayName: external_exports.string().optional(),
5127
- /**
5128
- * 飞书统一身份 Phase 2(spec 决策 #11,自动反向建立联系人):guest 自报自己可达的
5129
- * ws 地址(tunnel 地址优先)。hub introspect 验票成功后,若 guest 带 selfUrl,hub 自动
5130
- * 把 guest 落入本机 ReceivedCapabilityStore(remoteUrl = selfUrl)+ 广播 received-capability:added,
5131
- * 让 hub 的 People 立即出现该 guest——双向互为联系人,身份由 introspect 背书不可伪造。
5132
- *
5133
- * 缺省(A 无公网地址)→ hub 反向连不上 A,**不**自动建 cap;双向 IM 仅在 A 在线连接
5134
- * 期间通过该连接收发(spec 决策 #11 限制)。不填假地址(不造假数据)。
5135
- */
5136
- selfUrl: external_exports.string().min(1).optional()
5135
+ selfDisplayName: external_exports.string().optional()
5137
5136
  });
5138
5137
  AuthOkFrameSchema = external_exports.object({
5139
5138
  type: external_exports.literal("auth:ok"),
5140
5139
  version: external_exports.string().min(1).optional(),
5141
5140
  protocolVersion: external_exports.number().int().nonnegative().optional()
5142
5141
  });
5142
+ TunnelReadyEventSchema = external_exports.object({
5143
+ type: external_exports.literal("tunnel:ready"),
5144
+ url: external_exports.string().min(1),
5145
+ subdomain: external_exports.string().min(1)
5146
+ });
5143
5147
  TunnelExitedEventSchema = external_exports.object({
5144
5148
  type: external_exports.literal("tunnel:exited"),
5145
5149
  code: external_exports.number().int().nullable(),
@@ -5181,6 +5185,44 @@ var init_schemas = __esm({
5181
5185
  /** file-sharing HTTP 路由的 Authorization: Bearer token;与 WS token 解耦,HTTP 401 仅触发 ReAuth 不强踢 WS(spec §11 第 4 条) */
5182
5186
  httpToken: external_exports.string().optional()
5183
5187
  });
5188
+ PROJECT_PORT_MIN = 6173;
5189
+ PROJECT_PORT_MAX = 6182;
5190
+ projectNameRegex = /^[a-z][a-z0-9-]{0,39}$/;
5191
+ DEFAULT_DEV_COMMAND = "cd server && pnpm dev";
5192
+ ProjectMetadataSchema = external_exports.object({
5193
+ name: external_exports.string().regex(projectNameRegex, {
5194
+ message: "kebab-case, \u5C0F\u5199\u5B57\u6BCD\u5F00\u5934\uFF0C\u53EA\u5141\u8BB8\u5C0F\u5199\u5B57\u6BCD / \u6570\u5B57 / -\uFF0C\u6700\u957F 40 \u5B57\u7B26"
5195
+ }),
5196
+ port: external_exports.number().int().min(PROJECT_PORT_MIN).max(PROJECT_PORT_MAX),
5197
+ createdAt: external_exports.string().datetime(),
5198
+ /** dev server 启动命令(shell 字符串);占位符 $CLAWD_PREVIEW_PORT 替换 + env 注入 */
5199
+ devCommand: external_exports.string().min(1)
5200
+ });
5201
+ ProjectWithStatusSchema = ProjectMetadataSchema.extend({
5202
+ isRunning: external_exports.boolean(),
5203
+ boundSessionId: external_exports.string().nullable()
5204
+ });
5205
+ ListProjectsArgsSchema = external_exports.object({}).strict();
5206
+ ListProjectsResultSchema = external_exports.object({
5207
+ projects: external_exports.array(ProjectWithStatusSchema)
5208
+ }).strict();
5209
+ CreateProjectArgsSchema = external_exports.object({
5210
+ name: external_exports.string()
5211
+ }).strict();
5212
+ CreateProjectResultSchema = external_exports.object({
5213
+ project: ProjectMetadataSchema
5214
+ }).strict();
5215
+ DeleteProjectArgsSchema = external_exports.object({
5216
+ name: external_exports.string()
5217
+ }).strict();
5218
+ DeleteProjectResultSchema = external_exports.object({}).strict();
5219
+ UpdateProjectPortArgsSchema = external_exports.object({
5220
+ name: external_exports.string(),
5221
+ newPort: external_exports.number().int()
5222
+ }).strict();
5223
+ UpdateProjectPortResultSchema = external_exports.object({
5224
+ project: ProjectMetadataSchema
5225
+ }).strict();
5184
5226
  }
5185
5227
  });
5186
5228
 
@@ -5223,7 +5265,7 @@ function stripSecretHash(cap) {
5223
5265
  const { secretHash: _hash, ...wire } = cap;
5224
5266
  return wire;
5225
5267
  }
5226
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS;
5268
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS, PersonalCapGetResponseSchema;
5227
5269
  var init_capability = __esm({
5228
5270
  "../protocol/src/capability.ts"() {
5229
5271
  "use strict";
@@ -5291,6 +5333,13 @@ var init_capability = __esm({
5291
5333
  actions: Object.freeze(["read", "send"])
5292
5334
  })
5293
5335
  ]);
5336
+ PersonalCapGetResponseSchema = external_exports.object({
5337
+ type: external_exports.literal("personal-cap:get:ok"),
5338
+ token: external_exports.string().min(1),
5339
+ capability: CapabilityWireSchema,
5340
+ /** ws/wss base URL,UI deriveInviteBase 用来拼 inviteUrl(tunnel 优先 / 否则本机 ws) */
5341
+ shareBaseUrl: external_exports.string().min(1)
5342
+ }).strict();
5294
5343
  }
5295
5344
  });
5296
5345
 
@@ -5326,7 +5375,7 @@ var init_inbox = __esm({
5326
5375
  });
5327
5376
 
5328
5377
  // ../protocol/src/received-capability.ts
5329
- var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5378
+ var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5330
5379
  var init_received_capability = __esm({
5331
5380
  "../protocol/src/received-capability.ts"() {
5332
5381
  "use strict";
@@ -5337,15 +5386,19 @@ var init_received_capability = __esm({
5337
5386
  ownerDisplayName: external_exports.string(),
5338
5387
  remoteUrl: external_exports.string().min(1),
5339
5388
  capabilityId: external_exports.string().min(1),
5340
- // Phase 2 自动反向建立联系人(spec 决策 #11):hub introspect 验票成功后自动落 A 的
5341
- // received cap,但 hub 不持有 A 颁发的 token(A 从未给 hub 换票)→ connectToken 为空串。
5342
- // hub:connect 出站路径仍存中心 server 签发的真实 connect token(断线重连复用)。
5343
- // 不用 .min(1):自动反向路径合法地存空串,禁止填假 token 占位(不造假数据)。
5344
- connectToken: external_exports.string(),
5389
+ capabilityToken: external_exports.string().min(1),
5345
5390
  grants: external_exports.array(GrantSchema),
5346
5391
  receivedAt: external_exports.number().int().nonnegative()
5347
5392
  }).strict();
5348
5393
  ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5394
+ ReceivedCapabilityAddArgsSchema = external_exports.object({
5395
+ remoteUrl: external_exports.string().min(1),
5396
+ token: external_exports.string().min(1)
5397
+ }).strict();
5398
+ ReceivedCapabilityAddOkSchema = external_exports.object({
5399
+ type: external_exports.literal("received-capability:add:ok"),
5400
+ receivedCap: ReceivedCapabilityWireSchema
5401
+ }).strict();
5349
5402
  ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5350
5403
  ownerPrincipalId: external_exports.string().min(1)
5351
5404
  }).strict();
@@ -5357,6 +5410,17 @@ var init_received_capability = __esm({
5357
5410
  type: external_exports.literal("received-capability:list:ok"),
5358
5411
  receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
5359
5412
  }).strict();
5413
+ ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5414
+ ownerPrincipalId: external_exports.string().min(1),
5415
+ ownerDisplayName: external_exports.string(),
5416
+ remoteUrl: external_exports.string().min(1),
5417
+ capabilityId: external_exports.string().min(1),
5418
+ token: external_exports.string().min(1),
5419
+ grants: external_exports.array(GrantSchema)
5420
+ }).strict();
5421
+ ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5422
+ type: external_exports.literal("received-capability:autoAttach:ok")
5423
+ }).strict();
5360
5424
  ReceivedCapabilityAddedFrameSchema = external_exports.object({
5361
5425
  type: external_exports.literal("received-capability:added"),
5362
5426
  receivedCap: ReceivedCapabilityWireSchema
@@ -5567,94 +5631,6 @@ var init_extension = __esm({
5567
5631
  }
5568
5632
  });
5569
5633
 
5570
- // ../protocol/src/feishu-auth.ts
5571
- var FEISHU_GATED_METHODS, HubEntrySchema, HubListResponseSchema, HubConnectArgsSchema, HubConnectResponseSchema, FeishuIdentitySchema, AuthLoginStartResponseSchema, AuthGetIdentityResponseSchema, AuthLogoutResponseSchema, AuthLoginDoneEventSchema, AuthLoginFailedEventSchema;
5572
- var init_feishu_auth = __esm({
5573
- "../protocol/src/feishu-auth.ts"() {
5574
- "use strict";
5575
- init_zod();
5576
- FEISHU_GATED_METHODS = [
5577
- // capability(调试 / 撤销,仍属远程身份体系)
5578
- "capability:list",
5579
- "capability:delete",
5580
- // 联系人列表(hub:connect 落同一 store;list 读 / remove 删)
5581
- "received-capability:list",
5582
- "received-capability:remove",
5583
- // DM / 跨用户通知
5584
- "inbox:list",
5585
- "inbox:markRead",
5586
- "inbox:postMessage",
5587
- // mirror peer sessions(远端会话镜像)
5588
- "peerSession:upsert",
5589
- "peerSession:remove",
5590
- // Phase 2: hub 目录 + 连接(中心 server 代理,需要 owner 的 ttcJwt)
5591
- "hub:list",
5592
- "hub:connect"
5593
- ];
5594
- HubEntrySchema = external_exports.object({
5595
- /** hub owner 的飞书 union_id */
5596
- hubId: external_exports.string().min(1),
5597
- /** 显示名 */
5598
- name: external_exports.string().min(1),
5599
- /** ws/tunnel 地址 */
5600
- url: external_exports.string().min(1)
5601
- });
5602
- HubListResponseSchema = external_exports.object({
5603
- type: external_exports.literal("hub:list:ok"),
5604
- hubs: external_exports.array(HubEntrySchema)
5605
- });
5606
- HubConnectArgsSchema = external_exports.object({
5607
- /** 目标 hub 的 hubId(= hub owner unionId) */
5608
- hubId: external_exports.string().min(1),
5609
- /**
5610
- * 目标 hub 的 ws/tunnel 地址(决策 #13 统一注册表后改为可选):
5611
- * - 公开目录点击:带 url(hub:list 已下发,省一次 server 查询)
5612
- * - 私有 hub 仅凭 hubId 连接:缺省 url → daemon exchange 时由中心 server 下发该 hub 上报的 url
5613
- */
5614
- url: external_exports.string().min(1).optional(),
5615
- /** 显示名(公共 hub 来自目录;私有 hub 缺省,连上后用 whoami 回填) */
5616
- name: external_exports.string().optional()
5617
- });
5618
- HubConnectResponseSchema = external_exports.object({
5619
- type: external_exports.literal("hub:connect:ok"),
5620
- hubId: external_exports.string(),
5621
- name: external_exports.string(),
5622
- url: external_exports.string()
5623
- });
5624
- FeishuIdentitySchema = external_exports.object({
5625
- /** 飞书 union_id(租户维度,跨应用稳定)。TTC user_info 接口 data.union_id */
5626
- unionId: external_exports.string().min(1),
5627
- /** 飞书姓名。TTC user_info 接口 data.name */
5628
- displayName: external_exports.string().min(1),
5629
- /** TTC user_info 接口 data.avatar_url,可缺省 */
5630
- avatarUrl: external_exports.string().optional()
5631
- });
5632
- AuthLoginStartResponseSchema = external_exports.object({
5633
- type: external_exports.literal("auth:login:start:ok"),
5634
- /** 完整 TTC 授权页 URL(含 callback_url + state + auto=1),UI 直接 window.open */
5635
- authUrl: external_exports.string().min(1),
5636
- /** 防 CSRF 的一次性 nonce;回调时 daemon 校验 */
5637
- state: external_exports.string().min(1)
5638
- });
5639
- AuthGetIdentityResponseSchema = external_exports.object({
5640
- type: external_exports.literal("auth:getIdentity:ok"),
5641
- /** null = 未登录 */
5642
- identity: FeishuIdentitySchema.nullable()
5643
- });
5644
- AuthLogoutResponseSchema = external_exports.object({
5645
- type: external_exports.literal("auth:logout:ok")
5646
- });
5647
- AuthLoginDoneEventSchema = external_exports.object({
5648
- type: external_exports.literal("auth:login:done"),
5649
- identity: FeishuIdentitySchema
5650
- });
5651
- AuthLoginFailedEventSchema = external_exports.object({
5652
- type: external_exports.literal("auth:login:failed"),
5653
- reason: external_exports.string()
5654
- });
5655
- }
5656
- });
5657
-
5658
5634
  // ../protocol/src/runtime.ts
5659
5635
  var init_runtime = __esm({
5660
5636
  "../protocol/src/runtime.ts"() {
@@ -5672,7 +5648,6 @@ var init_runtime = __esm({
5672
5648
  init_inbox();
5673
5649
  init_received_capability();
5674
5650
  init_extension();
5675
- init_feishu_auth();
5676
5651
  }
5677
5652
  });
5678
5653
 
@@ -6859,8 +6834,8 @@ var require_atomic_sleep = __commonJS({
6859
6834
  var require_sonic_boom = __commonJS({
6860
6835
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6861
6836
  "use strict";
6862
- var fs47 = require("fs");
6863
- var EventEmitter2 = require("events");
6837
+ var fs48 = require("fs");
6838
+ var EventEmitter3 = require("events");
6864
6839
  var inherits = require("util").inherits;
6865
6840
  var path59 = require("path");
6866
6841
  var sleep = require_atomic_sleep();
@@ -6916,20 +6891,20 @@ var require_sonic_boom = __commonJS({
6916
6891
  const mode = sonic.mode;
6917
6892
  if (sonic.sync) {
6918
6893
  try {
6919
- if (sonic.mkdir) fs47.mkdirSync(path59.dirname(file), { recursive: true });
6920
- const fd = fs47.openSync(file, flags, mode);
6894
+ if (sonic.mkdir) fs48.mkdirSync(path59.dirname(file), { recursive: true });
6895
+ const fd = fs48.openSync(file, flags, mode);
6921
6896
  fileOpened(null, fd);
6922
6897
  } catch (err) {
6923
6898
  fileOpened(err);
6924
6899
  throw err;
6925
6900
  }
6926
6901
  } else if (sonic.mkdir) {
6927
- fs47.mkdir(path59.dirname(file), { recursive: true }, (err) => {
6902
+ fs48.mkdir(path59.dirname(file), { recursive: true }, (err) => {
6928
6903
  if (err) return fileOpened(err);
6929
- fs47.open(file, flags, mode, fileOpened);
6904
+ fs48.open(file, flags, mode, fileOpened);
6930
6905
  });
6931
6906
  } else {
6932
- fs47.open(file, flags, mode, fileOpened);
6907
+ fs48.open(file, flags, mode, fileOpened);
6933
6908
  }
6934
6909
  }
6935
6910
  function SonicBoom(opts) {
@@ -6970,8 +6945,8 @@ var require_sonic_boom = __commonJS({
6970
6945
  this.flush = flushBuffer;
6971
6946
  this.flushSync = flushBufferSync;
6972
6947
  this._actualWrite = actualWriteBuffer;
6973
- fsWriteSync = () => fs47.writeSync(this.fd, this._writingBuf);
6974
- fsWrite = () => fs47.write(this.fd, this._writingBuf, this.release);
6948
+ fsWriteSync = () => fs48.writeSync(this.fd, this._writingBuf);
6949
+ fsWrite = () => fs48.write(this.fd, this._writingBuf, this.release);
6975
6950
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6976
6951
  this._writingBuf = "";
6977
6952
  this.write = write;
@@ -6980,15 +6955,15 @@ var require_sonic_boom = __commonJS({
6980
6955
  this._actualWrite = actualWrite;
6981
6956
  fsWriteSync = () => {
6982
6957
  if (Buffer.isBuffer(this._writingBuf)) {
6983
- return fs47.writeSync(this.fd, this._writingBuf);
6958
+ return fs48.writeSync(this.fd, this._writingBuf);
6984
6959
  }
6985
- return fs47.writeSync(this.fd, this._writingBuf, "utf8");
6960
+ return fs48.writeSync(this.fd, this._writingBuf, "utf8");
6986
6961
  };
6987
6962
  fsWrite = () => {
6988
6963
  if (Buffer.isBuffer(this._writingBuf)) {
6989
- return fs47.write(this.fd, this._writingBuf, this.release);
6964
+ return fs48.write(this.fd, this._writingBuf, this.release);
6990
6965
  }
6991
- return fs47.write(this.fd, this._writingBuf, "utf8", this.release);
6966
+ return fs48.write(this.fd, this._writingBuf, "utf8", this.release);
6992
6967
  };
6993
6968
  } else {
6994
6969
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7045,7 +7020,7 @@ var require_sonic_boom = __commonJS({
7045
7020
  }
7046
7021
  }
7047
7022
  if (this._fsync) {
7048
- fs47.fsyncSync(this.fd);
7023
+ fs48.fsyncSync(this.fd);
7049
7024
  }
7050
7025
  const len = this._len;
7051
7026
  if (this._reopening) {
@@ -7097,7 +7072,7 @@ var require_sonic_boom = __commonJS({
7097
7072
  sonic._asyncDrainScheduled = false;
7098
7073
  sonic.emit("drain");
7099
7074
  }
7100
- inherits(SonicBoom, EventEmitter2);
7075
+ inherits(SonicBoom, EventEmitter3);
7101
7076
  function mergeBuf(bufs, len) {
7102
7077
  if (bufs.length === 0) {
7103
7078
  return kEmptyBuffer;
@@ -7159,7 +7134,7 @@ var require_sonic_boom = __commonJS({
7159
7134
  const onDrain = () => {
7160
7135
  if (!this._fsync) {
7161
7136
  try {
7162
- fs47.fsync(this.fd, (err) => {
7137
+ fs48.fsync(this.fd, (err) => {
7163
7138
  this._flushPending = false;
7164
7139
  cb(err);
7165
7140
  });
@@ -7261,7 +7236,7 @@ var require_sonic_boom = __commonJS({
7261
7236
  const fd = this.fd;
7262
7237
  this.once("ready", () => {
7263
7238
  if (fd !== this.fd) {
7264
- fs47.close(fd, (err) => {
7239
+ fs48.close(fd, (err) => {
7265
7240
  if (err) {
7266
7241
  return this.emit("error", err);
7267
7242
  }
@@ -7310,7 +7285,7 @@ var require_sonic_boom = __commonJS({
7310
7285
  buf = this._bufs[0];
7311
7286
  }
7312
7287
  try {
7313
- const n = Buffer.isBuffer(buf) ? fs47.writeSync(this.fd, buf) : fs47.writeSync(this.fd, buf, "utf8");
7288
+ const n = Buffer.isBuffer(buf) ? fs48.writeSync(this.fd, buf) : fs48.writeSync(this.fd, buf, "utf8");
7314
7289
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7315
7290
  buf = releasedBufObj.writingBuf;
7316
7291
  this._len = releasedBufObj.len;
@@ -7326,7 +7301,7 @@ var require_sonic_boom = __commonJS({
7326
7301
  }
7327
7302
  }
7328
7303
  try {
7329
- fs47.fsyncSync(this.fd);
7304
+ fs48.fsyncSync(this.fd);
7330
7305
  } catch {
7331
7306
  }
7332
7307
  }
@@ -7347,7 +7322,7 @@ var require_sonic_boom = __commonJS({
7347
7322
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7348
7323
  }
7349
7324
  try {
7350
- const n = fs47.writeSync(this.fd, buf);
7325
+ const n = fs48.writeSync(this.fd, buf);
7351
7326
  buf = buf.subarray(n);
7352
7327
  this._len = Math.max(this._len - n, 0);
7353
7328
  if (buf.length <= 0) {
@@ -7375,13 +7350,13 @@ var require_sonic_boom = __commonJS({
7375
7350
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7376
7351
  if (this.sync) {
7377
7352
  try {
7378
- const written = Buffer.isBuffer(this._writingBuf) ? fs47.writeSync(this.fd, this._writingBuf) : fs47.writeSync(this.fd, this._writingBuf, "utf8");
7353
+ const written = Buffer.isBuffer(this._writingBuf) ? fs48.writeSync(this.fd, this._writingBuf) : fs48.writeSync(this.fd, this._writingBuf, "utf8");
7379
7354
  release(null, written);
7380
7355
  } catch (err) {
7381
7356
  release(err);
7382
7357
  }
7383
7358
  } else {
7384
- fs47.write(this.fd, this._writingBuf, release);
7359
+ fs48.write(this.fd, this._writingBuf, release);
7385
7360
  }
7386
7361
  }
7387
7362
  function actualWriteBuffer() {
@@ -7390,7 +7365,7 @@ var require_sonic_boom = __commonJS({
7390
7365
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7391
7366
  if (this.sync) {
7392
7367
  try {
7393
- const written = fs47.writeSync(this.fd, this._writingBuf);
7368
+ const written = fs48.writeSync(this.fd, this._writingBuf);
7394
7369
  release(null, written);
7395
7370
  } catch (err) {
7396
7371
  release(err);
@@ -7399,7 +7374,7 @@ var require_sonic_boom = __commonJS({
7399
7374
  if (kCopyBuffer) {
7400
7375
  this._writingBuf = Buffer.from(this._writingBuf);
7401
7376
  }
7402
- fs47.write(this.fd, this._writingBuf, release);
7377
+ fs48.write(this.fd, this._writingBuf, release);
7403
7378
  }
7404
7379
  }
7405
7380
  function actualClose(sonic) {
@@ -7415,12 +7390,12 @@ var require_sonic_boom = __commonJS({
7415
7390
  sonic._lens = [];
7416
7391
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7417
7392
  try {
7418
- fs47.fsync(sonic.fd, closeWrapped);
7393
+ fs48.fsync(sonic.fd, closeWrapped);
7419
7394
  } catch {
7420
7395
  }
7421
7396
  function closeWrapped() {
7422
7397
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7423
- fs47.close(sonic.fd, done);
7398
+ fs48.close(sonic.fd, done);
7424
7399
  } else {
7425
7400
  done();
7426
7401
  }
@@ -7675,9 +7650,9 @@ var require_thread_stream = __commonJS({
7675
7650
  "../node_modules/.pnpm/thread-stream@3.1.0/node_modules/thread-stream/index.js"(exports2, module2) {
7676
7651
  "use strict";
7677
7652
  var { version: version2 } = require_package();
7678
- var { EventEmitter: EventEmitter2 } = require("events");
7653
+ var { EventEmitter: EventEmitter3 } = require("events");
7679
7654
  var { Worker } = require("worker_threads");
7680
- var { join: join11 } = require("path");
7655
+ var { join: join12 } = require("path");
7681
7656
  var { pathToFileURL } = require("url");
7682
7657
  var { wait } = require_wait();
7683
7658
  var {
@@ -7713,7 +7688,7 @@ var require_thread_stream = __commonJS({
7713
7688
  function createWorker(stream, opts) {
7714
7689
  const { filename, workerData } = opts;
7715
7690
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7716
- const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
7691
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join12(__dirname, "lib", "worker.js");
7717
7692
  const worker = new Worker(toExecute, {
7718
7693
  ...opts.workerOpts,
7719
7694
  trackUnmanagedFds: false,
@@ -7831,7 +7806,7 @@ var require_thread_stream = __commonJS({
7831
7806
  stream.worker.off("exit", onWorkerExit);
7832
7807
  destroy(stream, code !== 0 ? new Error("the worker thread exited") : null);
7833
7808
  }
7834
- var ThreadStream = class extends EventEmitter2 {
7809
+ var ThreadStream = class extends EventEmitter3 {
7835
7810
  constructor(opts = {}) {
7836
7811
  super();
7837
7812
  if (opts.bufferSize < 4) {
@@ -8099,7 +8074,7 @@ var require_transport = __commonJS({
8099
8074
  "use strict";
8100
8075
  var { createRequire } = require("module");
8101
8076
  var getCallers = require_caller();
8102
- var { join: join11, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8077
+ var { join: join12, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8103
8078
  var sleep = require_atomic_sleep();
8104
8079
  var onExit = require_on_exit_leak_free();
8105
8080
  var ThreadStream = require_thread_stream();
@@ -8162,7 +8137,7 @@ var require_transport = __commonJS({
8162
8137
  throw new Error("only one of target or targets can be specified");
8163
8138
  }
8164
8139
  if (targets) {
8165
- target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8140
+ target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8166
8141
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
8167
8142
  return {
8168
8143
  ...dest,
@@ -8180,7 +8155,7 @@ var require_transport = __commonJS({
8180
8155
  });
8181
8156
  });
8182
8157
  } else if (pipeline3) {
8183
- target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8158
+ target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8184
8159
  options.pipelines = [pipeline3.map((dest) => {
8185
8160
  return {
8186
8161
  ...dest,
@@ -8202,7 +8177,7 @@ var require_transport = __commonJS({
8202
8177
  return origin;
8203
8178
  }
8204
8179
  if (origin === "pino/file") {
8205
- return join11(__dirname, "..", "file.js");
8180
+ return join12(__dirname, "..", "file.js");
8206
8181
  }
8207
8182
  let fixTarget2;
8208
8183
  for (const filePath of callers) {
@@ -8789,7 +8764,7 @@ var require_meta = __commonJS({
8789
8764
  var require_proto = __commonJS({
8790
8765
  "../node_modules/.pnpm/pino@9.14.0/node_modules/pino/lib/proto.js"(exports2, module2) {
8791
8766
  "use strict";
8792
- var { EventEmitter: EventEmitter2 } = require("events");
8767
+ var { EventEmitter: EventEmitter3 } = require("events");
8793
8768
  var {
8794
8769
  lsCacheSym,
8795
8770
  levelValSym,
@@ -8871,7 +8846,7 @@ var require_proto = __commonJS({
8871
8846
  [getLevelSym]: getLevel,
8872
8847
  [setLevelSym]: setLevel
8873
8848
  };
8874
- Object.setPrototypeOf(prototype, EventEmitter2.prototype);
8849
+ Object.setPrototypeOf(prototype, EventEmitter3.prototype);
8875
8850
  module2.exports = function() {
8876
8851
  return Object.create(prototype);
8877
8852
  };
@@ -9192,7 +9167,7 @@ var require_safe_stable_stringify = __commonJS({
9192
9167
  return circularValue;
9193
9168
  }
9194
9169
  let res = "";
9195
- let join11 = ",";
9170
+ let join12 = ",";
9196
9171
  const originalIndentation = indentation;
9197
9172
  if (Array.isArray(value)) {
9198
9173
  if (value.length === 0) {
@@ -9206,7 +9181,7 @@ var require_safe_stable_stringify = __commonJS({
9206
9181
  indentation += spacer;
9207
9182
  res += `
9208
9183
  ${indentation}`;
9209
- join11 = `,
9184
+ join12 = `,
9210
9185
  ${indentation}`;
9211
9186
  }
9212
9187
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9214,13 +9189,13 @@ ${indentation}`;
9214
9189
  for (; i < maximumValuesToStringify - 1; i++) {
9215
9190
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9216
9191
  res += tmp2 !== void 0 ? tmp2 : "null";
9217
- res += join11;
9192
+ res += join12;
9218
9193
  }
9219
9194
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9220
9195
  res += tmp !== void 0 ? tmp : "null";
9221
9196
  if (value.length - 1 > maximumBreadth) {
9222
9197
  const removedKeys = value.length - maximumBreadth - 1;
9223
- res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9198
+ res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9224
9199
  }
9225
9200
  if (spacer !== "") {
9226
9201
  res += `
@@ -9241,7 +9216,7 @@ ${originalIndentation}`;
9241
9216
  let separator = "";
9242
9217
  if (spacer !== "") {
9243
9218
  indentation += spacer;
9244
- join11 = `,
9219
+ join12 = `,
9245
9220
  ${indentation}`;
9246
9221
  whitespace = " ";
9247
9222
  }
@@ -9255,13 +9230,13 @@ ${indentation}`;
9255
9230
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
9256
9231
  if (tmp !== void 0) {
9257
9232
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9258
- separator = join11;
9233
+ separator = join12;
9259
9234
  }
9260
9235
  }
9261
9236
  if (keyLength > maximumBreadth) {
9262
9237
  const removedKeys = keyLength - maximumBreadth;
9263
9238
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
9264
- separator = join11;
9239
+ separator = join12;
9265
9240
  }
9266
9241
  if (spacer !== "" && separator.length > 1) {
9267
9242
  res = `
@@ -9302,7 +9277,7 @@ ${originalIndentation}`;
9302
9277
  }
9303
9278
  const originalIndentation = indentation;
9304
9279
  let res = "";
9305
- let join11 = ",";
9280
+ let join12 = ",";
9306
9281
  if (Array.isArray(value)) {
9307
9282
  if (value.length === 0) {
9308
9283
  return "[]";
@@ -9315,7 +9290,7 @@ ${originalIndentation}`;
9315
9290
  indentation += spacer;
9316
9291
  res += `
9317
9292
  ${indentation}`;
9318
- join11 = `,
9293
+ join12 = `,
9319
9294
  ${indentation}`;
9320
9295
  }
9321
9296
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9323,13 +9298,13 @@ ${indentation}`;
9323
9298
  for (; i < maximumValuesToStringify - 1; i++) {
9324
9299
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9325
9300
  res += tmp2 !== void 0 ? tmp2 : "null";
9326
- res += join11;
9301
+ res += join12;
9327
9302
  }
9328
9303
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9329
9304
  res += tmp !== void 0 ? tmp : "null";
9330
9305
  if (value.length - 1 > maximumBreadth) {
9331
9306
  const removedKeys = value.length - maximumBreadth - 1;
9332
- res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9307
+ res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9333
9308
  }
9334
9309
  if (spacer !== "") {
9335
9310
  res += `
@@ -9342,7 +9317,7 @@ ${originalIndentation}`;
9342
9317
  let whitespace = "";
9343
9318
  if (spacer !== "") {
9344
9319
  indentation += spacer;
9345
- join11 = `,
9320
+ join12 = `,
9346
9321
  ${indentation}`;
9347
9322
  whitespace = " ";
9348
9323
  }
@@ -9351,7 +9326,7 @@ ${indentation}`;
9351
9326
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
9352
9327
  if (tmp !== void 0) {
9353
9328
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9354
- separator = join11;
9329
+ separator = join12;
9355
9330
  }
9356
9331
  }
9357
9332
  if (spacer !== "" && separator.length > 1) {
@@ -9409,20 +9384,20 @@ ${originalIndentation}`;
9409
9384
  indentation += spacer;
9410
9385
  let res2 = `
9411
9386
  ${indentation}`;
9412
- const join12 = `,
9387
+ const join13 = `,
9413
9388
  ${indentation}`;
9414
9389
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9415
9390
  let i = 0;
9416
9391
  for (; i < maximumValuesToStringify - 1; i++) {
9417
9392
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9418
9393
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9419
- res2 += join12;
9394
+ res2 += join13;
9420
9395
  }
9421
9396
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9422
9397
  res2 += tmp !== void 0 ? tmp : "null";
9423
9398
  if (value.length - 1 > maximumBreadth) {
9424
9399
  const removedKeys = value.length - maximumBreadth - 1;
9425
- res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9400
+ res2 += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9426
9401
  }
9427
9402
  res2 += `
9428
9403
  ${originalIndentation}`;
@@ -9438,16 +9413,16 @@ ${originalIndentation}`;
9438
9413
  return '"[Object]"';
9439
9414
  }
9440
9415
  indentation += spacer;
9441
- const join11 = `,
9416
+ const join12 = `,
9442
9417
  ${indentation}`;
9443
9418
  let res = "";
9444
9419
  let separator = "";
9445
9420
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9446
9421
  if (isTypedArrayWithEntries(value)) {
9447
- res += stringifyTypedArray(value, join11, maximumBreadth);
9422
+ res += stringifyTypedArray(value, join12, maximumBreadth);
9448
9423
  keys = keys.slice(value.length);
9449
9424
  maximumPropertiesToStringify -= value.length;
9450
- separator = join11;
9425
+ separator = join12;
9451
9426
  }
9452
9427
  if (deterministic) {
9453
9428
  keys = sort(keys, comparator);
@@ -9458,13 +9433,13 @@ ${indentation}`;
9458
9433
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9459
9434
  if (tmp !== void 0) {
9460
9435
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9461
- separator = join11;
9436
+ separator = join12;
9462
9437
  }
9463
9438
  }
9464
9439
  if (keyLength > maximumBreadth) {
9465
9440
  const removedKeys = keyLength - maximumBreadth;
9466
9441
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9467
- separator = join11;
9442
+ separator = join12;
9468
9443
  }
9469
9444
  if (separator !== "") {
9470
9445
  res = `
@@ -19346,10 +19321,10 @@ var require_extension = __commonJS({
19346
19321
  var require_websocket = __commonJS({
19347
19322
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket.js"(exports2, module2) {
19348
19323
  "use strict";
19349
- var EventEmitter2 = require("events");
19324
+ var EventEmitter3 = require("events");
19350
19325
  var https = require("https");
19351
- var http2 = require("http");
19352
- var net2 = require("net");
19326
+ var http3 = require("http");
19327
+ var net3 = require("net");
19353
19328
  var tls = require("tls");
19354
19329
  var { randomBytes, createHash: createHash2 } = require("crypto");
19355
19330
  var { Duplex, Readable: Readable3 } = require("stream");
@@ -19378,7 +19353,7 @@ var require_websocket = __commonJS({
19378
19353
  var protocolVersions = [8, 13];
19379
19354
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
19380
19355
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
19381
- var WebSocket2 = class _WebSocket extends EventEmitter2 {
19356
+ var WebSocket2 = class _WebSocket extends EventEmitter3 {
19382
19357
  /**
19383
19358
  * Create a new `WebSocket`.
19384
19359
  *
@@ -19882,7 +19857,7 @@ var require_websocket = __commonJS({
19882
19857
  }
19883
19858
  const defaultPort = isSecure ? 443 : 80;
19884
19859
  const key = randomBytes(16).toString("base64");
19885
- const request = isSecure ? https.request : http2.request;
19860
+ const request = isSecure ? https.request : http3.request;
19886
19861
  const protocolSet = /* @__PURE__ */ new Set();
19887
19862
  let perMessageDeflate;
19888
19863
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -20083,12 +20058,12 @@ var require_websocket = __commonJS({
20083
20058
  }
20084
20059
  function netConnect(options) {
20085
20060
  options.path = options.socketPath;
20086
- return net2.connect(options);
20061
+ return net3.connect(options);
20087
20062
  }
20088
20063
  function tlsConnect(options) {
20089
20064
  options.path = void 0;
20090
20065
  if (!options.servername && options.servername !== "") {
20091
- options.servername = net2.isIP(options.host) ? "" : options.host;
20066
+ options.servername = net3.isIP(options.host) ? "" : options.host;
20092
20067
  }
20093
20068
  return tls.connect(options);
20094
20069
  }
@@ -20375,8 +20350,8 @@ var require_subprotocol = __commonJS({
20375
20350
  var require_websocket_server = __commonJS({
20376
20351
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
20377
20352
  "use strict";
20378
- var EventEmitter2 = require("events");
20379
- var http2 = require("http");
20353
+ var EventEmitter3 = require("events");
20354
+ var http3 = require("http");
20380
20355
  var { Duplex } = require("stream");
20381
20356
  var { createHash: createHash2 } = require("crypto");
20382
20357
  var extension2 = require_extension();
@@ -20388,7 +20363,7 @@ var require_websocket_server = __commonJS({
20388
20363
  var RUNNING = 0;
20389
20364
  var CLOSING = 1;
20390
20365
  var CLOSED = 2;
20391
- var WebSocketServer2 = class extends EventEmitter2 {
20366
+ var WebSocketServer2 = class extends EventEmitter3 {
20392
20367
  /**
20393
20368
  * Create a `WebSocketServer` instance.
20394
20369
  *
@@ -20451,8 +20426,8 @@ var require_websocket_server = __commonJS({
20451
20426
  );
20452
20427
  }
20453
20428
  if (options.port != null) {
20454
- this._server = http2.createServer((req, res) => {
20455
- const body = http2.STATUS_CODES[426];
20429
+ this._server = http3.createServer((req, res) => {
20430
+ const body = http3.STATUS_CODES[426];
20456
20431
  res.writeHead(426, {
20457
20432
  "Content-Length": body.length,
20458
20433
  "Content-Type": "text/plain"
@@ -20739,7 +20714,7 @@ var require_websocket_server = __commonJS({
20739
20714
  this.destroy();
20740
20715
  }
20741
20716
  function abortHandshake(socket, code, message, headers) {
20742
- message = message || http2.STATUS_CODES[code];
20717
+ message = message || http3.STATUS_CODES[code];
20743
20718
  headers = {
20744
20719
  Connection: "close",
20745
20720
  "Content-Type": "text/html",
@@ -20748,7 +20723,7 @@ var require_websocket_server = __commonJS({
20748
20723
  };
20749
20724
  socket.once("finish", socket.destroy);
20750
20725
  socket.end(
20751
- `HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
20726
+ `HTTP/1.1 ${code} ${http3.STATUS_CODES[code]}\r
20752
20727
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
20753
20728
  );
20754
20729
  }
@@ -21051,7 +21026,7 @@ var require_BufferList = __commonJS({
21051
21026
  this.head = this.tail = null;
21052
21027
  this.length = 0;
21053
21028
  };
21054
- BufferList.prototype.join = function join11(s) {
21029
+ BufferList.prototype.join = function join12(s) {
21055
21030
  if (this.length === 0) return "";
21056
21031
  var p2 = this.head;
21057
21032
  var ret = "" + p2.data;
@@ -30508,7 +30483,7 @@ var require_lib3 = __commonJS({
30508
30483
  // src/run-case/recorder.ts
30509
30484
  function startRunCaseRecorder(opts) {
30510
30485
  const now = opts.now ?? Date.now;
30511
- const dir = import_node_path45.default.dirname(opts.recordPath);
30486
+ const dir = import_node_path46.default.dirname(opts.recordPath);
30512
30487
  let stream = null;
30513
30488
  let closing = false;
30514
30489
  let closedSettled = false;
@@ -30522,8 +30497,8 @@ function startRunCaseRecorder(opts) {
30522
30497
  });
30523
30498
  const ensureStream = () => {
30524
30499
  if (stream) return stream;
30525
- import_node_fs30.default.mkdirSync(dir, { recursive: true });
30526
- stream = import_node_fs30.default.createWriteStream(opts.recordPath, { flags: "a" });
30500
+ import_node_fs31.default.mkdirSync(dir, { recursive: true });
30501
+ stream = import_node_fs31.default.createWriteStream(opts.recordPath, { flags: "a" });
30527
30502
  stream.on("close", () => closedResolve());
30528
30503
  return stream;
30529
30504
  };
@@ -30548,12 +30523,12 @@ function startRunCaseRecorder(opts) {
30548
30523
  };
30549
30524
  return { tap, close, closed };
30550
30525
  }
30551
- var import_node_fs30, import_node_path45;
30526
+ var import_node_fs31, import_node_path46;
30552
30527
  var init_recorder = __esm({
30553
30528
  "src/run-case/recorder.ts"() {
30554
30529
  "use strict";
30555
- import_node_fs30 = __toESM(require("fs"), 1);
30556
- import_node_path45 = __toESM(require("path"), 1);
30530
+ import_node_fs31 = __toESM(require("fs"), 1);
30531
+ import_node_path46 = __toESM(require("path"), 1);
30557
30532
  }
30558
30533
  });
30559
30534
 
@@ -30596,7 +30571,7 @@ var init_wire = __esm({
30596
30571
  // src/run-case/controller.ts
30597
30572
  async function runController(opts) {
30598
30573
  const now = opts.now ?? Date.now;
30599
- const cwd = opts.cwd ?? (0, import_node_fs31.mkdtempSync)(import_node_path46.default.join(import_node_os19.default.tmpdir(), "clawd-runcase-"));
30574
+ const cwd = opts.cwd ?? (0, import_node_fs32.mkdtempSync)(import_node_path47.default.join(import_node_os19.default.tmpdir(), "clawd-runcase-"));
30600
30575
  const ownsCwd = opts.cwd === void 0;
30601
30576
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
30602
30577
  const spawnCtx = { cwd };
@@ -30757,19 +30732,19 @@ async function runController(opts) {
30757
30732
  if (sigintHandler) process.off("SIGINT", sigintHandler);
30758
30733
  if (ownsCwd) {
30759
30734
  try {
30760
- (0, import_node_fs31.rmSync)(cwd, { recursive: true, force: true });
30735
+ (0, import_node_fs32.rmSync)(cwd, { recursive: true, force: true });
30761
30736
  } catch {
30762
30737
  }
30763
30738
  }
30764
30739
  return exitCode ?? 0;
30765
30740
  }
30766
- var import_node_fs31, import_node_os19, import_node_path46;
30741
+ var import_node_fs32, import_node_os19, import_node_path47;
30767
30742
  var init_controller = __esm({
30768
30743
  "src/run-case/controller.ts"() {
30769
30744
  "use strict";
30770
- import_node_fs31 = require("fs");
30745
+ import_node_fs32 = require("fs");
30771
30746
  import_node_os19 = __toESM(require("os"), 1);
30772
- import_node_path46 = __toESM(require("path"), 1);
30747
+ import_node_path47 = __toESM(require("path"), 1);
30773
30748
  init_claude();
30774
30749
  init_stdout_splitter();
30775
30750
  init_permission_stdio();
@@ -30961,6 +30936,8 @@ function resolveConfig(opts) {
30961
30936
  const frpcBinary = env.CLAWD_FRPC_BIN ?? null;
30962
30937
  const rawMode = env.CLAWD_CC_MODE;
30963
30938
  const mode = DAEMON_MODE_VALUES.includes(rawMode ?? "") ? rawMode : "sdk";
30939
+ const filePreviewPorts = Array.isArray(fileCfg.previewPorts) ? fileCfg.previewPorts.map((n) => Number(n)).filter((n) => Number.isInteger(n) && n > 0) : null;
30940
+ const previewPorts = env.CLAWD_PREVIEW_PORTS ? env.CLAWD_PREVIEW_PORTS.split(",").map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0) : filePreviewPorts && filePreviewPorts.length > 0 ? filePreviewPorts : null;
30964
30941
  return {
30965
30942
  port,
30966
30943
  host,
@@ -30972,7 +30949,8 @@ function resolveConfig(opts) {
30972
30949
  authToken,
30973
30950
  noTunnelPersist,
30974
30951
  frpcBinary,
30975
- mode
30952
+ mode,
30953
+ previewPorts
30976
30954
  };
30977
30955
  }
30978
30956
  var HELP_TEXT = `clawd [options]
@@ -31001,8 +30979,8 @@ Env (advanced):
31001
30979
  `;
31002
30980
 
31003
30981
  // src/index.ts
31004
- var import_node_path44 = __toESM(require("path"), 1);
31005
- var import_node_fs29 = __toESM(require("fs"), 1);
30982
+ var import_node_path45 = __toESM(require("path"), 1);
30983
+ var import_node_fs30 = __toESM(require("fs"), 1);
31006
30984
 
31007
30985
  // src/logger.ts
31008
30986
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -33011,6 +32989,7 @@ var SessionManager = class {
33011
32989
  iconKey: args.iconKey,
33012
32990
  forkedFromSessionId: args.forkedFromSessionId,
33013
32991
  ownerPersonaId: args.ownerPersonaId,
32992
+ appBuilderProject: args.appBuilderProject,
33014
32993
  creatorPrincipalId,
33015
32994
  ...creatorOwnerPrincipalId ? { creatorOwnerPrincipalId } : {},
33016
32995
  createdAt: iso,
@@ -34647,6 +34626,15 @@ var DEFAULT_PERSONAS = [
34647
34626
  model: "opus",
34648
34627
  iconKey: "assist",
34649
34628
  public: false
34629
+ },
34630
+ {
34631
+ // app-builder:全栈应用搭建师,绑 extension-kit 模板 + multi-project picker
34632
+ // spec: superpowers/specs/2026-06-01-app-builder-project-picker-design.md
34633
+ personaId: "persona-app-builder",
34634
+ label: "App Builder",
34635
+ model: "opus",
34636
+ iconKey: "assist",
34637
+ public: false
34650
34638
  }
34651
34639
  ];
34652
34640
  function findDefaultsRoot() {
@@ -36343,13 +36331,63 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
36343
36331
  var wrapper_default = import_websocket.default;
36344
36332
 
36345
36333
  // src/transport/local-ws-server.ts
36334
+ var import_node_http2 = __toESM(require("http"), 1);
36335
+
36336
+ // src/transport/preview-proxy.ts
36346
36337
  var import_node_http = __toESM(require("http"), 1);
36338
+ var import_node_net = __toESM(require("net"), 1);
36339
+ var PREVIEW_RE = /^\/preview\/(\d+)(?:\/.*)?$/;
36340
+ function matchPreviewPort(pathname) {
36341
+ const m2 = pathname.match(PREVIEW_RE);
36342
+ return m2 ? Number(m2[1]) : null;
36343
+ }
36344
+ function isPreviewPortAllowed(port, cfg) {
36345
+ return cfg.allowedPorts.includes(port);
36346
+ }
36347
+ function proxyPreviewHttp(req, res, port) {
36348
+ return new Promise((resolve6) => {
36349
+ const upstream = import_node_http.default.request(
36350
+ { host: "localhost", port, method: req.method, path: req.url, headers: req.headers },
36351
+ (upRes) => {
36352
+ res.writeHead(upRes.statusCode || 502, upRes.headers);
36353
+ upRes.pipe(res);
36354
+ upRes.on("end", () => resolve6());
36355
+ }
36356
+ );
36357
+ upstream.on("error", (e) => {
36358
+ if (!res.headersSent) {
36359
+ res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
36360
+ }
36361
+ res.end(`preview upstream error: ${e.message}`);
36362
+ resolve6();
36363
+ });
36364
+ req.pipe(upstream);
36365
+ });
36366
+ }
36367
+ function proxyPreviewUpgrade(req, socket, head, port) {
36368
+ const upstream = import_node_net.default.connect(port, "localhost", () => {
36369
+ let raw = `${req.method} ${req.url} HTTP/1.1\r
36370
+ `;
36371
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
36372
+ raw += `${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r
36373
+ `;
36374
+ }
36375
+ raw += "\r\n";
36376
+ upstream.write(raw);
36377
+ if (head && head.length) upstream.write(head);
36378
+ upstream.pipe(socket);
36379
+ socket.pipe(upstream);
36380
+ });
36381
+ upstream.on("error", () => socket.destroy());
36382
+ socket.on("error", () => upstream.destroy());
36383
+ }
36384
+
36385
+ // src/transport/local-ws-server.ts
36347
36386
  var LocalWsServer = class {
36348
36387
  constructor(opts) {
36349
36388
  this.opts = opts;
36350
36389
  this.logger = opts.logger;
36351
36390
  this.pingIntervalMs = opts.pingIntervalMs ?? 3e4;
36352
- this.currentOwnerPrincipalId = opts.ownerPrincipalId;
36353
36391
  }
36354
36392
  opts;
36355
36393
  wss = null;
@@ -36361,19 +36399,28 @@ var LocalWsServer = class {
36361
36399
  capabilityIdToClients = /* @__PURE__ */ new Map();
36362
36400
  logger;
36363
36401
  pingIntervalMs;
36364
- // 飞书热切换(spec 决策 #9,2026-06-01 拍板):登录后实时更新,broadcastToPrincipal
36365
- // 路由用最新身份;初始值来自 opts.ownerPrincipalId
36366
- currentOwnerPrincipalId;
36367
- /** 飞书热切换:登录回调成功后由 daemon wiring 调用,更新 DM 路由身份 */
36368
- setOwnerPrincipalId(id) {
36369
- this.currentOwnerPrincipalId = id;
36370
- }
36371
36402
  async start() {
36372
36403
  const host = this.opts.host ?? "127.0.0.1";
36373
36404
  await new Promise((resolve6, reject) => {
36374
- const httpServer = import_node_http.default.createServer((req, res) => this.handleHttpRequest(req, res));
36405
+ const httpServer = import_node_http2.default.createServer((req, res) => this.handleHttpRequest(req, res));
36375
36406
  const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
36376
36407
  httpServer.on("upgrade", (req, socket, head) => {
36408
+ if (req.url?.startsWith("/preview/")) {
36409
+ const pathname = (() => {
36410
+ try {
36411
+ return new URL(req.url, "http://localhost").pathname;
36412
+ } catch {
36413
+ return "/";
36414
+ }
36415
+ })();
36416
+ const port = matchPreviewPort(pathname);
36417
+ if (port != null && (this.opts.previewPorts ?? []).includes(port)) {
36418
+ proxyPreviewUpgrade(req, socket, head, port);
36419
+ } else {
36420
+ socket.destroy();
36421
+ }
36422
+ return;
36423
+ }
36377
36424
  wss.handleUpgrade(req, socket, head, (ws) => {
36378
36425
  this.routeConnection(ws, req);
36379
36426
  });
@@ -36487,7 +36534,7 @@ var LocalWsServer = class {
36487
36534
  // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
36488
36535
  // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
36489
36536
  broadcastToPrincipal(principalId, frame) {
36490
- if (principalId === "owner" || principalId === this.currentOwnerPrincipalId) {
36537
+ if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
36491
36538
  this.broadcastToOwners(frame);
36492
36539
  return;
36493
36540
  }
@@ -36539,21 +36586,6 @@ var LocalWsServer = class {
36539
36586
  getClientContext(clientId) {
36540
36587
  return this.clients.get(clientId)?.ctx ?? null;
36541
36588
  }
36542
- // 飞书热切换(spec 决策 #9):身份切换后踢掉所有连接强制重连——在线连接的 ctx
36543
- // (attachClientContext 时绑定)持有旧身份,重连后用新身份重建。
36544
- // close code 4408(非 4401!4401 = token 撤销,客户端会停止重连):
36545
- // 客户端按普通断连处理 → 自动重连 → 新身份 ctx。
36546
- closeAllClients(code = 4408, reason = "IDENTITY_SWITCHED") {
36547
- const ids = [...this.clients.keys()];
36548
- for (const id of ids) {
36549
- const c = this.clients.get(id);
36550
- if (!c) continue;
36551
- try {
36552
- c.ws.close(code, reason);
36553
- } catch {
36554
- }
36555
- }
36556
- }
36557
36589
  // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
36558
36590
  // 让客户端识别 'capability revoked' 而非普通断连。
36559
36591
  closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
@@ -36622,9 +36654,7 @@ var LocalWsServer = class {
36622
36654
  this.logger?.warn("ready frame build failed", { err: err.message });
36623
36655
  }
36624
36656
  socket.on("message", (data) => {
36625
- void this.onMessage(handle, data, remoteAddress).catch((err) => {
36626
- this.logger?.warn("onMessage failed", { err: err.message });
36627
- });
36657
+ this.onMessage(handle, data, remoteAddress);
36628
36658
  });
36629
36659
  socket.on("close", () => {
36630
36660
  const c = this.clients.get(id);
@@ -36643,7 +36673,7 @@ var LocalWsServer = class {
36643
36673
  this.logger?.warn("client socket error", { clientId: id, err: err.message });
36644
36674
  });
36645
36675
  }
36646
- async onMessage(client, data, remoteAddress) {
36676
+ onMessage(client, data, remoteAddress) {
36647
36677
  let frame;
36648
36678
  try {
36649
36679
  frame = JSON.parse(data.toString());
@@ -36654,7 +36684,7 @@ var LocalWsServer = class {
36654
36684
  const authGate = this.opts.authGate;
36655
36685
  if (authGate) {
36656
36686
  const wasAuthed = authGate.isAuthed(client.id);
36657
- const verdict = await authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36687
+ const verdict = authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36658
36688
  if (verdict !== "pass") {
36659
36689
  if (!wasAuthed && authGate.isAuthed(client.id)) {
36660
36690
  try {
@@ -36757,7 +36787,7 @@ var AuthGate = class {
36757
36787
  registerConnection(handle) {
36758
36788
  const enforce = this.opts.shouldEnforce(handle.remoteAddress);
36759
36789
  if (!enforce) {
36760
- this.states.set(handle.id, { authed: true, enforce: false, timer: null, pending: false });
36790
+ this.states.set(handle.id, { authed: true, enforce: false, timer: null });
36761
36791
  return true;
36762
36792
  }
36763
36793
  const setT = this.opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
@@ -36766,7 +36796,7 @@ var AuthGate = class {
36766
36796
  if (!st || st.authed) return;
36767
36797
  this.opts.closeConnection(handle, 1008, "auth timeout");
36768
36798
  }, this.opts.timeoutMs);
36769
- this.states.set(handle.id, { authed: false, enforce: true, timer, pending: false });
36799
+ this.states.set(handle.id, { authed: false, enforce: true, timer });
36770
36800
  return false;
36771
36801
  }
36772
36802
  unregister(handleId) {
@@ -36782,15 +36812,12 @@ var AuthGate = class {
36782
36812
  }
36783
36813
  // 收到一帧时调;返回 'consumed' / 'reject' / 'pass'
36784
36814
  // - consumed:这一帧是 auth 帧(成功或失败),调用方不要再走业务路由
36785
- // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接 / 验证进行中)
36815
+ // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接)
36786
36816
  // - pass:已 authed,调用方继续走业务路由
36787
- //
36788
- // Phase 2:async(authenticate 注入路径走中心 server introspect HTTP 调用)。
36789
- async handleFrame(handle, frame) {
36817
+ handleFrame(handle, frame) {
36790
36818
  const st = this.states.get(handle.id);
36791
36819
  if (!st) return "reject";
36792
36820
  if (st.authed) return "pass";
36793
- if (st.pending) return "reject";
36794
36821
  const parsed = AuthRequestFrameSchema.safeParse(frame);
36795
36822
  if (!parsed.success) {
36796
36823
  const reason = frame.type === "auth" ? "auth failed" : "auth required";
@@ -36800,19 +36827,11 @@ var AuthGate = class {
36800
36827
  }
36801
36828
  let ctx = null;
36802
36829
  if (this.opts.authenticate) {
36803
- st.pending = true;
36804
- let r;
36805
- try {
36806
- r = await this.opts.authenticate(
36807
- parsed.data.token,
36808
- parsed.data.selfPrincipalId,
36809
- parsed.data.selfDisplayName,
36810
- parsed.data.selfUrl
36811
- );
36812
- } finally {
36813
- st.pending = false;
36814
- }
36815
- if (!this.states.has(handle.id)) return "reject";
36830
+ const r = this.opts.authenticate(
36831
+ parsed.data.token,
36832
+ parsed.data.selfPrincipalId,
36833
+ parsed.data.selfDisplayName
36834
+ );
36816
36835
  if (!r.ok) {
36817
36836
  this.opts.closeConnection(handle, 4401, r.code);
36818
36837
  this.markFailed(handle.id);
@@ -36915,33 +36934,26 @@ function ownerContext(ownerPrincipalId, displayName) {
36915
36934
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
36916
36935
  };
36917
36936
  }
36918
- function connectGuestContext(unionId, displayName) {
36937
+ function guestContext(cap, peerOwnerPrincipalId) {
36919
36938
  return {
36920
- principal: { id: unionId, kind: "guest", displayName },
36921
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
36922
- resource: { ...g2.resource },
36923
- actions: [...g2.actions]
36924
- })),
36925
- capabilityId: unionId,
36926
- peerOwnerPrincipalId: unionId
36939
+ principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
36940
+ grants: cap.grants,
36941
+ capabilityId: cap.id,
36942
+ ...peerOwnerPrincipalId ? { peerOwnerPrincipalId } : {}
36927
36943
  };
36928
36944
  }
36929
- async function authenticate(token, deps) {
36945
+ function authenticate(token, deps) {
36930
36946
  if (!token) return { ok: false, code: "NO_TOKEN" };
36931
36947
  if (deps.isOwnerToken(token)) {
36932
36948
  return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
36933
36949
  }
36934
- if (!deps.introspectConnectToken) return { ok: false, code: "BAD_TOKEN" };
36935
- let result;
36936
- try {
36937
- result = await deps.introspectConnectToken(token);
36938
- } catch {
36939
- return { ok: false, code: "BAD_TOKEN" };
36940
- }
36941
- if (!result.active) {
36942
- return { ok: false, code: "TOKEN_REVOKED" };
36950
+ if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
36951
+ const v2 = deps.capabilityRegistry.verifyToken(token);
36952
+ if (!v2.ok) {
36953
+ if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
36954
+ return { ok: false, code: v2.code };
36943
36955
  }
36944
- return { ok: true, context: connectGuestContext(result.unionId, result.displayName) };
36956
+ return { ok: true, context: guestContext(v2.capability, deps.selfPrincipalId) };
36945
36957
  }
36946
36958
 
36947
36959
  // src/permission/capability-store.ts
@@ -37121,14 +37133,89 @@ function cleanupGuestSessionsForCapability(cap, factory) {
37121
37133
  return { removed };
37122
37134
  }
37123
37135
 
37136
+ // src/permission/personal-capability.ts
37137
+ var import_node_fs15 = __toESM(require("fs"), 1);
37138
+ var import_node_path16 = __toESM(require("path"), 1);
37139
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
37140
+ var PERSONAL_CAP_FILE_NAME = "personal-capability.json";
37141
+ var PERSONAL_CAP_DISPLAY_NAME = "personal";
37142
+ function personalCapFilePath(dataDir) {
37143
+ return import_node_path16.default.join(dataDir, PERSONAL_CAP_FILE_NAME);
37144
+ }
37145
+ function loadOrCreatePersonalCapability(opts) {
37146
+ const file = personalCapFilePath(opts.dataDir);
37147
+ const generateToken = opts.generateToken ?? defaultGenerateToken;
37148
+ const generateId = opts.generateId ?? defaultGenerateId;
37149
+ const now = opts.now ?? Date.now;
37150
+ const existing = readFile(file);
37151
+ if (existing) return existing;
37152
+ const token = generateToken();
37153
+ const id = generateId();
37154
+ const capability = {
37155
+ id,
37156
+ secretHash: sha256Hex2(token),
37157
+ displayName: PERSONAL_CAP_DISPLAY_NAME,
37158
+ // CapabilitySchema 期待 mutable Grant[],protocol 常量是 readonly,落盘前拷一份
37159
+ grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37160
+ resource: { ...g2.resource },
37161
+ actions: [...g2.actions]
37162
+ })),
37163
+ issuedAt: now(),
37164
+ usedCount: 0
37165
+ };
37166
+ const content = { token, capability };
37167
+ writeFile(file, content);
37168
+ return content;
37169
+ }
37170
+ function readFile(file) {
37171
+ let raw;
37172
+ try {
37173
+ raw = import_node_fs15.default.readFileSync(file, "utf8");
37174
+ } catch (err) {
37175
+ const code = err?.code;
37176
+ if (code === "ENOENT") return null;
37177
+ return null;
37178
+ }
37179
+ let parsed;
37180
+ try {
37181
+ parsed = JSON.parse(raw);
37182
+ } catch {
37183
+ return null;
37184
+ }
37185
+ if (!parsed || typeof parsed !== "object") return null;
37186
+ const obj = parsed;
37187
+ if (typeof obj.token !== "string" || obj.token.length === 0) return null;
37188
+ const capParsed = CapabilitySchema.safeParse(obj.capability);
37189
+ if (!capParsed.success) return null;
37190
+ if (sha256Hex2(obj.token) !== capParsed.data.secretHash) return null;
37191
+ return { token: obj.token, capability: capParsed.data };
37192
+ }
37193
+ function writeFile(file, content) {
37194
+ import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
37195
+ import_node_fs15.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
37196
+ try {
37197
+ import_node_fs15.default.chmodSync(file, 384);
37198
+ } catch {
37199
+ }
37200
+ }
37201
+ function defaultGenerateToken() {
37202
+ return import_node_crypto4.default.randomBytes(24).toString("base64url");
37203
+ }
37204
+ function defaultGenerateId() {
37205
+ return "personal_" + import_node_crypto4.default.randomBytes(6).toString("base64url");
37206
+ }
37207
+ function sha256Hex2(s) {
37208
+ return import_node_crypto4.default.createHash("sha256").update(s).digest("hex");
37209
+ }
37210
+
37124
37211
  // src/inbox/inbox-store.ts
37125
- var fs19 = __toESM(require("fs"), 1);
37126
- var path21 = __toESM(require("path"), 1);
37212
+ var fs20 = __toESM(require("fs"), 1);
37213
+ var path22 = __toESM(require("path"), 1);
37127
37214
  var INBOX_SUBDIR = "inbox";
37128
37215
  var InboxStore = class {
37129
37216
  constructor(dataDir) {
37130
37217
  this.dataDir = dataDir;
37131
- fs19.mkdirSync(this.dirPath(), { recursive: true });
37218
+ fs20.mkdirSync(this.dirPath(), { recursive: true });
37132
37219
  }
37133
37220
  dataDir;
37134
37221
  /**
@@ -37140,7 +37227,7 @@ var InboxStore = class {
37140
37227
  const file = this.filePath(peerOwnerId);
37141
37228
  let raw;
37142
37229
  try {
37143
- raw = fs19.readFileSync(file, "utf8");
37230
+ raw = fs20.readFileSync(file, "utf8");
37144
37231
  } catch (err) {
37145
37232
  if (err?.code === "ENOENT") return [];
37146
37233
  return [];
@@ -37156,7 +37243,7 @@ var InboxStore = class {
37156
37243
  const dir = this.dirPath();
37157
37244
  let entries;
37158
37245
  try {
37159
- entries = fs19.readdirSync(dir);
37246
+ entries = fs20.readdirSync(dir);
37160
37247
  } catch (err) {
37161
37248
  if (err?.code === "ENOENT") return [];
37162
37249
  return [];
@@ -37172,9 +37259,9 @@ var InboxStore = class {
37172
37259
  if (existing.some((m2) => m2.id === message.id)) return;
37173
37260
  const file = this.filePath(message.peerOwnerId);
37174
37261
  const line = JSON.stringify(message) + "\n";
37175
- fs19.appendFileSync(file, line, { mode: 384 });
37262
+ fs20.appendFileSync(file, line, { mode: 384 });
37176
37263
  try {
37177
- fs19.chmodSync(file, 384);
37264
+ fs20.chmodSync(file, 384);
37178
37265
  } catch {
37179
37266
  }
37180
37267
  }
@@ -37204,7 +37291,7 @@ var InboxStore = class {
37204
37291
  removeByPeerOwnerId(peerOwnerId) {
37205
37292
  const file = this.filePath(peerOwnerId);
37206
37293
  try {
37207
- fs19.unlinkSync(file);
37294
+ fs20.unlinkSync(file);
37208
37295
  } catch (err) {
37209
37296
  if (err?.code === "ENOENT") return;
37210
37297
  }
@@ -37213,18 +37300,18 @@ var InboxStore = class {
37213
37300
  const file = this.filePath(peerOwnerId);
37214
37301
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37215
37302
  const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
37216
- fs19.writeFileSync(tmp, content, { mode: 384 });
37217
- fs19.renameSync(tmp, file);
37303
+ fs20.writeFileSync(tmp, content, { mode: 384 });
37304
+ fs20.renameSync(tmp, file);
37218
37305
  try {
37219
- fs19.chmodSync(file, 384);
37306
+ fs20.chmodSync(file, 384);
37220
37307
  } catch {
37221
37308
  }
37222
37309
  }
37223
37310
  dirPath() {
37224
- return path21.join(this.dataDir, INBOX_SUBDIR);
37311
+ return path22.join(this.dataDir, INBOX_SUBDIR);
37225
37312
  }
37226
37313
  filePath(peerOwnerId) {
37227
- return path21.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37314
+ return path22.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37228
37315
  }
37229
37316
  };
37230
37317
  function parseAllLines(raw) {
@@ -37311,8 +37398,8 @@ var InboxManager = class {
37311
37398
  };
37312
37399
 
37313
37400
  // src/state/received-capability-store.ts
37314
- var fs20 = __toESM(require("fs"), 1);
37315
- var path22 = __toESM(require("path"), 1);
37401
+ var fs21 = __toESM(require("fs"), 1);
37402
+ var path23 = __toESM(require("path"), 1);
37316
37403
  var FILE_NAME = "received-capabilities.json";
37317
37404
  var ReceivedCapabilityStore = class {
37318
37405
  constructor(dataDir) {
@@ -37322,10 +37409,10 @@ var ReceivedCapabilityStore = class {
37322
37409
  caps = /* @__PURE__ */ new Map();
37323
37410
  load() {
37324
37411
  this.caps.clear();
37325
- const file = path22.join(this.dataDir, FILE_NAME);
37412
+ const file = path23.join(this.dataDir, FILE_NAME);
37326
37413
  let raw;
37327
37414
  try {
37328
- raw = fs20.readFileSync(file, "utf8");
37415
+ raw = fs21.readFileSync(file, "utf8");
37329
37416
  } catch (err) {
37330
37417
  if (err?.code !== "ENOENT") this.renameBak(file);
37331
37418
  return;
@@ -37362,27 +37449,27 @@ var ReceivedCapabilityStore = class {
37362
37449
  this.flush();
37363
37450
  }
37364
37451
  flush() {
37365
- const file = path22.join(this.dataDir, FILE_NAME);
37452
+ const file = path23.join(this.dataDir, FILE_NAME);
37366
37453
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
37367
37454
  const content = JSON.stringify(
37368
37455
  { receivedCaps: Array.from(this.caps.values()) },
37369
37456
  null,
37370
37457
  2
37371
37458
  );
37372
- fs20.mkdirSync(this.dataDir, { recursive: true });
37373
- fs20.writeFileSync(tmp, content, { mode: 384 });
37374
- fs20.renameSync(tmp, file);
37459
+ fs21.mkdirSync(this.dataDir, { recursive: true });
37460
+ fs21.writeFileSync(tmp, content, { mode: 384 });
37461
+ fs21.renameSync(tmp, file);
37375
37462
  }
37376
37463
  renameBak(file) {
37377
37464
  try {
37378
- fs20.renameSync(file, `${file}.bak`);
37465
+ fs21.renameSync(file, `${file}.bak`);
37379
37466
  } catch {
37380
37467
  }
37381
37468
  }
37382
37469
  };
37383
37470
 
37384
37471
  // src/received-capability/connect-remote.ts
37385
- var crypto5 = __toESM(require("crypto"), 1);
37472
+ var crypto6 = __toESM(require("crypto"), 1);
37386
37473
  var HANDSHAKE_TIMEOUT_MS = 5e3;
37387
37474
  var RPC_TIMEOUT_MS = 5e3;
37388
37475
  async function connectRemote(args) {
@@ -37400,10 +37487,7 @@ async function connectRemote(args) {
37400
37487
  type: "auth",
37401
37488
  token: args.token,
37402
37489
  selfPrincipalId: args.selfPrincipalId,
37403
- selfDisplayName: args.selfDisplayName,
37404
- // Phase 2 自动反向(spec 决策 #11):带本机可达地址时 hub 反向建联系人;
37405
- // 缺省时省略字段(不发空串),hub 端 schema selfUrl 是 .min(1).optional()
37406
- ...args.selfUrl ? { selfUrl: args.selfUrl } : {}
37490
+ selfDisplayName: args.selfDisplayName
37407
37491
  })
37408
37492
  );
37409
37493
  await new Promise((resolve6, reject) => {
@@ -37431,7 +37515,7 @@ async function connectRemote(args) {
37431
37515
  }, HANDSHAKE_TIMEOUT_MS);
37432
37516
  });
37433
37517
  function call(method, payload) {
37434
- const requestId = crypto5.randomUUID();
37518
+ const requestId = crypto6.randomUUID();
37435
37519
  return new Promise((resolve6, reject) => {
37436
37520
  const onMessage = (raw) => {
37437
37521
  let f;
@@ -37475,85 +37559,62 @@ async function connectRemote(args) {
37475
37559
  };
37476
37560
  }
37477
37561
 
37478
- // src/received-capability/auto-reverse.ts
37479
- init_protocol();
37480
- function autoReverseContact(args) {
37481
- if (!args.selfUrl) return;
37482
- const now = args.now ?? Date.now;
37483
- const rc = {
37484
- ownerPrincipalId: args.unionId,
37485
- ownerDisplayName: args.displayName,
37486
- remoteUrl: args.selfUrl,
37487
- // capabilityId 复用 unionId(与 connectGuestContext 的 guest channel 索引键一致)
37488
- capabilityId: args.unionId,
37489
- // B 不持有 A 颁的 token:空串。schema 已放宽允许空(不造假数据)。
37490
- connectToken: "",
37491
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37492
- resource: { ...g2.resource },
37493
- actions: [...g2.actions]
37494
- })),
37495
- receivedAt: now()
37496
- };
37497
- args.store.upsert(rc);
37498
- args.broadcast({ type: "received-capability:added", receivedCap: rc });
37499
- }
37500
-
37501
37562
  // src/migrations/2026-05-20-flatten-sessions.ts
37502
- var fs21 = __toESM(require("fs"), 1);
37503
- var path23 = __toESM(require("path"), 1);
37563
+ var fs22 = __toESM(require("fs"), 1);
37564
+ var path24 = __toESM(require("path"), 1);
37504
37565
  var MIGRATION_FLAG_NAME = ".migration.v1.done";
37505
37566
  function migrateFlattenSessions(opts) {
37506
37567
  const dataDir = opts.dataDir;
37507
37568
  const now = opts.now ?? Date.now;
37508
- const sessionsDir = path23.join(dataDir, "sessions");
37509
- const flagPath = path23.join(sessionsDir, MIGRATION_FLAG_NAME);
37569
+ const sessionsDir = path24.join(dataDir, "sessions");
37570
+ const flagPath = path24.join(sessionsDir, MIGRATION_FLAG_NAME);
37510
37571
  if (existsSync3(flagPath)) {
37511
37572
  return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
37512
37573
  }
37513
37574
  let movedBare = 0;
37514
37575
  let movedVmOwner = 0;
37515
37576
  let archivedListener = 0;
37516
- const defaultDir = path23.join(sessionsDir, "default");
37577
+ const defaultDir = path24.join(sessionsDir, "default");
37517
37578
  if (existsSync3(defaultDir)) {
37518
37579
  for (const entry of readdirSafe(defaultDir)) {
37519
37580
  if (!entry.endsWith(".json")) continue;
37520
- const src = path23.join(defaultDir, entry);
37521
- const dst = path23.join(sessionsDir, entry);
37522
- fs21.renameSync(src, dst);
37581
+ const src = path24.join(defaultDir, entry);
37582
+ const dst = path24.join(sessionsDir, entry);
37583
+ fs22.renameSync(src, dst);
37523
37584
  movedBare += 1;
37524
37585
  }
37525
37586
  rmdirIfEmpty(defaultDir);
37526
37587
  }
37527
37588
  for (const pid of readdirSafe(sessionsDir)) {
37528
- const personaDir = path23.join(sessionsDir, pid);
37589
+ const personaDir = path24.join(sessionsDir, pid);
37529
37590
  if (!isDir(personaDir)) continue;
37530
37591
  if (pid === "default") continue;
37531
- const ownerSrc = path23.join(personaDir, "owner");
37592
+ const ownerSrc = path24.join(personaDir, "owner");
37532
37593
  if (existsSync3(ownerSrc) && isDir(ownerSrc)) {
37533
- const ownerDst = path23.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37534
- fs21.mkdirSync(ownerDst, { recursive: true });
37594
+ const ownerDst = path24.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37595
+ fs22.mkdirSync(ownerDst, { recursive: true });
37535
37596
  for (const file of readdirSafe(ownerSrc)) {
37536
37597
  if (!file.endsWith(".json")) continue;
37537
- fs21.renameSync(path23.join(ownerSrc, file), path23.join(ownerDst, file));
37598
+ fs22.renameSync(path24.join(ownerSrc, file), path24.join(ownerDst, file));
37538
37599
  movedVmOwner += 1;
37539
37600
  }
37540
37601
  rmdirIfEmpty(ownerSrc);
37541
37602
  }
37542
- const listenerSrc = path23.join(personaDir, "listener");
37603
+ const listenerSrc = path24.join(personaDir, "listener");
37543
37604
  if (existsSync3(listenerSrc) && isDir(listenerSrc)) {
37544
- const archiveDst = path23.join(dataDir, ".legacy", `listener-${pid}`);
37545
- fs21.mkdirSync(archiveDst, { recursive: true });
37605
+ const archiveDst = path24.join(dataDir, ".legacy", `listener-${pid}`);
37606
+ fs22.mkdirSync(archiveDst, { recursive: true });
37546
37607
  for (const file of readdirSafe(listenerSrc)) {
37547
37608
  if (!file.endsWith(".json")) continue;
37548
- fs21.renameSync(path23.join(listenerSrc, file), path23.join(archiveDst, file));
37609
+ fs22.renameSync(path24.join(listenerSrc, file), path24.join(archiveDst, file));
37549
37610
  archivedListener += 1;
37550
37611
  }
37551
37612
  rmdirIfEmpty(listenerSrc);
37552
37613
  }
37553
37614
  rmdirIfEmpty(personaDir);
37554
37615
  }
37555
- fs21.mkdirSync(sessionsDir, { recursive: true });
37556
- fs21.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37616
+ fs22.mkdirSync(sessionsDir, { recursive: true });
37617
+ fs22.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37557
37618
  return {
37558
37619
  skipped: false,
37559
37620
  flagWritten: true,
@@ -37564,7 +37625,7 @@ function migrateFlattenSessions(opts) {
37564
37625
  }
37565
37626
  function existsSync3(p2) {
37566
37627
  try {
37567
- fs21.statSync(p2);
37628
+ fs22.statSync(p2);
37568
37629
  return true;
37569
37630
  } catch {
37570
37631
  return false;
@@ -37572,31 +37633,31 @@ function existsSync3(p2) {
37572
37633
  }
37573
37634
  function isDir(p2) {
37574
37635
  try {
37575
- return fs21.statSync(p2).isDirectory();
37636
+ return fs22.statSync(p2).isDirectory();
37576
37637
  } catch {
37577
37638
  return false;
37578
37639
  }
37579
37640
  }
37580
37641
  function readdirSafe(p2) {
37581
37642
  try {
37582
- return fs21.readdirSync(p2);
37643
+ return fs22.readdirSync(p2);
37583
37644
  } catch {
37584
37645
  return [];
37585
37646
  }
37586
37647
  }
37587
37648
  function rmdirIfEmpty(p2) {
37588
37649
  try {
37589
- fs21.rmdirSync(p2);
37650
+ fs22.rmdirSync(p2);
37590
37651
  } catch {
37591
37652
  }
37592
37653
  }
37593
37654
 
37594
37655
  // src/transport/http-router.ts
37595
- var import_node_fs16 = __toESM(require("fs"), 1);
37596
- var import_node_path19 = __toESM(require("path"), 1);
37656
+ var import_node_fs17 = __toESM(require("fs"), 1);
37657
+ var import_node_path20 = __toESM(require("path"), 1);
37597
37658
 
37598
37659
  // src/attachment/mime.ts
37599
- var import_node_path16 = __toESM(require("path"), 1);
37660
+ var import_node_path17 = __toESM(require("path"), 1);
37600
37661
  var TEXT_PLAIN = "text/plain; charset=utf-8";
37601
37662
  var EXT_TO_NATIVE_MIME = {
37602
37663
  // 图片
@@ -37703,14 +37764,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
37703
37764
  ".mk"
37704
37765
  ]);
37705
37766
  function lookupMime(filePathOrName) {
37706
- const ext = import_node_path16.default.extname(filePathOrName).toLowerCase();
37767
+ const ext = import_node_path17.default.extname(filePathOrName).toLowerCase();
37707
37768
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
37708
37769
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
37709
37770
  return "application/octet-stream";
37710
37771
  }
37711
37772
 
37712
37773
  // src/attachment/sign-url.ts
37713
- var import_node_crypto4 = __toESM(require("crypto"), 1);
37774
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
37714
37775
  var HMAC_ALGO = "sha256";
37715
37776
  function base64urlEncode(buf) {
37716
37777
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -37727,7 +37788,7 @@ function decodeAbsPathFromUrl(encoded) {
37727
37788
  }
37728
37789
  function computeSig(secret, absPath, e) {
37729
37790
  const msg = e === null ? absPath : `${absPath}|${e}`;
37730
- return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37791
+ return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37731
37792
  }
37732
37793
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
37733
37794
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -37762,7 +37823,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37762
37823
  if (provided.length !== expected.length) {
37763
37824
  return { ok: false, code: "BAD_SIG" };
37764
37825
  }
37765
- if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
37826
+ if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
37766
37827
  return { ok: false, code: "BAD_SIG" };
37767
37828
  }
37768
37829
  if (e !== null && now() / 1e3 > e) {
@@ -37772,9 +37833,9 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37772
37833
  }
37773
37834
 
37774
37835
  // src/attachment/upload.ts
37775
- var import_node_fs15 = __toESM(require("fs"), 1);
37776
- var import_node_path17 = __toESM(require("path"), 1);
37777
- var import_node_crypto5 = __toESM(require("crypto"), 1);
37836
+ var import_node_fs16 = __toESM(require("fs"), 1);
37837
+ var import_node_path18 = __toESM(require("path"), 1);
37838
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
37778
37839
  var import_promises = require("stream/promises");
37779
37840
  var UploadError = class extends Error {
37780
37841
  constructor(code, message) {
@@ -37785,24 +37846,24 @@ var UploadError = class extends Error {
37785
37846
  code;
37786
37847
  };
37787
37848
  function assertValidFileName(fileName) {
37788
- if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path17.default.basename(fileName)) {
37849
+ if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path18.default.basename(fileName)) {
37789
37850
  throw new UploadError("INVALID_FILENAME", `fileName must be a plain basename, got: ${fileName}`);
37790
37851
  }
37791
37852
  }
37792
37853
  var HASH_PREFIX_LEN = 16;
37793
37854
  async function writeUploadedAttachment(args) {
37794
37855
  assertValidFileName(args.fileName);
37795
- const attachmentsRoot = import_node_path17.default.join(args.sessionDir, ".attachments");
37856
+ const attachmentsRoot = import_node_path18.default.join(args.sessionDir, ".attachments");
37796
37857
  try {
37797
- import_node_fs15.default.mkdirSync(attachmentsRoot, { recursive: true });
37858
+ import_node_fs16.default.mkdirSync(attachmentsRoot, { recursive: true });
37798
37859
  } catch (err) {
37799
37860
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
37800
37861
  }
37801
- const hasher = import_node_crypto5.default.createHash("sha256");
37862
+ const hasher = import_node_crypto6.default.createHash("sha256");
37802
37863
  let actualSize = 0;
37803
- const tmpPath = import_node_path17.default.join(
37864
+ const tmpPath = import_node_path18.default.join(
37804
37865
  attachmentsRoot,
37805
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto5.default.randomBytes(4).toString("hex")}`
37866
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto6.default.randomBytes(4).toString("hex")}`
37806
37867
  );
37807
37868
  try {
37808
37869
  await (0, import_promises.pipeline)(
@@ -37815,18 +37876,18 @@ async function writeUploadedAttachment(args) {
37815
37876
  yield buf;
37816
37877
  }
37817
37878
  },
37818
- import_node_fs15.default.createWriteStream(tmpPath, { mode: 384 })
37879
+ import_node_fs16.default.createWriteStream(tmpPath, { mode: 384 })
37819
37880
  );
37820
37881
  } catch (err) {
37821
37882
  try {
37822
- import_node_fs15.default.unlinkSync(tmpPath);
37883
+ import_node_fs16.default.unlinkSync(tmpPath);
37823
37884
  } catch {
37824
37885
  }
37825
37886
  throw new UploadError("STORAGE_ERROR", `write failed: ${err.message}`);
37826
37887
  }
37827
37888
  if (actualSize !== args.contentLength) {
37828
37889
  try {
37829
- import_node_fs15.default.unlinkSync(tmpPath);
37890
+ import_node_fs16.default.unlinkSync(tmpPath);
37830
37891
  } catch {
37831
37892
  }
37832
37893
  throw new UploadError(
@@ -37835,35 +37896,35 @@ async function writeUploadedAttachment(args) {
37835
37896
  );
37836
37897
  }
37837
37898
  const attachmentId = hasher.digest("hex").slice(0, HASH_PREFIX_LEN);
37838
- const hashDir = import_node_path17.default.join(attachmentsRoot, attachmentId);
37899
+ const hashDir = import_node_path18.default.join(attachmentsRoot, attachmentId);
37839
37900
  let finalFileName;
37840
37901
  let hashDirExists = false;
37841
37902
  try {
37842
- hashDirExists = import_node_fs15.default.statSync(hashDir).isDirectory();
37903
+ hashDirExists = import_node_fs16.default.statSync(hashDir).isDirectory();
37843
37904
  } catch {
37844
37905
  }
37845
37906
  if (hashDirExists) {
37846
- const existing = import_node_fs15.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
37907
+ const existing = import_node_fs16.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
37847
37908
  finalFileName = existing[0] ?? args.fileName;
37848
37909
  try {
37849
- import_node_fs15.default.unlinkSync(tmpPath);
37910
+ import_node_fs16.default.unlinkSync(tmpPath);
37850
37911
  } catch {
37851
37912
  }
37852
37913
  } else {
37853
37914
  try {
37854
- import_node_fs15.default.mkdirSync(hashDir, { recursive: true });
37915
+ import_node_fs16.default.mkdirSync(hashDir, { recursive: true });
37855
37916
  finalFileName = args.fileName;
37856
- import_node_fs15.default.renameSync(tmpPath, import_node_path17.default.join(hashDir, finalFileName));
37917
+ import_node_fs16.default.renameSync(tmpPath, import_node_path18.default.join(hashDir, finalFileName));
37857
37918
  } catch (err) {
37858
37919
  try {
37859
- import_node_fs15.default.unlinkSync(tmpPath);
37920
+ import_node_fs16.default.unlinkSync(tmpPath);
37860
37921
  } catch {
37861
37922
  }
37862
37923
  throw new UploadError("STORAGE_ERROR", `rename failed: ${err.message}`);
37863
37924
  }
37864
37925
  }
37865
- const absPath = import_node_path17.default.join(hashDir, finalFileName);
37866
- const relPath = import_node_path17.default.relative(args.sessionDir, absPath);
37926
+ const absPath = import_node_path18.default.join(hashDir, finalFileName);
37927
+ const relPath = import_node_path18.default.relative(args.sessionDir, absPath);
37867
37928
  args.groupFileStore.upsert(args.scope, args.sessionId, {
37868
37929
  relPath: absPath,
37869
37930
  // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
@@ -37877,7 +37938,7 @@ async function writeUploadedAttachment(args) {
37877
37938
 
37878
37939
  // src/extension/import.ts
37879
37940
  var import_promises2 = __toESM(require("fs/promises"), 1);
37880
- var import_node_path18 = __toESM(require("path"), 1);
37941
+ var import_node_path19 = __toESM(require("path"), 1);
37881
37942
  var import_node_os10 = __toESM(require("os"), 1);
37882
37943
  var import_jszip = __toESM(require_lib3(), 1);
37883
37944
  var ImportError = class extends Error {
@@ -37895,7 +37956,7 @@ async function importZip(buf, root) {
37895
37956
  throw new ImportError("ZIP_INVALID", `failed to load zip: ${e.message}`);
37896
37957
  }
37897
37958
  for (const name of Object.keys(zip.files)) {
37898
- if (name.includes("..") || name.startsWith("/") || import_node_path18.default.isAbsolute(name)) {
37959
+ if (name.includes("..") || name.startsWith("/") || import_node_path19.default.isAbsolute(name)) {
37899
37960
  throw new ImportError("ZIP_INVALID", `unsafe zip entry path: ${name}`);
37900
37961
  }
37901
37962
  }
@@ -37928,7 +37989,7 @@ async function importZip(buf, root) {
37928
37989
  );
37929
37990
  }
37930
37991
  }
37931
- const destDir = import_node_path18.default.join(root, manifest.id);
37992
+ const destDir = import_node_path19.default.join(root, manifest.id);
37932
37993
  let destExists = false;
37933
37994
  try {
37934
37995
  await import_promises2.default.access(destDir);
@@ -37938,15 +37999,15 @@ async function importZip(buf, root) {
37938
37999
  if (destExists) {
37939
38000
  throw new ImportError("ALREADY_EXISTS", `extension ${manifest.id} already installed`);
37940
38001
  }
37941
- const stage = await import_promises2.default.mkdtemp(import_node_path18.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38002
+ const stage = await import_promises2.default.mkdtemp(import_node_path19.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
37942
38003
  try {
37943
38004
  for (const [name, entry] of Object.entries(zip.files)) {
37944
- const dest = import_node_path18.default.join(stage, name);
38005
+ const dest = import_node_path19.default.join(stage, name);
37945
38006
  if (entry.dir) {
37946
38007
  await import_promises2.default.mkdir(dest, { recursive: true });
37947
38008
  continue;
37948
38009
  }
37949
- await import_promises2.default.mkdir(import_node_path18.default.dirname(dest), { recursive: true });
38010
+ await import_promises2.default.mkdir(import_node_path19.default.dirname(dest), { recursive: true });
37950
38011
  const content = await entry.async("nodebuffer");
37951
38012
  await import_promises2.default.writeFile(dest, content);
37952
38013
  }
@@ -37992,7 +38053,7 @@ function isValidUploadFileName(fileName) {
37992
38053
  if (fileName === "." || fileName === "..") return false;
37993
38054
  if (fileName.startsWith(".")) return false;
37994
38055
  if (fileName.includes("/") || fileName.includes("\\")) return false;
37995
- return fileName === import_node_path19.default.basename(fileName);
38056
+ return fileName === import_node_path20.default.basename(fileName);
37996
38057
  }
37997
38058
  function createHttpRouter(deps) {
37998
38059
  return async (req, res) => {
@@ -38007,23 +38068,13 @@ function createHttpRouter(deps) {
38007
38068
  });
38008
38069
  return true;
38009
38070
  }
38010
- if (url.pathname === "/auth/callback") {
38011
- if (req.method !== "GET") {
38012
- sendJson(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
38013
- return true;
38014
- }
38015
- if (!deps.feishuLogin) {
38016
- sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "feishu login not wired" });
38071
+ if (url.pathname.startsWith("/preview/")) {
38072
+ const port = matchPreviewPort(url.pathname);
38073
+ if (port != null && isPreviewPortAllowed(port, { allowedPorts: deps.previewPorts ?? [] })) {
38074
+ await proxyPreviewHttp(req, res, port);
38017
38075
  return true;
38018
38076
  }
38019
- const result = await deps.feishuLogin.handleLoginCallback({
38020
- token: url.searchParams.get("token") ?? "",
38021
- state: url.searchParams.get("state") ?? "",
38022
- expiresAt: url.searchParams.get("expires_at")
38023
- });
38024
- const html = result.ok ? `<!doctype html><meta charset="utf-8"><title>clawd</title><style>body{font-family:-apple-system,sans-serif;padding:48px;color:#222}</style><h2>\u767B\u5F55\u6210\u529F\uFF0C\u53EF\u5173\u95ED\u6B64\u9875\u3002</h2><p>\u8EAB\u4EFD\uFF1A${escapeHtml(result.identity.displayName)}</p><script>setTimeout(()=>window.close(),1500)</script>` : `<!doctype html><meta charset="utf-8"><title>clawd</title><style>body{font-family:-apple-system,sans-serif;padding:48px;color:#922}</style><h2>\u767B\u5F55\u5931\u8D25</h2><p>${escapeHtml(result.reason)}</p>`;
38025
- res.writeHead(result.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
38026
- res.end(html);
38077
+ sendJson(res, 403, { code: "PREVIEW_PORT_FORBIDDEN", message: "preview port not allowed" });
38027
38078
  return true;
38028
38079
  }
38029
38080
  if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/") && url.pathname !== "/attachments/upload" && url.pathname !== "/extensions/import") {
@@ -38186,7 +38237,7 @@ function createHttpRouter(deps) {
38186
38237
  return true;
38187
38238
  }
38188
38239
  let absPath;
38189
- if (import_node_path19.default.isAbsolute(pathParam)) {
38240
+ if (import_node_path20.default.isAbsolute(pathParam)) {
38190
38241
  absPath = pathParam;
38191
38242
  } else if (deps.sessionStore) {
38192
38243
  const file = deps.sessionStore.read(sid);
@@ -38194,7 +38245,7 @@ function createHttpRouter(deps) {
38194
38245
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
38195
38246
  return true;
38196
38247
  }
38197
- absPath = import_node_path19.default.join(file.cwd, pathParam);
38248
+ absPath = import_node_path20.default.join(file.cwd, pathParam);
38198
38249
  } else {
38199
38250
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
38200
38251
  return true;
@@ -38261,9 +38312,6 @@ function parseUrl(rawUrl) {
38261
38312
  return null;
38262
38313
  }
38263
38314
  }
38264
- function escapeHtml(s) {
38265
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
38266
- }
38267
38315
  function sendJson(res, status, body, extraHeaders) {
38268
38316
  res.writeHead(status, {
38269
38317
  "Content-Type": "application/json; charset=utf-8",
@@ -38277,7 +38325,7 @@ function withCtx(ctx, body) {
38277
38325
  function streamFile(res, absPath, logger) {
38278
38326
  let stat;
38279
38327
  try {
38280
- stat = import_node_fs16.default.statSync(absPath);
38328
+ stat = import_node_fs17.default.statSync(absPath);
38281
38329
  } catch (err) {
38282
38330
  const code = err?.code;
38283
38331
  if (code === "ENOENT") {
@@ -38292,7 +38340,7 @@ function streamFile(res, absPath, logger) {
38292
38340
  return;
38293
38341
  }
38294
38342
  const mime = lookupMime(absPath);
38295
- const basename = import_node_path19.default.basename(absPath);
38343
+ const basename = import_node_path20.default.basename(absPath);
38296
38344
  res.writeHead(200, {
38297
38345
  "Content-Type": mime,
38298
38346
  "Content-Length": String(stat.size),
@@ -38300,7 +38348,7 @@ function streamFile(res, absPath, logger) {
38300
38348
  // 防止浏览器把任意 mime 当 html 渲染
38301
38349
  "X-Content-Type-Options": "nosniff"
38302
38350
  });
38303
- const stream = import_node_fs16.default.createReadStream(absPath);
38351
+ const stream = import_node_fs17.default.createReadStream(absPath);
38304
38352
  stream.on("error", (err) => {
38305
38353
  logger?.warn("streamFile read error", { absPath, err: err.message });
38306
38354
  res.destroy();
@@ -38309,8 +38357,8 @@ function streamFile(res, absPath, logger) {
38309
38357
  }
38310
38358
 
38311
38359
  // src/attachment/gc.ts
38312
- var import_node_fs17 = __toESM(require("fs"), 1);
38313
- var import_node_path20 = __toESM(require("path"), 1);
38360
+ var import_node_fs18 = __toESM(require("fs"), 1);
38361
+ var import_node_path21 = __toESM(require("path"), 1);
38314
38362
  var DEFAULT_TTL_MS = 30 * 24 * 3600 * 1e3;
38315
38363
  function runAttachmentGc(args) {
38316
38364
  const now = (args.now ?? Date.now)();
@@ -38319,38 +38367,38 @@ function runAttachmentGc(args) {
38319
38367
  for (const { scope, sessionId } of args.sessionScopes) {
38320
38368
  for (const entry of args.groupFileStore.list(scope, sessionId)) {
38321
38369
  if (entry.stale) continue;
38322
- if (import_node_path20.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38370
+ if (import_node_path21.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38323
38371
  }
38324
38372
  }
38325
38373
  for (const { scope, sessionId } of args.sessionScopes) {
38326
- const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path20.default.join(
38374
+ const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path21.default.join(
38327
38375
  args.dataDir,
38328
38376
  "sessions",
38329
38377
  ...scopeSubPath(scope).map(safeFileName),
38330
38378
  safeFileName(sessionId)
38331
38379
  );
38332
- const attRoot = import_node_path20.default.join(sessionDir, ".attachments");
38380
+ const attRoot = import_node_path21.default.join(sessionDir, ".attachments");
38333
38381
  let hashDirs;
38334
38382
  try {
38335
- hashDirs = import_node_fs17.default.readdirSync(attRoot);
38383
+ hashDirs = import_node_fs18.default.readdirSync(attRoot);
38336
38384
  } catch (err) {
38337
38385
  if (err.code === "ENOENT") continue;
38338
38386
  args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
38339
38387
  continue;
38340
38388
  }
38341
38389
  for (const hashDir of hashDirs) {
38342
- const hashDirAbs = import_node_path20.default.join(attRoot, hashDir);
38390
+ const hashDirAbs = import_node_path21.default.join(attRoot, hashDir);
38343
38391
  let files;
38344
38392
  try {
38345
- files = import_node_fs17.default.readdirSync(hashDirAbs);
38393
+ files = import_node_fs18.default.readdirSync(hashDirAbs);
38346
38394
  } catch {
38347
38395
  continue;
38348
38396
  }
38349
38397
  for (const name of files) {
38350
- const file = import_node_path20.default.join(hashDirAbs, name);
38398
+ const file = import_node_path21.default.join(hashDirAbs, name);
38351
38399
  let stat;
38352
38400
  try {
38353
- stat = import_node_fs17.default.statSync(file);
38401
+ stat = import_node_fs18.default.statSync(file);
38354
38402
  } catch {
38355
38403
  continue;
38356
38404
  }
@@ -38359,27 +38407,27 @@ function runAttachmentGc(args) {
38359
38407
  if (age < ttlMs) continue;
38360
38408
  if (liveAbs.has(file)) continue;
38361
38409
  try {
38362
- import_node_fs17.default.unlinkSync(file);
38410
+ import_node_fs18.default.unlinkSync(file);
38363
38411
  } catch (err) {
38364
38412
  args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
38365
38413
  }
38366
38414
  }
38367
38415
  try {
38368
- if (import_node_fs17.default.readdirSync(hashDirAbs).length === 0) import_node_fs17.default.rmdirSync(hashDirAbs);
38416
+ if (import_node_fs18.default.readdirSync(hashDirAbs).length === 0) import_node_fs18.default.rmdirSync(hashDirAbs);
38369
38417
  } catch {
38370
38418
  }
38371
38419
  }
38372
38420
  try {
38373
- if (import_node_fs17.default.readdirSync(attRoot).length === 0) import_node_fs17.default.rmdirSync(attRoot);
38421
+ if (import_node_fs18.default.readdirSync(attRoot).length === 0) import_node_fs18.default.rmdirSync(attRoot);
38374
38422
  } catch {
38375
38423
  }
38376
38424
  }
38377
38425
  }
38378
38426
 
38379
38427
  // src/attachment/group.ts
38380
- var import_node_fs18 = __toESM(require("fs"), 1);
38381
- var import_node_path21 = __toESM(require("path"), 1);
38382
- var import_node_crypto6 = __toESM(require("crypto"), 1);
38428
+ var import_node_fs19 = __toESM(require("fs"), 1);
38429
+ var import_node_path22 = __toESM(require("path"), 1);
38430
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
38383
38431
  init_protocol();
38384
38432
  var GroupFileStore = class {
38385
38433
  dataDir;
@@ -38390,11 +38438,11 @@ var GroupFileStore = class {
38390
38438
  this.logger = opts.logger;
38391
38439
  }
38392
38440
  rootForScope(scope) {
38393
- return import_node_path21.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38441
+ return import_node_path22.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38394
38442
  }
38395
38443
  /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
38396
38444
  filePath(scope, sessionId) {
38397
- return import_node_path21.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38445
+ return import_node_path22.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38398
38446
  }
38399
38447
  cacheKey(scope, sessionId) {
38400
38448
  return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
@@ -38403,7 +38451,7 @@ var GroupFileStore = class {
38403
38451
  readFile(scope, sessionId) {
38404
38452
  const file = this.filePath(scope, sessionId);
38405
38453
  try {
38406
- const raw = import_node_fs18.default.readFileSync(file, "utf8");
38454
+ const raw = import_node_fs19.default.readFileSync(file, "utf8");
38407
38455
  const parsed = JSON.parse(raw);
38408
38456
  if (!Array.isArray(parsed)) {
38409
38457
  this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
@@ -38429,10 +38477,10 @@ var GroupFileStore = class {
38429
38477
  }
38430
38478
  writeFile(scope, sessionId, entries) {
38431
38479
  const file = this.filePath(scope, sessionId);
38432
- import_node_fs18.default.mkdirSync(import_node_path21.default.dirname(file), { recursive: true });
38480
+ import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true });
38433
38481
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
38434
- import_node_fs18.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38435
- import_node_fs18.default.renameSync(tmp, file);
38482
+ import_node_fs19.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38483
+ import_node_fs19.default.renameSync(tmp, file);
38436
38484
  }
38437
38485
  /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
38438
38486
  list(scope, sessionId) {
@@ -38468,7 +38516,7 @@ var GroupFileStore = class {
38468
38516
  entries[idx] = next;
38469
38517
  } else {
38470
38518
  next = {
38471
- id: `gf-${import_node_crypto6.default.randomBytes(6).toString("base64url")}`,
38519
+ id: `gf-${import_node_crypto7.default.randomBytes(6).toString("base64url")}`,
38472
38520
  relPath: input.relPath,
38473
38521
  from: input.from,
38474
38522
  label: input.label,
@@ -38518,10 +38566,10 @@ var GroupFileStore = class {
38518
38566
  };
38519
38567
 
38520
38568
  // src/discovery/state-file.ts
38521
- var import_node_fs19 = __toESM(require("fs"), 1);
38522
- var import_node_path22 = __toESM(require("path"), 1);
38569
+ var import_node_fs20 = __toESM(require("fs"), 1);
38570
+ var import_node_path23 = __toESM(require("path"), 1);
38523
38571
  function defaultStateFilePath(dataDir) {
38524
- return import_node_path22.default.join(dataDir, "state.json");
38572
+ return import_node_path23.default.join(dataDir, "state.json");
38525
38573
  }
38526
38574
  function isPidAlive(pid) {
38527
38575
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -38543,7 +38591,7 @@ var StateFileManager = class {
38543
38591
  }
38544
38592
  read() {
38545
38593
  try {
38546
- const raw = import_node_fs19.default.readFileSync(this.file, "utf8");
38594
+ const raw = import_node_fs20.default.readFileSync(this.file, "utf8");
38547
38595
  const parsed = JSON.parse(raw);
38548
38596
  return parsed;
38549
38597
  } catch {
@@ -38557,34 +38605,34 @@ var StateFileManager = class {
38557
38605
  return { status: "stale", existing };
38558
38606
  }
38559
38607
  write(state) {
38560
- import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(this.file), { recursive: true });
38608
+ import_node_fs20.default.mkdirSync(import_node_path23.default.dirname(this.file), { recursive: true });
38561
38609
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
38562
- import_node_fs19.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38563
- import_node_fs19.default.renameSync(tmp, this.file);
38610
+ import_node_fs20.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38611
+ import_node_fs20.default.renameSync(tmp, this.file);
38564
38612
  if (process.platform !== "win32") {
38565
38613
  try {
38566
- import_node_fs19.default.chmodSync(this.file, 384);
38614
+ import_node_fs20.default.chmodSync(this.file, 384);
38567
38615
  } catch {
38568
38616
  }
38569
38617
  }
38570
38618
  }
38571
38619
  delete() {
38572
38620
  try {
38573
- import_node_fs19.default.unlinkSync(this.file);
38621
+ import_node_fs20.default.unlinkSync(this.file);
38574
38622
  } catch {
38575
38623
  }
38576
38624
  }
38577
38625
  };
38578
38626
 
38579
38627
  // src/tunnel/tunnel-manager.ts
38580
- var import_node_fs23 = __toESM(require("fs"), 1);
38581
- var import_node_path26 = __toESM(require("path"), 1);
38582
- var import_node_crypto7 = __toESM(require("crypto"), 1);
38628
+ var import_node_fs24 = __toESM(require("fs"), 1);
38629
+ var import_node_path27 = __toESM(require("path"), 1);
38630
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
38583
38631
  var import_node_child_process5 = require("child_process");
38584
38632
 
38585
38633
  // src/tunnel/tunnel-store.ts
38586
- var import_node_fs20 = __toESM(require("fs"), 1);
38587
- var import_node_path23 = __toESM(require("path"), 1);
38634
+ var import_node_fs21 = __toESM(require("fs"), 1);
38635
+ var import_node_path24 = __toESM(require("path"), 1);
38588
38636
  var TunnelStore = class {
38589
38637
  constructor(filePath) {
38590
38638
  this.filePath = filePath;
@@ -38592,7 +38640,7 @@ var TunnelStore = class {
38592
38640
  filePath;
38593
38641
  async get() {
38594
38642
  try {
38595
- const raw = await import_node_fs20.default.promises.readFile(this.filePath, "utf8");
38643
+ const raw = await import_node_fs21.default.promises.readFile(this.filePath, "utf8");
38596
38644
  const obj = JSON.parse(raw);
38597
38645
  if (!isPersistedTunnel(obj)) return null;
38598
38646
  return obj;
@@ -38603,22 +38651,22 @@ var TunnelStore = class {
38603
38651
  }
38604
38652
  }
38605
38653
  async set(v2) {
38606
- const dir = import_node_path23.default.dirname(this.filePath);
38607
- await import_node_fs20.default.promises.mkdir(dir, { recursive: true });
38654
+ const dir = import_node_path24.default.dirname(this.filePath);
38655
+ await import_node_fs21.default.promises.mkdir(dir, { recursive: true });
38608
38656
  const data = JSON.stringify(v2, null, 2);
38609
38657
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
38610
- await import_node_fs20.default.promises.writeFile(tmp, data, { mode: 384 });
38658
+ await import_node_fs21.default.promises.writeFile(tmp, data, { mode: 384 });
38611
38659
  if (process.platform !== "win32") {
38612
38660
  try {
38613
- await import_node_fs20.default.promises.chmod(tmp, 384);
38661
+ await import_node_fs21.default.promises.chmod(tmp, 384);
38614
38662
  } catch {
38615
38663
  }
38616
38664
  }
38617
- await import_node_fs20.default.promises.rename(tmp, this.filePath);
38665
+ await import_node_fs21.default.promises.rename(tmp, this.filePath);
38618
38666
  }
38619
38667
  async clear() {
38620
38668
  try {
38621
- await import_node_fs20.default.promises.unlink(this.filePath);
38669
+ await import_node_fs21.default.promises.unlink(this.filePath);
38622
38670
  } catch (err) {
38623
38671
  const code = err?.code;
38624
38672
  if (code !== "ENOENT") throw err;
@@ -38713,9 +38761,9 @@ function escape(v2) {
38713
38761
  }
38714
38762
 
38715
38763
  // src/tunnel/frpc-binary.ts
38716
- var import_node_fs21 = __toESM(require("fs"), 1);
38764
+ var import_node_fs22 = __toESM(require("fs"), 1);
38717
38765
  var import_node_os11 = __toESM(require("os"), 1);
38718
- var import_node_path24 = __toESM(require("path"), 1);
38766
+ var import_node_path25 = __toESM(require("path"), 1);
38719
38767
  var import_node_child_process3 = require("child_process");
38720
38768
  var import_node_stream2 = require("stream");
38721
38769
  var import_promises3 = require("stream/promises");
@@ -38747,20 +38795,20 @@ function frpcDownloadUrl(version2, p2) {
38747
38795
  }
38748
38796
  async function ensureFrpcBinary(opts) {
38749
38797
  if (opts.override) {
38750
- if (!import_node_fs21.default.existsSync(opts.override)) {
38798
+ if (!import_node_fs22.default.existsSync(opts.override)) {
38751
38799
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
38752
38800
  }
38753
38801
  return opts.override;
38754
38802
  }
38755
38803
  const version2 = opts.version ?? FRPC_VERSION;
38756
38804
  const platform = opts.platform ?? detectPlatform();
38757
- const binDir = import_node_path24.default.join(opts.dataDir, "bin");
38758
- import_node_fs21.default.mkdirSync(binDir, { recursive: true });
38805
+ const binDir = import_node_path25.default.join(opts.dataDir, "bin");
38806
+ import_node_fs22.default.mkdirSync(binDir, { recursive: true });
38759
38807
  cleanupStaleArtifacts(binDir);
38760
- const stableBin = import_node_path24.default.join(binDir, "frpc");
38761
- if (import_node_fs21.default.existsSync(stableBin)) return stableBin;
38808
+ const stableBin = import_node_path25.default.join(binDir, "frpc");
38809
+ if (import_node_fs22.default.existsSync(stableBin)) return stableBin;
38762
38810
  const partialBin = `${stableBin}.partial`;
38763
- const tarballPath = import_node_path24.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
38811
+ const tarballPath = import_node_path25.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
38764
38812
  try {
38765
38813
  const url = frpcDownloadUrl(version2, platform);
38766
38814
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -38769,8 +38817,8 @@ async function ensureFrpcBinary(opts) {
38769
38817
  } else {
38770
38818
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
38771
38819
  }
38772
- import_node_fs21.default.chmodSync(partialBin, 493);
38773
- import_node_fs21.default.renameSync(partialBin, stableBin);
38820
+ import_node_fs22.default.chmodSync(partialBin, 493);
38821
+ import_node_fs22.default.renameSync(partialBin, stableBin);
38774
38822
  } finally {
38775
38823
  safeUnlink(tarballPath);
38776
38824
  safeUnlink(partialBin);
@@ -38780,15 +38828,15 @@ async function ensureFrpcBinary(opts) {
38780
38828
  function cleanupStaleArtifacts(binDir) {
38781
38829
  let entries;
38782
38830
  try {
38783
- entries = import_node_fs21.default.readdirSync(binDir);
38831
+ entries = import_node_fs22.default.readdirSync(binDir);
38784
38832
  } catch {
38785
38833
  return;
38786
38834
  }
38787
38835
  for (const name of entries) {
38788
38836
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
38789
- const full = import_node_path24.default.join(binDir, name);
38837
+ const full = import_node_path25.default.join(binDir, name);
38790
38838
  try {
38791
- import_node_fs21.default.rmSync(full, { recursive: true, force: true });
38839
+ import_node_fs22.default.rmSync(full, { recursive: true, force: true });
38792
38840
  } catch {
38793
38841
  }
38794
38842
  }
@@ -38796,7 +38844,7 @@ function cleanupStaleArtifacts(binDir) {
38796
38844
  }
38797
38845
  function safeUnlink(p2) {
38798
38846
  try {
38799
- import_node_fs21.default.unlinkSync(p2);
38847
+ import_node_fs22.default.unlinkSync(p2);
38800
38848
  } catch {
38801
38849
  }
38802
38850
  }
@@ -38807,13 +38855,13 @@ async function downloadToFile(url, dest, fetchImpl) {
38807
38855
  if (!res.ok || !res.body) {
38808
38856
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
38809
38857
  }
38810
- const out = import_node_fs21.default.createWriteStream(dest);
38858
+ const out = import_node_fs22.default.createWriteStream(dest);
38811
38859
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
38812
38860
  await (0, import_promises3.pipeline)(nodeStream, out);
38813
38861
  }
38814
38862
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
38815
- const work = import_node_path24.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
38816
- import_node_fs21.default.mkdirSync(work, { recursive: true });
38863
+ const work = import_node_path25.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
38864
+ import_node_fs22.default.mkdirSync(work, { recursive: true });
38817
38865
  try {
38818
38866
  await new Promise((resolve6, reject) => {
38819
38867
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -38821,32 +38869,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
38821
38869
  proc.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
38822
38870
  });
38823
38871
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
38824
- const src = import_node_path24.default.join(work, dirName, "frpc");
38825
- if (!import_node_fs21.default.existsSync(src)) {
38872
+ const src = import_node_path25.default.join(work, dirName, "frpc");
38873
+ if (!import_node_fs22.default.existsSync(src)) {
38826
38874
  throw new Error(`frpc not found inside tarball at ${src}`);
38827
38875
  }
38828
- import_node_fs21.default.copyFileSync(src, destBin);
38876
+ import_node_fs22.default.copyFileSync(src, destBin);
38829
38877
  } finally {
38830
- import_node_fs21.default.rmSync(work, { recursive: true, force: true });
38878
+ import_node_fs22.default.rmSync(work, { recursive: true, force: true });
38831
38879
  }
38832
38880
  }
38833
38881
 
38834
38882
  // src/tunnel/frpc-process.ts
38835
- var import_node_fs22 = __toESM(require("fs"), 1);
38836
- var import_node_path25 = __toESM(require("path"), 1);
38883
+ var import_node_fs23 = __toESM(require("fs"), 1);
38884
+ var import_node_path26 = __toESM(require("path"), 1);
38837
38885
  var import_node_child_process4 = require("child_process");
38838
38886
  function frpcPidFilePath(dataDir) {
38839
- return import_node_path25.default.join(dataDir, "frpc.pid");
38887
+ return import_node_path26.default.join(dataDir, "frpc.pid");
38840
38888
  }
38841
38889
  function writeFrpcPid(dataDir, pid) {
38842
38890
  try {
38843
- import_node_fs22.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
38891
+ import_node_fs23.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
38844
38892
  } catch {
38845
38893
  }
38846
38894
  }
38847
38895
  function clearFrpcPid(dataDir) {
38848
38896
  try {
38849
- import_node_fs22.default.unlinkSync(frpcPidFilePath(dataDir));
38897
+ import_node_fs23.default.unlinkSync(frpcPidFilePath(dataDir));
38850
38898
  } catch {
38851
38899
  }
38852
38900
  }
@@ -38862,7 +38910,7 @@ function defaultIsPidAlive(pid) {
38862
38910
  }
38863
38911
  function defaultReadPidFile(file) {
38864
38912
  try {
38865
- return import_node_fs22.default.readFileSync(file, "utf8");
38913
+ return import_node_fs23.default.readFileSync(file, "utf8");
38866
38914
  } catch {
38867
38915
  return null;
38868
38916
  }
@@ -38878,7 +38926,7 @@ function defaultSleep(ms) {
38878
38926
  }
38879
38927
  async function killStaleFrpc(deps) {
38880
38928
  const pidFile = frpcPidFilePath(deps.dataDir);
38881
- const tomlPath = import_node_path25.default.join(deps.dataDir, "frpc.toml");
38929
+ const tomlPath = import_node_path26.default.join(deps.dataDir, "frpc.toml");
38882
38930
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
38883
38931
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
38884
38932
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -38902,7 +38950,7 @@ async function killStaleFrpc(deps) {
38902
38950
  }
38903
38951
  if (victims.size === 0) {
38904
38952
  try {
38905
- import_node_fs22.default.unlinkSync(pidFile);
38953
+ import_node_fs23.default.unlinkSync(pidFile);
38906
38954
  } catch {
38907
38955
  }
38908
38956
  return;
@@ -38913,7 +38961,7 @@ async function killStaleFrpc(deps) {
38913
38961
  }
38914
38962
  await sleep(deps.reapWaitMs ?? 300);
38915
38963
  try {
38916
- import_node_fs22.default.unlinkSync(pidFile);
38964
+ import_node_fs23.default.unlinkSync(pidFile);
38917
38965
  } catch {
38918
38966
  }
38919
38967
  }
@@ -38950,7 +38998,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
38950
38998
  var TunnelManager = class {
38951
38999
  constructor(deps) {
38952
39000
  this.deps = deps;
38953
- this.store = deps.store ?? new TunnelStore(import_node_path26.default.join(deps.dataDir, "tunnel.json"));
39001
+ this.store = deps.store ?? new TunnelStore(import_node_path27.default.join(deps.dataDir, "tunnel.json"));
38954
39002
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
38955
39003
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
38956
39004
  }
@@ -39077,8 +39125,8 @@ var TunnelManager = class {
39077
39125
  dataDir: this.deps.dataDir,
39078
39126
  override: this.deps.frpcBinaryOverride ?? void 0
39079
39127
  });
39080
- const tomlPath = import_node_path26.default.join(this.deps.dataDir, "frpc.toml");
39081
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
39128
+ const tomlPath = import_node_path27.default.join(this.deps.dataDir, "frpc.toml");
39129
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto8.default.randomBytes(3).toString("hex")}`;
39082
39130
  const toml = buildFrpcToml({
39083
39131
  serverAddr: t.frpsHost,
39084
39132
  serverPort: t.frpsPort,
@@ -39088,12 +39136,12 @@ var TunnelManager = class {
39088
39136
  localPort,
39089
39137
  logLevel: "info"
39090
39138
  });
39091
- await import_node_fs23.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39139
+ await import_node_fs24.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39092
39140
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
39093
39141
  stdio: ["ignore", "pipe", "pipe"]
39094
39142
  });
39095
- const logFilePath = import_node_path26.default.join(this.deps.dataDir, "frpc.log");
39096
- const logStream = import_node_fs23.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39143
+ const logFilePath = import_node_path27.default.join(this.deps.dataDir, "frpc.log");
39144
+ const logStream = import_node_fs24.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39097
39145
  logStream.on("error", () => {
39098
39146
  });
39099
39147
  const tee = (chunk) => {
@@ -39176,31 +39224,31 @@ async function waitForFrpcReady(proc, timeoutMs) {
39176
39224
 
39177
39225
  // src/tunnel/device-key.ts
39178
39226
  var import_node_os12 = __toESM(require("os"), 1);
39179
- var import_node_path27 = __toESM(require("path"), 1);
39180
- var import_node_crypto8 = __toESM(require("crypto"), 1);
39227
+ var import_node_path28 = __toESM(require("path"), 1);
39228
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
39181
39229
  var DERIVE_SALT = "clawd-tunnel-device-v1";
39182
39230
  function deriveStableDeviceKey(opts = {}) {
39183
39231
  const hostname = opts.hostname ?? import_node_os12.default.hostname();
39184
39232
  const uid = opts.uid ?? (typeof import_node_os12.default.userInfo === "function" ? import_node_os12.default.userInfo().uid : 0);
39185
39233
  const home = opts.home ?? import_node_os12.default.homedir();
39186
- const defaultDataDir = import_node_path27.default.resolve(import_node_path27.default.join(home, ".clawd"));
39187
- const normalizedDataDir = opts.dataDir ? import_node_path27.default.resolve(opts.dataDir) : null;
39234
+ const defaultDataDir = import_node_path28.default.resolve(import_node_path28.default.join(home, ".clawd"));
39235
+ const normalizedDataDir = opts.dataDir ? import_node_path28.default.resolve(opts.dataDir) : null;
39188
39236
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
39189
39237
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
39190
- return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39238
+ return import_node_crypto9.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39191
39239
  }
39192
39240
 
39193
39241
  // src/auth-store.ts
39194
- var import_node_fs24 = __toESM(require("fs"), 1);
39195
- var import_node_path28 = __toESM(require("path"), 1);
39196
- var import_node_crypto9 = __toESM(require("crypto"), 1);
39242
+ var import_node_fs25 = __toESM(require("fs"), 1);
39243
+ var import_node_path29 = __toESM(require("path"), 1);
39244
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
39197
39245
  var AUTH_FILE_NAME = "auth.json";
39198
39246
  function authFilePath(dataDir) {
39199
- return import_node_path28.default.join(dataDir, AUTH_FILE_NAME);
39247
+ return import_node_path29.default.join(dataDir, AUTH_FILE_NAME);
39200
39248
  }
39201
39249
  function loadOrCreateAuthFile(opts) {
39202
39250
  const file = authFilePath(opts.dataDir);
39203
- const generate = opts.generate ?? defaultGenerateToken;
39251
+ const generate = opts.generate ?? defaultGenerateToken2;
39204
39252
  const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
39205
39253
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
39206
39254
  const existing = readAuthFile(file);
@@ -39220,15 +39268,15 @@ function loadOrCreateAuthFile(opts) {
39220
39268
  writeAuthFile(file, next);
39221
39269
  return next;
39222
39270
  }
39223
- function defaultGenerateToken() {
39224
- return import_node_crypto9.default.randomBytes(32).toString("base64url");
39271
+ function defaultGenerateToken2() {
39272
+ return import_node_crypto10.default.randomBytes(32).toString("base64url");
39225
39273
  }
39226
39274
  function defaultGenerateOwnerPrincipalId() {
39227
- return `owner-${import_node_crypto9.default.randomUUID()}`;
39275
+ return `owner-${import_node_crypto10.default.randomUUID()}`;
39228
39276
  }
39229
39277
  function readAuthFile(file) {
39230
39278
  try {
39231
- const raw = import_node_fs24.default.readFileSync(file, "utf8");
39279
+ const raw = import_node_fs25.default.readFileSync(file, "utf8");
39232
39280
  const parsed = JSON.parse(raw);
39233
39281
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
39234
39282
  return null;
@@ -39246,25 +39294,25 @@ function readAuthFile(file) {
39246
39294
  }
39247
39295
  }
39248
39296
  function writeAuthFile(file, content) {
39249
- import_node_fs24.default.mkdirSync(import_node_path28.default.dirname(file), { recursive: true });
39250
- import_node_fs24.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39297
+ import_node_fs25.default.mkdirSync(import_node_path29.default.dirname(file), { recursive: true });
39298
+ import_node_fs25.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39251
39299
  try {
39252
- import_node_fs24.default.chmodSync(file, 384);
39300
+ import_node_fs25.default.chmodSync(file, 384);
39253
39301
  } catch {
39254
39302
  }
39255
39303
  }
39256
39304
 
39257
39305
  // src/owner-profile.ts
39258
- var import_node_fs25 = __toESM(require("fs"), 1);
39306
+ var import_node_fs26 = __toESM(require("fs"), 1);
39259
39307
  var import_node_os13 = __toESM(require("os"), 1);
39260
- var import_node_path29 = __toESM(require("path"), 1);
39308
+ var import_node_path30 = __toESM(require("path"), 1);
39261
39309
  var PROFILE_FILENAME = "profile.json";
39262
39310
  function loadOwnerDisplayName(dataDir) {
39263
39311
  const fallback = import_node_os13.default.userInfo().username;
39264
- const profilePath = import_node_path29.default.join(dataDir, PROFILE_FILENAME);
39312
+ const profilePath = import_node_path30.default.join(dataDir, PROFILE_FILENAME);
39265
39313
  let raw;
39266
39314
  try {
39267
- raw = import_node_fs25.default.readFileSync(profilePath, "utf8");
39315
+ raw = import_node_fs26.default.readFileSync(profilePath, "utf8");
39268
39316
  } catch {
39269
39317
  return fallback;
39270
39318
  }
@@ -39286,259 +39334,6 @@ function loadOwnerDisplayName(dataDir) {
39286
39334
  return displayName;
39287
39335
  }
39288
39336
 
39289
- // src/feishu-auth/owner-identity-store.ts
39290
- var import_node_fs26 = __toESM(require("fs"), 1);
39291
- var import_node_path30 = __toESM(require("path"), 1);
39292
- var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
39293
- var OwnerIdentityStore = class {
39294
- file;
39295
- constructor(dataDir) {
39296
- this.file = import_node_path30.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
39297
- }
39298
- read() {
39299
- let raw;
39300
- try {
39301
- raw = import_node_fs26.default.readFileSync(this.file, "utf8");
39302
- } catch {
39303
- return null;
39304
- }
39305
- let parsed;
39306
- try {
39307
- parsed = JSON.parse(raw);
39308
- } catch {
39309
- return null;
39310
- }
39311
- const r = parsed;
39312
- if (!r.identity || typeof r.identity.unionId !== "string" || r.identity.unionId.length === 0 || typeof r.identity.displayName !== "string" || r.identity.displayName.length === 0 || typeof r.ttcToken !== "string" || r.ttcToken.length === 0) {
39313
- return null;
39314
- }
39315
- return {
39316
- identity: {
39317
- unionId: r.identity.unionId,
39318
- displayName: r.identity.displayName,
39319
- ...typeof r.identity.avatarUrl === "string" ? { avatarUrl: r.identity.avatarUrl } : {}
39320
- },
39321
- ttcToken: r.ttcToken,
39322
- ttcTokenExpiresAt: typeof r.ttcTokenExpiresAt === "number" ? r.ttcTokenExpiresAt : null,
39323
- loginAt: typeof r.loginAt === "string" ? r.loginAt : (/* @__PURE__ */ new Date(0)).toISOString()
39324
- };
39325
- }
39326
- write(record) {
39327
- import_node_fs26.default.mkdirSync(import_node_path30.default.dirname(this.file), { recursive: true });
39328
- import_node_fs26.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
39329
- try {
39330
- import_node_fs26.default.chmodSync(this.file, 384);
39331
- } catch {
39332
- }
39333
- }
39334
- clear() {
39335
- try {
39336
- import_node_fs26.default.unlinkSync(this.file);
39337
- } catch (err) {
39338
- const code = err?.code;
39339
- if (code !== "ENOENT") throw err;
39340
- }
39341
- }
39342
- };
39343
-
39344
- // src/feishu-auth/login-flow.ts
39345
- var import_node_crypto10 = __toESM(require("crypto"), 1);
39346
- var STATE_TTL_MS = 5 * 60 * 1e3;
39347
- var LoginFlow = class {
39348
- constructor(deps) {
39349
- this.deps = deps;
39350
- }
39351
- deps;
39352
- pendingStates = /* @__PURE__ */ new Map();
39353
- start() {
39354
- const state = import_node_crypto10.default.randomBytes(16).toString("base64url");
39355
- const now = (this.deps.now ?? Date.now)();
39356
- this.pendingStates.set(state, now);
39357
- this.gcExpired(now);
39358
- const url = new URL("/auth/authorize", this.deps.ttcAuthBase);
39359
- url.searchParams.set("callback_url", this.deps.getCallbackUrl());
39360
- url.searchParams.set("state", state);
39361
- url.searchParams.set("auto", "1");
39362
- return { authUrl: url.toString(), state };
39363
- }
39364
- async handleCallback(params) {
39365
- const now = (this.deps.now ?? Date.now)();
39366
- const issuedAt = this.pendingStates.get(params.state);
39367
- if (issuedAt === void 0) {
39368
- return { ok: false, reason: "state mismatch" };
39369
- }
39370
- this.pendingStates.delete(params.state);
39371
- if (now - issuedAt > STATE_TTL_MS) {
39372
- return { ok: false, reason: "state expired" };
39373
- }
39374
- if (!params.token) {
39375
- return { ok: false, reason: "missing token" };
39376
- }
39377
- let identity;
39378
- try {
39379
- identity = await this.deps.fetchUserInfo(params.token);
39380
- } catch (err) {
39381
- return { ok: false, reason: `user_info failed: ${err.message}` };
39382
- }
39383
- const expiresAtNum = params.expiresAt ? Number.parseInt(params.expiresAt, 10) : NaN;
39384
- this.deps.store.write({
39385
- identity,
39386
- ttcToken: params.token,
39387
- ttcTokenExpiresAt: Number.isFinite(expiresAtNum) ? expiresAtNum : null,
39388
- loginAt: new Date(now).toISOString()
39389
- });
39390
- return { ok: true, identity };
39391
- }
39392
- gcExpired(now) {
39393
- for (const [state, issuedAt] of this.pendingStates) {
39394
- if (now - issuedAt > STATE_TTL_MS) this.pendingStates.delete(state);
39395
- }
39396
- }
39397
- };
39398
-
39399
- // src/feishu-auth/ttc-client.ts
39400
- var TTC_HOSTS = {
39401
- auth: "https://app.ttcadvisory.com",
39402
- api: "https://api.ttcadvisory.com"
39403
- };
39404
- var TtcUserInfoError = class extends Error {
39405
- constructor(code, message) {
39406
- super(message);
39407
- this.code = code;
39408
- this.name = "TtcUserInfoError";
39409
- }
39410
- code;
39411
- };
39412
- async function fetchTtcUserInfo(opts) {
39413
- const doFetch = opts.fetchImpl ?? fetch;
39414
- let res;
39415
- try {
39416
- res = await doFetch(`${opts.apiBase}/api/user_service/v1/login/user`, {
39417
- headers: { Authorization: `Bearer ${opts.token}` }
39418
- });
39419
- } catch (err) {
39420
- throw new TtcUserInfoError("NETWORK", `TTC user_info request failed: ${err.message}`);
39421
- }
39422
- if (!res.ok) {
39423
- if (res.status === 401) {
39424
- throw new TtcUserInfoError("UNAUTHORIZED", "TTC token invalid or expired");
39425
- }
39426
- throw new TtcUserInfoError("API_ERROR", `TTC user_info HTTP ${res.status}`);
39427
- }
39428
- const body = await res.json();
39429
- if (body.code !== 0) {
39430
- throw new TtcUserInfoError("API_ERROR", `TTC user_info code=${body.code}: ${body.message ?? ""}`);
39431
- }
39432
- const d = body.data;
39433
- if (!d || typeof d.union_id !== "string" || d.union_id.length === 0 || typeof d.name !== "string" || d.name.length === 0) {
39434
- throw new TtcUserInfoError("BAD_RESPONSE", "TTC user_info missing union_id or name");
39435
- }
39436
- return {
39437
- unionId: d.union_id,
39438
- displayName: d.name,
39439
- ...typeof d.avatar_url === "string" && d.avatar_url.length > 0 ? { avatarUrl: d.avatar_url } : {}
39440
- };
39441
- }
39442
-
39443
- // src/feishu-auth/central-client.ts
39444
- var CentralClientError = class extends Error {
39445
- constructor(code, message, cause) {
39446
- super(message);
39447
- this.code = code;
39448
- this.cause = cause;
39449
- this.name = "CentralClientError";
39450
- }
39451
- code;
39452
- cause;
39453
- };
39454
- async function centralRequest(opts, path59, init) {
39455
- const f = opts.fetchImpl ?? globalThis.fetch;
39456
- const url = `${opts.api.replace(/\/+$/, "")}${path59}`;
39457
- const ctrl = new AbortController();
39458
- const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
39459
- let res;
39460
- try {
39461
- res = await f(url, {
39462
- method: init.method,
39463
- headers: {
39464
- "content-type": "application/json",
39465
- ...init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}
39466
- },
39467
- ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
39468
- signal: ctrl.signal
39469
- });
39470
- } catch (err) {
39471
- clearTimeout(timer);
39472
- throw new CentralClientError("NETWORK", `central server request failed: ${err.message}`, err);
39473
- }
39474
- clearTimeout(timer);
39475
- if (res.status === 401) {
39476
- throw new CentralClientError("UNAUTHORIZED", "TTC JWT invalid or expired");
39477
- }
39478
- if (!res.ok) {
39479
- throw new CentralClientError("API_ERROR", `central server HTTP ${res.status}`);
39480
- }
39481
- try {
39482
- return await res.json();
39483
- } catch (err) {
39484
- throw new CentralClientError("BAD_RESPONSE", "central server: invalid json response", err);
39485
- }
39486
- }
39487
- async function centralExchange(opts) {
39488
- const body = await centralRequest(opts, "/api/clawd-identity/exchange", {
39489
- method: "POST",
39490
- body: { ttcJwt: opts.ttcJwt, hubId: opts.hubId }
39491
- });
39492
- if (typeof body.token !== "string" || body.token.length === 0 || typeof body.unionId !== "string" || typeof body.displayName !== "string") {
39493
- throw new CentralClientError("BAD_RESPONSE", "exchange: missing token/unionId/displayName");
39494
- }
39495
- const hubUrl = typeof body.hubUrl === "string" && body.hubUrl.length > 0 ? body.hubUrl : null;
39496
- return { token: body.token, unionId: body.unionId, displayName: body.displayName, hubUrl };
39497
- }
39498
- async function centralUpsertHub(opts) {
39499
- const body = await centralRequest(opts, "/api/clawd-identity/hubs/self", {
39500
- method: "PUT",
39501
- bearer: opts.ttcJwt,
39502
- body: { url: opts.url, ...opts.name ? { name: opts.name } : {} }
39503
- });
39504
- if (body.ok !== true) {
39505
- throw new CentralClientError("BAD_RESPONSE", "upsertHub: server did not ack ok");
39506
- }
39507
- return { ok: true };
39508
- }
39509
- async function centralIntrospect(opts) {
39510
- const body = await centralRequest(opts, "/api/clawd-identity/introspect", {
39511
- method: "POST",
39512
- bearer: opts.hubTtcJwt,
39513
- body: { token: opts.token }
39514
- });
39515
- if (body.active !== true) {
39516
- return { active: false };
39517
- }
39518
- if (typeof body.unionId !== "string" || typeof body.displayName !== "string") {
39519
- throw new CentralClientError("BAD_RESPONSE", "introspect: active but missing identity");
39520
- }
39521
- return { active: true, unionId: body.unionId, displayName: body.displayName };
39522
- }
39523
- async function centralListHubs(opts) {
39524
- const body = await centralRequest(opts, "/api/clawd-identity/hubs", {
39525
- method: "GET",
39526
- bearer: opts.ttcJwt
39527
- });
39528
- if (!Array.isArray(body.hubs)) {
39529
- throw new CentralClientError("BAD_RESPONSE", "hubs: missing hubs array");
39530
- }
39531
- const out = [];
39532
- for (const raw of body.hubs) {
39533
- const h = raw;
39534
- if (typeof h.hubId !== "string" || typeof h.name !== "string" || typeof h.url !== "string") {
39535
- throw new CentralClientError("BAD_RESPONSE", "hubs: malformed hub entry");
39536
- }
39537
- out.push({ hubId: h.hubId, name: h.name, url: h.url });
39538
- }
39539
- return out;
39540
- }
39541
-
39542
39337
  // src/index.ts
39543
39338
  init_protocol();
39544
39339
 
@@ -39699,7 +39494,69 @@ function buildSessionHandlers(deps) {
39699
39494
  const update = async (frame, _client, ctx) => {
39700
39495
  const args = SessionUpdateArgs.parse(frame);
39701
39496
  ensureSessionAccess(ctx, args.sessionId, "send");
39497
+ const prevFile = manager.findOwnedSession(args.sessionId);
39498
+ const prevProject = prevFile?.appBuilderProject ?? null;
39499
+ const nextProject = args.patch.appBuilderProject ?? prevProject;
39702
39500
  const { response, broadcast } = manager.update(args);
39501
+ if (args.patch.appBuilderProject !== void 0) {
39502
+ deps.logger?.info("session-update.appBuilderProject", {
39503
+ sessionId: args.sessionId,
39504
+ prevProject,
39505
+ nextProject,
39506
+ hasLifecycle: !!deps.devServerLifecycle,
39507
+ hasStore: !!deps.appBuilderStore
39508
+ });
39509
+ }
39510
+ if (deps.devServerLifecycle && deps.appBuilderStore && args.patch.appBuilderProject !== void 0) {
39511
+ const lifecycle = deps.devServerLifecycle;
39512
+ const projectStore = deps.appBuilderStore;
39513
+ const tunnelUrl = deps.getTunnelUrl?.() ?? null;
39514
+ try {
39515
+ if (prevProject && prevProject !== nextProject) {
39516
+ await lifecycle.stopForSession(args.sessionId);
39517
+ }
39518
+ if (nextProject && nextProject !== "") {
39519
+ const occupantSessionId = lifecycle.boundSessionId(nextProject);
39520
+ const alreadyRunningForThisSession = lifecycle.isRunning(nextProject) && occupantSessionId === args.sessionId;
39521
+ deps.logger?.info("session-update.maybe-start-dev-server", {
39522
+ sessionId: args.sessionId,
39523
+ projectName: nextProject,
39524
+ occupantSessionId,
39525
+ alreadyRunningForThisSession
39526
+ });
39527
+ if (!alreadyRunningForThisSession) {
39528
+ if (occupantSessionId && occupantSessionId !== args.sessionId) {
39529
+ await lifecycle.stopForProject(nextProject);
39530
+ }
39531
+ const projects = await projectStore.list();
39532
+ const target = projects.find((p2) => p2.name === nextProject);
39533
+ deps.logger?.info("session-update.start-dev-server", {
39534
+ sessionId: args.sessionId,
39535
+ projectName: nextProject,
39536
+ targetFound: !!target,
39537
+ projectCwd: target ? projectStore.projectDir(nextProject) : null,
39538
+ devCommand: target?.devCommand,
39539
+ tunnelUrl
39540
+ });
39541
+ if (target) {
39542
+ lifecycle.startForSession({
39543
+ sessionId: args.sessionId,
39544
+ projectName: nextProject,
39545
+ cwd: projectStore.projectDir(nextProject),
39546
+ port: target.port,
39547
+ tunnelHost: tunnelUrl ? new URL(tunnelUrl.replace(/^wss?:\/\//i, "https://")).host : null,
39548
+ devCommand: target.devCommand
39549
+ });
39550
+ }
39551
+ }
39552
+ }
39553
+ } catch (err) {
39554
+ deps.logger?.warn("session-update.lifecycle-failed", {
39555
+ sessionId: args.sessionId,
39556
+ error: err instanceof Error ? err.message : String(err)
39557
+ });
39558
+ }
39559
+ }
39703
39560
  return { response: { type: "session:info", ...response }, broadcast };
39704
39561
  };
39705
39562
  const del = async (frame, _client, ctx) => {
@@ -40328,7 +40185,7 @@ var DeleteArgsSchema = external_exports.object({
40328
40185
  capabilityId: external_exports.string().min(1)
40329
40186
  }).strict();
40330
40187
  function buildCapabilityHandlers(deps) {
40331
- const { manager } = deps;
40188
+ const { manager, getShareBaseUrl, getPersonalCapability } = deps;
40332
40189
  const list = async () => {
40333
40190
  return {
40334
40191
  response: {
@@ -40354,9 +40211,21 @@ function buildCapabilityHandlers(deps) {
40354
40211
  }
40355
40212
  };
40356
40213
  };
40214
+ const personalCapGet = async () => {
40215
+ const { token, capability } = getPersonalCapability();
40216
+ return {
40217
+ response: {
40218
+ type: "personal-cap:get:ok",
40219
+ token,
40220
+ capability: stripSecretHash(capability),
40221
+ shareBaseUrl: getShareBaseUrl()
40222
+ }
40223
+ };
40224
+ };
40357
40225
  return {
40358
40226
  "capability:list": list,
40359
- "capability:delete": del
40227
+ "capability:delete": del,
40228
+ "personal-cap:get": personalCapGet
40360
40229
  };
40361
40230
  }
40362
40231
 
@@ -40464,7 +40333,16 @@ function ensureOwner(ctx) {
40464
40333
  );
40465
40334
  }
40466
40335
  }
40336
+ function ensureGuestCtx(ctx) {
40337
+ if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
40338
+ throw new ClawdError(
40339
+ ERROR_CODES.UNAUTHORIZED,
40340
+ "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
40341
+ );
40342
+ }
40343
+ }
40467
40344
  function buildReceivedCapabilityHandlers(deps) {
40345
+ const now = deps.now ?? Date.now;
40468
40346
  const list = async (_frame, _client, ctx) => {
40469
40347
  ensureOwner(ctx);
40470
40348
  return {
@@ -40474,6 +40352,47 @@ function buildReceivedCapabilityHandlers(deps) {
40474
40352
  }
40475
40353
  };
40476
40354
  };
40355
+ const add = async (frame, _client, ctx) => {
40356
+ ensureOwner(ctx);
40357
+ const { type: _t, requestId: _r, ...rest } = frame;
40358
+ const args = ReceivedCapabilityAddArgsSchema.parse(rest);
40359
+ let conn;
40360
+ try {
40361
+ conn = await deps.connectRemote({
40362
+ url: args.remoteUrl,
40363
+ token: args.token,
40364
+ selfPrincipalId: deps.selfPrincipalId(),
40365
+ selfDisplayName: deps.selfDisplayName()
40366
+ });
40367
+ } catch (e) {
40368
+ throw new ClawdError(
40369
+ ERROR_CODES.VALIDATION_ERROR,
40370
+ `INVITE_REMOTE_UNREACHABLE: ${e.message}`
40371
+ );
40372
+ }
40373
+ try {
40374
+ const wh = await conn.whoami();
40375
+ const rc = {
40376
+ ownerPrincipalId: wh.ownerId,
40377
+ ownerDisplayName: wh.displayName,
40378
+ remoteUrl: args.remoteUrl,
40379
+ capabilityId: wh.capabilityId,
40380
+ capabilityToken: args.token,
40381
+ grants: wh.grants,
40382
+ receivedAt: now()
40383
+ };
40384
+ deps.store.upsert(rc);
40385
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40386
+ return {
40387
+ response: { type: "received-capability:add:ok", receivedCap: rc }
40388
+ };
40389
+ } finally {
40390
+ try {
40391
+ conn.close();
40392
+ } catch {
40393
+ }
40394
+ }
40395
+ };
40477
40396
  const remove = async (frame, _client, ctx) => {
40478
40397
  ensureOwner(ctx);
40479
40398
  const { type: _t, requestId: _r, ...rest } = frame;
@@ -40490,9 +40409,30 @@ function buildReceivedCapabilityHandlers(deps) {
40490
40409
  }
40491
40410
  };
40492
40411
  };
40412
+ const autoAttach = async (frame, _client, ctx) => {
40413
+ ensureGuestCtx(ctx);
40414
+ const { type: _t, requestId: _r, ...rest } = frame;
40415
+ const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
40416
+ const rc = {
40417
+ ownerPrincipalId: args.ownerPrincipalId,
40418
+ ownerDisplayName: args.ownerDisplayName,
40419
+ remoteUrl: args.remoteUrl,
40420
+ capabilityId: args.capabilityId,
40421
+ capabilityToken: args.token,
40422
+ grants: args.grants,
40423
+ receivedAt: now()
40424
+ };
40425
+ deps.store.upsert(rc);
40426
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40427
+ return {
40428
+ response: { type: "received-capability:autoAttach:ok" }
40429
+ };
40430
+ };
40493
40431
  return {
40494
40432
  "received-capability:list": list,
40495
- "received-capability:remove": remove
40433
+ "received-capability:add": add,
40434
+ "received-capability:remove": remove,
40435
+ "received-capability:autoAttach": autoAttach
40496
40436
  };
40497
40437
  }
40498
40438
 
@@ -40514,13 +40454,14 @@ function buildWhoamiHandler(deps) {
40514
40454
  usedCount: 0
40515
40455
  };
40516
40456
  } else {
40517
- capability = {
40518
- id: ctx.principal.id,
40519
- displayName: ctx.principal.displayName ?? "guest",
40520
- grants: ctx.grants,
40521
- issuedAt: 0,
40522
- usedCount: 0
40523
- };
40457
+ if (!ctx.capabilityId) {
40458
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
40459
+ }
40460
+ const cap = deps.capabilityManager.findById(ctx.capabilityId);
40461
+ if (!cap) {
40462
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
40463
+ }
40464
+ capability = stripSecretHash(cap);
40524
40465
  }
40525
40466
  const grantedPersonas = [];
40526
40467
  const isGuest = ctx.principal.kind === "guest";
@@ -40551,117 +40492,6 @@ function buildWhoamiHandler(deps) {
40551
40492
  };
40552
40493
  }
40553
40494
 
40554
- // src/handlers/feishu-auth.ts
40555
- function buildFeishuAuthHandlers(deps) {
40556
- const loginStart = async () => {
40557
- const { authUrl, state } = deps.loginFlow.start();
40558
- return {
40559
- response: { type: "auth:login:start:ok", authUrl, state }
40560
- };
40561
- };
40562
- const getIdentity = async () => {
40563
- const record = deps.ownerIdentityStore.read();
40564
- return {
40565
- response: {
40566
- type: "auth:getIdentity:ok",
40567
- identity: record ? record.identity : null
40568
- }
40569
- };
40570
- };
40571
- const logout = async () => {
40572
- deps.ownerIdentityStore.clear();
40573
- return {
40574
- response: { type: "auth:logout:ok" }
40575
- };
40576
- };
40577
- return {
40578
- "auth:login:start": loginStart,
40579
- "auth:getIdentity": getIdentity,
40580
- "auth:logout": logout
40581
- };
40582
- }
40583
-
40584
- // src/handlers/hub.ts
40585
- init_protocol();
40586
- function ensureOwner2(ctx) {
40587
- if (!ctx || ctx.principal.kind !== "owner") {
40588
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: hub:* requires owner ctx");
40589
- }
40590
- }
40591
- function buildHubHandlers(deps) {
40592
- const now = deps.now ?? Date.now;
40593
- const list = async (_frame, _client, ctx) => {
40594
- ensureOwner2(ctx);
40595
- const hubs = await deps.listHubs();
40596
- return {
40597
- response: { type: "hub:list:ok", hubs }
40598
- };
40599
- };
40600
- const connect = async (frame, _client, ctx) => {
40601
- ensureOwner2(ctx);
40602
- const { type: _t, requestId: _r, ...rest } = frame;
40603
- const args = HubConnectArgsSchema.parse(rest);
40604
- const exchanged = await deps.exchange(args.hubId);
40605
- const url = args.url ?? exchanged.hubUrl;
40606
- if (!url) {
40607
- throw new ClawdError(
40608
- ERROR_CODES.VALIDATION_ERROR,
40609
- "HUB_URL_UNKNOWN: hub \u672A\u4E0A\u62A5\u5730\u5740\uFF08\u5BF9\u65B9 daemon \u672A\u767B\u5F55\u6216\u672A\u5F00 tunnel\uFF09\uFF0C\u65E0\u6CD5\u8FDE\u63A5"
40610
- );
40611
- }
40612
- let conn;
40613
- try {
40614
- conn = await deps.connectRemote({
40615
- url,
40616
- token: exchanged.token,
40617
- selfPrincipalId: deps.selfPrincipalId(),
40618
- selfDisplayName: deps.selfDisplayName(),
40619
- // Phase 2 自动反向(spec 决策 #11):自报本机可达地址,让 hub 反向加我为联系人;
40620
- // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)
40621
- selfUrl: deps.selfUrl() ?? void 0
40622
- });
40623
- } catch (e) {
40624
- throw new ClawdError(
40625
- ERROR_CODES.VALIDATION_ERROR,
40626
- `HUB_UNREACHABLE: ${e.message}`
40627
- );
40628
- }
40629
- try {
40630
- const wh = await conn.whoami();
40631
- const rc = {
40632
- ownerPrincipalId: wh.ownerId,
40633
- ownerDisplayName: wh.displayName || args.name || args.hubId,
40634
- remoteUrl: url,
40635
- capabilityId: wh.capabilityId,
40636
- // Phase 2:connectToken 存中心 server 签发的 connect token(断线重连复用,
40637
- // 不需要重新换票——token 长期有效,撤销由中心 server introspect 兜底)
40638
- connectToken: exchanged.token,
40639
- grants: wh.grants,
40640
- receivedAt: now()
40641
- };
40642
- deps.store.upsert(rc);
40643
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40644
- return {
40645
- response: {
40646
- type: "hub:connect:ok",
40647
- hubId: wh.ownerId,
40648
- name: rc.ownerDisplayName,
40649
- url
40650
- }
40651
- };
40652
- } finally {
40653
- try {
40654
- conn.close();
40655
- } catch {
40656
- }
40657
- }
40658
- };
40659
- return {
40660
- "hub:list": list,
40661
- "hub:connect": connect
40662
- };
40663
- }
40664
-
40665
40495
  // src/handlers/meta.ts
40666
40496
  var import_node_os15 = __toESM(require("os"), 1);
40667
40497
  init_protocol();
@@ -41622,6 +41452,62 @@ function buildExtensionHandlers(deps) {
41622
41452
  };
41623
41453
  }
41624
41454
 
41455
+ // src/handlers/app-builder.ts
41456
+ function buildAppBuilderHandlers(deps) {
41457
+ const { store, deleteSessionsByProject, devServerLifecycle } = deps;
41458
+ const listProjects = async () => {
41459
+ const projects = await store.list();
41460
+ return {
41461
+ response: {
41462
+ projects: projects.map((p2) => ({
41463
+ ...p2,
41464
+ isRunning: devServerLifecycle.isRunning(p2.name),
41465
+ boundSessionId: devServerLifecycle.boundSessionId(p2.name)
41466
+ }))
41467
+ }
41468
+ };
41469
+ };
41470
+ const createProject = async (frame) => {
41471
+ const name = frame.name;
41472
+ if (typeof name !== "string") throw new Error("createProject: name must be string");
41473
+ const project = await store.create(name);
41474
+ return { response: { project } };
41475
+ };
41476
+ const deleteProject = async (frame) => {
41477
+ const name = frame.name;
41478
+ if (typeof name !== "string") throw new Error("deleteProject: name must be string");
41479
+ await devServerLifecycle.stopForProject(name);
41480
+ await deleteSessionsByProject(name);
41481
+ await store.delete(name);
41482
+ return { response: {} };
41483
+ };
41484
+ const updateProjectPort = async (frame) => {
41485
+ const f = frame;
41486
+ if (typeof f.name !== "string") throw new Error("updateProjectPort: name must be string");
41487
+ if (typeof f.newPort !== "number") throw new Error("updateProjectPort: newPort must be number");
41488
+ const entry = devServerLifecycle.boundEntry(f.name);
41489
+ await devServerLifecycle.stopForProject(f.name);
41490
+ const project = await store.updatePort(f.name, f.newPort);
41491
+ if (entry) {
41492
+ await devServerLifecycle.restartForProjectAt({
41493
+ projectName: f.name,
41494
+ newPort: f.newPort,
41495
+ sessionId: entry.sessionId,
41496
+ cwd: entry.cwd,
41497
+ tunnelHost: entry.tunnelHost,
41498
+ devCommand: entry.devCommand
41499
+ });
41500
+ }
41501
+ return { response: { project } };
41502
+ };
41503
+ return {
41504
+ "appBuilder:listProjects": listProjects,
41505
+ "appBuilder:createProject": createProject,
41506
+ "appBuilder:deleteProject": deleteProject,
41507
+ "appBuilder:updateProjectPort": updateProjectPort
41508
+ };
41509
+ }
41510
+
41625
41511
  // src/extension/registry.ts
41626
41512
  var import_promises8 = __toESM(require("fs/promises"), 1);
41627
41513
  var import_node_path39 = __toESM(require("path"), 1);
@@ -41710,7 +41596,13 @@ async function uninstall(deps) {
41710
41596
  // src/handlers/index.ts
41711
41597
  function buildMethodHandlers(deps) {
41712
41598
  return {
41713
- ...buildSessionHandlers(deps),
41599
+ ...buildSessionHandlers({
41600
+ ...deps,
41601
+ devServerLifecycle: deps.devServerLifecycle,
41602
+ appBuilderStore: deps.appBuilderStore,
41603
+ getTunnelUrl: deps.getTunnelUrl,
41604
+ logger: deps.logger
41605
+ }),
41714
41606
  ...buildPermissionHandlers(deps),
41715
41607
  ...buildHistoryHandlers(deps),
41716
41608
  ...buildWorkspaceHandlers(deps),
@@ -41722,7 +41614,9 @@ function buildMethodHandlers(deps) {
41722
41614
  personaRegistry: deps.personaRegistry
41723
41615
  }),
41724
41616
  ...buildCapabilityHandlers({
41725
- manager: deps.capabilityManager
41617
+ manager: deps.capabilityManager,
41618
+ getShareBaseUrl: deps.getShareBaseUrl,
41619
+ getPersonalCapability: deps.getPersonalCapability
41726
41620
  }),
41727
41621
  ...buildInboxHandlers({
41728
41622
  manager: deps.inboxManager,
@@ -41730,7 +41624,10 @@ function buildMethodHandlers(deps) {
41730
41624
  }),
41731
41625
  ...buildReceivedCapabilityHandlers({
41732
41626
  store: deps.receivedCapabilityStore,
41733
- broadcast: deps.broadcastToOwners
41627
+ connectRemote: deps.connectRemote,
41628
+ broadcast: deps.broadcastToOwners,
41629
+ selfPrincipalId: () => deps.ownerPrincipalId,
41630
+ selfDisplayName: () => deps.ownerDisplayName
41734
41631
  }),
41735
41632
  whoami: buildWhoamiHandler({
41736
41633
  ownerDisplayName: deps.ownerDisplayName,
@@ -41738,8 +41635,6 @@ function buildMethodHandlers(deps) {
41738
41635
  personaStore: deps.personaStore,
41739
41636
  capabilityManager: deps.capabilityManager
41740
41637
  }),
41741
- ...buildFeishuAuthHandlers(deps.feishuAuth),
41742
- ...buildHubHandlers(deps.feishuHub),
41743
41638
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {},
41744
41639
  ...buildExtensionHandlers({
41745
41640
  loadAll,
@@ -41748,10 +41643,254 @@ function buildMethodHandlers(deps) {
41748
41643
  root: extensionsRoot(),
41749
41644
  publishedChannelStore: deps.publishedChannelStore,
41750
41645
  bundleCache: deps.bundleCache
41646
+ }),
41647
+ ...buildAppBuilderHandlers({
41648
+ store: deps.appBuilderStore,
41649
+ deleteSessionsByProject: deps.deleteSessionsByProject,
41650
+ devServerLifecycle: deps.devServerLifecycle
41751
41651
  })
41752
41652
  };
41753
41653
  }
41754
41654
 
41655
+ // src/app-builder/project-store.ts
41656
+ var import_node_fs29 = require("fs");
41657
+ var import_node_path41 = require("path");
41658
+ init_protocol();
41659
+ var PROJECTS_DIR = "projects";
41660
+ var META_FILE = ".clawd-project.json";
41661
+ var ProjectStore = class {
41662
+ /** @param personaRoot persona 目录,例如 `~/.clawd/personas/persona-app-builder/` */
41663
+ constructor(personaRoot) {
41664
+ this.personaRoot = personaRoot;
41665
+ }
41666
+ personaRoot;
41667
+ /** projects/<name>/.clawd-project.json 路径 */
41668
+ metaPath(name) {
41669
+ return (0, import_node_path41.join)(this.projectsRoot(), name, META_FILE);
41670
+ }
41671
+ /** projects/<name>/ 目录路径(cwd 用) */
41672
+ projectDir(name) {
41673
+ return (0, import_node_path41.join)(this.projectsRoot(), name);
41674
+ }
41675
+ projectsRoot() {
41676
+ return (0, import_node_path41.join)(this.personaRoot, PROJECTS_DIR);
41677
+ }
41678
+ async list() {
41679
+ let entries;
41680
+ try {
41681
+ entries = await import_node_fs29.promises.readdir(this.projectsRoot());
41682
+ } catch (err) {
41683
+ if (err.code === "ENOENT") return [];
41684
+ throw err;
41685
+ }
41686
+ const out = [];
41687
+ for (const name of entries) {
41688
+ try {
41689
+ const raw = await import_node_fs29.promises.readFile(this.metaPath(name), "utf8");
41690
+ const json = JSON.parse(raw);
41691
+ let migrated = false;
41692
+ if (typeof json.devCommand !== "string" || json.devCommand.length === 0) {
41693
+ json.devCommand = DEFAULT_DEV_COMMAND;
41694
+ migrated = true;
41695
+ }
41696
+ const parsed = ProjectMetadataSchema.safeParse(json);
41697
+ if (parsed.success) {
41698
+ out.push(parsed.data);
41699
+ if (migrated) {
41700
+ void import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
41701
+ });
41702
+ }
41703
+ }
41704
+ } catch {
41705
+ }
41706
+ }
41707
+ return out.sort((a, b2) => a.name.localeCompare(b2.name));
41708
+ }
41709
+ async create(name) {
41710
+ const existing = await this.list();
41711
+ if (existing.some((p2) => p2.name === name)) {
41712
+ throw new Error(`project "${name}" already exists / \u5DF2\u5B58\u5728`);
41713
+ }
41714
+ const usedPorts = new Set(existing.map((p2) => p2.port));
41715
+ let port = -1;
41716
+ for (let p2 = PROJECT_PORT_MIN; p2 <= PROJECT_PORT_MAX; p2++) {
41717
+ if (!usedPorts.has(p2)) {
41718
+ port = p2;
41719
+ break;
41720
+ }
41721
+ }
41722
+ if (port < 0) {
41723
+ throw new Error(
41724
+ `port pool exhausted / \u7AEF\u53E3\u6C60\u5DF2\u6EE1\uFF08${PROJECT_PORT_MIN}-${PROJECT_PORT_MAX}\uFF09\uFF0C\u5148\u5220\u4E00\u4E2A project \u91CA\u653E\u7AEF\u53E3`
41725
+ );
41726
+ }
41727
+ const meta = {
41728
+ name,
41729
+ port,
41730
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
41731
+ devCommand: DEFAULT_DEV_COMMAND
41732
+ };
41733
+ const validated = ProjectMetadataSchema.safeParse(meta);
41734
+ if (!validated.success) {
41735
+ throw new Error(`invalid name "${name}": ${validated.error.message}`);
41736
+ }
41737
+ const dir = this.projectDir(name);
41738
+ await import_node_fs29.promises.mkdir(dir, { recursive: true });
41739
+ await import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
41740
+ return meta;
41741
+ }
41742
+ async delete(name) {
41743
+ const dir = this.projectDir(name);
41744
+ await import_node_fs29.promises.rm(dir, { recursive: true, force: true });
41745
+ }
41746
+ async updatePort(name, newPort) {
41747
+ if (newPort < PROJECT_PORT_MIN || newPort > PROJECT_PORT_MAX) {
41748
+ throw new Error(
41749
+ `port range invalid: must be ${PROJECT_PORT_MIN}-${PROJECT_PORT_MAX}, got ${newPort}`
41750
+ );
41751
+ }
41752
+ const list = await this.list();
41753
+ const target = list.find((p2) => p2.name === name);
41754
+ if (!target) throw new Error(`project "${name}" not found / \u4E0D\u5B58\u5728`);
41755
+ const conflict = list.find((p2) => p2.name !== name && p2.port === newPort);
41756
+ if (conflict) {
41757
+ throw new Error(`port ${newPort} already used / \u5DF2\u88AB project "${conflict.name}" \u5360\u7528`);
41758
+ }
41759
+ const updated = { ...target, port: newPort };
41760
+ await import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
41761
+ return updated;
41762
+ }
41763
+ };
41764
+
41765
+ // src/app-builder/dev-server-supervisor.ts
41766
+ var import_node_child_process7 = require("child_process");
41767
+ var import_node_events2 = require("events");
41768
+ var DevServerSupervisor = class extends import_node_events2.EventEmitter {
41769
+ running = /* @__PURE__ */ new Map();
41770
+ // key: sessionId
41771
+ logger = { warn: () => {
41772
+ }, info: () => {
41773
+ } };
41774
+ /** daemon entry 启动后注入 logger,让 child 输出走 clawd.log */
41775
+ setLogger(logger) {
41776
+ this.logger = logger;
41777
+ }
41778
+ startForSession(args) {
41779
+ if (this.running.has(args.sessionId)) return;
41780
+ const augmentedPath = [
41781
+ process.env.PATH ?? "",
41782
+ "/opt/homebrew/bin",
41783
+ "/usr/local/bin",
41784
+ `${process.env.HOME}/.nvm/versions/node`,
41785
+ `${process.env.HOME}/.local/share/pnpm`
41786
+ ].filter(Boolean).join(":");
41787
+ const env = {
41788
+ ...process.env,
41789
+ PATH: augmentedPath,
41790
+ CLAWD_TUNNEL_HOST: args.tunnelHost ?? "",
41791
+ CLAWD_PREVIEW_PORT: String(args.port)
41792
+ };
41793
+ const cmd = args.devCommand.replace(/\$CLAWD_PREVIEW_PORT/g, String(args.port));
41794
+ this.logger.info("dev-server.start", {
41795
+ projectName: args.projectName,
41796
+ port: args.port,
41797
+ cwd: args.cwd,
41798
+ sessionId: args.sessionId,
41799
+ tunnelHost: args.tunnelHost,
41800
+ devCommand: cmd
41801
+ });
41802
+ const child = (0, import_node_child_process7.spawn)("sh", ["-c", cmd], { cwd: args.cwd, env, stdio: "pipe" });
41803
+ const entry = { ...args, process: child };
41804
+ this.running.set(args.sessionId, entry);
41805
+ child.stdout?.on("data", (chunk) => {
41806
+ this.logger.info("dev-server.stdout", {
41807
+ projectName: args.projectName,
41808
+ port: args.port,
41809
+ chunk: chunk.toString().trimEnd()
41810
+ });
41811
+ });
41812
+ child.stderr?.on("data", (chunk) => {
41813
+ this.logger.warn("dev-server.stderr", {
41814
+ projectName: args.projectName,
41815
+ port: args.port,
41816
+ chunk: chunk.toString().trimEnd()
41817
+ });
41818
+ });
41819
+ child.on("error", (err) => {
41820
+ this.running.delete(args.sessionId);
41821
+ this.logger.warn("dev-server.spawn-failed", {
41822
+ projectName: args.projectName,
41823
+ port: args.port,
41824
+ error: err.message,
41825
+ code: err.code
41826
+ });
41827
+ this.emit("devServer:failed", {
41828
+ sessionId: args.sessionId,
41829
+ projectName: args.projectName,
41830
+ port: args.port,
41831
+ exitCode: null
41832
+ });
41833
+ });
41834
+ child.on("exit", (code) => {
41835
+ this.running.delete(args.sessionId);
41836
+ this.logger.info("dev-server.exit", { projectName: args.projectName, port: args.port, code });
41837
+ if (code !== 0 && code !== null) {
41838
+ this.emit("devServer:failed", {
41839
+ sessionId: args.sessionId,
41840
+ projectName: args.projectName,
41841
+ port: args.port,
41842
+ exitCode: code
41843
+ });
41844
+ }
41845
+ });
41846
+ }
41847
+ stopForSession(sessionId) {
41848
+ const entry = this.running.get(sessionId);
41849
+ if (!entry) return Promise.resolve();
41850
+ return new Promise((resolve6) => {
41851
+ entry.process.once("exit", () => {
41852
+ this.running.delete(sessionId);
41853
+ resolve6();
41854
+ });
41855
+ entry.process.kill("SIGTERM");
41856
+ });
41857
+ }
41858
+ async stopForProject(projectName) {
41859
+ const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41860
+ if (!entry) return;
41861
+ await this.stopForSession(entry.sessionId);
41862
+ }
41863
+ async restartForProjectAt(args) {
41864
+ await this.stopForSession(args.sessionId);
41865
+ this.startForSession({
41866
+ sessionId: args.sessionId,
41867
+ projectName: args.projectName,
41868
+ port: args.newPort,
41869
+ cwd: args.cwd,
41870
+ tunnelHost: args.tunnelHost,
41871
+ devCommand: args.devCommand
41872
+ });
41873
+ }
41874
+ isRunning(projectName) {
41875
+ return [...this.running.values()].some((e) => e.projectName === projectName);
41876
+ }
41877
+ boundSessionId(projectName) {
41878
+ const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41879
+ return entry?.sessionId ?? null;
41880
+ }
41881
+ boundEntry(projectName) {
41882
+ const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41883
+ if (!entry) return null;
41884
+ return {
41885
+ sessionId: entry.sessionId,
41886
+ cwd: entry.cwd,
41887
+ port: entry.port,
41888
+ tunnelHost: entry.tunnelHost,
41889
+ devCommand: entry.devCommand
41890
+ };
41891
+ }
41892
+ };
41893
+
41755
41894
  // src/handlers/method-grants.ts
41756
41895
  var ADMIN_ANY = {
41757
41896
  kind: "fixed",
@@ -41768,22 +41907,23 @@ var METHOD_GRANT_MAP = {
41768
41907
  // "查 capabilityManager 拿 caller 的 guest capability"。
41769
41908
  "whoami": { kind: "public" },
41770
41909
  // ---- capability platform(admin-only) ----
41771
- // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal-cap:get / capability:issue /
41772
- // bindPeer 已下线(personal token / 邀请码全链路删除)。capability:list / delete 保留供调试 +
41773
- // 撤销级联 + 清旧 per-peer cap。
41910
+ // global personal token (2026-05-25): capability:issue / bindPeer 已下线;
41911
+ // personal capability daemon 启动时自动创建,personal-cap:get owner-only token + shareBaseUrl
41774
41912
  "capability:list": ADMIN_ANY,
41775
41913
  "capability:delete": ADMIN_ANY,
41914
+ "personal-cap:get": ADMIN_ANY,
41776
41915
  // ---- inbox: channel-based IM (capability platform v3) ----
41777
41916
  // 三条都 'public' — capability 本身就是授权凭证. handler 内额外校验
41778
41917
  // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
41779
41918
  "inbox:list": { kind: "public" },
41780
41919
  "inbox:markRead": { kind: "public" },
41781
41920
  "inbox:postMessage": { kind: "public" },
41782
- // ---- received-capability:* (联系人列表,视角 B 双向授权模型) ----
41783
- // 飞书统一身份 Phase 2 (spec 决策 #12): owner-only add / guest-only autoAttach 已删除
41784
- // (hub:connect 落同一 store + 自动反向替代)。list / remove 保留作联系人读 / 删。
41921
+ // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
41922
+ // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
41785
41923
  "received-capability:list": ADMIN_ANY,
41924
+ "received-capability:add": ADMIN_ANY,
41786
41925
  "received-capability:remove": ADMIN_ANY,
41926
+ "received-capability:autoAttach": { kind: "public" },
41787
41927
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
41788
41928
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
41789
41929
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
@@ -41881,15 +42021,14 @@ var METHOD_GRANT_MAP = {
41881
42021
  // guest-self:guest 在自己 daemon 上触发安装与更新(无持久化 subscription 概念)
41882
42022
  "extension.install-from-channel": ADMIN_ANY,
41883
42023
  "extension.update-from-channel": ADMIN_ANY,
41884
- // ---- auth:* 飞书统一身份 Phase 1(owner-only) ----
41885
- // 登录/登出/查身份都是 owner 本机操作,guest 不可调
41886
- "auth:login:start": ADMIN_ANY,
41887
- "auth:getIdentity": ADMIN_ANY,
41888
- "auth:logout": ADMIN_ANY,
41889
- // ---- hub:* 飞书统一身份 Phase 2(owner-only) ----
41890
- // hub 目录 / 连接都是 owner 本机操作(用 owner 的 ttcJwt 找中心 server),guest 不可调
41891
- "hub:list": ADMIN_ANY,
41892
- "hub:connect": ADMIN_ANY
42024
+ // ---- app-builder Project picker(spec 2026-06-01-app-builder-project-picker-design) ----
42025
+ // owner-only:picker UI 给 owner 管理本机 build 中的 project(fs + 端口分配)。
42026
+ // 即使 persona-app-builder 是 PreinstalledPersona,project 管理也属本机 owner 行为,
42027
+ // 不暴露给 guest(远端接入者)。
42028
+ "appBuilder:listProjects": ADMIN_ANY,
42029
+ "appBuilder:createProject": ADMIN_ANY,
42030
+ "appBuilder:deleteProject": ADMIN_ANY,
42031
+ "appBuilder:updateProjectPort": ADMIN_ANY
41893
42032
  };
41894
42033
  function computeGrantForFrame(method, frame) {
41895
42034
  const rule = METHOD_GRANT_MAP[method];
@@ -41905,12 +42044,12 @@ function computeGrantForFrame(method, frame) {
41905
42044
  }
41906
42045
 
41907
42046
  // src/extension/runtime.ts
41908
- var import_node_child_process7 = require("child_process");
41909
- var import_node_path41 = __toESM(require("path"), 1);
42047
+ var import_node_child_process8 = require("child_process");
42048
+ var import_node_path42 = __toESM(require("path"), 1);
41910
42049
  var import_promises10 = require("timers/promises");
41911
42050
 
41912
42051
  // src/extension/port-allocator.ts
41913
- var import_node_net = __toESM(require("net"), 1);
42052
+ var import_node_net2 = __toESM(require("net"), 1);
41914
42053
  var PortExhaustedError = class extends Error {
41915
42054
  constructor(min, max) {
41916
42055
  super(`no free port in [${min},${max}]`);
@@ -41923,7 +42062,7 @@ var PortExhaustedError = class extends Error {
41923
42062
  };
41924
42063
  function probe(port) {
41925
42064
  return new Promise((resolve6) => {
41926
- const srv = import_node_net.default.createServer();
42065
+ const srv = import_node_net2.default.createServer();
41927
42066
  srv.once("error", () => resolve6(false));
41928
42067
  srv.once("listening", () => {
41929
42068
  srv.close(() => resolve6(true));
@@ -42007,13 +42146,13 @@ var Runtime = class {
42007
42146
  /\$CLAWOS_EXT_PORT/g,
42008
42147
  String(port)
42009
42148
  );
42010
- const dir = import_node_path41.default.join(this.root, extId);
42149
+ const dir = import_node_path42.default.join(this.root, extId);
42011
42150
  const env = {
42012
42151
  ...process.env,
42013
42152
  CLAWOS_EXT_PORT: String(port),
42014
42153
  CLAWOS_EXT_ID: extId
42015
42154
  };
42016
- const child = (0, import_node_child_process7.spawn)("sh", ["-c", cmd], {
42155
+ const child = (0, import_node_child_process8.spawn)("sh", ["-c", cmd], {
42017
42156
  cwd: dir,
42018
42157
  env,
42019
42158
  stdio: ["ignore", "pipe", "pipe"],
@@ -42119,7 +42258,7 @@ ${handle.stderrTail}`
42119
42258
 
42120
42259
  // src/extension/published-channels.ts
42121
42260
  var import_promises11 = __toESM(require("fs/promises"), 1);
42122
- var import_node_path42 = __toESM(require("path"), 1);
42261
+ var import_node_path43 = __toESM(require("path"), 1);
42123
42262
  init_zod();
42124
42263
  var PublishedChannelsError = class extends Error {
42125
42264
  constructor(code, message) {
@@ -42218,7 +42357,7 @@ var PublishedChannelStore = class {
42218
42357
  )
42219
42358
  };
42220
42359
  const tmp = `${this.filePath}.tmp`;
42221
- await import_promises11.default.mkdir(import_node_path42.default.dirname(this.filePath), { recursive: true });
42360
+ await import_promises11.default.mkdir(import_node_path43.default.dirname(this.filePath), { recursive: true });
42222
42361
  await import_promises11.default.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
42223
42362
  await import_promises11.default.rename(tmp, this.filePath);
42224
42363
  }
@@ -42226,7 +42365,7 @@ var PublishedChannelStore = class {
42226
42365
 
42227
42366
  // src/extension/bundle-cache.ts
42228
42367
  var import_promises12 = __toESM(require("fs/promises"), 1);
42229
- var import_node_path43 = __toESM(require("path"), 1);
42368
+ var import_node_path44 = __toESM(require("path"), 1);
42230
42369
  var BundleCache = class {
42231
42370
  constructor(rootDir) {
42232
42371
  this.rootDir = rootDir;
@@ -42235,14 +42374,14 @@ var BundleCache = class {
42235
42374
  /** Atomic write: stage tmp → rename. Caller passes the hex sha256. */
42236
42375
  async write(snapshotHash, buffer) {
42237
42376
  await import_promises12.default.mkdir(this.rootDir, { recursive: true });
42238
- const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42377
+ const file = import_node_path44.default.join(this.rootDir, `${snapshotHash}.zip`);
42239
42378
  const tmp = `${file}.tmp`;
42240
42379
  await import_promises12.default.writeFile(tmp, buffer, { mode: 384 });
42241
42380
  await import_promises12.default.rename(tmp, file);
42242
42381
  }
42243
42382
  /** Returns the bundle bytes, or null when the file doesn't exist. */
42244
42383
  async read(snapshotHash) {
42245
- const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42384
+ const file = import_node_path44.default.join(this.rootDir, `${snapshotHash}.zip`);
42246
42385
  try {
42247
42386
  return await import_promises12.default.readFile(file);
42248
42387
  } catch (e) {
@@ -42252,7 +42391,7 @@ var BundleCache = class {
42252
42391
  }
42253
42392
  /** Idempotent — missing file is not an error. */
42254
42393
  async delete(snapshotHash) {
42255
- const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42394
+ const file = import_node_path44.default.join(this.rootDir, `${snapshotHash}.zip`);
42256
42395
  await import_promises12.default.rm(file, { force: true });
42257
42396
  }
42258
42397
  };
@@ -42261,7 +42400,7 @@ var BundleCache = class {
42261
42400
  async function startDaemon(config) {
42262
42401
  const logger = createLogger({
42263
42402
  level: config.logLevel,
42264
- file: import_node_path44.default.join(config.dataDir, "clawd.log")
42403
+ file: import_node_path45.default.join(config.dataDir, "clawd.log")
42265
42404
  });
42266
42405
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
42267
42406
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -42279,11 +42418,8 @@ async function startDaemon(config) {
42279
42418
  } else if (config.tunnel) {
42280
42419
  resolvedAuthToken = authFile.token;
42281
42420
  }
42282
- const ownerIdentityStore = new OwnerIdentityStore(config.dataDir);
42283
- const feishuIdentity = ownerIdentityStore.read();
42284
- let feishuActive = feishuIdentity !== null;
42285
- let ownerPrincipalId = feishuIdentity?.identity.unionId ?? authFile.ownerPrincipalId;
42286
- let ownerDisplayName = feishuIdentity?.identity.displayName ?? loadOwnerDisplayName(config.dataDir);
42421
+ const ownerPrincipalId = authFile.ownerPrincipalId;
42422
+ const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
42287
42423
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
42288
42424
  const inboxStore = new InboxStore(config.dataDir);
42289
42425
  const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
@@ -42293,6 +42429,8 @@ async function startDaemon(config) {
42293
42429
  receivedCapabilityStore.load();
42294
42430
  const capabilityStore = new CapabilityStore(config.dataDir);
42295
42431
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
42432
+ const personalCapability = loadOrCreatePersonalCapability({ dataDir: config.dataDir });
42433
+ capabilityRegistry.upsertCapability(personalCapability.capability);
42296
42434
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
42297
42435
  onDeleted: (cap) => {
42298
42436
  const deletedAt = Date.now();
@@ -42318,36 +42456,15 @@ async function startDaemon(config) {
42318
42456
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
42319
42457
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
42320
42458
  expectedToken: resolvedAuthToken,
42321
- authenticate: async (t, _selfPrincipalId, _selfDisplayName, selfUrl) => {
42322
- const result = await authenticate(t, {
42459
+ authenticate: (t, selfPrincipalId, selfDisplayName) => {
42460
+ return authenticate(t, {
42323
42461
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
42324
42462
  ownerPrincipalId,
42325
42463
  ownerDisplayName,
42326
- // 飞书 gate(spec 决策 #9):未激活飞书身份时不接受 guest 入站(不注入 introspect
42327
- // → 一律 BAD_TOKEN)。已激活时 hub 用自己的 ttcJwt 做验票方认证;
42328
- // ttcJwt 每次验票时从 store 现读(热切换后立即用新身份)。
42329
- ...feishuActive ? {
42330
- introspectConnectToken: async (token) => {
42331
- const record = ownerIdentityStore.read();
42332
- if (!record) return { active: false };
42333
- return centralIntrospect({
42334
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42335
- hubTtcJwt: record.ttcToken,
42336
- token
42337
- });
42338
- }
42339
- } : {}
42464
+ capabilityRegistry,
42465
+ selfPrincipalId,
42466
+ selfDisplayName
42340
42467
  });
42341
- if (result.ok && result.context.principal.kind === "guest") {
42342
- autoReverseContact({
42343
- store: receivedCapabilityStore,
42344
- broadcast: (frame) => wsServer?.broadcastToOwners(frame),
42345
- unionId: result.context.principal.id,
42346
- displayName: result.context.principal.displayName ?? result.context.principal.id,
42347
- selfUrl
42348
- });
42349
- }
42350
- return result;
42351
42468
  },
42352
42469
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
42353
42470
  buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
@@ -42370,7 +42487,7 @@ async function startDaemon(config) {
42370
42487
  const agents = new AgentsScanner();
42371
42488
  const history = new ClaudeHistoryReader();
42372
42489
  let transport = null;
42373
- const personaStore = new PersonaStore(import_node_path44.default.join(config.dataDir, "personas"));
42490
+ const personaStore = new PersonaStore(import_node_path45.default.join(config.dataDir, "personas"));
42374
42491
  const defaultsRoot = findDefaultsRoot();
42375
42492
  if (defaultsRoot) {
42376
42493
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -42387,7 +42504,7 @@ async function startDaemon(config) {
42387
42504
  getAdapter,
42388
42505
  historyReader: history,
42389
42506
  dataDir: config.dataDir,
42390
- personaRoot: import_node_path44.default.join(config.dataDir, "personas"),
42507
+ personaRoot: import_node_path45.default.join(config.dataDir, "personas"),
42391
42508
  personaStore,
42392
42509
  ownerDisplayName,
42393
42510
  ownerPrincipalId,
@@ -42411,10 +42528,10 @@ async function startDaemon(config) {
42411
42528
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
42412
42529
  attachmentGroup: {
42413
42530
  onFileEdit: (input) => {
42414
- const absPath = import_node_path44.default.isAbsolute(input.relPath) ? input.relPath : import_node_path44.default.join(input.cwd, input.relPath);
42531
+ const absPath = import_node_path45.default.isAbsolute(input.relPath) ? input.relPath : import_node_path45.default.join(input.cwd, input.relPath);
42415
42532
  let size = 0;
42416
42533
  try {
42417
- size = import_node_fs29.default.statSync(absPath).size;
42534
+ size = import_node_fs30.default.statSync(absPath).size;
42418
42535
  } catch (err) {
42419
42536
  logger.warn("attachment.onFileEdit stat failed", {
42420
42537
  sessionId: input.sessionId,
@@ -42497,33 +42614,23 @@ async function startDaemon(config) {
42497
42614
  }
42498
42615
  return `http://${config.host}:${config.port}`;
42499
42616
  };
42500
- const reportHubUrl = async () => {
42501
- if (!feishuActive || !currentTunnelUrl) return;
42502
- const record = ownerIdentityStore.read();
42503
- if (!record) return;
42504
- try {
42505
- await centralUpsertHub({
42506
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42507
- ttcJwt: record.ttcToken,
42508
- url: currentTunnelUrl,
42509
- name: ownerDisplayName
42510
- });
42511
- logger.info("hub url reported to central server", { url: currentTunnelUrl });
42512
- } catch (err) {
42513
- logger.warn("hub url report failed (best-effort)", { err: err.message });
42514
- }
42515
- };
42516
42617
  const extensionRuntime = new Runtime({ root: extensionsRoot() });
42517
42618
  const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
42518
42619
  await publishedChannelStore.load();
42519
42620
  const bundleCache = new BundleCache(bundleCacheRoot());
42520
- const loginFlow = new LoginFlow({
42521
- store: ownerIdentityStore,
42522
- fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
42523
- ttcAuthBase: TTC_HOSTS.auth,
42524
- getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`
42525
- });
42526
- const makeHandlers = () => buildMethodHandlers({
42621
+ const appBuilderStore = new ProjectStore(
42622
+ import_node_path45.default.join(config.dataDir, "personas/persona-app-builder")
42623
+ );
42624
+ const devServerSupervisor = new DevServerSupervisor();
42625
+ devServerSupervisor.setLogger(logger);
42626
+ const appBuilderPortRange = [];
42627
+ for (let p2 = PROJECT_PORT_MIN; p2 <= PROJECT_PORT_MAX; p2++) appBuilderPortRange.push(p2);
42628
+ const effectivePreviewPorts = Array.from(
42629
+ /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
42630
+ );
42631
+ const deleteSessionsByProject = async (_name) => {
42632
+ };
42633
+ const handlers = buildMethodHandlers({
42527
42634
  manager,
42528
42635
  workspace,
42529
42636
  skills,
@@ -42562,12 +42669,17 @@ async function startDaemon(config) {
42562
42669
  // 'persona/<pid>/owner',default 走 'default'。
42563
42670
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
42564
42671
  // guest path guard:candidate 必须在 personaRoot 子树下
42565
- personaRoot: import_node_path44.default.join(config.dataDir, "personas")
42672
+ personaRoot: import_node_path45.default.join(config.dataDir, "personas")
42566
42673
  },
42567
42674
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
42568
- personaRoot: import_node_path44.default.join(config.dataDir, "personas"),
42675
+ personaRoot: import_node_path45.default.join(config.dataDir, "personas"),
42569
42676
  // capability:list / delete handler 依赖
42570
42677
  capabilityManager,
42678
+ // personal-cap:get 返回的 shareBaseUrl 用此 base URL:
42679
+ // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
42680
+ getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
42681
+ // global personal token (2026-05-25): personal-cap:get handler 直接返回此 token + cap
42682
+ getPersonalCapability: () => personalCapability,
42571
42683
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
42572
42684
  ownerDisplayName,
42573
42685
  // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
@@ -42579,55 +42691,24 @@ async function startDaemon(config) {
42579
42691
  // cascade 接 store (list 统计 deletedInboxEvents).
42580
42692
  inboxManager,
42581
42693
  inboxStore,
42582
- // 联系人列表 store(hub:connect 落同一 store;视角 B 双向授权模型)
42694
+ // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
42583
42695
  receivedCapabilityStore,
42584
- // received-capability:removed broadcast;复用 capability:tokenIssued 同款通路
42696
+ // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
42585
42697
  broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
42698
+ // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
42699
+ connectRemote,
42586
42700
  // extension runtime (v1: local-mode only). Instance owned by daemon top
42587
42701
  // level so stopAll() runs in the shutdown hook below.
42588
42702
  extensionRuntime,
42589
42703
  // extension sharing v1 — owner-side published-channels store.
42590
42704
  publishedChannelStore,
42591
42705
  bundleCache,
42592
- // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
42593
- feishuAuth: { loginFlow, ownerIdentityStore },
42594
- // 飞书统一身份 Phase 2:hub 目录 + 连接(中心 server 代理)。
42595
- // exchange/listHubs 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
42596
- // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
42597
- // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
42598
- feishuHub: {
42599
- store: receivedCapabilityStore,
42600
- exchange: async (hubId) => {
42601
- const record = ownerIdentityStore.read();
42602
- if (!record) {
42603
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "hub:connect requires feishu login");
42604
- }
42605
- return centralExchange({
42606
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42607
- ttcJwt: record.ttcToken,
42608
- hubId
42609
- });
42610
- },
42611
- listHubs: async () => {
42612
- const record = ownerIdentityStore.read();
42613
- if (!record) {
42614
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "hub:list requires feishu login");
42615
- }
42616
- return centralListHubs({
42617
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42618
- ttcJwt: record.ttcToken
42619
- });
42620
- },
42621
- connectRemote,
42622
- broadcast: (frame) => wsServer?.broadcastToOwners(frame),
42623
- selfPrincipalId: () => ownerPrincipalId,
42624
- selfDisplayName: () => ownerDisplayName,
42625
- // Phase 2 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连 hub 时
42626
- // 透传让 hub 反向加我为联系人。无 tunnel(仅本机 ws)→ null,hub 不自动反向(不造假数据)。
42627
- selfUrl: () => currentTunnelUrl
42628
- }
42706
+ // app-builder
42707
+ appBuilderStore,
42708
+ deleteSessionsByProject,
42709
+ devServerLifecycle: devServerSupervisor,
42710
+ logger
42629
42711
  });
42630
- let handlers = makeHandlers();
42631
42712
  const authResolver = new AuthContextResolver({
42632
42713
  ownerToken: resolvedAuthToken
42633
42714
  });
@@ -42653,30 +42734,8 @@ async function startDaemon(config) {
42653
42734
  },
42654
42735
  // POST /extensions/import lands in ~/.clawd/extensions/<id>/
42655
42736
  extensionsRoot: () => extensionsRoot(),
42656
- // 飞书登录 loopback 回调(热切换,2026-06-01 拍板):成功落盘后——
42657
- // 1. 进程内实时切换身份(let binding + 值快照持有者重建)
42658
- // 2. 广播 auth:login:done 给在线 owner 连接(UI 即时反馈)
42659
- // 3. 踢掉所有 ws 连接(在线连接的 ctx 是旧身份)→ UI 自动重连 → 新身份 ctx →
42660
- // People 立即解锁,不需要重启 daemon
42661
- // 失败则广播 auth:login:failed。
42662
- feishuLogin: {
42663
- handleLoginCallback: async (params) => {
42664
- const result = await loginFlow.handleCallback(params);
42665
- if (result.ok) {
42666
- feishuActive = true;
42667
- ownerPrincipalId = result.identity.unionId;
42668
- ownerDisplayName = result.identity.displayName;
42669
- handlers = makeHandlers();
42670
- wsServer?.setOwnerPrincipalId(result.identity.unionId);
42671
- wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
42672
- setImmediate(() => wsServer?.closeAllClients());
42673
- void reportHubUrl();
42674
- } else {
42675
- wsServer?.broadcastToOwners({ type: "auth:login:failed", reason: result.reason });
42676
- }
42677
- return result;
42678
- }
42679
- }
42737
+ // app-builder build 模式:/preview/<port>/ 反代 dev server 的端口白名单(见 spec §三.2)
42738
+ previewPorts: effectivePreviewPorts
42680
42739
  });
42681
42740
  wsServer = new LocalWsServer({
42682
42741
  host: config.host,
@@ -42715,6 +42774,8 @@ async function startDaemon(config) {
42715
42774
  },
42716
42775
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
42717
42776
  httpRequestHandler: httpRouter,
42777
+ // app-builder build 模式:/preview/<port>/ HMR websocket upgrade 转发的端口白名单(同 http-router 配置)
42778
+ previewPorts: effectivePreviewPorts,
42718
42779
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
42719
42780
  // daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
42720
42781
  // 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
@@ -42759,12 +42820,6 @@ async function startDaemon(config) {
42759
42820
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
42760
42821
  const handler = handlers[type];
42761
42822
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
42762
- if (!feishuActive && FEISHU_GATED_METHODS.includes(type)) {
42763
- throw new ClawdError(
42764
- ERROR_CODES.FEISHU_LOGIN_REQUIRED,
42765
- `${type} requires feishu login (People/remote identity)`
42766
- );
42767
- }
42768
42823
  const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
42769
42824
  const verdict = computeGrantForFrame(type, frame);
42770
42825
  if (verdict.kind === "check") {
@@ -42837,12 +42892,13 @@ async function startDaemon(config) {
42837
42892
  stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url, tunnelError: void 0 };
42838
42893
  stateMgr.write(stateSnapshot);
42839
42894
  currentTunnelUrl = r.url;
42895
+ wss.broadcastAll({ type: "tunnel:ready", url: r.url, subdomain: r.subdomain });
42840
42896
  const connectUrl = resolvedAuthToken ? `${r.url}#token=${resolvedAuthToken}` : r.url;
42841
42897
  const lines = [
42842
42898
  `Tunnel: ${r.url}`,
42843
42899
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
42844
- `Frpc config: ${import_node_path44.default.join(config.dataDir, "frpc.toml")}`,
42845
- `Frpc log: ${import_node_path44.default.join(config.dataDir, "frpc.log")}`
42900
+ `Frpc config: ${import_node_path45.default.join(config.dataDir, "frpc.toml")}`,
42901
+ `Frpc log: ${import_node_path45.default.join(config.dataDir, "frpc.log")}`
42846
42902
  ];
42847
42903
  const width = Math.max(...lines.map((l) => l.length));
42848
42904
  const bar = "\u2550".repeat(width + 4);
@@ -42855,11 +42911,10 @@ ${bar}
42855
42911
 
42856
42912
  `);
42857
42913
  try {
42858
- const connectPath = import_node_path44.default.join(config.dataDir, "connect.txt");
42859
- import_node_fs29.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
42914
+ const connectPath = import_node_path45.default.join(config.dataDir, "connect.txt");
42915
+ import_node_fs30.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
42860
42916
  } catch {
42861
42917
  }
42862
- void reportHubUrl();
42863
42918
  } catch (err) {
42864
42919
  const tunnelError = err?.message ?? String(err);
42865
42920
  try {
@@ -42923,9 +42978,9 @@ ${bar}
42923
42978
  };
42924
42979
  }
42925
42980
  function migrateDropPersonsDir(dataDir) {
42926
- const dir = import_node_path44.default.join(dataDir, "persons");
42981
+ const dir = import_node_path45.default.join(dataDir, "persons");
42927
42982
  try {
42928
- import_node_fs29.default.rmSync(dir, { recursive: true, force: true });
42983
+ import_node_fs30.default.rmSync(dir, { recursive: true, force: true });
42929
42984
  } catch {
42930
42985
  }
42931
42986
  }