@clawos-dev/clawd 0.2.63 → 0.2.64-beta.104.6f4b611
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 +1838 -322
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -110,6 +110,22 @@ var init_methods = __esm({
|
|
|
110
110
|
// 触发频率低(仅 window resize),response 也是 ack。
|
|
111
111
|
"session:pty:input",
|
|
112
112
|
"session:pty:resize",
|
|
113
|
+
// ---- attachment.* file-sharing(spec §5;详见 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/POST,详见 spec §5),不在白名单内。
|
|
118
|
+
"attachment.outboxCreate",
|
|
119
|
+
"attachment.outboxRevoke",
|
|
120
|
+
"attachment.outboxList",
|
|
121
|
+
"attachment.mountAdd",
|
|
122
|
+
"attachment.mountRemove",
|
|
123
|
+
"attachment.mountList",
|
|
124
|
+
"attachment.groupAdd",
|
|
125
|
+
"attachment.groupRemove",
|
|
126
|
+
"attachment.groupList",
|
|
127
|
+
// v2:跨 session 聚合(本期 UI 不调,保留槽位用于 HTTP ACL 内部判定 / 未来 "All files" tab)
|
|
128
|
+
"attachment.groupListPersona",
|
|
113
129
|
"info",
|
|
114
130
|
"ping"
|
|
115
131
|
];
|
|
@@ -601,8 +617,8 @@ var init_parseUtil = __esm({
|
|
|
601
617
|
init_errors2();
|
|
602
618
|
init_en();
|
|
603
619
|
makeIssue = (params) => {
|
|
604
|
-
const { data, path:
|
|
605
|
-
const fullPath = [...
|
|
620
|
+
const { data, path: path34, errorMaps, issueData } = params;
|
|
621
|
+
const fullPath = [...path34, ...issueData.path || []];
|
|
606
622
|
const fullIssue = {
|
|
607
623
|
...issueData,
|
|
608
624
|
path: fullPath
|
|
@@ -913,11 +929,11 @@ var init_types = __esm({
|
|
|
913
929
|
init_parseUtil();
|
|
914
930
|
init_util();
|
|
915
931
|
ParseInputLazyPath = class {
|
|
916
|
-
constructor(parent, value,
|
|
932
|
+
constructor(parent, value, path34, key) {
|
|
917
933
|
this._cachedPath = [];
|
|
918
934
|
this.parent = parent;
|
|
919
935
|
this.data = value;
|
|
920
|
-
this._path =
|
|
936
|
+
this._path = path34;
|
|
921
937
|
this._key = key;
|
|
922
938
|
}
|
|
923
939
|
get path() {
|
|
@@ -4300,6 +4316,151 @@ var init_zod = __esm({
|
|
|
4300
4316
|
}
|
|
4301
4317
|
});
|
|
4302
4318
|
|
|
4319
|
+
// ../protocol/src/attachment-schemas.ts
|
|
4320
|
+
var TOKEN_ROLES, GROUP_FILE_SOURCES, MOUNT_MODES, GroupFileEntrySchema, MountEntrySchema, OutboxScopeSchema, OutboxCapEntrySchema, AttachmentOutboxCreateArgs, AttachmentOutboxCreateResponseSchema, AttachmentOutboxRevokeArgs, AttachmentOutboxRevokeResponseSchema, AttachmentOutboxListArgs, AttachmentOutboxListResponseSchema, AttachmentMountAddArgs, AttachmentMountAddResponseSchema, AttachmentMountRemoveArgs, AttachmentMountRemoveResponseSchema, AttachmentMountListArgs, AttachmentMountListResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema, AttachmentGroupListPersonaArgs, AttachmentGroupListPersonaResponseSchema;
|
|
4321
|
+
var init_attachment_schemas = __esm({
|
|
4322
|
+
"../protocol/src/attachment-schemas.ts"() {
|
|
4323
|
+
"use strict";
|
|
4324
|
+
init_zod();
|
|
4325
|
+
TOKEN_ROLES = ["owner", "personal"];
|
|
4326
|
+
GROUP_FILE_SOURCES = ["agent", "user", "owner"];
|
|
4327
|
+
MOUNT_MODES = ["link", "copy"];
|
|
4328
|
+
GroupFileEntrySchema = external_exports.object({
|
|
4329
|
+
/** daemon 派发的稳定 id(用于 RPC remove / UI key) */
|
|
4330
|
+
id: external_exports.string().min(1),
|
|
4331
|
+
/** 相对 personaDir / sessionCwd 的路径 */
|
|
4332
|
+
relPath: external_exports.string().min(1),
|
|
4333
|
+
from: external_exports.enum(GROUP_FILE_SOURCES),
|
|
4334
|
+
/** user 上传时的客户端标识(personal label / hostname),from='user' 才有 */
|
|
4335
|
+
addedBy: external_exports.string().optional(),
|
|
4336
|
+
/** owner 手动加群时的可选备注 */
|
|
4337
|
+
label: external_exports.string().optional(),
|
|
4338
|
+
/** 文件字节数(stat 时拍) */
|
|
4339
|
+
size: external_exports.number().int().nonnegative(),
|
|
4340
|
+
mime: external_exports.string().min(1),
|
|
4341
|
+
/** 入群时间戳(ms) */
|
|
4342
|
+
addedAt: external_exports.number().int().nonnegative(),
|
|
4343
|
+
/** 最近一次 agent Write/Edit 时间戳(agent 反复改同 path 时更新) */
|
|
4344
|
+
lastEditedAt: external_exports.number().int().nonnegative().optional(),
|
|
4345
|
+
/** agent rm / 文件不见了时打标;UI 灰显,不能再 share */
|
|
4346
|
+
stale: external_exports.boolean().optional()
|
|
4347
|
+
});
|
|
4348
|
+
MountEntrySchema = external_exports.object({
|
|
4349
|
+
/** personaDir 下的 basename(link 目标 / copy 目的名) */
|
|
4350
|
+
basename: external_exports.string().min(1),
|
|
4351
|
+
/** 原始绝对路径(link 模式下 realpath 后用于 sandbox.allowRead 派生) */
|
|
4352
|
+
absPath: external_exports.string().min(1),
|
|
4353
|
+
mode: external_exports.enum(MOUNT_MODES)
|
|
4354
|
+
});
|
|
4355
|
+
OutboxScopeSchema = external_exports.discriminatedUnion("kind", [
|
|
4356
|
+
external_exports.object({ kind: external_exports.literal("public") }),
|
|
4357
|
+
external_exports.object({ kind: external_exports.literal("personal"), personalId: external_exports.string().min(1) })
|
|
4358
|
+
]);
|
|
4359
|
+
OutboxCapEntrySchema = external_exports.object({
|
|
4360
|
+
capToken: external_exports.string().min(1),
|
|
4361
|
+
/** 归属:persona 域 cap 带 personaId;direct 会话 cap 带 sessionId */
|
|
4362
|
+
scopeRef: external_exports.union([
|
|
4363
|
+
external_exports.object({ kind: external_exports.literal("persona"), personaId: external_exports.string().min(1) }),
|
|
4364
|
+
external_exports.object({ kind: external_exports.literal("session"), sessionId: external_exports.string().min(1) })
|
|
4365
|
+
]),
|
|
4366
|
+
absPath: external_exports.string().min(1),
|
|
4367
|
+
/** display name(UI 列表显示 + Share dialog 标题),通常等于 basename */
|
|
4368
|
+
name: external_exports.string().min(1),
|
|
4369
|
+
expiresAt: external_exports.number().int().nonnegative().nullable(),
|
|
4370
|
+
oneShot: external_exports.boolean(),
|
|
4371
|
+
scope: OutboxScopeSchema,
|
|
4372
|
+
hits: external_exports.number().int().nonnegative(),
|
|
4373
|
+
revoked: external_exports.boolean().optional(),
|
|
4374
|
+
createdAt: external_exports.number().int().nonnegative()
|
|
4375
|
+
});
|
|
4376
|
+
AttachmentOutboxCreateArgs = external_exports.object({
|
|
4377
|
+
/** persona 域:传 personaId;direct 会话:传 sessionId(二选一) */
|
|
4378
|
+
personaId: external_exports.string().min(1).optional(),
|
|
4379
|
+
sessionId: external_exports.string().min(1).optional(),
|
|
4380
|
+
absPath: external_exports.string().min(1),
|
|
4381
|
+
/** TTL 秒数;缺省 24h;'never' 走 null */
|
|
4382
|
+
ttlSeconds: external_exports.number().int().positive().nullable().optional(),
|
|
4383
|
+
oneShot: external_exports.boolean().optional(),
|
|
4384
|
+
scope: OutboxScopeSchema.optional()
|
|
4385
|
+
});
|
|
4386
|
+
AttachmentOutboxCreateResponseSchema = external_exports.object({
|
|
4387
|
+
capToken: external_exports.string().min(1),
|
|
4388
|
+
/** 完整 URL(含 httpBaseUrl 前缀),UI 直接复制到剪贴板 */
|
|
4389
|
+
url: external_exports.string().min(1),
|
|
4390
|
+
expiresAt: external_exports.number().int().nonnegative().nullable()
|
|
4391
|
+
});
|
|
4392
|
+
AttachmentOutboxRevokeArgs = external_exports.object({
|
|
4393
|
+
capToken: external_exports.string().min(1)
|
|
4394
|
+
});
|
|
4395
|
+
AttachmentOutboxRevokeResponseSchema = external_exports.object({
|
|
4396
|
+
revoked: external_exports.literal(true)
|
|
4397
|
+
});
|
|
4398
|
+
AttachmentOutboxListArgs = external_exports.object({
|
|
4399
|
+
/** 缺省 = 全部(含 direct 会话);传 personaId 过滤到该 persona */
|
|
4400
|
+
personaId: external_exports.string().min(1).optional()
|
|
4401
|
+
});
|
|
4402
|
+
AttachmentOutboxListResponseSchema = external_exports.object({
|
|
4403
|
+
entries: external_exports.array(OutboxCapEntrySchema)
|
|
4404
|
+
});
|
|
4405
|
+
AttachmentMountAddArgs = external_exports.object({
|
|
4406
|
+
personaId: external_exports.string().min(1),
|
|
4407
|
+
absPath: external_exports.string().min(1),
|
|
4408
|
+
mode: external_exports.enum(MOUNT_MODES)
|
|
4409
|
+
});
|
|
4410
|
+
AttachmentMountAddResponseSchema = external_exports.object({
|
|
4411
|
+
entry: MountEntrySchema,
|
|
4412
|
+
/** sandbox.allowRead 派生后是否需要重启 session 才能让 CC 子进程跟 symlink */
|
|
4413
|
+
sandboxRestartNeeded: external_exports.boolean()
|
|
4414
|
+
});
|
|
4415
|
+
AttachmentMountRemoveArgs = external_exports.object({
|
|
4416
|
+
personaId: external_exports.string().min(1),
|
|
4417
|
+
basename: external_exports.string().min(1)
|
|
4418
|
+
});
|
|
4419
|
+
AttachmentMountRemoveResponseSchema = external_exports.object({
|
|
4420
|
+
removed: external_exports.literal(true)
|
|
4421
|
+
});
|
|
4422
|
+
AttachmentMountListArgs = external_exports.object({
|
|
4423
|
+
personaId: external_exports.string().min(1)
|
|
4424
|
+
});
|
|
4425
|
+
AttachmentMountListResponseSchema = external_exports.object({
|
|
4426
|
+
entries: external_exports.array(MountEntrySchema)
|
|
4427
|
+
});
|
|
4428
|
+
AttachmentGroupAddArgs = external_exports.object({
|
|
4429
|
+
sessionId: external_exports.string().min(1),
|
|
4430
|
+
relPath: external_exports.string().min(1),
|
|
4431
|
+
/** owner 手动加群时可选备注;agent / inbox 自动入清单不走此 RPC */
|
|
4432
|
+
label: external_exports.string().optional()
|
|
4433
|
+
});
|
|
4434
|
+
AttachmentGroupAddResponseSchema = external_exports.object({
|
|
4435
|
+
entry: GroupFileEntrySchema
|
|
4436
|
+
});
|
|
4437
|
+
AttachmentGroupRemoveArgs = external_exports.object({
|
|
4438
|
+
sessionId: external_exports.string().min(1),
|
|
4439
|
+
relPath: external_exports.string().min(1)
|
|
4440
|
+
});
|
|
4441
|
+
AttachmentGroupRemoveResponseSchema = external_exports.object({
|
|
4442
|
+
removed: external_exports.literal(true)
|
|
4443
|
+
});
|
|
4444
|
+
AttachmentGroupListArgs = external_exports.object({
|
|
4445
|
+
sessionId: external_exports.string().min(1)
|
|
4446
|
+
});
|
|
4447
|
+
AttachmentGroupListResponseSchema = external_exports.object({
|
|
4448
|
+
entries: external_exports.array(GroupFileEntrySchema)
|
|
4449
|
+
});
|
|
4450
|
+
AttachmentGroupListPersonaArgs = external_exports.object({
|
|
4451
|
+
personaId: external_exports.string().min(1)
|
|
4452
|
+
});
|
|
4453
|
+
AttachmentGroupListPersonaResponseSchema = external_exports.object({
|
|
4454
|
+
perSession: external_exports.array(
|
|
4455
|
+
external_exports.object({
|
|
4456
|
+
sessionId: external_exports.string().min(1),
|
|
4457
|
+
entries: external_exports.array(GroupFileEntrySchema)
|
|
4458
|
+
})
|
|
4459
|
+
)
|
|
4460
|
+
});
|
|
4461
|
+
}
|
|
4462
|
+
});
|
|
4463
|
+
|
|
4303
4464
|
// ../protocol/src/persona-schemas.ts
|
|
4304
4465
|
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
4466
|
var init_persona_schemas = __esm({
|
|
@@ -4505,6 +4666,7 @@ var init_schemas = __esm({
|
|
|
4505
4666
|
"use strict";
|
|
4506
4667
|
init_zod();
|
|
4507
4668
|
init_events();
|
|
4669
|
+
init_attachment_schemas();
|
|
4508
4670
|
init_persona_schemas();
|
|
4509
4671
|
SessionStatusSchema = external_exports.enum(SESSION_STATUS_VALUES);
|
|
4510
4672
|
UsageSchema = external_exports.object({
|
|
@@ -5061,7 +5223,22 @@ var init_schemas = __esm({
|
|
|
5061
5223
|
hostname: external_exports.string(),
|
|
5062
5224
|
os: external_exports.string(),
|
|
5063
5225
|
tools: external_exports.array(external_exports.object({ id: external_exports.string(), available: external_exports.boolean() })),
|
|
5064
|
-
runningSessions: external_exports.array(InfoRunningSessionSchema)
|
|
5226
|
+
runningSessions: external_exports.array(InfoRunningSessionSchema),
|
|
5227
|
+
// ── file-sharing 会话身份回执(spec: superpowers/specs/2026-05-19-clawd-file-sharing-design.md §8) ──
|
|
5228
|
+
// PR 1 阶段标 optional(daemon 还没下发,旧 daemon info 响应不带这五个字段);
|
|
5229
|
+
// PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
|
|
5230
|
+
// 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
|
|
5231
|
+
// 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
|
|
5232
|
+
/** 'owner' = 拿到 owner-token 的连接(不限来源 IP);'personal' = persona 派发的访客 token。来源 TOKEN_ROLES(spec §11 #1 中央真理源) */
|
|
5233
|
+
tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
|
|
5234
|
+
/** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
|
|
5235
|
+
tokenPersonaId: external_exports.string().min(1).optional(),
|
|
5236
|
+
/** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
|
|
5237
|
+
isLoopback: external_exports.boolean().optional(),
|
|
5238
|
+
/** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
|
|
5239
|
+
httpBaseUrl: external_exports.string().optional(),
|
|
5240
|
+
/** file-sharing HTTP 路由的 Authorization: Bearer token;与 WS token 解耦,HTTP 401 仅触发 ReAuth 不强踢 WS(spec §11 第 4 条) */
|
|
5241
|
+
httpToken: external_exports.string().optional()
|
|
5065
5242
|
});
|
|
5066
5243
|
}
|
|
5067
5244
|
});
|
|
@@ -5093,6 +5270,7 @@ var init_runtime = __esm({
|
|
|
5093
5270
|
init_frames();
|
|
5094
5271
|
init_persona_schemas();
|
|
5095
5272
|
init_persona_mode();
|
|
5273
|
+
init_attachment_schemas();
|
|
5096
5274
|
}
|
|
5097
5275
|
});
|
|
5098
5276
|
|
|
@@ -5367,8 +5545,8 @@ var require_req = __commonJS({
|
|
|
5367
5545
|
if (req.originalUrl) {
|
|
5368
5546
|
_req.url = req.originalUrl;
|
|
5369
5547
|
} else {
|
|
5370
|
-
const
|
|
5371
|
-
_req.url = typeof
|
|
5548
|
+
const path34 = req.path;
|
|
5549
|
+
_req.url = typeof path34 === "string" ? path34 : req.url ? req.url.path || req.url : void 0;
|
|
5372
5550
|
}
|
|
5373
5551
|
if (req.query) {
|
|
5374
5552
|
_req.query = req.query;
|
|
@@ -5533,14 +5711,14 @@ var require_redact = __commonJS({
|
|
|
5533
5711
|
}
|
|
5534
5712
|
return obj;
|
|
5535
5713
|
}
|
|
5536
|
-
function parsePath(
|
|
5714
|
+
function parsePath(path34) {
|
|
5537
5715
|
const parts = [];
|
|
5538
5716
|
let current = "";
|
|
5539
5717
|
let inBrackets = false;
|
|
5540
5718
|
let inQuotes = false;
|
|
5541
5719
|
let quoteChar = "";
|
|
5542
|
-
for (let i = 0; i <
|
|
5543
|
-
const char =
|
|
5720
|
+
for (let i = 0; i < path34.length; i++) {
|
|
5721
|
+
const char = path34[i];
|
|
5544
5722
|
if (!inBrackets && char === ".") {
|
|
5545
5723
|
if (current) {
|
|
5546
5724
|
parts.push(current);
|
|
@@ -5671,10 +5849,10 @@ var require_redact = __commonJS({
|
|
|
5671
5849
|
return current;
|
|
5672
5850
|
}
|
|
5673
5851
|
function redactPaths(obj, paths, censor, remove = false) {
|
|
5674
|
-
for (const
|
|
5675
|
-
const parts = parsePath(
|
|
5852
|
+
for (const path34 of paths) {
|
|
5853
|
+
const parts = parsePath(path34);
|
|
5676
5854
|
if (parts.includes("*")) {
|
|
5677
|
-
redactWildcardPath(obj, parts, censor,
|
|
5855
|
+
redactWildcardPath(obj, parts, censor, path34, remove);
|
|
5678
5856
|
} else {
|
|
5679
5857
|
if (remove) {
|
|
5680
5858
|
removeKey(obj, parts);
|
|
@@ -5759,8 +5937,8 @@ var require_redact = __commonJS({
|
|
|
5759
5937
|
}
|
|
5760
5938
|
} else {
|
|
5761
5939
|
if (afterWildcard.includes("*")) {
|
|
5762
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
5763
|
-
const fullPath = [...pathArray.slice(0, pathLength), ...
|
|
5940
|
+
const wrappedCensor = typeof censor === "function" ? (value, path34) => {
|
|
5941
|
+
const fullPath = [...pathArray.slice(0, pathLength), ...path34];
|
|
5764
5942
|
return censor(value, fullPath);
|
|
5765
5943
|
} : censor;
|
|
5766
5944
|
redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
|
|
@@ -5795,8 +5973,8 @@ var require_redact = __commonJS({
|
|
|
5795
5973
|
return null;
|
|
5796
5974
|
}
|
|
5797
5975
|
const pathStructure = /* @__PURE__ */ new Map();
|
|
5798
|
-
for (const
|
|
5799
|
-
const parts = parsePath(
|
|
5976
|
+
for (const path34 of pathsToClone) {
|
|
5977
|
+
const parts = parsePath(path34);
|
|
5800
5978
|
let current = pathStructure;
|
|
5801
5979
|
for (let i = 0; i < parts.length; i++) {
|
|
5802
5980
|
const part = parts[i];
|
|
@@ -5848,24 +6026,24 @@ var require_redact = __commonJS({
|
|
|
5848
6026
|
}
|
|
5849
6027
|
return cloneSelectively(obj, pathStructure);
|
|
5850
6028
|
}
|
|
5851
|
-
function validatePath(
|
|
5852
|
-
if (typeof
|
|
6029
|
+
function validatePath(path34) {
|
|
6030
|
+
if (typeof path34 !== "string") {
|
|
5853
6031
|
throw new Error("Paths must be (non-empty) strings");
|
|
5854
6032
|
}
|
|
5855
|
-
if (
|
|
6033
|
+
if (path34 === "") {
|
|
5856
6034
|
throw new Error("Invalid redaction path ()");
|
|
5857
6035
|
}
|
|
5858
|
-
if (
|
|
5859
|
-
throw new Error(`Invalid redaction path (${
|
|
6036
|
+
if (path34.includes("..")) {
|
|
6037
|
+
throw new Error(`Invalid redaction path (${path34})`);
|
|
5860
6038
|
}
|
|
5861
|
-
if (
|
|
5862
|
-
throw new Error(`Invalid redaction path (${
|
|
6039
|
+
if (path34.includes(",")) {
|
|
6040
|
+
throw new Error(`Invalid redaction path (${path34})`);
|
|
5863
6041
|
}
|
|
5864
6042
|
let bracketCount = 0;
|
|
5865
6043
|
let inQuotes = false;
|
|
5866
6044
|
let quoteChar = "";
|
|
5867
|
-
for (let i = 0; i <
|
|
5868
|
-
const char =
|
|
6045
|
+
for (let i = 0; i < path34.length; i++) {
|
|
6046
|
+
const char = path34[i];
|
|
5869
6047
|
if ((char === '"' || char === "'") && bracketCount > 0) {
|
|
5870
6048
|
if (!inQuotes) {
|
|
5871
6049
|
inQuotes = true;
|
|
@@ -5879,20 +6057,20 @@ var require_redact = __commonJS({
|
|
|
5879
6057
|
} else if (char === "]" && !inQuotes) {
|
|
5880
6058
|
bracketCount--;
|
|
5881
6059
|
if (bracketCount < 0) {
|
|
5882
|
-
throw new Error(`Invalid redaction path (${
|
|
6060
|
+
throw new Error(`Invalid redaction path (${path34})`);
|
|
5883
6061
|
}
|
|
5884
6062
|
}
|
|
5885
6063
|
}
|
|
5886
6064
|
if (bracketCount !== 0) {
|
|
5887
|
-
throw new Error(`Invalid redaction path (${
|
|
6065
|
+
throw new Error(`Invalid redaction path (${path34})`);
|
|
5888
6066
|
}
|
|
5889
6067
|
}
|
|
5890
6068
|
function validatePaths(paths) {
|
|
5891
6069
|
if (!Array.isArray(paths)) {
|
|
5892
6070
|
throw new TypeError("paths must be an array");
|
|
5893
6071
|
}
|
|
5894
|
-
for (const
|
|
5895
|
-
validatePath(
|
|
6072
|
+
for (const path34 of paths) {
|
|
6073
|
+
validatePath(path34);
|
|
5896
6074
|
}
|
|
5897
6075
|
}
|
|
5898
6076
|
function slowRedact(options = {}) {
|
|
@@ -6060,8 +6238,8 @@ var require_redaction = __commonJS({
|
|
|
6060
6238
|
if (shape[k2] === null) {
|
|
6061
6239
|
o[k2] = (value) => topCensor(value, [k2]);
|
|
6062
6240
|
} else {
|
|
6063
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
6064
|
-
return censor(value, [k2, ...
|
|
6241
|
+
const wrappedCensor = typeof censor === "function" ? (value, path34) => {
|
|
6242
|
+
return censor(value, [k2, ...path34]);
|
|
6065
6243
|
} : censor;
|
|
6066
6244
|
o[k2] = Redact({
|
|
6067
6245
|
paths: shape[k2],
|
|
@@ -6279,10 +6457,10 @@ var require_atomic_sleep = __commonJS({
|
|
|
6279
6457
|
var require_sonic_boom = __commonJS({
|
|
6280
6458
|
"../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
|
|
6281
6459
|
"use strict";
|
|
6282
|
-
var
|
|
6460
|
+
var fs31 = require("fs");
|
|
6283
6461
|
var EventEmitter2 = require("events");
|
|
6284
6462
|
var inherits = require("util").inherits;
|
|
6285
|
-
var
|
|
6463
|
+
var path34 = require("path");
|
|
6286
6464
|
var sleep = require_atomic_sleep();
|
|
6287
6465
|
var assert = require("assert");
|
|
6288
6466
|
var BUSY_WRITE_TIMEOUT = 100;
|
|
@@ -6336,20 +6514,20 @@ var require_sonic_boom = __commonJS({
|
|
|
6336
6514
|
const mode = sonic.mode;
|
|
6337
6515
|
if (sonic.sync) {
|
|
6338
6516
|
try {
|
|
6339
|
-
if (sonic.mkdir)
|
|
6340
|
-
const fd =
|
|
6517
|
+
if (sonic.mkdir) fs31.mkdirSync(path34.dirname(file), { recursive: true });
|
|
6518
|
+
const fd = fs31.openSync(file, flags, mode);
|
|
6341
6519
|
fileOpened(null, fd);
|
|
6342
6520
|
} catch (err) {
|
|
6343
6521
|
fileOpened(err);
|
|
6344
6522
|
throw err;
|
|
6345
6523
|
}
|
|
6346
6524
|
} else if (sonic.mkdir) {
|
|
6347
|
-
|
|
6525
|
+
fs31.mkdir(path34.dirname(file), { recursive: true }, (err) => {
|
|
6348
6526
|
if (err) return fileOpened(err);
|
|
6349
|
-
|
|
6527
|
+
fs31.open(file, flags, mode, fileOpened);
|
|
6350
6528
|
});
|
|
6351
6529
|
} else {
|
|
6352
|
-
|
|
6530
|
+
fs31.open(file, flags, mode, fileOpened);
|
|
6353
6531
|
}
|
|
6354
6532
|
}
|
|
6355
6533
|
function SonicBoom(opts) {
|
|
@@ -6390,8 +6568,8 @@ var require_sonic_boom = __commonJS({
|
|
|
6390
6568
|
this.flush = flushBuffer;
|
|
6391
6569
|
this.flushSync = flushBufferSync;
|
|
6392
6570
|
this._actualWrite = actualWriteBuffer;
|
|
6393
|
-
fsWriteSync = () =>
|
|
6394
|
-
fsWrite = () =>
|
|
6571
|
+
fsWriteSync = () => fs31.writeSync(this.fd, this._writingBuf);
|
|
6572
|
+
fsWrite = () => fs31.write(this.fd, this._writingBuf, this.release);
|
|
6395
6573
|
} else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
|
|
6396
6574
|
this._writingBuf = "";
|
|
6397
6575
|
this.write = write;
|
|
@@ -6400,15 +6578,15 @@ var require_sonic_boom = __commonJS({
|
|
|
6400
6578
|
this._actualWrite = actualWrite;
|
|
6401
6579
|
fsWriteSync = () => {
|
|
6402
6580
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
6403
|
-
return
|
|
6581
|
+
return fs31.writeSync(this.fd, this._writingBuf);
|
|
6404
6582
|
}
|
|
6405
|
-
return
|
|
6583
|
+
return fs31.writeSync(this.fd, this._writingBuf, "utf8");
|
|
6406
6584
|
};
|
|
6407
6585
|
fsWrite = () => {
|
|
6408
6586
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
6409
|
-
return
|
|
6587
|
+
return fs31.write(this.fd, this._writingBuf, this.release);
|
|
6410
6588
|
}
|
|
6411
|
-
return
|
|
6589
|
+
return fs31.write(this.fd, this._writingBuf, "utf8", this.release);
|
|
6412
6590
|
};
|
|
6413
6591
|
} else {
|
|
6414
6592
|
throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
|
|
@@ -6465,7 +6643,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6465
6643
|
}
|
|
6466
6644
|
}
|
|
6467
6645
|
if (this._fsync) {
|
|
6468
|
-
|
|
6646
|
+
fs31.fsyncSync(this.fd);
|
|
6469
6647
|
}
|
|
6470
6648
|
const len = this._len;
|
|
6471
6649
|
if (this._reopening) {
|
|
@@ -6579,7 +6757,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6579
6757
|
const onDrain = () => {
|
|
6580
6758
|
if (!this._fsync) {
|
|
6581
6759
|
try {
|
|
6582
|
-
|
|
6760
|
+
fs31.fsync(this.fd, (err) => {
|
|
6583
6761
|
this._flushPending = false;
|
|
6584
6762
|
cb(err);
|
|
6585
6763
|
});
|
|
@@ -6681,7 +6859,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6681
6859
|
const fd = this.fd;
|
|
6682
6860
|
this.once("ready", () => {
|
|
6683
6861
|
if (fd !== this.fd) {
|
|
6684
|
-
|
|
6862
|
+
fs31.close(fd, (err) => {
|
|
6685
6863
|
if (err) {
|
|
6686
6864
|
return this.emit("error", err);
|
|
6687
6865
|
}
|
|
@@ -6730,7 +6908,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6730
6908
|
buf = this._bufs[0];
|
|
6731
6909
|
}
|
|
6732
6910
|
try {
|
|
6733
|
-
const n = Buffer.isBuffer(buf) ?
|
|
6911
|
+
const n = Buffer.isBuffer(buf) ? fs31.writeSync(this.fd, buf) : fs31.writeSync(this.fd, buf, "utf8");
|
|
6734
6912
|
const releasedBufObj = releaseWritingBuf(buf, this._len, n);
|
|
6735
6913
|
buf = releasedBufObj.writingBuf;
|
|
6736
6914
|
this._len = releasedBufObj.len;
|
|
@@ -6746,7 +6924,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6746
6924
|
}
|
|
6747
6925
|
}
|
|
6748
6926
|
try {
|
|
6749
|
-
|
|
6927
|
+
fs31.fsyncSync(this.fd);
|
|
6750
6928
|
} catch {
|
|
6751
6929
|
}
|
|
6752
6930
|
}
|
|
@@ -6767,7 +6945,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6767
6945
|
buf = mergeBuf(this._bufs[0], this._lens[0]);
|
|
6768
6946
|
}
|
|
6769
6947
|
try {
|
|
6770
|
-
const n =
|
|
6948
|
+
const n = fs31.writeSync(this.fd, buf);
|
|
6771
6949
|
buf = buf.subarray(n);
|
|
6772
6950
|
this._len = Math.max(this._len - n, 0);
|
|
6773
6951
|
if (buf.length <= 0) {
|
|
@@ -6795,13 +6973,13 @@ var require_sonic_boom = __commonJS({
|
|
|
6795
6973
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
|
|
6796
6974
|
if (this.sync) {
|
|
6797
6975
|
try {
|
|
6798
|
-
const written = Buffer.isBuffer(this._writingBuf) ?
|
|
6976
|
+
const written = Buffer.isBuffer(this._writingBuf) ? fs31.writeSync(this.fd, this._writingBuf) : fs31.writeSync(this.fd, this._writingBuf, "utf8");
|
|
6799
6977
|
release(null, written);
|
|
6800
6978
|
} catch (err) {
|
|
6801
6979
|
release(err);
|
|
6802
6980
|
}
|
|
6803
6981
|
} else {
|
|
6804
|
-
|
|
6982
|
+
fs31.write(this.fd, this._writingBuf, release);
|
|
6805
6983
|
}
|
|
6806
6984
|
}
|
|
6807
6985
|
function actualWriteBuffer() {
|
|
@@ -6810,7 +6988,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6810
6988
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
|
|
6811
6989
|
if (this.sync) {
|
|
6812
6990
|
try {
|
|
6813
|
-
const written =
|
|
6991
|
+
const written = fs31.writeSync(this.fd, this._writingBuf);
|
|
6814
6992
|
release(null, written);
|
|
6815
6993
|
} catch (err) {
|
|
6816
6994
|
release(err);
|
|
@@ -6819,7 +6997,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6819
6997
|
if (kCopyBuffer) {
|
|
6820
6998
|
this._writingBuf = Buffer.from(this._writingBuf);
|
|
6821
6999
|
}
|
|
6822
|
-
|
|
7000
|
+
fs31.write(this.fd, this._writingBuf, release);
|
|
6823
7001
|
}
|
|
6824
7002
|
}
|
|
6825
7003
|
function actualClose(sonic) {
|
|
@@ -6835,12 +7013,12 @@ var require_sonic_boom = __commonJS({
|
|
|
6835
7013
|
sonic._lens = [];
|
|
6836
7014
|
assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
|
|
6837
7015
|
try {
|
|
6838
|
-
|
|
7016
|
+
fs31.fsync(sonic.fd, closeWrapped);
|
|
6839
7017
|
} catch {
|
|
6840
7018
|
}
|
|
6841
7019
|
function closeWrapped() {
|
|
6842
7020
|
if (sonic.fd !== 1 && sonic.fd !== 2) {
|
|
6843
|
-
|
|
7021
|
+
fs31.close(sonic.fd, done);
|
|
6844
7022
|
} else {
|
|
6845
7023
|
done();
|
|
6846
7024
|
}
|
|
@@ -9975,11 +10153,11 @@ var init_lib = __esm({
|
|
|
9975
10153
|
}
|
|
9976
10154
|
}
|
|
9977
10155
|
},
|
|
9978
|
-
addToPath: function addToPath(
|
|
9979
|
-
var last =
|
|
10156
|
+
addToPath: function addToPath(path34, added, removed, oldPosInc, options) {
|
|
10157
|
+
var last = path34.lastComponent;
|
|
9980
10158
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
9981
10159
|
return {
|
|
9982
|
-
oldPos:
|
|
10160
|
+
oldPos: path34.oldPos + oldPosInc,
|
|
9983
10161
|
lastComponent: {
|
|
9984
10162
|
count: last.count + 1,
|
|
9985
10163
|
added,
|
|
@@ -9989,7 +10167,7 @@ var init_lib = __esm({
|
|
|
9989
10167
|
};
|
|
9990
10168
|
} else {
|
|
9991
10169
|
return {
|
|
9992
|
-
oldPos:
|
|
10170
|
+
oldPos: path34.oldPos + oldPosInc,
|
|
9993
10171
|
lastComponent: {
|
|
9994
10172
|
count: 1,
|
|
9995
10173
|
added,
|
|
@@ -10420,10 +10598,10 @@ function attachmentToHistoryMessage(o, ts) {
|
|
|
10420
10598
|
const memories = raw.map((m2) => {
|
|
10421
10599
|
if (!m2 || typeof m2 !== "object") return null;
|
|
10422
10600
|
const rec = m2;
|
|
10423
|
-
const
|
|
10601
|
+
const path34 = typeof rec.path === "string" ? rec.path : null;
|
|
10424
10602
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
10425
|
-
if (!
|
|
10426
|
-
const entry = { path:
|
|
10603
|
+
if (!path34 || content == null) return null;
|
|
10604
|
+
const entry = { path: path34, content };
|
|
10427
10605
|
if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
|
|
10428
10606
|
return entry;
|
|
10429
10607
|
}).filter((m2) => m2 !== null);
|
|
@@ -11227,10 +11405,10 @@ function parseAttachment(obj) {
|
|
|
11227
11405
|
const memories = raw.map((m2) => {
|
|
11228
11406
|
if (!m2 || typeof m2 !== "object") return null;
|
|
11229
11407
|
const rec = m2;
|
|
11230
|
-
const
|
|
11408
|
+
const path34 = typeof rec.path === "string" ? rec.path : null;
|
|
11231
11409
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
11232
|
-
if (!
|
|
11233
|
-
const out = { path:
|
|
11410
|
+
if (!path34 || content == null) return null;
|
|
11411
|
+
const out = { path: path34, content };
|
|
11234
11412
|
if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
|
|
11235
11413
|
return out;
|
|
11236
11414
|
}).filter((m2) => m2 !== null);
|
|
@@ -18725,7 +18903,7 @@ var require_websocket = __commonJS({
|
|
|
18725
18903
|
"use strict";
|
|
18726
18904
|
var EventEmitter2 = require("events");
|
|
18727
18905
|
var https = require("https");
|
|
18728
|
-
var
|
|
18906
|
+
var http2 = require("http");
|
|
18729
18907
|
var net = require("net");
|
|
18730
18908
|
var tls = require("tls");
|
|
18731
18909
|
var { randomBytes, createHash } = require("crypto");
|
|
@@ -19259,7 +19437,7 @@ var require_websocket = __commonJS({
|
|
|
19259
19437
|
}
|
|
19260
19438
|
const defaultPort = isSecure ? 443 : 80;
|
|
19261
19439
|
const key = randomBytes(16).toString("base64");
|
|
19262
|
-
const request = isSecure ? https.request :
|
|
19440
|
+
const request = isSecure ? https.request : http2.request;
|
|
19263
19441
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
19264
19442
|
let perMessageDeflate;
|
|
19265
19443
|
opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
|
|
@@ -19753,7 +19931,7 @@ var require_websocket_server = __commonJS({
|
|
|
19753
19931
|
"../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
|
|
19754
19932
|
"use strict";
|
|
19755
19933
|
var EventEmitter2 = require("events");
|
|
19756
|
-
var
|
|
19934
|
+
var http2 = require("http");
|
|
19757
19935
|
var { Duplex } = require("stream");
|
|
19758
19936
|
var { createHash } = require("crypto");
|
|
19759
19937
|
var extension2 = require_extension();
|
|
@@ -19828,8 +20006,8 @@ var require_websocket_server = __commonJS({
|
|
|
19828
20006
|
);
|
|
19829
20007
|
}
|
|
19830
20008
|
if (options.port != null) {
|
|
19831
|
-
this._server =
|
|
19832
|
-
const body =
|
|
20009
|
+
this._server = http2.createServer((req, res) => {
|
|
20010
|
+
const body = http2.STATUS_CODES[426];
|
|
19833
20011
|
res.writeHead(426, {
|
|
19834
20012
|
"Content-Length": body.length,
|
|
19835
20013
|
"Content-Type": "text/plain"
|
|
@@ -20116,7 +20294,7 @@ var require_websocket_server = __commonJS({
|
|
|
20116
20294
|
this.destroy();
|
|
20117
20295
|
}
|
|
20118
20296
|
function abortHandshake(socket, code, message, headers) {
|
|
20119
|
-
message = message ||
|
|
20297
|
+
message = message || http2.STATUS_CODES[code];
|
|
20120
20298
|
headers = {
|
|
20121
20299
|
Connection: "close",
|
|
20122
20300
|
"Content-Type": "text/html",
|
|
@@ -20125,7 +20303,7 @@ var require_websocket_server = __commonJS({
|
|
|
20125
20303
|
};
|
|
20126
20304
|
socket.once("finish", socket.destroy);
|
|
20127
20305
|
socket.end(
|
|
20128
|
-
`HTTP/1.1 ${code} ${
|
|
20306
|
+
`HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
|
|
20129
20307
|
` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
|
|
20130
20308
|
);
|
|
20131
20309
|
}
|
|
@@ -20144,7 +20322,7 @@ var require_websocket_server = __commonJS({
|
|
|
20144
20322
|
// src/run-case/recorder.ts
|
|
20145
20323
|
function startRunCaseRecorder(opts) {
|
|
20146
20324
|
const now = opts.now ?? Date.now;
|
|
20147
|
-
const dir =
|
|
20325
|
+
const dir = import_node_path30.default.dirname(opts.recordPath);
|
|
20148
20326
|
let stream = null;
|
|
20149
20327
|
let closing = false;
|
|
20150
20328
|
let closedSettled = false;
|
|
@@ -20158,8 +20336,8 @@ function startRunCaseRecorder(opts) {
|
|
|
20158
20336
|
});
|
|
20159
20337
|
const ensureStream = () => {
|
|
20160
20338
|
if (stream) return stream;
|
|
20161
|
-
|
|
20162
|
-
stream =
|
|
20339
|
+
import_node_fs28.default.mkdirSync(dir, { recursive: true });
|
|
20340
|
+
stream = import_node_fs28.default.createWriteStream(opts.recordPath, { flags: "a" });
|
|
20163
20341
|
stream.on("close", () => closedResolve());
|
|
20164
20342
|
return stream;
|
|
20165
20343
|
};
|
|
@@ -20184,12 +20362,12 @@ function startRunCaseRecorder(opts) {
|
|
|
20184
20362
|
};
|
|
20185
20363
|
return { tap, close, closed };
|
|
20186
20364
|
}
|
|
20187
|
-
var
|
|
20365
|
+
var import_node_fs28, import_node_path30;
|
|
20188
20366
|
var init_recorder = __esm({
|
|
20189
20367
|
"src/run-case/recorder.ts"() {
|
|
20190
20368
|
"use strict";
|
|
20191
|
-
|
|
20192
|
-
|
|
20369
|
+
import_node_fs28 = __toESM(require("fs"), 1);
|
|
20370
|
+
import_node_path30 = __toESM(require("path"), 1);
|
|
20193
20371
|
}
|
|
20194
20372
|
});
|
|
20195
20373
|
|
|
@@ -20232,7 +20410,7 @@ var init_wire = __esm({
|
|
|
20232
20410
|
// src/run-case/controller.ts
|
|
20233
20411
|
async function runController(opts) {
|
|
20234
20412
|
const now = opts.now ?? Date.now;
|
|
20235
|
-
const cwd = opts.cwd ?? (0,
|
|
20413
|
+
const cwd = opts.cwd ?? (0, import_node_fs29.mkdtempSync)(import_node_path31.default.join(import_node_os15.default.tmpdir(), "clawd-runcase-"));
|
|
20236
20414
|
const ownsCwd = opts.cwd === void 0;
|
|
20237
20415
|
const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
|
|
20238
20416
|
const spawnCtx = { cwd };
|
|
@@ -20393,19 +20571,19 @@ async function runController(opts) {
|
|
|
20393
20571
|
if (sigintHandler) process.off("SIGINT", sigintHandler);
|
|
20394
20572
|
if (ownsCwd) {
|
|
20395
20573
|
try {
|
|
20396
|
-
(0,
|
|
20574
|
+
(0, import_node_fs29.rmSync)(cwd, { recursive: true, force: true });
|
|
20397
20575
|
} catch {
|
|
20398
20576
|
}
|
|
20399
20577
|
}
|
|
20400
20578
|
return exitCode ?? 0;
|
|
20401
20579
|
}
|
|
20402
|
-
var
|
|
20580
|
+
var import_node_fs29, import_node_os15, import_node_path31;
|
|
20403
20581
|
var init_controller = __esm({
|
|
20404
20582
|
"src/run-case/controller.ts"() {
|
|
20405
20583
|
"use strict";
|
|
20406
|
-
|
|
20584
|
+
import_node_fs29 = require("fs");
|
|
20407
20585
|
import_node_os15 = __toESM(require("os"), 1);
|
|
20408
|
-
|
|
20586
|
+
import_node_path31 = __toESM(require("path"), 1);
|
|
20409
20587
|
init_claude();
|
|
20410
20588
|
init_stdout_splitter();
|
|
20411
20589
|
init_permission_stdio();
|
|
@@ -20637,8 +20815,8 @@ Env (advanced):
|
|
|
20637
20815
|
`;
|
|
20638
20816
|
|
|
20639
20817
|
// src/index.ts
|
|
20640
|
-
var
|
|
20641
|
-
var
|
|
20818
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
20819
|
+
var import_node_fs27 = __toESM(require("fs"), 1);
|
|
20642
20820
|
|
|
20643
20821
|
// src/logger.ts
|
|
20644
20822
|
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
@@ -21630,6 +21808,11 @@ var SessionRunner = class {
|
|
|
21630
21808
|
// sub-session idle-kill timer ref;key = sessionId(实际同一 runner 只对应一个 session,
|
|
21631
21809
|
// 但 reducer Effect schema 带 sessionId 作为唯一键,对齐 schedule/cancel 配对)
|
|
21632
21810
|
idleKillTimers = /* @__PURE__ */ new Map();
|
|
21811
|
+
// file-sharing 待配对的 file-edit tool_use(spec §6 PR 3):在同一 session 流里
|
|
21812
|
+
// tool_use(kind='tool_call')与 tool_result 通过 toolUseId 配对。tool_use 提前到,
|
|
21813
|
+
// 等 tool_result 来时反查 path 调 onFileEdit。条目 in-flight 期间很少(个位数),
|
|
21814
|
+
// 不需要 LRU;session 退出时整个 runner 销毁自然清理。
|
|
21815
|
+
pendingFileEdits = /* @__PURE__ */ new Map();
|
|
21633
21816
|
getState() {
|
|
21634
21817
|
return this.state;
|
|
21635
21818
|
}
|
|
@@ -21638,7 +21821,13 @@ var SessionRunner = class {
|
|
|
21638
21821
|
const personaStore = this.hooks.personaStore;
|
|
21639
21822
|
const adapter = this.hooks.adapter;
|
|
21640
21823
|
const deps = {
|
|
21641
|
-
|
|
21824
|
+
// file-sharing (spec §6 PR 3):在 stdout-line 解析时同步观察 file-edit 工具事件,
|
|
21825
|
+
// 配对 tool_use ↔ tool_result 调 onFileEdit。原 reducer 路径不变。
|
|
21826
|
+
parseLine: (l) => {
|
|
21827
|
+
const events = adapter.parseLine(l);
|
|
21828
|
+
if (this.hooks.onFileEdit) this.observeForFileEdit(events);
|
|
21829
|
+
return events;
|
|
21830
|
+
},
|
|
21642
21831
|
// persona mention injection 在 deps 装配处包一层,对 reducer 完全透明。
|
|
21643
21832
|
// 命中 mention 时拆成两条 stdin 帧(先 system-reminder 块、后 user 原文):
|
|
21644
21833
|
// - CC stream-json 按行处理,落盘是两条独立 user message;
|
|
@@ -21706,8 +21895,54 @@ var SessionRunner = class {
|
|
|
21706
21895
|
// reducer 的 batch dedup 按 events[0].uuid 整组处理,避免跨路径重复。
|
|
21707
21896
|
feedObserverEvents(events) {
|
|
21708
21897
|
if (events.length === 0) return;
|
|
21898
|
+
if (this.hooks.onFileEdit) this.observeForFileEdit(events);
|
|
21709
21899
|
this.input({ kind: "inject-events", events });
|
|
21710
21900
|
}
|
|
21901
|
+
/**
|
|
21902
|
+
* file-sharing tool_use ↔ tool_result 配对(spec §6 PR 3)。
|
|
21903
|
+
*
|
|
21904
|
+
* 1) tool_call kind + tool ∈ FILE_EDIT_TOOLS → cache toolUseId → { tool, relPath }
|
|
21905
|
+
* relPath 从 input 里取(Write/Edit/MultiEdit 是 file_path,NotebookEdit 是 notebook_path)。
|
|
21906
|
+
* 2) tool_result kind + cache 命中 + 非 error → 调 onFileEdit + 删 cache
|
|
21907
|
+
* 3) tool_result kind + cache 命中 + 是 error → 删 cache,不调(spec 只要"成功"才入清单)
|
|
21908
|
+
* 4) tool_call 但 input 没有 path → 不 cache(防御)
|
|
21909
|
+
*
|
|
21910
|
+
* 不破坏 reducer 路径——纯只读扫描 events 数组。
|
|
21911
|
+
*/
|
|
21912
|
+
observeForFileEdit(events) {
|
|
21913
|
+
const cb = this.hooks.onFileEdit;
|
|
21914
|
+
if (!cb) return;
|
|
21915
|
+
for (const ev of events) {
|
|
21916
|
+
if (ev.kind === "tool_call") {
|
|
21917
|
+
const tool = ev.tool;
|
|
21918
|
+
if (!isFileEditTool(tool)) continue;
|
|
21919
|
+
const relPath = extractEditPath(ev.input);
|
|
21920
|
+
if (!relPath) continue;
|
|
21921
|
+
this.pendingFileEdits.set(ev.toolUseId, {
|
|
21922
|
+
tool,
|
|
21923
|
+
relPath
|
|
21924
|
+
});
|
|
21925
|
+
} else if (ev.kind === "tool_result") {
|
|
21926
|
+
const pending = this.pendingFileEdits.get(ev.toolUseId);
|
|
21927
|
+
if (!pending) continue;
|
|
21928
|
+
this.pendingFileEdits.delete(ev.toolUseId);
|
|
21929
|
+
if (ev.error != null) continue;
|
|
21930
|
+
try {
|
|
21931
|
+
cb({
|
|
21932
|
+
toolUseId: ev.toolUseId,
|
|
21933
|
+
tool: pending.tool,
|
|
21934
|
+
relPath: pending.relPath,
|
|
21935
|
+
cwd: this.state.file.cwd
|
|
21936
|
+
});
|
|
21937
|
+
} catch (err) {
|
|
21938
|
+
this.hooks.logger?.warn("onFileEdit hook threw", {
|
|
21939
|
+
err: err.message,
|
|
21940
|
+
sessionId: this.state.file.sessionId
|
|
21941
|
+
});
|
|
21942
|
+
}
|
|
21943
|
+
}
|
|
21944
|
+
}
|
|
21945
|
+
}
|
|
21711
21946
|
// 向子进程 stdin 写一条 CC IPC `control_request` 并返回 Promise<response>
|
|
21712
21947
|
// —— CC 侧会异步回写匹配 request_id 的 `control_response` 帧,
|
|
21713
21948
|
// 在 stdout 行处理入口被 tryHandleControlResponse 拦截并 resolve pending
|
|
@@ -21897,6 +22132,15 @@ var SessionRunner = class {
|
|
|
21897
22132
|
}
|
|
21898
22133
|
}
|
|
21899
22134
|
};
|
|
22135
|
+
function isFileEditTool(name) {
|
|
22136
|
+
return name === "Write" || name === "Edit" || name === "MultiEdit" || name === "NotebookEdit";
|
|
22137
|
+
}
|
|
22138
|
+
function extractEditPath(input) {
|
|
22139
|
+
if (!input || typeof input !== "object") return null;
|
|
22140
|
+
const o = input;
|
|
22141
|
+
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;
|
|
22142
|
+
return candidate || null;
|
|
22143
|
+
}
|
|
21900
22144
|
|
|
21901
22145
|
// src/session/manager.ts
|
|
21902
22146
|
function compressFrameForWire(frame) {
|
|
@@ -22111,6 +22355,7 @@ var SessionManager = class {
|
|
|
22111
22355
|
const adapter = this.deps.getAdapter(file.tool ?? "claude");
|
|
22112
22356
|
const store = this.storeFor(scope);
|
|
22113
22357
|
const subSessionMeta = metaFromScope(scope, this.deps.personaRoot ?? "");
|
|
22358
|
+
const attachmentGroup = this.deps.attachmentGroup;
|
|
22114
22359
|
const runner = new SessionRunner(makeInitialState(file, subSessionMeta), {
|
|
22115
22360
|
broadcastFrame: (frame, target) => this.routeFromRunner(frame, target),
|
|
22116
22361
|
store,
|
|
@@ -22123,7 +22368,15 @@ var SessionManager = class {
|
|
|
22123
22368
|
resolveContextWindow: (tool, modelId) => this.deps.getAdapter(tool).resolveContextWindow(modelId),
|
|
22124
22369
|
dataDir: this.deps.dataDir,
|
|
22125
22370
|
personaStore: this.deps.personaStore,
|
|
22126
|
-
ownerDisplayName: this.deps.ownerDisplayName
|
|
22371
|
+
ownerDisplayName: this.deps.ownerDisplayName,
|
|
22372
|
+
// file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
|
|
22373
|
+
onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
|
|
22374
|
+
scope,
|
|
22375
|
+
sessionId: file.sessionId,
|
|
22376
|
+
tool: input.tool,
|
|
22377
|
+
relPath: input.relPath,
|
|
22378
|
+
cwd: input.cwd
|
|
22379
|
+
}) : void 0
|
|
22127
22380
|
});
|
|
22128
22381
|
if (this.deps.mode === "tui" && !file.toolSessionId) {
|
|
22129
22382
|
const newTsid = v4_default();
|
|
@@ -22211,15 +22464,14 @@ var SessionManager = class {
|
|
|
22211
22464
|
}
|
|
22212
22465
|
// ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
|
|
22213
22466
|
create(args) {
|
|
22214
|
-
let cwd;
|
|
22215
|
-
if (args.ownerPersonaId) {
|
|
22467
|
+
let cwd = args.cwd;
|
|
22468
|
+
if (args.ownerPersonaId && !cwd) {
|
|
22216
22469
|
if (!this.deps.personaRoot) {
|
|
22217
22470
|
throw new Error("personaRoot required to derive cwd from ownerPersonaId");
|
|
22218
22471
|
}
|
|
22219
22472
|
cwd = import_node_path6.default.join(this.deps.personaRoot, safeFileName(args.ownerPersonaId));
|
|
22220
|
-
}
|
|
22221
|
-
|
|
22222
|
-
} else {
|
|
22473
|
+
}
|
|
22474
|
+
if (!cwd) {
|
|
22223
22475
|
throw new ClawdError(ERROR_CODES.INVALID_CWD, "cwd required when ownerPersonaId is absent");
|
|
22224
22476
|
}
|
|
22225
22477
|
try {
|
|
@@ -23275,6 +23527,21 @@ var PersonaRegistry = class {
|
|
|
23275
23527
|
if (entry.revoked) return { ok: false, code: "TOKEN_REVOKED" };
|
|
23276
23528
|
return { ok: true, label: entry.label };
|
|
23277
23529
|
}
|
|
23530
|
+
/**
|
|
23531
|
+
* file-sharing HTTP auth-context 用的逆向查表:只有 Bearer token,没有 personaId。
|
|
23532
|
+
* 线性扫所有 persona.tokenMap 找到第一条 ok 命中的;revoked / non-public persona 跳过。
|
|
23533
|
+
* persona 数量通常 < 100,性能可忽略。
|
|
23534
|
+
*/
|
|
23535
|
+
findByToken(token) {
|
|
23536
|
+
if (!token) return null;
|
|
23537
|
+
for (const persona of this.cache.values()) {
|
|
23538
|
+
if (!persona.public) continue;
|
|
23539
|
+
const entry = persona.tokenMap[token];
|
|
23540
|
+
if (!entry || entry.revoked) continue;
|
|
23541
|
+
return { personaId: persona.personaId, label: entry.label };
|
|
23542
|
+
}
|
|
23543
|
+
return null;
|
|
23544
|
+
}
|
|
23278
23545
|
};
|
|
23279
23546
|
|
|
23280
23547
|
// src/persona/manager.ts
|
|
@@ -25216,6 +25483,7 @@ var import_websocket = __toESM(require_websocket(), 1);
|
|
|
25216
25483
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
25217
25484
|
|
|
25218
25485
|
// src/transport/local-ws-server.ts
|
|
25486
|
+
var import_node_http = __toESM(require("http"), 1);
|
|
25219
25487
|
var PERSONA_PATH_RE = /^\/personas\/([a-zA-Z0-9._-]+)$/;
|
|
25220
25488
|
var LocalWsServer = class {
|
|
25221
25489
|
constructor(opts) {
|
|
@@ -25225,6 +25493,7 @@ var LocalWsServer = class {
|
|
|
25225
25493
|
}
|
|
25226
25494
|
opts;
|
|
25227
25495
|
wss = null;
|
|
25496
|
+
httpServer = null;
|
|
25228
25497
|
frameHandler = null;
|
|
25229
25498
|
clients = /* @__PURE__ */ new Map();
|
|
25230
25499
|
logger;
|
|
@@ -25232,25 +25501,28 @@ var LocalWsServer = class {
|
|
|
25232
25501
|
async start() {
|
|
25233
25502
|
const host = this.opts.host ?? "127.0.0.1";
|
|
25234
25503
|
await new Promise((resolve2, reject) => {
|
|
25235
|
-
const
|
|
25236
|
-
|
|
25237
|
-
|
|
25238
|
-
|
|
25504
|
+
const httpServer = import_node_http.default.createServer((req, res) => this.handleHttpRequest(req, res));
|
|
25505
|
+
const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
|
|
25506
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
25507
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
25508
|
+
this.routeConnection(ws, req);
|
|
25509
|
+
});
|
|
25239
25510
|
});
|
|
25240
|
-
|
|
25511
|
+
httpServer.on("listening", () => {
|
|
25241
25512
|
this.logger?.info("ws listening", { host, port: this.opts.port });
|
|
25242
25513
|
resolve2();
|
|
25243
25514
|
});
|
|
25244
|
-
|
|
25515
|
+
httpServer.on("error", (err) => {
|
|
25245
25516
|
this.logger?.error("ws server error", { err: err.message });
|
|
25246
25517
|
reject(err);
|
|
25247
25518
|
});
|
|
25248
|
-
|
|
25519
|
+
httpServer.listen(this.opts.port, host);
|
|
25520
|
+
this.httpServer = httpServer;
|
|
25249
25521
|
this.wss = wss;
|
|
25250
25522
|
});
|
|
25251
25523
|
}
|
|
25252
25524
|
async stop() {
|
|
25253
|
-
if (!this.
|
|
25525
|
+
if (!this.httpServer) return;
|
|
25254
25526
|
for (const c of this.clients.values()) {
|
|
25255
25527
|
try {
|
|
25256
25528
|
c.ws.close(1001, "shutdown");
|
|
@@ -25262,7 +25534,32 @@ var LocalWsServer = class {
|
|
|
25262
25534
|
await new Promise((resolve2) => {
|
|
25263
25535
|
this.wss?.close(() => resolve2());
|
|
25264
25536
|
});
|
|
25537
|
+
await new Promise((resolve2) => {
|
|
25538
|
+
this.httpServer?.close(() => resolve2());
|
|
25539
|
+
});
|
|
25265
25540
|
this.wss = null;
|
|
25541
|
+
this.httpServer = null;
|
|
25542
|
+
}
|
|
25543
|
+
/** http.createServer 'request' 入口:file-sharing 路由优先,未命中走 404 */
|
|
25544
|
+
async handleHttpRequest(req, res) {
|
|
25545
|
+
const handler = this.opts.httpRequestHandler;
|
|
25546
|
+
if (handler) {
|
|
25547
|
+
try {
|
|
25548
|
+
const handled = await handler(req, res);
|
|
25549
|
+
if (handled) return;
|
|
25550
|
+
} catch (err) {
|
|
25551
|
+
this.logger?.warn("http handler threw", { err: err.message });
|
|
25552
|
+
if (!res.headersSent) {
|
|
25553
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
25554
|
+
res.end(JSON.stringify({ code: "INTERNAL", message: "http handler error" }));
|
|
25555
|
+
}
|
|
25556
|
+
return;
|
|
25557
|
+
}
|
|
25558
|
+
}
|
|
25559
|
+
if (!res.headersSent) {
|
|
25560
|
+
res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
|
|
25561
|
+
res.end(JSON.stringify({ code: "NOT_FOUND", message: "no route" }));
|
|
25562
|
+
}
|
|
25266
25563
|
}
|
|
25267
25564
|
onFrame(handler) {
|
|
25268
25565
|
this.frameHandler = handler;
|
|
@@ -25364,7 +25661,7 @@ var LocalWsServer = class {
|
|
|
25364
25661
|
}
|
|
25365
25662
|
try {
|
|
25366
25663
|
if (authed) {
|
|
25367
|
-
const readyFrame = this.opts.readyFrameBuilder();
|
|
25664
|
+
const readyFrame = this.opts.readyFrameBuilder({ remoteAddress });
|
|
25368
25665
|
this.safeSend(socket, { type: "ready", ...readyFrame });
|
|
25369
25666
|
} else {
|
|
25370
25667
|
this.safeSend(socket, { type: "ready", protocolVersion: this.opts.protocolVersion });
|
|
@@ -25401,7 +25698,7 @@ var LocalWsServer = class {
|
|
|
25401
25698
|
if (verdict !== "pass") {
|
|
25402
25699
|
if (!wasAuthed && authGate.isAuthed(client.id)) {
|
|
25403
25700
|
try {
|
|
25404
|
-
const full = this.opts.readyFrameBuilder();
|
|
25701
|
+
const full = this.opts.readyFrameBuilder({ remoteAddress });
|
|
25405
25702
|
this.safeSend(this.clients.get(client.id).ws, { type: "ready", ...full });
|
|
25406
25703
|
} catch (err) {
|
|
25407
25704
|
this.logger?.warn("post-auth ready frame build failed", { err: err.message });
|
|
@@ -25998,150 +26295,1071 @@ function isLocalhost(addr) {
|
|
|
25998
26295
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
25999
26296
|
}
|
|
26000
26297
|
|
|
26001
|
-
// src/
|
|
26002
|
-
var
|
|
26003
|
-
|
|
26004
|
-
|
|
26005
|
-
return import_node_path14.default.join(dataDir, "state.json");
|
|
26006
|
-
}
|
|
26007
|
-
function isPidAlive(pid) {
|
|
26008
|
-
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
26009
|
-
try {
|
|
26010
|
-
process.kill(pid, 0);
|
|
26011
|
-
return true;
|
|
26012
|
-
} catch (err) {
|
|
26013
|
-
const code = err.code;
|
|
26014
|
-
return code === "EPERM";
|
|
26298
|
+
// src/transport/auth-context.ts
|
|
26299
|
+
var AuthContextResolver = class {
|
|
26300
|
+
constructor(opts) {
|
|
26301
|
+
this.opts = opts;
|
|
26015
26302
|
}
|
|
26303
|
+
opts;
|
|
26304
|
+
/**
|
|
26305
|
+
* 从 `Authorization` 头解析。null = 无凭证 / 不识别 / revoked,调用方一律 401。
|
|
26306
|
+
* remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
|
|
26307
|
+
*/
|
|
26308
|
+
resolveFromHeader(authHeader, remoteAddress) {
|
|
26309
|
+
const token = parseBearer(authHeader);
|
|
26310
|
+
if (!token) return null;
|
|
26311
|
+
const isLoopback = isLoopbackAddr(remoteAddress);
|
|
26312
|
+
if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
|
|
26313
|
+
return { role: "owner", isLoopback };
|
|
26314
|
+
}
|
|
26315
|
+
const personalHit = this.opts.personaRegistry.findByToken(token);
|
|
26316
|
+
if (personalHit) {
|
|
26317
|
+
return {
|
|
26318
|
+
role: "personal",
|
|
26319
|
+
personaId: personalHit.personaId,
|
|
26320
|
+
label: personalHit.label,
|
|
26321
|
+
isLoopback
|
|
26322
|
+
};
|
|
26323
|
+
}
|
|
26324
|
+
return null;
|
|
26325
|
+
}
|
|
26326
|
+
};
|
|
26327
|
+
function parseBearer(header) {
|
|
26328
|
+
if (!header) return null;
|
|
26329
|
+
const raw = Array.isArray(header) ? header[0] : header;
|
|
26330
|
+
if (typeof raw !== "string") return null;
|
|
26331
|
+
const m2 = /^Bearer\s+(\S+)$/i.exec(raw.trim());
|
|
26332
|
+
return m2 ? m2[1] : null;
|
|
26016
26333
|
}
|
|
26017
|
-
|
|
26018
|
-
|
|
26334
|
+
function isLoopbackAddr(addr) {
|
|
26335
|
+
if (!addr) return false;
|
|
26336
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
26337
|
+
}
|
|
26338
|
+
function constantTimeEqual2(a, b2) {
|
|
26339
|
+
if (a.length !== b2.length) return false;
|
|
26340
|
+
let diff2 = 0;
|
|
26341
|
+
for (let i = 0; i < a.length; i++) diff2 |= a.charCodeAt(i) ^ b2.charCodeAt(i);
|
|
26342
|
+
return diff2 === 0;
|
|
26343
|
+
}
|
|
26344
|
+
|
|
26345
|
+
// src/transport/http-router.ts
|
|
26346
|
+
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
26347
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
26348
|
+
|
|
26349
|
+
// src/attachment/group.ts
|
|
26350
|
+
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
26351
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
26352
|
+
var import_node_crypto4 = __toESM(require("crypto"), 1);
|
|
26353
|
+
init_protocol();
|
|
26354
|
+
var GroupFileStore = class {
|
|
26355
|
+
dataDir;
|
|
26356
|
+
logger;
|
|
26357
|
+
cache = /* @__PURE__ */ new Map();
|
|
26019
26358
|
constructor(opts) {
|
|
26020
|
-
this.
|
|
26359
|
+
this.dataDir = opts.dataDir;
|
|
26360
|
+
this.logger = opts.logger;
|
|
26021
26361
|
}
|
|
26022
|
-
|
|
26023
|
-
return this.
|
|
26362
|
+
rootForScope(scope) {
|
|
26363
|
+
return import_node_path14.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
|
|
26024
26364
|
}
|
|
26025
|
-
|
|
26365
|
+
/** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
|
|
26366
|
+
filePath(scope, sessionId) {
|
|
26367
|
+
return import_node_path14.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
|
|
26368
|
+
}
|
|
26369
|
+
cacheKey(scope, sessionId) {
|
|
26370
|
+
return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
|
|
26371
|
+
}
|
|
26372
|
+
/** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
|
|
26373
|
+
readFile(scope, sessionId) {
|
|
26374
|
+
const file = this.filePath(scope, sessionId);
|
|
26026
26375
|
try {
|
|
26027
|
-
const raw = import_node_fs13.default.readFileSync(
|
|
26376
|
+
const raw = import_node_fs13.default.readFileSync(file, "utf8");
|
|
26028
26377
|
const parsed = JSON.parse(raw);
|
|
26029
|
-
|
|
26030
|
-
|
|
26031
|
-
|
|
26378
|
+
if (!Array.isArray(parsed)) {
|
|
26379
|
+
this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
|
|
26380
|
+
file
|
|
26381
|
+
});
|
|
26382
|
+
return [];
|
|
26383
|
+
}
|
|
26384
|
+
const out = [];
|
|
26385
|
+
for (const entry of parsed) {
|
|
26386
|
+
const r = GroupFileEntrySchema.safeParse(entry);
|
|
26387
|
+
if (r.success) out.push(r.data);
|
|
26388
|
+
}
|
|
26389
|
+
return out;
|
|
26390
|
+
} catch (err) {
|
|
26391
|
+
const code = err?.code;
|
|
26392
|
+
if (code === "ENOENT") return [];
|
|
26393
|
+
this.logger?.warn("GroupFileStore.readFile failed", {
|
|
26394
|
+
file,
|
|
26395
|
+
err: err.message
|
|
26396
|
+
});
|
|
26397
|
+
return [];
|
|
26032
26398
|
}
|
|
26033
26399
|
}
|
|
26034
|
-
|
|
26035
|
-
const
|
|
26036
|
-
|
|
26037
|
-
|
|
26038
|
-
|
|
26400
|
+
writeFile(scope, sessionId, entries) {
|
|
26401
|
+
const file = this.filePath(scope, sessionId);
|
|
26402
|
+
import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(file), { recursive: true });
|
|
26403
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
26404
|
+
import_node_fs13.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
|
|
26405
|
+
import_node_fs13.default.renameSync(tmp, file);
|
|
26039
26406
|
}
|
|
26040
|
-
|
|
26041
|
-
|
|
26042
|
-
const
|
|
26043
|
-
|
|
26044
|
-
|
|
26045
|
-
|
|
26046
|
-
|
|
26047
|
-
|
|
26048
|
-
} catch {
|
|
26049
|
-
}
|
|
26050
|
-
}
|
|
26407
|
+
/** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
|
|
26408
|
+
list(scope, sessionId) {
|
|
26409
|
+
const key = this.cacheKey(scope, sessionId);
|
|
26410
|
+
const cached = this.cache.get(key);
|
|
26411
|
+
if (cached) return cached.entries;
|
|
26412
|
+
const entries = this.readFile(scope, sessionId);
|
|
26413
|
+
this.cache.set(key, { entries, scope });
|
|
26414
|
+
return entries;
|
|
26051
26415
|
}
|
|
26052
|
-
|
|
26053
|
-
|
|
26054
|
-
|
|
26055
|
-
|
|
26416
|
+
/**
|
|
26417
|
+
* upsert:
|
|
26418
|
+
* - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
|
|
26419
|
+
* (保留首次入群人 / 入群时刻)
|
|
26420
|
+
* - 不存在 → append,id 派生稳定 uuid,addedAt = now
|
|
26421
|
+
*
|
|
26422
|
+
* 返回最新 entry(caller 可用来 broadcast 通知)。
|
|
26423
|
+
*/
|
|
26424
|
+
upsert(scope, sessionId, input, now = Date.now()) {
|
|
26425
|
+
const entries = this.list(scope, sessionId).slice();
|
|
26426
|
+
const idx = entries.findIndex((e) => e.relPath === input.relPath);
|
|
26427
|
+
let next;
|
|
26428
|
+
if (idx >= 0) {
|
|
26429
|
+
const prev = entries[idx];
|
|
26430
|
+
next = {
|
|
26431
|
+
...prev,
|
|
26432
|
+
size: input.size,
|
|
26433
|
+
mime: input.mime,
|
|
26434
|
+
lastEditedAt: now,
|
|
26435
|
+
stale: false
|
|
26436
|
+
// addedBy / label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
|
|
26437
|
+
};
|
|
26438
|
+
entries[idx] = next;
|
|
26439
|
+
} else {
|
|
26440
|
+
next = {
|
|
26441
|
+
id: `gf-${import_node_crypto4.default.randomBytes(6).toString("base64url")}`,
|
|
26442
|
+
relPath: input.relPath,
|
|
26443
|
+
from: input.from,
|
|
26444
|
+
addedBy: input.addedBy,
|
|
26445
|
+
label: input.label,
|
|
26446
|
+
size: input.size,
|
|
26447
|
+
mime: input.mime,
|
|
26448
|
+
addedAt: now
|
|
26449
|
+
// agent / inbox 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
|
|
26450
|
+
};
|
|
26451
|
+
entries.push(next);
|
|
26056
26452
|
}
|
|
26453
|
+
this.writeFile(scope, sessionId, entries);
|
|
26454
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
|
|
26455
|
+
return next;
|
|
26057
26456
|
}
|
|
26058
|
-
|
|
26059
|
-
|
|
26060
|
-
|
|
26061
|
-
|
|
26062
|
-
|
|
26063
|
-
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
|
|
26067
|
-
|
|
26068
|
-
|
|
26069
|
-
|
|
26070
|
-
|
|
26071
|
-
this.
|
|
26457
|
+
/**
|
|
26458
|
+
* 标记一个 relPath stale(agent rm / mv 后文件不在)。
|
|
26459
|
+
* - 命中 → stale=true,UI 灰显
|
|
26460
|
+
* - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
|
|
26461
|
+
*
|
|
26462
|
+
* 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
|
|
26463
|
+
*/
|
|
26464
|
+
markStale(scope, sessionId, relPath) {
|
|
26465
|
+
const entries = this.list(scope, sessionId).slice();
|
|
26466
|
+
const idx = entries.findIndex((e) => e.relPath === relPath);
|
|
26467
|
+
if (idx < 0) return;
|
|
26468
|
+
if (entries[idx].stale) return;
|
|
26469
|
+
entries[idx] = { ...entries[idx], stale: true };
|
|
26470
|
+
this.writeFile(scope, sessionId, entries);
|
|
26471
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
|
|
26072
26472
|
}
|
|
26073
|
-
|
|
26074
|
-
|
|
26075
|
-
|
|
26076
|
-
|
|
26077
|
-
|
|
26078
|
-
|
|
26079
|
-
|
|
26080
|
-
|
|
26081
|
-
|
|
26082
|
-
|
|
26083
|
-
|
|
26084
|
-
|
|
26473
|
+
/**
|
|
26474
|
+
* 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
|
|
26475
|
+
* agent / inbox 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
|
|
26476
|
+
* owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
|
|
26477
|
+
*
|
|
26478
|
+
* 返回值:true=命中并删除;false=relPath 不在群里。
|
|
26479
|
+
*/
|
|
26480
|
+
remove(scope, sessionId, relPath) {
|
|
26481
|
+
const entries = this.list(scope, sessionId).slice();
|
|
26482
|
+
const idx = entries.findIndex((e) => e.relPath === relPath);
|
|
26483
|
+
if (idx < 0) return false;
|
|
26484
|
+
entries.splice(idx, 1);
|
|
26485
|
+
this.writeFile(scope, sessionId, entries);
|
|
26486
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
|
|
26487
|
+
return true;
|
|
26085
26488
|
}
|
|
26086
|
-
|
|
26087
|
-
|
|
26088
|
-
|
|
26089
|
-
|
|
26090
|
-
|
|
26091
|
-
|
|
26092
|
-
|
|
26489
|
+
/**
|
|
26490
|
+
* 跨 session 聚合查询(spec §4 HTTP ACL:personal 视野并集)。
|
|
26491
|
+
*
|
|
26492
|
+
* 扫 <dataDir>/sessions/<personaId>/owner/*.group-files.json 和
|
|
26493
|
+
* <dataDir>/sessions/<personaId>/listener/*.group-files.json,每文件读一份。
|
|
26494
|
+
*
|
|
26495
|
+
* 复杂度 O(N sessions × N entries),N 通常 < 100,可接受。
|
|
26496
|
+
*/
|
|
26497
|
+
listByPersona(personaId) {
|
|
26498
|
+
const out = [];
|
|
26499
|
+
for (const mode of ["owner", "listener"]) {
|
|
26500
|
+
const scope = { kind: "persona", personaId, mode };
|
|
26501
|
+
const root = this.rootForScope(scope);
|
|
26502
|
+
let names;
|
|
26093
26503
|
try {
|
|
26094
|
-
|
|
26095
|
-
} catch {
|
|
26504
|
+
names = import_node_fs13.default.readdirSync(root);
|
|
26505
|
+
} catch (err) {
|
|
26506
|
+
const code = err?.code;
|
|
26507
|
+
if (code === "ENOENT") continue;
|
|
26508
|
+
continue;
|
|
26509
|
+
}
|
|
26510
|
+
for (const name of names) {
|
|
26511
|
+
if (!name.endsWith(".group-files.json")) continue;
|
|
26512
|
+
const sessionId = name.slice(0, -".group-files.json".length);
|
|
26513
|
+
if (!sessionId) continue;
|
|
26514
|
+
const entries = this.list(scope, sessionId);
|
|
26515
|
+
out.push({ sessionId, entries });
|
|
26096
26516
|
}
|
|
26097
26517
|
}
|
|
26098
|
-
|
|
26518
|
+
return out;
|
|
26099
26519
|
}
|
|
26100
|
-
|
|
26101
|
-
|
|
26102
|
-
|
|
26103
|
-
|
|
26104
|
-
|
|
26105
|
-
|
|
26520
|
+
};
|
|
26521
|
+
function personalViewable(groupStore, personaDir, personaId, absPath) {
|
|
26522
|
+
const realTarget = safeRealpath(absPath);
|
|
26523
|
+
if (!realTarget) {
|
|
26524
|
+
return false;
|
|
26525
|
+
}
|
|
26526
|
+
const personasUnion = groupStore.listByPersona(personaId);
|
|
26527
|
+
for (const { entries } of personasUnion) {
|
|
26528
|
+
for (const e of entries) {
|
|
26529
|
+
if (e.stale) continue;
|
|
26530
|
+
const realEntry = safeRealpath(import_node_path14.default.join(personaDir, e.relPath));
|
|
26531
|
+
if (realEntry && realEntry === realTarget) return true;
|
|
26106
26532
|
}
|
|
26107
26533
|
}
|
|
26108
|
-
|
|
26109
|
-
function isPersistedTunnel(o) {
|
|
26110
|
-
if (!o || typeof o !== "object") return false;
|
|
26111
|
-
const r = o;
|
|
26112
|
-
return typeof r.subdomain === "string" && typeof r.frpsHost === "string" && typeof r.frpsPort === "number" && typeof r.frpsToken === "string" && typeof r.url === "string" && typeof r.registeredAt === "string";
|
|
26534
|
+
return false;
|
|
26113
26535
|
}
|
|
26114
|
-
|
|
26115
|
-
// src/tunnel/register-client.ts
|
|
26116
|
-
var TunnelRegisterError = class extends Error {
|
|
26117
|
-
constructor(message, cause) {
|
|
26118
|
-
super(message);
|
|
26119
|
-
this.cause = cause;
|
|
26120
|
-
this.name = "TunnelRegisterError";
|
|
26121
|
-
}
|
|
26122
|
-
cause;
|
|
26123
|
-
};
|
|
26124
|
-
async function registerTunnel(opts) {
|
|
26125
|
-
const f = opts.fetchImpl ?? globalThis.fetch;
|
|
26126
|
-
if (!f) throw new TunnelRegisterError("fetch is not available (Node >= 18 required)");
|
|
26127
|
-
const url = `${opts.api.replace(/\/+$/, "")}/api/tunnel/register`;
|
|
26128
|
-
const ctrl = new AbortController();
|
|
26129
|
-
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
|
|
26130
|
-
let res;
|
|
26536
|
+
function safeRealpath(p2) {
|
|
26131
26537
|
try {
|
|
26132
|
-
|
|
26133
|
-
|
|
26134
|
-
|
|
26135
|
-
body: JSON.stringify({ gatewayCount: 1, deviceKeys: [opts.deviceKey] }),
|
|
26136
|
-
signal: ctrl.signal
|
|
26137
|
-
});
|
|
26138
|
-
} catch (err) {
|
|
26139
|
-
clearTimeout(timer);
|
|
26140
|
-
throw new TunnelRegisterError(`tunnel register network error: ${err.message}`, err);
|
|
26538
|
+
return import_node_fs13.default.realpathSync(p2);
|
|
26539
|
+
} catch {
|
|
26540
|
+
return null;
|
|
26141
26541
|
}
|
|
26142
|
-
|
|
26143
|
-
|
|
26144
|
-
|
|
26542
|
+
}
|
|
26543
|
+
|
|
26544
|
+
// src/attachment/mime.ts
|
|
26545
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
26546
|
+
var EXT_TO_MIME = {
|
|
26547
|
+
".md": "text/markdown",
|
|
26548
|
+
".markdown": "text/markdown",
|
|
26549
|
+
".txt": "text/plain",
|
|
26550
|
+
".log": "text/plain",
|
|
26551
|
+
".json": "application/json",
|
|
26552
|
+
".html": "text/html",
|
|
26553
|
+
".htm": "text/html",
|
|
26554
|
+
".css": "text/css",
|
|
26555
|
+
".js": "application/javascript",
|
|
26556
|
+
".ts": "application/typescript",
|
|
26557
|
+
".tsx": "application/typescript",
|
|
26558
|
+
".png": "image/png",
|
|
26559
|
+
".jpg": "image/jpeg",
|
|
26560
|
+
".jpeg": "image/jpeg",
|
|
26561
|
+
".gif": "image/gif",
|
|
26562
|
+
".webp": "image/webp",
|
|
26563
|
+
".svg": "image/svg+xml",
|
|
26564
|
+
".pdf": "application/pdf",
|
|
26565
|
+
".mp4": "video/mp4",
|
|
26566
|
+
".webm": "video/webm",
|
|
26567
|
+
".mp3": "audio/mpeg",
|
|
26568
|
+
".wav": "audio/wav",
|
|
26569
|
+
".zip": "application/zip",
|
|
26570
|
+
".gz": "application/gzip",
|
|
26571
|
+
".tar": "application/x-tar",
|
|
26572
|
+
".csv": "text/csv",
|
|
26573
|
+
".xml": "application/xml",
|
|
26574
|
+
".yaml": "application/yaml",
|
|
26575
|
+
".yml": "application/yaml"
|
|
26576
|
+
};
|
|
26577
|
+
function lookupMime(filePathOrName) {
|
|
26578
|
+
const ext = import_node_path15.default.extname(filePathOrName).toLowerCase();
|
|
26579
|
+
return EXT_TO_MIME[ext] ?? "application/octet-stream";
|
|
26580
|
+
}
|
|
26581
|
+
|
|
26582
|
+
// src/attachment/inbox.ts
|
|
26583
|
+
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
26584
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
26585
|
+
var import_node_crypto5 = __toESM(require("crypto"), 1);
|
|
26586
|
+
var DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
26587
|
+
async function handlePersonaInbox(req, args, deps) {
|
|
26588
|
+
const filename = readFilenameFromUrl(req.url);
|
|
26589
|
+
if (!filename) throw httpErr(400, "INVALID_PARAM", "missing ?name= query");
|
|
26590
|
+
const safeName = sanitizeFilename(filename);
|
|
26591
|
+
const mime = lookupMime(safeName);
|
|
26592
|
+
if (deps.isAllowedMime && !deps.isAllowedMime(mime)) {
|
|
26593
|
+
throw httpErr(415, "UNSUPPORTED_MEDIA", `mime ${mime} not allowed`);
|
|
26594
|
+
}
|
|
26595
|
+
const inboxDir = import_node_path16.default.join(args.personaDir, "_inbox");
|
|
26596
|
+
import_node_fs14.default.mkdirSync(inboxDir, { recursive: true });
|
|
26597
|
+
const sha = import_node_crypto5.default.randomBytes(6).toString("hex");
|
|
26598
|
+
const fileName = `${sha}-${safeName}`;
|
|
26599
|
+
const absPath = import_node_path16.default.join(inboxDir, fileName);
|
|
26600
|
+
const size = await streamBodyToFile(req, absPath, deps.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
26601
|
+
const relPath = `_inbox/${fileName}`;
|
|
26602
|
+
deps.groupFileStore.upsert(args.scope, args.sessionId, {
|
|
26603
|
+
relPath,
|
|
26604
|
+
from: args.from,
|
|
26605
|
+
addedBy: args.addedBy,
|
|
26606
|
+
size,
|
|
26607
|
+
mime
|
|
26608
|
+
});
|
|
26609
|
+
return { relPath, size, mime };
|
|
26610
|
+
}
|
|
26611
|
+
async function handleSessionInbox(req, args, deps) {
|
|
26612
|
+
const filename = readFilenameFromUrl(req.url);
|
|
26613
|
+
if (!filename) throw httpErr(400, "INVALID_PARAM", "missing ?name= query");
|
|
26614
|
+
const safeName = sanitizeFilename(filename);
|
|
26615
|
+
const mime = lookupMime(safeName);
|
|
26616
|
+
if (deps.isAllowedMime && !deps.isAllowedMime(mime)) {
|
|
26617
|
+
throw httpErr(415, "UNSUPPORTED_MEDIA", `mime ${mime} not allowed`);
|
|
26618
|
+
}
|
|
26619
|
+
const inboxDir = import_node_path16.default.join(args.sessionCwd, "_inbox");
|
|
26620
|
+
import_node_fs14.default.mkdirSync(inboxDir, { recursive: true });
|
|
26621
|
+
const sha = import_node_crypto5.default.randomBytes(6).toString("hex");
|
|
26622
|
+
const fileName = `${sha}-${safeName}`;
|
|
26623
|
+
const absPath = import_node_path16.default.join(inboxDir, fileName);
|
|
26624
|
+
const size = await streamBodyToFile(req, absPath, deps.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
26625
|
+
const relPath = `_inbox/${fileName}`;
|
|
26626
|
+
deps.groupFileStore.upsert(args.scope, args.sessionId, {
|
|
26627
|
+
relPath,
|
|
26628
|
+
from: "owner",
|
|
26629
|
+
size,
|
|
26630
|
+
mime
|
|
26631
|
+
});
|
|
26632
|
+
return { relPath, size, mime };
|
|
26633
|
+
}
|
|
26634
|
+
function streamBodyToFile(req, absPath, maxBytes) {
|
|
26635
|
+
return new Promise((resolve2, reject) => {
|
|
26636
|
+
let received = 0;
|
|
26637
|
+
const tmp = `${absPath}.tmp-${process.pid}-${Date.now()}`;
|
|
26638
|
+
const stream = import_node_fs14.default.createWriteStream(tmp, { mode: 384 });
|
|
26639
|
+
req.on("data", (chunk) => {
|
|
26640
|
+
received += chunk.length;
|
|
26641
|
+
if (received > maxBytes) {
|
|
26642
|
+
stream.destroy();
|
|
26643
|
+
try {
|
|
26644
|
+
import_node_fs14.default.unlinkSync(tmp);
|
|
26645
|
+
} catch {
|
|
26646
|
+
}
|
|
26647
|
+
reject(httpErr(413, "TOO_LARGE", `upload exceeds ${maxBytes} bytes`));
|
|
26648
|
+
req.destroy();
|
|
26649
|
+
return;
|
|
26650
|
+
}
|
|
26651
|
+
stream.write(chunk);
|
|
26652
|
+
});
|
|
26653
|
+
req.on("end", () => {
|
|
26654
|
+
stream.end();
|
|
26655
|
+
stream.on("finish", () => {
|
|
26656
|
+
try {
|
|
26657
|
+
import_node_fs14.default.renameSync(tmp, absPath);
|
|
26658
|
+
resolve2(received);
|
|
26659
|
+
} catch (err) {
|
|
26660
|
+
reject(err);
|
|
26661
|
+
}
|
|
26662
|
+
});
|
|
26663
|
+
});
|
|
26664
|
+
req.on("error", reject);
|
|
26665
|
+
stream.on("error", reject);
|
|
26666
|
+
});
|
|
26667
|
+
}
|
|
26668
|
+
function readFilenameFromUrl(rawUrl) {
|
|
26669
|
+
if (!rawUrl) return null;
|
|
26670
|
+
try {
|
|
26671
|
+
const u = new URL(rawUrl, "http://placeholder");
|
|
26672
|
+
return u.searchParams.get("name");
|
|
26673
|
+
} catch {
|
|
26674
|
+
return null;
|
|
26675
|
+
}
|
|
26676
|
+
}
|
|
26677
|
+
function sanitizeFilename(name) {
|
|
26678
|
+
const trimmed = name.replace(/[\x00-\x1f]/g, "").replace(/\//g, "_").replace(/\\/g, "_");
|
|
26679
|
+
const dotsOnly = trimmed.replace(/^\.+/, (m2) => "_".repeat(m2.length));
|
|
26680
|
+
if (!dotsOnly) return "unnamed";
|
|
26681
|
+
return dotsOnly.slice(0, 200);
|
|
26682
|
+
}
|
|
26683
|
+
function httpErr(status, code, message) {
|
|
26684
|
+
const err = new Error(message);
|
|
26685
|
+
err.httpStatus = status;
|
|
26686
|
+
err.code = code;
|
|
26687
|
+
return err;
|
|
26688
|
+
}
|
|
26689
|
+
function isHttpError(err) {
|
|
26690
|
+
return err instanceof Error && typeof err.httpStatus === "number" && typeof err.code === "string";
|
|
26691
|
+
}
|
|
26692
|
+
async function handleInboxRequest(req, res, ctxRouter, deps) {
|
|
26693
|
+
const personaMatch = ctxRouter.pathname.match(/^\/persona\/([^/]+)\/inbox$/);
|
|
26694
|
+
if (personaMatch && req.method === "POST") {
|
|
26695
|
+
if (!deps.getPersonaDir || !deps.getPersonaScope) {
|
|
26696
|
+
sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "inbox not wired" });
|
|
26697
|
+
return true;
|
|
26698
|
+
}
|
|
26699
|
+
const pid = personaMatch[1];
|
|
26700
|
+
const personaDir = deps.getPersonaDir(pid);
|
|
26701
|
+
if (!personaDir) {
|
|
26702
|
+
sendJson(res, 404, { code: "NOT_FOUND", message: `persona ${pid} not found` });
|
|
26703
|
+
return true;
|
|
26704
|
+
}
|
|
26705
|
+
const sessionId = new URL(req.url ?? "", "http://placeholder").searchParams.get("session") || pid;
|
|
26706
|
+
try {
|
|
26707
|
+
const result = await handlePersonaInbox(
|
|
26708
|
+
req,
|
|
26709
|
+
{
|
|
26710
|
+
personaId: pid,
|
|
26711
|
+
personaDir,
|
|
26712
|
+
sessionId,
|
|
26713
|
+
scope: deps.getPersonaScope(pid),
|
|
26714
|
+
addedBy: deps.addedBy,
|
|
26715
|
+
from: ctxRouter.isOwner ? "owner" : "user"
|
|
26716
|
+
},
|
|
26717
|
+
deps.inboxOpts ?? { groupFileStore: deps.groupFileStore }
|
|
26718
|
+
);
|
|
26719
|
+
sendJson(res, 200, result);
|
|
26720
|
+
} catch (err) {
|
|
26721
|
+
handleInboxError(err, res);
|
|
26722
|
+
}
|
|
26723
|
+
return true;
|
|
26724
|
+
}
|
|
26725
|
+
const sessionMatch = ctxRouter.pathname.match(/^\/session\/([^/]+)\/inbox$/);
|
|
26726
|
+
if (sessionMatch && req.method === "POST") {
|
|
26727
|
+
if (!ctxRouter.isOwner) {
|
|
26728
|
+
sendJson(res, 403, { code: "FORBIDDEN", message: "direct session inbox is owner-only" });
|
|
26729
|
+
return true;
|
|
26730
|
+
}
|
|
26731
|
+
if (!deps.getSessionCwd) {
|
|
26732
|
+
sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "inbox not wired" });
|
|
26733
|
+
return true;
|
|
26734
|
+
}
|
|
26735
|
+
const sid = sessionMatch[1];
|
|
26736
|
+
const sessionCwd = deps.getSessionCwd(sid);
|
|
26737
|
+
if (!sessionCwd) {
|
|
26738
|
+
sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
|
|
26739
|
+
return true;
|
|
26740
|
+
}
|
|
26741
|
+
try {
|
|
26742
|
+
const result = await handleSessionInbox(
|
|
26743
|
+
req,
|
|
26744
|
+
{
|
|
26745
|
+
sessionId: sid,
|
|
26746
|
+
sessionCwd: sessionCwd.cwd,
|
|
26747
|
+
scope: sessionCwd.scope,
|
|
26748
|
+
from: "owner"
|
|
26749
|
+
},
|
|
26750
|
+
deps.inboxOpts ?? { groupFileStore: deps.groupFileStore }
|
|
26751
|
+
);
|
|
26752
|
+
sendJson(res, 200, result);
|
|
26753
|
+
} catch (err) {
|
|
26754
|
+
handleInboxError(err, res);
|
|
26755
|
+
}
|
|
26756
|
+
return true;
|
|
26757
|
+
}
|
|
26758
|
+
return false;
|
|
26759
|
+
}
|
|
26760
|
+
function handleInboxError(err, res) {
|
|
26761
|
+
if (isHttpError(err)) {
|
|
26762
|
+
sendJson(res, err.httpStatus, { code: err.code, message: err.message });
|
|
26763
|
+
return;
|
|
26764
|
+
}
|
|
26765
|
+
sendJson(res, 500, { code: "INTERNAL", message: err.message });
|
|
26766
|
+
}
|
|
26767
|
+
function sendJson(res, status, body) {
|
|
26768
|
+
if (res.headersSent) return;
|
|
26769
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
26770
|
+
res.end(JSON.stringify(body));
|
|
26771
|
+
}
|
|
26772
|
+
|
|
26773
|
+
// src/transport/http-router.ts
|
|
26774
|
+
function createHttpRouter(deps) {
|
|
26775
|
+
return async (req, res) => {
|
|
26776
|
+
const url = parseUrl(req.url);
|
|
26777
|
+
if (!url) {
|
|
26778
|
+
sendJson2(res, 400, { code: "INVALID_URL", message: "malformed request URL" });
|
|
26779
|
+
return true;
|
|
26780
|
+
}
|
|
26781
|
+
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
26782
|
+
sendJson2(res, 200, { ok: true, version: deps.daemonVersion });
|
|
26783
|
+
return true;
|
|
26784
|
+
}
|
|
26785
|
+
if (!url.pathname.startsWith("/persona/") && !url.pathname.startsWith("/session/") && url.pathname !== "/outbox" && !url.pathname.startsWith("/outbox/")) {
|
|
26786
|
+
return false;
|
|
26787
|
+
}
|
|
26788
|
+
{
|
|
26789
|
+
const directMatch = url.pathname.match(/^\/outbox\/([^/]+)$/);
|
|
26790
|
+
const personaMatch = url.pathname.match(/^\/persona\/([^/]+)\/outbox\/([^/]+)$/);
|
|
26791
|
+
if (directMatch || personaMatch) {
|
|
26792
|
+
if (!deps.outboxStore) {
|
|
26793
|
+
sendJson2(res, 501, { code: "NOT_IMPLEMENTED", message: "outbox store not wired" });
|
|
26794
|
+
return true;
|
|
26795
|
+
}
|
|
26796
|
+
const capToken = directMatch ? directMatch[1] : personaMatch[2];
|
|
26797
|
+
const lookup = deps.outboxStore.lookup(capToken);
|
|
26798
|
+
if (!lookup.ok) {
|
|
26799
|
+
const statusByCode = {
|
|
26800
|
+
NOT_FOUND: 404,
|
|
26801
|
+
EXPIRED: 410,
|
|
26802
|
+
REVOKED: 410
|
|
26803
|
+
};
|
|
26804
|
+
sendJson2(res, statusByCode[lookup.code], { code: lookup.code, message: "outbox cap not usable" });
|
|
26805
|
+
return true;
|
|
26806
|
+
}
|
|
26807
|
+
const { entry } = lookup;
|
|
26808
|
+
if (entry.scope.kind === "personal") {
|
|
26809
|
+
const ctx2 = deps.authResolver.resolveFromHeader(
|
|
26810
|
+
req.headers.authorization,
|
|
26811
|
+
req.socket.remoteAddress ?? void 0
|
|
26812
|
+
);
|
|
26813
|
+
if (!ctx2 || ctx2.role !== "personal" || ctx2.personaId !== entry.scope.personalId) {
|
|
26814
|
+
sendJson2(res, 403, { code: "FORBIDDEN", message: "personal-scoped cap requires matching token" });
|
|
26815
|
+
return true;
|
|
26816
|
+
}
|
|
26817
|
+
}
|
|
26818
|
+
const outboxStore = deps.outboxStore;
|
|
26819
|
+
streamFile(res, entry.absPath, deps.logger, () => outboxStore.consume(capToken));
|
|
26820
|
+
return true;
|
|
26821
|
+
}
|
|
26822
|
+
}
|
|
26823
|
+
const ctx = deps.authResolver.resolveFromHeader(
|
|
26824
|
+
req.headers.authorization,
|
|
26825
|
+
req.socket.remoteAddress ?? void 0
|
|
26826
|
+
);
|
|
26827
|
+
if (!ctx) {
|
|
26828
|
+
sendJson2(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
|
|
26829
|
+
return true;
|
|
26830
|
+
}
|
|
26831
|
+
const personaFilesMatch = url.pathname.match(/^\/persona\/([^/]+)\/files$/);
|
|
26832
|
+
if (personaFilesMatch && req.method === "GET") {
|
|
26833
|
+
const pid = personaFilesMatch[1];
|
|
26834
|
+
const pathParam = url.searchParams.get("path");
|
|
26835
|
+
if (!pathParam) {
|
|
26836
|
+
sendJson2(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
|
|
26837
|
+
return true;
|
|
26838
|
+
}
|
|
26839
|
+
if (!deps.personaStore || !deps.groupFileStore) {
|
|
26840
|
+
sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "files endpoint not wired" }));
|
|
26841
|
+
return true;
|
|
26842
|
+
}
|
|
26843
|
+
const personaDir = deps.personaStore.personaDirPath(pid);
|
|
26844
|
+
const absPath = import_node_path17.default.isAbsolute(pathParam) ? pathParam : import_node_path17.default.join(personaDir, pathParam);
|
|
26845
|
+
if (!import_node_path17.default.isAbsolute(pathParam) && !isContainedIn(absPath, personaDir)) {
|
|
26846
|
+
sendJson2(res, 400, { code: "PATH_TRAVERSAL", message: "rel path escapes personaDir" });
|
|
26847
|
+
return true;
|
|
26848
|
+
}
|
|
26849
|
+
if (ctx.role === "personal") {
|
|
26850
|
+
if (ctx.personaId !== pid) {
|
|
26851
|
+
sendJson2(res, 403, { code: "FORBIDDEN", message: "personal token bound to other persona" });
|
|
26852
|
+
return true;
|
|
26853
|
+
}
|
|
26854
|
+
if (!personalViewable(deps.groupFileStore, personaDir, pid, absPath)) {
|
|
26855
|
+
sendJson2(res, 403, { code: "FORBIDDEN", message: "path not in personal viewable scope" });
|
|
26856
|
+
return true;
|
|
26857
|
+
}
|
|
26858
|
+
}
|
|
26859
|
+
streamFile(res, absPath, deps.logger);
|
|
26860
|
+
return true;
|
|
26861
|
+
}
|
|
26862
|
+
const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
|
|
26863
|
+
if (sessionFilesMatch && req.method === "GET") {
|
|
26864
|
+
if (ctx.role !== "owner") {
|
|
26865
|
+
sendJson2(res, 403, { code: "FORBIDDEN", message: "direct session files are owner-only" });
|
|
26866
|
+
return true;
|
|
26867
|
+
}
|
|
26868
|
+
const sid = sessionFilesMatch[1];
|
|
26869
|
+
const pathParam = url.searchParams.get("path");
|
|
26870
|
+
if (!pathParam) {
|
|
26871
|
+
sendJson2(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
|
|
26872
|
+
return true;
|
|
26873
|
+
}
|
|
26874
|
+
let absPath;
|
|
26875
|
+
if (import_node_path17.default.isAbsolute(pathParam)) {
|
|
26876
|
+
absPath = pathParam;
|
|
26877
|
+
} else if (deps.sessionStore) {
|
|
26878
|
+
const file = deps.sessionStore.read(sid);
|
|
26879
|
+
if (!file) {
|
|
26880
|
+
sendJson2(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
|
|
26881
|
+
return true;
|
|
26882
|
+
}
|
|
26883
|
+
absPath = import_node_path17.default.join(file.cwd, pathParam);
|
|
26884
|
+
} else {
|
|
26885
|
+
sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
|
|
26886
|
+
return true;
|
|
26887
|
+
}
|
|
26888
|
+
streamFile(res, absPath, deps.logger);
|
|
26889
|
+
return true;
|
|
26890
|
+
}
|
|
26891
|
+
if (/^\/persona\/[^/]+\/inbox$/.test(url.pathname) && req.method === "POST") {
|
|
26892
|
+
if (!deps.groupFileStore || !deps.personaStore) {
|
|
26893
|
+
sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "inbox not wired" }));
|
|
26894
|
+
return true;
|
|
26895
|
+
}
|
|
26896
|
+
const handled = await handleInboxRequest(
|
|
26897
|
+
req,
|
|
26898
|
+
res,
|
|
26899
|
+
{
|
|
26900
|
+
pathname: url.pathname,
|
|
26901
|
+
isOwner: ctx.role === "owner",
|
|
26902
|
+
personaId: ctx.personaId
|
|
26903
|
+
},
|
|
26904
|
+
{
|
|
26905
|
+
groupFileStore: deps.groupFileStore,
|
|
26906
|
+
getPersonaDir: (pid) => deps.personaStore.personaDirPath(pid),
|
|
26907
|
+
// PR 7:persona inbox 默认走 owner-mode scope;UI 端如果用 personal listener
|
|
26908
|
+
// 也归到 owner scope(session 维度)—— PR 9 完善时再走 listener scope。
|
|
26909
|
+
getPersonaScope: (pid) => ({
|
|
26910
|
+
kind: "persona",
|
|
26911
|
+
personaId: pid,
|
|
26912
|
+
mode: "owner"
|
|
26913
|
+
}),
|
|
26914
|
+
addedBy: ctx.label ?? (ctx.role === "owner" ? "owner" : "unknown")
|
|
26915
|
+
}
|
|
26916
|
+
);
|
|
26917
|
+
if (handled) return true;
|
|
26918
|
+
}
|
|
26919
|
+
if (/^\/session\/[^/]+\/inbox$/.test(url.pathname) && req.method === "POST") {
|
|
26920
|
+
if (!deps.groupFileStore || !deps.sessionStore) {
|
|
26921
|
+
sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "inbox not wired" }));
|
|
26922
|
+
return true;
|
|
26923
|
+
}
|
|
26924
|
+
const handled = await handleInboxRequest(
|
|
26925
|
+
req,
|
|
26926
|
+
res,
|
|
26927
|
+
{
|
|
26928
|
+
pathname: url.pathname,
|
|
26929
|
+
isOwner: ctx.role === "owner"
|
|
26930
|
+
},
|
|
26931
|
+
{
|
|
26932
|
+
groupFileStore: deps.groupFileStore,
|
|
26933
|
+
getSessionCwd: (sid) => {
|
|
26934
|
+
const f = deps.sessionStore.read(sid);
|
|
26935
|
+
if (!f) return null;
|
|
26936
|
+
const scope = f.ownerPersonaId ? { kind: "persona", personaId: f.ownerPersonaId, mode: "owner" } : { kind: "default" };
|
|
26937
|
+
return { cwd: f.cwd, scope };
|
|
26938
|
+
},
|
|
26939
|
+
addedBy: ctx.role === "owner" ? "owner" : "unknown"
|
|
26940
|
+
}
|
|
26941
|
+
);
|
|
26942
|
+
if (handled) return true;
|
|
26943
|
+
}
|
|
26944
|
+
if (/^\/persona\/[^/]+\/attachment-meta$/.test(url.pathname) && req.method === "GET") {
|
|
26945
|
+
sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "attachment-meta \u2014 PR 6" }));
|
|
26946
|
+
return true;
|
|
26947
|
+
}
|
|
26948
|
+
sendJson2(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
|
|
26949
|
+
return true;
|
|
26950
|
+
};
|
|
26951
|
+
}
|
|
26952
|
+
function parseUrl(rawUrl) {
|
|
26953
|
+
if (!rawUrl) return null;
|
|
26954
|
+
try {
|
|
26955
|
+
return new URL(rawUrl, "http://placeholder");
|
|
26956
|
+
} catch {
|
|
26957
|
+
return null;
|
|
26958
|
+
}
|
|
26959
|
+
}
|
|
26960
|
+
function sendJson2(res, status, body) {
|
|
26961
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
26962
|
+
res.end(JSON.stringify(body));
|
|
26963
|
+
}
|
|
26964
|
+
function withCtx(ctx, body) {
|
|
26965
|
+
return { ...body, role: ctx.role, personaId: ctx.personaId };
|
|
26966
|
+
}
|
|
26967
|
+
function isContainedIn(abs, root) {
|
|
26968
|
+
const normalized = import_node_path17.default.resolve(abs);
|
|
26969
|
+
const normalizedRoot = import_node_path17.default.resolve(root);
|
|
26970
|
+
if (normalized === normalizedRoot) return true;
|
|
26971
|
+
return normalized.startsWith(normalizedRoot + import_node_path17.default.sep);
|
|
26972
|
+
}
|
|
26973
|
+
function streamFile(res, absPath, logger, onComplete) {
|
|
26974
|
+
let stat;
|
|
26975
|
+
try {
|
|
26976
|
+
stat = import_node_fs15.default.statSync(absPath);
|
|
26977
|
+
} catch (err) {
|
|
26978
|
+
const code = err?.code;
|
|
26979
|
+
if (code === "ENOENT") {
|
|
26980
|
+
sendJson2(res, 404, { code: "NOT_FOUND", message: "file not found" });
|
|
26981
|
+
} else {
|
|
26982
|
+
sendJson2(res, 500, { code: "STAT_FAILED", message: err.message });
|
|
26983
|
+
}
|
|
26984
|
+
return;
|
|
26985
|
+
}
|
|
26986
|
+
if (!stat.isFile()) {
|
|
26987
|
+
sendJson2(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
|
|
26988
|
+
return;
|
|
26989
|
+
}
|
|
26990
|
+
const mime = lookupMime(absPath);
|
|
26991
|
+
res.writeHead(200, {
|
|
26992
|
+
"Content-Type": mime,
|
|
26993
|
+
"Content-Length": String(stat.size),
|
|
26994
|
+
// 防止浏览器把任意 mime 当 html 渲染(spec §11 #8 安全心智)
|
|
26995
|
+
"X-Content-Type-Options": "nosniff"
|
|
26996
|
+
});
|
|
26997
|
+
const stream = import_node_fs15.default.createReadStream(absPath);
|
|
26998
|
+
stream.on("error", (err) => {
|
|
26999
|
+
logger?.warn("streamFile read error", { absPath, err: err.message });
|
|
27000
|
+
res.destroy();
|
|
27001
|
+
});
|
|
27002
|
+
if (onComplete) {
|
|
27003
|
+
res.once("finish", onComplete);
|
|
27004
|
+
}
|
|
27005
|
+
stream.pipe(res);
|
|
27006
|
+
}
|
|
27007
|
+
|
|
27008
|
+
// src/attachment/outbox.ts
|
|
27009
|
+
var import_node_fs16 = __toESM(require("fs"), 1);
|
|
27010
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
27011
|
+
var import_node_crypto6 = __toESM(require("crypto"), 1);
|
|
27012
|
+
init_protocol();
|
|
27013
|
+
var FILE_NAME = "outbox-caps.json";
|
|
27014
|
+
var OutboxStore = class {
|
|
27015
|
+
file;
|
|
27016
|
+
now;
|
|
27017
|
+
logger;
|
|
27018
|
+
cache = [];
|
|
27019
|
+
constructor(opts) {
|
|
27020
|
+
this.file = import_node_path18.default.join(opts.dataDir, FILE_NAME);
|
|
27021
|
+
this.now = opts.now ?? Date.now;
|
|
27022
|
+
this.logger = opts.logger;
|
|
27023
|
+
this.reload();
|
|
27024
|
+
}
|
|
27025
|
+
reload() {
|
|
27026
|
+
try {
|
|
27027
|
+
const raw = import_node_fs16.default.readFileSync(this.file, "utf8");
|
|
27028
|
+
const parsed = JSON.parse(raw);
|
|
27029
|
+
if (!Array.isArray(parsed)) {
|
|
27030
|
+
this.logger?.warn("OutboxStore.reload: outbox-caps.json is not an array; resetting", {
|
|
27031
|
+
file: this.file
|
|
27032
|
+
});
|
|
27033
|
+
this.cache = [];
|
|
27034
|
+
return;
|
|
27035
|
+
}
|
|
27036
|
+
const out = [];
|
|
27037
|
+
for (const item of parsed) {
|
|
27038
|
+
const r = OutboxCapEntrySchema.safeParse(item);
|
|
27039
|
+
if (r.success) out.push(r.data);
|
|
27040
|
+
}
|
|
27041
|
+
this.cache = out;
|
|
27042
|
+
} catch (err) {
|
|
27043
|
+
const code = err?.code;
|
|
27044
|
+
if (code !== "ENOENT") {
|
|
27045
|
+
this.logger?.warn("OutboxStore.reload failed; outbox-caps.json may be corrupt", {
|
|
27046
|
+
file: this.file,
|
|
27047
|
+
err: err.message
|
|
27048
|
+
});
|
|
27049
|
+
}
|
|
27050
|
+
this.cache = [];
|
|
27051
|
+
}
|
|
27052
|
+
}
|
|
27053
|
+
persist() {
|
|
27054
|
+
import_node_fs16.default.mkdirSync(import_node_path18.default.dirname(this.file), { recursive: true });
|
|
27055
|
+
const tmp = `${this.file}.tmp-${process.pid}-${Date.now()}`;
|
|
27056
|
+
import_node_fs16.default.writeFileSync(tmp, JSON.stringify(this.cache, null, 2), { mode: 384 });
|
|
27057
|
+
import_node_fs16.default.renameSync(tmp, this.file);
|
|
27058
|
+
}
|
|
27059
|
+
/** 列出当前缓存(含 revoked / 过期,UI Drawer 显灰;过滤靠调用方) */
|
|
27060
|
+
list() {
|
|
27061
|
+
return this.cache.slice();
|
|
27062
|
+
}
|
|
27063
|
+
/** 按 personaId / sessionId 过滤;为 outbox Drawer 服务 */
|
|
27064
|
+
listByPersona(personaId) {
|
|
27065
|
+
return this.cache.filter(
|
|
27066
|
+
(c) => c.scopeRef.kind === "persona" && c.scopeRef.personaId === personaId
|
|
27067
|
+
);
|
|
27068
|
+
}
|
|
27069
|
+
/**
|
|
27070
|
+
* Mint a new cap. capToken = 32B 随机 base64url(256-bit entropy),spec §11 #8 防剪贴板/微信
|
|
27071
|
+
* 截图泄露通过短码的暴力面。返回完整 entry 让 caller 拼 URL。
|
|
27072
|
+
*
|
|
27073
|
+
* @param ttlSeconds null = 永久;undefined = 默认 24h(spec §12)
|
|
27074
|
+
*/
|
|
27075
|
+
mint(input) {
|
|
27076
|
+
const ts = this.now();
|
|
27077
|
+
const ttl = input.ttlSeconds === null ? null : input.ttlSeconds ?? 24 * 3600;
|
|
27078
|
+
const entry = {
|
|
27079
|
+
capToken: import_node_crypto6.default.randomBytes(32).toString("base64url"),
|
|
27080
|
+
scopeRef: input.scopeRef,
|
|
27081
|
+
absPath: input.absPath,
|
|
27082
|
+
name: input.name,
|
|
27083
|
+
expiresAt: ttl === null ? null : ts + ttl * 1e3,
|
|
27084
|
+
oneShot: input.oneShot ?? false,
|
|
27085
|
+
scope: input.scope ?? { kind: "public" },
|
|
27086
|
+
hits: 0,
|
|
27087
|
+
createdAt: ts
|
|
27088
|
+
};
|
|
27089
|
+
this.cache.push(entry);
|
|
27090
|
+
this.persist();
|
|
27091
|
+
return entry;
|
|
27092
|
+
}
|
|
27093
|
+
/** Mark revoked;不删除条目(便于审计),上层 lookup 后返回 410 */
|
|
27094
|
+
revoke(capToken) {
|
|
27095
|
+
const idx = this.cache.findIndex((c) => c.capToken === capToken);
|
|
27096
|
+
if (idx < 0) return false;
|
|
27097
|
+
if (this.cache[idx].revoked) return false;
|
|
27098
|
+
this.cache[idx] = { ...this.cache[idx], revoked: true };
|
|
27099
|
+
this.persist();
|
|
27100
|
+
return true;
|
|
27101
|
+
}
|
|
27102
|
+
/**
|
|
27103
|
+
* HTTP 层用:根据 capToken 找到 entry 并判定能不能用。
|
|
27104
|
+
* 三种结果:
|
|
27105
|
+
* - { ok: true, entry } 可用,调用方流文件
|
|
27106
|
+
* - { ok: false, code: 'EXPIRED' | 'REVOKED' | 'NOT_FOUND' }
|
|
27107
|
+
*
|
|
27108
|
+
* 命中 + oneShot=true 时由调用方调 consume() 把 cache 中 hits 自增并立即 revoke
|
|
27109
|
+
* (consume 应当在 response 'finish' 后才触发,避免中途断流误算消费)。
|
|
27110
|
+
*/
|
|
27111
|
+
lookup(capToken) {
|
|
27112
|
+
const entry = this.cache.find((c) => c.capToken === capToken);
|
|
27113
|
+
if (!entry) return { ok: false, code: "NOT_FOUND" };
|
|
27114
|
+
if (entry.revoked) return { ok: false, code: "REVOKED" };
|
|
27115
|
+
if (entry.expiresAt !== null && this.now() > entry.expiresAt) {
|
|
27116
|
+
return { ok: false, code: "EXPIRED" };
|
|
27117
|
+
}
|
|
27118
|
+
return { ok: true, entry };
|
|
27119
|
+
}
|
|
27120
|
+
/** 命中后真正"消费"一次:hits++ + oneShot 时 revoke。daemon HTTP 层 stream 成功后再调 */
|
|
27121
|
+
consume(capToken) {
|
|
27122
|
+
const idx = this.cache.findIndex((c) => c.capToken === capToken);
|
|
27123
|
+
if (idx < 0) return;
|
|
27124
|
+
const prev = this.cache[idx];
|
|
27125
|
+
this.cache[idx] = {
|
|
27126
|
+
...prev,
|
|
27127
|
+
hits: prev.hits + 1,
|
|
27128
|
+
revoked: prev.oneShot ? true : prev.revoked
|
|
27129
|
+
};
|
|
27130
|
+
this.persist();
|
|
27131
|
+
}
|
|
27132
|
+
};
|
|
27133
|
+
|
|
27134
|
+
// src/attachment/mount.ts
|
|
27135
|
+
var import_node_fs17 = __toESM(require("fs"), 1);
|
|
27136
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
27137
|
+
init_protocol();
|
|
27138
|
+
var MountStore = class {
|
|
27139
|
+
constructor(opts) {
|
|
27140
|
+
this.opts = opts;
|
|
27141
|
+
}
|
|
27142
|
+
opts;
|
|
27143
|
+
filePath(personaId) {
|
|
27144
|
+
return import_node_path19.default.join(this.opts.personaDirPath(personaId), ".clawd", "shared-files.json");
|
|
27145
|
+
}
|
|
27146
|
+
list(personaId) {
|
|
27147
|
+
try {
|
|
27148
|
+
const raw = import_node_fs17.default.readFileSync(this.filePath(personaId), "utf8");
|
|
27149
|
+
const parsed = JSON.parse(raw);
|
|
27150
|
+
if (!Array.isArray(parsed)) return [];
|
|
27151
|
+
const out = [];
|
|
27152
|
+
for (const item of parsed) {
|
|
27153
|
+
const r = MountEntrySchema.safeParse(item);
|
|
27154
|
+
if (r.success) out.push(r.data);
|
|
27155
|
+
}
|
|
27156
|
+
return out;
|
|
27157
|
+
} catch {
|
|
27158
|
+
return [];
|
|
27159
|
+
}
|
|
27160
|
+
}
|
|
27161
|
+
/**
|
|
27162
|
+
* Add a mount。在 personaDir 创建 link/copy + 写 manifest。冲突 basename 抛错(caller
|
|
27163
|
+
* 决定是覆盖还是改名)。
|
|
27164
|
+
*/
|
|
27165
|
+
add(personaId, input) {
|
|
27166
|
+
const personaDir = this.opts.personaDirPath(personaId);
|
|
27167
|
+
if (!import_node_fs17.default.existsSync(personaDir)) {
|
|
27168
|
+
throw new Error(`personaDir not found: ${personaDir}`);
|
|
27169
|
+
}
|
|
27170
|
+
const source = import_node_path19.default.resolve(input.absPath);
|
|
27171
|
+
if (!import_node_fs17.default.existsSync(source)) {
|
|
27172
|
+
throw new Error(`source path not found: ${source}`);
|
|
27173
|
+
}
|
|
27174
|
+
const basename = import_node_path19.default.basename(source);
|
|
27175
|
+
const dest = import_node_path19.default.join(personaDir, basename);
|
|
27176
|
+
if (import_node_fs17.default.existsSync(dest)) {
|
|
27177
|
+
throw new Error(`destination already exists: ${dest}`);
|
|
27178
|
+
}
|
|
27179
|
+
if (input.mode === "link") {
|
|
27180
|
+
import_node_fs17.default.symlinkSync(source, dest);
|
|
27181
|
+
} else {
|
|
27182
|
+
import_node_fs17.default.copyFileSync(source, dest);
|
|
27183
|
+
}
|
|
27184
|
+
const entry = {
|
|
27185
|
+
basename,
|
|
27186
|
+
absPath: source,
|
|
27187
|
+
mode: input.mode
|
|
27188
|
+
};
|
|
27189
|
+
const next = this.list(personaId).concat([entry]);
|
|
27190
|
+
this.writeManifest(personaId, next);
|
|
27191
|
+
return entry;
|
|
27192
|
+
}
|
|
27193
|
+
remove(personaId, basename) {
|
|
27194
|
+
const entries = this.list(personaId);
|
|
27195
|
+
const idx = entries.findIndex((e) => e.basename === basename);
|
|
27196
|
+
if (idx < 0) return false;
|
|
27197
|
+
const entry = entries[idx];
|
|
27198
|
+
const personaDir = this.opts.personaDirPath(personaId);
|
|
27199
|
+
const dest = import_node_path19.default.join(personaDir, basename);
|
|
27200
|
+
try {
|
|
27201
|
+
import_node_fs17.default.unlinkSync(dest);
|
|
27202
|
+
} catch {
|
|
27203
|
+
}
|
|
27204
|
+
const next = entries.slice();
|
|
27205
|
+
next.splice(idx, 1);
|
|
27206
|
+
this.writeManifest(personaId, next);
|
|
27207
|
+
void entry;
|
|
27208
|
+
return true;
|
|
27209
|
+
}
|
|
27210
|
+
writeManifest(personaId, entries) {
|
|
27211
|
+
const file = this.filePath(personaId);
|
|
27212
|
+
import_node_fs17.default.mkdirSync(import_node_path19.default.dirname(file), { recursive: true });
|
|
27213
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
27214
|
+
import_node_fs17.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
|
|
27215
|
+
import_node_fs17.default.renameSync(tmp, file);
|
|
27216
|
+
}
|
|
27217
|
+
};
|
|
27218
|
+
|
|
27219
|
+
// src/discovery/state-file.ts
|
|
27220
|
+
var import_node_fs18 = __toESM(require("fs"), 1);
|
|
27221
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
27222
|
+
function defaultStateFilePath(dataDir) {
|
|
27223
|
+
return import_node_path20.default.join(dataDir, "state.json");
|
|
27224
|
+
}
|
|
27225
|
+
function isPidAlive(pid) {
|
|
27226
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
27227
|
+
try {
|
|
27228
|
+
process.kill(pid, 0);
|
|
27229
|
+
return true;
|
|
27230
|
+
} catch (err) {
|
|
27231
|
+
const code = err.code;
|
|
27232
|
+
return code === "EPERM";
|
|
27233
|
+
}
|
|
27234
|
+
}
|
|
27235
|
+
var StateFileManager = class {
|
|
27236
|
+
file;
|
|
27237
|
+
constructor(opts) {
|
|
27238
|
+
this.file = defaultStateFilePath(opts.dataDir);
|
|
27239
|
+
}
|
|
27240
|
+
filePath() {
|
|
27241
|
+
return this.file;
|
|
27242
|
+
}
|
|
27243
|
+
read() {
|
|
27244
|
+
try {
|
|
27245
|
+
const raw = import_node_fs18.default.readFileSync(this.file, "utf8");
|
|
27246
|
+
const parsed = JSON.parse(raw);
|
|
27247
|
+
return parsed;
|
|
27248
|
+
} catch {
|
|
27249
|
+
return null;
|
|
27250
|
+
}
|
|
27251
|
+
}
|
|
27252
|
+
preflight() {
|
|
27253
|
+
const existing = this.read();
|
|
27254
|
+
if (!existing) return { status: "ok" };
|
|
27255
|
+
if (isPidAlive(existing.pid)) return { status: "active", existing };
|
|
27256
|
+
return { status: "stale", existing };
|
|
27257
|
+
}
|
|
27258
|
+
write(state) {
|
|
27259
|
+
import_node_fs18.default.mkdirSync(import_node_path20.default.dirname(this.file), { recursive: true });
|
|
27260
|
+
const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
|
|
27261
|
+
import_node_fs18.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
|
|
27262
|
+
import_node_fs18.default.renameSync(tmp, this.file);
|
|
27263
|
+
if (process.platform !== "win32") {
|
|
27264
|
+
try {
|
|
27265
|
+
import_node_fs18.default.chmodSync(this.file, 384);
|
|
27266
|
+
} catch {
|
|
27267
|
+
}
|
|
27268
|
+
}
|
|
27269
|
+
}
|
|
27270
|
+
delete() {
|
|
27271
|
+
try {
|
|
27272
|
+
import_node_fs18.default.unlinkSync(this.file);
|
|
27273
|
+
} catch {
|
|
27274
|
+
}
|
|
27275
|
+
}
|
|
27276
|
+
};
|
|
27277
|
+
|
|
27278
|
+
// src/tunnel/tunnel-manager.ts
|
|
27279
|
+
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
27280
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
27281
|
+
var import_node_crypto7 = __toESM(require("crypto"), 1);
|
|
27282
|
+
var import_node_child_process5 = require("child_process");
|
|
27283
|
+
|
|
27284
|
+
// src/tunnel/tunnel-store.ts
|
|
27285
|
+
var import_node_fs19 = __toESM(require("fs"), 1);
|
|
27286
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
27287
|
+
var TunnelStore = class {
|
|
27288
|
+
constructor(filePath) {
|
|
27289
|
+
this.filePath = filePath;
|
|
27290
|
+
}
|
|
27291
|
+
filePath;
|
|
27292
|
+
async get() {
|
|
27293
|
+
try {
|
|
27294
|
+
const raw = await import_node_fs19.default.promises.readFile(this.filePath, "utf8");
|
|
27295
|
+
const obj = JSON.parse(raw);
|
|
27296
|
+
if (!isPersistedTunnel(obj)) return null;
|
|
27297
|
+
return obj;
|
|
27298
|
+
} catch (err) {
|
|
27299
|
+
const code = err?.code;
|
|
27300
|
+
if (code === "ENOENT") return null;
|
|
27301
|
+
return null;
|
|
27302
|
+
}
|
|
27303
|
+
}
|
|
27304
|
+
async set(v2) {
|
|
27305
|
+
const dir = import_node_path21.default.dirname(this.filePath);
|
|
27306
|
+
await import_node_fs19.default.promises.mkdir(dir, { recursive: true });
|
|
27307
|
+
const data = JSON.stringify(v2, null, 2);
|
|
27308
|
+
const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
27309
|
+
await import_node_fs19.default.promises.writeFile(tmp, data, { mode: 384 });
|
|
27310
|
+
if (process.platform !== "win32") {
|
|
27311
|
+
try {
|
|
27312
|
+
await import_node_fs19.default.promises.chmod(tmp, 384);
|
|
27313
|
+
} catch {
|
|
27314
|
+
}
|
|
27315
|
+
}
|
|
27316
|
+
await import_node_fs19.default.promises.rename(tmp, this.filePath);
|
|
27317
|
+
}
|
|
27318
|
+
async clear() {
|
|
27319
|
+
try {
|
|
27320
|
+
await import_node_fs19.default.promises.unlink(this.filePath);
|
|
27321
|
+
} catch (err) {
|
|
27322
|
+
const code = err?.code;
|
|
27323
|
+
if (code !== "ENOENT") throw err;
|
|
27324
|
+
}
|
|
27325
|
+
}
|
|
27326
|
+
};
|
|
27327
|
+
function isPersistedTunnel(o) {
|
|
27328
|
+
if (!o || typeof o !== "object") return false;
|
|
27329
|
+
const r = o;
|
|
27330
|
+
return typeof r.subdomain === "string" && typeof r.frpsHost === "string" && typeof r.frpsPort === "number" && typeof r.frpsToken === "string" && typeof r.url === "string" && typeof r.registeredAt === "string";
|
|
27331
|
+
}
|
|
27332
|
+
|
|
27333
|
+
// src/tunnel/register-client.ts
|
|
27334
|
+
var TunnelRegisterError = class extends Error {
|
|
27335
|
+
constructor(message, cause) {
|
|
27336
|
+
super(message);
|
|
27337
|
+
this.cause = cause;
|
|
27338
|
+
this.name = "TunnelRegisterError";
|
|
27339
|
+
}
|
|
27340
|
+
cause;
|
|
27341
|
+
};
|
|
27342
|
+
async function registerTunnel(opts) {
|
|
27343
|
+
const f = opts.fetchImpl ?? globalThis.fetch;
|
|
27344
|
+
if (!f) throw new TunnelRegisterError("fetch is not available (Node >= 18 required)");
|
|
27345
|
+
const url = `${opts.api.replace(/\/+$/, "")}/api/tunnel/register`;
|
|
27346
|
+
const ctrl = new AbortController();
|
|
27347
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
|
|
27348
|
+
let res;
|
|
27349
|
+
try {
|
|
27350
|
+
res = await f(url, {
|
|
27351
|
+
method: "POST",
|
|
27352
|
+
headers: { "content-type": "application/json" },
|
|
27353
|
+
body: JSON.stringify({ gatewayCount: 1, deviceKeys: [opts.deviceKey] }),
|
|
27354
|
+
signal: ctrl.signal
|
|
27355
|
+
});
|
|
27356
|
+
} catch (err) {
|
|
27357
|
+
clearTimeout(timer);
|
|
27358
|
+
throw new TunnelRegisterError(`tunnel register network error: ${err.message}`, err);
|
|
27359
|
+
}
|
|
27360
|
+
clearTimeout(timer);
|
|
27361
|
+
if (!res.ok) {
|
|
27362
|
+
let bodyText = "";
|
|
26145
27363
|
try {
|
|
26146
27364
|
bodyText = (await res.text()).slice(0, 200);
|
|
26147
27365
|
} catch {
|
|
@@ -26194,9 +27412,9 @@ function escape(v2) {
|
|
|
26194
27412
|
}
|
|
26195
27413
|
|
|
26196
27414
|
// src/tunnel/frpc-binary.ts
|
|
26197
|
-
var
|
|
27415
|
+
var import_node_fs20 = __toESM(require("fs"), 1);
|
|
26198
27416
|
var import_node_os9 = __toESM(require("os"), 1);
|
|
26199
|
-
var
|
|
27417
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
26200
27418
|
var import_node_child_process3 = require("child_process");
|
|
26201
27419
|
var import_node_stream2 = require("stream");
|
|
26202
27420
|
var import_promises = require("stream/promises");
|
|
@@ -26228,20 +27446,20 @@ function frpcDownloadUrl(version2, p2) {
|
|
|
26228
27446
|
}
|
|
26229
27447
|
async function ensureFrpcBinary(opts) {
|
|
26230
27448
|
if (opts.override) {
|
|
26231
|
-
if (!
|
|
27449
|
+
if (!import_node_fs20.default.existsSync(opts.override)) {
|
|
26232
27450
|
throw new Error(`frpc binary not found at override path: ${opts.override}`);
|
|
26233
27451
|
}
|
|
26234
27452
|
return opts.override;
|
|
26235
27453
|
}
|
|
26236
27454
|
const version2 = opts.version ?? FRPC_VERSION;
|
|
26237
27455
|
const platform = opts.platform ?? detectPlatform();
|
|
26238
|
-
const binDir =
|
|
26239
|
-
|
|
27456
|
+
const binDir = import_node_path22.default.join(opts.dataDir, "bin");
|
|
27457
|
+
import_node_fs20.default.mkdirSync(binDir, { recursive: true });
|
|
26240
27458
|
cleanupStaleArtifacts(binDir);
|
|
26241
|
-
const stableBin =
|
|
26242
|
-
if (
|
|
27459
|
+
const stableBin = import_node_path22.default.join(binDir, "frpc");
|
|
27460
|
+
if (import_node_fs20.default.existsSync(stableBin)) return stableBin;
|
|
26243
27461
|
const partialBin = `${stableBin}.partial`;
|
|
26244
|
-
const tarballPath =
|
|
27462
|
+
const tarballPath = import_node_path22.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
|
|
26245
27463
|
try {
|
|
26246
27464
|
const url = frpcDownloadUrl(version2, platform);
|
|
26247
27465
|
await downloadToFile(url, tarballPath, opts.fetchImpl);
|
|
@@ -26250,8 +27468,8 @@ async function ensureFrpcBinary(opts) {
|
|
|
26250
27468
|
} else {
|
|
26251
27469
|
await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
|
|
26252
27470
|
}
|
|
26253
|
-
|
|
26254
|
-
|
|
27471
|
+
import_node_fs20.default.chmodSync(partialBin, 493);
|
|
27472
|
+
import_node_fs20.default.renameSync(partialBin, stableBin);
|
|
26255
27473
|
} finally {
|
|
26256
27474
|
safeUnlink(tarballPath);
|
|
26257
27475
|
safeUnlink(partialBin);
|
|
@@ -26261,15 +27479,15 @@ async function ensureFrpcBinary(opts) {
|
|
|
26261
27479
|
function cleanupStaleArtifacts(binDir) {
|
|
26262
27480
|
let entries;
|
|
26263
27481
|
try {
|
|
26264
|
-
entries =
|
|
27482
|
+
entries = import_node_fs20.default.readdirSync(binDir);
|
|
26265
27483
|
} catch {
|
|
26266
27484
|
return;
|
|
26267
27485
|
}
|
|
26268
27486
|
for (const name of entries) {
|
|
26269
27487
|
if (name.endsWith(".partial") || name.startsWith("extract-")) {
|
|
26270
|
-
const full =
|
|
27488
|
+
const full = import_node_path22.default.join(binDir, name);
|
|
26271
27489
|
try {
|
|
26272
|
-
|
|
27490
|
+
import_node_fs20.default.rmSync(full, { recursive: true, force: true });
|
|
26273
27491
|
} catch {
|
|
26274
27492
|
}
|
|
26275
27493
|
}
|
|
@@ -26277,7 +27495,7 @@ function cleanupStaleArtifacts(binDir) {
|
|
|
26277
27495
|
}
|
|
26278
27496
|
function safeUnlink(p2) {
|
|
26279
27497
|
try {
|
|
26280
|
-
|
|
27498
|
+
import_node_fs20.default.unlinkSync(p2);
|
|
26281
27499
|
} catch {
|
|
26282
27500
|
}
|
|
26283
27501
|
}
|
|
@@ -26288,13 +27506,13 @@ async function downloadToFile(url, dest, fetchImpl) {
|
|
|
26288
27506
|
if (!res.ok || !res.body) {
|
|
26289
27507
|
throw new Error(`download failed: ${res.status} ${res.statusText}`);
|
|
26290
27508
|
}
|
|
26291
|
-
const out =
|
|
27509
|
+
const out = import_node_fs20.default.createWriteStream(dest);
|
|
26292
27510
|
const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
|
|
26293
27511
|
await (0, import_promises.pipeline)(nodeStream, out);
|
|
26294
27512
|
}
|
|
26295
27513
|
async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
|
|
26296
|
-
const work =
|
|
26297
|
-
|
|
27514
|
+
const work = import_node_path22.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
|
|
27515
|
+
import_node_fs20.default.mkdirSync(work, { recursive: true });
|
|
26298
27516
|
try {
|
|
26299
27517
|
await new Promise((resolve2, reject) => {
|
|
26300
27518
|
const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
|
|
@@ -26302,32 +27520,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
|
|
|
26302
27520
|
proc.on("exit", (code) => code === 0 ? resolve2() : reject(new Error(`tar exited ${code}`)));
|
|
26303
27521
|
});
|
|
26304
27522
|
const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
|
|
26305
|
-
const src =
|
|
26306
|
-
if (!
|
|
27523
|
+
const src = import_node_path22.default.join(work, dirName, "frpc");
|
|
27524
|
+
if (!import_node_fs20.default.existsSync(src)) {
|
|
26307
27525
|
throw new Error(`frpc not found inside tarball at ${src}`);
|
|
26308
27526
|
}
|
|
26309
|
-
|
|
27527
|
+
import_node_fs20.default.copyFileSync(src, destBin);
|
|
26310
27528
|
} finally {
|
|
26311
|
-
|
|
27529
|
+
import_node_fs20.default.rmSync(work, { recursive: true, force: true });
|
|
26312
27530
|
}
|
|
26313
27531
|
}
|
|
26314
27532
|
|
|
26315
27533
|
// src/tunnel/frpc-process.ts
|
|
26316
|
-
var
|
|
26317
|
-
var
|
|
27534
|
+
var import_node_fs21 = __toESM(require("fs"), 1);
|
|
27535
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
26318
27536
|
var import_node_child_process4 = require("child_process");
|
|
26319
27537
|
function frpcPidFilePath(dataDir) {
|
|
26320
|
-
return
|
|
27538
|
+
return import_node_path23.default.join(dataDir, "frpc.pid");
|
|
26321
27539
|
}
|
|
26322
27540
|
function writeFrpcPid(dataDir, pid) {
|
|
26323
27541
|
try {
|
|
26324
|
-
|
|
27542
|
+
import_node_fs21.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
|
|
26325
27543
|
} catch {
|
|
26326
27544
|
}
|
|
26327
27545
|
}
|
|
26328
27546
|
function clearFrpcPid(dataDir) {
|
|
26329
27547
|
try {
|
|
26330
|
-
|
|
27548
|
+
import_node_fs21.default.unlinkSync(frpcPidFilePath(dataDir));
|
|
26331
27549
|
} catch {
|
|
26332
27550
|
}
|
|
26333
27551
|
}
|
|
@@ -26343,7 +27561,7 @@ function defaultIsPidAlive(pid) {
|
|
|
26343
27561
|
}
|
|
26344
27562
|
function defaultReadPidFile(file) {
|
|
26345
27563
|
try {
|
|
26346
|
-
return
|
|
27564
|
+
return import_node_fs21.default.readFileSync(file, "utf8");
|
|
26347
27565
|
} catch {
|
|
26348
27566
|
return null;
|
|
26349
27567
|
}
|
|
@@ -26359,7 +27577,7 @@ function defaultSleep(ms) {
|
|
|
26359
27577
|
}
|
|
26360
27578
|
async function killStaleFrpc(deps) {
|
|
26361
27579
|
const pidFile = frpcPidFilePath(deps.dataDir);
|
|
26362
|
-
const tomlPath =
|
|
27580
|
+
const tomlPath = import_node_path23.default.join(deps.dataDir, "frpc.toml");
|
|
26363
27581
|
const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
|
|
26364
27582
|
const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
|
|
26365
27583
|
const killPid = deps.killPidImpl ?? defaultKillPid;
|
|
@@ -26383,7 +27601,7 @@ async function killStaleFrpc(deps) {
|
|
|
26383
27601
|
}
|
|
26384
27602
|
if (victims.size === 0) {
|
|
26385
27603
|
try {
|
|
26386
|
-
|
|
27604
|
+
import_node_fs21.default.unlinkSync(pidFile);
|
|
26387
27605
|
} catch {
|
|
26388
27606
|
}
|
|
26389
27607
|
return;
|
|
@@ -26394,7 +27612,7 @@ async function killStaleFrpc(deps) {
|
|
|
26394
27612
|
}
|
|
26395
27613
|
await sleep(deps.reapWaitMs ?? 300);
|
|
26396
27614
|
try {
|
|
26397
|
-
|
|
27615
|
+
import_node_fs21.default.unlinkSync(pidFile);
|
|
26398
27616
|
} catch {
|
|
26399
27617
|
}
|
|
26400
27618
|
}
|
|
@@ -26431,7 +27649,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
26431
27649
|
var TunnelManager = class {
|
|
26432
27650
|
constructor(deps) {
|
|
26433
27651
|
this.deps = deps;
|
|
26434
|
-
this.store = deps.store ?? new TunnelStore(
|
|
27652
|
+
this.store = deps.store ?? new TunnelStore(import_node_path24.default.join(deps.dataDir, "tunnel.json"));
|
|
26435
27653
|
this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
|
|
26436
27654
|
this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
|
|
26437
27655
|
}
|
|
@@ -26558,8 +27776,8 @@ var TunnelManager = class {
|
|
|
26558
27776
|
dataDir: this.deps.dataDir,
|
|
26559
27777
|
override: this.deps.frpcBinaryOverride ?? void 0
|
|
26560
27778
|
});
|
|
26561
|
-
const tomlPath =
|
|
26562
|
-
const proxyName = `clawd-${t.subdomain}-${localPort}-${
|
|
27779
|
+
const tomlPath = import_node_path24.default.join(this.deps.dataDir, "frpc.toml");
|
|
27780
|
+
const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
|
|
26563
27781
|
const toml = buildFrpcToml({
|
|
26564
27782
|
serverAddr: t.frpsHost,
|
|
26565
27783
|
serverPort: t.frpsPort,
|
|
@@ -26569,12 +27787,12 @@ var TunnelManager = class {
|
|
|
26569
27787
|
localPort,
|
|
26570
27788
|
logLevel: "info"
|
|
26571
27789
|
});
|
|
26572
|
-
await
|
|
27790
|
+
await import_node_fs22.default.promises.writeFile(tomlPath, toml, { mode: 384 });
|
|
26573
27791
|
const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
|
|
26574
27792
|
stdio: ["ignore", "pipe", "pipe"]
|
|
26575
27793
|
});
|
|
26576
|
-
const logFilePath =
|
|
26577
|
-
const logStream =
|
|
27794
|
+
const logFilePath = import_node_path24.default.join(this.deps.dataDir, "frpc.log");
|
|
27795
|
+
const logStream = import_node_fs22.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
|
|
26578
27796
|
logStream.on("error", () => {
|
|
26579
27797
|
});
|
|
26580
27798
|
const tee = (chunk) => {
|
|
@@ -26657,22 +27875,22 @@ async function waitForFrpcReady(proc, timeoutMs) {
|
|
|
26657
27875
|
|
|
26658
27876
|
// src/tunnel/device-key.ts
|
|
26659
27877
|
var import_node_os10 = __toESM(require("os"), 1);
|
|
26660
|
-
var
|
|
27878
|
+
var import_node_crypto8 = __toESM(require("crypto"), 1);
|
|
26661
27879
|
var DERIVE_SALT = "clawd-tunnel-device-v1";
|
|
26662
27880
|
function deriveStableDeviceKey(opts = {}) {
|
|
26663
27881
|
const hostname = opts.hostname ?? import_node_os10.default.hostname();
|
|
26664
27882
|
const uid = opts.uid ?? (typeof import_node_os10.default.userInfo === "function" ? import_node_os10.default.userInfo().uid : 0);
|
|
26665
27883
|
const input = `${hostname}::${uid}`;
|
|
26666
|
-
return
|
|
27884
|
+
return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
|
|
26667
27885
|
}
|
|
26668
27886
|
|
|
26669
27887
|
// src/auth-store.ts
|
|
26670
|
-
var
|
|
26671
|
-
var
|
|
26672
|
-
var
|
|
27888
|
+
var import_node_fs23 = __toESM(require("fs"), 1);
|
|
27889
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
27890
|
+
var import_node_crypto9 = __toESM(require("crypto"), 1);
|
|
26673
27891
|
var AUTH_FILE_NAME = "auth.json";
|
|
26674
27892
|
function authFilePath(dataDir) {
|
|
26675
|
-
return
|
|
27893
|
+
return import_node_path25.default.join(dataDir, AUTH_FILE_NAME);
|
|
26676
27894
|
}
|
|
26677
27895
|
function loadOrCreateAuthToken(opts) {
|
|
26678
27896
|
const file = authFilePath(opts.dataDir);
|
|
@@ -26684,11 +27902,11 @@ function loadOrCreateAuthToken(opts) {
|
|
|
26684
27902
|
return token;
|
|
26685
27903
|
}
|
|
26686
27904
|
function defaultGenerate() {
|
|
26687
|
-
return
|
|
27905
|
+
return import_node_crypto9.default.randomBytes(32).toString("base64url");
|
|
26688
27906
|
}
|
|
26689
27907
|
function readAuthFile(file) {
|
|
26690
27908
|
try {
|
|
26691
|
-
const raw =
|
|
27909
|
+
const raw = import_node_fs23.default.readFileSync(file, "utf8");
|
|
26692
27910
|
const parsed = JSON.parse(raw);
|
|
26693
27911
|
if (typeof parsed?.token === "string" && parsed.token.length > 0) {
|
|
26694
27912
|
return {
|
|
@@ -26704,25 +27922,25 @@ function readAuthFile(file) {
|
|
|
26704
27922
|
}
|
|
26705
27923
|
}
|
|
26706
27924
|
function writeAuthFile(file, content) {
|
|
26707
|
-
|
|
26708
|
-
|
|
27925
|
+
import_node_fs23.default.mkdirSync(import_node_path25.default.dirname(file), { recursive: true });
|
|
27926
|
+
import_node_fs23.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
|
|
26709
27927
|
try {
|
|
26710
|
-
|
|
27928
|
+
import_node_fs23.default.chmodSync(file, 384);
|
|
26711
27929
|
} catch {
|
|
26712
27930
|
}
|
|
26713
27931
|
}
|
|
26714
27932
|
|
|
26715
27933
|
// src/owner-profile.ts
|
|
26716
|
-
var
|
|
27934
|
+
var import_node_fs24 = __toESM(require("fs"), 1);
|
|
26717
27935
|
var import_node_os11 = __toESM(require("os"), 1);
|
|
26718
|
-
var
|
|
27936
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
26719
27937
|
var PROFILE_FILENAME = "profile.json";
|
|
26720
27938
|
function loadOwnerDisplayName(dataDir) {
|
|
26721
27939
|
const fallback = import_node_os11.default.userInfo().username;
|
|
26722
|
-
const profilePath =
|
|
27940
|
+
const profilePath = import_node_path26.default.join(dataDir, PROFILE_FILENAME);
|
|
26723
27941
|
let raw;
|
|
26724
27942
|
try {
|
|
26725
|
-
raw =
|
|
27943
|
+
raw = import_node_fs24.default.readFileSync(profilePath, "utf8");
|
|
26726
27944
|
} catch {
|
|
26727
27945
|
return fallback;
|
|
26728
27946
|
}
|
|
@@ -26751,12 +27969,12 @@ init_protocol();
|
|
|
26751
27969
|
init_protocol();
|
|
26752
27970
|
|
|
26753
27971
|
// src/session/fork.ts
|
|
26754
|
-
var
|
|
27972
|
+
var import_node_fs25 = __toESM(require("fs"), 1);
|
|
26755
27973
|
var import_node_os12 = __toESM(require("os"), 1);
|
|
26756
|
-
var
|
|
27974
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
26757
27975
|
init_claude_history();
|
|
26758
27976
|
function readJsonlEntries(file) {
|
|
26759
|
-
const raw =
|
|
27977
|
+
const raw = import_node_fs25.default.readFileSync(file, "utf8");
|
|
26760
27978
|
const out = [];
|
|
26761
27979
|
for (const line of raw.split("\n")) {
|
|
26762
27980
|
const t = line.trim();
|
|
@@ -26769,10 +27987,10 @@ function readJsonlEntries(file) {
|
|
|
26769
27987
|
return out;
|
|
26770
27988
|
}
|
|
26771
27989
|
function forkSession(input) {
|
|
26772
|
-
const baseDir = input.baseDir ??
|
|
26773
|
-
const projectDir =
|
|
26774
|
-
const sourceFile =
|
|
26775
|
-
if (!
|
|
27990
|
+
const baseDir = input.baseDir ?? import_node_path27.default.join(import_node_os12.default.homedir(), ".claude");
|
|
27991
|
+
const projectDir = import_node_path27.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
|
|
27992
|
+
const sourceFile = import_node_path27.default.join(projectDir, `${input.toolSessionId}.jsonl`);
|
|
27993
|
+
if (!import_node_fs25.default.existsSync(sourceFile)) {
|
|
26776
27994
|
throw new Error(`fork: source transcript not found: ${sourceFile}`);
|
|
26777
27995
|
}
|
|
26778
27996
|
const entries = readJsonlEntries(sourceFile);
|
|
@@ -26802,9 +28020,9 @@ function forkSession(input) {
|
|
|
26802
28020
|
}
|
|
26803
28021
|
forkedLines.push(JSON.stringify(forked));
|
|
26804
28022
|
}
|
|
26805
|
-
const forkedFilePath =
|
|
26806
|
-
|
|
26807
|
-
|
|
28023
|
+
const forkedFilePath = import_node_path27.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
|
|
28024
|
+
import_node_fs25.default.mkdirSync(projectDir, { recursive: true });
|
|
28025
|
+
import_node_fs25.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
|
|
26808
28026
|
return { forkedToolSessionId, forkedFilePath };
|
|
26809
28027
|
}
|
|
26810
28028
|
|
|
@@ -27125,9 +28343,9 @@ init_protocol();
|
|
|
27125
28343
|
|
|
27126
28344
|
// src/workspace/git.ts
|
|
27127
28345
|
var import_node_child_process6 = require("child_process");
|
|
27128
|
-
var
|
|
28346
|
+
var import_node_fs26 = __toESM(require("fs"), 1);
|
|
27129
28347
|
var import_node_os13 = __toESM(require("os"), 1);
|
|
27130
|
-
var
|
|
28348
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
27131
28349
|
var import_node_util = require("util");
|
|
27132
28350
|
var pexec = (0, import_node_util.promisify)(import_node_child_process6.execFile);
|
|
27133
28351
|
function formatChildProcessError(err) {
|
|
@@ -27142,9 +28360,9 @@ function formatChildProcessError(err) {
|
|
|
27142
28360
|
return e.message ?? "unknown error";
|
|
27143
28361
|
}
|
|
27144
28362
|
function normalizePath(p2) {
|
|
27145
|
-
const resolved =
|
|
28363
|
+
const resolved = import_node_path28.default.resolve(p2);
|
|
27146
28364
|
try {
|
|
27147
|
-
return
|
|
28365
|
+
return import_node_fs26.default.realpathSync(resolved);
|
|
27148
28366
|
} catch {
|
|
27149
28367
|
return resolved;
|
|
27150
28368
|
}
|
|
@@ -27245,13 +28463,13 @@ function flattenToDirName(branch) {
|
|
|
27245
28463
|
}
|
|
27246
28464
|
function encodeClaudeProjectDir(absPath) {
|
|
27247
28465
|
if (!absPath || typeof absPath !== "string") return "";
|
|
27248
|
-
let canonical =
|
|
28466
|
+
let canonical = import_node_path28.default.resolve(absPath);
|
|
27249
28467
|
try {
|
|
27250
|
-
canonical =
|
|
28468
|
+
canonical = import_node_fs26.default.realpathSync(canonical);
|
|
27251
28469
|
} catch {
|
|
27252
28470
|
try {
|
|
27253
|
-
const parent =
|
|
27254
|
-
canonical =
|
|
28471
|
+
const parent = import_node_fs26.default.realpathSync(import_node_path28.default.dirname(canonical));
|
|
28472
|
+
canonical = import_node_path28.default.join(parent, import_node_path28.default.basename(canonical));
|
|
27255
28473
|
} catch {
|
|
27256
28474
|
}
|
|
27257
28475
|
}
|
|
@@ -27275,11 +28493,11 @@ async function createWorktree(input) {
|
|
|
27275
28493
|
if (!isGitRoot) {
|
|
27276
28494
|
throw new Error(`\u76EE\u5F55 ${cwd} \u4E0D\u662F git repo \u6839`);
|
|
27277
28495
|
}
|
|
27278
|
-
const parent =
|
|
27279
|
-
if (parent === "/" || parent ===
|
|
28496
|
+
const parent = import_node_path28.default.dirname(import_node_path28.default.resolve(cwd));
|
|
28497
|
+
if (parent === "/" || parent === import_node_path28.default.resolve(cwd)) {
|
|
27280
28498
|
throw new Error("repo \u5728\u78C1\u76D8\u6839\u76EE\u5F55\uFF0C\u65E0\u6CD5\u5728\u540C\u7EA7\u521B\u5EFA worktree");
|
|
27281
28499
|
}
|
|
27282
|
-
const worktreeRoot =
|
|
28500
|
+
const worktreeRoot = import_node_path28.default.join(parent, dirName);
|
|
27283
28501
|
try {
|
|
27284
28502
|
await pexec("git", ["-C", cwd, "fetch", "origin", baseBranch, "--no-tags"], {
|
|
27285
28503
|
timeout: 3e4
|
|
@@ -27298,7 +28516,7 @@ async function createWorktree(input) {
|
|
|
27298
28516
|
const msg = err.message;
|
|
27299
28517
|
if (msg.startsWith("\u5206\u652F ")) throw err;
|
|
27300
28518
|
}
|
|
27301
|
-
if (
|
|
28519
|
+
if (import_node_fs26.default.existsSync(worktreeRoot)) {
|
|
27302
28520
|
throw new Error(`\u76EE\u5F55 ${worktreeRoot} \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E00\u4E2A label \u6216\u6E05\u7406\u540E\u91CD\u8BD5`);
|
|
27303
28521
|
}
|
|
27304
28522
|
try {
|
|
@@ -27326,8 +28544,8 @@ async function removeWorktree(input) {
|
|
|
27326
28544
|
);
|
|
27327
28545
|
const gitCommonDir = stdout.trim();
|
|
27328
28546
|
if (!gitCommonDir) throw new Error("empty git-common-dir");
|
|
27329
|
-
const absGitCommon =
|
|
27330
|
-
repoRoot =
|
|
28547
|
+
const absGitCommon = import_node_path28.default.isAbsolute(gitCommonDir) ? gitCommonDir : import_node_path28.default.resolve(worktreeRoot, gitCommonDir);
|
|
28548
|
+
repoRoot = import_node_path28.default.dirname(absGitCommon);
|
|
27331
28549
|
} catch {
|
|
27332
28550
|
repoRoot = null;
|
|
27333
28551
|
}
|
|
@@ -27339,7 +28557,7 @@ async function removeWorktree(input) {
|
|
|
27339
28557
|
} catch (err) {
|
|
27340
28558
|
const stderr = err.stderr ?? "";
|
|
27341
28559
|
const lower = stderr.toLowerCase();
|
|
27342
|
-
const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !
|
|
28560
|
+
const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !import_node_fs26.default.existsSync(worktreeRoot);
|
|
27343
28561
|
if (!vanished) {
|
|
27344
28562
|
throw new Error(`\u6E05\u7406 worktree \u5931\u8D25\uFF1A${formatChildProcessError(err)}`);
|
|
27345
28563
|
}
|
|
@@ -27358,10 +28576,10 @@ async function removeWorktree(input) {
|
|
|
27358
28576
|
try {
|
|
27359
28577
|
const encoded = encodeClaudeProjectDir(worktreeRoot);
|
|
27360
28578
|
if (encoded) {
|
|
27361
|
-
const projectsRoot =
|
|
27362
|
-
const target =
|
|
27363
|
-
if (target.startsWith(projectsRoot +
|
|
27364
|
-
|
|
28579
|
+
const projectsRoot = import_node_path28.default.join(import_node_os13.default.homedir(), ".claude", "projects");
|
|
28580
|
+
const target = import_node_path28.default.resolve(projectsRoot, encoded);
|
|
28581
|
+
if (target.startsWith(projectsRoot + import_node_path28.default.sep) && target !== projectsRoot) {
|
|
28582
|
+
import_node_fs26.default.rmSync(target, { recursive: true, force: true });
|
|
27365
28583
|
}
|
|
27366
28584
|
}
|
|
27367
28585
|
} catch {
|
|
@@ -27440,7 +28658,7 @@ init_protocol();
|
|
|
27440
28658
|
var version = "0.2.6".length > 0 ? "0.2.6" : "dev";
|
|
27441
28659
|
|
|
27442
28660
|
// src/handlers/meta.ts
|
|
27443
|
-
function buildReadyFrame(deps) {
|
|
28661
|
+
function buildReadyFrame(deps, client) {
|
|
27444
28662
|
const info = deps.manager.info();
|
|
27445
28663
|
const tools = [];
|
|
27446
28664
|
for (const id of listRegistered()) {
|
|
@@ -27452,6 +28670,14 @@ function buildReadyFrame(deps) {
|
|
|
27452
28670
|
}
|
|
27453
28671
|
}
|
|
27454
28672
|
const tunnelUrl = deps.getTunnelUrl ? deps.getTunnelUrl() : null;
|
|
28673
|
+
const fileSharing = {};
|
|
28674
|
+
const httpBaseUrl = deps.getHttpBaseUrl ? deps.getHttpBaseUrl() : null;
|
|
28675
|
+
if (httpBaseUrl) {
|
|
28676
|
+
fileSharing.tokenRole = "owner";
|
|
28677
|
+
fileSharing.isLoopback = isLoopbackAddr(client?.remoteAddress);
|
|
28678
|
+
fileSharing.httpBaseUrl = httpBaseUrl;
|
|
28679
|
+
if (deps.httpToken) fileSharing.httpToken = deps.httpToken;
|
|
28680
|
+
}
|
|
27455
28681
|
return {
|
|
27456
28682
|
version,
|
|
27457
28683
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -27460,7 +28686,8 @@ function buildReadyFrame(deps) {
|
|
|
27460
28686
|
tools,
|
|
27461
28687
|
runningSessions: info.runningSessions,
|
|
27462
28688
|
tunnelUrl,
|
|
27463
|
-
mode: deps.mode
|
|
28689
|
+
mode: deps.mode,
|
|
28690
|
+
...fileSharing
|
|
27464
28691
|
};
|
|
27465
28692
|
}
|
|
27466
28693
|
function buildMetaHandlers(deps) {
|
|
@@ -27571,6 +28798,200 @@ function buildPersonaHandlers(deps) {
|
|
|
27571
28798
|
};
|
|
27572
28799
|
}
|
|
27573
28800
|
|
|
28801
|
+
// src/handlers/attachment.ts
|
|
28802
|
+
init_protocol();
|
|
28803
|
+
init_protocol();
|
|
28804
|
+
function buildAttachmentHandlers(deps) {
|
|
28805
|
+
const outboxCreate = async (frame) => {
|
|
28806
|
+
const parsed = AttachmentOutboxCreateArgs.safeParse(frame);
|
|
28807
|
+
if (!parsed.success) {
|
|
28808
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28809
|
+
}
|
|
28810
|
+
const args = parsed.data;
|
|
28811
|
+
if (args.personaId && args.sessionId || !args.personaId && !args.sessionId) {
|
|
28812
|
+
throw new ClawdError(
|
|
28813
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
28814
|
+
"outboxCreate requires exactly one of personaId / sessionId"
|
|
28815
|
+
);
|
|
28816
|
+
}
|
|
28817
|
+
const scopeRef = deps.getPersonaScopeForRequest(args);
|
|
28818
|
+
if (!scopeRef) {
|
|
28819
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "invalid scope");
|
|
28820
|
+
}
|
|
28821
|
+
const name = args.absPath.split("/").pop() || args.absPath;
|
|
28822
|
+
const entry = deps.outboxStore.mint({
|
|
28823
|
+
scopeRef,
|
|
28824
|
+
absPath: args.absPath,
|
|
28825
|
+
name,
|
|
28826
|
+
ttlSeconds: args.ttlSeconds,
|
|
28827
|
+
oneShot: args.oneShot,
|
|
28828
|
+
scope: args.scope
|
|
28829
|
+
});
|
|
28830
|
+
const httpBaseUrl = deps.getHttpBaseUrl();
|
|
28831
|
+
const urlPath = scopeRef.kind === "persona" ? `/persona/${encodeURIComponent(scopeRef.personaId)}/outbox/${entry.capToken}` : `/outbox/${entry.capToken}`;
|
|
28832
|
+
const url = httpBaseUrl ? `${httpBaseUrl}${urlPath}` : urlPath;
|
|
28833
|
+
return {
|
|
28834
|
+
response: {
|
|
28835
|
+
type: "attachment.outboxCreate",
|
|
28836
|
+
capToken: entry.capToken,
|
|
28837
|
+
url,
|
|
28838
|
+
expiresAt: entry.expiresAt
|
|
28839
|
+
}
|
|
28840
|
+
};
|
|
28841
|
+
};
|
|
28842
|
+
const outboxRevoke = async (frame) => {
|
|
28843
|
+
const parsed = AttachmentOutboxRevokeArgs.safeParse(frame);
|
|
28844
|
+
if (!parsed.success) {
|
|
28845
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28846
|
+
}
|
|
28847
|
+
const ok = deps.outboxStore.revoke(parsed.data.capToken);
|
|
28848
|
+
if (!ok) {
|
|
28849
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `capToken not found / already revoked`);
|
|
28850
|
+
}
|
|
28851
|
+
return { response: { type: "attachment.outboxRevoke", revoked: true } };
|
|
28852
|
+
};
|
|
28853
|
+
const outboxList = async (frame) => {
|
|
28854
|
+
const parsed = AttachmentOutboxListArgs.safeParse(frame);
|
|
28855
|
+
if (!parsed.success) {
|
|
28856
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28857
|
+
}
|
|
28858
|
+
const entries = parsed.data.personaId ? deps.outboxStore.listByPersona(parsed.data.personaId) : deps.outboxStore.list();
|
|
28859
|
+
return {
|
|
28860
|
+
response: { type: "attachment.outboxList", entries }
|
|
28861
|
+
};
|
|
28862
|
+
};
|
|
28863
|
+
const mountAdd = async (frame) => {
|
|
28864
|
+
if (!deps.mountStore) {
|
|
28865
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "mountStore not wired");
|
|
28866
|
+
}
|
|
28867
|
+
const parsed = AttachmentMountAddArgs.safeParse(frame);
|
|
28868
|
+
if (!parsed.success) {
|
|
28869
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28870
|
+
}
|
|
28871
|
+
const args = parsed.data;
|
|
28872
|
+
try {
|
|
28873
|
+
const entry = deps.mountStore.add(args.personaId, {
|
|
28874
|
+
absPath: args.absPath,
|
|
28875
|
+
mode: args.mode
|
|
28876
|
+
});
|
|
28877
|
+
return {
|
|
28878
|
+
response: {
|
|
28879
|
+
type: "attachment.mountAdd",
|
|
28880
|
+
entry,
|
|
28881
|
+
sandboxRestartNeeded: true
|
|
28882
|
+
}
|
|
28883
|
+
};
|
|
28884
|
+
} catch (err) {
|
|
28885
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, err.message);
|
|
28886
|
+
}
|
|
28887
|
+
};
|
|
28888
|
+
const mountRemove = async (frame) => {
|
|
28889
|
+
if (!deps.mountStore) {
|
|
28890
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "mountStore not wired");
|
|
28891
|
+
}
|
|
28892
|
+
const parsed = AttachmentMountRemoveArgs.safeParse(frame);
|
|
28893
|
+
if (!parsed.success) {
|
|
28894
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28895
|
+
}
|
|
28896
|
+
const ok = deps.mountStore.remove(parsed.data.personaId, parsed.data.basename);
|
|
28897
|
+
if (!ok) {
|
|
28898
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "mount not found");
|
|
28899
|
+
}
|
|
28900
|
+
return { response: { type: "attachment.mountRemove", removed: true } };
|
|
28901
|
+
};
|
|
28902
|
+
const mountList = async (frame) => {
|
|
28903
|
+
if (!deps.mountStore) {
|
|
28904
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "mountStore not wired");
|
|
28905
|
+
}
|
|
28906
|
+
const parsed = AttachmentMountListArgs.safeParse(frame);
|
|
28907
|
+
if (!parsed.success) {
|
|
28908
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28909
|
+
}
|
|
28910
|
+
const entries = deps.mountStore.list(parsed.data.personaId);
|
|
28911
|
+
return { response: { type: "attachment.mountList", entries } };
|
|
28912
|
+
};
|
|
28913
|
+
const groupAdd = async (frame) => {
|
|
28914
|
+
if (!deps.groupFileStore || !deps.getSessionScope) {
|
|
28915
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
|
|
28916
|
+
}
|
|
28917
|
+
const parsed = AttachmentGroupAddArgs.safeParse(frame);
|
|
28918
|
+
if (!parsed.success) {
|
|
28919
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28920
|
+
}
|
|
28921
|
+
const args = parsed.data;
|
|
28922
|
+
const scope = deps.getSessionScope(args.sessionId);
|
|
28923
|
+
if (!scope) {
|
|
28924
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `session ${args.sessionId} not found`);
|
|
28925
|
+
}
|
|
28926
|
+
const size = 0;
|
|
28927
|
+
const entry = deps.groupFileStore.upsert(scope, args.sessionId, {
|
|
28928
|
+
relPath: args.relPath,
|
|
28929
|
+
from: "owner",
|
|
28930
|
+
label: args.label,
|
|
28931
|
+
size,
|
|
28932
|
+
mime: lookupMime(args.relPath)
|
|
28933
|
+
});
|
|
28934
|
+
return { response: { type: "attachment.groupAdd", entry } };
|
|
28935
|
+
};
|
|
28936
|
+
const groupRemove = async (frame) => {
|
|
28937
|
+
if (!deps.groupFileStore || !deps.getSessionScope) {
|
|
28938
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
|
|
28939
|
+
}
|
|
28940
|
+
const parsed = AttachmentGroupRemoveArgs.safeParse(frame);
|
|
28941
|
+
if (!parsed.success) {
|
|
28942
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28943
|
+
}
|
|
28944
|
+
const scope = deps.getSessionScope(parsed.data.sessionId);
|
|
28945
|
+
if (!scope) {
|
|
28946
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "session not found");
|
|
28947
|
+
}
|
|
28948
|
+
const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
|
|
28949
|
+
const target = entries.find((e) => e.relPath === parsed.data.relPath);
|
|
28950
|
+
if (target?.from === "owner") {
|
|
28951
|
+
deps.groupFileStore.remove(scope, parsed.data.sessionId, parsed.data.relPath);
|
|
28952
|
+
} else {
|
|
28953
|
+
deps.groupFileStore.markStale(scope, parsed.data.sessionId, parsed.data.relPath);
|
|
28954
|
+
}
|
|
28955
|
+
return { response: { type: "attachment.groupRemove", removed: true } };
|
|
28956
|
+
};
|
|
28957
|
+
const groupList = async (frame) => {
|
|
28958
|
+
if (!deps.groupFileStore || !deps.getSessionScope) {
|
|
28959
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
|
|
28960
|
+
}
|
|
28961
|
+
const parsed = AttachmentGroupListArgs.safeParse(frame);
|
|
28962
|
+
if (!parsed.success) {
|
|
28963
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28964
|
+
}
|
|
28965
|
+
const scope = deps.getSessionScope(parsed.data.sessionId);
|
|
28966
|
+
if (!scope) return { response: { type: "attachment.groupList", entries: [] } };
|
|
28967
|
+
const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
|
|
28968
|
+
return { response: { type: "attachment.groupList", entries } };
|
|
28969
|
+
};
|
|
28970
|
+
const groupListPersona = async (frame) => {
|
|
28971
|
+
if (!deps.groupFileStore) {
|
|
28972
|
+
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
|
|
28973
|
+
}
|
|
28974
|
+
const parsed = AttachmentGroupListPersonaArgs.safeParse(frame);
|
|
28975
|
+
if (!parsed.success) {
|
|
28976
|
+
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
28977
|
+
}
|
|
28978
|
+
const perSession = deps.groupFileStore.listByPersona(parsed.data.personaId);
|
|
28979
|
+
return { response: { type: "attachment.groupListPersona", perSession } };
|
|
28980
|
+
};
|
|
28981
|
+
return {
|
|
28982
|
+
"attachment.outboxCreate": outboxCreate,
|
|
28983
|
+
"attachment.outboxRevoke": outboxRevoke,
|
|
28984
|
+
"attachment.outboxList": outboxList,
|
|
28985
|
+
"attachment.mountAdd": mountAdd,
|
|
28986
|
+
"attachment.mountRemove": mountRemove,
|
|
28987
|
+
"attachment.mountList": mountList,
|
|
28988
|
+
"attachment.groupAdd": groupAdd,
|
|
28989
|
+
"attachment.groupRemove": groupRemove,
|
|
28990
|
+
"attachment.groupList": groupList,
|
|
28991
|
+
"attachment.groupListPersona": groupListPersona
|
|
28992
|
+
};
|
|
28993
|
+
}
|
|
28994
|
+
|
|
27574
28995
|
// src/handlers/index.ts
|
|
27575
28996
|
function buildMethodHandlers(deps) {
|
|
27576
28997
|
return {
|
|
@@ -27586,7 +29007,8 @@ function buildMethodHandlers(deps) {
|
|
|
27586
29007
|
personaRegistry: deps.personaRegistry,
|
|
27587
29008
|
sessionManager: deps.manager,
|
|
27588
29009
|
personaBoundHandler: deps.personaBoundHandler
|
|
27589
|
-
})
|
|
29010
|
+
}),
|
|
29011
|
+
...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
|
|
27590
29012
|
};
|
|
27591
29013
|
}
|
|
27592
29014
|
|
|
@@ -27594,7 +29016,7 @@ function buildMethodHandlers(deps) {
|
|
|
27594
29016
|
async function startDaemon(config) {
|
|
27595
29017
|
const logger = createLogger({
|
|
27596
29018
|
level: config.logLevel,
|
|
27597
|
-
file:
|
|
29019
|
+
file: import_node_path29.default.join(config.dataDir, "clawd.log")
|
|
27598
29020
|
});
|
|
27599
29021
|
logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
|
|
27600
29022
|
const stateMgr = new StateFileManager({ dataDir: config.dataDir });
|
|
@@ -27626,7 +29048,7 @@ async function startDaemon(config) {
|
|
|
27626
29048
|
const agents = new AgentsScanner();
|
|
27627
29049
|
const history = new ClaudeHistoryReader();
|
|
27628
29050
|
let transport = null;
|
|
27629
|
-
const personaStore = new PersonaStore(
|
|
29051
|
+
const personaStore = new PersonaStore(import_node_path29.default.join(config.dataDir, "personas"));
|
|
27630
29052
|
const defaultsRoot = findDefaultsRoot();
|
|
27631
29053
|
if (defaultsRoot) {
|
|
27632
29054
|
seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
|
|
@@ -27634,13 +29056,18 @@ async function startDaemon(config) {
|
|
|
27634
29056
|
logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
|
|
27635
29057
|
}
|
|
27636
29058
|
const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
|
|
29059
|
+
const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
|
|
29060
|
+
const outboxStore = new OutboxStore({ dataDir: config.dataDir, logger });
|
|
29061
|
+
const mountStore = new MountStore({
|
|
29062
|
+
personaDirPath: (pid) => personaStore.personaDirPath(pid)
|
|
29063
|
+
});
|
|
27637
29064
|
const manager = new SessionManager({
|
|
27638
29065
|
store,
|
|
27639
29066
|
logger,
|
|
27640
29067
|
getAdapter,
|
|
27641
29068
|
historyReader: history,
|
|
27642
29069
|
dataDir: config.dataDir,
|
|
27643
|
-
personaRoot:
|
|
29070
|
+
personaRoot: import_node_path29.default.join(config.dataDir, "personas"),
|
|
27644
29071
|
personaStore,
|
|
27645
29072
|
ownerDisplayName,
|
|
27646
29073
|
mode: config.mode,
|
|
@@ -27657,6 +29084,38 @@ async function startDaemon(config) {
|
|
|
27657
29084
|
return;
|
|
27658
29085
|
}
|
|
27659
29086
|
transport?.broadcastToSession(sid, frame);
|
|
29087
|
+
},
|
|
29088
|
+
// file-sharing (spec §6 PR 3):runner 检测到成功 file-edit tool_result 时,
|
|
29089
|
+
// 闭包 stat + mime 写入群清单。stat 失败不阻塞主流程(log warn + 跳过本条),
|
|
29090
|
+
// 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
|
|
29091
|
+
attachmentGroup: {
|
|
29092
|
+
onFileEdit: (input) => {
|
|
29093
|
+
const absPath = import_node_path29.default.isAbsolute(input.relPath) ? input.relPath : import_node_path29.default.join(input.cwd, input.relPath);
|
|
29094
|
+
let size = 0;
|
|
29095
|
+
try {
|
|
29096
|
+
size = import_node_fs27.default.statSync(absPath).size;
|
|
29097
|
+
} catch (err) {
|
|
29098
|
+
logger.warn("attachment.onFileEdit stat failed", {
|
|
29099
|
+
sessionId: input.sessionId,
|
|
29100
|
+
absPath,
|
|
29101
|
+
err: err.message
|
|
29102
|
+
});
|
|
29103
|
+
}
|
|
29104
|
+
try {
|
|
29105
|
+
groupFileStore.upsert(input.scope, input.sessionId, {
|
|
29106
|
+
relPath: input.relPath,
|
|
29107
|
+
from: "agent",
|
|
29108
|
+
size,
|
|
29109
|
+
mime: lookupMime(input.relPath)
|
|
29110
|
+
});
|
|
29111
|
+
} catch (err) {
|
|
29112
|
+
logger.warn("attachment.onFileEdit upsert failed", {
|
|
29113
|
+
sessionId: input.sessionId,
|
|
29114
|
+
relPath: input.relPath,
|
|
29115
|
+
err: err.message
|
|
29116
|
+
});
|
|
29117
|
+
}
|
|
29118
|
+
}
|
|
27660
29119
|
}
|
|
27661
29120
|
});
|
|
27662
29121
|
const observer = new SessionObserver({
|
|
@@ -27702,6 +29161,12 @@ async function startDaemon(config) {
|
|
|
27702
29161
|
sessionManager: manager
|
|
27703
29162
|
});
|
|
27704
29163
|
let currentTunnelUrl = null;
|
|
29164
|
+
const getHttpBaseUrl = () => {
|
|
29165
|
+
if (currentTunnelUrl) {
|
|
29166
|
+
return currentTunnelUrl.replace(/^wss:/i, "https:").replace(/^ws:/i, "http:");
|
|
29167
|
+
}
|
|
29168
|
+
return `http://${config.host}:${config.port}`;
|
|
29169
|
+
};
|
|
27705
29170
|
const personaBoundHandler = new PersonaBoundHandler({
|
|
27706
29171
|
registry: personaRegistry,
|
|
27707
29172
|
personaManager,
|
|
@@ -27727,22 +29192,73 @@ async function startDaemon(config) {
|
|
|
27727
29192
|
getTunnelUrl: () => currentTunnelUrl,
|
|
27728
29193
|
// ready / info 帧的 mode = daemon CC spawn 模式('sdk' | 'tui')。UI 据此挂 XtermPanel +
|
|
27729
29194
|
// 订阅 session:pty / session:control;业务帧名两种 mode 完全一致,UI 业务订阅代码不变
|
|
27730
|
-
mode: config.mode
|
|
29195
|
+
mode: config.mode,
|
|
29196
|
+
// file-sharing (spec §8):ready / info 帧把 httpBaseUrl + httpToken 下发给 UI。
|
|
29197
|
+
// PR 2 阶段 httpToken 复用 owner WS token;noAuth 模式下为 null(UI 看到无 httpToken
|
|
29198
|
+
// 时禁用文件 GET/POST,保持 1.0 行为兼容)。
|
|
29199
|
+
getHttpBaseUrl,
|
|
29200
|
+
httpToken: resolvedAuthToken,
|
|
29201
|
+
// file-sharing attachment.* RPC(spec §5 PR 6+)。getPersonaScopeForRequest 把 RPC
|
|
29202
|
+
// args 里的 personaId/sessionId 翻译成 scopeRef 给 OutboxStore;mountAdd/groupAdd
|
|
29203
|
+
// 后续 PR 复用同一份装配。
|
|
29204
|
+
attachment: {
|
|
29205
|
+
outboxStore,
|
|
29206
|
+
mountStore,
|
|
29207
|
+
groupFileStore,
|
|
29208
|
+
personaDirPath: (pid) => personaStore.personaDirPath(pid),
|
|
29209
|
+
getHttpBaseUrl,
|
|
29210
|
+
getPersonaScopeForRequest: (args) => {
|
|
29211
|
+
if (args.personaId) return { kind: "persona", personaId: args.personaId };
|
|
29212
|
+
if (args.sessionId) return { kind: "session", sessionId: args.sessionId };
|
|
29213
|
+
return null;
|
|
29214
|
+
},
|
|
29215
|
+
// group RPC:根据 sessionId 反查 scope;owner-mode persona session 走
|
|
29216
|
+
// 'persona/<pid>/owner',default 走 'default'。PR 9 UI Drawer 用。
|
|
29217
|
+
getSessionScope: (sessionId) => {
|
|
29218
|
+
const file = store.read(sessionId);
|
|
29219
|
+
if (!file) return null;
|
|
29220
|
+
if (file.ownerPersonaId) {
|
|
29221
|
+
return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
|
|
29222
|
+
}
|
|
29223
|
+
return { kind: "default" };
|
|
29224
|
+
}
|
|
29225
|
+
}
|
|
29226
|
+
});
|
|
29227
|
+
const authResolver = new AuthContextResolver({
|
|
29228
|
+
ownerToken: resolvedAuthToken,
|
|
29229
|
+
personaRegistry
|
|
29230
|
+
});
|
|
29231
|
+
const httpRouter = createHttpRouter({
|
|
29232
|
+
authResolver,
|
|
29233
|
+
daemonVersion: version,
|
|
29234
|
+
logger,
|
|
29235
|
+
personaStore,
|
|
29236
|
+
groupFileStore,
|
|
29237
|
+
sessionStore: store,
|
|
29238
|
+
outboxStore
|
|
27731
29239
|
});
|
|
27732
29240
|
wsServer = new LocalWsServer({
|
|
27733
29241
|
host: config.host,
|
|
27734
29242
|
port: config.port,
|
|
27735
29243
|
logger,
|
|
27736
|
-
readyFrameBuilder: () => buildReadyFrame(
|
|
27737
|
-
|
|
27738
|
-
|
|
27739
|
-
|
|
27740
|
-
|
|
27741
|
-
|
|
27742
|
-
|
|
29244
|
+
readyFrameBuilder: (ctx) => buildReadyFrame(
|
|
29245
|
+
{
|
|
29246
|
+
manager,
|
|
29247
|
+
getAdapter,
|
|
29248
|
+
getTunnelUrl: () => currentTunnelUrl,
|
|
29249
|
+
// ready 帧 mode = daemon CC spawn 模式('sdk' | 'tui');UI 用它挂 XtermPanel
|
|
29250
|
+
mode: config.mode,
|
|
29251
|
+
// file-sharing 字段:httpBaseUrl 跟 tunnel 状态走;httpToken 复用 owner WS token
|
|
29252
|
+
getHttpBaseUrl,
|
|
29253
|
+
httpToken: resolvedAuthToken
|
|
29254
|
+
},
|
|
29255
|
+
ctx
|
|
29256
|
+
),
|
|
27743
29257
|
protocolVersion: PROTOCOL_VERSION,
|
|
27744
29258
|
authGate: authGate ?? void 0,
|
|
27745
29259
|
personaBoundHandler,
|
|
29260
|
+
// file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
|
|
29261
|
+
httpRequestHandler: httpRouter,
|
|
27746
29262
|
// 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
|
|
27747
29263
|
// daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
|
|
27748
29264
|
// 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
|
|
@@ -27858,8 +29374,8 @@ async function startDaemon(config) {
|
|
|
27858
29374
|
const lines = [
|
|
27859
29375
|
`Tunnel: ${r.url}`,
|
|
27860
29376
|
...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
|
|
27861
|
-
`Frpc config: ${
|
|
27862
|
-
`Frpc log: ${
|
|
29377
|
+
`Frpc config: ${import_node_path29.default.join(config.dataDir, "frpc.toml")}`,
|
|
29378
|
+
`Frpc log: ${import_node_path29.default.join(config.dataDir, "frpc.log")}`
|
|
27863
29379
|
];
|
|
27864
29380
|
const width = Math.max(...lines.map((l) => l.length));
|
|
27865
29381
|
const bar = "\u2550".repeat(width + 4);
|
|
@@ -27872,8 +29388,8 @@ ${bar}
|
|
|
27872
29388
|
|
|
27873
29389
|
`);
|
|
27874
29390
|
try {
|
|
27875
|
-
const connectPath =
|
|
27876
|
-
|
|
29391
|
+
const connectPath = import_node_path29.default.join(config.dataDir, "connect.txt");
|
|
29392
|
+
import_node_fs27.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
|
|
27877
29393
|
} catch {
|
|
27878
29394
|
}
|
|
27879
29395
|
} catch (err) {
|