@clawos-dev/clawd 0.2.263 → 0.2.264

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 (2) hide show
  1. package/dist/cli.cjs +228 -227
  2. package/package.json +4 -3
package/dist/cli.cjs CHANGED
@@ -54704,7 +54704,11 @@ function buildSessionHandlers(deps) {
54704
54704
  const args = SessionSendArgs.parse(frame);
54705
54705
  ensureSessionAccess(ctx, args.sessionId, "send");
54706
54706
  const sender = ctx ? principalToSender(ctx.principal) : principalToSender(
54707
- makeOwnerPrincipal(ownerPrincipalId, deps.ownerDisplayName, deps.ownerFeishuUnionId)
54707
+ makeOwnerPrincipal(
54708
+ ownerPrincipalId,
54709
+ deps.getOwnerIdentity().displayName,
54710
+ deps.getOwnerIdentity().feishuUnionId
54711
+ )
54708
54712
  );
54709
54713
  const { broadcast } = manager.send(args, sender);
54710
54714
  return { response: { type: "session:send", ok: true }, broadcast };
@@ -55703,14 +55707,15 @@ function buildContactHandlers(deps) {
55703
55707
  contact,
55704
55708
  exchangeToken: deps.exchangeToken
55705
55709
  });
55710
+ const identity = deps.getOwnerIdentity();
55706
55711
  return {
55707
55712
  response: {
55708
55713
  type: "contact:handshake:ok",
55709
55714
  owner: {
55710
55715
  id: deps.ownerPrincipalId,
55711
- ownerId: deps.ownerId,
55712
- provider: deps.ownerProvider,
55713
- displayName: deps.ownerDisplayName
55716
+ ownerId: identity.ownerId,
55717
+ provider: identity.provider,
55718
+ displayName: identity.displayName
55714
55719
  },
55715
55720
  grants: ctx.grants
55716
55721
  }
@@ -55772,6 +55777,7 @@ function buildWhoamiHandler(deps) {
55772
55777
  if (!ctx) {
55773
55778
  throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
55774
55779
  }
55780
+ const identity = deps.getOwnerIdentity();
55775
55781
  if (ctx.tokenKind === "connect") {
55776
55782
  return {
55777
55783
  response: {
@@ -55781,15 +55787,15 @@ function buildWhoamiHandler(deps) {
55781
55787
  };
55782
55788
  }
55783
55789
  const owner = {
55784
- ...makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName),
55785
- ownerId: deps.ownerId,
55786
- provider: deps.ownerProvider
55790
+ ...makeOwnerPrincipal(deps.ownerPrincipalId, identity.displayName),
55791
+ ownerId: identity.ownerId,
55792
+ provider: identity.provider
55787
55793
  };
55788
55794
  let capability;
55789
55795
  if (ctx.principal.kind === "owner") {
55790
55796
  capability = {
55791
55797
  id: deps.ownerPrincipalId,
55792
- displayName: deps.ownerDisplayName,
55798
+ displayName: identity.displayName,
55793
55799
  grants: ctx.grants,
55794
55800
  issuedAt: 0,
55795
55801
  usedCount: 0
@@ -58174,8 +58180,8 @@ function buildAppBuilderHandlers(deps) {
58174
58180
  const ownerSender = principalToSender(
58175
58181
  makeOwnerPrincipal(
58176
58182
  deps.ownerPrincipalId,
58177
- deps.ownerDisplayName ?? "Owner",
58178
- deps.ownerFeishuUnionId
58183
+ deps.getOwnerIdentity().displayName || "Owner",
58184
+ deps.getOwnerIdentity().feishuUnionId
58179
58185
  )
58180
58186
  );
58181
58187
  const { broadcast } = sessionManager.send({ sessionId, text }, ownerSender);
@@ -58568,9 +58574,7 @@ function buildMethodHandlers(deps) {
58568
58574
  broadcast: deps.broadcastToOwners,
58569
58575
  now: () => Date.now(),
58570
58576
  ownerPrincipalId: deps.ownerPrincipalId,
58571
- ownerDisplayName: deps.ownerDisplayName,
58572
- ownerId: deps.ownerId,
58573
- ownerProvider: deps.ownerProvider,
58577
+ getOwnerIdentity: deps.getOwnerIdentity,
58574
58578
  exchangeToken: deps.exchangeToken
58575
58579
  }),
58576
58580
  ...buildContactRemoteAccessHandlers({
@@ -58584,10 +58588,8 @@ function buildMethodHandlers(deps) {
58584
58588
  forwardToPeer: (a) => forwardPeerExecToPeer(a)
58585
58589
  }),
58586
58590
  whoami: buildWhoamiHandler({
58587
- ownerDisplayName: deps.ownerDisplayName,
58588
58591
  ownerPrincipalId: deps.ownerPrincipalId,
58589
- ownerId: deps.ownerId,
58590
- ownerProvider: deps.ownerProvider,
58592
+ getOwnerIdentity: deps.getOwnerIdentity,
58591
58593
  personaStore: deps.personaStore
58592
58594
  }),
58593
58595
  ...buildFeishuAuthHandlers(deps.feishuAuth),
@@ -58608,8 +58610,7 @@ function buildMethodHandlers(deps) {
58608
58610
  devServerLifecycle: deps.devServerLifecycle,
58609
58611
  sessionManager: deps.manager,
58610
58612
  ownerPrincipalId: deps.ownerPrincipalId,
58611
- ownerDisplayName: deps.ownerDisplayName,
58612
- ownerFeishuUnionId: deps.ownerFeishuUnionId,
58613
+ getOwnerIdentity: deps.getOwnerIdentity,
58613
58614
  getTunnelUrl: deps.getTunnelUrl,
58614
58615
  broadcastProjectUpdated: deps.broadcastProjectUpdated,
58615
58616
  logger: deps.logger,
@@ -59357,7 +59358,6 @@ async function startDaemon(config) {
59357
59358
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
59358
59359
  ownerPrincipalId,
59359
59360
  ownerDisplayName,
59360
- ownerFeishuUnionId,
59361
59361
  // persona web 分享:访客 visitor token = daemon 自签 HMAC(无 aud),独立于飞书 connect token
59362
59362
  // 基建——secret 恒存在(auth-store backfill),故无条件注入。验签 ok → guest ctx(persona wildcard)。
59363
59363
  verifyVisitorToken: (t) => {
@@ -59868,217 +59868,219 @@ async function startDaemon(config) {
59868
59868
  const effectivePreviewPorts = Array.from(
59869
59869
  /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
59870
59870
  );
59871
- const makeHandlers = () => buildMethodHandlers({
59872
- // meta:methods 自省:飞书 gate getter + 最终 handler 表(下面还会 spread 进
59873
- // dispatch / shift-internal 两组,只看 buildMethodHandlers 内部表会误报死方法)
59874
- feishuActive: () => feishuActive,
59875
- getAllHandlers: () => handlers,
59876
- manager,
59877
- workspace,
59878
- skills,
59879
- agents,
59880
- // skills:list tool 路由:codex → app-server skills/list;缺省 claude → SkillsScanner(同步包成 Promise)。
59881
- getSkillsForTool: (tool, cwd) => tool === "codex" ? listCodexSkills(cwd) : Promise.resolve(skills.list({ cwd })),
59882
- history,
59883
- // history:read 按 session.tool 路由:codex → adapter.historyReader(CodexHistoryReader);缺省 claude。
59884
- getHistoryReader: (tool) => getAdapter(tool).historyReader ?? history,
59885
- observer,
59886
- getAdapter,
59887
- store,
59888
- personaManager,
59889
- personaRegistry,
59890
- getTunnelUrl: () => currentTunnelUrl,
59891
- // ready / info 帧的 mode = daemon CC spawn 模式('sdk' | 'tui')。UI 据此挂 XtermPanel +
59892
- // 订阅 session:pty / session:control;业务帧名两种 mode 完全一致,UI 业务订阅代码不变
59893
- mode: config.mode,
59894
- // file-sharing (spec §8):ready / info 帧把 httpBaseUrl + httpToken 下发给 UI。
59895
- // PR 2 阶段 httpToken 复用 owner WS token;noAuth 模式下为 null(UI 看到无 httpToken
59896
- // 时禁用文件 GET/POST,保持 1.0 行为兼容)。
59897
- getHttpBaseUrl,
59898
- httpToken: resolvedAuthToken,
59899
- // file-sharing attachment.* RPC。signUrl owner token 做 HMAC secret;group RPC
59900
- // 根据 sessionId 反查 scope 写盘。
59901
- //
59902
- // sessionStore / getSessionScope 都走 manager 的跨 scope 公开 APIfindOwnedSession /
59903
- // findOwnedSessionScope)—— 否则 default-only SessionStore 找不到 persona owner-mode
59904
- // session,attachment 整套 RPC 对 persona session 都会报 "session not found"。
59905
- // 详见 fix(daemon) #703:file-sharing v1 预存 wiring bug,#701 把 desktop 默认 --tunnel
59906
- // 后首次让 desktop 用户用上 file-sharing 才被暴露。
59907
- attachment: {
59908
- groupFileStore,
59909
- sessionStore: { read: (sid) => manager.findOwnedSession(sid) },
59871
+ const getOwnerIdentity = () => ({
59872
+ displayName: ownerDisplayName,
59873
+ ownerId,
59874
+ provider: ownerProvider,
59875
+ feishuUnionId: ownerFeishuUnionId
59876
+ });
59877
+ const handlers = {
59878
+ ...buildMethodHandlers({
59879
+ // meta:methods 自省:飞书 gate getter + 最终 handler 表(下面还会 spread 进
59880
+ // dispatch / shift-internal 两组,只看 buildMethodHandlers 内部表会误报死方法)
59881
+ feishuActive: () => feishuActive,
59882
+ getAllHandlers: () => handlers,
59883
+ manager,
59884
+ workspace,
59885
+ skills,
59886
+ agents,
59887
+ // skills:list 按 tool 路由:codex → app-server skills/list;缺省 claude → SkillsScanner(同步包成 Promise)。
59888
+ getSkillsForTool: (tool, cwd) => tool === "codex" ? listCodexSkills(cwd) : Promise.resolve(skills.list({ cwd })),
59889
+ history,
59890
+ // history:read 按 session.tool 路由:codex → adapter.historyReader(CodexHistoryReader);缺省 claude。
59891
+ getHistoryReader: (tool) => getAdapter(tool).historyReader ?? history,
59892
+ observer,
59893
+ getAdapter,
59894
+ store,
59895
+ personaManager,
59896
+ personaRegistry,
59897
+ getTunnelUrl: () => currentTunnelUrl,
59898
+ // ready / info 帧的 mode = daemon CC spawn 模式('sdk' | 'tui')。UI 据此挂 XtermPanel +
59899
+ // 订阅 session:pty / session:control;业务帧名两种 mode 完全一致,UI 业务订阅代码不变
59900
+ mode: config.mode,
59901
+ // file-sharing (spec §8):ready / info 帧把 httpBaseUrl + httpToken 下发给 UI。
59902
+ // PR 2 阶段 httpToken 复用 owner WS token;noAuth 模式下为 nullUI 看到无 httpToken
59903
+ // 时禁用文件 GET/POST,保持 1.0 行为兼容)。
59910
59904
  getHttpBaseUrl,
59911
- // HMAC sign secret = auth.json 唯一 token(authFile 恒非空,startDaemon 开头加载)。
59912
- getSignSecret: () => authFile.token,
59913
- // group RPC + sign 都用:根据 sessionId 反查 scope。owner-mode persona session 走
59914
- // 'persona/<pid>/owner',default 走 'default'。
59915
- getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
59916
- // guest path guard:candidate 必须在 personaRoot 子树或调用者自己的 user-dir 下
59905
+ httpToken: resolvedAuthToken,
59906
+ // file-sharing attachment.* RPC。signUrl 用 owner token 做 HMAC secret;group RPC
59907
+ // 根据 sessionId 反查 scope 写盘。
59908
+ //
59909
+ // sessionStore / getSessionScope 都走 manager 的跨 scope 公开 API(findOwnedSession /
59910
+ // findOwnedSessionScope)—— 否则 default-only SessionStore 找不到 persona owner-mode
59911
+ // session,attachment 整套 RPC 对 persona session 都会报 "session not found"。
59912
+ // 详见 fix(daemon) #703:file-sharing v1 预存 wiring bug,#701 把 desktop 默认 --tunnel
59913
+ // 后首次让 desktop 用户用上 file-sharing 才被暴露。
59914
+ attachment: {
59915
+ groupFileStore,
59916
+ sessionStore: { read: (sid) => manager.findOwnedSession(sid) },
59917
+ getHttpBaseUrl,
59918
+ // HMAC sign secret = auth.json 唯一 token(authFile 恒非空,startDaemon 开头加载)。
59919
+ getSignSecret: () => authFile.token,
59920
+ // group RPC + sign 都用:根据 sessionId 反查 scope。owner-mode persona session 走
59921
+ // 'persona/<pid>/owner',default 走 'default'。
59922
+ getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
59923
+ // guest path guard:candidate 必须在 personaRoot 子树或调用者自己的 user-dir 下
59924
+ personaRoot: import_node_path62.default.join(config.dataDir, "personas"),
59925
+ usersRoot
59926
+ },
59927
+ // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
59917
59928
  personaRoot: import_node_path62.default.join(config.dataDir, "personas"),
59918
- usersRoot
59919
- },
59920
- // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
59921
- personaRoot: import_node_path62.default.join(config.dataDir, "personas"),
59922
- // v2 多人 persona 隔离:handler 派生 guest user-dir 放行
59923
- usersRoot,
59924
- // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
59925
- ownerDisplayName,
59926
- // 决策 #16: whoami owner.id = 本机 deviceId(路由主键)+ ownerId/provider 归属人
59927
- ownerPrincipalId,
59928
- // per-turn sender §4.6:send handler 无 ctx 兜底 + AppBuilder publish injectUserText 需要
59929
- ownerFeishuUnionId,
59930
- ownerId,
59931
- ownerProvider,
59932
- personaStore,
59933
- // capability platform v3 inbox: handler manager (post/list/markRead),
59934
- // cascade 接 store (list 统计 deletedInboxEvents).
59935
- inboxManager,
59936
- inboxStore,
59937
- // 联系人列表 store(device:connect 出站 / contact:handshake 入站落同一 store)
59938
- contactStore,
59939
- // contact:handshake 换票回填(spec 2026-07-22):用 owner 当前 jwt 换 aud=对方设备 的票。
59940
- // 现读 ownerIdentityStore(热切换后用新身份);失败 / 未登录 → null → 保持空票。
59941
- exchangeToken: async (targetDeviceId) => {
59942
- const record = ownerIdentityStore.read();
59943
- if (!record) return null;
59944
- try {
59945
- const r = await centralExchange({
59946
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
59947
- jwt: record.ttcToken,
59948
- provider: record.identity.provider,
59949
- deviceId: targetDeviceId,
59950
- selfDeviceId: authFile.deviceId
59951
- });
59952
- return r.token;
59953
- } catch (e) {
59954
- logger.warn("contact:handshake exchange token failed", {
59955
- targetDeviceId,
59956
- err: e.message
59957
- });
59958
- return null;
59959
- }
59960
- },
59961
- // inbox:sendDm 用:sessionId → session 出身(复用 attachment 同款 findOwnedSessionScope)
59962
- getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
59963
- // contact:removed broadcast;复用 capability:tokenIssued 同款通路
59964
- broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
59965
- // extension runtime (v1: local-mode only). Instance owned by daemon top
59966
- // level so stopAll() runs in the shutdown hook below.
59967
- extensionRuntime,
59968
- // extension sharing v1 — owner-side published-channels store.
59969
- publishedChannelStore,
59970
- bundleCache,
59971
- // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
59972
- // deviceId(决策 #15):getIdentity 返回给 UI 拼"我的设备标识"
59973
- feishuAuth: {
59974
- loginFlow,
59975
- ownerIdentityStore,
59976
- deviceId: authFile.deviceId,
59977
- serverKeyStore,
59978
- // 退出登录:登录(feishuLogin.handleLoginCallback)在进程内设置的身份状态的对称逆操作。
59979
- // 核心是 feishuActive 翻 false——否则 dispatcher 飞书 gate(下方 feishuActive 判定,每帧
59980
- // 现读 let binding)退登后仍放行 gated 方法。一并把归属人重置回未登录初值 + 重建 handlers
59981
- // (deps 持 ownerId/displayName 值快照,whoami/出站自报身份用),与登录置位三件套对称。
59982
- // 不踢连:踢连是登录热切换换身份 ctx 的需要;退登只是降级,gate 每帧现读即时生效,
59983
- // 既有连接继续用,避免改动连接生命周期 / wire 契约。
59984
- onLogout: () => {
59985
- feishuActive = false;
59986
- ownerId = "";
59987
- ownerProvider = "";
59988
- ownerDisplayName = loadOwnerDisplayName(config.dataDir);
59989
- handlers = makeHandlers();
59990
- }
59991
- },
59992
- // 设备目录 + 连接(决策 #15:daemon 代理中心 server)。
59993
- // exchange/listDevices 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
59994
- // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
59995
- // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
59996
- feishuDevice: {
59997
- store: contactStore,
59998
- exchange: async (deviceId) => {
59929
+ // v2 多人 persona 隔离:handler 派生 guest user-dir 放行
59930
+ usersRoot,
59931
+ // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
59932
+ getOwnerIdentity,
59933
+ // 决策 #16: whoami owner.id = 本机 deviceId(路由主键,const,热切换不变)
59934
+ ownerPrincipalId,
59935
+ personaStore,
59936
+ // capability platform v3 inbox: handler 接 manager (post/list/markRead),
59937
+ // cascade store (list 统计 deletedInboxEvents).
59938
+ inboxManager,
59939
+ inboxStore,
59940
+ // 联系人列表 store(device:connect 出站 / contact:handshake 入站落同一 store)
59941
+ contactStore,
59942
+ // contact:handshake 换票回填(spec 2026-07-22):用 owner 当前 jwt 换 aud=对方设备 的票。
59943
+ // 现读 ownerIdentityStore(热切换后用新身份);失败 / 未登录 → null → 保持空票。
59944
+ exchangeToken: async (targetDeviceId) => {
59999
59945
  const record = ownerIdentityStore.read();
60000
- if (!record) {
60001
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
60002
- }
60003
- return centralExchange({
60004
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
60005
- jwt: record.ttcToken,
60006
- provider: record.identity.provider,
60007
- deviceId,
60008
- selfDeviceId: authFile.deviceId
60009
- });
59946
+ if (!record) return null;
59947
+ try {
59948
+ const r = await centralExchange({
59949
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
59950
+ jwt: record.ttcToken,
59951
+ provider: record.identity.provider,
59952
+ deviceId: targetDeviceId,
59953
+ selfDeviceId: authFile.deviceId
59954
+ });
59955
+ return r.token;
59956
+ } catch (e) {
59957
+ logger.warn("contact:handshake exchange token failed", {
59958
+ targetDeviceId,
59959
+ err: e.message
59960
+ });
59961
+ return null;
59962
+ }
60010
59963
  },
60011
- listDevices: async () => {
60012
- const record = ownerIdentityStore.read();
60013
- if (!record) {
60014
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
59964
+ // inbox:sendDm 用:sessionId session 出身(复用 attachment 同款 findOwnedSessionScope)
59965
+ getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
59966
+ // contact:removed broadcast;复用 capability:tokenIssued 同款通路
59967
+ broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
59968
+ // extension runtime (v1: local-mode only). Instance owned by daemon top
59969
+ // level so stopAll() runs in the shutdown hook below.
59970
+ extensionRuntime,
59971
+ // extension sharing v1 — owner-side published-channels store.
59972
+ publishedChannelStore,
59973
+ bundleCache,
59974
+ // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
59975
+ // deviceId(决策 #15):getIdentity 返回给 UI 拼"我的设备标识"
59976
+ feishuAuth: {
59977
+ loginFlow,
59978
+ ownerIdentityStore,
59979
+ deviceId: authFile.deviceId,
59980
+ serverKeyStore,
59981
+ // 退出登录:登录(feishuLogin.handleLoginCallback)在进程内设置的身份状态的对称逆操作。
59982
+ // 核心是 feishuActive 翻 false——否则 dispatcher 飞书 gate(下方 feishuActive 判定,每帧
59983
+ // 现读 let binding)退登后仍放行 gated 方法。一并把归属人重置回未登录初值 + 重建 handlers
59984
+ // (deps 持 ownerId/displayName 值快照,whoami/出站自报身份用),与登录置位三件套对称。
59985
+ // 不踢连:踢连是登录热切换换身份 ctx 的需要;退登只是降级,gate 每帧现读即时生效,
59986
+ // 既有连接继续用,避免改动连接生命周期 / wire 契约。
59987
+ onLogout: () => {
59988
+ feishuActive = false;
59989
+ ownerId = "";
59990
+ ownerProvider = "";
59991
+ ownerDisplayName = loadOwnerDisplayName(config.dataDir);
59992
+ }
59993
+ },
59994
+ // 设备目录 + 连接(决策 #15:daemon 代理中心 server)。
59995
+ // exchange/listDevices 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
59996
+ // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
59997
+ // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
59998
+ feishuDevice: {
59999
+ store: contactStore,
60000
+ exchange: async (deviceId) => {
60001
+ const record = ownerIdentityStore.read();
60002
+ if (!record) {
60003
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
60004
+ }
60005
+ return centralExchange({
60006
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
60007
+ jwt: record.ttcToken,
60008
+ provider: record.identity.provider,
60009
+ deviceId,
60010
+ selfDeviceId: authFile.deviceId
60011
+ });
60012
+ },
60013
+ listDevices: async () => {
60014
+ const record = ownerIdentityStore.read();
60015
+ if (!record) {
60016
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
60017
+ }
60018
+ return centralListDevices({
60019
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
60020
+ ttcJwt: record.ttcToken,
60021
+ provider: record.identity.provider
60022
+ });
60023
+ },
60024
+ connectRemote,
60025
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
60026
+ // 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
60027
+ // 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
60028
+ // 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
60029
+ selfUrl: () => currentTunnelUrl
60030
+ },
60031
+ // app-builder
60032
+ appBuilderStore,
60033
+ getProjectStore,
60034
+ devServerLifecycle: devServerSupervisor,
60035
+ // 单栏 refactor §5.6.2 stage fix:appBuilder:project-updated 帧通过 wsServer broadcast。
60036
+ // 推给所有 owner 连接(picker 可能没订阅特定 session,全局 broadcast 最稳)。
60037
+ // people/mirror(spec 2026-06-04-people-mode-app-builder-write-and-push):额外推给订阅该 project
60038
+ // 绑定 session 的 client(mirror guest 经 session:observe 订阅),让远端协同者也看到 stage / 发布进度。
60039
+ // 同一帧 owner 也走 project-updated 复用给 publish-progress/failed(app-builder.ts 内 broadcast 闭包):
60040
+ // name 在 event.project.name(project-updated)或 event.name(publish 帧)。owner 双栏会同时命中
60041
+ // owner-broadcast + session-broadcast,但 UI patch 幂等(同 project 覆盖)无害。
60042
+ broadcastProjectUpdated: (event) => {
60043
+ const ev = event;
60044
+ wsServer?.broadcastToOwners(ev);
60045
+ const projectName = event.project?.name ?? event.name;
60046
+ const boundSid = projectName ? manager.findOwnedSessionByAppBuilderProject(projectName)?.sessionId : void 0;
60047
+ if (boundSid) wsServer?.broadcastToSession(boundSid, ev);
60048
+ },
60049
+ // 发布上线脚手架化 (spec 2026-06-03 §5.2):
60050
+ // appBuilderPersonaRoot 用于拼 publish.sh 绝对路径(persona-app-builder 安装在
60051
+ // dataDir/personas/persona-app-builder 之下,extension-kit/scripts/publish.sh 是相对路径)。
60052
+ appBuilderPersonaRoot: import_node_path62.default.join(config.dataDir, "personas", "persona-app-builder"),
60053
+ // 共享 deploy-kit 根:scaffold/publish 脚本骨架 + 阿里云凭证单一真源。
60054
+ deployKitRoot: import_node_path62.default.join(config.dataDir, "deploy-kit"),
60055
+ // scaffold/publish 按当前 session 的 persona 解析其安装根,让每个 persona 用自己的模板/注入配置。
60056
+ resolvePersonaRoot: (personaId2) => import_node_path62.default.join(config.dataDir, "personas", personaId2),
60057
+ // 发布上线脚手架化 (spec 2026-06-03 §5.2.2):
60058
+ // 复用 SessionManagerDeps.broadcastFrame 同款 dispatch 逻辑 —— runner 调 manager.send
60059
+ // 取回 broadcast 帧后逐帧 push 到 transport,跟 manager 自身的 deps 一致。
60060
+ broadcastSessionFrame: (frame, target) => {
60061
+ if (target === "all") {
60062
+ transport?.broadcastAll(frame);
60063
+ return;
60015
60064
  }
60016
- return centralListDevices({
60017
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
60018
- ttcJwt: record.ttcToken,
60019
- provider: record.identity.provider
60020
- });
60065
+ const sid = frame.sessionId;
60066
+ if (!sid) return;
60067
+ if (target === "first-subscriber") {
60068
+ const handle = transport?.firstSubscriber(sid);
60069
+ if (handle) handle.send(frame);
60070
+ return;
60071
+ }
60072
+ transport?.broadcastToSession(sid, frame);
60021
60073
  },
60022
- connectRemote,
60023
- broadcast: (frame) => wsServer?.broadcastToOwners(frame),
60024
- // 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
60025
- // 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
60026
- // 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
60027
- selfUrl: () => currentTunnelUrl
60028
- },
60029
- // app-builder
60030
- appBuilderStore,
60031
- getProjectStore,
60032
- devServerLifecycle: devServerSupervisor,
60033
- // 单栏 refactor §5.6.2 stage fix:appBuilder:project-updated 帧通过 wsServer broadcast。
60034
- // 推给所有 owner 连接(picker 可能没订阅特定 session,全局 broadcast 最稳)。
60035
- // people/mirror(spec 2026-06-04-people-mode-app-builder-write-and-push):额外推给订阅该 project
60036
- // 绑定 session 的 client(mirror guest 经 session:observe 订阅),让远端协同者也看到 stage / 发布进度。
60037
- // 同一帧 owner 也走 project-updated 复用给 publish-progress/failed(app-builder.ts 内 broadcast 闭包):
60038
- // name 在 event.project.name(project-updated)或 event.name(publish 帧)。owner 双栏会同时命中
60039
- // owner-broadcast + session-broadcast,但 UI patch 幂等(同 project 覆盖)无害。
60040
- broadcastProjectUpdated: (event) => {
60041
- const ev = event;
60042
- wsServer?.broadcastToOwners(ev);
60043
- const projectName = event.project?.name ?? event.name;
60044
- const boundSid = projectName ? manager.findOwnedSessionByAppBuilderProject(projectName)?.sessionId : void 0;
60045
- if (boundSid) wsServer?.broadcastToSession(boundSid, ev);
60046
- },
60047
- // 发布上线脚手架化 (spec 2026-06-03 §5.2):
60048
- // appBuilderPersonaRoot 用于拼 publish.sh 绝对路径(persona-app-builder 安装在
60049
- // dataDir/personas/persona-app-builder 之下,extension-kit/scripts/publish.sh 是相对路径)。
60050
- appBuilderPersonaRoot: import_node_path62.default.join(config.dataDir, "personas", "persona-app-builder"),
60051
- // 共享 deploy-kit 根:scaffold/publish 脚本骨架 + 阿里云凭证单一真源。
60052
- deployKitRoot: import_node_path62.default.join(config.dataDir, "deploy-kit"),
60053
- // scaffold/publish 按当前 session 的 persona 解析其安装根,让每个 persona 用自己的模板/注入配置。
60054
- resolvePersonaRoot: (personaId2) => import_node_path62.default.join(config.dataDir, "personas", personaId2),
60055
- // 发布上线脚手架化 (spec 2026-06-03 §5.2.2):
60056
- // 复用 SessionManagerDeps.broadcastFrame 同款 dispatch 逻辑 —— runner 调 manager.send
60057
- // 取回 broadcast 帧后逐帧 push 到 transport,跟 manager 自身的 deps 一致。
60058
- broadcastSessionFrame: (frame, target) => {
60059
- if (target === "all") {
60060
- transport?.broadcastAll(frame);
60061
- return;
60062
- }
60063
- const sid = frame.sessionId;
60064
- if (!sid) return;
60065
- if (target === "first-subscriber") {
60066
- const handle = transport?.firstSubscriber(sid);
60067
- if (handle) handle.send(frame);
60068
- return;
60069
- }
60070
- transport?.broadcastToSession(sid, frame);
60071
- },
60072
- logger,
60073
- // shift v1:shift:list 桥接 handler 透传 ShiftStore 单例(同实例也喂给
60074
- // shift-internal:* MCP loopback handler)。owner 过滤用 ownerPrincipalId(跨
60075
- // daemon 稳定身份),跟我们 ShiftStore boot 时用的 provider 一致。
60076
- shiftStore,
60077
- shiftOwnerIdProvider: () => ownerPrincipalId,
60078
- // persona web 分享:visitor:list owner RPC 读访客名单(同 exchange upsert 的实例)
60079
- visitorStore
60080
- });
60081
- let handlers = makeHandlers();
60074
+ logger,
60075
+ // shift v1:shift:list 桥接 handler 透传 ShiftStore 单例(同实例也喂给
60076
+ // shift-internal:* MCP loopback handler)。owner 过滤用 ownerPrincipalId(跨
60077
+ // daemon 稳定身份),跟我们 ShiftStore boot 时用的 provider 一致。
60078
+ shiftStore,
60079
+ shiftOwnerIdProvider: () => ownerPrincipalId,
60080
+ // persona web 分享:visitor:list owner RPC 读访客名单(同 exchange upsert 的实例)
60081
+ visitorStore
60082
+ })
60083
+ };
60082
60084
  const dispatchHandlers = buildPersonaDispatchHandlers({
60083
60085
  personaDispatchManager,
60084
60086
  store: dispatchStore,
@@ -60149,12 +60151,12 @@ async function startDaemon(config) {
60149
60151
  // model 校验按 target persona 绑定 tool 选表(claude / codex 模型集不同)
60150
60152
  getPersonaTool: (personaId2) => personaRegistry.get(personaId2)?.tool
60151
60153
  });
60152
- handlers = { ...handlers, ...dispatchHandlers };
60154
+ Object.assign(handlers, dispatchHandlers);
60153
60155
  const shiftHandlers = buildShiftInternalHandlers({
60154
60156
  store: shiftStore,
60155
60157
  ownerIdProvider: () => ownerPrincipalId
60156
60158
  });
60157
- handlers = { ...handlers, ...shiftHandlers };
60159
+ Object.assign(handlers, shiftHandlers);
60158
60160
  const shiftSpawnFn = createShiftSpawnFn({ sessionManager: manager });
60159
60161
  const shiftScheduler = createShiftScheduler({
60160
60162
  store: shiftStore,
@@ -60233,7 +60235,6 @@ async function startDaemon(config) {
60233
60235
  ownerProvider = result.identity.provider;
60234
60236
  ownerDisplayName = result.identity.displayName;
60235
60237
  ownerFeishuUnionId = result.identity.unionId;
60236
- handlers = makeHandlers();
60237
60238
  wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
60238
60239
  setImmediate(() => wsServer?.closeAllClients());
60239
60240
  void reportDevice();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.263",
3
+ "version": "0.2.264",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,13 +17,14 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "build": "pnpm -F @clawos/clawd-ui build:assets && tsup && node scripts/copy-defaults.mjs",
20
- "typecheck": "tsc -p tsconfig.json --noEmit",
20
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p scripts/tsconfig.json --noEmit",
21
21
  "dev": "tsx src/cli.ts",
22
22
  "dev:watch": "tsx watch --clear-screen=false src/cli.ts",
23
23
  "test": "vitest run",
24
24
  "test:watch": "vitest",
25
25
  "test:changed": "vitest run --changed origin/release",
26
- "depcruise": "depcruise --config .dependency-cruiser.cjs src"
26
+ "depcruise": "depcruise --config .dependency-cruiser.cjs src",
27
+ "rpc-live": "tsup && node scripts/copy-defaults.mjs && tsx scripts/manual-rpc-live.ts"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@clawos/clawd-protocol": "workspace:*",