@messenger-agent/claude-agent 0.24.0-alpha.2 → 0.24.0-alpha.3
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/app.js +1 -15
- package/dist/claude.js +1 -1
- package/dist/config.js +1 -147
- package/dist/index.js +1 -19
- package/dist/input-queue.js +1 -40
- package/dist/live-session.js +1 -225
- package/dist/llm-proxy-bindings.js +3 -37
- package/dist/managed-task-mcp.js +1 -41
- package/dist/pending-asks.js +5 -246
- package/dist/platform-instructions.js +6 -42
- package/dist/routes/chat.js +8 -1229
- package/dist/schemas.js +1 -89
- package/dist/session-manager.js +1 -57
- package/dist/task-aggregator.js +1 -143
- package/dist/token-usage.js +1 -75
- package/dist/tunnel-client.js +1 -223
- package/dist/workspace-files.js +1 -56
- package/package.json +2 -2
package/dist/schemas.js
CHANGED
|
@@ -1,89 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
export const ChatTextPartSchema = z.object({
|
|
3
|
-
type: z.literal("text"),
|
|
4
|
-
text: z.string(),
|
|
5
|
-
});
|
|
6
|
-
export const ChatFilePartSchema = z.object({
|
|
7
|
-
type: z.literal("file"),
|
|
8
|
-
mediaType: z.string(),
|
|
9
|
-
filename: z.string().optional(),
|
|
10
|
-
url: z.string(),
|
|
11
|
-
});
|
|
12
|
-
export const ChatStepStartPartSchema = z.object({
|
|
13
|
-
type: z.literal("step-start"),
|
|
14
|
-
});
|
|
15
|
-
export const ChatToolPartSchema = z.object({
|
|
16
|
-
type: z.literal("tool"),
|
|
17
|
-
toolCallId: z.string(),
|
|
18
|
-
toolName: z.string(),
|
|
19
|
-
title: z.string().optional(),
|
|
20
|
-
state: z.string(),
|
|
21
|
-
input: z.unknown().optional(),
|
|
22
|
-
output: z.unknown().optional(),
|
|
23
|
-
});
|
|
24
|
-
export const ClaudeAskUserQuestionOutputSchema = z.object({
|
|
25
|
-
answers: z.record(z.string(), z.string()),
|
|
26
|
-
});
|
|
27
|
-
export const AskUserQuestionToolPartSchema = z.object({
|
|
28
|
-
type: z.literal("tool"),
|
|
29
|
-
toolCallId: z.string(),
|
|
30
|
-
// Accept the camelCase SDK tool names (now passed through downstream) plus the
|
|
31
|
-
// legacy snake_case aliases for backward compatibility.
|
|
32
|
-
toolName: z.enum(["AskUserQuestion", "ask_user_question"]),
|
|
33
|
-
state: z.literal("output-available"),
|
|
34
|
-
output: ClaudeAskUserQuestionOutputSchema,
|
|
35
|
-
});
|
|
36
|
-
export const ExitPlanModeToolPartSchema = z.object({
|
|
37
|
-
type: z.literal("tool"),
|
|
38
|
-
toolCallId: z.string(),
|
|
39
|
-
toolName: z.enum(["ExitPlanMode", "exit_plan_mode"]),
|
|
40
|
-
state: z.literal("output-available"),
|
|
41
|
-
output: z.unknown(),
|
|
42
|
-
});
|
|
43
|
-
export const ToolApprovalSchema = z.object({
|
|
44
|
-
id: z.string(),
|
|
45
|
-
approved: z.boolean(),
|
|
46
|
-
reason: z.string().optional(),
|
|
47
|
-
});
|
|
48
|
-
export const ToolApprovalPartSchema = z.object({
|
|
49
|
-
type: z.literal("tool"),
|
|
50
|
-
toolCallId: z.string(),
|
|
51
|
-
toolName: z.string(),
|
|
52
|
-
state: z.literal("approval-responded"),
|
|
53
|
-
input: z.unknown().optional(),
|
|
54
|
-
approval: ToolApprovalSchema,
|
|
55
|
-
});
|
|
56
|
-
export const AgentAnswerToolPartSchema = z.union([
|
|
57
|
-
AskUserQuestionToolPartSchema,
|
|
58
|
-
ExitPlanModeToolPartSchema,
|
|
59
|
-
ToolApprovalPartSchema,
|
|
60
|
-
]);
|
|
61
|
-
export const ChatCustomPartSchema = z.looseObject({
|
|
62
|
-
type: z.string(),
|
|
63
|
-
});
|
|
64
|
-
export const CodexChatForkSchema = z.strictObject({
|
|
65
|
-
threadId: z.string().min(1),
|
|
66
|
-
turnId: z.string().min(1),
|
|
67
|
-
});
|
|
68
|
-
export const ClaudeChatForkSchema = z.strictObject({
|
|
69
|
-
sessionId: z.string().min(1),
|
|
70
|
-
messageId: z.string().min(1),
|
|
71
|
-
});
|
|
72
|
-
export const ChatForkSchema = z.union([CodexChatForkSchema, ClaudeChatForkSchema]);
|
|
73
|
-
export const ChatBodySchema = z.object({
|
|
74
|
-
conversationId: z.string().optional(),
|
|
75
|
-
fork: ChatForkSchema.optional(),
|
|
76
|
-
model: z.string(),
|
|
77
|
-
message: z.object({
|
|
78
|
-
id: z.string().optional(),
|
|
79
|
-
role: z.enum(["user", "assistant"]).optional(),
|
|
80
|
-
parts: z.array(z.union([
|
|
81
|
-
ChatTextPartSchema,
|
|
82
|
-
ChatFilePartSchema,
|
|
83
|
-
ChatStepStartPartSchema,
|
|
84
|
-
AgentAnswerToolPartSchema,
|
|
85
|
-
ChatToolPartSchema,
|
|
86
|
-
ChatCustomPartSchema,
|
|
87
|
-
])),
|
|
88
|
-
}),
|
|
89
|
-
});
|
|
1
|
+
import{z as t}from"zod";const o=t.object({type:t.literal("text"),text:t.string()}),e=t.object({type:t.literal("file"),mediaType:t.string(),filename:t.string().optional(),url:t.string()}),a=t.object({type:t.literal("step-start")}),n=t.object({type:t.literal("tool"),toolCallId:t.string(),toolName:t.string(),title:t.string().optional(),state:t.string(),input:t.unknown().optional(),output:t.unknown().optional()}),r=t.object({answers:t.record(t.string(),t.string())}),s=t.object({type:t.literal("tool"),toolCallId:t.string(),toolName:t.enum(["AskUserQuestion","ask_user_question"]),state:t.literal("output-available"),output:r}),l=t.object({type:t.literal("tool"),toolCallId:t.string(),toolName:t.enum(["ExitPlanMode","exit_plan_mode"]),state:t.literal("output-available"),output:t.unknown()}),i=t.object({id:t.string(),approved:t.boolean(),reason:t.string().optional()}),p=t.object({type:t.literal("tool"),toolCallId:t.string(),toolName:t.string(),state:t.literal("approval-responded"),input:t.unknown().optional(),approval:i}),c=t.union([s,l,p]),m=t.looseObject({type:t.string()}),u=t.strictObject({threadId:t.string().min(1),turnId:t.string().min(1)}),g=t.strictObject({sessionId:t.string().min(1),messageId:t.string().min(1)}),d=t.union([u,g]),x=t.object({conversationId:t.string().optional(),fork:d.optional(),model:t.string(),message:t.object({id:t.string().optional(),role:t.enum(["user","assistant"]).optional(),parts:t.array(t.union([o,e,a,c,n,m]))})});export{c as AgentAnswerToolPartSchema,s as AskUserQuestionToolPartSchema,x as ChatBodySchema,m as ChatCustomPartSchema,e as ChatFilePartSchema,d as ChatForkSchema,a as ChatStepStartPartSchema,o as ChatTextPartSchema,n as ChatToolPartSchema,r as ClaudeAskUserQuestionOutputSchema,g as ClaudeChatForkSchema,u as CodexChatForkSchema,l as ExitPlanModeToolPartSchema,p as ToolApprovalPartSchema,i as ToolApprovalSchema};
|
package/dist/session-manager.js
CHANGED
|
@@ -1,57 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
export class SessionManager {
|
|
3
|
-
idleTimeoutMs;
|
|
4
|
-
sessions = new Map();
|
|
5
|
-
constructor(idleTimeoutMs) {
|
|
6
|
-
this.idleTimeoutMs = idleTimeoutMs;
|
|
7
|
-
}
|
|
8
|
-
get(conversationId) {
|
|
9
|
-
if (!conversationId)
|
|
10
|
-
return undefined;
|
|
11
|
-
const session = this.sessions.get(conversationId);
|
|
12
|
-
if (session?.isDead) {
|
|
13
|
-
this.removeByValue(session);
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
return session;
|
|
17
|
-
}
|
|
18
|
-
create(params) {
|
|
19
|
-
const session = new LiveSession({
|
|
20
|
-
...params,
|
|
21
|
-
idleTimeoutMs: this.idleTimeoutMs(),
|
|
22
|
-
onRecycle: (candidate, generation) => this.recycle(candidate, generation),
|
|
23
|
-
});
|
|
24
|
-
this.sessions.set(params.conversationId, session);
|
|
25
|
-
return session;
|
|
26
|
-
}
|
|
27
|
-
bind(session, conversationId) {
|
|
28
|
-
if (session.isDead)
|
|
29
|
-
return;
|
|
30
|
-
const previousId = session.conversationId;
|
|
31
|
-
session.bindConversation(conversationId);
|
|
32
|
-
this.sessions.set(conversationId, session);
|
|
33
|
-
if (previousId !== conversationId && this.sessions.get(previousId) === session)
|
|
34
|
-
this.sessions.delete(previousId);
|
|
35
|
-
}
|
|
36
|
-
cancel(conversationId) {
|
|
37
|
-
const session = this.get(conversationId);
|
|
38
|
-
if (!session)
|
|
39
|
-
return false;
|
|
40
|
-
session.abort();
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
get size() {
|
|
44
|
-
return new Set(this.sessions.values()).size;
|
|
45
|
-
}
|
|
46
|
-
recycle(session, generation) {
|
|
47
|
-
if (!session.isDead || generation !== session.currentGeneration)
|
|
48
|
-
return;
|
|
49
|
-
this.removeByValue(session);
|
|
50
|
-
}
|
|
51
|
-
removeByValue(session) {
|
|
52
|
-
for (const [key, candidate] of this.sessions) {
|
|
53
|
-
if (candidate === session)
|
|
54
|
-
this.sessions.delete(key);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
1
|
+
import{LiveSession as n}from"./live-session.js";class u{idleTimeoutMs;sessions=new Map;constructor(e){this.idleTimeoutMs=e}get(e){if(!e)return;const s=this.sessions.get(e);if(s?.isDead){this.removeByValue(s);return}return s}create(e){const s=new n({...e,idleTimeoutMs:this.idleTimeoutMs(),onRecycle:(t,i)=>this.recycle(t,i)});return this.sessions.set(e.conversationId,s),s}bind(e,s){if(e.isDead)return;const t=e.conversationId;e.bindConversation(s),this.sessions.set(s,e),t!==s&&this.sessions.get(t)===e&&this.sessions.delete(t)}cancel(e){const s=this.get(e);return s?(s.abort(),!0):!1}get size(){return new Set(this.sessions.values()).size}recycle(e,s){!e.isDead||s!==e.currentGeneration||this.removeByValue(e)}removeByValue(e){for(const[s,t]of this.sessions)t===e&&this.sessions.delete(s)}}export{u as SessionManager};
|
package/dist/task-aggregator.js
CHANGED
|
@@ -1,143 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Aggregates Claude Code's incremental task tools (TaskCreate / TaskUpdate /
|
|
3
|
-
* TaskList / TaskGet / TaskStop / TaskOutput) into a single todo list that is
|
|
4
|
-
* compatible with the legacy `TodoWrite` event shape consumed downstream:
|
|
5
|
-
*
|
|
6
|
-
* { todos: [{ content, status, activeForm? }] }
|
|
7
|
-
*
|
|
8
|
-
* Claude Code replaced the old all-at-once `TodoWrite` tool with a set of
|
|
9
|
-
* incremental CRUD tools that persist each task as a JSON file under
|
|
10
|
-
* `<claude config dir>/tasks/<session_id>/<N>.json` (N starts at 1):
|
|
11
|
-
*
|
|
12
|
-
* {
|
|
13
|
-
* "id": "1",
|
|
14
|
-
* "subject": "...",
|
|
15
|
-
* "description": "...",
|
|
16
|
-
* "activeForm": "...",
|
|
17
|
-
* "status": "pending" | "in_progress" | "completed",
|
|
18
|
-
* "blocks": [],
|
|
19
|
-
* "blockedBy": []
|
|
20
|
-
* }
|
|
21
|
-
*
|
|
22
|
-
* Rather than replaying tool_use / tool_result payloads (whose shapes are not
|
|
23
|
-
* guaranteed by the minified SDK), this aggregator reads those files directly
|
|
24
|
-
* after each Task* tool call completes, which is the canonical source of truth.
|
|
25
|
-
*/
|
|
26
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
27
|
-
import { homedir } from "node:os";
|
|
28
|
-
import { join } from "node:path";
|
|
29
|
-
const TASK_TOOL_NAMES = new Set(["TaskCreate", "TaskUpdate", "TaskList", "TaskGet", "TaskStop", "TaskOutput"]);
|
|
30
|
-
function isRecord(value) {
|
|
31
|
-
return Boolean(value && typeof value === "object");
|
|
32
|
-
}
|
|
33
|
-
function asString(value) {
|
|
34
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
35
|
-
}
|
|
36
|
-
function normalizeStatus(value) {
|
|
37
|
-
if (value === "pending" || value === "in_progress" || value === "completed")
|
|
38
|
-
return value;
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
/** Root directory under which per-session task files are persisted. */
|
|
42
|
-
export function defaultTasksRoot() {
|
|
43
|
-
const base = process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
|
|
44
|
-
return join(base, "tasks");
|
|
45
|
-
}
|
|
46
|
-
function toTodo(raw) {
|
|
47
|
-
if (!isRecord(raw))
|
|
48
|
-
return undefined;
|
|
49
|
-
const status = normalizeStatus(raw.status);
|
|
50
|
-
// Files may exist for tasks in states we do not surface; skip unknown ones.
|
|
51
|
-
if (!status)
|
|
52
|
-
return undefined;
|
|
53
|
-
const content = asString(raw.subject) ?? asString(raw.activeForm) ?? asString(raw.description);
|
|
54
|
-
if (!content)
|
|
55
|
-
return undefined;
|
|
56
|
-
const activeForm = asString(raw.activeForm);
|
|
57
|
-
return { content, status, ...(activeForm ? { activeForm } : null) };
|
|
58
|
-
}
|
|
59
|
-
/** Reads and orders the task files for a single session directory. */
|
|
60
|
-
export function readSessionTasks(tasksRoot, sessionId) {
|
|
61
|
-
const dir = join(tasksRoot, sessionId);
|
|
62
|
-
let files;
|
|
63
|
-
try {
|
|
64
|
-
files = readdirSync(dir);
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
const ordered = files
|
|
70
|
-
.filter((name) => name.endsWith(".json"))
|
|
71
|
-
.map((name) => ({ name, n: Number.parseInt(name, 10) }))
|
|
72
|
-
.filter((entry) => Number.isFinite(entry.n))
|
|
73
|
-
.sort((a, b) => a.n - b.n);
|
|
74
|
-
const todos = [];
|
|
75
|
-
for (const { name } of ordered) {
|
|
76
|
-
try {
|
|
77
|
-
const raw = JSON.parse(readFileSync(join(dir, name), "utf-8"));
|
|
78
|
-
const todo = toTodo(raw);
|
|
79
|
-
if (todo)
|
|
80
|
-
todos.push(todo);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
// Ignore unreadable / partially-written files; the next refresh recovers.
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return todos;
|
|
87
|
-
}
|
|
88
|
-
export function createTaskAggregator(options = {}) {
|
|
89
|
-
const tasksRoot = options.tasksRoot ?? defaultTasksRoot();
|
|
90
|
-
// All tool_use_ids that belonged to Task* tools, so their raw tool input /
|
|
91
|
-
// result can be suppressed downstream in favor of the aggregated event.
|
|
92
|
-
const taskToolCallIds = new Set();
|
|
93
|
-
let sessionId = options.sessionId;
|
|
94
|
-
let todos = [];
|
|
95
|
-
let lastSerialized = JSON.stringify(todos);
|
|
96
|
-
let isDirty = false;
|
|
97
|
-
const refresh = () => {
|
|
98
|
-
if (!sessionId)
|
|
99
|
-
return false;
|
|
100
|
-
const next = readSessionTasks(tasksRoot, sessionId);
|
|
101
|
-
const serialized = JSON.stringify(next);
|
|
102
|
-
if (serialized === lastSerialized)
|
|
103
|
-
return false;
|
|
104
|
-
todos = next;
|
|
105
|
-
lastSerialized = serialized;
|
|
106
|
-
isDirty = true;
|
|
107
|
-
return true;
|
|
108
|
-
};
|
|
109
|
-
return {
|
|
110
|
-
setSessionId(next) {
|
|
111
|
-
if (next && next !== sessionId) {
|
|
112
|
-
sessionId = next;
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
isTaskTool(name) {
|
|
116
|
-
return TASK_TOOL_NAMES.has(name);
|
|
117
|
-
},
|
|
118
|
-
applyToolUse(toolCallId, name) {
|
|
119
|
-
if (!TASK_TOOL_NAMES.has(name))
|
|
120
|
-
return false;
|
|
121
|
-
taskToolCallIds.add(toolCallId);
|
|
122
|
-
return refresh();
|
|
123
|
-
},
|
|
124
|
-
applyToolResult(toolCallId) {
|
|
125
|
-
if (!taskToolCallIds.has(toolCallId))
|
|
126
|
-
return false;
|
|
127
|
-
return refresh();
|
|
128
|
-
},
|
|
129
|
-
isTaskToolCall(toolCallId) {
|
|
130
|
-
return taskToolCallIds.has(toolCallId);
|
|
131
|
-
},
|
|
132
|
-
refresh,
|
|
133
|
-
snapshot() {
|
|
134
|
-
return { todos: todos.map((todo) => ({ ...todo })) };
|
|
135
|
-
},
|
|
136
|
-
dirty() {
|
|
137
|
-
return isDirty;
|
|
138
|
-
},
|
|
139
|
-
clearDirty() {
|
|
140
|
-
isDirty = false;
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
}
|
|
1
|
+
import{readdirSync as p,readFileSync as T}from"node:fs";import{homedir as m}from"node:os";import{join as c}from"node:path";const l=new Set(["TaskCreate","TaskUpdate","TaskList","TaskGet","TaskStop","TaskOutput"]);function k(t){return!!(t&&typeof t=="object")}function d(t){return typeof t=="string"&&t.length>0?t:void 0}function S(t){if(t==="pending"||t==="in_progress"||t==="completed")return t}function y(){const t=process.env.CLAUDE_CONFIG_DIR?.trim()||c(m(),".claude");return c(t,"tasks")}function h(t){if(!k(t))return;const i=S(t.status);if(!i)return;const n=d(t.subject)??d(t.activeForm)??d(t.description);if(!n)return;const s=d(t.activeForm);return{content:n,status:i,...s?{activeForm:s}:null}}function g(t,i){const n=c(t,i);let s;try{s=p(n)}catch{return[]}const a=s.filter(r=>r.endsWith(".json")).map(r=>({name:r,n:Number.parseInt(r,10)})).filter(r=>Number.isFinite(r.n)).sort((r,o)=>r.n-o.n),u=[];for(const{name:r}of a)try{const o=JSON.parse(T(c(n,r),"utf-8")),e=h(o);e&&u.push(e)}catch{}return u}function F(t={}){const i=t.tasksRoot??y(),n=new Set;let s=t.sessionId,a=[],u=JSON.stringify(a),r=!1;const o=()=>{if(!s)return!1;const e=g(i,s),f=JSON.stringify(e);return f===u?!1:(a=e,u=f,r=!0,!0)};return{setSessionId(e){e&&e!==s&&(s=e)},isTaskTool(e){return l.has(e)},applyToolUse(e,f){return l.has(f)?(n.add(e),o()):!1},applyToolResult(e){return n.has(e)?o():!1},isTaskToolCall(e){return n.has(e)},refresh:o,snapshot(){return{todos:a.map(e=>({...e}))}},dirty(){return r},clearDirty(){r=!1}}}export{F as createTaskAggregator,y as defaultTasksRoot,g as readSessionTasks};
|
package/dist/token-usage.js
CHANGED
|
@@ -1,75 +1 @@
|
|
|
1
|
-
function
|
|
2
|
-
let total = 0;
|
|
3
|
-
for (const value of values) {
|
|
4
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
5
|
-
return undefined;
|
|
6
|
-
total += value;
|
|
7
|
-
if (!Number.isSafeInteger(total))
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
return total;
|
|
11
|
-
}
|
|
12
|
-
function resultBreakdown(message) {
|
|
13
|
-
const modelUsages = Object.values(message.modelUsage ?? {});
|
|
14
|
-
if (modelUsages.length === 0)
|
|
15
|
-
return undefined;
|
|
16
|
-
let result;
|
|
17
|
-
for (const usage of modelUsages) {
|
|
18
|
-
const inputTokens = sumTokens(usage.inputTokens, usage.cacheCreationInputTokens, usage.cacheReadInputTokens);
|
|
19
|
-
const outputTokens = sumTokens(usage.outputTokens);
|
|
20
|
-
if (inputTokens === undefined || outputTokens === undefined)
|
|
21
|
-
return undefined;
|
|
22
|
-
const totalTokens = sumTokens(inputTokens, outputTokens);
|
|
23
|
-
if (totalTokens === undefined)
|
|
24
|
-
return undefined;
|
|
25
|
-
result = addBreakdowns(result, {
|
|
26
|
-
input_tokens: inputTokens,
|
|
27
|
-
output_tokens: outputTokens,
|
|
28
|
-
total_tokens: totalTokens,
|
|
29
|
-
});
|
|
30
|
-
if (!result)
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
return result;
|
|
34
|
-
}
|
|
35
|
-
function addBreakdowns(left, right) {
|
|
36
|
-
const inputTokens = sumTokens(left?.input_tokens ?? 0, right.input_tokens);
|
|
37
|
-
const outputTokens = sumTokens(left?.output_tokens ?? 0, right.output_tokens);
|
|
38
|
-
const totalTokens = sumTokens(left?.total_tokens ?? 0, right.total_tokens);
|
|
39
|
-
if (inputTokens === undefined || outputTokens === undefined || totalTokens === undefined)
|
|
40
|
-
return undefined;
|
|
41
|
-
return { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: totalTokens };
|
|
42
|
-
}
|
|
43
|
-
export function claudeContextSnapshot(message) {
|
|
44
|
-
const rawUsage = message.usage;
|
|
45
|
-
if (!rawUsage || typeof rawUsage !== "object")
|
|
46
|
-
return undefined;
|
|
47
|
-
const usage = rawUsage;
|
|
48
|
-
const usedTokens = sumTokens(usage.input_tokens, usage.cache_creation_input_tokens, usage.cache_read_input_tokens, usage.output_tokens);
|
|
49
|
-
if (usedTokens === undefined)
|
|
50
|
-
return undefined;
|
|
51
|
-
return {
|
|
52
|
-
usedTokens,
|
|
53
|
-
...(typeof message.model === "string" && message.model ? { model: message.model } : {}),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function modelContextWindow(message, model) {
|
|
57
|
-
const entries = Object.entries(message.modelUsage ?? {});
|
|
58
|
-
const selected = entries.length === 1 ? entries[0][1] : model ? entries.find(([name]) => name === model)?.[1] : undefined;
|
|
59
|
-
const contextWindow = selected?.contextWindow;
|
|
60
|
-
return typeof contextWindow === "number" && Number.isSafeInteger(contextWindow) && contextWindow > 0
|
|
61
|
-
? contextWindow
|
|
62
|
-
: undefined;
|
|
63
|
-
}
|
|
64
|
-
export function claudeTokenUsage(message, contextSnapshot) {
|
|
65
|
-
const last = resultBreakdown(message);
|
|
66
|
-
if (!last)
|
|
67
|
-
return undefined;
|
|
68
|
-
const contextWindow = modelContextWindow(message, contextSnapshot?.model);
|
|
69
|
-
return {
|
|
70
|
-
last,
|
|
71
|
-
...(contextSnapshot && contextWindow !== undefined
|
|
72
|
-
? { context: { used_tokens: contextSnapshot.usedTokens, window_tokens: contextWindow } }
|
|
73
|
-
: {}),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
1
|
+
function d(...t){let n=0;for(const e of t)if(typeof e!="number"||!Number.isSafeInteger(e)||e<0||(n+=e,!Number.isSafeInteger(n)))return;return n}function r(t){const n=Object.values(t.modelUsage??{});if(n.length===0)return;let e;for(const o of n){const u=d(o.inputTokens,o.cacheCreationInputTokens,o.cacheReadInputTokens),s=d(o.outputTokens);if(u===void 0||s===void 0)return;const i=d(u,s);if(i===void 0||(e=f(e,{input_tokens:u,output_tokens:s,total_tokens:i}),!e))return}return e}function f(t,n){const e=d(t?.input_tokens??0,n.input_tokens),o=d(t?.output_tokens??0,n.output_tokens),u=d(t?.total_tokens??0,n.total_tokens);if(!(e===void 0||o===void 0||u===void 0))return{input_tokens:e,output_tokens:o,total_tokens:u}}function k(t){const n=t.usage;if(!n||typeof n!="object")return;const e=n,o=d(e.input_tokens,e.cache_creation_input_tokens,e.cache_read_input_tokens,e.output_tokens);if(o!==void 0)return{usedTokens:o,...typeof t.model=="string"&&t.model?{model:t.model}:{}}}function c(t,n){const e=Object.entries(t.modelUsage??{}),u=(e.length===1?e[0][1]:n?e.find(([s])=>s===n)?.[1]:void 0)?.contextWindow;return typeof u=="number"&&Number.isSafeInteger(u)&&u>0?u:void 0}function a(t,n){const e=r(t);if(!e)return;const o=c(t,n?.model);return{last:e,...n&&o!==void 0?{context:{used_tokens:n.usedTokens,window_tokens:o}}:{}}}export{k as claudeContextSnapshot,a as claudeTokenUsage};
|
package/dist/tunnel-client.js
CHANGED
|
@@ -1,223 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import WebSocket from "ws";
|
|
3
|
-
import { appConfig } from "./config.js";
|
|
4
|
-
import { ChatBodySchema } from "./schemas.js";
|
|
5
|
-
import { cancelClaudeConversation, ChatRequestError, handleClaudeChatStream } from "./routes/chat.js";
|
|
6
|
-
import { logger } from "@messenger-agent/shared/logger";
|
|
7
|
-
import { sendHttpTunnelResponse } from "@messenger-agent/shared/tunnel-http";
|
|
8
|
-
import { CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE, CODING_AGENT_TUNNEL_PROTOCOL_VERSION, } from "@messenger-agent/shared/tunnel-protocol";
|
|
9
|
-
const clientVersion = createRequire(import.meta.url)("../package.json").version;
|
|
10
|
-
function delay(ms) {
|
|
11
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
-
}
|
|
13
|
-
function parseServerMessage(raw) {
|
|
14
|
-
try {
|
|
15
|
-
const value = JSON.parse(raw.toString());
|
|
16
|
-
if (!value || typeof value !== "object" || !("type" in value))
|
|
17
|
-
return undefined;
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function headersFromRecord(headers) {
|
|
25
|
-
const result = new Headers();
|
|
26
|
-
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
27
|
-
result.set(key, value);
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
function cancelConversationId(message) {
|
|
32
|
-
if (message.method !== "POST")
|
|
33
|
-
return undefined;
|
|
34
|
-
try {
|
|
35
|
-
const pathname = new URL(message.path, "http://coding-agent-tunnel.local").pathname;
|
|
36
|
-
const match = /^\/v1\/chat\/([^/]+)\/cancel$/.exec(pathname);
|
|
37
|
-
return match ? decodeURIComponent(match[1]) : undefined;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
export class ClaudeTunnelClient {
|
|
44
|
-
config;
|
|
45
|
-
stopped = false;
|
|
46
|
-
socket;
|
|
47
|
-
running = new Map();
|
|
48
|
-
constructor(config) {
|
|
49
|
-
this.config = config;
|
|
50
|
-
}
|
|
51
|
-
async start() {
|
|
52
|
-
if (!this.config.enabled)
|
|
53
|
-
return;
|
|
54
|
-
if (!this.config.serverUrl || !this.config.tunnelId || !this.config.token) {
|
|
55
|
-
throw new Error("Tunnel config requires server_url, tunnel_id, and token when enabled");
|
|
56
|
-
}
|
|
57
|
-
let backoffMs = this.config.reconnectInitialMs;
|
|
58
|
-
while (!this.stopped) {
|
|
59
|
-
try {
|
|
60
|
-
await this.connectOnce();
|
|
61
|
-
backoffMs = this.config.reconnectInitialMs;
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
if (this.stopped)
|
|
65
|
-
break;
|
|
66
|
-
logger.warn("[TunnelClient] websocket tunnel connection failed:", err);
|
|
67
|
-
}
|
|
68
|
-
if (!this.stopped) {
|
|
69
|
-
await delay(backoffMs);
|
|
70
|
-
backoffMs = Math.min(backoffMs * 2, this.config.reconnectMaxMs);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
stop() {
|
|
75
|
-
this.stopped = true;
|
|
76
|
-
for (const request of this.running.values()) {
|
|
77
|
-
request.abortController.abort();
|
|
78
|
-
}
|
|
79
|
-
this.running.clear();
|
|
80
|
-
this.socket?.close();
|
|
81
|
-
}
|
|
82
|
-
connectOnce() {
|
|
83
|
-
return new Promise((resolve, reject) => {
|
|
84
|
-
const headers = { Authorization: `Bearer ${this.config.token}` };
|
|
85
|
-
const ws = new WebSocket(this.config.serverUrl, { headers });
|
|
86
|
-
this.socket = ws;
|
|
87
|
-
let heartbeatTimer;
|
|
88
|
-
let settled = false;
|
|
89
|
-
const cleanup = () => {
|
|
90
|
-
if (heartbeatTimer)
|
|
91
|
-
clearInterval(heartbeatTimer);
|
|
92
|
-
if (this.socket === ws)
|
|
93
|
-
this.socket = undefined;
|
|
94
|
-
for (const request of this.running.values()) {
|
|
95
|
-
request.abortController.abort(new Error("Tunnel connection closed"));
|
|
96
|
-
}
|
|
97
|
-
this.running.clear();
|
|
98
|
-
};
|
|
99
|
-
ws.on("open", () => {
|
|
100
|
-
this.send({
|
|
101
|
-
type: "hello",
|
|
102
|
-
protocolVersion: CODING_AGENT_TUNNEL_PROTOCOL_VERSION,
|
|
103
|
-
tunnelId: this.config.tunnelId,
|
|
104
|
-
agentType: "claude",
|
|
105
|
-
clientVersion,
|
|
106
|
-
});
|
|
107
|
-
heartbeatTimer = setInterval(() => {
|
|
108
|
-
this.send({ type: "heartbeat" });
|
|
109
|
-
}, this.config.heartbeatIntervalMs);
|
|
110
|
-
logger.info(`[TunnelClient] connected to ${this.config.serverUrl} as ${this.config.tunnelId}`);
|
|
111
|
-
});
|
|
112
|
-
ws.on("message", (raw) => {
|
|
113
|
-
const message = parseServerMessage(raw);
|
|
114
|
-
if (!message) {
|
|
115
|
-
logger.warn("[TunnelClient] received invalid websocket message");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (message.type === "heartbeat")
|
|
119
|
-
return;
|
|
120
|
-
if (message.type === "cancel") {
|
|
121
|
-
this.running.get(message.id)?.abortController.abort();
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (message.type === "request") {
|
|
125
|
-
this.handleRequest(message).catch((err) => {
|
|
126
|
-
logger.error(`[TunnelClient] request ${message.id} failed:`, err);
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
ws.once("error", (err) => {
|
|
131
|
-
if (!settled) {
|
|
132
|
-
settled = true;
|
|
133
|
-
cleanup();
|
|
134
|
-
reject(err);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
ws.once("close", (code, reason) => {
|
|
138
|
-
cleanup();
|
|
139
|
-
if (code === CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE) {
|
|
140
|
-
this.stopped = true;
|
|
141
|
-
logger.warn(`[TunnelClient] duplicate websocket tunnel connection rejected; reconnect disabled: ${reason.toString()}`);
|
|
142
|
-
}
|
|
143
|
-
if (!settled) {
|
|
144
|
-
settled = true;
|
|
145
|
-
resolve();
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
send(message) {
|
|
151
|
-
const socket = this.socket;
|
|
152
|
-
if (!socket || socket.readyState !== WebSocket.OPEN)
|
|
153
|
-
return;
|
|
154
|
-
socket.send(JSON.stringify(message));
|
|
155
|
-
}
|
|
156
|
-
async handleRequest(message) {
|
|
157
|
-
const conversationId = cancelConversationId(message);
|
|
158
|
-
if (conversationId) {
|
|
159
|
-
const cancelled = cancelClaudeConversation(conversationId);
|
|
160
|
-
const response = Response.json(cancelled
|
|
161
|
-
? { success: true }
|
|
162
|
-
: {
|
|
163
|
-
error: {
|
|
164
|
-
type: "not_found_error",
|
|
165
|
-
code: "conversation_not_found",
|
|
166
|
-
message: `Conversation not found or already completed: ${conversationId}`,
|
|
167
|
-
param: "conversation_id",
|
|
168
|
-
},
|
|
169
|
-
}, { status: cancelled ? 200 : 404 });
|
|
170
|
-
await sendHttpTunnelResponse(message.id, response, (responseMessage) => this.send(responseMessage));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (message.method !== "POST" || message.path !== "/v1/chat/stream") {
|
|
174
|
-
this.send({
|
|
175
|
-
type: "response.error",
|
|
176
|
-
id: message.id,
|
|
177
|
-
code: "unsupported_request",
|
|
178
|
-
errorText: `Unsupported tunnel request: ${message.method} ${message.path}`,
|
|
179
|
-
});
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const body = ChatBodySchema.safeParse(message.body);
|
|
183
|
-
if (!body.success) {
|
|
184
|
-
this.send({
|
|
185
|
-
type: "response.error",
|
|
186
|
-
id: message.id,
|
|
187
|
-
code: "invalid_request",
|
|
188
|
-
errorText: `Invalid chat request body: ${body.error.message}`,
|
|
189
|
-
});
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const abortController = new AbortController();
|
|
193
|
-
this.running.set(message.id, { abortController });
|
|
194
|
-
this.send({ type: "response.start", id: message.id, mode: "sse" });
|
|
195
|
-
try {
|
|
196
|
-
await handleClaudeChatStream({
|
|
197
|
-
body: body.data,
|
|
198
|
-
headers: headersFromRecord(message.headers),
|
|
199
|
-
signal: abortController.signal,
|
|
200
|
-
}, {
|
|
201
|
-
writeData: async (data) => this.send({ type: "response.chunk", id: message.id, data }),
|
|
202
|
-
writeDone: async () => this.send({ type: "response.end", id: message.id }),
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
catch (err) {
|
|
206
|
-
const errorText = err instanceof Error ? err.message : "Tunnel request failed";
|
|
207
|
-
const code = err instanceof ChatRequestError ? `http_${err.status}` : "request_failed";
|
|
208
|
-
this.send({ type: "response.error", id: message.id, code, errorText });
|
|
209
|
-
}
|
|
210
|
-
finally {
|
|
211
|
-
this.running.delete(message.id);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
export function startClaudeTunnelClient(config = appConfig.tunnel) {
|
|
216
|
-
if (!config.enabled)
|
|
217
|
-
return undefined;
|
|
218
|
-
const client = new ClaudeTunnelClient(config);
|
|
219
|
-
client.start().catch((err) => {
|
|
220
|
-
logger.error("[TunnelClient] stopped unexpectedly:", err);
|
|
221
|
-
});
|
|
222
|
-
return client;
|
|
223
|
-
}
|
|
1
|
+
import{createRequire as f}from"node:module";import u from"ws";import{appConfig as p}from"./config.js";import{ChatBodySchema as y}from"./schemas.js";import{cancelClaudeConversation as C,ChatRequestError as b,handleClaudeChatStream as g}from"./routes/chat.js";import{logger as l}from"@messenger-agent/shared/logger";import{sendHttpTunnelResponse as m}from"@messenger-agent/shared/tunnel-http";import{CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE as w,CODING_AGENT_TUNNEL_PROTOCOL_VERSION as T}from"@messenger-agent/shared/tunnel-protocol";const v=f(import.meta.url)("../package.json").version;function _(r){return new Promise(e=>setTimeout(e,r))}function k(r){try{const e=JSON.parse(r.toString());return!e||typeof e!="object"||!("type"in e)?void 0:e}catch{return}}function I(r){const e=new Headers;for(const[t,c]of Object.entries(r??{}))e.set(t,c);return e}function O(r){if(r.method==="POST")try{const e=new URL(r.path,"http://coding-agent-tunnel.local").pathname,t=/^\/v1\/chat\/([^/]+)\/cancel$/.exec(e);return t?decodeURIComponent(t[1]):void 0}catch{return}}class q{config;stopped=!1;socket;running=new Map;constructor(e){this.config=e}async start(){if(!this.config.enabled)return;if(!this.config.serverUrl||!this.config.tunnelId||!this.config.token)throw new Error("Tunnel config requires server_url, tunnel_id, and token when enabled");let e=this.config.reconnectInitialMs;for(;!this.stopped;){try{await this.connectOnce(),e=this.config.reconnectInitialMs}catch(t){if(this.stopped)break;l.warn("[TunnelClient] websocket tunnel connection failed:",t)}this.stopped||(await _(e),e=Math.min(e*2,this.config.reconnectMaxMs))}}stop(){this.stopped=!0;for(const e of this.running.values())e.abortController.abort();this.running.clear(),this.socket?.close()}connectOnce(){return new Promise((e,t)=>{const c={Authorization:`Bearer ${this.config.token}`},o=new u(this.config.serverUrl,{headers:c});this.socket=o;let n,s=!1;const d=()=>{n&&clearInterval(n),this.socket===o&&(this.socket=void 0);for(const a of this.running.values())a.abortController.abort(new Error("Tunnel connection closed"));this.running.clear()};o.on("open",()=>{this.send({type:"hello",protocolVersion:T,tunnelId:this.config.tunnelId,agentType:"claude",clientVersion:v}),n=setInterval(()=>{this.send({type:"heartbeat"})},this.config.heartbeatIntervalMs),l.info(`[TunnelClient] connected to ${this.config.serverUrl} as ${this.config.tunnelId}`)}),o.on("message",a=>{const i=k(a);if(!i){l.warn("[TunnelClient] received invalid websocket message");return}if(i.type!=="heartbeat"){if(i.type==="cancel"){this.running.get(i.id)?.abortController.abort();return}i.type==="request"&&this.handleRequest(i).catch(h=>{l.error(`[TunnelClient] request ${i.id} failed:`,h)})}}),o.once("error",a=>{s||(s=!0,d(),t(a))}),o.once("close",(a,i)=>{d(),a===w&&(this.stopped=!0,l.warn(`[TunnelClient] duplicate websocket tunnel connection rejected; reconnect disabled: ${i.toString()}`)),s||(s=!0,e())})})}send(e){const t=this.socket;!t||t.readyState!==u.OPEN||t.send(JSON.stringify(e))}async handleRequest(e){const t=O(e);if(t){const n=C(t),s=Response.json(n?{success:!0}:{error:{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${t}`,param:"conversation_id"}},{status:n?200:404});await m(e.id,s,d=>this.send(d));return}if(e.method!=="POST"||e.path!=="/v1/chat/stream"){this.send({type:"response.error",id:e.id,code:"unsupported_request",errorText:`Unsupported tunnel request: ${e.method} ${e.path}`});return}const c=y.safeParse(e.body);if(!c.success){this.send({type:"response.error",id:e.id,code:"invalid_request",errorText:`Invalid chat request body: ${c.error.message}`});return}const o=new AbortController;this.running.set(e.id,{abortController:o}),this.send({type:"response.start",id:e.id,mode:"sse"});try{await g({body:c.data,headers:I(e.headers),signal:o.signal},{writeData:async n=>this.send({type:"response.chunk",id:e.id,data:n}),writeDone:async()=>this.send({type:"response.end",id:e.id})})}catch(n){const s=n instanceof Error?n.message:"Tunnel request failed",d=n instanceof b?`http_${n.status}`:"request_failed";this.send({type:"response.error",id:e.id,code:d,errorText:s})}finally{this.running.delete(e.id)}}}function P(r=p.tunnel){if(!r.enabled)return;const e=new q(r);return e.start().catch(t=>{l.error("[TunnelClient] stopped unexpectedly:",t)}),e}export{q as ClaudeTunnelClient,P as startClaudeTunnelClient};
|
package/dist/workspace-files.js
CHANGED
|
@@ -1,56 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { access, realpath, stat } from "node:fs/promises";
|
|
3
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
|
-
export const WORKSPACES_ROOT = "/workspaces";
|
|
5
|
-
export class WorkspaceFileError extends Error {
|
|
6
|
-
code;
|
|
7
|
-
constructor(message, code) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.code = code;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export function isPathInside(parent, child) {
|
|
13
|
-
const relativePath = relative(parent, child);
|
|
14
|
-
return relativePath.length === 0 || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
15
|
-
}
|
|
16
|
-
export async function resolveWorkspaceFilePath(rawPath, workspaces = new Map()) {
|
|
17
|
-
if (!isAbsolute(rawPath)) {
|
|
18
|
-
throw new WorkspaceFileError("Path must be absolute", "INVALID_PATH");
|
|
19
|
-
}
|
|
20
|
-
const virtualPath = resolve(rawPath);
|
|
21
|
-
if (!virtualPath.startsWith(`${WORKSPACES_ROOT}/`)) {
|
|
22
|
-
throw new WorkspaceFileError("Access denied: path must be under /workspaces/", "OUTSIDE_WORKSPACE");
|
|
23
|
-
}
|
|
24
|
-
const relativePath = relative(WORKSPACES_ROOT, virtualPath);
|
|
25
|
-
const [workspaceId, ...workspacePathParts] = relativePath.split(/[\\/]+/);
|
|
26
|
-
const configuredWorkspace = workspaces.get(workspaceId);
|
|
27
|
-
if (!configuredWorkspace) {
|
|
28
|
-
throw new WorkspaceFileError(`Unknown workspace: ${workspaceId}`, "NOT_FOUND");
|
|
29
|
-
}
|
|
30
|
-
const configuredRoot = resolve(configuredWorkspace.path);
|
|
31
|
-
const actualPath = resolve(configuredRoot, ...workspacePathParts);
|
|
32
|
-
if (!isPathInside(configuredRoot, actualPath)) {
|
|
33
|
-
throw new WorkspaceFileError("Access denied: path must stay under workspace root", "OUTSIDE_WORKSPACE");
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
const [realRoot, realFile] = await Promise.all([realpath(configuredRoot), realpath(actualPath)]);
|
|
37
|
-
if (!isPathInside(realRoot, realFile)) {
|
|
38
|
-
throw new WorkspaceFileError("Access denied: path must stay under workspace root", "OUTSIDE_WORKSPACE");
|
|
39
|
-
}
|
|
40
|
-
const fileStat = await stat(realFile);
|
|
41
|
-
if (!fileStat.isFile()) {
|
|
42
|
-
throw new WorkspaceFileError("Path is not a file", "NOT_FILE");
|
|
43
|
-
}
|
|
44
|
-
await access(realFile, constants.R_OK);
|
|
45
|
-
return realFile;
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
if (err instanceof WorkspaceFileError)
|
|
49
|
-
throw err;
|
|
50
|
-
const error = err;
|
|
51
|
-
if (error.code === "ENOENT") {
|
|
52
|
-
throw new WorkspaceFileError("File not found", "NOT_FOUND");
|
|
53
|
-
}
|
|
54
|
-
throw err;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
import{constants as E}from"node:fs";import{access as k,realpath as l,stat as m}from"node:fs/promises";import{isAbsolute as p,relative as f,resolve as c}from"node:path";const u="/workspaces";class e extends Error{code;constructor(s,t){super(s),this.code=t}}function d(r,s){const t=f(r,s);return t.length===0||!t.startsWith("..")&&!p(t)}async function _(r,s=new Map){if(!p(r))throw new e("Path must be absolute","INVALID_PATH");const t=c(r);if(!t.startsWith(`${u}/`))throw new e("Access denied: path must be under /workspaces/","OUTSIDE_WORKSPACE");const O=f(u,t),[i,...P]=O.split(/[\\/]+/),h=s.get(i);if(!h)throw new e(`Unknown workspace: ${i}`,"NOT_FOUND");const n=c(h.path),w=c(n,...P);if(!d(n,w))throw new e("Access denied: path must stay under workspace root","OUTSIDE_WORKSPACE");try{const[o,a]=await Promise.all([l(n),l(w)]);if(!d(o,a))throw new e("Access denied: path must stay under workspace root","OUTSIDE_WORKSPACE");if(!(await m(a)).isFile())throw new e("Path is not a file","NOT_FILE");return await k(a,E.R_OK),a}catch(o){throw o instanceof e?o:o.code==="ENOENT"?new e("File not found","NOT_FOUND"):o}}export{u as WORKSPACES_ROOT,e as WorkspaceFileError,d as isPathInside,_ as resolveWorkspaceFilePath};
|