@clawos-dev/clawd 0.2.86-beta.162.77b3c16 → 0.2.86

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 +585 -331
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -65,14 +65,6 @@ var init_methods = __esm({
65
65
  "session:unsubscribe",
66
66
  "session:pin",
67
67
  "session:reorderPins",
68
- // mirror peer sessions (2026-05-26): owner-only RPC,写/删本机 mirror SessionFile —
69
- // mirror 是「**我**在朋友 daemon 上自己创建的 session」的本机持久化副本(不是朋友的会话),
70
- // 方便 sidebar 跨设备聚合。daemon dumb 写删,**不广播任何 push 帧** (不发 session:info /
71
- // session:deleted);UI 是写入唯一源,双写完成后通过 cache.setData('sessions', ...) 同步
72
- // 本地 sidebar state。native session:* handler 完全不感知 mirror (UI 操作 mirror 不复用
73
- // native 路径)。
74
- "peerSession:upsert",
75
- "peerSession:remove",
76
68
  "permission:respond",
77
69
  // AskUserQuestion 表单回写:UI 答完所有 question 后调用,daemon 把答案合并进 updated_input
78
70
  // 写一条 control_response 到 CC stdin(详见 events.ts session:question JSDoc)
@@ -651,8 +643,8 @@ var init_parseUtil = __esm({
651
643
  init_errors2();
652
644
  init_en();
653
645
  makeIssue = (params) => {
654
- const { data, path: path44, errorMaps, issueData } = params;
655
- const fullPath = [...path44, ...issueData.path || []];
646
+ const { data, path: path46, errorMaps, issueData } = params;
647
+ const fullPath = [...path46, ...issueData.path || []];
656
648
  const fullIssue = {
657
649
  ...issueData,
658
650
  path: fullPath
@@ -963,11 +955,11 @@ var init_types = __esm({
963
955
  init_parseUtil();
964
956
  init_util();
965
957
  ParseInputLazyPath = class {
966
- constructor(parent, value, path44, key) {
958
+ constructor(parent, value, path46, key) {
967
959
  this._cachedPath = [];
968
960
  this.parent = parent;
969
961
  this.data = value;
970
- this._path = path44;
962
+ this._path = path46;
971
963
  this._key = key;
972
964
  }
973
965
  get path() {
@@ -4351,13 +4343,13 @@ var init_zod = __esm({
4351
4343
  });
4352
4344
 
4353
4345
  // ../protocol/src/attachment-schemas.ts
4354
- var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema;
4346
+ var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema, AttachmentUploadResponseSchema;
4355
4347
  var init_attachment_schemas = __esm({
4356
4348
  "../protocol/src/attachment-schemas.ts"() {
4357
4349
  "use strict";
4358
4350
  init_zod();
4359
4351
  TOKEN_ROLES = ["owner"];
4360
- GROUP_FILE_SOURCES = ["agent", "owner"];
4352
+ GROUP_FILE_SOURCES = ["agent", "owner", "user-upload"];
4361
4353
  GroupFileEntrySchema = external_exports.object({
4362
4354
  /** daemon 派发的稳定 id(用于 RPC remove / UI key) */
4363
4355
  id: external_exports.string().min(1),
@@ -4412,6 +4404,16 @@ var init_attachment_schemas = __esm({
4412
4404
  AttachmentGroupListResponseSchema = external_exports.object({
4413
4405
  entries: external_exports.array(GroupFileEntrySchema)
4414
4406
  });
4407
+ AttachmentUploadResponseSchema = external_exports.object({
4408
+ /** sha256(bytes) 前 16 位,前端持有的稳定 ref */
4409
+ attachmentId: external_exports.string().min(1),
4410
+ /** 完整 tunnel signed URL,前端拼 marker / 直接送 Anthropic 都用它 */
4411
+ url: external_exports.string().min(1),
4412
+ /** 落在 session 目录下的相对路径(后续 group RPC / drawer 展示用) */
4413
+ relPath: external_exports.string().min(1),
4414
+ sizeBytes: external_exports.number().int().nonnegative(),
4415
+ mimeType: external_exports.string().min(1)
4416
+ });
4415
4417
  }
4416
4418
  });
4417
4419
 
@@ -4491,7 +4493,7 @@ var init_persona_schemas = __esm({
4491
4493
  });
4492
4494
 
4493
4495
  // ../protocol/src/schemas.ts
4494
- 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;
4496
+ 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, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema;
4495
4497
  var init_schemas = __esm({
4496
4498
  "../protocol/src/schemas.ts"() {
4497
4499
  "use strict";
@@ -4619,35 +4621,6 @@ var init_schemas = __esm({
4619
4621
  * 当前为 caller 自报,daemon 不密码学验证真伪;强身份验签是独立工作(B 方案)。
4620
4622
  */
4621
4623
  creatorOwnerPrincipalId: external_exports.string().min(1).optional(),
4622
- /**
4623
- * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上以 guest ctx
4624
- * 创建的 session**,远端是 source of truth,本机是该 session 的持久化副本,方便
4625
- * sidebar 跨设备聚合。字段值是远端 daemon 的 ownerPrincipalId(即我接入的那位朋友)。
4626
- *
4627
- * 语义关键澄清:mirror **不是朋友创建给我的会话**——它是我自己的会话,只是在朋友
4628
- * 的机器上跑。sidebar chip 显示 `@<朋友>` 暗示「位置」,不是「来源」。
4629
- *
4630
- * - undefined / 缺省 → 本机 native session(在我自己机器上跑)
4631
- * - 非空 → 我的会话在该 ownerPrincipalId 朋友的 daemon 上跑,本机为持久化副本
4632
- *
4633
- * 与 `creatorPrincipalId` / `creatorOwnerPrincipalId` 在四类 session 下取值矩阵:
4634
- * | 形态 | creatorPrincipalId | creatorOwnerPrincipalId | originOwnerPrincipalId |
4635
- * |-------------------------------------|--------------------------|-------------------------|------------------------|
4636
- * | 本机 owner 自创 | 本机 ownerPrincipalId | 本机 ownerPrincipalId | undefined |
4637
- * | 本机被别人通过 personal token 接入创建 | 远端接入者 cap.id | 远端接入者 ownerPrincipalId | undefined |
4638
- * | 我在朋友 daemon 上以 guest ctx 创建(mirror 回本机) | 朋友 daemon personal cap.id | 我自己 ownerPrincipalId | 朋友 ownerPrincipalId |
4639
- *
4640
- * UI 用法:
4641
- * - sidebar Active/Pinned/Recents 过滤:`origin 非空 → 一律视作 self`(绕过 #739 隔离)
4642
- * - 行内显示 `@<朋友>` chip 暗示位置
4643
- * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端
4644
- *
4645
- * daemon 用法:
4646
- * - `peerSession:upsert` / `peerSession:remove` 一族 RPC 专管 mirror 写删;
4647
- * daemon dumb write/delete,**不广播任何 push 帧**,UI 是写入唯一源
4648
- * - native `session:*` handler 完全不感知 mirror(UI 操作 mirror 不走 native 路径)
4649
- */
4650
- originOwnerPrincipalId: external_exports.string().min(1).optional(),
4651
4624
  createdAt: external_exports.string().min(1),
4652
4625
  updatedAt: external_exports.string().min(1)
4653
4626
  });
@@ -5003,14 +4976,6 @@ var init_schemas = __esm({
5003
4976
  sessionId: external_exports.string().min(1),
5004
4977
  pinned: external_exports.boolean()
5005
4978
  });
5006
- PeerSessionUpsertArgs = SessionFileSchema;
5007
- PeerSessionUpsertResponseSchema = external_exports.object({
5008
- type: external_exports.literal("peerSession:upsert:ok")
5009
- });
5010
- PeerSessionRemoveArgs = external_exports.object({ sessionId: external_exports.string().min(1) });
5011
- PeerSessionRemoveResponseSchema = external_exports.object({
5012
- type: external_exports.literal("peerSession:remove:ok")
5013
- });
5014
4979
  SessionReorderPinsArgs = external_exports.object({
5015
4980
  orderedIds: external_exports.array(external_exports.string().min(1)).min(1)
5016
4981
  });
@@ -5625,8 +5590,8 @@ var require_req = __commonJS({
5625
5590
  if (req.originalUrl) {
5626
5591
  _req.url = req.originalUrl;
5627
5592
  } else {
5628
- const path44 = req.path;
5629
- _req.url = typeof path44 === "string" ? path44 : req.url ? req.url.path || req.url : void 0;
5593
+ const path46 = req.path;
5594
+ _req.url = typeof path46 === "string" ? path46 : req.url ? req.url.path || req.url : void 0;
5630
5595
  }
5631
5596
  if (req.query) {
5632
5597
  _req.query = req.query;
@@ -5791,14 +5756,14 @@ var require_redact = __commonJS({
5791
5756
  }
5792
5757
  return obj;
5793
5758
  }
5794
- function parsePath(path44) {
5759
+ function parsePath(path46) {
5795
5760
  const parts = [];
5796
5761
  let current = "";
5797
5762
  let inBrackets = false;
5798
5763
  let inQuotes = false;
5799
5764
  let quoteChar = "";
5800
- for (let i = 0; i < path44.length; i++) {
5801
- const char = path44[i];
5765
+ for (let i = 0; i < path46.length; i++) {
5766
+ const char = path46[i];
5802
5767
  if (!inBrackets && char === ".") {
5803
5768
  if (current) {
5804
5769
  parts.push(current);
@@ -5929,10 +5894,10 @@ var require_redact = __commonJS({
5929
5894
  return current;
5930
5895
  }
5931
5896
  function redactPaths(obj, paths, censor, remove = false) {
5932
- for (const path44 of paths) {
5933
- const parts = parsePath(path44);
5897
+ for (const path46 of paths) {
5898
+ const parts = parsePath(path46);
5934
5899
  if (parts.includes("*")) {
5935
- redactWildcardPath(obj, parts, censor, path44, remove);
5900
+ redactWildcardPath(obj, parts, censor, path46, remove);
5936
5901
  } else {
5937
5902
  if (remove) {
5938
5903
  removeKey(obj, parts);
@@ -6017,8 +5982,8 @@ var require_redact = __commonJS({
6017
5982
  }
6018
5983
  } else {
6019
5984
  if (afterWildcard.includes("*")) {
6020
- const wrappedCensor = typeof censor === "function" ? (value, path44) => {
6021
- const fullPath = [...pathArray.slice(0, pathLength), ...path44];
5985
+ const wrappedCensor = typeof censor === "function" ? (value, path46) => {
5986
+ const fullPath = [...pathArray.slice(0, pathLength), ...path46];
6022
5987
  return censor(value, fullPath);
6023
5988
  } : censor;
6024
5989
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6053,8 +6018,8 @@ var require_redact = __commonJS({
6053
6018
  return null;
6054
6019
  }
6055
6020
  const pathStructure = /* @__PURE__ */ new Map();
6056
- for (const path44 of pathsToClone) {
6057
- const parts = parsePath(path44);
6021
+ for (const path46 of pathsToClone) {
6022
+ const parts = parsePath(path46);
6058
6023
  let current = pathStructure;
6059
6024
  for (let i = 0; i < parts.length; i++) {
6060
6025
  const part = parts[i];
@@ -6106,24 +6071,24 @@ var require_redact = __commonJS({
6106
6071
  }
6107
6072
  return cloneSelectively(obj, pathStructure);
6108
6073
  }
6109
- function validatePath(path44) {
6110
- if (typeof path44 !== "string") {
6074
+ function validatePath(path46) {
6075
+ if (typeof path46 !== "string") {
6111
6076
  throw new Error("Paths must be (non-empty) strings");
6112
6077
  }
6113
- if (path44 === "") {
6078
+ if (path46 === "") {
6114
6079
  throw new Error("Invalid redaction path ()");
6115
6080
  }
6116
- if (path44.includes("..")) {
6117
- throw new Error(`Invalid redaction path (${path44})`);
6081
+ if (path46.includes("..")) {
6082
+ throw new Error(`Invalid redaction path (${path46})`);
6118
6083
  }
6119
- if (path44.includes(",")) {
6120
- throw new Error(`Invalid redaction path (${path44})`);
6084
+ if (path46.includes(",")) {
6085
+ throw new Error(`Invalid redaction path (${path46})`);
6121
6086
  }
6122
6087
  let bracketCount = 0;
6123
6088
  let inQuotes = false;
6124
6089
  let quoteChar = "";
6125
- for (let i = 0; i < path44.length; i++) {
6126
- const char = path44[i];
6090
+ for (let i = 0; i < path46.length; i++) {
6091
+ const char = path46[i];
6127
6092
  if ((char === '"' || char === "'") && bracketCount > 0) {
6128
6093
  if (!inQuotes) {
6129
6094
  inQuotes = true;
@@ -6137,20 +6102,20 @@ var require_redact = __commonJS({
6137
6102
  } else if (char === "]" && !inQuotes) {
6138
6103
  bracketCount--;
6139
6104
  if (bracketCount < 0) {
6140
- throw new Error(`Invalid redaction path (${path44})`);
6105
+ throw new Error(`Invalid redaction path (${path46})`);
6141
6106
  }
6142
6107
  }
6143
6108
  }
6144
6109
  if (bracketCount !== 0) {
6145
- throw new Error(`Invalid redaction path (${path44})`);
6110
+ throw new Error(`Invalid redaction path (${path46})`);
6146
6111
  }
6147
6112
  }
6148
6113
  function validatePaths(paths) {
6149
6114
  if (!Array.isArray(paths)) {
6150
6115
  throw new TypeError("paths must be an array");
6151
6116
  }
6152
- for (const path44 of paths) {
6153
- validatePath(path44);
6117
+ for (const path46 of paths) {
6118
+ validatePath(path46);
6154
6119
  }
6155
6120
  }
6156
6121
  function slowRedact(options = {}) {
@@ -6318,8 +6283,8 @@ var require_redaction = __commonJS({
6318
6283
  if (shape[k2] === null) {
6319
6284
  o[k2] = (value) => topCensor(value, [k2]);
6320
6285
  } else {
6321
- const wrappedCensor = typeof censor === "function" ? (value, path44) => {
6322
- return censor(value, [k2, ...path44]);
6286
+ const wrappedCensor = typeof censor === "function" ? (value, path46) => {
6287
+ return censor(value, [k2, ...path46]);
6323
6288
  } : censor;
6324
6289
  o[k2] = Redact({
6325
6290
  paths: shape[k2],
@@ -6537,10 +6502,10 @@ var require_atomic_sleep = __commonJS({
6537
6502
  var require_sonic_boom = __commonJS({
6538
6503
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6539
6504
  "use strict";
6540
- var fs35 = require("fs");
6505
+ var fs37 = require("fs");
6541
6506
  var EventEmitter2 = require("events");
6542
6507
  var inherits = require("util").inherits;
6543
- var path44 = require("path");
6508
+ var path46 = require("path");
6544
6509
  var sleep = require_atomic_sleep();
6545
6510
  var assert = require("assert");
6546
6511
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6594,20 +6559,20 @@ var require_sonic_boom = __commonJS({
6594
6559
  const mode = sonic.mode;
6595
6560
  if (sonic.sync) {
6596
6561
  try {
6597
- if (sonic.mkdir) fs35.mkdirSync(path44.dirname(file), { recursive: true });
6598
- const fd = fs35.openSync(file, flags, mode);
6562
+ if (sonic.mkdir) fs37.mkdirSync(path46.dirname(file), { recursive: true });
6563
+ const fd = fs37.openSync(file, flags, mode);
6599
6564
  fileOpened(null, fd);
6600
6565
  } catch (err) {
6601
6566
  fileOpened(err);
6602
6567
  throw err;
6603
6568
  }
6604
6569
  } else if (sonic.mkdir) {
6605
- fs35.mkdir(path44.dirname(file), { recursive: true }, (err) => {
6570
+ fs37.mkdir(path46.dirname(file), { recursive: true }, (err) => {
6606
6571
  if (err) return fileOpened(err);
6607
- fs35.open(file, flags, mode, fileOpened);
6572
+ fs37.open(file, flags, mode, fileOpened);
6608
6573
  });
6609
6574
  } else {
6610
- fs35.open(file, flags, mode, fileOpened);
6575
+ fs37.open(file, flags, mode, fileOpened);
6611
6576
  }
6612
6577
  }
6613
6578
  function SonicBoom(opts) {
@@ -6648,8 +6613,8 @@ var require_sonic_boom = __commonJS({
6648
6613
  this.flush = flushBuffer;
6649
6614
  this.flushSync = flushBufferSync;
6650
6615
  this._actualWrite = actualWriteBuffer;
6651
- fsWriteSync = () => fs35.writeSync(this.fd, this._writingBuf);
6652
- fsWrite = () => fs35.write(this.fd, this._writingBuf, this.release);
6616
+ fsWriteSync = () => fs37.writeSync(this.fd, this._writingBuf);
6617
+ fsWrite = () => fs37.write(this.fd, this._writingBuf, this.release);
6653
6618
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6654
6619
  this._writingBuf = "";
6655
6620
  this.write = write;
@@ -6658,15 +6623,15 @@ var require_sonic_boom = __commonJS({
6658
6623
  this._actualWrite = actualWrite;
6659
6624
  fsWriteSync = () => {
6660
6625
  if (Buffer.isBuffer(this._writingBuf)) {
6661
- return fs35.writeSync(this.fd, this._writingBuf);
6626
+ return fs37.writeSync(this.fd, this._writingBuf);
6662
6627
  }
6663
- return fs35.writeSync(this.fd, this._writingBuf, "utf8");
6628
+ return fs37.writeSync(this.fd, this._writingBuf, "utf8");
6664
6629
  };
6665
6630
  fsWrite = () => {
6666
6631
  if (Buffer.isBuffer(this._writingBuf)) {
6667
- return fs35.write(this.fd, this._writingBuf, this.release);
6632
+ return fs37.write(this.fd, this._writingBuf, this.release);
6668
6633
  }
6669
- return fs35.write(this.fd, this._writingBuf, "utf8", this.release);
6634
+ return fs37.write(this.fd, this._writingBuf, "utf8", this.release);
6670
6635
  };
6671
6636
  } else {
6672
6637
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -6723,7 +6688,7 @@ var require_sonic_boom = __commonJS({
6723
6688
  }
6724
6689
  }
6725
6690
  if (this._fsync) {
6726
- fs35.fsyncSync(this.fd);
6691
+ fs37.fsyncSync(this.fd);
6727
6692
  }
6728
6693
  const len = this._len;
6729
6694
  if (this._reopening) {
@@ -6837,7 +6802,7 @@ var require_sonic_boom = __commonJS({
6837
6802
  const onDrain = () => {
6838
6803
  if (!this._fsync) {
6839
6804
  try {
6840
- fs35.fsync(this.fd, (err) => {
6805
+ fs37.fsync(this.fd, (err) => {
6841
6806
  this._flushPending = false;
6842
6807
  cb(err);
6843
6808
  });
@@ -6939,7 +6904,7 @@ var require_sonic_boom = __commonJS({
6939
6904
  const fd = this.fd;
6940
6905
  this.once("ready", () => {
6941
6906
  if (fd !== this.fd) {
6942
- fs35.close(fd, (err) => {
6907
+ fs37.close(fd, (err) => {
6943
6908
  if (err) {
6944
6909
  return this.emit("error", err);
6945
6910
  }
@@ -6988,7 +6953,7 @@ var require_sonic_boom = __commonJS({
6988
6953
  buf = this._bufs[0];
6989
6954
  }
6990
6955
  try {
6991
- const n = Buffer.isBuffer(buf) ? fs35.writeSync(this.fd, buf) : fs35.writeSync(this.fd, buf, "utf8");
6956
+ const n = Buffer.isBuffer(buf) ? fs37.writeSync(this.fd, buf) : fs37.writeSync(this.fd, buf, "utf8");
6992
6957
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
6993
6958
  buf = releasedBufObj.writingBuf;
6994
6959
  this._len = releasedBufObj.len;
@@ -7004,7 +6969,7 @@ var require_sonic_boom = __commonJS({
7004
6969
  }
7005
6970
  }
7006
6971
  try {
7007
- fs35.fsyncSync(this.fd);
6972
+ fs37.fsyncSync(this.fd);
7008
6973
  } catch {
7009
6974
  }
7010
6975
  }
@@ -7025,7 +6990,7 @@ var require_sonic_boom = __commonJS({
7025
6990
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7026
6991
  }
7027
6992
  try {
7028
- const n = fs35.writeSync(this.fd, buf);
6993
+ const n = fs37.writeSync(this.fd, buf);
7029
6994
  buf = buf.subarray(n);
7030
6995
  this._len = Math.max(this._len - n, 0);
7031
6996
  if (buf.length <= 0) {
@@ -7053,13 +7018,13 @@ var require_sonic_boom = __commonJS({
7053
7018
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7054
7019
  if (this.sync) {
7055
7020
  try {
7056
- const written = Buffer.isBuffer(this._writingBuf) ? fs35.writeSync(this.fd, this._writingBuf) : fs35.writeSync(this.fd, this._writingBuf, "utf8");
7021
+ const written = Buffer.isBuffer(this._writingBuf) ? fs37.writeSync(this.fd, this._writingBuf) : fs37.writeSync(this.fd, this._writingBuf, "utf8");
7057
7022
  release(null, written);
7058
7023
  } catch (err) {
7059
7024
  release(err);
7060
7025
  }
7061
7026
  } else {
7062
- fs35.write(this.fd, this._writingBuf, release);
7027
+ fs37.write(this.fd, this._writingBuf, release);
7063
7028
  }
7064
7029
  }
7065
7030
  function actualWriteBuffer() {
@@ -7068,7 +7033,7 @@ var require_sonic_boom = __commonJS({
7068
7033
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7069
7034
  if (this.sync) {
7070
7035
  try {
7071
- const written = fs35.writeSync(this.fd, this._writingBuf);
7036
+ const written = fs37.writeSync(this.fd, this._writingBuf);
7072
7037
  release(null, written);
7073
7038
  } catch (err) {
7074
7039
  release(err);
@@ -7077,7 +7042,7 @@ var require_sonic_boom = __commonJS({
7077
7042
  if (kCopyBuffer) {
7078
7043
  this._writingBuf = Buffer.from(this._writingBuf);
7079
7044
  }
7080
- fs35.write(this.fd, this._writingBuf, release);
7045
+ fs37.write(this.fd, this._writingBuf, release);
7081
7046
  }
7082
7047
  }
7083
7048
  function actualClose(sonic) {
@@ -7093,12 +7058,12 @@ var require_sonic_boom = __commonJS({
7093
7058
  sonic._lens = [];
7094
7059
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7095
7060
  try {
7096
- fs35.fsync(sonic.fd, closeWrapped);
7061
+ fs37.fsync(sonic.fd, closeWrapped);
7097
7062
  } catch {
7098
7063
  }
7099
7064
  function closeWrapped() {
7100
7065
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7101
- fs35.close(sonic.fd, done);
7066
+ fs37.close(sonic.fd, done);
7102
7067
  } else {
7103
7068
  done();
7104
7069
  }
@@ -7829,7 +7794,7 @@ var require_transport = __commonJS({
7829
7794
  stream.flushSync();
7830
7795
  }
7831
7796
  function transport(fullOptions) {
7832
- const { pipeline: pipeline2, targets, levels, dedupe, worker = {}, caller = getCallers(), sync = false } = fullOptions;
7797
+ const { pipeline: pipeline3, targets, levels, dedupe, worker = {}, caller = getCallers(), sync = false } = fullOptions;
7833
7798
  const options = {
7834
7799
  ...fullOptions.options
7835
7800
  };
@@ -7857,9 +7822,9 @@ var require_transport = __commonJS({
7857
7822
  };
7858
7823
  });
7859
7824
  });
7860
- } else if (pipeline2) {
7825
+ } else if (pipeline3) {
7861
7826
  target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
7862
- options.pipelines = [pipeline2.map((dest) => {
7827
+ options.pipelines = [pipeline3.map((dest) => {
7863
7828
  return {
7864
7829
  ...dest,
7865
7830
  target: fixTarget(dest.target)
@@ -10233,11 +10198,11 @@ var init_lib = __esm({
10233
10198
  }
10234
10199
  }
10235
10200
  },
10236
- addToPath: function addToPath(path44, added, removed, oldPosInc, options) {
10237
- var last = path44.lastComponent;
10201
+ addToPath: function addToPath(path46, added, removed, oldPosInc, options) {
10202
+ var last = path46.lastComponent;
10238
10203
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
10239
10204
  return {
10240
- oldPos: path44.oldPos + oldPosInc,
10205
+ oldPos: path46.oldPos + oldPosInc,
10241
10206
  lastComponent: {
10242
10207
  count: last.count + 1,
10243
10208
  added,
@@ -10247,7 +10212,7 @@ var init_lib = __esm({
10247
10212
  };
10248
10213
  } else {
10249
10214
  return {
10250
- oldPos: path44.oldPos + oldPosInc,
10215
+ oldPos: path46.oldPos + oldPosInc,
10251
10216
  lastComponent: {
10252
10217
  count: 1,
10253
10218
  added,
@@ -10678,10 +10643,10 @@ function attachmentToHistoryMessage(o, ts) {
10678
10643
  const memories = raw.map((m2) => {
10679
10644
  if (!m2 || typeof m2 !== "object") return null;
10680
10645
  const rec = m2;
10681
- const path44 = typeof rec.path === "string" ? rec.path : null;
10646
+ const path46 = typeof rec.path === "string" ? rec.path : null;
10682
10647
  const content = typeof rec.content === "string" ? rec.content : null;
10683
- if (!path44 || content == null) return null;
10684
- const entry = { path: path44, content };
10648
+ if (!path46 || content == null) return null;
10649
+ const entry = { path: path46, content };
10685
10650
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
10686
10651
  return entry;
10687
10652
  }).filter((m2) => m2 !== null);
@@ -11507,10 +11472,10 @@ function parseAttachment(obj) {
11507
11472
  const memories = raw.map((m2) => {
11508
11473
  if (!m2 || typeof m2 !== "object") return null;
11509
11474
  const rec = m2;
11510
- const path44 = typeof rec.path === "string" ? rec.path : null;
11475
+ const path46 = typeof rec.path === "string" ? rec.path : null;
11511
11476
  const content = typeof rec.content === "string" ? rec.content : null;
11512
- if (!path44 || content == null) return null;
11513
- const out = { path: path44, content };
11477
+ if (!path46 || content == null) return null;
11478
+ const out = { path: path46, content };
11514
11479
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11515
11480
  return out;
11516
11481
  }).filter((m2) => m2 !== null);
@@ -20425,7 +20390,7 @@ var require_websocket_server = __commonJS({
20425
20390
  // src/run-case/recorder.ts
20426
20391
  function startRunCaseRecorder(opts) {
20427
20392
  const now = opts.now ?? Date.now;
20428
- const dir = import_node_path30.default.dirname(opts.recordPath);
20393
+ const dir = import_node_path32.default.dirname(opts.recordPath);
20429
20394
  let stream = null;
20430
20395
  let closing = false;
20431
20396
  let closedSettled = false;
@@ -20439,8 +20404,8 @@ function startRunCaseRecorder(opts) {
20439
20404
  });
20440
20405
  const ensureStream = () => {
20441
20406
  if (stream) return stream;
20442
- import_node_fs27.default.mkdirSync(dir, { recursive: true });
20443
- stream = import_node_fs27.default.createWriteStream(opts.recordPath, { flags: "a" });
20407
+ import_node_fs29.default.mkdirSync(dir, { recursive: true });
20408
+ stream = import_node_fs29.default.createWriteStream(opts.recordPath, { flags: "a" });
20444
20409
  stream.on("close", () => closedResolve());
20445
20410
  return stream;
20446
20411
  };
@@ -20465,12 +20430,12 @@ function startRunCaseRecorder(opts) {
20465
20430
  };
20466
20431
  return { tap, close, closed };
20467
20432
  }
20468
- var import_node_fs27, import_node_path30;
20433
+ var import_node_fs29, import_node_path32;
20469
20434
  var init_recorder = __esm({
20470
20435
  "src/run-case/recorder.ts"() {
20471
20436
  "use strict";
20472
- import_node_fs27 = __toESM(require("fs"), 1);
20473
- import_node_path30 = __toESM(require("path"), 1);
20437
+ import_node_fs29 = __toESM(require("fs"), 1);
20438
+ import_node_path32 = __toESM(require("path"), 1);
20474
20439
  }
20475
20440
  });
20476
20441
 
@@ -20513,7 +20478,7 @@ var init_wire = __esm({
20513
20478
  // src/run-case/controller.ts
20514
20479
  async function runController(opts) {
20515
20480
  const now = opts.now ?? Date.now;
20516
- const cwd = opts.cwd ?? (0, import_node_fs28.mkdtempSync)(import_node_path31.default.join(import_node_os15.default.tmpdir(), "clawd-runcase-"));
20481
+ const cwd = opts.cwd ?? (0, import_node_fs30.mkdtempSync)(import_node_path33.default.join(import_node_os15.default.tmpdir(), "clawd-runcase-"));
20517
20482
  const ownsCwd = opts.cwd === void 0;
20518
20483
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
20519
20484
  const spawnCtx = { cwd };
@@ -20674,19 +20639,19 @@ async function runController(opts) {
20674
20639
  if (sigintHandler) process.off("SIGINT", sigintHandler);
20675
20640
  if (ownsCwd) {
20676
20641
  try {
20677
- (0, import_node_fs28.rmSync)(cwd, { recursive: true, force: true });
20642
+ (0, import_node_fs30.rmSync)(cwd, { recursive: true, force: true });
20678
20643
  } catch {
20679
20644
  }
20680
20645
  }
20681
20646
  return exitCode ?? 0;
20682
20647
  }
20683
- var import_node_fs28, import_node_os15, import_node_path31;
20648
+ var import_node_fs30, import_node_os15, import_node_path33;
20684
20649
  var init_controller = __esm({
20685
20650
  "src/run-case/controller.ts"() {
20686
20651
  "use strict";
20687
- import_node_fs28 = require("fs");
20652
+ import_node_fs30 = require("fs");
20688
20653
  import_node_os15 = __toESM(require("os"), 1);
20689
- import_node_path31 = __toESM(require("path"), 1);
20654
+ import_node_path33 = __toESM(require("path"), 1);
20690
20655
  init_claude();
20691
20656
  init_stdout_splitter();
20692
20657
  init_permission_stdio();
@@ -20918,8 +20883,8 @@ Env (advanced):
20918
20883
  `;
20919
20884
 
20920
20885
  // src/index.ts
20921
- var import_node_path29 = __toESM(require("path"), 1);
20922
- var import_node_fs26 = __toESM(require("fs"), 1);
20886
+ var import_node_path31 = __toESM(require("path"), 1);
20887
+ var import_node_fs28 = __toESM(require("fs"), 1);
20923
20888
 
20924
20889
  // src/logger.ts
20925
20890
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -22654,6 +22619,18 @@ var SessionManager = class {
22654
22619
  (a, b2) => a.updatedAt > b2.updatedAt ? -1 : a.updatedAt < b2.updatedAt ? 1 : 0
22655
22620
  );
22656
22621
  }
22622
+ /**
22623
+ * 跨 scope 列出所有 owner 视角的 session(default + persona owner + persona guest),
22624
+ * 附带派生的 SessionScope。attachment GC tick 用 —— 需要遍历所有活跃 session 的
22625
+ * .attachments 目录 + 群文件清单求 liveSet。
22626
+ */
22627
+ listOwnedSessionScopes() {
22628
+ return this.listAllOwned().map((file) => ({
22629
+ scope: this.scopeForFile(file),
22630
+ sessionId: file.sessionId,
22631
+ cwd: file.cwd
22632
+ }));
22633
+ }
22657
22634
  // 写回 SessionFile,自动根据 ownerPersonaId / creatorPrincipalId 派生写入 scope.
22658
22635
  // 调用方原先都是 `deps.store.write(updated)`,全部走这里收口路由.
22659
22636
  writeOwned(file) {
@@ -22907,25 +22884,6 @@ var SessionManager = class {
22907
22884
  const file = this.getFile(args.sessionId);
22908
22885
  return { response: file, broadcast: [] };
22909
22886
  }
22910
- // mirror peer sessions (2026-05-26):把来自朋友 daemon 的 SessionFile(originOwnerPrincipalId
22911
- // 非空)持久化到本机 default store。dumb write,**不广播 push 帧**——UI 是写入唯一源,
22912
- // 双写完成后通过 cache.setData 同步 sidebar 本地 state。
22913
- //
22914
- // 强制走 deps.store(即 default scope),不走 scopeForFile:即使 mirror SessionFile 带
22915
- // 远端 ownerPersonaId(远端 persona session),本机也落 default,不创建本机 persona 目录。
22916
- // 远端 creator / origin 字段原值保留(不被本机 ctx 派生覆写)。
22917
- //
22918
- // 与 manager.create / manager.update 区别:
22919
- // - 不走 newRunner / attachObserver / cwd 校验(mirror 是纯 metadata 层)
22920
- // - 不发 session:info 帧(manager.create / .update 通过 reducer/wireCollector 发 push)
22921
- mirrorUpsert(file) {
22922
- return this.deps.store.write(file);
22923
- }
22924
- // mirror peer sessions:删本机 mirror SessionFile。强制 default store。
22925
- // idempotent:找不到不 throw(UI 反复点删容错)。**不广播 session:deleted**。
22926
- mirrorRemove(sessionId) {
22927
- this.deps.store.delete(sessionId);
22928
- }
22929
22887
  // observer 上报"jsonl 写入了一条 user 行"时调用:在 buffer 里找到与之对应的、
22930
22888
  // 还没建立映射的最早一条 synth user_text,记下 synth → real 映射。
22931
22889
  // 找不到匹配的 synth(runner 不存在 / buffer 已被 /clear 清空 / 真 uuid 来自纯 history
@@ -27163,8 +27121,8 @@ function rmdirIfEmpty(p2) {
27163
27121
  }
27164
27122
 
27165
27123
  // src/transport/http-router.ts
27166
- var import_node_fs15 = __toESM(require("fs"), 1);
27167
- var import_node_path17 = __toESM(require("path"), 1);
27124
+ var import_node_fs16 = __toESM(require("fs"), 1);
27125
+ var import_node_path18 = __toESM(require("path"), 1);
27168
27126
 
27169
27127
  // src/attachment/mime.ts
27170
27128
  var import_node_path16 = __toESM(require("path"), 1);
@@ -27342,7 +27300,131 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
27342
27300
  return { ok: true, absPath };
27343
27301
  }
27344
27302
 
27303
+ // src/attachment/upload.ts
27304
+ var import_node_fs15 = __toESM(require("fs"), 1);
27305
+ var import_node_path17 = __toESM(require("path"), 1);
27306
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
27307
+ var import_promises = require("stream/promises");
27308
+ var UploadError = class extends Error {
27309
+ constructor(code, message) {
27310
+ super(message);
27311
+ this.code = code;
27312
+ this.name = "UploadError";
27313
+ }
27314
+ code;
27315
+ };
27316
+ function assertValidFileName(fileName) {
27317
+ if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path17.default.basename(fileName)) {
27318
+ throw new UploadError("INVALID_FILENAME", `fileName must be a plain basename, got: ${fileName}`);
27319
+ }
27320
+ }
27321
+ var HASH_PREFIX_LEN = 16;
27322
+ async function writeUploadedAttachment(args) {
27323
+ assertValidFileName(args.fileName);
27324
+ const attachmentsRoot = import_node_path17.default.join(args.sessionDir, ".attachments");
27325
+ try {
27326
+ import_node_fs15.default.mkdirSync(attachmentsRoot, { recursive: true });
27327
+ } catch (err) {
27328
+ throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
27329
+ }
27330
+ const hasher = import_node_crypto6.default.createHash("sha256");
27331
+ let actualSize = 0;
27332
+ const tmpPath = import_node_path17.default.join(
27333
+ attachmentsRoot,
27334
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto6.default.randomBytes(4).toString("hex")}`
27335
+ );
27336
+ try {
27337
+ await (0, import_promises.pipeline)(
27338
+ args.body,
27339
+ async function* (source) {
27340
+ for await (const chunk of source) {
27341
+ const buf = chunk;
27342
+ actualSize += buf.length;
27343
+ hasher.update(buf);
27344
+ yield buf;
27345
+ }
27346
+ },
27347
+ import_node_fs15.default.createWriteStream(tmpPath, { mode: 384 })
27348
+ );
27349
+ } catch (err) {
27350
+ try {
27351
+ import_node_fs15.default.unlinkSync(tmpPath);
27352
+ } catch {
27353
+ }
27354
+ throw new UploadError("STORAGE_ERROR", `write failed: ${err.message}`);
27355
+ }
27356
+ if (actualSize !== args.contentLength) {
27357
+ try {
27358
+ import_node_fs15.default.unlinkSync(tmpPath);
27359
+ } catch {
27360
+ }
27361
+ throw new UploadError(
27362
+ "SIZE_MISMATCH",
27363
+ `expected ${args.contentLength} bytes, got ${actualSize}`
27364
+ );
27365
+ }
27366
+ const attachmentId = hasher.digest("hex").slice(0, HASH_PREFIX_LEN);
27367
+ const hashDir = import_node_path17.default.join(attachmentsRoot, attachmentId);
27368
+ let finalFileName;
27369
+ let hashDirExists = false;
27370
+ try {
27371
+ hashDirExists = import_node_fs15.default.statSync(hashDir).isDirectory();
27372
+ } catch {
27373
+ }
27374
+ if (hashDirExists) {
27375
+ const existing = import_node_fs15.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
27376
+ finalFileName = existing[0] ?? args.fileName;
27377
+ try {
27378
+ import_node_fs15.default.unlinkSync(tmpPath);
27379
+ } catch {
27380
+ }
27381
+ } else {
27382
+ try {
27383
+ import_node_fs15.default.mkdirSync(hashDir, { recursive: true });
27384
+ finalFileName = args.fileName;
27385
+ import_node_fs15.default.renameSync(tmpPath, import_node_path17.default.join(hashDir, finalFileName));
27386
+ } catch (err) {
27387
+ try {
27388
+ import_node_fs15.default.unlinkSync(tmpPath);
27389
+ } catch {
27390
+ }
27391
+ throw new UploadError("STORAGE_ERROR", `rename failed: ${err.message}`);
27392
+ }
27393
+ }
27394
+ const absPath = import_node_path17.default.join(hashDir, finalFileName);
27395
+ const relPath = import_node_path17.default.relative(args.sessionDir, absPath);
27396
+ args.groupFileStore.upsert(args.scope, args.sessionId, {
27397
+ relPath: absPath,
27398
+ // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
27399
+ from: "user-upload",
27400
+ label: args.fileName,
27401
+ size: actualSize,
27402
+ mime: args.mimeType
27403
+ });
27404
+ return { attachmentId, absPath, relPath, sizeBytes: actualSize, mimeType: args.mimeType };
27405
+ }
27406
+
27345
27407
  // src/transport/http-router.ts
27408
+ var UPLOAD_MIME_ALLOW = /* @__PURE__ */ new Set([
27409
+ "image/png",
27410
+ "image/jpeg",
27411
+ "image/gif",
27412
+ "image/webp",
27413
+ "application/pdf"
27414
+ ]);
27415
+ var UPLOAD_MIME_ALLOW_PREFIX = ["text/"];
27416
+ var UPLOAD_MAX_BYTES = 25 * 1024 * 1024;
27417
+ function isUploadMimeAllowed(mime) {
27418
+ if (UPLOAD_MIME_ALLOW.has(mime)) return true;
27419
+ return UPLOAD_MIME_ALLOW_PREFIX.some((p2) => mime.startsWith(p2));
27420
+ }
27421
+ function isValidUploadFileName(fileName) {
27422
+ if (fileName.length === 0) return false;
27423
+ if (fileName === "." || fileName === "..") return false;
27424
+ if (fileName.startsWith(".")) return false;
27425
+ if (fileName.includes("/") || fileName.includes("\\")) return false;
27426
+ return fileName === import_node_path18.default.basename(fileName);
27427
+ }
27346
27428
  function createHttpRouter(deps) {
27347
27429
  return async (req, res) => {
27348
27430
  const url = parseUrl(req.url);
@@ -27356,7 +27438,7 @@ function createHttpRouter(deps) {
27356
27438
  });
27357
27439
  return true;
27358
27440
  }
27359
- if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
27441
+ if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/") && url.pathname !== "/attachments/upload") {
27360
27442
  return false;
27361
27443
  }
27362
27444
  if (url.pathname.startsWith("/files/") && req.method === "GET") {
@@ -27396,6 +27478,106 @@ function createHttpRouter(deps) {
27396
27478
  sendJson(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
27397
27479
  return true;
27398
27480
  }
27481
+ if (url.pathname === "/attachments/upload" && req.method === "POST") {
27482
+ if (ctx.role !== "owner") {
27483
+ sendJson(res, 403, { code: "FORBIDDEN", message: "owner only" });
27484
+ return true;
27485
+ }
27486
+ const up = deps.attachmentUpload;
27487
+ if (!up) {
27488
+ sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "attachment upload not wired" });
27489
+ return true;
27490
+ }
27491
+ const sessionId = url.searchParams.get("sessionId");
27492
+ const fileName = url.searchParams.get("fileName");
27493
+ const mimeType = url.searchParams.get("mimeType");
27494
+ const contentLengthRaw = req.headers["content-length"];
27495
+ if (!sessionId || !fileName || !mimeType || !contentLengthRaw) {
27496
+ sendJson(res, 400, {
27497
+ code: "INVALID_PARAM",
27498
+ message: "missing sessionId/fileName/mimeType/Content-Length"
27499
+ });
27500
+ return true;
27501
+ }
27502
+ if (!isValidUploadFileName(fileName)) {
27503
+ sendJson(res, 400, {
27504
+ code: "INVALID_FILENAME",
27505
+ message: `fileName must be a plain basename (no path separators, no dotfiles): ${fileName}`
27506
+ });
27507
+ return true;
27508
+ }
27509
+ const contentLength = Number.parseInt(contentLengthRaw, 10);
27510
+ if (!Number.isFinite(contentLength) || contentLength < 0) {
27511
+ sendJson(res, 400, { code: "INVALID_PARAM", message: "malformed Content-Length" });
27512
+ return true;
27513
+ }
27514
+ if (contentLength > UPLOAD_MAX_BYTES) {
27515
+ sendJson(res, 413, {
27516
+ code: "FILE_TOO_LARGE",
27517
+ message: `max ${UPLOAD_MAX_BYTES} bytes`
27518
+ });
27519
+ return true;
27520
+ }
27521
+ if (!isUploadMimeAllowed(mimeType)) {
27522
+ sendJson(res, 400, {
27523
+ code: "UNSUPPORTED_MIME",
27524
+ message: `mime ${mimeType} not allowed`
27525
+ });
27526
+ return true;
27527
+ }
27528
+ const sessionDir = up.getSessionDir(sessionId);
27529
+ const scope = up.getSessionScope(sessionId);
27530
+ if (!sessionDir || !scope) {
27531
+ sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sessionId} not found` });
27532
+ return true;
27533
+ }
27534
+ const httpBaseUrl = up.getHttpBaseUrl();
27535
+ if (!httpBaseUrl) {
27536
+ sendJson(res, 503, {
27537
+ code: "TUNNEL_NOT_READY",
27538
+ message: "httpBaseUrl unavailable"
27539
+ });
27540
+ return true;
27541
+ }
27542
+ const secret = deps.getSignSecret?.();
27543
+ if (!secret) {
27544
+ sendJson(res, 501, {
27545
+ code: "NOT_IMPLEMENTED",
27546
+ message: "sign secret unavailable (noAuth?)"
27547
+ });
27548
+ return true;
27549
+ }
27550
+ try {
27551
+ const result = await writeUploadedAttachment({
27552
+ sessionDir,
27553
+ sessionId,
27554
+ scope,
27555
+ fileName,
27556
+ mimeType,
27557
+ contentLength,
27558
+ body: req,
27559
+ groupFileStore: up.groupFileStore
27560
+ });
27561
+ const parts = signUrlParts(secret, result.absPath, null);
27562
+ const fullUrl = buildSignedFileUrl(httpBaseUrl, parts);
27563
+ sendJson(res, 200, {
27564
+ attachmentId: result.attachmentId,
27565
+ url: fullUrl,
27566
+ relPath: result.relPath,
27567
+ sizeBytes: result.sizeBytes,
27568
+ mimeType: result.mimeType
27569
+ });
27570
+ } catch (err) {
27571
+ if (err instanceof UploadError) {
27572
+ const status = err.code === "SIZE_MISMATCH" ? 400 : 500;
27573
+ sendJson(res, status, { code: err.code, message: err.message });
27574
+ } else {
27575
+ deps.logger?.warn("attachment upload failed", { err: err.message });
27576
+ sendJson(res, 500, { code: "STORAGE_ERROR", message: err.message });
27577
+ }
27578
+ }
27579
+ return true;
27580
+ }
27399
27581
  const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
27400
27582
  if (sessionFilesMatch && req.method === "GET") {
27401
27583
  const sid = sessionFilesMatch[1];
@@ -27405,7 +27587,7 @@ function createHttpRouter(deps) {
27405
27587
  return true;
27406
27588
  }
27407
27589
  let absPath;
27408
- if (import_node_path17.default.isAbsolute(pathParam)) {
27590
+ if (import_node_path18.default.isAbsolute(pathParam)) {
27409
27591
  absPath = pathParam;
27410
27592
  } else if (deps.sessionStore) {
27411
27593
  const file = deps.sessionStore.read(sid);
@@ -27413,7 +27595,7 @@ function createHttpRouter(deps) {
27413
27595
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
27414
27596
  return true;
27415
27597
  }
27416
- absPath = import_node_path17.default.join(file.cwd, pathParam);
27598
+ absPath = import_node_path18.default.join(file.cwd, pathParam);
27417
27599
  } else {
27418
27600
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
27419
27601
  return true;
@@ -27446,7 +27628,7 @@ function withCtx(ctx, body) {
27446
27628
  function streamFile(res, absPath, logger) {
27447
27629
  let stat;
27448
27630
  try {
27449
- stat = import_node_fs15.default.statSync(absPath);
27631
+ stat = import_node_fs16.default.statSync(absPath);
27450
27632
  } catch (err) {
27451
27633
  const code = err?.code;
27452
27634
  if (code === "ENOENT") {
@@ -27461,7 +27643,7 @@ function streamFile(res, absPath, logger) {
27461
27643
  return;
27462
27644
  }
27463
27645
  const mime = lookupMime(absPath);
27464
- const basename = import_node_path17.default.basename(absPath);
27646
+ const basename = import_node_path18.default.basename(absPath);
27465
27647
  res.writeHead(200, {
27466
27648
  "Content-Type": mime,
27467
27649
  "Content-Length": String(stat.size),
@@ -27469,7 +27651,7 @@ function streamFile(res, absPath, logger) {
27469
27651
  // 防止浏览器把任意 mime 当 html 渲染
27470
27652
  "X-Content-Type-Options": "nosniff"
27471
27653
  });
27472
- const stream = import_node_fs15.default.createReadStream(absPath);
27654
+ const stream = import_node_fs16.default.createReadStream(absPath);
27473
27655
  stream.on("error", (err) => {
27474
27656
  logger?.warn("streamFile read error", { absPath, err: err.message });
27475
27657
  res.destroy();
@@ -27477,10 +27659,78 @@ function streamFile(res, absPath, logger) {
27477
27659
  stream.pipe(res);
27478
27660
  }
27479
27661
 
27662
+ // src/attachment/gc.ts
27663
+ var import_node_fs17 = __toESM(require("fs"), 1);
27664
+ var import_node_path19 = __toESM(require("path"), 1);
27665
+ var DEFAULT_TTL_MS = 30 * 24 * 3600 * 1e3;
27666
+ function runAttachmentGc(args) {
27667
+ const now = (args.now ?? Date.now)();
27668
+ const ttlMs = args.ttlMs ?? DEFAULT_TTL_MS;
27669
+ const liveAbs = /* @__PURE__ */ new Set();
27670
+ for (const { scope, sessionId } of args.sessionScopes) {
27671
+ for (const entry of args.groupFileStore.list(scope, sessionId)) {
27672
+ if (entry.stale) continue;
27673
+ if (import_node_path19.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
27674
+ }
27675
+ }
27676
+ for (const { scope, sessionId } of args.sessionScopes) {
27677
+ const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path19.default.join(
27678
+ args.dataDir,
27679
+ "sessions",
27680
+ ...scopeSubPath(scope).map(safeFileName),
27681
+ safeFileName(sessionId)
27682
+ );
27683
+ const attRoot = import_node_path19.default.join(sessionDir, ".attachments");
27684
+ let hashDirs;
27685
+ try {
27686
+ hashDirs = import_node_fs17.default.readdirSync(attRoot);
27687
+ } catch (err) {
27688
+ if (err.code === "ENOENT") continue;
27689
+ args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
27690
+ continue;
27691
+ }
27692
+ for (const hashDir of hashDirs) {
27693
+ const hashDirAbs = import_node_path19.default.join(attRoot, hashDir);
27694
+ let files;
27695
+ try {
27696
+ files = import_node_fs17.default.readdirSync(hashDirAbs);
27697
+ } catch {
27698
+ continue;
27699
+ }
27700
+ for (const name of files) {
27701
+ const file = import_node_path19.default.join(hashDirAbs, name);
27702
+ let stat;
27703
+ try {
27704
+ stat = import_node_fs17.default.statSync(file);
27705
+ } catch {
27706
+ continue;
27707
+ }
27708
+ if (!stat.isFile()) continue;
27709
+ const age = now - stat.mtimeMs;
27710
+ if (age < ttlMs) continue;
27711
+ if (liveAbs.has(file)) continue;
27712
+ try {
27713
+ import_node_fs17.default.unlinkSync(file);
27714
+ } catch (err) {
27715
+ args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
27716
+ }
27717
+ }
27718
+ try {
27719
+ if (import_node_fs17.default.readdirSync(hashDirAbs).length === 0) import_node_fs17.default.rmdirSync(hashDirAbs);
27720
+ } catch {
27721
+ }
27722
+ }
27723
+ try {
27724
+ if (import_node_fs17.default.readdirSync(attRoot).length === 0) import_node_fs17.default.rmdirSync(attRoot);
27725
+ } catch {
27726
+ }
27727
+ }
27728
+ }
27729
+
27480
27730
  // src/attachment/group.ts
27481
- var import_node_fs16 = __toESM(require("fs"), 1);
27482
- var import_node_path18 = __toESM(require("path"), 1);
27483
- var import_node_crypto6 = __toESM(require("crypto"), 1);
27731
+ var import_node_fs18 = __toESM(require("fs"), 1);
27732
+ var import_node_path20 = __toESM(require("path"), 1);
27733
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
27484
27734
  init_protocol();
27485
27735
  var GroupFileStore = class {
27486
27736
  dataDir;
@@ -27491,11 +27741,11 @@ var GroupFileStore = class {
27491
27741
  this.logger = opts.logger;
27492
27742
  }
27493
27743
  rootForScope(scope) {
27494
- return import_node_path18.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
27744
+ return import_node_path20.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
27495
27745
  }
27496
27746
  /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
27497
27747
  filePath(scope, sessionId) {
27498
- return import_node_path18.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
27748
+ return import_node_path20.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
27499
27749
  }
27500
27750
  cacheKey(scope, sessionId) {
27501
27751
  return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
@@ -27504,7 +27754,7 @@ var GroupFileStore = class {
27504
27754
  readFile(scope, sessionId) {
27505
27755
  const file = this.filePath(scope, sessionId);
27506
27756
  try {
27507
- const raw = import_node_fs16.default.readFileSync(file, "utf8");
27757
+ const raw = import_node_fs18.default.readFileSync(file, "utf8");
27508
27758
  const parsed = JSON.parse(raw);
27509
27759
  if (!Array.isArray(parsed)) {
27510
27760
  this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
@@ -27530,10 +27780,10 @@ var GroupFileStore = class {
27530
27780
  }
27531
27781
  writeFile(scope, sessionId, entries) {
27532
27782
  const file = this.filePath(scope, sessionId);
27533
- import_node_fs16.default.mkdirSync(import_node_path18.default.dirname(file), { recursive: true });
27783
+ import_node_fs18.default.mkdirSync(import_node_path20.default.dirname(file), { recursive: true });
27534
27784
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
27535
- import_node_fs16.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
27536
- import_node_fs16.default.renameSync(tmp, file);
27785
+ import_node_fs18.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
27786
+ import_node_fs18.default.renameSync(tmp, file);
27537
27787
  }
27538
27788
  /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
27539
27789
  list(scope, sessionId) {
@@ -27569,7 +27819,7 @@ var GroupFileStore = class {
27569
27819
  entries[idx] = next;
27570
27820
  } else {
27571
27821
  next = {
27572
- id: `gf-${import_node_crypto6.default.randomBytes(6).toString("base64url")}`,
27822
+ id: `gf-${import_node_crypto7.default.randomBytes(6).toString("base64url")}`,
27573
27823
  relPath: input.relPath,
27574
27824
  from: input.from,
27575
27825
  label: input.label,
@@ -27619,10 +27869,10 @@ var GroupFileStore = class {
27619
27869
  };
27620
27870
 
27621
27871
  // src/discovery/state-file.ts
27622
- var import_node_fs17 = __toESM(require("fs"), 1);
27623
- var import_node_path19 = __toESM(require("path"), 1);
27872
+ var import_node_fs19 = __toESM(require("fs"), 1);
27873
+ var import_node_path21 = __toESM(require("path"), 1);
27624
27874
  function defaultStateFilePath(dataDir) {
27625
- return import_node_path19.default.join(dataDir, "state.json");
27875
+ return import_node_path21.default.join(dataDir, "state.json");
27626
27876
  }
27627
27877
  function isPidAlive(pid) {
27628
27878
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -27644,7 +27894,7 @@ var StateFileManager = class {
27644
27894
  }
27645
27895
  read() {
27646
27896
  try {
27647
- const raw = import_node_fs17.default.readFileSync(this.file, "utf8");
27897
+ const raw = import_node_fs19.default.readFileSync(this.file, "utf8");
27648
27898
  const parsed = JSON.parse(raw);
27649
27899
  return parsed;
27650
27900
  } catch {
@@ -27658,34 +27908,34 @@ var StateFileManager = class {
27658
27908
  return { status: "stale", existing };
27659
27909
  }
27660
27910
  write(state) {
27661
- import_node_fs17.default.mkdirSync(import_node_path19.default.dirname(this.file), { recursive: true });
27911
+ import_node_fs19.default.mkdirSync(import_node_path21.default.dirname(this.file), { recursive: true });
27662
27912
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
27663
- import_node_fs17.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
27664
- import_node_fs17.default.renameSync(tmp, this.file);
27913
+ import_node_fs19.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
27914
+ import_node_fs19.default.renameSync(tmp, this.file);
27665
27915
  if (process.platform !== "win32") {
27666
27916
  try {
27667
- import_node_fs17.default.chmodSync(this.file, 384);
27917
+ import_node_fs19.default.chmodSync(this.file, 384);
27668
27918
  } catch {
27669
27919
  }
27670
27920
  }
27671
27921
  }
27672
27922
  delete() {
27673
27923
  try {
27674
- import_node_fs17.default.unlinkSync(this.file);
27924
+ import_node_fs19.default.unlinkSync(this.file);
27675
27925
  } catch {
27676
27926
  }
27677
27927
  }
27678
27928
  };
27679
27929
 
27680
27930
  // src/tunnel/tunnel-manager.ts
27681
- var import_node_fs21 = __toESM(require("fs"), 1);
27682
- var import_node_path23 = __toESM(require("path"), 1);
27683
- var import_node_crypto7 = __toESM(require("crypto"), 1);
27931
+ var import_node_fs23 = __toESM(require("fs"), 1);
27932
+ var import_node_path25 = __toESM(require("path"), 1);
27933
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
27684
27934
  var import_node_child_process5 = require("child_process");
27685
27935
 
27686
27936
  // src/tunnel/tunnel-store.ts
27687
- var import_node_fs18 = __toESM(require("fs"), 1);
27688
- var import_node_path20 = __toESM(require("path"), 1);
27937
+ var import_node_fs20 = __toESM(require("fs"), 1);
27938
+ var import_node_path22 = __toESM(require("path"), 1);
27689
27939
  var TunnelStore = class {
27690
27940
  constructor(filePath) {
27691
27941
  this.filePath = filePath;
@@ -27693,7 +27943,7 @@ var TunnelStore = class {
27693
27943
  filePath;
27694
27944
  async get() {
27695
27945
  try {
27696
- const raw = await import_node_fs18.default.promises.readFile(this.filePath, "utf8");
27946
+ const raw = await import_node_fs20.default.promises.readFile(this.filePath, "utf8");
27697
27947
  const obj = JSON.parse(raw);
27698
27948
  if (!isPersistedTunnel(obj)) return null;
27699
27949
  return obj;
@@ -27704,22 +27954,22 @@ var TunnelStore = class {
27704
27954
  }
27705
27955
  }
27706
27956
  async set(v2) {
27707
- const dir = import_node_path20.default.dirname(this.filePath);
27708
- await import_node_fs18.default.promises.mkdir(dir, { recursive: true });
27957
+ const dir = import_node_path22.default.dirname(this.filePath);
27958
+ await import_node_fs20.default.promises.mkdir(dir, { recursive: true });
27709
27959
  const data = JSON.stringify(v2, null, 2);
27710
27960
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
27711
- await import_node_fs18.default.promises.writeFile(tmp, data, { mode: 384 });
27961
+ await import_node_fs20.default.promises.writeFile(tmp, data, { mode: 384 });
27712
27962
  if (process.platform !== "win32") {
27713
27963
  try {
27714
- await import_node_fs18.default.promises.chmod(tmp, 384);
27964
+ await import_node_fs20.default.promises.chmod(tmp, 384);
27715
27965
  } catch {
27716
27966
  }
27717
27967
  }
27718
- await import_node_fs18.default.promises.rename(tmp, this.filePath);
27968
+ await import_node_fs20.default.promises.rename(tmp, this.filePath);
27719
27969
  }
27720
27970
  async clear() {
27721
27971
  try {
27722
- await import_node_fs18.default.promises.unlink(this.filePath);
27972
+ await import_node_fs20.default.promises.unlink(this.filePath);
27723
27973
  } catch (err) {
27724
27974
  const code = err?.code;
27725
27975
  if (code !== "ENOENT") throw err;
@@ -27814,12 +28064,12 @@ function escape(v2) {
27814
28064
  }
27815
28065
 
27816
28066
  // src/tunnel/frpc-binary.ts
27817
- var import_node_fs19 = __toESM(require("fs"), 1);
28067
+ var import_node_fs21 = __toESM(require("fs"), 1);
27818
28068
  var import_node_os10 = __toESM(require("os"), 1);
27819
- var import_node_path21 = __toESM(require("path"), 1);
28069
+ var import_node_path23 = __toESM(require("path"), 1);
27820
28070
  var import_node_child_process3 = require("child_process");
27821
28071
  var import_node_stream2 = require("stream");
27822
- var import_promises = require("stream/promises");
28072
+ var import_promises2 = require("stream/promises");
27823
28073
  var FRPC_VERSION = "0.68.0";
27824
28074
  var UnsupportedPlatformError = class extends Error {
27825
28075
  constructor(platform, arch) {
@@ -27848,20 +28098,20 @@ function frpcDownloadUrl(version2, p2) {
27848
28098
  }
27849
28099
  async function ensureFrpcBinary(opts) {
27850
28100
  if (opts.override) {
27851
- if (!import_node_fs19.default.existsSync(opts.override)) {
28101
+ if (!import_node_fs21.default.existsSync(opts.override)) {
27852
28102
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
27853
28103
  }
27854
28104
  return opts.override;
27855
28105
  }
27856
28106
  const version2 = opts.version ?? FRPC_VERSION;
27857
28107
  const platform = opts.platform ?? detectPlatform();
27858
- const binDir = import_node_path21.default.join(opts.dataDir, "bin");
27859
- import_node_fs19.default.mkdirSync(binDir, { recursive: true });
28108
+ const binDir = import_node_path23.default.join(opts.dataDir, "bin");
28109
+ import_node_fs21.default.mkdirSync(binDir, { recursive: true });
27860
28110
  cleanupStaleArtifacts(binDir);
27861
- const stableBin = import_node_path21.default.join(binDir, "frpc");
27862
- if (import_node_fs19.default.existsSync(stableBin)) return stableBin;
28111
+ const stableBin = import_node_path23.default.join(binDir, "frpc");
28112
+ if (import_node_fs21.default.existsSync(stableBin)) return stableBin;
27863
28113
  const partialBin = `${stableBin}.partial`;
27864
- const tarballPath = import_node_path21.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
28114
+ const tarballPath = import_node_path23.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
27865
28115
  try {
27866
28116
  const url = frpcDownloadUrl(version2, platform);
27867
28117
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -27870,8 +28120,8 @@ async function ensureFrpcBinary(opts) {
27870
28120
  } else {
27871
28121
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
27872
28122
  }
27873
- import_node_fs19.default.chmodSync(partialBin, 493);
27874
- import_node_fs19.default.renameSync(partialBin, stableBin);
28123
+ import_node_fs21.default.chmodSync(partialBin, 493);
28124
+ import_node_fs21.default.renameSync(partialBin, stableBin);
27875
28125
  } finally {
27876
28126
  safeUnlink(tarballPath);
27877
28127
  safeUnlink(partialBin);
@@ -27881,15 +28131,15 @@ async function ensureFrpcBinary(opts) {
27881
28131
  function cleanupStaleArtifacts(binDir) {
27882
28132
  let entries;
27883
28133
  try {
27884
- entries = import_node_fs19.default.readdirSync(binDir);
28134
+ entries = import_node_fs21.default.readdirSync(binDir);
27885
28135
  } catch {
27886
28136
  return;
27887
28137
  }
27888
28138
  for (const name of entries) {
27889
28139
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
27890
- const full = import_node_path21.default.join(binDir, name);
28140
+ const full = import_node_path23.default.join(binDir, name);
27891
28141
  try {
27892
- import_node_fs19.default.rmSync(full, { recursive: true, force: true });
28142
+ import_node_fs21.default.rmSync(full, { recursive: true, force: true });
27893
28143
  } catch {
27894
28144
  }
27895
28145
  }
@@ -27897,7 +28147,7 @@ function cleanupStaleArtifacts(binDir) {
27897
28147
  }
27898
28148
  function safeUnlink(p2) {
27899
28149
  try {
27900
- import_node_fs19.default.unlinkSync(p2);
28150
+ import_node_fs21.default.unlinkSync(p2);
27901
28151
  } catch {
27902
28152
  }
27903
28153
  }
@@ -27908,13 +28158,13 @@ async function downloadToFile(url, dest, fetchImpl) {
27908
28158
  if (!res.ok || !res.body) {
27909
28159
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
27910
28160
  }
27911
- const out = import_node_fs19.default.createWriteStream(dest);
28161
+ const out = import_node_fs21.default.createWriteStream(dest);
27912
28162
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
27913
- await (0, import_promises.pipeline)(nodeStream, out);
28163
+ await (0, import_promises2.pipeline)(nodeStream, out);
27914
28164
  }
27915
28165
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
27916
- const work = import_node_path21.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
27917
- import_node_fs19.default.mkdirSync(work, { recursive: true });
28166
+ const work = import_node_path23.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
28167
+ import_node_fs21.default.mkdirSync(work, { recursive: true });
27918
28168
  try {
27919
28169
  await new Promise((resolve6, reject) => {
27920
28170
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -27922,32 +28172,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
27922
28172
  proc.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
27923
28173
  });
27924
28174
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
27925
- const src = import_node_path21.default.join(work, dirName, "frpc");
27926
- if (!import_node_fs19.default.existsSync(src)) {
28175
+ const src = import_node_path23.default.join(work, dirName, "frpc");
28176
+ if (!import_node_fs21.default.existsSync(src)) {
27927
28177
  throw new Error(`frpc not found inside tarball at ${src}`);
27928
28178
  }
27929
- import_node_fs19.default.copyFileSync(src, destBin);
28179
+ import_node_fs21.default.copyFileSync(src, destBin);
27930
28180
  } finally {
27931
- import_node_fs19.default.rmSync(work, { recursive: true, force: true });
28181
+ import_node_fs21.default.rmSync(work, { recursive: true, force: true });
27932
28182
  }
27933
28183
  }
27934
28184
 
27935
28185
  // src/tunnel/frpc-process.ts
27936
- var import_node_fs20 = __toESM(require("fs"), 1);
27937
- var import_node_path22 = __toESM(require("path"), 1);
28186
+ var import_node_fs22 = __toESM(require("fs"), 1);
28187
+ var import_node_path24 = __toESM(require("path"), 1);
27938
28188
  var import_node_child_process4 = require("child_process");
27939
28189
  function frpcPidFilePath(dataDir) {
27940
- return import_node_path22.default.join(dataDir, "frpc.pid");
28190
+ return import_node_path24.default.join(dataDir, "frpc.pid");
27941
28191
  }
27942
28192
  function writeFrpcPid(dataDir, pid) {
27943
28193
  try {
27944
- import_node_fs20.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
28194
+ import_node_fs22.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
27945
28195
  } catch {
27946
28196
  }
27947
28197
  }
27948
28198
  function clearFrpcPid(dataDir) {
27949
28199
  try {
27950
- import_node_fs20.default.unlinkSync(frpcPidFilePath(dataDir));
28200
+ import_node_fs22.default.unlinkSync(frpcPidFilePath(dataDir));
27951
28201
  } catch {
27952
28202
  }
27953
28203
  }
@@ -27963,7 +28213,7 @@ function defaultIsPidAlive(pid) {
27963
28213
  }
27964
28214
  function defaultReadPidFile(file) {
27965
28215
  try {
27966
- return import_node_fs20.default.readFileSync(file, "utf8");
28216
+ return import_node_fs22.default.readFileSync(file, "utf8");
27967
28217
  } catch {
27968
28218
  return null;
27969
28219
  }
@@ -27979,7 +28229,7 @@ function defaultSleep(ms) {
27979
28229
  }
27980
28230
  async function killStaleFrpc(deps) {
27981
28231
  const pidFile = frpcPidFilePath(deps.dataDir);
27982
- const tomlPath = import_node_path22.default.join(deps.dataDir, "frpc.toml");
28232
+ const tomlPath = import_node_path24.default.join(deps.dataDir, "frpc.toml");
27983
28233
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
27984
28234
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
27985
28235
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -28003,7 +28253,7 @@ async function killStaleFrpc(deps) {
28003
28253
  }
28004
28254
  if (victims.size === 0) {
28005
28255
  try {
28006
- import_node_fs20.default.unlinkSync(pidFile);
28256
+ import_node_fs22.default.unlinkSync(pidFile);
28007
28257
  } catch {
28008
28258
  }
28009
28259
  return;
@@ -28014,7 +28264,7 @@ async function killStaleFrpc(deps) {
28014
28264
  }
28015
28265
  await sleep(deps.reapWaitMs ?? 300);
28016
28266
  try {
28017
- import_node_fs20.default.unlinkSync(pidFile);
28267
+ import_node_fs22.default.unlinkSync(pidFile);
28018
28268
  } catch {
28019
28269
  }
28020
28270
  }
@@ -28051,7 +28301,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
28051
28301
  var TunnelManager = class {
28052
28302
  constructor(deps) {
28053
28303
  this.deps = deps;
28054
- this.store = deps.store ?? new TunnelStore(import_node_path23.default.join(deps.dataDir, "tunnel.json"));
28304
+ this.store = deps.store ?? new TunnelStore(import_node_path25.default.join(deps.dataDir, "tunnel.json"));
28055
28305
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
28056
28306
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
28057
28307
  }
@@ -28178,8 +28428,8 @@ var TunnelManager = class {
28178
28428
  dataDir: this.deps.dataDir,
28179
28429
  override: this.deps.frpcBinaryOverride ?? void 0
28180
28430
  });
28181
- const tomlPath = import_node_path23.default.join(this.deps.dataDir, "frpc.toml");
28182
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
28431
+ const tomlPath = import_node_path25.default.join(this.deps.dataDir, "frpc.toml");
28432
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto8.default.randomBytes(3).toString("hex")}`;
28183
28433
  const toml = buildFrpcToml({
28184
28434
  serverAddr: t.frpsHost,
28185
28435
  serverPort: t.frpsPort,
@@ -28189,12 +28439,12 @@ var TunnelManager = class {
28189
28439
  localPort,
28190
28440
  logLevel: "info"
28191
28441
  });
28192
- await import_node_fs21.default.promises.writeFile(tomlPath, toml, { mode: 384 });
28442
+ await import_node_fs23.default.promises.writeFile(tomlPath, toml, { mode: 384 });
28193
28443
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
28194
28444
  stdio: ["ignore", "pipe", "pipe"]
28195
28445
  });
28196
- const logFilePath = import_node_path23.default.join(this.deps.dataDir, "frpc.log");
28197
- const logStream = import_node_fs21.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
28446
+ const logFilePath = import_node_path25.default.join(this.deps.dataDir, "frpc.log");
28447
+ const logStream = import_node_fs23.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
28198
28448
  logStream.on("error", () => {
28199
28449
  });
28200
28450
  const tee = (chunk) => {
@@ -28277,22 +28527,22 @@ async function waitForFrpcReady(proc, timeoutMs) {
28277
28527
 
28278
28528
  // src/tunnel/device-key.ts
28279
28529
  var import_node_os11 = __toESM(require("os"), 1);
28280
- var import_node_crypto8 = __toESM(require("crypto"), 1);
28530
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
28281
28531
  var DERIVE_SALT = "clawd-tunnel-device-v1";
28282
28532
  function deriveStableDeviceKey(opts = {}) {
28283
28533
  const hostname = opts.hostname ?? import_node_os11.default.hostname();
28284
28534
  const uid = opts.uid ?? (typeof import_node_os11.default.userInfo === "function" ? import_node_os11.default.userInfo().uid : 0);
28285
28535
  const input = `${hostname}::${uid}`;
28286
- return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
28536
+ return import_node_crypto9.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
28287
28537
  }
28288
28538
 
28289
28539
  // src/auth-store.ts
28290
- var import_node_fs22 = __toESM(require("fs"), 1);
28291
- var import_node_path24 = __toESM(require("path"), 1);
28292
- var import_node_crypto9 = __toESM(require("crypto"), 1);
28540
+ var import_node_fs24 = __toESM(require("fs"), 1);
28541
+ var import_node_path26 = __toESM(require("path"), 1);
28542
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
28293
28543
  var AUTH_FILE_NAME = "auth.json";
28294
28544
  function authFilePath(dataDir) {
28295
- return import_node_path24.default.join(dataDir, AUTH_FILE_NAME);
28545
+ return import_node_path26.default.join(dataDir, AUTH_FILE_NAME);
28296
28546
  }
28297
28547
  function loadOrCreateAuthFile(opts) {
28298
28548
  const file = authFilePath(opts.dataDir);
@@ -28317,14 +28567,14 @@ function loadOrCreateAuthFile(opts) {
28317
28567
  return next;
28318
28568
  }
28319
28569
  function defaultGenerateToken2() {
28320
- return import_node_crypto9.default.randomBytes(32).toString("base64url");
28570
+ return import_node_crypto10.default.randomBytes(32).toString("base64url");
28321
28571
  }
28322
28572
  function defaultGenerateOwnerPrincipalId() {
28323
- return `owner-${import_node_crypto9.default.randomUUID()}`;
28573
+ return `owner-${import_node_crypto10.default.randomUUID()}`;
28324
28574
  }
28325
28575
  function readAuthFile(file) {
28326
28576
  try {
28327
- const raw = import_node_fs22.default.readFileSync(file, "utf8");
28577
+ const raw = import_node_fs24.default.readFileSync(file, "utf8");
28328
28578
  const parsed = JSON.parse(raw);
28329
28579
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
28330
28580
  return null;
@@ -28342,25 +28592,25 @@ function readAuthFile(file) {
28342
28592
  }
28343
28593
  }
28344
28594
  function writeAuthFile(file, content) {
28345
- import_node_fs22.default.mkdirSync(import_node_path24.default.dirname(file), { recursive: true });
28346
- import_node_fs22.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
28595
+ import_node_fs24.default.mkdirSync(import_node_path26.default.dirname(file), { recursive: true });
28596
+ import_node_fs24.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
28347
28597
  try {
28348
- import_node_fs22.default.chmodSync(file, 384);
28598
+ import_node_fs24.default.chmodSync(file, 384);
28349
28599
  } catch {
28350
28600
  }
28351
28601
  }
28352
28602
 
28353
28603
  // src/owner-profile.ts
28354
- var import_node_fs23 = __toESM(require("fs"), 1);
28604
+ var import_node_fs25 = __toESM(require("fs"), 1);
28355
28605
  var import_node_os12 = __toESM(require("os"), 1);
28356
- var import_node_path25 = __toESM(require("path"), 1);
28606
+ var import_node_path27 = __toESM(require("path"), 1);
28357
28607
  var PROFILE_FILENAME = "profile.json";
28358
28608
  function loadOwnerDisplayName(dataDir) {
28359
28609
  const fallback = import_node_os12.default.userInfo().username;
28360
- const profilePath = import_node_path25.default.join(dataDir, PROFILE_FILENAME);
28610
+ const profilePath = import_node_path27.default.join(dataDir, PROFILE_FILENAME);
28361
28611
  let raw;
28362
28612
  try {
28363
- raw = import_node_fs23.default.readFileSync(profilePath, "utf8");
28613
+ raw = import_node_fs25.default.readFileSync(profilePath, "utf8");
28364
28614
  } catch {
28365
28615
  return fallback;
28366
28616
  }
@@ -28389,12 +28639,12 @@ init_protocol();
28389
28639
  init_protocol();
28390
28640
 
28391
28641
  // src/session/fork.ts
28392
- var import_node_fs24 = __toESM(require("fs"), 1);
28642
+ var import_node_fs26 = __toESM(require("fs"), 1);
28393
28643
  var import_node_os13 = __toESM(require("os"), 1);
28394
- var import_node_path26 = __toESM(require("path"), 1);
28644
+ var import_node_path28 = __toESM(require("path"), 1);
28395
28645
  init_claude_history();
28396
28646
  function readJsonlEntries(file) {
28397
- const raw = import_node_fs24.default.readFileSync(file, "utf8");
28647
+ const raw = import_node_fs26.default.readFileSync(file, "utf8");
28398
28648
  const out = [];
28399
28649
  for (const line of raw.split("\n")) {
28400
28650
  const t = line.trim();
@@ -28407,10 +28657,10 @@ function readJsonlEntries(file) {
28407
28657
  return out;
28408
28658
  }
28409
28659
  function forkSession(input) {
28410
- const baseDir = input.baseDir ?? import_node_path26.default.join(import_node_os13.default.homedir(), ".claude");
28411
- const projectDir = import_node_path26.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
28412
- const sourceFile = import_node_path26.default.join(projectDir, `${input.toolSessionId}.jsonl`);
28413
- if (!import_node_fs24.default.existsSync(sourceFile)) {
28660
+ const baseDir = input.baseDir ?? import_node_path28.default.join(import_node_os13.default.homedir(), ".claude");
28661
+ const projectDir = import_node_path28.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
28662
+ const sourceFile = import_node_path28.default.join(projectDir, `${input.toolSessionId}.jsonl`);
28663
+ if (!import_node_fs26.default.existsSync(sourceFile)) {
28414
28664
  throw new Error(`fork: source transcript not found: ${sourceFile}`);
28415
28665
  }
28416
28666
  const entries = readJsonlEntries(sourceFile);
@@ -28440,9 +28690,9 @@ function forkSession(input) {
28440
28690
  }
28441
28691
  forkedLines.push(JSON.stringify(forked));
28442
28692
  }
28443
- const forkedFilePath = import_node_path26.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
28444
- import_node_fs24.default.mkdirSync(projectDir, { recursive: true });
28445
- import_node_fs24.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
28693
+ const forkedFilePath = import_node_path28.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
28694
+ import_node_fs26.default.mkdirSync(projectDir, { recursive: true });
28695
+ import_node_fs26.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
28446
28696
  return { forkedToolSessionId, forkedFilePath };
28447
28697
  }
28448
28698
 
@@ -28730,28 +28980,6 @@ function buildSessionHandlers(deps) {
28730
28980
  }
28731
28981
  };
28732
28982
  };
28733
- const peerSessionUpsert = async (frame, _client, ctx) => {
28734
- if (ctx && ctx.principal.kind !== "owner") {
28735
- throw new ClawdError(
28736
- ERROR_CODES.UNAUTHORIZED,
28737
- `principal ${ctx.principal.kind}:${ctx.principal.id} cannot peerSession:upsert`
28738
- );
28739
- }
28740
- const file = PeerSessionUpsertArgs.parse(frame);
28741
- manager.mirrorUpsert(file);
28742
- return { response: { type: "peerSession:upsert:ok" }, broadcast: [] };
28743
- };
28744
- const peerSessionRemove = async (frame, _client, ctx) => {
28745
- if (ctx && ctx.principal.kind !== "owner") {
28746
- throw new ClawdError(
28747
- ERROR_CODES.UNAUTHORIZED,
28748
- `principal ${ctx.principal.kind}:${ctx.principal.id} cannot peerSession:remove`
28749
- );
28750
- }
28751
- const args = PeerSessionRemoveArgs.parse(frame);
28752
- manager.mirrorRemove(args.sessionId);
28753
- return { response: { type: "peerSession:remove:ok" }, broadcast: [] };
28754
- };
28755
28983
  return {
28756
28984
  "session:create": create,
28757
28985
  "session:list": list,
@@ -28777,9 +29005,7 @@ function buildSessionHandlers(deps) {
28777
29005
  "session:cancelQuestion": cancelQuestion,
28778
29006
  "session:pty:input": ptyInput,
28779
29007
  "session:pty:resize": ptyResize,
28780
- "session:pty:snapshot": ptySnapshot,
28781
- "peerSession:upsert": peerSessionUpsert,
28782
- "peerSession:remove": peerSessionRemove
29008
+ "session:pty:snapshot": ptySnapshot
28783
29009
  };
28784
29010
  }
28785
29011
 
@@ -28798,7 +29024,7 @@ function buildPermissionHandlers(deps) {
28798
29024
  }
28799
29025
 
28800
29026
  // src/handlers/history.ts
28801
- var path36 = __toESM(require("path"), 1);
29027
+ var path38 = __toESM(require("path"), 1);
28802
29028
  init_protocol();
28803
29029
 
28804
29030
  // src/session/recent-dirs.ts
@@ -28816,7 +29042,7 @@ function listRecentDirs(store, limit = 50) {
28816
29042
  }
28817
29043
 
28818
29044
  // src/permission/persona-paths.ts
28819
- var path35 = __toESM(require("path"), 1);
29045
+ var path37 = __toESM(require("path"), 1);
28820
29046
  function getAllowedPersonaIds(grants, action) {
28821
29047
  const ids = /* @__PURE__ */ new Set();
28822
29048
  for (const g2 of grants) {
@@ -28829,26 +29055,26 @@ function getAllowedPersonaIds(grants, action) {
28829
29055
  return ids;
28830
29056
  }
28831
29057
  function isGuestPathAllowed(grants, absPath, personaRoot, action = "read") {
28832
- const root = path35.resolve(personaRoot);
28833
- const target = path35.resolve(absPath);
28834
- const sep3 = root.endsWith(path35.sep) ? "" : path35.sep;
29058
+ const root = path37.resolve(personaRoot);
29059
+ const target = path37.resolve(absPath);
29060
+ const sep3 = root.endsWith(path37.sep) ? "" : path37.sep;
28835
29061
  if (!target.startsWith(root + sep3)) return false;
28836
- const rel = path35.relative(root, target);
29062
+ const rel = path37.relative(root, target);
28837
29063
  if (!rel || rel.startsWith("..")) return false;
28838
- const personaId = rel.split(path35.sep)[0];
29064
+ const personaId = rel.split(path37.sep)[0];
28839
29065
  if (!personaId) return false;
28840
29066
  const allowed = getAllowedPersonaIds(grants, action);
28841
29067
  if (allowed === "*") return true;
28842
29068
  return allowed.has(personaId);
28843
29069
  }
28844
29070
  function personaIdFromPath(absPath, personaRoot) {
28845
- const root = path35.resolve(personaRoot);
28846
- const target = path35.resolve(absPath);
28847
- const sep3 = root.endsWith(path35.sep) ? "" : path35.sep;
29071
+ const root = path37.resolve(personaRoot);
29072
+ const target = path37.resolve(absPath);
29073
+ const sep3 = root.endsWith(path37.sep) ? "" : path37.sep;
28848
29074
  if (!target.startsWith(root + sep3)) return null;
28849
- const rel = path35.relative(root, target);
29075
+ const rel = path37.relative(root, target);
28850
29076
  if (!rel || rel.startsWith("..")) return null;
28851
- const id = rel.split(path35.sep)[0];
29077
+ const id = rel.split(path37.sep)[0];
28852
29078
  return id || null;
28853
29079
  }
28854
29080
 
@@ -28873,7 +29099,7 @@ function buildHistoryHandlers(deps) {
28873
29099
  if (!pid) return false;
28874
29100
  return isGuestPathAllowed(
28875
29101
  ctx.grants,
28876
- path36.join(personaRoot, pid),
29102
+ path38.join(personaRoot, pid),
28877
29103
  personaRoot,
28878
29104
  "read"
28879
29105
  );
@@ -28884,7 +29110,7 @@ function buildHistoryHandlers(deps) {
28884
29110
  };
28885
29111
  const list = async (frame, _client, ctx) => {
28886
29112
  const args = HistoryListArgs.parse(frame);
28887
- assertGuestPath(ctx, path36.resolve(args.projectPath), personaRoot, "history:list");
29113
+ assertGuestPath(ctx, path38.resolve(args.projectPath), personaRoot, "history:list");
28888
29114
  const sessions = await history.listSessions(args);
28889
29115
  return { response: { type: "history:list", sessions } };
28890
29116
  };
@@ -28916,13 +29142,13 @@ function buildHistoryHandlers(deps) {
28916
29142
  };
28917
29143
  const subagents = async (frame, _client, ctx) => {
28918
29144
  const args = HistorySubagentsArgs.parse(frame);
28919
- assertGuestPath(ctx, path36.resolve(args.cwd), personaRoot, "history:subagents");
29145
+ assertGuestPath(ctx, path38.resolve(args.cwd), personaRoot, "history:subagents");
28920
29146
  const subs = await history.listSubagents(args);
28921
29147
  return { response: { type: "history:subagents", subagents: subs } };
28922
29148
  };
28923
29149
  const subagentRead = async (frame, _client, ctx) => {
28924
29150
  const args = HistorySubagentReadArgs.parse(frame);
28925
- assertGuestPath(ctx, path36.resolve(args.cwd), personaRoot, "history:subagent-read");
29151
+ assertGuestPath(ctx, path38.resolve(args.cwd), personaRoot, "history:subagent-read");
28926
29152
  const res = await history.readSubagent(args);
28927
29153
  return { response: { type: "history:subagent-read", ...res } };
28928
29154
  };
@@ -28930,7 +29156,7 @@ function buildHistoryHandlers(deps) {
28930
29156
  const dirs = listRecentDirs(store);
28931
29157
  if (ctx?.principal.kind === "guest" && personaRoot) {
28932
29158
  const filtered = dirs.filter(
28933
- (d) => isGuestPathAllowed(ctx.grants, path36.resolve(d.cwd), personaRoot, "read")
29159
+ (d) => isGuestPathAllowed(ctx.grants, path38.resolve(d.cwd), personaRoot, "read")
28934
29160
  );
28935
29161
  return { response: { type: "history:recentDirs", dirs: filtered } };
28936
29162
  }
@@ -28947,7 +29173,7 @@ function buildHistoryHandlers(deps) {
28947
29173
  }
28948
29174
 
28949
29175
  // src/handlers/workspace.ts
28950
- var path37 = __toESM(require("path"), 1);
29176
+ var path39 = __toESM(require("path"), 1);
28951
29177
  var os14 = __toESM(require("os"), 1);
28952
29178
  init_protocol();
28953
29179
  init_protocol();
@@ -28988,22 +29214,22 @@ function buildWorkspaceHandlers(deps) {
28988
29214
  const args = WorkspaceListArgs.parse(frame);
28989
29215
  const isGuest = ctx?.principal.kind === "guest";
28990
29216
  const fallbackCwd = isGuest && personaRoot ? personaRoot : os14.homedir();
28991
- const resolvedCwd = path37.resolve(args.cwd ?? fallbackCwd);
28992
- const target = args.path ? path37.resolve(resolvedCwd, args.path) : resolvedCwd;
29217
+ const resolvedCwd = path39.resolve(args.cwd ?? fallbackCwd);
29218
+ const target = args.path ? path39.resolve(resolvedCwd, args.path) : resolvedCwd;
28993
29219
  assertGuestPath2(ctx, target, personaRoot, "workspace:list");
28994
29220
  const res = workspace.list({ ...args, cwd: resolvedCwd });
28995
29221
  return { response: { type: "workspace:list", ...res } };
28996
29222
  };
28997
29223
  const read = async (frame, _client, ctx) => {
28998
29224
  const args = WorkspaceReadArgs.parse(frame);
28999
- const target = path37.isAbsolute(args.path) ? path37.resolve(args.path) : path37.resolve(args.cwd, args.path);
29225
+ const target = path39.isAbsolute(args.path) ? path39.resolve(args.path) : path39.resolve(args.cwd, args.path);
29000
29226
  assertGuestPath2(ctx, target, personaRoot, "workspace:read");
29001
29227
  const res = workspace.read(args);
29002
29228
  return { response: { type: "workspace:read", ...res } };
29003
29229
  };
29004
29230
  const skillsList = async (frame, _client, ctx) => {
29005
29231
  const args = SkillsListArgs.parse(frame);
29006
- const cwdAbs = path37.resolve(args.cwd);
29232
+ const cwdAbs = path39.resolve(args.cwd);
29007
29233
  assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list");
29008
29234
  const list2 = skills.list(args);
29009
29235
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -29015,7 +29241,7 @@ function buildWorkspaceHandlers(deps) {
29015
29241
  };
29016
29242
  const agentsList = async (frame, _client, ctx) => {
29017
29243
  const args = AgentsListArgs.parse(frame);
29018
- const cwdAbs = path37.resolve(args.cwd);
29244
+ const cwdAbs = path39.resolve(args.cwd);
29019
29245
  assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list");
29020
29246
  const list2 = agents.list(args);
29021
29247
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -29034,20 +29260,20 @@ function buildWorkspaceHandlers(deps) {
29034
29260
  }
29035
29261
 
29036
29262
  // src/handlers/git.ts
29037
- var path39 = __toESM(require("path"), 1);
29263
+ var path41 = __toESM(require("path"), 1);
29038
29264
  init_protocol();
29039
29265
  init_protocol();
29040
29266
 
29041
29267
  // src/workspace/git.ts
29042
29268
  var import_node_child_process6 = require("child_process");
29043
- var import_node_fs25 = __toESM(require("fs"), 1);
29044
- var import_node_path27 = __toESM(require("path"), 1);
29269
+ var import_node_fs27 = __toESM(require("fs"), 1);
29270
+ var import_node_path29 = __toESM(require("path"), 1);
29045
29271
  var import_node_util = require("util");
29046
29272
  var pexec = (0, import_node_util.promisify)(import_node_child_process6.execFile);
29047
29273
  function normalizePath(p2) {
29048
- const resolved = import_node_path27.default.resolve(p2);
29274
+ const resolved = import_node_path29.default.resolve(p2);
29049
29275
  try {
29050
- return import_node_fs25.default.realpathSync(resolved);
29276
+ return import_node_fs27.default.realpathSync(resolved);
29051
29277
  } catch {
29052
29278
  return resolved;
29053
29279
  }
@@ -29120,7 +29346,7 @@ async function listGitBranches(cwd) {
29120
29346
  // src/handlers/git.ts
29121
29347
  function assertGuestCwd(ctx, cwd, personaRoot, method) {
29122
29348
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
29123
- if (!isGuestPathAllowed(ctx.grants, path39.resolve(cwd), personaRoot, "read")) {
29349
+ if (!isGuestPathAllowed(ctx.grants, path41.resolve(cwd), personaRoot, "read")) {
29124
29350
  throw new ClawdError(
29125
29351
  ERROR_CODES.UNAUTHORIZED,
29126
29352
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -29607,7 +29833,7 @@ function buildPersonaHandlers(deps) {
29607
29833
  }
29608
29834
 
29609
29835
  // src/handlers/attachment.ts
29610
- var import_node_path28 = __toESM(require("path"), 1);
29836
+ var import_node_path30 = __toESM(require("path"), 1);
29611
29837
  init_protocol();
29612
29838
  init_protocol();
29613
29839
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -29661,12 +29887,12 @@ function buildAttachmentHandlers(deps) {
29661
29887
  `session ${args.sessionId} scope unresolved`
29662
29888
  );
29663
29889
  }
29664
- const cwdAbs = import_node_path28.default.resolve(sessionFile.cwd);
29665
- const candidateAbs = import_node_path28.default.isAbsolute(args.relPath) ? import_node_path28.default.resolve(args.relPath) : import_node_path28.default.resolve(cwdAbs, args.relPath);
29890
+ const cwdAbs = import_node_path30.default.resolve(sessionFile.cwd);
29891
+ const candidateAbs = import_node_path30.default.isAbsolute(args.relPath) ? import_node_path30.default.resolve(args.relPath) : import_node_path30.default.resolve(cwdAbs, args.relPath);
29666
29892
  assertGuestAttachmentPath(ctx, candidateAbs, deps.personaRoot, "attachment.signUrl");
29667
29893
  const entries = deps.groupFileStore.list(scope, args.sessionId);
29668
29894
  const entry = entries.find((e) => {
29669
- const storedAbs = import_node_path28.default.isAbsolute(e.relPath) ? import_node_path28.default.resolve(e.relPath) : import_node_path28.default.resolve(cwdAbs, e.relPath);
29895
+ const storedAbs = import_node_path30.default.isAbsolute(e.relPath) ? import_node_path30.default.resolve(e.relPath) : import_node_path30.default.resolve(cwdAbs, e.relPath);
29670
29896
  return storedAbs === candidateAbs && !e.stale;
29671
29897
  });
29672
29898
  if (!entry) {
@@ -29690,7 +29916,7 @@ function buildAttachmentHandlers(deps) {
29690
29916
  if (!ctx || ctx.principal.kind !== "guest" || !deps.personaRoot || !deps.sessionStore) return;
29691
29917
  const f = deps.sessionStore.read(sessionId);
29692
29918
  if (!f) return;
29693
- assertGuestAttachmentPath(ctx, import_node_path28.default.resolve(f.cwd), deps.personaRoot, method);
29919
+ assertGuestAttachmentPath(ctx, import_node_path30.default.resolve(f.cwd), deps.personaRoot, method);
29694
29920
  }
29695
29921
  const groupAdd = async (frame, _client, ctx) => {
29696
29922
  if (!deps.groupFileStore || !deps.getSessionScope) {
@@ -29731,7 +29957,7 @@ function buildAttachmentHandlers(deps) {
29731
29957
  }
29732
29958
  const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
29733
29959
  const target = entries.find((e) => e.relPath === parsed.data.relPath);
29734
- if (target?.from === "owner") {
29960
+ if (target?.from === "owner" || target?.from === "user-upload") {
29735
29961
  deps.groupFileStore.remove(scope, parsed.data.sessionId, parsed.data.relPath);
29736
29962
  } else {
29737
29963
  deps.groupFileStore.markStale(scope, parsed.data.sessionId, parsed.data.relPath);
@@ -29857,11 +30083,6 @@ var METHOD_GRANT_MAP = {
29857
30083
  "session:pin": CAPABILITY_SCOPED,
29858
30084
  "session:reorderPins": ADMIN_ANY,
29859
30085
  // owner 全局操作,无 personaId 维度
29860
- // mirror peer sessions (2026-05-26):owner-only — guest 不应能写/删本机 mirror SessionFile
29861
- // (会绕过 #739 隔离规则把伪造身份的 SessionFile 写进 default store)。handler 端内部 ctx
29862
- // 校验,dispatcher 层一刀切 admin 也兜底。
29863
- "peerSession:upsert": ADMIN_ANY,
29864
- "peerSession:remove": ADMIN_ANY,
29865
30086
  "permission:respond": CAPABILITY_SCOPED,
29866
30087
  "session:answerQuestion": CAPABILITY_SCOPED,
29867
30088
  "session:cancelQuestion": CAPABILITY_SCOPED,
@@ -29932,7 +30153,7 @@ function computeGrantForFrame(method, frame) {
29932
30153
  async function startDaemon(config) {
29933
30154
  const logger = createLogger({
29934
30155
  level: config.logLevel,
29935
- file: import_node_path29.default.join(config.dataDir, "clawd.log")
30156
+ file: import_node_path31.default.join(config.dataDir, "clawd.log")
29936
30157
  });
29937
30158
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
29938
30159
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -30019,7 +30240,7 @@ async function startDaemon(config) {
30019
30240
  const agents = new AgentsScanner();
30020
30241
  const history = new ClaudeHistoryReader();
30021
30242
  let transport = null;
30022
- const personaStore = new PersonaStore(import_node_path29.default.join(config.dataDir, "personas"));
30243
+ const personaStore = new PersonaStore(import_node_path31.default.join(config.dataDir, "personas"));
30023
30244
  const defaultsRoot = findDefaultsRoot();
30024
30245
  if (defaultsRoot) {
30025
30246
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -30036,7 +30257,7 @@ async function startDaemon(config) {
30036
30257
  getAdapter,
30037
30258
  historyReader: history,
30038
30259
  dataDir: config.dataDir,
30039
- personaRoot: import_node_path29.default.join(config.dataDir, "personas"),
30260
+ personaRoot: import_node_path31.default.join(config.dataDir, "personas"),
30040
30261
  personaStore,
30041
30262
  ownerDisplayName,
30042
30263
  ownerPrincipalId,
@@ -30060,10 +30281,10 @@ async function startDaemon(config) {
30060
30281
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
30061
30282
  attachmentGroup: {
30062
30283
  onFileEdit: (input) => {
30063
- const absPath = import_node_path29.default.isAbsolute(input.relPath) ? input.relPath : import_node_path29.default.join(input.cwd, input.relPath);
30284
+ const absPath = import_node_path31.default.isAbsolute(input.relPath) ? input.relPath : import_node_path31.default.join(input.cwd, input.relPath);
30064
30285
  let size = 0;
30065
30286
  try {
30066
- size = import_node_fs26.default.statSync(absPath).size;
30287
+ size = import_node_fs28.default.statSync(absPath).size;
30067
30288
  } catch (err) {
30068
30289
  logger.warn("attachment.onFileEdit stat failed", {
30069
30290
  sessionId: input.sessionId,
@@ -30181,10 +30402,10 @@ async function startDaemon(config) {
30181
30402
  // 'persona/<pid>/owner',default 走 'default'。
30182
30403
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
30183
30404
  // guest path guard:candidate 必须在 personaRoot 子树下
30184
- personaRoot: import_node_path29.default.join(config.dataDir, "personas")
30405
+ personaRoot: import_node_path31.default.join(config.dataDir, "personas")
30185
30406
  },
30186
30407
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
30187
- personaRoot: import_node_path29.default.join(config.dataDir, "personas"),
30408
+ personaRoot: import_node_path31.default.join(config.dataDir, "personas"),
30188
30409
  // capability:list / delete handler 依赖
30189
30410
  capabilityManager,
30190
30411
  // personal-cap:get 返回的 shareBaseUrl 用此 base URL:
@@ -30220,7 +30441,19 @@ async function startDaemon(config) {
30220
30441
  sessionStore: store,
30221
30442
  // /files HMAC verify 用 auth.json 的 signSecret 字段(与 attachment.signUrl 同源)。
30222
30443
  // --auth-token CLI 模式没 signSecret → 路由返 501,sign URL 功能整体禁用。
30223
- getSignSecret: () => authFile?.signSecret ?? null
30444
+ getSignSecret: () => authFile?.signSecret ?? null,
30445
+ // chatinput 附件上传(POST /attachments/upload,spec §6.1)。upload pipeline
30446
+ // 在 session.cwd 下的 .attachments/ 落盘;signSecret 与 /files HMAC 同源;
30447
+ // tunnel 未启时返 503 TUNNEL_NOT_READY 让前端显式提示。
30448
+ attachmentUpload: {
30449
+ groupFileStore,
30450
+ getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
30451
+ getSessionDir: (sid) => {
30452
+ const f = manager.findOwnedSession(sid);
30453
+ return f ? f.cwd : null;
30454
+ },
30455
+ getHttpBaseUrl: () => currentTunnelUrl ? getHttpBaseUrl() : null
30456
+ }
30224
30457
  });
30225
30458
  wsServer = new LocalWsServer({
30226
30459
  host: config.host,
@@ -30385,8 +30618,8 @@ async function startDaemon(config) {
30385
30618
  const lines = [
30386
30619
  `Tunnel: ${r.url}`,
30387
30620
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
30388
- `Frpc config: ${import_node_path29.default.join(config.dataDir, "frpc.toml")}`,
30389
- `Frpc log: ${import_node_path29.default.join(config.dataDir, "frpc.log")}`
30621
+ `Frpc config: ${import_node_path31.default.join(config.dataDir, "frpc.toml")}`,
30622
+ `Frpc log: ${import_node_path31.default.join(config.dataDir, "frpc.log")}`
30390
30623
  ];
30391
30624
  const width = Math.max(...lines.map((l) => l.length));
30392
30625
  const bar = "\u2550".repeat(width + 4);
@@ -30399,8 +30632,8 @@ ${bar}
30399
30632
 
30400
30633
  `);
30401
30634
  try {
30402
- const connectPath = import_node_path29.default.join(config.dataDir, "connect.txt");
30403
- import_node_fs26.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
30635
+ const connectPath = import_node_path31.default.join(config.dataDir, "connect.txt");
30636
+ import_node_fs28.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
30404
30637
  } catch {
30405
30638
  }
30406
30639
  } catch (err) {
@@ -30425,8 +30658,29 @@ ${bar}
30425
30658
  logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
30426
30659
  }
30427
30660
  }
30661
+ const tickAttachmentGc = () => {
30662
+ try {
30663
+ runAttachmentGc({
30664
+ dataDir: config.dataDir,
30665
+ groupFileStore,
30666
+ sessionScopes: manager.listOwnedSessionScopes().map(({ scope, sessionId }) => ({
30667
+ scope,
30668
+ sessionId
30669
+ })),
30670
+ // 注入 cwd 让 GC 直接定位 .attachments 目录(与 upload 落盘点一致)
30671
+ getSessionCwd: (sid) => manager.findOwnedSession(sid)?.cwd ?? null,
30672
+ logger
30673
+ });
30674
+ } catch (err) {
30675
+ logger.warn("attachment gc tick failed", { err: err.message });
30676
+ }
30677
+ };
30678
+ setImmediate(tickAttachmentGc);
30679
+ const attachmentGcInterval = setInterval(tickAttachmentGc, 24 * 3600 * 1e3);
30680
+ attachmentGcInterval.unref();
30428
30681
  const shutdown = async () => {
30429
30682
  logger.info("stopping clawd");
30683
+ clearInterval(attachmentGcInterval);
30430
30684
  observer.stopAll();
30431
30685
  manager.stopAll();
30432
30686
  if (tunnelMgr) {
@@ -30444,9 +30698,9 @@ ${bar}
30444
30698
  };
30445
30699
  }
30446
30700
  function migrateDropPersonsDir(dataDir) {
30447
- const dir = import_node_path29.default.join(dataDir, "persons");
30701
+ const dir = import_node_path31.default.join(dataDir, "persons");
30448
30702
  try {
30449
- import_node_fs26.default.rmSync(dir, { recursive: true, force: true });
30703
+ import_node_fs28.default.rmSync(dir, { recursive: true, force: true });
30450
30704
  } catch {
30451
30705
  }
30452
30706
  }