@clawos-dev/clawd 0.2.64 → 0.2.65-beta.107.ac81439
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1247 -270
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -110,6 +110,17 @@ var init_methods = __esm({
|
|
|
110
110
|
// 触发频率低(仅 window resize),response 也是 ack。
|
|
111
111
|
"session:pty:input",
|
|
112
112
|
"session:pty:resize",
|
|
113
|
+
// ---- attachment.* file-sharing(详见 attachment-schemas.ts) ----
|
|
114
|
+
// 命名警告:这里的 `attachment.*` RPC 与 CC v2.x 上行 `type:"attachment"` 系统行
|
|
115
|
+
// (attachment-skills / attachment-deferred-tools / attachment_memories)是不同概念。
|
|
116
|
+
// 全部管理类 RPC handler 入口 requireOwner(personal token 调任意一个都 403);
|
|
117
|
+
// 实际文件传输走 HTTP 路由(GET /files?p=&e=&s=,签名验证),不在白名单内。
|
|
118
|
+
"attachment.signUrl",
|
|
119
|
+
"attachment.groupAdd",
|
|
120
|
+
"attachment.groupRemove",
|
|
121
|
+
"attachment.groupList",
|
|
122
|
+
// v2:跨 session 聚合(本期 UI 不调,保留槽位用于 HTTP ACL 内部判定 / 未来 "All files" tab)
|
|
123
|
+
"attachment.groupListPersona",
|
|
113
124
|
"info",
|
|
114
125
|
"ping"
|
|
115
126
|
];
|
|
@@ -601,8 +612,8 @@ var init_parseUtil = __esm({
|
|
|
601
612
|
init_errors2();
|
|
602
613
|
init_en();
|
|
603
614
|
makeIssue = (params) => {
|
|
604
|
-
const { data, path:
|
|
605
|
-
const fullPath = [...
|
|
615
|
+
const { data, path: path31, errorMaps, issueData } = params;
|
|
616
|
+
const fullPath = [...path31, ...issueData.path || []];
|
|
606
617
|
const fullIssue = {
|
|
607
618
|
...issueData,
|
|
608
619
|
path: fullPath
|
|
@@ -913,11 +924,11 @@ var init_types = __esm({
|
|
|
913
924
|
init_parseUtil();
|
|
914
925
|
init_util();
|
|
915
926
|
ParseInputLazyPath = class {
|
|
916
|
-
constructor(parent, value,
|
|
927
|
+
constructor(parent, value, path31, key) {
|
|
917
928
|
this._cachedPath = [];
|
|
918
929
|
this.parent = parent;
|
|
919
930
|
this.data = value;
|
|
920
|
-
this._path =
|
|
931
|
+
this._path = path31;
|
|
921
932
|
this._key = key;
|
|
922
933
|
}
|
|
923
934
|
get path() {
|
|
@@ -4300,6 +4311,80 @@ var init_zod = __esm({
|
|
|
4300
4311
|
}
|
|
4301
4312
|
});
|
|
4302
4313
|
|
|
4314
|
+
// ../protocol/src/attachment-schemas.ts
|
|
4315
|
+
var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema, AttachmentGroupListPersonaArgs, AttachmentGroupListPersonaResponseSchema;
|
|
4316
|
+
var init_attachment_schemas = __esm({
|
|
4317
|
+
"../protocol/src/attachment-schemas.ts"() {
|
|
4318
|
+
"use strict";
|
|
4319
|
+
init_zod();
|
|
4320
|
+
TOKEN_ROLES = ["owner", "personal"];
|
|
4321
|
+
GROUP_FILE_SOURCES = ["agent", "owner"];
|
|
4322
|
+
GroupFileEntrySchema = external_exports.object({
|
|
4323
|
+
/** daemon 派发的稳定 id(用于 RPC remove / UI key) */
|
|
4324
|
+
id: external_exports.string().min(1),
|
|
4325
|
+
/** 相对 personaDir / sessionCwd 的路径 */
|
|
4326
|
+
relPath: external_exports.string().min(1),
|
|
4327
|
+
from: external_exports.enum(GROUP_FILE_SOURCES),
|
|
4328
|
+
/** owner 手动加入时的可选备注 */
|
|
4329
|
+
label: external_exports.string().optional(),
|
|
4330
|
+
/** 文件字节数(stat 时拍) */
|
|
4331
|
+
size: external_exports.number().int().nonnegative(),
|
|
4332
|
+
mime: external_exports.string().min(1),
|
|
4333
|
+
/** 加入清单时间戳(ms) */
|
|
4334
|
+
addedAt: external_exports.number().int().nonnegative(),
|
|
4335
|
+
/** 最近一次 agent Write/Edit 时间戳(agent 反复改同 path 时更新) */
|
|
4336
|
+
lastEditedAt: external_exports.number().int().nonnegative().optional(),
|
|
4337
|
+
/** agent rm / 文件不见了时打标;UI 灰显,不能再 share */
|
|
4338
|
+
stale: external_exports.boolean().optional()
|
|
4339
|
+
});
|
|
4340
|
+
AttachmentSignUrlArgs = external_exports.object({
|
|
4341
|
+
/** 要分享的绝对路径;签名只关心 absPath,不区分 persona/session */
|
|
4342
|
+
absPath: external_exports.string().min(1),
|
|
4343
|
+
/** TTL 秒数;缺省 24h;'never' 走 null 不带 exp 字段(永久有效) */
|
|
4344
|
+
ttlSeconds: external_exports.number().int().positive().nullable().optional()
|
|
4345
|
+
});
|
|
4346
|
+
AttachmentSignUrlResponseSchema = external_exports.object({
|
|
4347
|
+
/** 完整 URL(含 httpBaseUrl 前缀),UI 直接 window.open / 复制到剪贴板 */
|
|
4348
|
+
url: external_exports.string().min(1),
|
|
4349
|
+
/** 失效时间戳(ms);null = 永久 */
|
|
4350
|
+
expiresAt: external_exports.number().int().nonnegative().nullable()
|
|
4351
|
+
});
|
|
4352
|
+
AttachmentGroupAddArgs = external_exports.object({
|
|
4353
|
+
sessionId: external_exports.string().min(1),
|
|
4354
|
+
relPath: external_exports.string().min(1),
|
|
4355
|
+
/** owner 手动加入时可选备注;agent 自动入清单不走此 RPC */
|
|
4356
|
+
label: external_exports.string().optional()
|
|
4357
|
+
});
|
|
4358
|
+
AttachmentGroupAddResponseSchema = external_exports.object({
|
|
4359
|
+
entry: GroupFileEntrySchema
|
|
4360
|
+
});
|
|
4361
|
+
AttachmentGroupRemoveArgs = external_exports.object({
|
|
4362
|
+
sessionId: external_exports.string().min(1),
|
|
4363
|
+
relPath: external_exports.string().min(1)
|
|
4364
|
+
});
|
|
4365
|
+
AttachmentGroupRemoveResponseSchema = external_exports.object({
|
|
4366
|
+
removed: external_exports.literal(true)
|
|
4367
|
+
});
|
|
4368
|
+
AttachmentGroupListArgs = external_exports.object({
|
|
4369
|
+
sessionId: external_exports.string().min(1)
|
|
4370
|
+
});
|
|
4371
|
+
AttachmentGroupListResponseSchema = external_exports.object({
|
|
4372
|
+
entries: external_exports.array(GroupFileEntrySchema)
|
|
4373
|
+
});
|
|
4374
|
+
AttachmentGroupListPersonaArgs = external_exports.object({
|
|
4375
|
+
personaId: external_exports.string().min(1)
|
|
4376
|
+
});
|
|
4377
|
+
AttachmentGroupListPersonaResponseSchema = external_exports.object({
|
|
4378
|
+
perSession: external_exports.array(
|
|
4379
|
+
external_exports.object({
|
|
4380
|
+
sessionId: external_exports.string().min(1),
|
|
4381
|
+
entries: external_exports.array(GroupFileEntrySchema)
|
|
4382
|
+
})
|
|
4383
|
+
)
|
|
4384
|
+
});
|
|
4385
|
+
}
|
|
4386
|
+
});
|
|
4387
|
+
|
|
4303
4388
|
// ../protocol/src/persona-schemas.ts
|
|
4304
4389
|
var PersonaTokenEntrySchema, PersonaFileSchema, PersonaSkillSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema, PersonaAppendOwnerMessageArgsSchema, FRAME_TYPE_CHAT_OPEN, FRAME_TYPE_CHAT_RENAME, FRAME_TYPE_CHAT_DELETE, FRAME_TYPE_CHAT_LIST, FRAME_TYPE_CHAT_CREATED, FRAME_TYPE_CHAT_RENAMED, FRAME_TYPE_CHAT_DELETED, FRAME_TYPE_PERSONA_LISTENER_CHAT_CREATED, FRAME_TYPE_PERSONA_LISTENER_CHAT_RENAMED, FRAME_TYPE_PERSONA_LISTENER_CHAT_DELETED, FRAME_TYPE_PERSONA_LISTENER_STATUS, ChatSummarySchema, ChatOpenRequestSchema, ChatRenameRequestSchema, ChatDeleteRequestSchema, ChatRenameResponseSchema, ChatDeleteResponseSchema, ChatListPushSchema, ChatCreatedFrameSchema, ChatRenamedFrameSchema, ChatDeletedFrameSchema, PersonaListenerChatCreatedFrameSchema, PersonaListenerChatRenamedFrameSchema, PersonaListenerChatDeletedFrameSchema, PersonaListenerStatusFrameSchema, PersonaListListenerChatsForTokenArgsSchema, PersonaListListenerChatsForTokenResponseSchema;
|
|
4305
4390
|
var init_persona_schemas = __esm({
|
|
@@ -4505,6 +4590,7 @@ var init_schemas = __esm({
|
|
|
4505
4590
|
"use strict";
|
|
4506
4591
|
init_zod();
|
|
4507
4592
|
init_events();
|
|
4593
|
+
init_attachment_schemas();
|
|
4508
4594
|
init_persona_schemas();
|
|
4509
4595
|
SessionStatusSchema = external_exports.enum(SESSION_STATUS_VALUES);
|
|
4510
4596
|
UsageSchema = external_exports.object({
|
|
@@ -5061,7 +5147,22 @@ var init_schemas = __esm({
|
|
|
5061
5147
|
hostname: external_exports.string(),
|
|
5062
5148
|
os: external_exports.string(),
|
|
5063
5149
|
tools: external_exports.array(external_exports.object({ id: external_exports.string(), available: external_exports.boolean() })),
|
|
5064
|
-
runningSessions: external_exports.array(InfoRunningSessionSchema)
|
|
5150
|
+
runningSessions: external_exports.array(InfoRunningSessionSchema),
|
|
5151
|
+
// ── file-sharing 会话身份回执(spec: superpowers/specs/2026-05-19-clawd-file-sharing-design.md §8) ──
|
|
5152
|
+
// PR 1 阶段标 optional(daemon 还没下发,旧 daemon info 响应不带这五个字段);
|
|
5153
|
+
// PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
|
|
5154
|
+
// 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
|
|
5155
|
+
// 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
|
|
5156
|
+
/** 'owner' = 拿到 owner-token 的连接(不限来源 IP);'personal' = persona 派发的访客 token。来源 TOKEN_ROLES(spec §11 #1 中央真理源) */
|
|
5157
|
+
tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
|
|
5158
|
+
/** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
|
|
5159
|
+
tokenPersonaId: external_exports.string().min(1).optional(),
|
|
5160
|
+
/** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
|
|
5161
|
+
isLoopback: external_exports.boolean().optional(),
|
|
5162
|
+
/** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
|
|
5163
|
+
httpBaseUrl: external_exports.string().optional(),
|
|
5164
|
+
/** file-sharing HTTP 路由的 Authorization: Bearer token;与 WS token 解耦,HTTP 401 仅触发 ReAuth 不强踢 WS(spec §11 第 4 条) */
|
|
5165
|
+
httpToken: external_exports.string().optional()
|
|
5065
5166
|
});
|
|
5066
5167
|
}
|
|
5067
5168
|
});
|
|
@@ -5093,6 +5194,7 @@ var init_runtime = __esm({
|
|
|
5093
5194
|
init_frames();
|
|
5094
5195
|
init_persona_schemas();
|
|
5095
5196
|
init_persona_mode();
|
|
5197
|
+
init_attachment_schemas();
|
|
5096
5198
|
}
|
|
5097
5199
|
});
|
|
5098
5200
|
|
|
@@ -5367,8 +5469,8 @@ var require_req = __commonJS({
|
|
|
5367
5469
|
if (req.originalUrl) {
|
|
5368
5470
|
_req.url = req.originalUrl;
|
|
5369
5471
|
} else {
|
|
5370
|
-
const
|
|
5371
|
-
_req.url = typeof
|
|
5472
|
+
const path31 = req.path;
|
|
5473
|
+
_req.url = typeof path31 === "string" ? path31 : req.url ? req.url.path || req.url : void 0;
|
|
5372
5474
|
}
|
|
5373
5475
|
if (req.query) {
|
|
5374
5476
|
_req.query = req.query;
|
|
@@ -5533,14 +5635,14 @@ var require_redact = __commonJS({
|
|
|
5533
5635
|
}
|
|
5534
5636
|
return obj;
|
|
5535
5637
|
}
|
|
5536
|
-
function parsePath(
|
|
5638
|
+
function parsePath(path31) {
|
|
5537
5639
|
const parts = [];
|
|
5538
5640
|
let current = "";
|
|
5539
5641
|
let inBrackets = false;
|
|
5540
5642
|
let inQuotes = false;
|
|
5541
5643
|
let quoteChar = "";
|
|
5542
|
-
for (let i = 0; i <
|
|
5543
|
-
const char =
|
|
5644
|
+
for (let i = 0; i < path31.length; i++) {
|
|
5645
|
+
const char = path31[i];
|
|
5544
5646
|
if (!inBrackets && char === ".") {
|
|
5545
5647
|
if (current) {
|
|
5546
5648
|
parts.push(current);
|
|
@@ -5671,10 +5773,10 @@ var require_redact = __commonJS({
|
|
|
5671
5773
|
return current;
|
|
5672
5774
|
}
|
|
5673
5775
|
function redactPaths(obj, paths, censor, remove = false) {
|
|
5674
|
-
for (const
|
|
5675
|
-
const parts = parsePath(
|
|
5776
|
+
for (const path31 of paths) {
|
|
5777
|
+
const parts = parsePath(path31);
|
|
5676
5778
|
if (parts.includes("*")) {
|
|
5677
|
-
redactWildcardPath(obj, parts, censor,
|
|
5779
|
+
redactWildcardPath(obj, parts, censor, path31, remove);
|
|
5678
5780
|
} else {
|
|
5679
5781
|
if (remove) {
|
|
5680
5782
|
removeKey(obj, parts);
|
|
@@ -5759,8 +5861,8 @@ var require_redact = __commonJS({
|
|
|
5759
5861
|
}
|
|
5760
5862
|
} else {
|
|
5761
5863
|
if (afterWildcard.includes("*")) {
|
|
5762
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
5763
|
-
const fullPath = [...pathArray.slice(0, pathLength), ...
|
|
5864
|
+
const wrappedCensor = typeof censor === "function" ? (value, path31) => {
|
|
5865
|
+
const fullPath = [...pathArray.slice(0, pathLength), ...path31];
|
|
5764
5866
|
return censor(value, fullPath);
|
|
5765
5867
|
} : censor;
|
|
5766
5868
|
redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
|
|
@@ -5795,8 +5897,8 @@ var require_redact = __commonJS({
|
|
|
5795
5897
|
return null;
|
|
5796
5898
|
}
|
|
5797
5899
|
const pathStructure = /* @__PURE__ */ new Map();
|
|
5798
|
-
for (const
|
|
5799
|
-
const parts = parsePath(
|
|
5900
|
+
for (const path31 of pathsToClone) {
|
|
5901
|
+
const parts = parsePath(path31);
|
|
5800
5902
|
let current = pathStructure;
|
|
5801
5903
|
for (let i = 0; i < parts.length; i++) {
|
|
5802
5904
|
const part = parts[i];
|
|
@@ -5848,24 +5950,24 @@ var require_redact = __commonJS({
|
|
|
5848
5950
|
}
|
|
5849
5951
|
return cloneSelectively(obj, pathStructure);
|
|
5850
5952
|
}
|
|
5851
|
-
function validatePath(
|
|
5852
|
-
if (typeof
|
|
5953
|
+
function validatePath(path31) {
|
|
5954
|
+
if (typeof path31 !== "string") {
|
|
5853
5955
|
throw new Error("Paths must be (non-empty) strings");
|
|
5854
5956
|
}
|
|
5855
|
-
if (
|
|
5957
|
+
if (path31 === "") {
|
|
5856
5958
|
throw new Error("Invalid redaction path ()");
|
|
5857
5959
|
}
|
|
5858
|
-
if (
|
|
5859
|
-
throw new Error(`Invalid redaction path (${
|
|
5960
|
+
if (path31.includes("..")) {
|
|
5961
|
+
throw new Error(`Invalid redaction path (${path31})`);
|
|
5860
5962
|
}
|
|
5861
|
-
if (
|
|
5862
|
-
throw new Error(`Invalid redaction path (${
|
|
5963
|
+
if (path31.includes(",")) {
|
|
5964
|
+
throw new Error(`Invalid redaction path (${path31})`);
|
|
5863
5965
|
}
|
|
5864
5966
|
let bracketCount = 0;
|
|
5865
5967
|
let inQuotes = false;
|
|
5866
5968
|
let quoteChar = "";
|
|
5867
|
-
for (let i = 0; i <
|
|
5868
|
-
const char =
|
|
5969
|
+
for (let i = 0; i < path31.length; i++) {
|
|
5970
|
+
const char = path31[i];
|
|
5869
5971
|
if ((char === '"' || char === "'") && bracketCount > 0) {
|
|
5870
5972
|
if (!inQuotes) {
|
|
5871
5973
|
inQuotes = true;
|
|
@@ -5879,20 +5981,20 @@ var require_redact = __commonJS({
|
|
|
5879
5981
|
} else if (char === "]" && !inQuotes) {
|
|
5880
5982
|
bracketCount--;
|
|
5881
5983
|
if (bracketCount < 0) {
|
|
5882
|
-
throw new Error(`Invalid redaction path (${
|
|
5984
|
+
throw new Error(`Invalid redaction path (${path31})`);
|
|
5883
5985
|
}
|
|
5884
5986
|
}
|
|
5885
5987
|
}
|
|
5886
5988
|
if (bracketCount !== 0) {
|
|
5887
|
-
throw new Error(`Invalid redaction path (${
|
|
5989
|
+
throw new Error(`Invalid redaction path (${path31})`);
|
|
5888
5990
|
}
|
|
5889
5991
|
}
|
|
5890
5992
|
function validatePaths(paths) {
|
|
5891
5993
|
if (!Array.isArray(paths)) {
|
|
5892
5994
|
throw new TypeError("paths must be an array");
|
|
5893
5995
|
}
|
|
5894
|
-
for (const
|
|
5895
|
-
validatePath(
|
|
5996
|
+
for (const path31 of paths) {
|
|
5997
|
+
validatePath(path31);
|
|
5896
5998
|
}
|
|
5897
5999
|
}
|
|
5898
6000
|
function slowRedact(options = {}) {
|
|
@@ -6060,8 +6162,8 @@ var require_redaction = __commonJS({
|
|
|
6060
6162
|
if (shape[k2] === null) {
|
|
6061
6163
|
o[k2] = (value) => topCensor(value, [k2]);
|
|
6062
6164
|
} else {
|
|
6063
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
6064
|
-
return censor(value, [k2, ...
|
|
6165
|
+
const wrappedCensor = typeof censor === "function" ? (value, path31) => {
|
|
6166
|
+
return censor(value, [k2, ...path31]);
|
|
6065
6167
|
} : censor;
|
|
6066
6168
|
o[k2] = Redact({
|
|
6067
6169
|
paths: shape[k2],
|
|
@@ -6279,10 +6381,10 @@ var require_atomic_sleep = __commonJS({
|
|
|
6279
6381
|
var require_sonic_boom = __commonJS({
|
|
6280
6382
|
"../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
|
|
6281
6383
|
"use strict";
|
|
6282
|
-
var
|
|
6384
|
+
var fs28 = require("fs");
|
|
6283
6385
|
var EventEmitter2 = require("events");
|
|
6284
6386
|
var inherits = require("util").inherits;
|
|
6285
|
-
var
|
|
6387
|
+
var path31 = require("path");
|
|
6286
6388
|
var sleep = require_atomic_sleep();
|
|
6287
6389
|
var assert = require("assert");
|
|
6288
6390
|
var BUSY_WRITE_TIMEOUT = 100;
|
|
@@ -6336,20 +6438,20 @@ var require_sonic_boom = __commonJS({
|
|
|
6336
6438
|
const mode = sonic.mode;
|
|
6337
6439
|
if (sonic.sync) {
|
|
6338
6440
|
try {
|
|
6339
|
-
if (sonic.mkdir)
|
|
6340
|
-
const fd =
|
|
6441
|
+
if (sonic.mkdir) fs28.mkdirSync(path31.dirname(file), { recursive: true });
|
|
6442
|
+
const fd = fs28.openSync(file, flags, mode);
|
|
6341
6443
|
fileOpened(null, fd);
|
|
6342
6444
|
} catch (err) {
|
|
6343
6445
|
fileOpened(err);
|
|
6344
6446
|
throw err;
|
|
6345
6447
|
}
|
|
6346
6448
|
} else if (sonic.mkdir) {
|
|
6347
|
-
|
|
6449
|
+
fs28.mkdir(path31.dirname(file), { recursive: true }, (err) => {
|
|
6348
6450
|
if (err) return fileOpened(err);
|
|
6349
|
-
|
|
6451
|
+
fs28.open(file, flags, mode, fileOpened);
|
|
6350
6452
|
});
|
|
6351
6453
|
} else {
|
|
6352
|
-
|
|
6454
|
+
fs28.open(file, flags, mode, fileOpened);
|
|
6353
6455
|
}
|
|
6354
6456
|
}
|
|
6355
6457
|
function SonicBoom(opts) {
|
|
@@ -6390,8 +6492,8 @@ var require_sonic_boom = __commonJS({
|
|
|
6390
6492
|
this.flush = flushBuffer;
|
|
6391
6493
|
this.flushSync = flushBufferSync;
|
|
6392
6494
|
this._actualWrite = actualWriteBuffer;
|
|
6393
|
-
fsWriteSync = () =>
|
|
6394
|
-
fsWrite = () =>
|
|
6495
|
+
fsWriteSync = () => fs28.writeSync(this.fd, this._writingBuf);
|
|
6496
|
+
fsWrite = () => fs28.write(this.fd, this._writingBuf, this.release);
|
|
6395
6497
|
} else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
|
|
6396
6498
|
this._writingBuf = "";
|
|
6397
6499
|
this.write = write;
|
|
@@ -6400,15 +6502,15 @@ var require_sonic_boom = __commonJS({
|
|
|
6400
6502
|
this._actualWrite = actualWrite;
|
|
6401
6503
|
fsWriteSync = () => {
|
|
6402
6504
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
6403
|
-
return
|
|
6505
|
+
return fs28.writeSync(this.fd, this._writingBuf);
|
|
6404
6506
|
}
|
|
6405
|
-
return
|
|
6507
|
+
return fs28.writeSync(this.fd, this._writingBuf, "utf8");
|
|
6406
6508
|
};
|
|
6407
6509
|
fsWrite = () => {
|
|
6408
6510
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
6409
|
-
return
|
|
6511
|
+
return fs28.write(this.fd, this._writingBuf, this.release);
|
|
6410
6512
|
}
|
|
6411
|
-
return
|
|
6513
|
+
return fs28.write(this.fd, this._writingBuf, "utf8", this.release);
|
|
6412
6514
|
};
|
|
6413
6515
|
} else {
|
|
6414
6516
|
throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
|
|
@@ -6465,7 +6567,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6465
6567
|
}
|
|
6466
6568
|
}
|
|
6467
6569
|
if (this._fsync) {
|
|
6468
|
-
|
|
6570
|
+
fs28.fsyncSync(this.fd);
|
|
6469
6571
|
}
|
|
6470
6572
|
const len = this._len;
|
|
6471
6573
|
if (this._reopening) {
|
|
@@ -6579,7 +6681,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6579
6681
|
const onDrain = () => {
|
|
6580
6682
|
if (!this._fsync) {
|
|
6581
6683
|
try {
|
|
6582
|
-
|
|
6684
|
+
fs28.fsync(this.fd, (err) => {
|
|
6583
6685
|
this._flushPending = false;
|
|
6584
6686
|
cb(err);
|
|
6585
6687
|
});
|
|
@@ -6681,7 +6783,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6681
6783
|
const fd = this.fd;
|
|
6682
6784
|
this.once("ready", () => {
|
|
6683
6785
|
if (fd !== this.fd) {
|
|
6684
|
-
|
|
6786
|
+
fs28.close(fd, (err) => {
|
|
6685
6787
|
if (err) {
|
|
6686
6788
|
return this.emit("error", err);
|
|
6687
6789
|
}
|
|
@@ -6730,7 +6832,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6730
6832
|
buf = this._bufs[0];
|
|
6731
6833
|
}
|
|
6732
6834
|
try {
|
|
6733
|
-
const n = Buffer.isBuffer(buf) ?
|
|
6835
|
+
const n = Buffer.isBuffer(buf) ? fs28.writeSync(this.fd, buf) : fs28.writeSync(this.fd, buf, "utf8");
|
|
6734
6836
|
const releasedBufObj = releaseWritingBuf(buf, this._len, n);
|
|
6735
6837
|
buf = releasedBufObj.writingBuf;
|
|
6736
6838
|
this._len = releasedBufObj.len;
|
|
@@ -6746,7 +6848,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6746
6848
|
}
|
|
6747
6849
|
}
|
|
6748
6850
|
try {
|
|
6749
|
-
|
|
6851
|
+
fs28.fsyncSync(this.fd);
|
|
6750
6852
|
} catch {
|
|
6751
6853
|
}
|
|
6752
6854
|
}
|
|
@@ -6767,7 +6869,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6767
6869
|
buf = mergeBuf(this._bufs[0], this._lens[0]);
|
|
6768
6870
|
}
|
|
6769
6871
|
try {
|
|
6770
|
-
const n =
|
|
6872
|
+
const n = fs28.writeSync(this.fd, buf);
|
|
6771
6873
|
buf = buf.subarray(n);
|
|
6772
6874
|
this._len = Math.max(this._len - n, 0);
|
|
6773
6875
|
if (buf.length <= 0) {
|
|
@@ -6795,13 +6897,13 @@ var require_sonic_boom = __commonJS({
|
|
|
6795
6897
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
|
|
6796
6898
|
if (this.sync) {
|
|
6797
6899
|
try {
|
|
6798
|
-
const written = Buffer.isBuffer(this._writingBuf) ?
|
|
6900
|
+
const written = Buffer.isBuffer(this._writingBuf) ? fs28.writeSync(this.fd, this._writingBuf) : fs28.writeSync(this.fd, this._writingBuf, "utf8");
|
|
6799
6901
|
release(null, written);
|
|
6800
6902
|
} catch (err) {
|
|
6801
6903
|
release(err);
|
|
6802
6904
|
}
|
|
6803
6905
|
} else {
|
|
6804
|
-
|
|
6906
|
+
fs28.write(this.fd, this._writingBuf, release);
|
|
6805
6907
|
}
|
|
6806
6908
|
}
|
|
6807
6909
|
function actualWriteBuffer() {
|
|
@@ -6810,7 +6912,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6810
6912
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
|
|
6811
6913
|
if (this.sync) {
|
|
6812
6914
|
try {
|
|
6813
|
-
const written =
|
|
6915
|
+
const written = fs28.writeSync(this.fd, this._writingBuf);
|
|
6814
6916
|
release(null, written);
|
|
6815
6917
|
} catch (err) {
|
|
6816
6918
|
release(err);
|
|
@@ -6819,7 +6921,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6819
6921
|
if (kCopyBuffer) {
|
|
6820
6922
|
this._writingBuf = Buffer.from(this._writingBuf);
|
|
6821
6923
|
}
|
|
6822
|
-
|
|
6924
|
+
fs28.write(this.fd, this._writingBuf, release);
|
|
6823
6925
|
}
|
|
6824
6926
|
}
|
|
6825
6927
|
function actualClose(sonic) {
|
|
@@ -6835,12 +6937,12 @@ var require_sonic_boom = __commonJS({
|
|
|
6835
6937
|
sonic._lens = [];
|
|
6836
6938
|
assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
|
|
6837
6939
|
try {
|
|
6838
|
-
|
|
6940
|
+
fs28.fsync(sonic.fd, closeWrapped);
|
|
6839
6941
|
} catch {
|
|
6840
6942
|
}
|
|
6841
6943
|
function closeWrapped() {
|
|
6842
6944
|
if (sonic.fd !== 1 && sonic.fd !== 2) {
|
|
6843
|
-
|
|
6945
|
+
fs28.close(sonic.fd, done);
|
|
6844
6946
|
} else {
|
|
6845
6947
|
done();
|
|
6846
6948
|
}
|
|
@@ -9975,11 +10077,11 @@ var init_lib = __esm({
|
|
|
9975
10077
|
}
|
|
9976
10078
|
}
|
|
9977
10079
|
},
|
|
9978
|
-
addToPath: function addToPath(
|
|
9979
|
-
var last =
|
|
10080
|
+
addToPath: function addToPath(path31, added, removed, oldPosInc, options) {
|
|
10081
|
+
var last = path31.lastComponent;
|
|
9980
10082
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
9981
10083
|
return {
|
|
9982
|
-
oldPos:
|
|
10084
|
+
oldPos: path31.oldPos + oldPosInc,
|
|
9983
10085
|
lastComponent: {
|
|
9984
10086
|
count: last.count + 1,
|
|
9985
10087
|
added,
|
|
@@ -9989,7 +10091,7 @@ var init_lib = __esm({
|
|
|
9989
10091
|
};
|
|
9990
10092
|
} else {
|
|
9991
10093
|
return {
|
|
9992
|
-
oldPos:
|
|
10094
|
+
oldPos: path31.oldPos + oldPosInc,
|
|
9993
10095
|
lastComponent: {
|
|
9994
10096
|
count: 1,
|
|
9995
10097
|
added,
|
|
@@ -10420,10 +10522,10 @@ function attachmentToHistoryMessage(o, ts) {
|
|
|
10420
10522
|
const memories = raw.map((m2) => {
|
|
10421
10523
|
if (!m2 || typeof m2 !== "object") return null;
|
|
10422
10524
|
const rec = m2;
|
|
10423
|
-
const
|
|
10525
|
+
const path31 = typeof rec.path === "string" ? rec.path : null;
|
|
10424
10526
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
10425
|
-
if (!
|
|
10426
|
-
const entry = { path:
|
|
10527
|
+
if (!path31 || content == null) return null;
|
|
10528
|
+
const entry = { path: path31, content };
|
|
10427
10529
|
if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
|
|
10428
10530
|
return entry;
|
|
10429
10531
|
}).filter((m2) => m2 !== null);
|
|
@@ -11227,10 +11329,10 @@ function parseAttachment(obj) {
|
|
|
11227
11329
|
const memories = raw.map((m2) => {
|
|
11228
11330
|
if (!m2 || typeof m2 !== "object") return null;
|
|
11229
11331
|
const rec = m2;
|
|
11230
|
-
const
|
|
11332
|
+
const path31 = typeof rec.path === "string" ? rec.path : null;
|
|
11231
11333
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
11232
|
-
if (!
|
|
11233
|
-
const out = { path:
|
|
11334
|
+
if (!path31 || content == null) return null;
|
|
11335
|
+
const out = { path: path31, content };
|
|
11234
11336
|
if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
|
|
11235
11337
|
return out;
|
|
11236
11338
|
}).filter((m2) => m2 !== null);
|
|
@@ -18725,7 +18827,7 @@ var require_websocket = __commonJS({
|
|
|
18725
18827
|
"use strict";
|
|
18726
18828
|
var EventEmitter2 = require("events");
|
|
18727
18829
|
var https = require("https");
|
|
18728
|
-
var
|
|
18830
|
+
var http2 = require("http");
|
|
18729
18831
|
var net = require("net");
|
|
18730
18832
|
var tls = require("tls");
|
|
18731
18833
|
var { randomBytes, createHash } = require("crypto");
|
|
@@ -19259,7 +19361,7 @@ var require_websocket = __commonJS({
|
|
|
19259
19361
|
}
|
|
19260
19362
|
const defaultPort = isSecure ? 443 : 80;
|
|
19261
19363
|
const key = randomBytes(16).toString("base64");
|
|
19262
|
-
const request = isSecure ? https.request :
|
|
19364
|
+
const request = isSecure ? https.request : http2.request;
|
|
19263
19365
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
19264
19366
|
let perMessageDeflate;
|
|
19265
19367
|
opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
|
|
@@ -19753,7 +19855,7 @@ var require_websocket_server = __commonJS({
|
|
|
19753
19855
|
"../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
|
|
19754
19856
|
"use strict";
|
|
19755
19857
|
var EventEmitter2 = require("events");
|
|
19756
|
-
var
|
|
19858
|
+
var http2 = require("http");
|
|
19757
19859
|
var { Duplex } = require("stream");
|
|
19758
19860
|
var { createHash } = require("crypto");
|
|
19759
19861
|
var extension2 = require_extension();
|
|
@@ -19828,8 +19930,8 @@ var require_websocket_server = __commonJS({
|
|
|
19828
19930
|
);
|
|
19829
19931
|
}
|
|
19830
19932
|
if (options.port != null) {
|
|
19831
|
-
this._server =
|
|
19832
|
-
const body =
|
|
19933
|
+
this._server = http2.createServer((req, res) => {
|
|
19934
|
+
const body = http2.STATUS_CODES[426];
|
|
19833
19935
|
res.writeHead(426, {
|
|
19834
19936
|
"Content-Length": body.length,
|
|
19835
19937
|
"Content-Type": "text/plain"
|
|
@@ -20116,7 +20218,7 @@ var require_websocket_server = __commonJS({
|
|
|
20116
20218
|
this.destroy();
|
|
20117
20219
|
}
|
|
20118
20220
|
function abortHandshake(socket, code, message, headers) {
|
|
20119
|
-
message = message ||
|
|
20221
|
+
message = message || http2.STATUS_CODES[code];
|
|
20120
20222
|
headers = {
|
|
20121
20223
|
Connection: "close",
|
|
20122
20224
|
"Content-Type": "text/html",
|
|
@@ -20125,7 +20227,7 @@ var require_websocket_server = __commonJS({
|
|
|
20125
20227
|
};
|
|
20126
20228
|
socket.once("finish", socket.destroy);
|
|
20127
20229
|
socket.end(
|
|
20128
|
-
`HTTP/1.1 ${code} ${
|
|
20230
|
+
`HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
|
|
20129
20231
|
` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
|
|
20130
20232
|
);
|
|
20131
20233
|
}
|
|
@@ -20144,7 +20246,7 @@ var require_websocket_server = __commonJS({
|
|
|
20144
20246
|
// src/run-case/recorder.ts
|
|
20145
20247
|
function startRunCaseRecorder(opts) {
|
|
20146
20248
|
const now = opts.now ?? Date.now;
|
|
20147
|
-
const dir =
|
|
20249
|
+
const dir = import_node_path27.default.dirname(opts.recordPath);
|
|
20148
20250
|
let stream = null;
|
|
20149
20251
|
let closing = false;
|
|
20150
20252
|
let closedSettled = false;
|
|
@@ -20158,8 +20260,8 @@ function startRunCaseRecorder(opts) {
|
|
|
20158
20260
|
});
|
|
20159
20261
|
const ensureStream = () => {
|
|
20160
20262
|
if (stream) return stream;
|
|
20161
|
-
|
|
20162
|
-
stream =
|
|
20263
|
+
import_node_fs25.default.mkdirSync(dir, { recursive: true });
|
|
20264
|
+
stream = import_node_fs25.default.createWriteStream(opts.recordPath, { flags: "a" });
|
|
20163
20265
|
stream.on("close", () => closedResolve());
|
|
20164
20266
|
return stream;
|
|
20165
20267
|
};
|
|
@@ -20184,12 +20286,12 @@ function startRunCaseRecorder(opts) {
|
|
|
20184
20286
|
};
|
|
20185
20287
|
return { tap, close, closed };
|
|
20186
20288
|
}
|
|
20187
|
-
var
|
|
20289
|
+
var import_node_fs25, import_node_path27;
|
|
20188
20290
|
var init_recorder = __esm({
|
|
20189
20291
|
"src/run-case/recorder.ts"() {
|
|
20190
20292
|
"use strict";
|
|
20191
|
-
|
|
20192
|
-
|
|
20293
|
+
import_node_fs25 = __toESM(require("fs"), 1);
|
|
20294
|
+
import_node_path27 = __toESM(require("path"), 1);
|
|
20193
20295
|
}
|
|
20194
20296
|
});
|
|
20195
20297
|
|
|
@@ -20232,7 +20334,7 @@ var init_wire = __esm({
|
|
|
20232
20334
|
// src/run-case/controller.ts
|
|
20233
20335
|
async function runController(opts) {
|
|
20234
20336
|
const now = opts.now ?? Date.now;
|
|
20235
|
-
const cwd = opts.cwd ?? (0,
|
|
20337
|
+
const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path28.default.join(import_node_os15.default.tmpdir(), "clawd-runcase-"));
|
|
20236
20338
|
const ownsCwd = opts.cwd === void 0;
|
|
20237
20339
|
const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
|
|
20238
20340
|
const spawnCtx = { cwd };
|
|
@@ -20393,19 +20495,19 @@ async function runController(opts) {
|
|
|
20393
20495
|
if (sigintHandler) process.off("SIGINT", sigintHandler);
|
|
20394
20496
|
if (ownsCwd) {
|
|
20395
20497
|
try {
|
|
20396
|
-
(0,
|
|
20498
|
+
(0, import_node_fs26.rmSync)(cwd, { recursive: true, force: true });
|
|
20397
20499
|
} catch {
|
|
20398
20500
|
}
|
|
20399
20501
|
}
|
|
20400
20502
|
return exitCode ?? 0;
|
|
20401
20503
|
}
|
|
20402
|
-
var
|
|
20504
|
+
var import_node_fs26, import_node_os15, import_node_path28;
|
|
20403
20505
|
var init_controller = __esm({
|
|
20404
20506
|
"src/run-case/controller.ts"() {
|
|
20405
20507
|
"use strict";
|
|
20406
|
-
|
|
20508
|
+
import_node_fs26 = require("fs");
|
|
20407
20509
|
import_node_os15 = __toESM(require("os"), 1);
|
|
20408
|
-
|
|
20510
|
+
import_node_path28 = __toESM(require("path"), 1);
|
|
20409
20511
|
init_claude();
|
|
20410
20512
|
init_stdout_splitter();
|
|
20411
20513
|
init_permission_stdio();
|
|
@@ -20637,8 +20739,8 @@ Env (advanced):
|
|
|
20637
20739
|
`;
|
|
20638
20740
|
|
|
20639
20741
|
// src/index.ts
|
|
20640
|
-
var
|
|
20641
|
-
var
|
|
20742
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
20743
|
+
var import_node_fs24 = __toESM(require("fs"), 1);
|
|
20642
20744
|
|
|
20643
20745
|
// src/logger.ts
|
|
20644
20746
|
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
@@ -21630,6 +21732,11 @@ var SessionRunner = class {
|
|
|
21630
21732
|
// sub-session idle-kill timer ref;key = sessionId(实际同一 runner 只对应一个 session,
|
|
21631
21733
|
// 但 reducer Effect schema 带 sessionId 作为唯一键,对齐 schedule/cancel 配对)
|
|
21632
21734
|
idleKillTimers = /* @__PURE__ */ new Map();
|
|
21735
|
+
// file-sharing 待配对的 file-edit tool_use(spec §6 PR 3):在同一 session 流里
|
|
21736
|
+
// tool_use(kind='tool_call')与 tool_result 通过 toolUseId 配对。tool_use 提前到,
|
|
21737
|
+
// 等 tool_result 来时反查 path 调 onFileEdit。条目 in-flight 期间很少(个位数),
|
|
21738
|
+
// 不需要 LRU;session 退出时整个 runner 销毁自然清理。
|
|
21739
|
+
pendingFileEdits = /* @__PURE__ */ new Map();
|
|
21633
21740
|
getState() {
|
|
21634
21741
|
return this.state;
|
|
21635
21742
|
}
|
|
@@ -21638,7 +21745,13 @@ var SessionRunner = class {
|
|
|
21638
21745
|
const personaStore = this.hooks.personaStore;
|
|
21639
21746
|
const adapter = this.hooks.adapter;
|
|
21640
21747
|
const deps = {
|
|
21641
|
-
|
|
21748
|
+
// file-sharing (spec §6 PR 3):在 stdout-line 解析时同步观察 file-edit 工具事件,
|
|
21749
|
+
// 配对 tool_use ↔ tool_result 调 onFileEdit。原 reducer 路径不变。
|
|
21750
|
+
parseLine: (l) => {
|
|
21751
|
+
const events = adapter.parseLine(l);
|
|
21752
|
+
if (this.hooks.onFileEdit) this.observeForFileEdit(events);
|
|
21753
|
+
return events;
|
|
21754
|
+
},
|
|
21642
21755
|
// persona mention injection 在 deps 装配处包一层,对 reducer 完全透明。
|
|
21643
21756
|
// 命中 mention 时拆成两条 stdin 帧(先 system-reminder 块、后 user 原文):
|
|
21644
21757
|
// - CC stream-json 按行处理,落盘是两条独立 user message;
|
|
@@ -21706,8 +21819,54 @@ var SessionRunner = class {
|
|
|
21706
21819
|
// reducer 的 batch dedup 按 events[0].uuid 整组处理,避免跨路径重复。
|
|
21707
21820
|
feedObserverEvents(events) {
|
|
21708
21821
|
if (events.length === 0) return;
|
|
21822
|
+
if (this.hooks.onFileEdit) this.observeForFileEdit(events);
|
|
21709
21823
|
this.input({ kind: "inject-events", events });
|
|
21710
21824
|
}
|
|
21825
|
+
/**
|
|
21826
|
+
* file-sharing tool_use ↔ tool_result 配对(spec §6 PR 3)。
|
|
21827
|
+
*
|
|
21828
|
+
* 1) tool_call kind + tool ∈ FILE_EDIT_TOOLS → cache toolUseId → { tool, relPath }
|
|
21829
|
+
* relPath 从 input 里取(Write/Edit/MultiEdit 是 file_path,NotebookEdit 是 notebook_path)。
|
|
21830
|
+
* 2) tool_result kind + cache 命中 + 非 error → 调 onFileEdit + 删 cache
|
|
21831
|
+
* 3) tool_result kind + cache 命中 + 是 error → 删 cache,不调(spec 只要"成功"才入清单)
|
|
21832
|
+
* 4) tool_call 但 input 没有 path → 不 cache(防御)
|
|
21833
|
+
*
|
|
21834
|
+
* 不破坏 reducer 路径——纯只读扫描 events 数组。
|
|
21835
|
+
*/
|
|
21836
|
+
observeForFileEdit(events) {
|
|
21837
|
+
const cb = this.hooks.onFileEdit;
|
|
21838
|
+
if (!cb) return;
|
|
21839
|
+
for (const ev of events) {
|
|
21840
|
+
if (ev.kind === "tool_call") {
|
|
21841
|
+
const tool = ev.tool;
|
|
21842
|
+
if (!isFileEditTool(tool)) continue;
|
|
21843
|
+
const relPath = extractEditPath(ev.input);
|
|
21844
|
+
if (!relPath) continue;
|
|
21845
|
+
this.pendingFileEdits.set(ev.toolUseId, {
|
|
21846
|
+
tool,
|
|
21847
|
+
relPath
|
|
21848
|
+
});
|
|
21849
|
+
} else if (ev.kind === "tool_result") {
|
|
21850
|
+
const pending = this.pendingFileEdits.get(ev.toolUseId);
|
|
21851
|
+
if (!pending) continue;
|
|
21852
|
+
this.pendingFileEdits.delete(ev.toolUseId);
|
|
21853
|
+
if (ev.error != null) continue;
|
|
21854
|
+
try {
|
|
21855
|
+
cb({
|
|
21856
|
+
toolUseId: ev.toolUseId,
|
|
21857
|
+
tool: pending.tool,
|
|
21858
|
+
relPath: pending.relPath,
|
|
21859
|
+
cwd: this.state.file.cwd
|
|
21860
|
+
});
|
|
21861
|
+
} catch (err) {
|
|
21862
|
+
this.hooks.logger?.warn("onFileEdit hook threw", {
|
|
21863
|
+
err: err.message,
|
|
21864
|
+
sessionId: this.state.file.sessionId
|
|
21865
|
+
});
|
|
21866
|
+
}
|
|
21867
|
+
}
|
|
21868
|
+
}
|
|
21869
|
+
}
|
|
21711
21870
|
// 向子进程 stdin 写一条 CC IPC `control_request` 并返回 Promise<response>
|
|
21712
21871
|
// —— CC 侧会异步回写匹配 request_id 的 `control_response` 帧,
|
|
21713
21872
|
// 在 stdout 行处理入口被 tryHandleControlResponse 拦截并 resolve pending
|
|
@@ -21897,6 +22056,15 @@ var SessionRunner = class {
|
|
|
21897
22056
|
}
|
|
21898
22057
|
}
|
|
21899
22058
|
};
|
|
22059
|
+
function isFileEditTool(name) {
|
|
22060
|
+
return name === "Write" || name === "Edit" || name === "MultiEdit" || name === "NotebookEdit";
|
|
22061
|
+
}
|
|
22062
|
+
function extractEditPath(input) {
|
|
22063
|
+
if (!input || typeof input !== "object") return null;
|
|
22064
|
+
const o = input;
|
|
22065
|
+
const candidate = typeof o.file_path === "string" && o.file_path || typeof o.notebook_path === "string" && o.notebook_path || typeof o.path === "string" && o.path || null;
|
|
22066
|
+
return candidate || null;
|
|
22067
|
+
}
|
|
21900
22068
|
|
|
21901
22069
|
// src/session/manager.ts
|
|
21902
22070
|
function compressFrameForWire(frame) {
|
|
@@ -21953,16 +22121,6 @@ function nowIso2(deps) {
|
|
|
21953
22121
|
function newSessionId() {
|
|
21954
22122
|
return v4_default().replace(/-/g, "").slice(0, 16);
|
|
21955
22123
|
}
|
|
21956
|
-
function derivePersonaSpawnCwd(file, personaRoot) {
|
|
21957
|
-
const personaId = file.ownerPersonaId;
|
|
21958
|
-
if (!personaId) return file.cwd;
|
|
21959
|
-
if (!personaRoot) {
|
|
21960
|
-
throw new Error(
|
|
21961
|
-
`derivePersonaSpawnCwd: personaRoot missing for owner session ${file.sessionId} (ownerPersonaId=${personaId})`
|
|
21962
|
-
);
|
|
21963
|
-
}
|
|
21964
|
-
return import_node_path6.default.join(personaRoot, safeFileName(personaId));
|
|
21965
|
-
}
|
|
21966
22124
|
function makeInitialState(file, subSessionMeta) {
|
|
21967
22125
|
return {
|
|
21968
22126
|
file,
|
|
@@ -22121,24 +22279,7 @@ var SessionManager = class {
|
|
|
22121
22279
|
const adapter = this.deps.getAdapter(file.tool ?? "claude");
|
|
22122
22280
|
const store = this.storeFor(scope);
|
|
22123
22281
|
const subSessionMeta = metaFromScope(scope, this.deps.personaRoot ?? "");
|
|
22124
|
-
const
|
|
22125
|
-
if (correctedCwd !== file.cwd) {
|
|
22126
|
-
this.deps.logger?.warn("owner session cwd drifted from persona dir; auto-correcting", {
|
|
22127
|
-
sessionId: file.sessionId,
|
|
22128
|
-
ownerPersonaId: file.ownerPersonaId,
|
|
22129
|
-
stale: file.cwd,
|
|
22130
|
-
corrected: correctedCwd
|
|
22131
|
-
});
|
|
22132
|
-
file = { ...file, cwd: correctedCwd, updatedAt: nowIso2(this.deps) };
|
|
22133
|
-
try {
|
|
22134
|
-
store.write(file);
|
|
22135
|
-
} catch (err) {
|
|
22136
|
-
this.deps.logger?.warn("failed to persist auto-corrected owner cwd", {
|
|
22137
|
-
sessionId: file.sessionId,
|
|
22138
|
-
error: err.message
|
|
22139
|
-
});
|
|
22140
|
-
}
|
|
22141
|
-
}
|
|
22282
|
+
const attachmentGroup = this.deps.attachmentGroup;
|
|
22142
22283
|
const runner = new SessionRunner(makeInitialState(file, subSessionMeta), {
|
|
22143
22284
|
broadcastFrame: (frame, target) => this.routeFromRunner(frame, target),
|
|
22144
22285
|
store,
|
|
@@ -22151,7 +22292,15 @@ var SessionManager = class {
|
|
|
22151
22292
|
resolveContextWindow: (tool, modelId) => this.deps.getAdapter(tool).resolveContextWindow(modelId),
|
|
22152
22293
|
dataDir: this.deps.dataDir,
|
|
22153
22294
|
personaStore: this.deps.personaStore,
|
|
22154
|
-
ownerDisplayName: this.deps.ownerDisplayName
|
|
22295
|
+
ownerDisplayName: this.deps.ownerDisplayName,
|
|
22296
|
+
// file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
|
|
22297
|
+
onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
|
|
22298
|
+
scope,
|
|
22299
|
+
sessionId: file.sessionId,
|
|
22300
|
+
tool: input.tool,
|
|
22301
|
+
relPath: input.relPath,
|
|
22302
|
+
cwd: input.cwd
|
|
22303
|
+
}) : void 0
|
|
22155
22304
|
});
|
|
22156
22305
|
if (this.deps.mode === "tui" && !file.toolSessionId) {
|
|
22157
22306
|
const newTsid = v4_default();
|
|
@@ -22239,31 +22388,14 @@ var SessionManager = class {
|
|
|
22239
22388
|
}
|
|
22240
22389
|
// ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
|
|
22241
22390
|
create(args) {
|
|
22242
|
-
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
22250
|
-
{
|
|
22251
|
-
sessionId: "",
|
|
22252
|
-
cwd: args.cwd ?? "",
|
|
22253
|
-
tool: "claude",
|
|
22254
|
-
ownerPersonaId: args.ownerPersonaId,
|
|
22255
|
-
createdAt: "",
|
|
22256
|
-
updatedAt: ""
|
|
22257
|
-
},
|
|
22258
|
-
this.deps.personaRoot
|
|
22259
|
-
);
|
|
22260
|
-
this.deps.logger?.warn("[spawn-cwd-debug] derived persona cwd", { derivedCwd: cwd });
|
|
22261
|
-
} else if (args.cwd) {
|
|
22262
|
-
cwd = args.cwd;
|
|
22263
|
-
this.deps.logger?.warn("[spawn-cwd-debug] using args.cwd (no ownerPersonaId)", {
|
|
22264
|
-
cwd
|
|
22265
|
-
});
|
|
22266
|
-
} else {
|
|
22391
|
+
let cwd = args.cwd;
|
|
22392
|
+
if (args.ownerPersonaId && !cwd) {
|
|
22393
|
+
if (!this.deps.personaRoot) {
|
|
22394
|
+
throw new Error("personaRoot required to derive cwd from ownerPersonaId");
|
|
22395
|
+
}
|
|
22396
|
+
cwd = import_node_path6.default.join(this.deps.personaRoot, safeFileName(args.ownerPersonaId));
|
|
22397
|
+
}
|
|
22398
|
+
if (!cwd) {
|
|
22267
22399
|
throw new ClawdError(ERROR_CODES.INVALID_CWD, "cwd required when ownerPersonaId is absent");
|
|
22268
22400
|
}
|
|
22269
22401
|
try {
|
|
@@ -23319,6 +23451,21 @@ var PersonaRegistry = class {
|
|
|
23319
23451
|
if (entry.revoked) return { ok: false, code: "TOKEN_REVOKED" };
|
|
23320
23452
|
return { ok: true, label: entry.label };
|
|
23321
23453
|
}
|
|
23454
|
+
/**
|
|
23455
|
+
* file-sharing HTTP auth-context 用的逆向查表:只有 Bearer token,没有 personaId。
|
|
23456
|
+
* 线性扫所有 persona.tokenMap 找到第一条 ok 命中的;revoked / non-public persona 跳过。
|
|
23457
|
+
* persona 数量通常 < 100,性能可忽略。
|
|
23458
|
+
*/
|
|
23459
|
+
findByToken(token) {
|
|
23460
|
+
if (!token) return null;
|
|
23461
|
+
for (const persona of this.cache.values()) {
|
|
23462
|
+
if (!persona.public) continue;
|
|
23463
|
+
const entry = persona.tokenMap[token];
|
|
23464
|
+
if (!entry || entry.revoked) continue;
|
|
23465
|
+
return { personaId: persona.personaId, label: entry.label };
|
|
23466
|
+
}
|
|
23467
|
+
return null;
|
|
23468
|
+
}
|
|
23322
23469
|
};
|
|
23323
23470
|
|
|
23324
23471
|
// src/persona/manager.ts
|
|
@@ -25260,6 +25407,7 @@ var import_websocket = __toESM(require_websocket(), 1);
|
|
|
25260
25407
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
25261
25408
|
|
|
25262
25409
|
// src/transport/local-ws-server.ts
|
|
25410
|
+
var import_node_http = __toESM(require("http"), 1);
|
|
25263
25411
|
var PERSONA_PATH_RE = /^\/personas\/([a-zA-Z0-9._-]+)$/;
|
|
25264
25412
|
var LocalWsServer = class {
|
|
25265
25413
|
constructor(opts) {
|
|
@@ -25269,6 +25417,7 @@ var LocalWsServer = class {
|
|
|
25269
25417
|
}
|
|
25270
25418
|
opts;
|
|
25271
25419
|
wss = null;
|
|
25420
|
+
httpServer = null;
|
|
25272
25421
|
frameHandler = null;
|
|
25273
25422
|
clients = /* @__PURE__ */ new Map();
|
|
25274
25423
|
logger;
|
|
@@ -25276,25 +25425,28 @@ var LocalWsServer = class {
|
|
|
25276
25425
|
async start() {
|
|
25277
25426
|
const host = this.opts.host ?? "127.0.0.1";
|
|
25278
25427
|
await new Promise((resolve2, reject) => {
|
|
25279
|
-
const
|
|
25280
|
-
|
|
25281
|
-
|
|
25282
|
-
|
|
25428
|
+
const httpServer = import_node_http.default.createServer((req, res) => this.handleHttpRequest(req, res));
|
|
25429
|
+
const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
|
|
25430
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
25431
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
25432
|
+
this.routeConnection(ws, req);
|
|
25433
|
+
});
|
|
25283
25434
|
});
|
|
25284
|
-
|
|
25435
|
+
httpServer.on("listening", () => {
|
|
25285
25436
|
this.logger?.info("ws listening", { host, port: this.opts.port });
|
|
25286
25437
|
resolve2();
|
|
25287
25438
|
});
|
|
25288
|
-
|
|
25439
|
+
httpServer.on("error", (err) => {
|
|
25289
25440
|
this.logger?.error("ws server error", { err: err.message });
|
|
25290
25441
|
reject(err);
|
|
25291
25442
|
});
|
|
25292
|
-
|
|
25443
|
+
httpServer.listen(this.opts.port, host);
|
|
25444
|
+
this.httpServer = httpServer;
|
|
25293
25445
|
this.wss = wss;
|
|
25294
25446
|
});
|
|
25295
25447
|
}
|
|
25296
25448
|
async stop() {
|
|
25297
|
-
if (!this.
|
|
25449
|
+
if (!this.httpServer) return;
|
|
25298
25450
|
for (const c of this.clients.values()) {
|
|
25299
25451
|
try {
|
|
25300
25452
|
c.ws.close(1001, "shutdown");
|
|
@@ -25306,7 +25458,32 @@ var LocalWsServer = class {
|
|
|
25306
25458
|
await new Promise((resolve2) => {
|
|
25307
25459
|
this.wss?.close(() => resolve2());
|
|
25308
25460
|
});
|
|
25461
|
+
await new Promise((resolve2) => {
|
|
25462
|
+
this.httpServer?.close(() => resolve2());
|
|
25463
|
+
});
|
|
25309
25464
|
this.wss = null;
|
|
25465
|
+
this.httpServer = null;
|
|
25466
|
+
}
|
|
25467
|
+
/** http.createServer 'request' 入口:file-sharing 路由优先,未命中走 404 */
|
|
25468
|
+
async handleHttpRequest(req, res) {
|
|
25469
|
+
const handler = this.opts.httpRequestHandler;
|
|
25470
|
+
if (handler) {
|
|
25471
|
+
try {
|
|
25472
|
+
const handled = await handler(req, res);
|
|
25473
|
+
if (handled) return;
|
|
25474
|
+
} catch (err) {
|
|
25475
|
+
this.logger?.warn("http handler threw", { err: err.message });
|
|
25476
|
+
if (!res.headersSent) {
|
|
25477
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
25478
|
+
res.end(JSON.stringify({ code: "INTERNAL", message: "http handler error" }));
|
|
25479
|
+
}
|
|
25480
|
+
return;
|
|
25481
|
+
}
|
|
25482
|
+
}
|
|
25483
|
+
if (!res.headersSent) {
|
|
25484
|
+
res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
|
|
25485
|
+
res.end(JSON.stringify({ code: "NOT_FOUND", message: "no route" }));
|
|
25486
|
+
}
|
|
25310
25487
|
}
|
|
25311
25488
|
onFrame(handler) {
|
|
25312
25489
|
this.frameHandler = handler;
|
|
@@ -25408,7 +25585,7 @@ var LocalWsServer = class {
|
|
|
25408
25585
|
}
|
|
25409
25586
|
try {
|
|
25410
25587
|
if (authed) {
|
|
25411
|
-
const readyFrame = this.opts.readyFrameBuilder();
|
|
25588
|
+
const readyFrame = this.opts.readyFrameBuilder({ remoteAddress });
|
|
25412
25589
|
this.safeSend(socket, { type: "ready", ...readyFrame });
|
|
25413
25590
|
} else {
|
|
25414
25591
|
this.safeSend(socket, { type: "ready", protocolVersion: this.opts.protocolVersion });
|
|
@@ -25445,7 +25622,7 @@ var LocalWsServer = class {
|
|
|
25445
25622
|
if (verdict !== "pass") {
|
|
25446
25623
|
if (!wasAuthed && authGate.isAuthed(client.id)) {
|
|
25447
25624
|
try {
|
|
25448
|
-
const full = this.opts.readyFrameBuilder();
|
|
25625
|
+
const full = this.opts.readyFrameBuilder({ remoteAddress });
|
|
25449
25626
|
this.safeSend(this.clients.get(client.id).ws, { type: "ready", ...full });
|
|
25450
25627
|
} catch (err) {
|
|
25451
25628
|
this.logger?.warn("post-auth ready frame build failed", { err: err.message });
|
|
@@ -26042,11 +26219,603 @@ function isLocalhost(addr) {
|
|
|
26042
26219
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
26043
26220
|
}
|
|
26044
26221
|
|
|
26045
|
-
// src/
|
|
26222
|
+
// src/transport/auth-context.ts
|
|
26223
|
+
var AuthContextResolver = class {
|
|
26224
|
+
constructor(opts) {
|
|
26225
|
+
this.opts = opts;
|
|
26226
|
+
}
|
|
26227
|
+
opts;
|
|
26228
|
+
/**
|
|
26229
|
+
* 从 `Authorization` 头解析。null = 无凭证 / 不识别 / revoked,调用方一律 401。
|
|
26230
|
+
* remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
|
|
26231
|
+
*/
|
|
26232
|
+
resolveFromHeader(authHeader, remoteAddress) {
|
|
26233
|
+
const token = parseBearer(authHeader);
|
|
26234
|
+
if (!token) return null;
|
|
26235
|
+
const isLoopback = isLoopbackAddr(remoteAddress);
|
|
26236
|
+
if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
|
|
26237
|
+
return { role: "owner", isLoopback };
|
|
26238
|
+
}
|
|
26239
|
+
const personalHit = this.opts.personaRegistry.findByToken(token);
|
|
26240
|
+
if (personalHit) {
|
|
26241
|
+
return {
|
|
26242
|
+
role: "personal",
|
|
26243
|
+
personaId: personalHit.personaId,
|
|
26244
|
+
label: personalHit.label,
|
|
26245
|
+
isLoopback
|
|
26246
|
+
};
|
|
26247
|
+
}
|
|
26248
|
+
return null;
|
|
26249
|
+
}
|
|
26250
|
+
};
|
|
26251
|
+
function parseBearer(header) {
|
|
26252
|
+
if (!header) return null;
|
|
26253
|
+
const raw = Array.isArray(header) ? header[0] : header;
|
|
26254
|
+
if (typeof raw !== "string") return null;
|
|
26255
|
+
const m2 = /^Bearer\s+(\S+)$/i.exec(raw.trim());
|
|
26256
|
+
return m2 ? m2[1] : null;
|
|
26257
|
+
}
|
|
26258
|
+
function isLoopbackAddr(addr) {
|
|
26259
|
+
if (!addr) return false;
|
|
26260
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
26261
|
+
}
|
|
26262
|
+
function constantTimeEqual2(a, b2) {
|
|
26263
|
+
if (a.length !== b2.length) return false;
|
|
26264
|
+
let diff2 = 0;
|
|
26265
|
+
for (let i = 0; i < a.length; i++) diff2 |= a.charCodeAt(i) ^ b2.charCodeAt(i);
|
|
26266
|
+
return diff2 === 0;
|
|
26267
|
+
}
|
|
26268
|
+
|
|
26269
|
+
// src/transport/http-router.ts
|
|
26270
|
+
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
26271
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
26272
|
+
|
|
26273
|
+
// src/attachment/group.ts
|
|
26046
26274
|
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
26047
26275
|
var import_node_path14 = __toESM(require("path"), 1);
|
|
26276
|
+
var import_node_crypto4 = __toESM(require("crypto"), 1);
|
|
26277
|
+
init_protocol();
|
|
26278
|
+
var GroupFileStore = class {
|
|
26279
|
+
dataDir;
|
|
26280
|
+
logger;
|
|
26281
|
+
cache = /* @__PURE__ */ new Map();
|
|
26282
|
+
constructor(opts) {
|
|
26283
|
+
this.dataDir = opts.dataDir;
|
|
26284
|
+
this.logger = opts.logger;
|
|
26285
|
+
}
|
|
26286
|
+
rootForScope(scope) {
|
|
26287
|
+
return import_node_path14.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
|
|
26288
|
+
}
|
|
26289
|
+
/** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
|
|
26290
|
+
filePath(scope, sessionId) {
|
|
26291
|
+
return import_node_path14.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
|
|
26292
|
+
}
|
|
26293
|
+
cacheKey(scope, sessionId) {
|
|
26294
|
+
return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
|
|
26295
|
+
}
|
|
26296
|
+
/** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
|
|
26297
|
+
readFile(scope, sessionId) {
|
|
26298
|
+
const file = this.filePath(scope, sessionId);
|
|
26299
|
+
try {
|
|
26300
|
+
const raw = import_node_fs13.default.readFileSync(file, "utf8");
|
|
26301
|
+
const parsed = JSON.parse(raw);
|
|
26302
|
+
if (!Array.isArray(parsed)) {
|
|
26303
|
+
this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
|
|
26304
|
+
file
|
|
26305
|
+
});
|
|
26306
|
+
return [];
|
|
26307
|
+
}
|
|
26308
|
+
const out = [];
|
|
26309
|
+
for (const entry of parsed) {
|
|
26310
|
+
const r = GroupFileEntrySchema.safeParse(entry);
|
|
26311
|
+
if (r.success) out.push(r.data);
|
|
26312
|
+
}
|
|
26313
|
+
return out;
|
|
26314
|
+
} catch (err) {
|
|
26315
|
+
const code = err?.code;
|
|
26316
|
+
if (code === "ENOENT") return [];
|
|
26317
|
+
this.logger?.warn("GroupFileStore.readFile failed", {
|
|
26318
|
+
file,
|
|
26319
|
+
err: err.message
|
|
26320
|
+
});
|
|
26321
|
+
return [];
|
|
26322
|
+
}
|
|
26323
|
+
}
|
|
26324
|
+
writeFile(scope, sessionId, entries) {
|
|
26325
|
+
const file = this.filePath(scope, sessionId);
|
|
26326
|
+
import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(file), { recursive: true });
|
|
26327
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
26328
|
+
import_node_fs13.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
|
|
26329
|
+
import_node_fs13.default.renameSync(tmp, file);
|
|
26330
|
+
}
|
|
26331
|
+
/** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
|
|
26332
|
+
list(scope, sessionId) {
|
|
26333
|
+
const key = this.cacheKey(scope, sessionId);
|
|
26334
|
+
const cached = this.cache.get(key);
|
|
26335
|
+
if (cached) return cached.entries;
|
|
26336
|
+
const entries = this.readFile(scope, sessionId);
|
|
26337
|
+
this.cache.set(key, { entries, scope });
|
|
26338
|
+
return entries;
|
|
26339
|
+
}
|
|
26340
|
+
/**
|
|
26341
|
+
* upsert:
|
|
26342
|
+
* - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
|
|
26343
|
+
* (保留首次入群人 / 入群时刻)
|
|
26344
|
+
* - 不存在 → append,id 派生稳定 uuid,addedAt = now
|
|
26345
|
+
*
|
|
26346
|
+
* 返回最新 entry(caller 可用来 broadcast 通知)。
|
|
26347
|
+
*/
|
|
26348
|
+
upsert(scope, sessionId, input, now = Date.now()) {
|
|
26349
|
+
const entries = this.list(scope, sessionId).slice();
|
|
26350
|
+
const idx = entries.findIndex((e) => e.relPath === input.relPath);
|
|
26351
|
+
let next;
|
|
26352
|
+
if (idx >= 0) {
|
|
26353
|
+
const prev = entries[idx];
|
|
26354
|
+
next = {
|
|
26355
|
+
...prev,
|
|
26356
|
+
size: input.size,
|
|
26357
|
+
mime: input.mime,
|
|
26358
|
+
lastEditedAt: now,
|
|
26359
|
+
stale: false
|
|
26360
|
+
// label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
|
|
26361
|
+
};
|
|
26362
|
+
entries[idx] = next;
|
|
26363
|
+
} else {
|
|
26364
|
+
next = {
|
|
26365
|
+
id: `gf-${import_node_crypto4.default.randomBytes(6).toString("base64url")}`,
|
|
26366
|
+
relPath: input.relPath,
|
|
26367
|
+
from: input.from,
|
|
26368
|
+
label: input.label,
|
|
26369
|
+
size: input.size,
|
|
26370
|
+
mime: input.mime,
|
|
26371
|
+
addedAt: now
|
|
26372
|
+
// agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
|
|
26373
|
+
};
|
|
26374
|
+
entries.push(next);
|
|
26375
|
+
}
|
|
26376
|
+
this.writeFile(scope, sessionId, entries);
|
|
26377
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
|
|
26378
|
+
return next;
|
|
26379
|
+
}
|
|
26380
|
+
/**
|
|
26381
|
+
* 标记一个 relPath stale(agent rm / mv 后文件不在)。
|
|
26382
|
+
* - 命中 → stale=true,UI 灰显
|
|
26383
|
+
* - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
|
|
26384
|
+
*
|
|
26385
|
+
* 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
|
|
26386
|
+
*/
|
|
26387
|
+
markStale(scope, sessionId, relPath) {
|
|
26388
|
+
const entries = this.list(scope, sessionId).slice();
|
|
26389
|
+
const idx = entries.findIndex((e) => e.relPath === relPath);
|
|
26390
|
+
if (idx < 0) return;
|
|
26391
|
+
if (entries[idx].stale) return;
|
|
26392
|
+
entries[idx] = { ...entries[idx], stale: true };
|
|
26393
|
+
this.writeFile(scope, sessionId, entries);
|
|
26394
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
|
|
26395
|
+
}
|
|
26396
|
+
/**
|
|
26397
|
+
* 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
|
|
26398
|
+
* agent 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
|
|
26399
|
+
* owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
|
|
26400
|
+
*
|
|
26401
|
+
* 返回值:true=命中并删除;false=relPath 不在群里。
|
|
26402
|
+
*/
|
|
26403
|
+
remove(scope, sessionId, relPath) {
|
|
26404
|
+
const entries = this.list(scope, sessionId).slice();
|
|
26405
|
+
const idx = entries.findIndex((e) => e.relPath === relPath);
|
|
26406
|
+
if (idx < 0) return false;
|
|
26407
|
+
entries.splice(idx, 1);
|
|
26408
|
+
this.writeFile(scope, sessionId, entries);
|
|
26409
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
|
|
26410
|
+
return true;
|
|
26411
|
+
}
|
|
26412
|
+
/**
|
|
26413
|
+
* 跨 session 聚合查询(spec §4 HTTP ACL:personal 视野并集)。
|
|
26414
|
+
*
|
|
26415
|
+
* 扫 <dataDir>/sessions/<personaId>/owner/*.group-files.json 和
|
|
26416
|
+
* <dataDir>/sessions/<personaId>/listener/*.group-files.json,每文件读一份。
|
|
26417
|
+
*
|
|
26418
|
+
* 复杂度 O(N sessions × N entries),N 通常 < 100,可接受。
|
|
26419
|
+
*/
|
|
26420
|
+
listByPersona(personaId) {
|
|
26421
|
+
const out = [];
|
|
26422
|
+
for (const mode of ["owner", "listener"]) {
|
|
26423
|
+
const scope = { kind: "persona", personaId, mode };
|
|
26424
|
+
const root = this.rootForScope(scope);
|
|
26425
|
+
let names;
|
|
26426
|
+
try {
|
|
26427
|
+
names = import_node_fs13.default.readdirSync(root);
|
|
26428
|
+
} catch (err) {
|
|
26429
|
+
const code = err?.code;
|
|
26430
|
+
if (code === "ENOENT") continue;
|
|
26431
|
+
continue;
|
|
26432
|
+
}
|
|
26433
|
+
for (const name of names) {
|
|
26434
|
+
if (!name.endsWith(".group-files.json")) continue;
|
|
26435
|
+
const sessionId = name.slice(0, -".group-files.json".length);
|
|
26436
|
+
if (!sessionId) continue;
|
|
26437
|
+
const entries = this.list(scope, sessionId);
|
|
26438
|
+
out.push({ sessionId, entries });
|
|
26439
|
+
}
|
|
26440
|
+
}
|
|
26441
|
+
return out;
|
|
26442
|
+
}
|
|
26443
|
+
};
|
|
26444
|
+
function personalViewable(groupStore, personaDir, personaId, absPath) {
|
|
26445
|
+
const realTarget = safeRealpath(absPath);
|
|
26446
|
+
if (!realTarget) {
|
|
26447
|
+
return false;
|
|
26448
|
+
}
|
|
26449
|
+
const personasUnion = groupStore.listByPersona(personaId);
|
|
26450
|
+
for (const { entries } of personasUnion) {
|
|
26451
|
+
for (const e of entries) {
|
|
26452
|
+
if (e.stale) continue;
|
|
26453
|
+
const realEntry = safeRealpath(import_node_path14.default.join(personaDir, e.relPath));
|
|
26454
|
+
if (realEntry && realEntry === realTarget) return true;
|
|
26455
|
+
}
|
|
26456
|
+
}
|
|
26457
|
+
return false;
|
|
26458
|
+
}
|
|
26459
|
+
function safeRealpath(p2) {
|
|
26460
|
+
try {
|
|
26461
|
+
return import_node_fs13.default.realpathSync(p2);
|
|
26462
|
+
} catch {
|
|
26463
|
+
return null;
|
|
26464
|
+
}
|
|
26465
|
+
}
|
|
26466
|
+
|
|
26467
|
+
// src/attachment/mime.ts
|
|
26468
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
26469
|
+
var TEXT_PLAIN = "text/plain; charset=utf-8";
|
|
26470
|
+
var EXT_TO_NATIVE_MIME = {
|
|
26471
|
+
// 图片
|
|
26472
|
+
".png": "image/png",
|
|
26473
|
+
".jpg": "image/jpeg",
|
|
26474
|
+
".jpeg": "image/jpeg",
|
|
26475
|
+
".gif": "image/gif",
|
|
26476
|
+
".webp": "image/webp",
|
|
26477
|
+
".svg": "image/svg+xml",
|
|
26478
|
+
".bmp": "image/bmp",
|
|
26479
|
+
".ico": "image/x-icon",
|
|
26480
|
+
".avif": "image/avif",
|
|
26481
|
+
// 文档 / 富文本
|
|
26482
|
+
".pdf": "application/pdf",
|
|
26483
|
+
".html": "text/html; charset=utf-8",
|
|
26484
|
+
".htm": "text/html; charset=utf-8",
|
|
26485
|
+
// 视频 / 音频
|
|
26486
|
+
".mp4": "video/mp4",
|
|
26487
|
+
".webm": "video/webm",
|
|
26488
|
+
".mov": "video/quicktime",
|
|
26489
|
+
".mp3": "audio/mpeg",
|
|
26490
|
+
".wav": "audio/wav",
|
|
26491
|
+
".ogg": "audio/ogg",
|
|
26492
|
+
".flac": "audio/flac",
|
|
26493
|
+
// 真二进制(让浏览器下载而不是错把它当文本明文)
|
|
26494
|
+
".zip": "application/zip",
|
|
26495
|
+
".gz": "application/gzip",
|
|
26496
|
+
".tar": "application/x-tar",
|
|
26497
|
+
".7z": "application/x-7z-compressed",
|
|
26498
|
+
".rar": "application/x-rar-compressed"
|
|
26499
|
+
};
|
|
26500
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
26501
|
+
// 文档
|
|
26502
|
+
".md",
|
|
26503
|
+
".markdown",
|
|
26504
|
+
".rst",
|
|
26505
|
+
".adoc",
|
|
26506
|
+
".txt",
|
|
26507
|
+
".log",
|
|
26508
|
+
// 通用结构化
|
|
26509
|
+
".json",
|
|
26510
|
+
".jsonc",
|
|
26511
|
+
".json5",
|
|
26512
|
+
".yaml",
|
|
26513
|
+
".yml",
|
|
26514
|
+
".toml",
|
|
26515
|
+
".xml",
|
|
26516
|
+
".csv",
|
|
26517
|
+
".tsv",
|
|
26518
|
+
".ini",
|
|
26519
|
+
".conf",
|
|
26520
|
+
".cfg",
|
|
26521
|
+
".env",
|
|
26522
|
+
// 前端
|
|
26523
|
+
".js",
|
|
26524
|
+
".jsx",
|
|
26525
|
+
".mjs",
|
|
26526
|
+
".cjs",
|
|
26527
|
+
".ts",
|
|
26528
|
+
".tsx",
|
|
26529
|
+
".css",
|
|
26530
|
+
".scss",
|
|
26531
|
+
".sass",
|
|
26532
|
+
".less",
|
|
26533
|
+
".vue",
|
|
26534
|
+
".svelte",
|
|
26535
|
+
".graphql",
|
|
26536
|
+
".gql",
|
|
26537
|
+
// 后端 / 各语言
|
|
26538
|
+
".py",
|
|
26539
|
+
".rb",
|
|
26540
|
+
".go",
|
|
26541
|
+
".rs",
|
|
26542
|
+
".java",
|
|
26543
|
+
".kt",
|
|
26544
|
+
".kts",
|
|
26545
|
+
".scala",
|
|
26546
|
+
".c",
|
|
26547
|
+
".h",
|
|
26548
|
+
".cpp",
|
|
26549
|
+
".hpp",
|
|
26550
|
+
".cc",
|
|
26551
|
+
".hh",
|
|
26552
|
+
".cs",
|
|
26553
|
+
".php",
|
|
26554
|
+
".swift",
|
|
26555
|
+
".m",
|
|
26556
|
+
".mm",
|
|
26557
|
+
".dart",
|
|
26558
|
+
".lua",
|
|
26559
|
+
".pl",
|
|
26560
|
+
".r",
|
|
26561
|
+
".sh",
|
|
26562
|
+
".bash",
|
|
26563
|
+
".zsh",
|
|
26564
|
+
".fish",
|
|
26565
|
+
// 其他
|
|
26566
|
+
".sql",
|
|
26567
|
+
".proto",
|
|
26568
|
+
".dockerfile",
|
|
26569
|
+
".diff",
|
|
26570
|
+
".patch",
|
|
26571
|
+
".makefile",
|
|
26572
|
+
".mk"
|
|
26573
|
+
]);
|
|
26574
|
+
function lookupMime(filePathOrName) {
|
|
26575
|
+
const ext = import_node_path15.default.extname(filePathOrName).toLowerCase();
|
|
26576
|
+
if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
|
|
26577
|
+
if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
|
|
26578
|
+
return "application/octet-stream";
|
|
26579
|
+
}
|
|
26580
|
+
|
|
26581
|
+
// src/attachment/sign-url.ts
|
|
26582
|
+
var import_node_crypto5 = __toESM(require("crypto"), 1);
|
|
26583
|
+
var HMAC_ALGO = "sha256";
|
|
26584
|
+
function base64urlEncode(buf) {
|
|
26585
|
+
const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
|
|
26586
|
+
return b2.toString("base64url");
|
|
26587
|
+
}
|
|
26588
|
+
function base64urlDecodeBuf(s) {
|
|
26589
|
+
return Buffer.from(s, "base64url");
|
|
26590
|
+
}
|
|
26591
|
+
function encodeAbsPathForUrl(absPath) {
|
|
26592
|
+
return absPath.split("/").map(encodeURIComponent).join("/");
|
|
26593
|
+
}
|
|
26594
|
+
function decodeAbsPathFromUrl(encoded) {
|
|
26595
|
+
return encoded.split("/").map(decodeURIComponent).join("/");
|
|
26596
|
+
}
|
|
26597
|
+
function computeSig(secret, absPath, e) {
|
|
26598
|
+
const msg = e === null ? absPath : `${absPath}|${e}`;
|
|
26599
|
+
return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
|
|
26600
|
+
}
|
|
26601
|
+
function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
|
|
26602
|
+
const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
|
|
26603
|
+
const s = base64urlEncode(computeSig(secret, absPath, e));
|
|
26604
|
+
return { absPath, e, s };
|
|
26605
|
+
}
|
|
26606
|
+
function buildSignedFileUrl(httpBaseUrl, parts) {
|
|
26607
|
+
const encodedPath = encodeAbsPathForUrl(parts.absPath);
|
|
26608
|
+
const query = parts.e === null ? `s=${encodeURIComponent(parts.s)}` : `e=${parts.e}&s=${encodeURIComponent(parts.s)}`;
|
|
26609
|
+
const base = httpBaseUrl.endsWith("/") ? httpBaseUrl.slice(0, -1) : httpBaseUrl;
|
|
26610
|
+
return `${base}/files${encodedPath}?${query}`;
|
|
26611
|
+
}
|
|
26612
|
+
function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
|
|
26613
|
+
let e;
|
|
26614
|
+
if (eRaw === null || eRaw === void 0 || eRaw === "") {
|
|
26615
|
+
e = null;
|
|
26616
|
+
} else {
|
|
26617
|
+
const parsed = Number.parseInt(eRaw, 10);
|
|
26618
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
26619
|
+
return { ok: false, code: "MALFORMED" };
|
|
26620
|
+
}
|
|
26621
|
+
e = parsed;
|
|
26622
|
+
}
|
|
26623
|
+
if (!absPath || !s) return { ok: false, code: "MALFORMED" };
|
|
26624
|
+
const expected = computeSig(secret, absPath, e);
|
|
26625
|
+
let provided;
|
|
26626
|
+
try {
|
|
26627
|
+
provided = base64urlDecodeBuf(s);
|
|
26628
|
+
} catch {
|
|
26629
|
+
return { ok: false, code: "MALFORMED" };
|
|
26630
|
+
}
|
|
26631
|
+
if (provided.length !== expected.length) {
|
|
26632
|
+
return { ok: false, code: "BAD_SIG" };
|
|
26633
|
+
}
|
|
26634
|
+
if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
|
|
26635
|
+
return { ok: false, code: "BAD_SIG" };
|
|
26636
|
+
}
|
|
26637
|
+
if (e !== null && now() / 1e3 > e) {
|
|
26638
|
+
return { ok: false, code: "EXPIRED" };
|
|
26639
|
+
}
|
|
26640
|
+
return { ok: true, absPath };
|
|
26641
|
+
}
|
|
26642
|
+
|
|
26643
|
+
// src/transport/http-router.ts
|
|
26644
|
+
function createHttpRouter(deps) {
|
|
26645
|
+
return async (req, res) => {
|
|
26646
|
+
const url = parseUrl(req.url);
|
|
26647
|
+
if (!url) {
|
|
26648
|
+
sendJson(res, 400, { code: "INVALID_URL", message: "malformed request URL" });
|
|
26649
|
+
return true;
|
|
26650
|
+
}
|
|
26651
|
+
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
26652
|
+
sendJson(res, 200, { ok: true, version: deps.daemonVersion });
|
|
26653
|
+
return true;
|
|
26654
|
+
}
|
|
26655
|
+
if (!url.pathname.startsWith("/persona/") && !url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
|
|
26656
|
+
return false;
|
|
26657
|
+
}
|
|
26658
|
+
if (url.pathname.startsWith("/files/") && req.method === "GET") {
|
|
26659
|
+
const secret = deps.getSignSecret?.();
|
|
26660
|
+
if (!secret) {
|
|
26661
|
+
sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "signed URL secret unavailable (noAuth?)" });
|
|
26662
|
+
return true;
|
|
26663
|
+
}
|
|
26664
|
+
const encodedPath = url.pathname.slice("/files".length);
|
|
26665
|
+
let absPath;
|
|
26666
|
+
try {
|
|
26667
|
+
absPath = decodeAbsPathFromUrl(encodedPath);
|
|
26668
|
+
} catch {
|
|
26669
|
+
sendJson(res, 400, { code: "MALFORMED", message: "invalid path encoding" });
|
|
26670
|
+
return true;
|
|
26671
|
+
}
|
|
26672
|
+
const e = url.searchParams.get("e");
|
|
26673
|
+
const s = url.searchParams.get("s") ?? "";
|
|
26674
|
+
const r = verifySignedUrl(secret, absPath, e, s);
|
|
26675
|
+
if (!r.ok) {
|
|
26676
|
+
const statusByCode = {
|
|
26677
|
+
BAD_SIG: 403,
|
|
26678
|
+
EXPIRED: 410,
|
|
26679
|
+
MALFORMED: 400
|
|
26680
|
+
};
|
|
26681
|
+
sendJson(res, statusByCode[r.code], { code: r.code, message: "signed URL invalid" });
|
|
26682
|
+
return true;
|
|
26683
|
+
}
|
|
26684
|
+
streamFile(res, r.absPath, deps.logger);
|
|
26685
|
+
return true;
|
|
26686
|
+
}
|
|
26687
|
+
const ctx = deps.authResolver.resolveFromHeader(
|
|
26688
|
+
req.headers.authorization,
|
|
26689
|
+
req.socket.remoteAddress ?? void 0
|
|
26690
|
+
);
|
|
26691
|
+
if (!ctx) {
|
|
26692
|
+
sendJson(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
|
|
26693
|
+
return true;
|
|
26694
|
+
}
|
|
26695
|
+
const personaFilesMatch = url.pathname.match(/^\/persona\/([^/]+)\/files$/);
|
|
26696
|
+
if (personaFilesMatch && req.method === "GET") {
|
|
26697
|
+
const pid = personaFilesMatch[1];
|
|
26698
|
+
const pathParam = url.searchParams.get("path");
|
|
26699
|
+
if (!pathParam) {
|
|
26700
|
+
sendJson(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
|
|
26701
|
+
return true;
|
|
26702
|
+
}
|
|
26703
|
+
if (!deps.personaStore || !deps.groupFileStore) {
|
|
26704
|
+
sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "files endpoint not wired" }));
|
|
26705
|
+
return true;
|
|
26706
|
+
}
|
|
26707
|
+
const personaDir = deps.personaStore.personaDirPath(pid);
|
|
26708
|
+
const absPath = import_node_path16.default.isAbsolute(pathParam) ? pathParam : import_node_path16.default.join(personaDir, pathParam);
|
|
26709
|
+
if (!import_node_path16.default.isAbsolute(pathParam) && !isContainedIn(absPath, personaDir)) {
|
|
26710
|
+
sendJson(res, 400, { code: "PATH_TRAVERSAL", message: "rel path escapes personaDir" });
|
|
26711
|
+
return true;
|
|
26712
|
+
}
|
|
26713
|
+
if (ctx.role === "personal") {
|
|
26714
|
+
if (ctx.personaId !== pid) {
|
|
26715
|
+
sendJson(res, 403, { code: "FORBIDDEN", message: "personal token bound to other persona" });
|
|
26716
|
+
return true;
|
|
26717
|
+
}
|
|
26718
|
+
if (!personalViewable(deps.groupFileStore, personaDir, pid, absPath)) {
|
|
26719
|
+
sendJson(res, 403, { code: "FORBIDDEN", message: "path not in personal viewable scope" });
|
|
26720
|
+
return true;
|
|
26721
|
+
}
|
|
26722
|
+
}
|
|
26723
|
+
streamFile(res, absPath, deps.logger);
|
|
26724
|
+
return true;
|
|
26725
|
+
}
|
|
26726
|
+
const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
|
|
26727
|
+
if (sessionFilesMatch && req.method === "GET") {
|
|
26728
|
+
if (ctx.role !== "owner") {
|
|
26729
|
+
sendJson(res, 403, { code: "FORBIDDEN", message: "direct session files are owner-only" });
|
|
26730
|
+
return true;
|
|
26731
|
+
}
|
|
26732
|
+
const sid = sessionFilesMatch[1];
|
|
26733
|
+
const pathParam = url.searchParams.get("path");
|
|
26734
|
+
if (!pathParam) {
|
|
26735
|
+
sendJson(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
|
|
26736
|
+
return true;
|
|
26737
|
+
}
|
|
26738
|
+
let absPath;
|
|
26739
|
+
if (import_node_path16.default.isAbsolute(pathParam)) {
|
|
26740
|
+
absPath = pathParam;
|
|
26741
|
+
} else if (deps.sessionStore) {
|
|
26742
|
+
const file = deps.sessionStore.read(sid);
|
|
26743
|
+
if (!file) {
|
|
26744
|
+
sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
|
|
26745
|
+
return true;
|
|
26746
|
+
}
|
|
26747
|
+
absPath = import_node_path16.default.join(file.cwd, pathParam);
|
|
26748
|
+
} else {
|
|
26749
|
+
sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
|
|
26750
|
+
return true;
|
|
26751
|
+
}
|
|
26752
|
+
streamFile(res, absPath, deps.logger);
|
|
26753
|
+
return true;
|
|
26754
|
+
}
|
|
26755
|
+
sendJson(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
|
|
26756
|
+
return true;
|
|
26757
|
+
};
|
|
26758
|
+
}
|
|
26759
|
+
function parseUrl(rawUrl) {
|
|
26760
|
+
if (!rawUrl) return null;
|
|
26761
|
+
try {
|
|
26762
|
+
return new URL(rawUrl, "http://placeholder");
|
|
26763
|
+
} catch {
|
|
26764
|
+
return null;
|
|
26765
|
+
}
|
|
26766
|
+
}
|
|
26767
|
+
function sendJson(res, status, body) {
|
|
26768
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
26769
|
+
res.end(JSON.stringify(body));
|
|
26770
|
+
}
|
|
26771
|
+
function withCtx(ctx, body) {
|
|
26772
|
+
return { ...body, role: ctx.role, personaId: ctx.personaId };
|
|
26773
|
+
}
|
|
26774
|
+
function isContainedIn(abs, root) {
|
|
26775
|
+
const normalized = import_node_path16.default.resolve(abs);
|
|
26776
|
+
const normalizedRoot = import_node_path16.default.resolve(root);
|
|
26777
|
+
if (normalized === normalizedRoot) return true;
|
|
26778
|
+
return normalized.startsWith(normalizedRoot + import_node_path16.default.sep);
|
|
26779
|
+
}
|
|
26780
|
+
function streamFile(res, absPath, logger) {
|
|
26781
|
+
let stat;
|
|
26782
|
+
try {
|
|
26783
|
+
stat = import_node_fs14.default.statSync(absPath);
|
|
26784
|
+
} catch (err) {
|
|
26785
|
+
const code = err?.code;
|
|
26786
|
+
if (code === "ENOENT") {
|
|
26787
|
+
sendJson(res, 404, { code: "NOT_FOUND", message: "file not found" });
|
|
26788
|
+
} else {
|
|
26789
|
+
sendJson(res, 500, { code: "STAT_FAILED", message: err.message });
|
|
26790
|
+
}
|
|
26791
|
+
return;
|
|
26792
|
+
}
|
|
26793
|
+
if (!stat.isFile()) {
|
|
26794
|
+
sendJson(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
|
|
26795
|
+
return;
|
|
26796
|
+
}
|
|
26797
|
+
const mime = lookupMime(absPath);
|
|
26798
|
+
const basename = import_node_path16.default.basename(absPath);
|
|
26799
|
+
res.writeHead(200, {
|
|
26800
|
+
"Content-Type": mime,
|
|
26801
|
+
"Content-Length": String(stat.size),
|
|
26802
|
+
"Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(basename)}`,
|
|
26803
|
+
// 防止浏览器把任意 mime 当 html 渲染
|
|
26804
|
+
"X-Content-Type-Options": "nosniff"
|
|
26805
|
+
});
|
|
26806
|
+
const stream = import_node_fs14.default.createReadStream(absPath);
|
|
26807
|
+
stream.on("error", (err) => {
|
|
26808
|
+
logger?.warn("streamFile read error", { absPath, err: err.message });
|
|
26809
|
+
res.destroy();
|
|
26810
|
+
});
|
|
26811
|
+
stream.pipe(res);
|
|
26812
|
+
}
|
|
26813
|
+
|
|
26814
|
+
// src/discovery/state-file.ts
|
|
26815
|
+
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
26816
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
26048
26817
|
function defaultStateFilePath(dataDir) {
|
|
26049
|
-
return
|
|
26818
|
+
return import_node_path17.default.join(dataDir, "state.json");
|
|
26050
26819
|
}
|
|
26051
26820
|
function isPidAlive(pid) {
|
|
26052
26821
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
@@ -26068,7 +26837,7 @@ var StateFileManager = class {
|
|
|
26068
26837
|
}
|
|
26069
26838
|
read() {
|
|
26070
26839
|
try {
|
|
26071
|
-
const raw =
|
|
26840
|
+
const raw = import_node_fs15.default.readFileSync(this.file, "utf8");
|
|
26072
26841
|
const parsed = JSON.parse(raw);
|
|
26073
26842
|
return parsed;
|
|
26074
26843
|
} catch {
|
|
@@ -26082,34 +26851,34 @@ var StateFileManager = class {
|
|
|
26082
26851
|
return { status: "stale", existing };
|
|
26083
26852
|
}
|
|
26084
26853
|
write(state) {
|
|
26085
|
-
|
|
26854
|
+
import_node_fs15.default.mkdirSync(import_node_path17.default.dirname(this.file), { recursive: true });
|
|
26086
26855
|
const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
|
|
26087
|
-
|
|
26088
|
-
|
|
26856
|
+
import_node_fs15.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
|
|
26857
|
+
import_node_fs15.default.renameSync(tmp, this.file);
|
|
26089
26858
|
if (process.platform !== "win32") {
|
|
26090
26859
|
try {
|
|
26091
|
-
|
|
26860
|
+
import_node_fs15.default.chmodSync(this.file, 384);
|
|
26092
26861
|
} catch {
|
|
26093
26862
|
}
|
|
26094
26863
|
}
|
|
26095
26864
|
}
|
|
26096
26865
|
delete() {
|
|
26097
26866
|
try {
|
|
26098
|
-
|
|
26867
|
+
import_node_fs15.default.unlinkSync(this.file);
|
|
26099
26868
|
} catch {
|
|
26100
26869
|
}
|
|
26101
26870
|
}
|
|
26102
26871
|
};
|
|
26103
26872
|
|
|
26104
26873
|
// src/tunnel/tunnel-manager.ts
|
|
26105
|
-
var
|
|
26106
|
-
var
|
|
26107
|
-
var
|
|
26874
|
+
var import_node_fs19 = __toESM(require("fs"), 1);
|
|
26875
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
26876
|
+
var import_node_crypto6 = __toESM(require("crypto"), 1);
|
|
26108
26877
|
var import_node_child_process5 = require("child_process");
|
|
26109
26878
|
|
|
26110
26879
|
// src/tunnel/tunnel-store.ts
|
|
26111
|
-
var
|
|
26112
|
-
var
|
|
26880
|
+
var import_node_fs16 = __toESM(require("fs"), 1);
|
|
26881
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
26113
26882
|
var TunnelStore = class {
|
|
26114
26883
|
constructor(filePath) {
|
|
26115
26884
|
this.filePath = filePath;
|
|
@@ -26117,7 +26886,7 @@ var TunnelStore = class {
|
|
|
26117
26886
|
filePath;
|
|
26118
26887
|
async get() {
|
|
26119
26888
|
try {
|
|
26120
|
-
const raw = await
|
|
26889
|
+
const raw = await import_node_fs16.default.promises.readFile(this.filePath, "utf8");
|
|
26121
26890
|
const obj = JSON.parse(raw);
|
|
26122
26891
|
if (!isPersistedTunnel(obj)) return null;
|
|
26123
26892
|
return obj;
|
|
@@ -26128,22 +26897,22 @@ var TunnelStore = class {
|
|
|
26128
26897
|
}
|
|
26129
26898
|
}
|
|
26130
26899
|
async set(v2) {
|
|
26131
|
-
const dir =
|
|
26132
|
-
await
|
|
26900
|
+
const dir = import_node_path18.default.dirname(this.filePath);
|
|
26901
|
+
await import_node_fs16.default.promises.mkdir(dir, { recursive: true });
|
|
26133
26902
|
const data = JSON.stringify(v2, null, 2);
|
|
26134
26903
|
const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
26135
|
-
await
|
|
26904
|
+
await import_node_fs16.default.promises.writeFile(tmp, data, { mode: 384 });
|
|
26136
26905
|
if (process.platform !== "win32") {
|
|
26137
26906
|
try {
|
|
26138
|
-
await
|
|
26907
|
+
await import_node_fs16.default.promises.chmod(tmp, 384);
|
|
26139
26908
|
} catch {
|
|
26140
26909
|
}
|
|
26141
26910
|
}
|
|
26142
|
-
await
|
|
26911
|
+
await import_node_fs16.default.promises.rename(tmp, this.filePath);
|
|
26143
26912
|
}
|
|
26144
26913
|
async clear() {
|
|
26145
26914
|
try {
|
|
26146
|
-
await
|
|
26915
|
+
await import_node_fs16.default.promises.unlink(this.filePath);
|
|
26147
26916
|
} catch (err) {
|
|
26148
26917
|
const code = err?.code;
|
|
26149
26918
|
if (code !== "ENOENT") throw err;
|
|
@@ -26238,9 +27007,9 @@ function escape(v2) {
|
|
|
26238
27007
|
}
|
|
26239
27008
|
|
|
26240
27009
|
// src/tunnel/frpc-binary.ts
|
|
26241
|
-
var
|
|
27010
|
+
var import_node_fs17 = __toESM(require("fs"), 1);
|
|
26242
27011
|
var import_node_os9 = __toESM(require("os"), 1);
|
|
26243
|
-
var
|
|
27012
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
26244
27013
|
var import_node_child_process3 = require("child_process");
|
|
26245
27014
|
var import_node_stream2 = require("stream");
|
|
26246
27015
|
var import_promises = require("stream/promises");
|
|
@@ -26272,20 +27041,20 @@ function frpcDownloadUrl(version2, p2) {
|
|
|
26272
27041
|
}
|
|
26273
27042
|
async function ensureFrpcBinary(opts) {
|
|
26274
27043
|
if (opts.override) {
|
|
26275
|
-
if (!
|
|
27044
|
+
if (!import_node_fs17.default.existsSync(opts.override)) {
|
|
26276
27045
|
throw new Error(`frpc binary not found at override path: ${opts.override}`);
|
|
26277
27046
|
}
|
|
26278
27047
|
return opts.override;
|
|
26279
27048
|
}
|
|
26280
27049
|
const version2 = opts.version ?? FRPC_VERSION;
|
|
26281
27050
|
const platform = opts.platform ?? detectPlatform();
|
|
26282
|
-
const binDir =
|
|
26283
|
-
|
|
27051
|
+
const binDir = import_node_path19.default.join(opts.dataDir, "bin");
|
|
27052
|
+
import_node_fs17.default.mkdirSync(binDir, { recursive: true });
|
|
26284
27053
|
cleanupStaleArtifacts(binDir);
|
|
26285
|
-
const stableBin =
|
|
26286
|
-
if (
|
|
27054
|
+
const stableBin = import_node_path19.default.join(binDir, "frpc");
|
|
27055
|
+
if (import_node_fs17.default.existsSync(stableBin)) return stableBin;
|
|
26287
27056
|
const partialBin = `${stableBin}.partial`;
|
|
26288
|
-
const tarballPath =
|
|
27057
|
+
const tarballPath = import_node_path19.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
|
|
26289
27058
|
try {
|
|
26290
27059
|
const url = frpcDownloadUrl(version2, platform);
|
|
26291
27060
|
await downloadToFile(url, tarballPath, opts.fetchImpl);
|
|
@@ -26294,8 +27063,8 @@ async function ensureFrpcBinary(opts) {
|
|
|
26294
27063
|
} else {
|
|
26295
27064
|
await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
|
|
26296
27065
|
}
|
|
26297
|
-
|
|
26298
|
-
|
|
27066
|
+
import_node_fs17.default.chmodSync(partialBin, 493);
|
|
27067
|
+
import_node_fs17.default.renameSync(partialBin, stableBin);
|
|
26299
27068
|
} finally {
|
|
26300
27069
|
safeUnlink(tarballPath);
|
|
26301
27070
|
safeUnlink(partialBin);
|
|
@@ -26305,15 +27074,15 @@ async function ensureFrpcBinary(opts) {
|
|
|
26305
27074
|
function cleanupStaleArtifacts(binDir) {
|
|
26306
27075
|
let entries;
|
|
26307
27076
|
try {
|
|
26308
|
-
entries =
|
|
27077
|
+
entries = import_node_fs17.default.readdirSync(binDir);
|
|
26309
27078
|
} catch {
|
|
26310
27079
|
return;
|
|
26311
27080
|
}
|
|
26312
27081
|
for (const name of entries) {
|
|
26313
27082
|
if (name.endsWith(".partial") || name.startsWith("extract-")) {
|
|
26314
|
-
const full =
|
|
27083
|
+
const full = import_node_path19.default.join(binDir, name);
|
|
26315
27084
|
try {
|
|
26316
|
-
|
|
27085
|
+
import_node_fs17.default.rmSync(full, { recursive: true, force: true });
|
|
26317
27086
|
} catch {
|
|
26318
27087
|
}
|
|
26319
27088
|
}
|
|
@@ -26321,7 +27090,7 @@ function cleanupStaleArtifacts(binDir) {
|
|
|
26321
27090
|
}
|
|
26322
27091
|
function safeUnlink(p2) {
|
|
26323
27092
|
try {
|
|
26324
|
-
|
|
27093
|
+
import_node_fs17.default.unlinkSync(p2);
|
|
26325
27094
|
} catch {
|
|
26326
27095
|
}
|
|
26327
27096
|
}
|
|
@@ -26332,13 +27101,13 @@ async function downloadToFile(url, dest, fetchImpl) {
|
|
|
26332
27101
|
if (!res.ok || !res.body) {
|
|
26333
27102
|
throw new Error(`download failed: ${res.status} ${res.statusText}`);
|
|
26334
27103
|
}
|
|
26335
|
-
const out =
|
|
27104
|
+
const out = import_node_fs17.default.createWriteStream(dest);
|
|
26336
27105
|
const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
|
|
26337
27106
|
await (0, import_promises.pipeline)(nodeStream, out);
|
|
26338
27107
|
}
|
|
26339
27108
|
async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
|
|
26340
|
-
const work =
|
|
26341
|
-
|
|
27109
|
+
const work = import_node_path19.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
|
|
27110
|
+
import_node_fs17.default.mkdirSync(work, { recursive: true });
|
|
26342
27111
|
try {
|
|
26343
27112
|
await new Promise((resolve2, reject) => {
|
|
26344
27113
|
const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
|
|
@@ -26346,32 +27115,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
|
|
|
26346
27115
|
proc.on("exit", (code) => code === 0 ? resolve2() : reject(new Error(`tar exited ${code}`)));
|
|
26347
27116
|
});
|
|
26348
27117
|
const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
|
|
26349
|
-
const src =
|
|
26350
|
-
if (!
|
|
27118
|
+
const src = import_node_path19.default.join(work, dirName, "frpc");
|
|
27119
|
+
if (!import_node_fs17.default.existsSync(src)) {
|
|
26351
27120
|
throw new Error(`frpc not found inside tarball at ${src}`);
|
|
26352
27121
|
}
|
|
26353
|
-
|
|
27122
|
+
import_node_fs17.default.copyFileSync(src, destBin);
|
|
26354
27123
|
} finally {
|
|
26355
|
-
|
|
27124
|
+
import_node_fs17.default.rmSync(work, { recursive: true, force: true });
|
|
26356
27125
|
}
|
|
26357
27126
|
}
|
|
26358
27127
|
|
|
26359
27128
|
// src/tunnel/frpc-process.ts
|
|
26360
|
-
var
|
|
26361
|
-
var
|
|
27129
|
+
var import_node_fs18 = __toESM(require("fs"), 1);
|
|
27130
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
26362
27131
|
var import_node_child_process4 = require("child_process");
|
|
26363
27132
|
function frpcPidFilePath(dataDir) {
|
|
26364
|
-
return
|
|
27133
|
+
return import_node_path20.default.join(dataDir, "frpc.pid");
|
|
26365
27134
|
}
|
|
26366
27135
|
function writeFrpcPid(dataDir, pid) {
|
|
26367
27136
|
try {
|
|
26368
|
-
|
|
27137
|
+
import_node_fs18.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
|
|
26369
27138
|
} catch {
|
|
26370
27139
|
}
|
|
26371
27140
|
}
|
|
26372
27141
|
function clearFrpcPid(dataDir) {
|
|
26373
27142
|
try {
|
|
26374
|
-
|
|
27143
|
+
import_node_fs18.default.unlinkSync(frpcPidFilePath(dataDir));
|
|
26375
27144
|
} catch {
|
|
26376
27145
|
}
|
|
26377
27146
|
}
|
|
@@ -26387,7 +27156,7 @@ function defaultIsPidAlive(pid) {
|
|
|
26387
27156
|
}
|
|
26388
27157
|
function defaultReadPidFile(file) {
|
|
26389
27158
|
try {
|
|
26390
|
-
return
|
|
27159
|
+
return import_node_fs18.default.readFileSync(file, "utf8");
|
|
26391
27160
|
} catch {
|
|
26392
27161
|
return null;
|
|
26393
27162
|
}
|
|
@@ -26403,7 +27172,7 @@ function defaultSleep(ms) {
|
|
|
26403
27172
|
}
|
|
26404
27173
|
async function killStaleFrpc(deps) {
|
|
26405
27174
|
const pidFile = frpcPidFilePath(deps.dataDir);
|
|
26406
|
-
const tomlPath =
|
|
27175
|
+
const tomlPath = import_node_path20.default.join(deps.dataDir, "frpc.toml");
|
|
26407
27176
|
const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
|
|
26408
27177
|
const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
|
|
26409
27178
|
const killPid = deps.killPidImpl ?? defaultKillPid;
|
|
@@ -26427,7 +27196,7 @@ async function killStaleFrpc(deps) {
|
|
|
26427
27196
|
}
|
|
26428
27197
|
if (victims.size === 0) {
|
|
26429
27198
|
try {
|
|
26430
|
-
|
|
27199
|
+
import_node_fs18.default.unlinkSync(pidFile);
|
|
26431
27200
|
} catch {
|
|
26432
27201
|
}
|
|
26433
27202
|
return;
|
|
@@ -26438,7 +27207,7 @@ async function killStaleFrpc(deps) {
|
|
|
26438
27207
|
}
|
|
26439
27208
|
await sleep(deps.reapWaitMs ?? 300);
|
|
26440
27209
|
try {
|
|
26441
|
-
|
|
27210
|
+
import_node_fs18.default.unlinkSync(pidFile);
|
|
26442
27211
|
} catch {
|
|
26443
27212
|
}
|
|
26444
27213
|
}
|
|
@@ -26475,7 +27244,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
26475
27244
|
var TunnelManager = class {
|
|
26476
27245
|
constructor(deps) {
|
|
26477
27246
|
this.deps = deps;
|
|
26478
|
-
this.store = deps.store ?? new TunnelStore(
|
|
27247
|
+
this.store = deps.store ?? new TunnelStore(import_node_path21.default.join(deps.dataDir, "tunnel.json"));
|
|
26479
27248
|
this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
|
|
26480
27249
|
this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
|
|
26481
27250
|
}
|
|
@@ -26602,8 +27371,8 @@ var TunnelManager = class {
|
|
|
26602
27371
|
dataDir: this.deps.dataDir,
|
|
26603
27372
|
override: this.deps.frpcBinaryOverride ?? void 0
|
|
26604
27373
|
});
|
|
26605
|
-
const tomlPath =
|
|
26606
|
-
const proxyName = `clawd-${t.subdomain}-${localPort}-${
|
|
27374
|
+
const tomlPath = import_node_path21.default.join(this.deps.dataDir, "frpc.toml");
|
|
27375
|
+
const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto6.default.randomBytes(3).toString("hex")}`;
|
|
26607
27376
|
const toml = buildFrpcToml({
|
|
26608
27377
|
serverAddr: t.frpsHost,
|
|
26609
27378
|
serverPort: t.frpsPort,
|
|
@@ -26613,12 +27382,12 @@ var TunnelManager = class {
|
|
|
26613
27382
|
localPort,
|
|
26614
27383
|
logLevel: "info"
|
|
26615
27384
|
});
|
|
26616
|
-
await
|
|
27385
|
+
await import_node_fs19.default.promises.writeFile(tomlPath, toml, { mode: 384 });
|
|
26617
27386
|
const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
|
|
26618
27387
|
stdio: ["ignore", "pipe", "pipe"]
|
|
26619
27388
|
});
|
|
26620
|
-
const logFilePath =
|
|
26621
|
-
const logStream =
|
|
27389
|
+
const logFilePath = import_node_path21.default.join(this.deps.dataDir, "frpc.log");
|
|
27390
|
+
const logStream = import_node_fs19.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
|
|
26622
27391
|
logStream.on("error", () => {
|
|
26623
27392
|
});
|
|
26624
27393
|
const tee = (chunk) => {
|
|
@@ -26701,22 +27470,22 @@ async function waitForFrpcReady(proc, timeoutMs) {
|
|
|
26701
27470
|
|
|
26702
27471
|
// src/tunnel/device-key.ts
|
|
26703
27472
|
var import_node_os10 = __toESM(require("os"), 1);
|
|
26704
|
-
var
|
|
27473
|
+
var import_node_crypto7 = __toESM(require("crypto"), 1);
|
|
26705
27474
|
var DERIVE_SALT = "clawd-tunnel-device-v1";
|
|
26706
27475
|
function deriveStableDeviceKey(opts = {}) {
|
|
26707
27476
|
const hostname = opts.hostname ?? import_node_os10.default.hostname();
|
|
26708
27477
|
const uid = opts.uid ?? (typeof import_node_os10.default.userInfo === "function" ? import_node_os10.default.userInfo().uid : 0);
|
|
26709
27478
|
const input = `${hostname}::${uid}`;
|
|
26710
|
-
return
|
|
27479
|
+
return import_node_crypto7.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
|
|
26711
27480
|
}
|
|
26712
27481
|
|
|
26713
27482
|
// src/auth-store.ts
|
|
26714
|
-
var
|
|
26715
|
-
var
|
|
26716
|
-
var
|
|
27483
|
+
var import_node_fs20 = __toESM(require("fs"), 1);
|
|
27484
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
27485
|
+
var import_node_crypto8 = __toESM(require("crypto"), 1);
|
|
26717
27486
|
var AUTH_FILE_NAME = "auth.json";
|
|
26718
27487
|
function authFilePath(dataDir) {
|
|
26719
|
-
return
|
|
27488
|
+
return import_node_path22.default.join(dataDir, AUTH_FILE_NAME);
|
|
26720
27489
|
}
|
|
26721
27490
|
function loadOrCreateAuthToken(opts) {
|
|
26722
27491
|
const file = authFilePath(opts.dataDir);
|
|
@@ -26728,11 +27497,11 @@ function loadOrCreateAuthToken(opts) {
|
|
|
26728
27497
|
return token;
|
|
26729
27498
|
}
|
|
26730
27499
|
function defaultGenerate() {
|
|
26731
|
-
return
|
|
27500
|
+
return import_node_crypto8.default.randomBytes(32).toString("base64url");
|
|
26732
27501
|
}
|
|
26733
27502
|
function readAuthFile(file) {
|
|
26734
27503
|
try {
|
|
26735
|
-
const raw =
|
|
27504
|
+
const raw = import_node_fs20.default.readFileSync(file, "utf8");
|
|
26736
27505
|
const parsed = JSON.parse(raw);
|
|
26737
27506
|
if (typeof parsed?.token === "string" && parsed.token.length > 0) {
|
|
26738
27507
|
return {
|
|
@@ -26748,25 +27517,25 @@ function readAuthFile(file) {
|
|
|
26748
27517
|
}
|
|
26749
27518
|
}
|
|
26750
27519
|
function writeAuthFile(file, content) {
|
|
26751
|
-
|
|
26752
|
-
|
|
27520
|
+
import_node_fs20.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true });
|
|
27521
|
+
import_node_fs20.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
|
|
26753
27522
|
try {
|
|
26754
|
-
|
|
27523
|
+
import_node_fs20.default.chmodSync(file, 384);
|
|
26755
27524
|
} catch {
|
|
26756
27525
|
}
|
|
26757
27526
|
}
|
|
26758
27527
|
|
|
26759
27528
|
// src/owner-profile.ts
|
|
26760
|
-
var
|
|
27529
|
+
var import_node_fs21 = __toESM(require("fs"), 1);
|
|
26761
27530
|
var import_node_os11 = __toESM(require("os"), 1);
|
|
26762
|
-
var
|
|
27531
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
26763
27532
|
var PROFILE_FILENAME = "profile.json";
|
|
26764
27533
|
function loadOwnerDisplayName(dataDir) {
|
|
26765
27534
|
const fallback = import_node_os11.default.userInfo().username;
|
|
26766
|
-
const profilePath =
|
|
27535
|
+
const profilePath = import_node_path23.default.join(dataDir, PROFILE_FILENAME);
|
|
26767
27536
|
let raw;
|
|
26768
27537
|
try {
|
|
26769
|
-
raw =
|
|
27538
|
+
raw = import_node_fs21.default.readFileSync(profilePath, "utf8");
|
|
26770
27539
|
} catch {
|
|
26771
27540
|
return fallback;
|
|
26772
27541
|
}
|
|
@@ -26795,12 +27564,12 @@ init_protocol();
|
|
|
26795
27564
|
init_protocol();
|
|
26796
27565
|
|
|
26797
27566
|
// src/session/fork.ts
|
|
26798
|
-
var
|
|
27567
|
+
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
26799
27568
|
var import_node_os12 = __toESM(require("os"), 1);
|
|
26800
|
-
var
|
|
27569
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
26801
27570
|
init_claude_history();
|
|
26802
27571
|
function readJsonlEntries(file) {
|
|
26803
|
-
const raw =
|
|
27572
|
+
const raw = import_node_fs22.default.readFileSync(file, "utf8");
|
|
26804
27573
|
const out = [];
|
|
26805
27574
|
for (const line of raw.split("\n")) {
|
|
26806
27575
|
const t = line.trim();
|
|
@@ -26813,10 +27582,10 @@ function readJsonlEntries(file) {
|
|
|
26813
27582
|
return out;
|
|
26814
27583
|
}
|
|
26815
27584
|
function forkSession(input) {
|
|
26816
|
-
const baseDir = input.baseDir ??
|
|
26817
|
-
const projectDir =
|
|
26818
|
-
const sourceFile =
|
|
26819
|
-
if (!
|
|
27585
|
+
const baseDir = input.baseDir ?? import_node_path24.default.join(import_node_os12.default.homedir(), ".claude");
|
|
27586
|
+
const projectDir = import_node_path24.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
|
|
27587
|
+
const sourceFile = import_node_path24.default.join(projectDir, `${input.toolSessionId}.jsonl`);
|
|
27588
|
+
if (!import_node_fs22.default.existsSync(sourceFile)) {
|
|
26820
27589
|
throw new Error(`fork: source transcript not found: ${sourceFile}`);
|
|
26821
27590
|
}
|
|
26822
27591
|
const entries = readJsonlEntries(sourceFile);
|
|
@@ -26846,9 +27615,9 @@ function forkSession(input) {
|
|
|
26846
27615
|
}
|
|
26847
27616
|
forkedLines.push(JSON.stringify(forked));
|
|
26848
27617
|
}
|
|
26849
|
-
const forkedFilePath =
|
|
26850
|
-
|
|
26851
|
-
|
|
27618
|
+
const forkedFilePath = import_node_path24.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
|
|
27619
|
+
import_node_fs22.default.mkdirSync(projectDir, { recursive: true });
|
|
27620
|
+
import_node_fs22.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
|
|
26852
27621
|
return { forkedToolSessionId, forkedFilePath };
|
|
26853
27622
|
}
|
|
26854
27623
|
|
|
@@ -27169,9 +27938,9 @@ init_protocol();
|
|
|
27169
27938
|
|
|
27170
27939
|
// src/workspace/git.ts
|
|
27171
27940
|
var import_node_child_process6 = require("child_process");
|
|
27172
|
-
var
|
|
27941
|
+
var import_node_fs23 = __toESM(require("fs"), 1);
|
|
27173
27942
|
var import_node_os13 = __toESM(require("os"), 1);
|
|
27174
|
-
var
|
|
27943
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
27175
27944
|
var import_node_util = require("util");
|
|
27176
27945
|
var pexec = (0, import_node_util.promisify)(import_node_child_process6.execFile);
|
|
27177
27946
|
function formatChildProcessError(err) {
|
|
@@ -27186,9 +27955,9 @@ function formatChildProcessError(err) {
|
|
|
27186
27955
|
return e.message ?? "unknown error";
|
|
27187
27956
|
}
|
|
27188
27957
|
function normalizePath(p2) {
|
|
27189
|
-
const resolved =
|
|
27958
|
+
const resolved = import_node_path25.default.resolve(p2);
|
|
27190
27959
|
try {
|
|
27191
|
-
return
|
|
27960
|
+
return import_node_fs23.default.realpathSync(resolved);
|
|
27192
27961
|
} catch {
|
|
27193
27962
|
return resolved;
|
|
27194
27963
|
}
|
|
@@ -27289,13 +28058,13 @@ function flattenToDirName(branch) {
|
|
|
27289
28058
|
}
|
|
27290
28059
|
function encodeClaudeProjectDir(absPath) {
|
|
27291
28060
|
if (!absPath || typeof absPath !== "string") return "";
|
|
27292
|
-
let canonical =
|
|
28061
|
+
let canonical = import_node_path25.default.resolve(absPath);
|
|
27293
28062
|
try {
|
|
27294
|
-
canonical =
|
|
28063
|
+
canonical = import_node_fs23.default.realpathSync(canonical);
|
|
27295
28064
|
} catch {
|
|
27296
28065
|
try {
|
|
27297
|
-
const parent =
|
|
27298
|
-
canonical =
|
|
28066
|
+
const parent = import_node_fs23.default.realpathSync(import_node_path25.default.dirname(canonical));
|
|
28067
|
+
canonical = import_node_path25.default.join(parent, import_node_path25.default.basename(canonical));
|
|
27299
28068
|
} catch {
|
|
27300
28069
|
}
|
|
27301
28070
|
}
|
|
@@ -27319,11 +28088,11 @@ async function createWorktree(input) {
|
|
|
27319
28088
|
if (!isGitRoot) {
|
|
27320
28089
|
throw new Error(`\u76EE\u5F55 ${cwd} \u4E0D\u662F git repo \u6839`);
|
|
27321
28090
|
}
|
|
27322
|
-
const parent =
|
|
27323
|
-
if (parent === "/" || parent ===
|
|
28091
|
+
const parent = import_node_path25.default.dirname(import_node_path25.default.resolve(cwd));
|
|
28092
|
+
if (parent === "/" || parent === import_node_path25.default.resolve(cwd)) {
|
|
27324
28093
|
throw new Error("repo \u5728\u78C1\u76D8\u6839\u76EE\u5F55\uFF0C\u65E0\u6CD5\u5728\u540C\u7EA7\u521B\u5EFA worktree");
|
|
27325
28094
|
}
|
|
27326
|
-
const worktreeRoot =
|
|
28095
|
+
const worktreeRoot = import_node_path25.default.join(parent, dirName);
|
|
27327
28096
|
try {
|
|
27328
28097
|
await pexec("git", ["-C", cwd, "fetch", "origin", baseBranch, "--no-tags"], {
|
|
27329
28098
|
timeout: 3e4
|
|
@@ -27342,7 +28111,7 @@ async function createWorktree(input) {
|
|
|
27342
28111
|
const msg = err.message;
|
|
27343
28112
|
if (msg.startsWith("\u5206\u652F ")) throw err;
|
|
27344
28113
|
}
|
|
27345
|
-
if (
|
|
28114
|
+
if (import_node_fs23.default.existsSync(worktreeRoot)) {
|
|
27346
28115
|
throw new Error(`\u76EE\u5F55 ${worktreeRoot} \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E00\u4E2A label \u6216\u6E05\u7406\u540E\u91CD\u8BD5`);
|
|
27347
28116
|
}
|
|
27348
28117
|
try {
|
|
@@ -27370,8 +28139,8 @@ async function removeWorktree(input) {
|
|
|
27370
28139
|
);
|
|
27371
28140
|
const gitCommonDir = stdout.trim();
|
|
27372
28141
|
if (!gitCommonDir) throw new Error("empty git-common-dir");
|
|
27373
|
-
const absGitCommon =
|
|
27374
|
-
repoRoot =
|
|
28142
|
+
const absGitCommon = import_node_path25.default.isAbsolute(gitCommonDir) ? gitCommonDir : import_node_path25.default.resolve(worktreeRoot, gitCommonDir);
|
|
28143
|
+
repoRoot = import_node_path25.default.dirname(absGitCommon);
|
|
27375
28144
|
} catch {
|
|
27376
28145
|
repoRoot = null;
|
|
27377
28146
|
}
|
|
@@ -27383,7 +28152,7 @@ async function removeWorktree(input) {
|
|
|
27383
28152
|
} catch (err) {
|
|
27384
28153
|
const stderr = err.stderr ?? "";
|
|
27385
28154
|
const lower = stderr.toLowerCase();
|
|
27386
|
-
const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !
|
|
28155
|
+
const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !import_node_fs23.default.existsSync(worktreeRoot);
|
|
27387
28156
|
if (!vanished) {
|
|
27388
28157
|
throw new Error(`\u6E05\u7406 worktree \u5931\u8D25\uFF1A${formatChildProcessError(err)}`);
|
|
27389
28158
|
}
|
|
@@ -27402,10 +28171,10 @@ async function removeWorktree(input) {
|
|
|
27402
28171
|
try {
|
|
27403
28172
|
const encoded = encodeClaudeProjectDir(worktreeRoot);
|
|
27404
28173
|
if (encoded) {
|
|
27405
|
-
const projectsRoot =
|
|
27406
|
-
const target =
|
|
27407
|
-
if (target.startsWith(projectsRoot +
|
|
27408
|
-
|
|
28174
|
+
const projectsRoot = import_node_path25.default.join(import_node_os13.default.homedir(), ".claude", "projects");
|
|
28175
|
+
const target = import_node_path25.default.resolve(projectsRoot, encoded);
|
|
28176
|
+
if (target.startsWith(projectsRoot + import_node_path25.default.sep) && target !== projectsRoot) {
|
|
28177
|
+
import_node_fs23.default.rmSync(target, { recursive: true, force: true });
|
|
27409
28178
|
}
|
|
27410
28179
|
}
|
|
27411
28180
|
} catch {
|
|
@@ -27484,7 +28253,7 @@ init_protocol();
|
|
|
27484
28253
|
var version = "0.2.6".length > 0 ? "0.2.6" : "dev";
|
|
27485
28254
|
|
|
27486
28255
|
// src/handlers/meta.ts
|
|
27487
|
-
function buildReadyFrame(deps) {
|
|
28256
|
+
function buildReadyFrame(deps, client) {
|
|
27488
28257
|
const info = deps.manager.info();
|
|
27489
28258
|
const tools = [];
|
|
27490
28259
|
for (const id of listRegistered()) {
|
|
@@ -27496,6 +28265,14 @@ function buildReadyFrame(deps) {
|
|
|
27496
28265
|
}
|
|
27497
28266
|
}
|
|
27498
28267
|
const tunnelUrl = deps.getTunnelUrl ? deps.getTunnelUrl() : null;
|
|
28268
|
+
const fileSharing = {};
|
|
28269
|
+
const httpBaseUrl = deps.getHttpBaseUrl ? deps.getHttpBaseUrl() : null;
|
|
28270
|
+
if (httpBaseUrl) {
|
|
28271
|
+
fileSharing.tokenRole = "owner";
|
|
28272
|
+
fileSharing.isLoopback = isLoopbackAddr(client?.remoteAddress);
|
|
28273
|
+
fileSharing.httpBaseUrl = httpBaseUrl;
|
|
28274
|
+
if (deps.httpToken) fileSharing.httpToken = deps.httpToken;
|
|
28275
|
+
}
|
|
27499
28276
|
return {
|
|
27500
28277
|
version,
|
|
27501
28278
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -27504,7 +28281,8 @@ function buildReadyFrame(deps) {
|
|
|
27504
28281
|
tools,
|
|
27505
28282
|
runningSessions: info.runningSessions,
|
|
27506
28283
|
tunnelUrl,
|
|
27507
|
-
mode: deps.mode
|
|
28284
|
+
mode: deps.mode,
|
|
28285
|
+
...fileSharing
|
|
27508
28286
|
};
|
|
27509
28287
|
}
|
|
27510
28288
|
function buildMetaHandlers(deps) {
|
|
@@ -27615,6 +28393,119 @@ function buildPersonaHandlers(deps) {
|
|
|
27615
28393
|
};
|
|
27616
28394
|
}
|
|
27617
28395
|
|
|
28396
|
+
// src/handlers/attachment.ts
|
|
28397
|
+
init_protocol();
|
|
28398
|
+
init_protocol();
|
|
28399
|
+
var DEFAULT_TTL_SECONDS = 24 * 3600;
|
|
28400
|
+
function buildAttachmentHandlers(deps) {
|
|
28401
|
+
const signUrl = async (frame) => {
|
|
28402
|
+
const parsed = AttachmentSignUrlArgs.safeParse(frame);
|
|
28403
|
+
if (!parsed.success) {
|
|
28404
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28405
|
+
}
|
|
28406
|
+
const args = parsed.data;
|
|
28407
|
+
const secret = deps.getSignSecret();
|
|
28408
|
+
if (!secret) {
|
|
28409
|
+
throw new ClawdError(
|
|
28410
|
+
ERROR_CODES.METHOD_NOT_IMPLEMENTED,
|
|
28411
|
+
"signUrl requires an owner token (daemon noAuth mode disables share URLs)"
|
|
28412
|
+
);
|
|
28413
|
+
}
|
|
28414
|
+
const httpBaseUrl = deps.getHttpBaseUrl();
|
|
28415
|
+
if (!httpBaseUrl) {
|
|
28416
|
+
throw new ClawdError(
|
|
28417
|
+
ERROR_CODES.METHOD_NOT_IMPLEMENTED,
|
|
28418
|
+
"httpBaseUrl unavailable (daemon HTTP not ready)"
|
|
28419
|
+
);
|
|
28420
|
+
}
|
|
28421
|
+
const ttl = args.ttlSeconds === null ? null : args.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
28422
|
+
const parts = signUrlParts(secret, args.absPath, ttl);
|
|
28423
|
+
const url = buildSignedFileUrl(httpBaseUrl, parts);
|
|
28424
|
+
return {
|
|
28425
|
+
response: {
|
|
28426
|
+
type: "attachment.signUrl",
|
|
28427
|
+
url,
|
|
28428
|
+
expiresAt: parts.e === null ? null : parts.e * 1e3
|
|
28429
|
+
}
|
|
28430
|
+
};
|
|
28431
|
+
};
|
|
28432
|
+
const groupAdd = async (frame) => {
|
|
28433
|
+
if (!deps.groupFileStore || !deps.getSessionScope) {
|
|
28434
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
|
|
28435
|
+
}
|
|
28436
|
+
const parsed = AttachmentGroupAddArgs.safeParse(frame);
|
|
28437
|
+
if (!parsed.success) {
|
|
28438
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28439
|
+
}
|
|
28440
|
+
const args = parsed.data;
|
|
28441
|
+
const scope = deps.getSessionScope(args.sessionId);
|
|
28442
|
+
if (!scope) {
|
|
28443
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `session ${args.sessionId} not found`);
|
|
28444
|
+
}
|
|
28445
|
+
const size = 0;
|
|
28446
|
+
const entry = deps.groupFileStore.upsert(scope, args.sessionId, {
|
|
28447
|
+
relPath: args.relPath,
|
|
28448
|
+
from: "owner",
|
|
28449
|
+
label: args.label,
|
|
28450
|
+
size,
|
|
28451
|
+
mime: lookupMime(args.relPath)
|
|
28452
|
+
});
|
|
28453
|
+
return { response: { type: "attachment.groupAdd", entry } };
|
|
28454
|
+
};
|
|
28455
|
+
const groupRemove = async (frame) => {
|
|
28456
|
+
if (!deps.groupFileStore || !deps.getSessionScope) {
|
|
28457
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
|
|
28458
|
+
}
|
|
28459
|
+
const parsed = AttachmentGroupRemoveArgs.safeParse(frame);
|
|
28460
|
+
if (!parsed.success) {
|
|
28461
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28462
|
+
}
|
|
28463
|
+
const scope = deps.getSessionScope(parsed.data.sessionId);
|
|
28464
|
+
if (!scope) {
|
|
28465
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "session not found");
|
|
28466
|
+
}
|
|
28467
|
+
const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
|
|
28468
|
+
const target = entries.find((e) => e.relPath === parsed.data.relPath);
|
|
28469
|
+
if (target?.from === "owner") {
|
|
28470
|
+
deps.groupFileStore.remove(scope, parsed.data.sessionId, parsed.data.relPath);
|
|
28471
|
+
} else {
|
|
28472
|
+
deps.groupFileStore.markStale(scope, parsed.data.sessionId, parsed.data.relPath);
|
|
28473
|
+
}
|
|
28474
|
+
return { response: { type: "attachment.groupRemove", removed: true } };
|
|
28475
|
+
};
|
|
28476
|
+
const groupList = async (frame) => {
|
|
28477
|
+
if (!deps.groupFileStore || !deps.getSessionScope) {
|
|
28478
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
|
|
28479
|
+
}
|
|
28480
|
+
const parsed = AttachmentGroupListArgs.safeParse(frame);
|
|
28481
|
+
if (!parsed.success) {
|
|
28482
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28483
|
+
}
|
|
28484
|
+
const scope = deps.getSessionScope(parsed.data.sessionId);
|
|
28485
|
+
if (!scope) return { response: { type: "attachment.groupList", entries: [] } };
|
|
28486
|
+
const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
|
|
28487
|
+
return { response: { type: "attachment.groupList", entries } };
|
|
28488
|
+
};
|
|
28489
|
+
const groupListPersona = async (frame) => {
|
|
28490
|
+
if (!deps.groupFileStore) {
|
|
28491
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
|
|
28492
|
+
}
|
|
28493
|
+
const parsed = AttachmentGroupListPersonaArgs.safeParse(frame);
|
|
28494
|
+
if (!parsed.success) {
|
|
28495
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28496
|
+
}
|
|
28497
|
+
const perSession = deps.groupFileStore.listByPersona(parsed.data.personaId);
|
|
28498
|
+
return { response: { type: "attachment.groupListPersona", perSession } };
|
|
28499
|
+
};
|
|
28500
|
+
return {
|
|
28501
|
+
"attachment.signUrl": signUrl,
|
|
28502
|
+
"attachment.groupAdd": groupAdd,
|
|
28503
|
+
"attachment.groupRemove": groupRemove,
|
|
28504
|
+
"attachment.groupList": groupList,
|
|
28505
|
+
"attachment.groupListPersona": groupListPersona
|
|
28506
|
+
};
|
|
28507
|
+
}
|
|
28508
|
+
|
|
27618
28509
|
// src/handlers/index.ts
|
|
27619
28510
|
function buildMethodHandlers(deps) {
|
|
27620
28511
|
return {
|
|
@@ -27630,7 +28521,8 @@ function buildMethodHandlers(deps) {
|
|
|
27630
28521
|
personaRegistry: deps.personaRegistry,
|
|
27631
28522
|
sessionManager: deps.manager,
|
|
27632
28523
|
personaBoundHandler: deps.personaBoundHandler
|
|
27633
|
-
})
|
|
28524
|
+
}),
|
|
28525
|
+
...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
|
|
27634
28526
|
};
|
|
27635
28527
|
}
|
|
27636
28528
|
|
|
@@ -27638,7 +28530,7 @@ function buildMethodHandlers(deps) {
|
|
|
27638
28530
|
async function startDaemon(config) {
|
|
27639
28531
|
const logger = createLogger({
|
|
27640
28532
|
level: config.logLevel,
|
|
27641
|
-
file:
|
|
28533
|
+
file: import_node_path26.default.join(config.dataDir, "clawd.log")
|
|
27642
28534
|
});
|
|
27643
28535
|
logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
|
|
27644
28536
|
const stateMgr = new StateFileManager({ dataDir: config.dataDir });
|
|
@@ -27670,7 +28562,7 @@ async function startDaemon(config) {
|
|
|
27670
28562
|
const agents = new AgentsScanner();
|
|
27671
28563
|
const history = new ClaudeHistoryReader();
|
|
27672
28564
|
let transport = null;
|
|
27673
|
-
const personaStore = new PersonaStore(
|
|
28565
|
+
const personaStore = new PersonaStore(import_node_path26.default.join(config.dataDir, "personas"));
|
|
27674
28566
|
const defaultsRoot = findDefaultsRoot();
|
|
27675
28567
|
if (defaultsRoot) {
|
|
27676
28568
|
seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
|
|
@@ -27678,13 +28570,14 @@ async function startDaemon(config) {
|
|
|
27678
28570
|
logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
|
|
27679
28571
|
}
|
|
27680
28572
|
const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
|
|
28573
|
+
const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
|
|
27681
28574
|
const manager = new SessionManager({
|
|
27682
28575
|
store,
|
|
27683
28576
|
logger,
|
|
27684
28577
|
getAdapter,
|
|
27685
28578
|
historyReader: history,
|
|
27686
28579
|
dataDir: config.dataDir,
|
|
27687
|
-
personaRoot:
|
|
28580
|
+
personaRoot: import_node_path26.default.join(config.dataDir, "personas"),
|
|
27688
28581
|
personaStore,
|
|
27689
28582
|
ownerDisplayName,
|
|
27690
28583
|
mode: config.mode,
|
|
@@ -27701,6 +28594,38 @@ async function startDaemon(config) {
|
|
|
27701
28594
|
return;
|
|
27702
28595
|
}
|
|
27703
28596
|
transport?.broadcastToSession(sid, frame);
|
|
28597
|
+
},
|
|
28598
|
+
// file-sharing (spec §6 PR 3):runner 检测到成功 file-edit tool_result 时,
|
|
28599
|
+
// 闭包 stat + mime 写入群清单。stat 失败不阻塞主流程(log warn + 跳过本条),
|
|
28600
|
+
// 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
|
|
28601
|
+
attachmentGroup: {
|
|
28602
|
+
onFileEdit: (input) => {
|
|
28603
|
+
const absPath = import_node_path26.default.isAbsolute(input.relPath) ? input.relPath : import_node_path26.default.join(input.cwd, input.relPath);
|
|
28604
|
+
let size = 0;
|
|
28605
|
+
try {
|
|
28606
|
+
size = import_node_fs24.default.statSync(absPath).size;
|
|
28607
|
+
} catch (err) {
|
|
28608
|
+
logger.warn("attachment.onFileEdit stat failed", {
|
|
28609
|
+
sessionId: input.sessionId,
|
|
28610
|
+
absPath,
|
|
28611
|
+
err: err.message
|
|
28612
|
+
});
|
|
28613
|
+
}
|
|
28614
|
+
try {
|
|
28615
|
+
groupFileStore.upsert(input.scope, input.sessionId, {
|
|
28616
|
+
relPath: input.relPath,
|
|
28617
|
+
from: "agent",
|
|
28618
|
+
size,
|
|
28619
|
+
mime: lookupMime(input.relPath)
|
|
28620
|
+
});
|
|
28621
|
+
} catch (err) {
|
|
28622
|
+
logger.warn("attachment.onFileEdit upsert failed", {
|
|
28623
|
+
sessionId: input.sessionId,
|
|
28624
|
+
relPath: input.relPath,
|
|
28625
|
+
err: err.message
|
|
28626
|
+
});
|
|
28627
|
+
}
|
|
28628
|
+
}
|
|
27704
28629
|
}
|
|
27705
28630
|
});
|
|
27706
28631
|
const observer = new SessionObserver({
|
|
@@ -27746,6 +28671,12 @@ async function startDaemon(config) {
|
|
|
27746
28671
|
sessionManager: manager
|
|
27747
28672
|
});
|
|
27748
28673
|
let currentTunnelUrl = null;
|
|
28674
|
+
const getHttpBaseUrl = () => {
|
|
28675
|
+
if (currentTunnelUrl) {
|
|
28676
|
+
return currentTunnelUrl.replace(/^wss:/i, "https:").replace(/^ws:/i, "http:");
|
|
28677
|
+
}
|
|
28678
|
+
return `http://${config.host}:${config.port}`;
|
|
28679
|
+
};
|
|
27749
28680
|
const personaBoundHandler = new PersonaBoundHandler({
|
|
27750
28681
|
registry: personaRegistry,
|
|
27751
28682
|
personaManager,
|
|
@@ -27771,22 +28702,68 @@ async function startDaemon(config) {
|
|
|
27771
28702
|
getTunnelUrl: () => currentTunnelUrl,
|
|
27772
28703
|
// ready / info 帧的 mode = daemon CC spawn 模式('sdk' | 'tui')。UI 据此挂 XtermPanel +
|
|
27773
28704
|
// 订阅 session:pty / session:control;业务帧名两种 mode 完全一致,UI 业务订阅代码不变
|
|
27774
|
-
mode: config.mode
|
|
28705
|
+
mode: config.mode,
|
|
28706
|
+
// file-sharing (spec §8):ready / info 帧把 httpBaseUrl + httpToken 下发给 UI。
|
|
28707
|
+
// PR 2 阶段 httpToken 复用 owner WS token;noAuth 模式下为 null(UI 看到无 httpToken
|
|
28708
|
+
// 时禁用文件 GET/POST,保持 1.0 行为兼容)。
|
|
28709
|
+
getHttpBaseUrl,
|
|
28710
|
+
httpToken: resolvedAuthToken,
|
|
28711
|
+
// file-sharing attachment.* RPC。signUrl 用 owner token 做 HMAC secret;group RPC
|
|
28712
|
+
// 根据 sessionId 反查 scope 写盘。
|
|
28713
|
+
attachment: {
|
|
28714
|
+
groupFileStore,
|
|
28715
|
+
getHttpBaseUrl,
|
|
28716
|
+
// HMAC sign secret:复用 ~/.clawd/auth.json owner token(持久跨重启)。
|
|
28717
|
+
// noAuth 模式 resolvedAuthToken 为 null → handler 自己返 NOT_IMPLEMENTED。
|
|
28718
|
+
getSignSecret: () => resolvedAuthToken ?? "",
|
|
28719
|
+
// group RPC:根据 sessionId 反查 scope;owner-mode persona session 走
|
|
28720
|
+
// 'persona/<pid>/owner',default 走 'default'。
|
|
28721
|
+
getSessionScope: (sessionId) => {
|
|
28722
|
+
const file = store.read(sessionId);
|
|
28723
|
+
if (!file) return null;
|
|
28724
|
+
if (file.ownerPersonaId) {
|
|
28725
|
+
return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
|
|
28726
|
+
}
|
|
28727
|
+
return { kind: "default" };
|
|
28728
|
+
}
|
|
28729
|
+
}
|
|
28730
|
+
});
|
|
28731
|
+
const authResolver = new AuthContextResolver({
|
|
28732
|
+
ownerToken: resolvedAuthToken,
|
|
28733
|
+
personaRegistry
|
|
28734
|
+
});
|
|
28735
|
+
const httpRouter = createHttpRouter({
|
|
28736
|
+
authResolver,
|
|
28737
|
+
daemonVersion: version,
|
|
28738
|
+
logger,
|
|
28739
|
+
personaStore,
|
|
28740
|
+
groupFileStore,
|
|
28741
|
+
sessionStore: store,
|
|
28742
|
+
// /files HMAC verify 用同一份 owner token 做 secret(与 attachment.signUrl 同源)
|
|
28743
|
+
getSignSecret: () => resolvedAuthToken ?? null
|
|
27775
28744
|
});
|
|
27776
28745
|
wsServer = new LocalWsServer({
|
|
27777
28746
|
host: config.host,
|
|
27778
28747
|
port: config.port,
|
|
27779
28748
|
logger,
|
|
27780
|
-
readyFrameBuilder: () => buildReadyFrame(
|
|
27781
|
-
|
|
27782
|
-
|
|
27783
|
-
|
|
27784
|
-
|
|
27785
|
-
|
|
27786
|
-
|
|
28749
|
+
readyFrameBuilder: (ctx) => buildReadyFrame(
|
|
28750
|
+
{
|
|
28751
|
+
manager,
|
|
28752
|
+
getAdapter,
|
|
28753
|
+
getTunnelUrl: () => currentTunnelUrl,
|
|
28754
|
+
// ready 帧 mode = daemon CC spawn 模式('sdk' | 'tui');UI 用它挂 XtermPanel
|
|
28755
|
+
mode: config.mode,
|
|
28756
|
+
// file-sharing 字段:httpBaseUrl 跟 tunnel 状态走;httpToken 复用 owner WS token
|
|
28757
|
+
getHttpBaseUrl,
|
|
28758
|
+
httpToken: resolvedAuthToken
|
|
28759
|
+
},
|
|
28760
|
+
ctx
|
|
28761
|
+
),
|
|
27787
28762
|
protocolVersion: PROTOCOL_VERSION,
|
|
27788
28763
|
authGate: authGate ?? void 0,
|
|
27789
28764
|
personaBoundHandler,
|
|
28765
|
+
// file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
|
|
28766
|
+
httpRequestHandler: httpRouter,
|
|
27790
28767
|
// 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
|
|
27791
28768
|
// daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
|
|
27792
28769
|
// 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
|
|
@@ -27902,8 +28879,8 @@ async function startDaemon(config) {
|
|
|
27902
28879
|
const lines = [
|
|
27903
28880
|
`Tunnel: ${r.url}`,
|
|
27904
28881
|
...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
|
|
27905
|
-
`Frpc config: ${
|
|
27906
|
-
`Frpc log: ${
|
|
28882
|
+
`Frpc config: ${import_node_path26.default.join(config.dataDir, "frpc.toml")}`,
|
|
28883
|
+
`Frpc log: ${import_node_path26.default.join(config.dataDir, "frpc.log")}`
|
|
27907
28884
|
];
|
|
27908
28885
|
const width = Math.max(...lines.map((l) => l.length));
|
|
27909
28886
|
const bar = "\u2550".repeat(width + 4);
|
|
@@ -27916,8 +28893,8 @@ ${bar}
|
|
|
27916
28893
|
|
|
27917
28894
|
`);
|
|
27918
28895
|
try {
|
|
27919
|
-
const connectPath =
|
|
27920
|
-
|
|
28896
|
+
const connectPath = import_node_path26.default.join(config.dataDir, "connect.txt");
|
|
28897
|
+
import_node_fs24.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
|
|
27921
28898
|
} catch {
|
|
27922
28899
|
}
|
|
27923
28900
|
} catch (err) {
|