@messenger-agent/claude-agent 0.24.0-alpha.2
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.d.ts +3 -0
- package/dist/app.js +15 -0
- package/dist/assets/codex-home/AGENTS.md +50 -0
- package/dist/claude.d.ts +1 -0
- package/dist/claude.js +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +147 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +19 -0
- package/dist/input-queue.d.ts +8 -0
- package/dist/input-queue.js +40 -0
- package/dist/live-session.d.ts +69 -0
- package/dist/live-session.js +225 -0
- package/dist/llm-proxy-bindings.d.ts +3 -0
- package/dist/llm-proxy-bindings.js +45 -0
- package/dist/managed-task-mcp.d.ts +7 -0
- package/dist/managed-task-mcp.js +41 -0
- package/dist/pending-asks.d.ts +85 -0
- package/dist/pending-asks.js +283 -0
- package/dist/platform-instructions.d.ts +2 -0
- package/dist/platform-instructions.js +42 -0
- package/dist/routes/chat.d.ts +27 -0
- package/dist/routes/chat.js +1229 -0
- package/dist/schemas.d.ts +188 -0
- package/dist/schemas.js +89 -0
- package/dist/session-manager.d.ts +14 -0
- package/dist/session-manager.js +57 -0
- package/dist/task-aggregator.d.ts +69 -0
- package/dist/task-aggregator.js +143 -0
- package/dist/token-usage.d.ts +23 -0
- package/dist/token-usage.js +75 -0
- package/dist/tunnel-client.d.ts +15 -0
- package/dist/tunnel-client.js +223 -0
- package/dist/workspace-files.d.ts +8 -0
- package/dist/workspace-files.js +56 -0
- package/package.json +33 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentWorkspaceConfig } from "@messenger-agent/shared/agent-config";
|
|
2
|
+
export declare const WORKSPACES_ROOT = "/workspaces";
|
|
3
|
+
export declare class WorkspaceFileError extends Error {
|
|
4
|
+
readonly code: "INVALID_PATH" | "OUTSIDE_WORKSPACE" | "NOT_FOUND" | "NOT_FILE";
|
|
5
|
+
constructor(message: string, code: "INVALID_PATH" | "OUTSIDE_WORKSPACE" | "NOT_FOUND" | "NOT_FILE");
|
|
6
|
+
}
|
|
7
|
+
export declare function isPathInside(parent: string, child: string): boolean;
|
|
8
|
+
export declare function resolveWorkspaceFilePath(rawPath: string, workspaces?: Map<string, AgentWorkspaceConfig>): Promise<string>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@messenger-agent/claude-agent",
|
|
3
|
+
"version": "0.24.0-alpha.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"registry": "https://registry.npmjs.org"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.215",
|
|
16
|
+
"@hono/node-server": "^2.0.2",
|
|
17
|
+
"@hono/zod-validator": "^0.7.6",
|
|
18
|
+
"hono": "^4.12.18",
|
|
19
|
+
"ws": "^8.21.0",
|
|
20
|
+
"yaml": "^2.9.0",
|
|
21
|
+
"zod": "^4.4.3",
|
|
22
|
+
"@messenger-agent/shared": "0.24.0-alpha.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/ws": "^8.18.1"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "rm -rf dist && tsc -p tsconfig.json && node ../../scripts/copy-assets.mjs claude-agent",
|
|
29
|
+
"build:sourcemap": "rm -rf dist && tsc -p tsconfig.json --sourceMap && node ../../scripts/copy-assets.mjs claude-agent",
|
|
30
|
+
"start": "node dist/index.js",
|
|
31
|
+
"dev": "tsx src/index.ts"
|
|
32
|
+
}
|
|
33
|
+
}
|