@yeaft/webchat-agent 0.0.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/claude.js +405 -0
- package/cli.js +151 -0
- package/connection.js +391 -0
- package/context.js +26 -0
- package/conversation.js +452 -0
- package/encryption.js +105 -0
- package/history.js +283 -0
- package/index.js +159 -0
- package/package.json +75 -0
- package/proxy.js +169 -0
- package/sdk/index.js +9 -0
- package/sdk/query.js +396 -0
- package/sdk/stream.js +112 -0
- package/sdk/types.js +13 -0
- package/sdk/utils.js +194 -0
- package/service.js +587 -0
- package/terminal.js +176 -0
- package/workbench.js +907 -0
package/history.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import ctx from './context.js';
|
|
5
|
+
|
|
6
|
+
// Claude 项目目录
|
|
7
|
+
export function getClaudeProjectsDir() {
|
|
8
|
+
return join(homedir(), '.claude', 'projects');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 将路径转换为 Claude 项目文件夹名
|
|
12
|
+
export function pathToProjectFolder(workDir) {
|
|
13
|
+
return workDir
|
|
14
|
+
.replace(/:/g, '-')
|
|
15
|
+
.replace(/[\\\/]/g, '-');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 从 session 文件中提取原始工作目录路径
|
|
19
|
+
// Claude session jsonl 文件的每条消息都包含 cwd 字段
|
|
20
|
+
export function extractWorkDirFromSessionFile(sessionFilePath) {
|
|
21
|
+
try {
|
|
22
|
+
const content = readFileSync(sessionFilePath, 'utf-8');
|
|
23
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
24
|
+
|
|
25
|
+
for (const line of lines.slice(0, 5)) { // 只检查前几行
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(line);
|
|
28
|
+
// 每条消息都有 cwd 字段
|
|
29
|
+
if (data.cwd) {
|
|
30
|
+
return data.cwd.replace(/\\/g, '/');
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 从项目文件夹中获取原始工作目录路径
|
|
39
|
+
// 优先从 session 文件读取,失败则用简单转换
|
|
40
|
+
export function getWorkDirFromProjectFolder(projectFolderPath, folderName) {
|
|
41
|
+
// 尝试从第一个 session 文件读取真实路径
|
|
42
|
+
try {
|
|
43
|
+
const files = readdirSync(projectFolderPath);
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
if (file.endsWith('.jsonl')) {
|
|
46
|
+
const workDir = extractWorkDirFromSessionFile(join(projectFolderPath, file));
|
|
47
|
+
if (workDir) {
|
|
48
|
+
return workDir;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
|
|
54
|
+
// 回退:简单转换(可能不准确,但总比没有好)
|
|
55
|
+
if (/^[A-Za-z]--/.test(folderName)) {
|
|
56
|
+
return folderName.replace(/^([A-Za-z])--/, '$1:/').replace(/-/g, '/');
|
|
57
|
+
}
|
|
58
|
+
if (folderName.startsWith('-')) {
|
|
59
|
+
return '/' + folderName.substring(1).replace(/-/g, '/');
|
|
60
|
+
}
|
|
61
|
+
return folderName.replace(/-/g, '/');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 获取指定目录的历史会话列表
|
|
65
|
+
export async function getHistorySessions(workDir) {
|
|
66
|
+
const projectsDir = getClaudeProjectsDir();
|
|
67
|
+
const projectFolder = pathToProjectFolder(workDir);
|
|
68
|
+
const projectPath = join(projectsDir, projectFolder);
|
|
69
|
+
|
|
70
|
+
console.log(`Looking for sessions in: ${projectPath}`);
|
|
71
|
+
|
|
72
|
+
if (!existsSync(projectPath)) {
|
|
73
|
+
console.log(`Project folder not found: ${projectPath}`);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const sessions = [];
|
|
78
|
+
const files = readdirSync(projectPath);
|
|
79
|
+
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
if (file.endsWith('.jsonl') && file !== 'sessions-index.json') {
|
|
82
|
+
const sessionId = file.replace('.jsonl', '');
|
|
83
|
+
const filePath = join(projectPath, file);
|
|
84
|
+
const stats = statSync(filePath);
|
|
85
|
+
|
|
86
|
+
let title = '';
|
|
87
|
+
let firstMessage = '';
|
|
88
|
+
let hasUserMessage = false;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
92
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
93
|
+
|
|
94
|
+
for (const line of lines.slice(0, 30)) {
|
|
95
|
+
try {
|
|
96
|
+
const data = JSON.parse(line);
|
|
97
|
+
if (data.type === 'user' && data.message?.content) {
|
|
98
|
+
const text = typeof data.message.content === 'string'
|
|
99
|
+
? data.message.content
|
|
100
|
+
: data.message.content[0]?.text || '';
|
|
101
|
+
if (text.trim()) {
|
|
102
|
+
firstMessage = text.substring(0, 100);
|
|
103
|
+
title = text.substring(0, 100);
|
|
104
|
+
hasUserMessage = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.error(`Error reading session file: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 只添加有实际用户消息的会话,过滤掉空会话
|
|
115
|
+
if (hasUserMessage) {
|
|
116
|
+
sessions.push({
|
|
117
|
+
sessionId,
|
|
118
|
+
workDir,
|
|
119
|
+
title: title || sessionId.slice(0, 8),
|
|
120
|
+
preview: firstMessage,
|
|
121
|
+
lastModified: stats.mtime.getTime(),
|
|
122
|
+
size: stats.size
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
129
|
+
return sessions;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 读取 session 文件中的历史消息
|
|
133
|
+
export function loadSessionHistory(workDir, claudeSessionId, limit = 500) {
|
|
134
|
+
const projectsDir = getClaudeProjectsDir();
|
|
135
|
+
const projectFolder = pathToProjectFolder(workDir);
|
|
136
|
+
const sessionFile = join(projectsDir, projectFolder, `${claudeSessionId}.jsonl`);
|
|
137
|
+
|
|
138
|
+
console.log(`Loading session history from: ${sessionFile}`);
|
|
139
|
+
|
|
140
|
+
if (!existsSync(sessionFile)) {
|
|
141
|
+
console.log(`Session file not found: ${sessionFile}`);
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const messages = [];
|
|
146
|
+
try {
|
|
147
|
+
const content = readFileSync(sessionFile, 'utf-8');
|
|
148
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
149
|
+
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
try {
|
|
152
|
+
const data = JSON.parse(line);
|
|
153
|
+
if (data.type === 'user' || data.type === 'assistant') {
|
|
154
|
+
messages.push(data);
|
|
155
|
+
}
|
|
156
|
+
} catch {}
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.error(`Error reading session file: ${e.message}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ★ Phase 6: 限制返回数量(取最后 N 条)
|
|
163
|
+
if (limit && messages.length > limit) {
|
|
164
|
+
return messages.slice(-limit);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return messages;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function handleListHistorySessions(msg) {
|
|
171
|
+
const { workDir, requestId } = msg;
|
|
172
|
+
const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
|
|
173
|
+
|
|
174
|
+
console.log(`Listing history sessions for: ${effectiveWorkDir}`);
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const sessions = await getHistorySessions(effectiveWorkDir);
|
|
178
|
+
ctx.sendToServer({
|
|
179
|
+
type: 'history_sessions_list',
|
|
180
|
+
requestId,
|
|
181
|
+
workDir: effectiveWorkDir,
|
|
182
|
+
sessions
|
|
183
|
+
});
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.error('Error listing history sessions:', e);
|
|
186
|
+
ctx.sendToServer({
|
|
187
|
+
type: 'history_sessions_list',
|
|
188
|
+
requestId,
|
|
189
|
+
workDir: effectiveWorkDir,
|
|
190
|
+
sessions: [],
|
|
191
|
+
error: e.message
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 列出 Claude projects 目录下的所有 folder (工作目录)
|
|
197
|
+
export async function handleListFolders(msg) {
|
|
198
|
+
const { requestId } = msg;
|
|
199
|
+
const projectsDir = getClaudeProjectsDir();
|
|
200
|
+
|
|
201
|
+
console.log(`Listing folders from: ${projectsDir}`);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const folders = [];
|
|
205
|
+
|
|
206
|
+
if (existsSync(projectsDir)) {
|
|
207
|
+
const entries = readdirSync(projectsDir);
|
|
208
|
+
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
const entryPath = join(projectsDir, entry);
|
|
211
|
+
const stats = statSync(entryPath);
|
|
212
|
+
|
|
213
|
+
if (stats.isDirectory()) {
|
|
214
|
+
// 从 session 文件读取真实的工作目录路径
|
|
215
|
+
const originalPath = getWorkDirFromProjectFolder(entryPath, entry);
|
|
216
|
+
|
|
217
|
+
// 获取该目录下的会话数量(只计算有实际用户消息的会话)
|
|
218
|
+
let sessionCount = 0;
|
|
219
|
+
let lastModified = stats.mtime.getTime();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const files = readdirSync(entryPath);
|
|
223
|
+
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
if (file.endsWith('.jsonl')) {
|
|
226
|
+
const filePath = join(entryPath, file);
|
|
227
|
+
const fileStats = statSync(filePath);
|
|
228
|
+
if (fileStats.mtime.getTime() > lastModified) {
|
|
229
|
+
lastModified = fileStats.mtime.getTime();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 检查文件是否包含用户消息(与 getHistorySessions 保持一致)
|
|
233
|
+
try {
|
|
234
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
235
|
+
const lines = content.split('\n').filter(l => l.trim()).slice(0, 30);
|
|
236
|
+
for (const line of lines) {
|
|
237
|
+
try {
|
|
238
|
+
const data = JSON.parse(line);
|
|
239
|
+
if (data.type === 'user' && data.message?.content) {
|
|
240
|
+
const text = typeof data.message.content === 'string'
|
|
241
|
+
? data.message.content
|
|
242
|
+
: data.message.content[0]?.text || '';
|
|
243
|
+
if (text.trim()) {
|
|
244
|
+
sessionCount++;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
249
|
+
}
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
|
|
255
|
+
folders.push({
|
|
256
|
+
name: entry,
|
|
257
|
+
path: originalPath,
|
|
258
|
+
sessionCount,
|
|
259
|
+
lastModified
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
folders.sort((a, b) => b.lastModified - a.lastModified);
|
|
266
|
+
|
|
267
|
+
console.log(`Found ${folders.length} folders, sending response...`);
|
|
268
|
+
ctx.sendToServer({
|
|
269
|
+
type: 'folders_list',
|
|
270
|
+
requestId,
|
|
271
|
+
folders
|
|
272
|
+
});
|
|
273
|
+
console.log(`folders_list sent with ${folders.length} folders`);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.error('Error listing folders:', e);
|
|
276
|
+
ctx.sendToServer({
|
|
277
|
+
type: 'folders_list',
|
|
278
|
+
requestId,
|
|
279
|
+
folders: [],
|
|
280
|
+
error: e.message
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { platform } from 'os';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import ctx from './context.js';
|
|
9
|
+
import { getConfigPath, loadServiceConfig } from './service.js';
|
|
10
|
+
import { loadNodePty } from './terminal.js';
|
|
11
|
+
import { connect } from './connection.js';
|
|
12
|
+
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
// Load package version
|
|
17
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
|
|
18
|
+
ctx.agentVersion = pkg.version;
|
|
19
|
+
|
|
20
|
+
// 配置文件路径(向后兼容:先查当前目录 .claude-agent.json)
|
|
21
|
+
const LOCAL_CONFIG_FILE = join(process.cwd(), '.claude-agent.json');
|
|
22
|
+
|
|
23
|
+
// 加载或创建配置
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
const defaults = {
|
|
26
|
+
serverUrl: 'ws://localhost:3456',
|
|
27
|
+
agentName: `Worker-${platform()}-${process.pid}`,
|
|
28
|
+
workDir: process.cwd(),
|
|
29
|
+
reconnectInterval: 5000,
|
|
30
|
+
agentSecret: 'agent-shared-secret'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Priority 1: Local .claude-agent.json (backward compat)
|
|
34
|
+
if (existsSync(LOCAL_CONFIG_FILE)) {
|
|
35
|
+
try {
|
|
36
|
+
const saved = JSON.parse(readFileSync(LOCAL_CONFIG_FILE, 'utf-8'));
|
|
37
|
+
const { agentId, ...rest } = saved;
|
|
38
|
+
return { ...defaults, ...rest };
|
|
39
|
+
} catch {
|
|
40
|
+
// fall through
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Priority 2: Standard config location (~/.config/yeaft-agent/config.json)
|
|
45
|
+
const serviceConfig = loadServiceConfig();
|
|
46
|
+
if (serviceConfig) {
|
|
47
|
+
return { ...defaults, ...serviceConfig };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return defaults;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function saveConfig(config) {
|
|
54
|
+
writeFileSync(LOCAL_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fileConfig = loadConfig();
|
|
58
|
+
const CONFIG = {
|
|
59
|
+
serverUrl: process.env.SERVER_URL || fileConfig.serverUrl,
|
|
60
|
+
agentName: process.env.AGENT_NAME || fileConfig.agentName,
|
|
61
|
+
workDir: process.env.WORK_DIR || fileConfig.workDir,
|
|
62
|
+
reconnectInterval: fileConfig.reconnectInterval,
|
|
63
|
+
agentSecret: process.env.AGENT_SECRET || fileConfig.agentSecret,
|
|
64
|
+
// 禁用的工具列表(逗号分隔),如 "mcp__github,mcp__sentry"
|
|
65
|
+
// 默认禁用所有 MCP 工具(避免超过 Claude API 128 工具限制)
|
|
66
|
+
// 设置 DISALLOWED_TOOLS=none 可取消默认禁用
|
|
67
|
+
disallowedTools: (() => {
|
|
68
|
+
const raw = process.env.DISALLOWED_TOOLS || fileConfig.disallowedTools || '';
|
|
69
|
+
if (raw === 'none') return [];
|
|
70
|
+
const list = raw.split(',').map(s => s.trim()).filter(Boolean);
|
|
71
|
+
return list.length > 0 ? list : ['mcp__*'];
|
|
72
|
+
})()
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// 初始化共享上下文
|
|
76
|
+
ctx.CONFIG = CONFIG;
|
|
77
|
+
ctx.saveConfig = saveConfig;
|
|
78
|
+
|
|
79
|
+
// Agent capabilities(启动时自动检测)
|
|
80
|
+
async function detectCapabilities() {
|
|
81
|
+
const capabilities = ['background_tasks', 'file_editor'];
|
|
82
|
+
const pty = await loadNodePty();
|
|
83
|
+
if (pty) capabilities.push('terminal');
|
|
84
|
+
console.log(`[Capabilities] Detected: ${capabilities.join(', ')}`);
|
|
85
|
+
return capabilities;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 确保依赖已安装(特别是 optionalDependencies 如 node-pty)
|
|
89
|
+
async function ensureDependencies() {
|
|
90
|
+
const agentDir = new URL('.', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
|
|
91
|
+
const nodeModulesPath = join(agentDir, 'node_modules');
|
|
92
|
+
|
|
93
|
+
// 检查 node_modules 是否存在
|
|
94
|
+
if (!existsSync(nodeModulesPath)) {
|
|
95
|
+
console.log('[Startup] node_modules not found, running npm install...');
|
|
96
|
+
try {
|
|
97
|
+
await execAsync('npm install', { cwd: agentDir, timeout: 120000 });
|
|
98
|
+
console.log('[Startup] npm install completed');
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.warn('[Startup] npm install failed:', e.message);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 检查 node-pty 是否可用(optionalDependency,可能需要编译)
|
|
106
|
+
try {
|
|
107
|
+
await import('node-pty');
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.log('[Startup] node-pty not available, attempting install...');
|
|
110
|
+
try {
|
|
111
|
+
await execAsync('npm install node-pty', { cwd: agentDir, timeout: 120000 });
|
|
112
|
+
console.log('[Startup] node-pty installed successfully');
|
|
113
|
+
} catch (installErr) {
|
|
114
|
+
console.warn('[Startup] node-pty install failed (terminal will be unavailable):', installErr.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 优雅退出
|
|
120
|
+
function cleanup() {
|
|
121
|
+
// 清理所有终端
|
|
122
|
+
for (const [, term] of ctx.terminals) {
|
|
123
|
+
if (term.pty) {
|
|
124
|
+
try { term.pty.kill(); } catch {}
|
|
125
|
+
}
|
|
126
|
+
if (term.timer) clearTimeout(term.timer);
|
|
127
|
+
}
|
|
128
|
+
ctx.terminals.clear();
|
|
129
|
+
|
|
130
|
+
for (const [, state] of ctx.conversations) {
|
|
131
|
+
if (state.abortController) {
|
|
132
|
+
state.abortController.abort();
|
|
133
|
+
}
|
|
134
|
+
if (state.inputStream) {
|
|
135
|
+
state.inputStream.done();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
ctx.conversations.clear();
|
|
139
|
+
if (ctx.ws) ctx.ws.close();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
process.on('SIGINT', () => {
|
|
143
|
+
console.log('Shutting down...');
|
|
144
|
+
cleanup();
|
|
145
|
+
process.exit(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
process.on('SIGTERM', () => {
|
|
149
|
+
console.log('Shutting down...');
|
|
150
|
+
cleanup();
|
|
151
|
+
process.exit(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// 启动 - 先确保依赖,再检测能力,再连接
|
|
155
|
+
(async () => {
|
|
156
|
+
await ensureDependencies();
|
|
157
|
+
ctx.agentCapabilities = await detectCapabilities();
|
|
158
|
+
connect();
|
|
159
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yeaft/webchat-agent",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"yeaft-agent": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"dev": "nodemon index.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude",
|
|
19
|
+
"agent",
|
|
20
|
+
"remote",
|
|
21
|
+
"websocket",
|
|
22
|
+
"cli",
|
|
23
|
+
"yeaft"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://github.com/yeaft/claude-web-chat",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/yeaft/claude-web-chat.git",
|
|
29
|
+
"directory": "agent"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/yeaft/claude-web-chat/issues"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"cli.js",
|
|
36
|
+
"service.js",
|
|
37
|
+
"index.js",
|
|
38
|
+
"context.js",
|
|
39
|
+
"connection.js",
|
|
40
|
+
"conversation.js",
|
|
41
|
+
"claude.js",
|
|
42
|
+
"terminal.js",
|
|
43
|
+
"proxy.js",
|
|
44
|
+
"workbench.js",
|
|
45
|
+
"history.js",
|
|
46
|
+
"encryption.js",
|
|
47
|
+
"sdk/"
|
|
48
|
+
],
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"author": "Yeaft",
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"nodemon": "^3.0.2"
|
|
53
|
+
},
|
|
54
|
+
"nodemonConfig": {
|
|
55
|
+
"watch": [
|
|
56
|
+
"*.js",
|
|
57
|
+
"sdk/*.js"
|
|
58
|
+
],
|
|
59
|
+
"ignore": [
|
|
60
|
+
"node_modules",
|
|
61
|
+
"package-lock.json",
|
|
62
|
+
".claude-agent.json"
|
|
63
|
+
],
|
|
64
|
+
"ext": "js"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"dotenv": "^16.3.1",
|
|
68
|
+
"tweetnacl": "^1.0.3",
|
|
69
|
+
"tweetnacl-util": "^0.15.1",
|
|
70
|
+
"ws": "^8.16.0"
|
|
71
|
+
},
|
|
72
|
+
"optionalDependencies": {
|
|
73
|
+
"node-pty": "^1.0.0"
|
|
74
|
+
}
|
|
75
|
+
}
|
package/proxy.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
import ctx from './context.js';
|
|
5
|
+
|
|
6
|
+
export function handleProxyHttpRequest(msg) {
|
|
7
|
+
const { requestId, port, method, path, headers, body, scheme, host } = msg;
|
|
8
|
+
const effectiveHost = host || 'localhost';
|
|
9
|
+
const effectiveScheme = scheme || 'http';
|
|
10
|
+
const httpModule = effectiveScheme === 'https' ? https : http;
|
|
11
|
+
|
|
12
|
+
const options = {
|
|
13
|
+
hostname: effectiveHost,
|
|
14
|
+
port,
|
|
15
|
+
path,
|
|
16
|
+
method,
|
|
17
|
+
headers: { ...headers },
|
|
18
|
+
timeout: 60000
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// For HTTPS: skip certificate verification for local services
|
|
22
|
+
if (effectiveScheme === 'https') {
|
|
23
|
+
options.rejectUnauthorized = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Clean hop-by-hop headers
|
|
27
|
+
delete options.headers['host'];
|
|
28
|
+
options.headers['host'] = `${effectiveHost}:${port}`;
|
|
29
|
+
delete options.headers['connection'];
|
|
30
|
+
delete options.headers['upgrade'];
|
|
31
|
+
delete options.headers['accept-encoding']; // Let agent handle raw data
|
|
32
|
+
|
|
33
|
+
const req = httpModule.request(options, (res) => {
|
|
34
|
+
const contentType = res.headers['content-type'] || '';
|
|
35
|
+
const isStreaming = (
|
|
36
|
+
contentType.includes('text/event-stream') ||
|
|
37
|
+
(contentType.includes('text/plain') && res.headers['transfer-encoding'] === 'chunked')
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (isStreaming) {
|
|
41
|
+
ctx.sendToServer({
|
|
42
|
+
type: 'proxy_response_chunk',
|
|
43
|
+
requestId,
|
|
44
|
+
statusCode: res.statusCode,
|
|
45
|
+
headers: res.headers
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
res.on('data', (chunk) => {
|
|
49
|
+
ctx.sendToServer({
|
|
50
|
+
type: 'proxy_response_chunk',
|
|
51
|
+
requestId,
|
|
52
|
+
chunk: chunk.toString('base64')
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
res.on('end', () => {
|
|
57
|
+
ctx.sendToServer({ type: 'proxy_response_end', requestId });
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
const chunks = [];
|
|
61
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
62
|
+
res.on('end', () => {
|
|
63
|
+
const responseBody = Buffer.concat(chunks);
|
|
64
|
+
ctx.sendToServer({
|
|
65
|
+
type: 'proxy_response',
|
|
66
|
+
requestId,
|
|
67
|
+
statusCode: res.statusCode,
|
|
68
|
+
headers: res.headers,
|
|
69
|
+
body: responseBody.toString('base64')
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
res.on('error', (err) => {
|
|
75
|
+
ctx.sendToServer({
|
|
76
|
+
type: 'proxy_response',
|
|
77
|
+
requestId,
|
|
78
|
+
statusCode: 502,
|
|
79
|
+
headers: { 'content-type': 'text/plain' },
|
|
80
|
+
body: Buffer.from(`Proxy stream error: ${err.message}`).toString('base64')
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
req.on('error', (err) => {
|
|
86
|
+
ctx.sendToServer({
|
|
87
|
+
type: 'proxy_response',
|
|
88
|
+
requestId,
|
|
89
|
+
statusCode: 502,
|
|
90
|
+
headers: { 'content-type': 'text/plain' },
|
|
91
|
+
body: Buffer.from(`Proxy error: ${err.message}`).toString('base64')
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
req.on('timeout', () => {
|
|
96
|
+
req.destroy();
|
|
97
|
+
ctx.sendToServer({
|
|
98
|
+
type: 'proxy_response',
|
|
99
|
+
requestId,
|
|
100
|
+
statusCode: 504,
|
|
101
|
+
headers: { 'content-type': 'text/plain' },
|
|
102
|
+
body: Buffer.from('Proxy request timeout').toString('base64')
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (body) req.write(Buffer.from(body, 'base64'));
|
|
107
|
+
req.end();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function handleProxyWsOpen(msg) {
|
|
111
|
+
const { proxyWsId, port, path, headers, scheme, host } = msg;
|
|
112
|
+
const effectiveHost = host || 'localhost';
|
|
113
|
+
const wsScheme = (scheme === 'https') ? 'wss' : 'ws';
|
|
114
|
+
const url = `${wsScheme}://${effectiveHost}:${port}${path || '/'}`;
|
|
115
|
+
|
|
116
|
+
const wsHeaders = { ...headers };
|
|
117
|
+
wsHeaders['host'] = `${effectiveHost}:${port}`;
|
|
118
|
+
|
|
119
|
+
const wsOptions = { headers: wsHeaders };
|
|
120
|
+
if (wsScheme === 'wss') {
|
|
121
|
+
wsOptions.rejectUnauthorized = false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const localWs = new WebSocket(url, wsOptions);
|
|
125
|
+
|
|
126
|
+
localWs.on('open', () => {
|
|
127
|
+
ctx.sendToServer({ type: 'proxy_ws_opened', proxyWsId });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
localWs.on('message', (data, isBinary) => {
|
|
131
|
+
ctx.sendToServer({
|
|
132
|
+
type: 'proxy_ws_message',
|
|
133
|
+
proxyWsId,
|
|
134
|
+
data: isBinary ? Buffer.from(data).toString('base64') : data.toString(),
|
|
135
|
+
isBinary
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
localWs.on('close', (code) => {
|
|
140
|
+
ctx.proxyWsSockets.delete(proxyWsId);
|
|
141
|
+
ctx.sendToServer({ type: 'proxy_ws_closed', proxyWsId, code });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
localWs.on('error', (err) => {
|
|
145
|
+
ctx.proxyWsSockets.delete(proxyWsId);
|
|
146
|
+
ctx.sendToServer({ type: 'proxy_ws_error', proxyWsId, error: err.message });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
ctx.proxyWsSockets.set(proxyWsId, localWs);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function handleProxyWsMessage(msg) {
|
|
153
|
+
const localWs = ctx.proxyWsSockets.get(msg.proxyWsId);
|
|
154
|
+
if (localWs && localWs.readyState === WebSocket.OPEN) {
|
|
155
|
+
if (msg.isBinary) {
|
|
156
|
+
localWs.send(Buffer.from(msg.data, 'base64'));
|
|
157
|
+
} else {
|
|
158
|
+
localWs.send(msg.data);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function handleProxyWsClose(msg) {
|
|
164
|
+
const localWs = ctx.proxyWsSockets.get(msg.proxyWsId);
|
|
165
|
+
if (localWs) {
|
|
166
|
+
localWs.close(msg.code || 1000);
|
|
167
|
+
ctx.proxyWsSockets.delete(msg.proxyWsId);
|
|
168
|
+
}
|
|
169
|
+
}
|
package/sdk/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code SDK integration for WebChat Agent
|
|
3
|
+
* Spawns Claude CLI as a subprocess with stream-json I/O
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { query, Query } from './query.js';
|
|
7
|
+
export { Stream } from './stream.js';
|
|
8
|
+
export { AbortError } from './types.js';
|
|
9
|
+
export { logDebug, getDefaultClaudeCodePath, isWindows } from './utils.js';
|