amalgm 0.0.0 → 0.0.1
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/README.md +37 -1
- package/bin/amalgm.js +8 -2
- package/lib/auth-store.js +223 -0
- package/lib/cli.js +1000 -0
- package/lib/paths.js +30 -0
- package/lib/supervisor.js +467 -0
- package/lib/tunnel-chat.js +328 -0
- package/lib/tunnel-events.js +499 -0
- package/package.json +29 -3
- package/runtime/README.md +4 -0
- package/runtime/lib/chatInput.js +306 -0
- package/runtime/lib/harnesses.js +988 -0
- package/runtime/lib/local/amalgmStore.js +128 -0
- package/runtime/lib/local/credentialResolver.js +425 -0
- package/runtime/lib/mcpApps/registry.js +619 -0
- package/runtime/package.json +5 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +165 -0
- package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
- package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
- package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -0
- package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +141 -0
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
- package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
- package/runtime/scripts/amalgm-mcp/config.js +138 -0
- package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
- package/runtime/scripts/amalgm-mcp/deps.js +40 -0
- package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
- package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
- package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +98 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
- package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
- package/runtime/scripts/amalgm-mcp/index.js +100 -0
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +393 -0
- package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
- package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
- package/runtime/scripts/amalgm-mcp/local/rest.js +80 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
- package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
- package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +139 -0
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
- package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
- package/runtime/scripts/chat-core/adapters/claude.js +163 -0
- package/runtime/scripts/chat-core/adapters/codex.js +313 -0
- package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
- package/runtime/scripts/chat-core/auth.js +177 -0
- package/runtime/scripts/chat-core/contract.js +326 -0
- package/runtime/scripts/chat-core/credentials/store.js +212 -0
- package/runtime/scripts/chat-core/egress.js +87 -0
- package/runtime/scripts/chat-core/engine.js +195 -0
- package/runtime/scripts/chat-core/event-schema.js +231 -0
- package/runtime/scripts/chat-core/events.js +190 -0
- package/runtime/scripts/chat-core/index.js +11 -0
- package/runtime/scripts/chat-core/input.js +50 -0
- package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
- package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
- package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
- package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
- package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
- package/runtime/scripts/chat-core/parts.js +253 -0
- package/runtime/scripts/chat-core/recorder.js +65 -0
- package/runtime/scripts/chat-core/runtime.js +86 -0
- package/runtime/scripts/chat-core/server.js +163 -0
- package/runtime/scripts/chat-core/sse.js +196 -0
- package/runtime/scripts/chat-core/stores.js +100 -0
- package/runtime/scripts/chat-core/tool-display.js +149 -0
- package/runtime/scripts/chat-core/tool-shape.js +143 -0
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
- package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
- package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
- package/runtime/scripts/chat-core/usage.js +343 -0
- package/runtime/scripts/chat-server/config.js +110 -0
- package/runtime/scripts/chat-server/db.js +529 -0
- package/runtime/scripts/chat-server/index.js +33 -0
- package/runtime/scripts/chat-server/model-catalog.js +327 -0
- package/runtime/scripts/chat-server.js +75 -0
- package/runtime/scripts/credential-adapter.js +129 -0
- package/runtime/scripts/fs-watcher.js +888 -0
- package/runtime/scripts/local-gateway.js +852 -0
- package/runtime/scripts/platform-context.txt +246 -0
- package/runtime/scripts/port-monitor.js +175 -0
- package/runtime/scripts/proxy-token-store.js +162 -0
- package/runtime/scripts/runtime-auth.js +163 -0
- package/runtime/scripts/test-claude-code-models.js +87 -0
- package/runtime/tsconfig.json +15 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /workspace/* REST routes for the Next.js API to call.
|
|
3
|
+
* Not part of the MCP tool surface — used by the internal UI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execFile } = require('child_process');
|
|
9
|
+
const { promisify } = require('util');
|
|
10
|
+
const { DEFAULT_CWD } = require('../config');
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
14
|
+
const WORKTREE_SUFFIX_RE = /-amalgm-wt-[a-z0-9]+$/;
|
|
15
|
+
|
|
16
|
+
function isValidWorkspacePath(workspacePath) {
|
|
17
|
+
return typeof workspacePath === 'string' && workspacePath.length > 0 && path.isAbsolute(workspacePath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function runGit(args, cwd) {
|
|
21
|
+
try {
|
|
22
|
+
const { stdout, stderr } = await execFileAsync('git', args, {
|
|
23
|
+
cwd,
|
|
24
|
+
maxBuffer: MAX_BUFFER,
|
|
25
|
+
});
|
|
26
|
+
return { stdout, stderr, exitCode: 0 };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
stdout: error?.stdout ?? '',
|
|
30
|
+
stderr: error?.stderr ?? error?.message ?? '',
|
|
31
|
+
exitCode: typeof error?.code === 'number' ? error.code : 1,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function isGitRepo(workspacePath) {
|
|
37
|
+
try {
|
|
38
|
+
await fs.promises.access(workspacePath);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = await runGit(['rev-parse', '--git-dir'], workspacePath);
|
|
44
|
+
return result.exitCode === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readBranch(projectPath) {
|
|
48
|
+
try {
|
|
49
|
+
const { stdout } = await execFileAsync('git', ['-C', projectPath, 'rev-parse', '--abbrev-ref', 'HEAD']);
|
|
50
|
+
const branch = stdout.trim();
|
|
51
|
+
return branch || null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseWorktreeList(output) {
|
|
58
|
+
const worktrees = [];
|
|
59
|
+
let current = {};
|
|
60
|
+
|
|
61
|
+
for (const line of output.trim().split('\n')) {
|
|
62
|
+
if (line.startsWith('worktree ')) {
|
|
63
|
+
if (current.path) worktrees.push(current);
|
|
64
|
+
current = { path: line.slice('worktree '.length).trim(), isMain: false };
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (line.startsWith('HEAD ')) {
|
|
68
|
+
current.head = line.slice('HEAD '.length).trim();
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (line.startsWith('branch ')) {
|
|
72
|
+
current.branch = line.slice('branch '.length).trim().replace(/^refs\/heads\//, '');
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (line === 'bare') {
|
|
76
|
+
current = {};
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (line === '') {
|
|
80
|
+
if (current.path) worktrees.push(current);
|
|
81
|
+
current = {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (current.path) worktrees.push(current);
|
|
86
|
+
if (worktrees.length > 0) worktrees[0].isMain = true;
|
|
87
|
+
return worktrees;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getLocalWorktreePath(workspacePath, shortId) {
|
|
91
|
+
const repoBase = path.basename(workspacePath).replace(WORKTREE_SUFFIX_RE, '');
|
|
92
|
+
return path.join(path.dirname(workspacePath), `${repoBase}-amalgm-wt-${shortId}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function handleProjects(sendJson) {
|
|
96
|
+
try {
|
|
97
|
+
const rootDir = DEFAULT_CWD;
|
|
98
|
+
const dirents = await fs.promises.readdir(rootDir, { withFileTypes: true }).catch(() => []);
|
|
99
|
+
const projects = [];
|
|
100
|
+
|
|
101
|
+
for (const dirent of dirents) {
|
|
102
|
+
if (!dirent.isDirectory() || dirent.name.startsWith('.')) continue;
|
|
103
|
+
|
|
104
|
+
const projectPath = path.join(rootDir, dirent.name);
|
|
105
|
+
const branch = await readBranch(projectPath);
|
|
106
|
+
|
|
107
|
+
projects.push({
|
|
108
|
+
name: dirent.name,
|
|
109
|
+
path: projectPath,
|
|
110
|
+
branch,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
115
|
+
sendJson(200, { projects });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
sendJson(500, {
|
|
118
|
+
error: error instanceof Error ? error.message : 'Failed to list projects',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function handleClone(body, sendJson) {
|
|
124
|
+
const cloneUrl = body?.clone_url;
|
|
125
|
+
const repoName = body?.repo_name;
|
|
126
|
+
const githubToken = typeof body?.githubToken === 'string' ? body.githubToken : '';
|
|
127
|
+
|
|
128
|
+
if (!cloneUrl || !repoName) {
|
|
129
|
+
sendJson(400, { error: 'clone_url and repo_name are required' });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (repoName.includes('/') || repoName.includes('..') || repoName.includes('\0')) {
|
|
134
|
+
sendJson(400, { error: 'Invalid repo name' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const targetPath = path.join(DEFAULT_CWD, repoName);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await fs.promises.access(targetPath);
|
|
142
|
+
if (await isGitRepo(targetPath)) {
|
|
143
|
+
sendJson(200, { success: true, path: targetPath, alreadyExists: true });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
sendJson(409, {
|
|
147
|
+
error: `Directory already exists and is not a Git repository: ${targetPath}`,
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
} catch {
|
|
151
|
+
// Does not exist yet.
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const cloneArgs = [];
|
|
155
|
+
if (githubToken && cloneUrl.includes('github.com')) {
|
|
156
|
+
const auth = Buffer.from(`x-access-token:${githubToken}`).toString('base64');
|
|
157
|
+
cloneArgs.push('-c', `http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`);
|
|
158
|
+
}
|
|
159
|
+
cloneArgs.push('clone', cloneUrl, targetPath);
|
|
160
|
+
|
|
161
|
+
const cloneResult = await runGit(cloneArgs, DEFAULT_CWD);
|
|
162
|
+
if (cloneResult.exitCode !== 0) {
|
|
163
|
+
sendJson(500, {
|
|
164
|
+
error: cloneResult.stderr.trim() || cloneResult.stdout.trim() || 'Clone failed',
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
sendJson(200, { success: true, path: targetPath, alreadyExists: false });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function handleBranches(query, sendJson) {
|
|
173
|
+
const workspacePath = query?.path;
|
|
174
|
+
if (!isValidWorkspacePath(workspacePath)) {
|
|
175
|
+
sendJson(400, { error: 'Invalid path' });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!(await isGitRepo(workspacePath))) {
|
|
180
|
+
sendJson(200, { branches: [], current: '', hasRemote: false, isGit: false });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const remoteResult = await runGit(['remote'], workspacePath);
|
|
185
|
+
const hasRemote = remoteResult.stdout.trim().length > 0;
|
|
186
|
+
if (hasRemote) {
|
|
187
|
+
await runGit(['fetch', '--prune'], workspacePath);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const currentBranch = (await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], workspacePath)).stdout.trim();
|
|
191
|
+
const localBranches = (await runGit(['branch', '--format=%(refname:short)'], workspacePath)).stdout
|
|
192
|
+
.split('\n')
|
|
193
|
+
.map((line) => line.trim())
|
|
194
|
+
.filter(Boolean);
|
|
195
|
+
const remoteBranches = (await runGit(['branch', '-r', '--format=%(refname:short)'], workspacePath)).stdout
|
|
196
|
+
.split('\n')
|
|
197
|
+
.map((line) => line.trim())
|
|
198
|
+
.filter(Boolean);
|
|
199
|
+
|
|
200
|
+
const branches = [];
|
|
201
|
+
const seen = new Set();
|
|
202
|
+
|
|
203
|
+
for (const name of localBranches) {
|
|
204
|
+
if (seen.has(name)) continue;
|
|
205
|
+
seen.add(name);
|
|
206
|
+
branches.push({ name, current: name === currentBranch, remote: false });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const raw of remoteBranches) {
|
|
210
|
+
if (!raw || raw.includes(' -> ')) continue;
|
|
211
|
+
const name = raw.replace(/^[^/]+\//, '');
|
|
212
|
+
if (!name || seen.has(name)) continue;
|
|
213
|
+
seen.add(name);
|
|
214
|
+
branches.push({ name, current: name === currentBranch, remote: true });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
branches.sort((a, b) => {
|
|
218
|
+
if (a.current && !b.current) return -1;
|
|
219
|
+
if (!a.current && b.current) return 1;
|
|
220
|
+
if (!a.remote && b.remote) return -1;
|
|
221
|
+
if (a.remote && !b.remote) return 1;
|
|
222
|
+
return a.name.localeCompare(b.name);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
sendJson(200, { branches, current: currentBranch, hasRemote, isGit: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function handleCheckout(body, sendJson) {
|
|
229
|
+
const workspacePath = body?.path;
|
|
230
|
+
const branch = body?.branch;
|
|
231
|
+
|
|
232
|
+
if (!isValidWorkspacePath(workspacePath)) {
|
|
233
|
+
sendJson(400, { error: 'Invalid path' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (typeof branch !== 'string' || !branch.trim()) {
|
|
237
|
+
sendJson(400, { error: 'Invalid branch' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!(await isGitRepo(workspacePath))) {
|
|
241
|
+
sendJson(400, { error: 'Not a git repository' });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const checkoutResult = await runGit(['checkout', branch], workspacePath);
|
|
246
|
+
const currentBranch = (await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], workspacePath)).stdout.trim() || branch;
|
|
247
|
+
|
|
248
|
+
if (checkoutResult.exitCode !== 0) {
|
|
249
|
+
sendJson(400, {
|
|
250
|
+
error: checkoutResult.stderr || checkoutResult.stdout || 'Checkout failed',
|
|
251
|
+
branch: currentBranch,
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
sendJson(200, { success: true, branch: currentBranch });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function handleDiff(query, sendJson) {
|
|
260
|
+
const workspacePath = query?.path;
|
|
261
|
+
if (!isValidWorkspacePath(workspacePath)) {
|
|
262
|
+
sendJson(400, { error: 'Invalid path' });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!(await isGitRepo(workspacePath))) {
|
|
266
|
+
sendJson(200, { isGit: false, files: [], diff: '', additions: 0, deletions: 0 });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const runDiff = async (...args) => {
|
|
271
|
+
const withHead = await runGit(['diff', ...args, 'HEAD'], workspacePath);
|
|
272
|
+
if (withHead.exitCode === 0) return withHead.stdout;
|
|
273
|
+
return (await runGit(['diff', ...args], workspacePath)).stdout;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const stat = (await runDiff('--shortstat')).trim();
|
|
277
|
+
const diff = (await runDiff()).trim();
|
|
278
|
+
const changedFiles = (await runDiff('--name-status')).trim();
|
|
279
|
+
const untracked = (await runGit(['ls-files', '--others', '--exclude-standard'], workspacePath)).stdout.trim();
|
|
280
|
+
|
|
281
|
+
let additions = 0;
|
|
282
|
+
let deletions = 0;
|
|
283
|
+
const addMatch = stat.match(/(\d+) insertion/);
|
|
284
|
+
const delMatch = stat.match(/(\d+) deletion/);
|
|
285
|
+
if (addMatch) additions = parseInt(addMatch[1], 10);
|
|
286
|
+
if (delMatch) deletions = parseInt(delMatch[1], 10);
|
|
287
|
+
|
|
288
|
+
const files = [];
|
|
289
|
+
for (const line of changedFiles.split('\n').filter(Boolean)) {
|
|
290
|
+
const [status, ...pathParts] = line.split('\t');
|
|
291
|
+
if (status && pathParts.length > 0) files.push({ path: pathParts.join('\t'), status: status.trim() });
|
|
292
|
+
}
|
|
293
|
+
for (const line of untracked.split('\n').filter(Boolean)) {
|
|
294
|
+
files.push({ path: line.trim(), status: '?' });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
sendJson(200, { isGit: true, files, diff, additions, deletions });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function handleWorktreeList(query, sendJson) {
|
|
301
|
+
const workspacePath = query?.path;
|
|
302
|
+
if (!isValidWorkspacePath(workspacePath)) {
|
|
303
|
+
sendJson(400, { error: 'Invalid path' });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!(await isGitRepo(workspacePath))) {
|
|
307
|
+
sendJson(200, { worktrees: [], isGit: false });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const result = await runGit(['worktree', 'list', '--porcelain'], workspacePath);
|
|
312
|
+
sendJson(200, { worktrees: parseWorktreeList(result.stdout), isGit: true });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function handleWorktreeCreate(body, sendJson) {
|
|
316
|
+
const workspacePath = body?.path;
|
|
317
|
+
if (!isValidWorkspacePath(workspacePath)) {
|
|
318
|
+
sendJson(400, { error: 'Invalid path' });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (!(await isGitRepo(workspacePath))) {
|
|
322
|
+
sendJson(400, { error: 'Not a git repository' });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const shortId = Math.random().toString(36).slice(2, 8);
|
|
327
|
+
const currentBranch = (await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], workspacePath)).stdout.trim() || 'HEAD';
|
|
328
|
+
const worktreePath = getLocalWorktreePath(workspacePath, shortId);
|
|
329
|
+
const branch = `amalgm-${shortId}-${currentBranch}`;
|
|
330
|
+
const createResult = await runGit(['worktree', 'add', worktreePath, '-b', branch, 'HEAD'], workspacePath);
|
|
331
|
+
|
|
332
|
+
if (createResult.exitCode !== 0) {
|
|
333
|
+
sendJson(500, {
|
|
334
|
+
error: `Failed to create worktree: ${createResult.stderr || createResult.stdout}`,
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
sendJson(200, { success: true, worktreePath, branch });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function handleWorktreeDelete(body, sendJson) {
|
|
343
|
+
const workspacePath = body?.path;
|
|
344
|
+
const worktreePath = body?.worktreePath;
|
|
345
|
+
if (!isValidWorkspacePath(workspacePath)) {
|
|
346
|
+
sendJson(400, { error: 'Invalid path' });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!isValidWorkspacePath(worktreePath)) {
|
|
350
|
+
sendJson(400, { error: 'Invalid worktree path' });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (workspacePath === worktreePath) {
|
|
354
|
+
sendJson(400, { error: 'Cannot remove the main worktree' });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!(await isGitRepo(workspacePath))) {
|
|
358
|
+
sendJson(400, { error: 'Not a git repository' });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const branch = (await runGit(['-C', worktreePath, 'rev-parse', '--abbrev-ref', 'HEAD'], workspacePath)).stdout.trim();
|
|
363
|
+
const removeResult = await runGit(['worktree', 'remove', worktreePath, '--force'], workspacePath);
|
|
364
|
+
|
|
365
|
+
if (removeResult.exitCode !== 0) {
|
|
366
|
+
sendJson(500, {
|
|
367
|
+
error: `Failed to remove worktree: ${removeResult.stderr || removeResult.stdout}`,
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (branch.startsWith('amalgm-')) {
|
|
373
|
+
await runGit(['branch', '-D', branch], workspacePath);
|
|
374
|
+
}
|
|
375
|
+
await runGit(['worktree', 'prune'], workspacePath);
|
|
376
|
+
|
|
377
|
+
sendJson(200, { success: true });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = {
|
|
381
|
+
handleProjects,
|
|
382
|
+
handleClone,
|
|
383
|
+
handleBranches,
|
|
384
|
+
handleCheckout,
|
|
385
|
+
handleDiff,
|
|
386
|
+
handleWorktreeList,
|
|
387
|
+
handleWorktreeCreate,
|
|
388
|
+
handleWorktreeDelete,
|
|
389
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { done, errorEvent, reasoningStarted, usageFinal } = require('../events');
|
|
5
|
+
const { promptText } = require('../input');
|
|
6
|
+
const { runtimeEnv } = require('../auth');
|
|
7
|
+
const { normalizeClaudeMessage, usageRecordsFromClaudeResult, usageFromClaude } = require('../normalizers/claude');
|
|
8
|
+
const { recordNativeEvent } = require('../recorder');
|
|
9
|
+
const { toClaudeMcpServers } = require('../tooling/mcp-bundle');
|
|
10
|
+
const { bundledClaudeBinary } = require('../tooling/native-binaries');
|
|
11
|
+
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
12
|
+
|
|
13
|
+
function redactSecrets(text) {
|
|
14
|
+
return String(text || '')
|
|
15
|
+
.replace(/\b(npm_[A-Za-z0-9]{20,})\b/g, '[REDACTED_NPM_TOKEN]')
|
|
16
|
+
.replace(/\b(sk-[A-Za-z0-9_-]{20,})\b/g, '[REDACTED_API_KEY]')
|
|
17
|
+
.replace(/\b(sk-ant-[A-Za-z0-9_-]{20,})\b/g, '[REDACTED_ANTHROPIC_KEY]')
|
|
18
|
+
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{20,}/gi, '$1[REDACTED_TOKEN]');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class ClaudeAdapter {
|
|
22
|
+
async create(contract) {
|
|
23
|
+
return {
|
|
24
|
+
sessionId: contract.sessionId,
|
|
25
|
+
providerSessionId: contract.providerSessionId || null,
|
|
26
|
+
currentCliModel: contract.cliModel,
|
|
27
|
+
currentQuery: null,
|
|
28
|
+
closed: false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
options(contract, extra = {}) {
|
|
33
|
+
const systemPrompt = composeSystemPrompt(contract);
|
|
34
|
+
const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
|
|
35
|
+
return {
|
|
36
|
+
cwd: contract.cwd,
|
|
37
|
+
env: runtimeEnv(contract),
|
|
38
|
+
model: contract.cliModel,
|
|
39
|
+
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
|
|
40
|
+
...(systemPrompt ? { systemPrompt: { type: 'preset', preset: 'claude_code', append: systemPrompt } } : {}),
|
|
41
|
+
mcpServers: toClaudeMcpServers(contract),
|
|
42
|
+
permissionMode: 'bypassPermissions',
|
|
43
|
+
allowDangerouslySkipPermissions: true,
|
|
44
|
+
...(process.env.CHAT_CORE_DEBUG_CLAUDE === '1'
|
|
45
|
+
? { debug: true, debugFile: path.join(contract.auth.runtimeHome || process.cwd(), 'claude-debug.log') }
|
|
46
|
+
: {}),
|
|
47
|
+
settingSources: [],
|
|
48
|
+
strictMcpConfig: false,
|
|
49
|
+
...extra,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async resume(session, contract) {
|
|
54
|
+
session.currentCliModel = contract.cliModel;
|
|
55
|
+
session.closed = false;
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async stop(session) {
|
|
60
|
+
session.currentQuery?.close?.();
|
|
61
|
+
session.currentQuery = null;
|
|
62
|
+
session.closed = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async destroy(session) {
|
|
66
|
+
session.currentQuery?.close?.();
|
|
67
|
+
session.currentQuery = null;
|
|
68
|
+
session.closed = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async *prompt(session, input, contract) {
|
|
72
|
+
const contextLimit = Number(contract.contextWindow || 0) || undefined;
|
|
73
|
+
const state = {
|
|
74
|
+
providerSessionId: session.providerSessionId,
|
|
75
|
+
contextLimit,
|
|
76
|
+
claudeTools: new Map(),
|
|
77
|
+
claudeCompletedTools: new Set(),
|
|
78
|
+
claudeUsageSnapshotFingerprints: new Set(),
|
|
79
|
+
};
|
|
80
|
+
const stderrChunks = [];
|
|
81
|
+
const captureStderr = (chunk) => {
|
|
82
|
+
const text = redactSecrets(String(chunk || ''));
|
|
83
|
+
if (!text) return;
|
|
84
|
+
stderrChunks.push(text);
|
|
85
|
+
while (stderrChunks.length > 20) stderrChunks.shift();
|
|
86
|
+
if (process.env.CHAT_CORE_DEBUG_CLAUDE === '1' && text.trim()) {
|
|
87
|
+
console.warn('[Claude]', text.trim().slice(0, 2000));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
const sdk = await import('@anthropic-ai/claude-agent-sdk');
|
|
92
|
+
const query = sdk.query({
|
|
93
|
+
prompt: promptText(input, contract),
|
|
94
|
+
options: {
|
|
95
|
+
...this.options(contract, { stderr: captureStderr }),
|
|
96
|
+
...(session.providerSessionId ? { resume: session.providerSessionId } : {}),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
session.currentQuery = query;
|
|
100
|
+
session.closed = false;
|
|
101
|
+
yield reasoningStarted({ providerSessionId: state.providerSessionId });
|
|
102
|
+
for await (const message of query) {
|
|
103
|
+
recordNativeEvent('claude.sdk.message', message, {
|
|
104
|
+
providerSessionId: state.providerSessionId,
|
|
105
|
+
sessionId: contract.sessionId,
|
|
106
|
+
assistantMessageId: contract.assistantMessageId,
|
|
107
|
+
});
|
|
108
|
+
if (message.session_id) {
|
|
109
|
+
session.providerSessionId = message.session_id;
|
|
110
|
+
state.providerSessionId = message.session_id;
|
|
111
|
+
}
|
|
112
|
+
const normalized = normalizeClaudeMessage(message, state);
|
|
113
|
+
if (normalized.length > 0) {
|
|
114
|
+
for (const e of normalized) yield e;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (message.type === 'result') {
|
|
118
|
+
const usageRecords = usageRecordsFromClaudeResult(message, contextLimit);
|
|
119
|
+
if (usageRecords.length === 0) {
|
|
120
|
+
usageRecords.push(usageFromClaude(message.usage, contextLimit));
|
|
121
|
+
}
|
|
122
|
+
for (const [index, usageRecord] of usageRecords.entries()) {
|
|
123
|
+
yield usageFinal({
|
|
124
|
+
...usageRecord,
|
|
125
|
+
costUsd: usageRecords.length === 1 ? message.total_cost_usd ?? null : null,
|
|
126
|
+
}, {
|
|
127
|
+
providerSessionId: state.providerSessionId,
|
|
128
|
+
raw: {
|
|
129
|
+
source: 'claude.sdk.message',
|
|
130
|
+
method: 'claude/result',
|
|
131
|
+
payload: {
|
|
132
|
+
...message,
|
|
133
|
+
usageRecordIndex: index,
|
|
134
|
+
usageRecordCount: usageRecords.length,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (message.is_error) {
|
|
140
|
+
yield errorEvent(message.errors?.join('\n') || message.stop_reason || 'Claude turn failed', {
|
|
141
|
+
providerSessionId: state.providerSessionId,
|
|
142
|
+
raw: { source: 'claude.sdk.message', method: 'claude/result', payload: message },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
yield done({
|
|
146
|
+
providerSessionId: state.providerSessionId,
|
|
147
|
+
stopReason: message.stop_reason || 'end_turn',
|
|
148
|
+
raw: { source: 'claude.sdk.message', method: 'claude/result', payload: message },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const stderrTail = stderrChunks.join('').trim().slice(-3000);
|
|
154
|
+
const message = stderrTail ? `${err.message}\n${stderrTail}` : err.message;
|
|
155
|
+
yield errorEvent(message, { providerSessionId: session.providerSessionId });
|
|
156
|
+
yield done({ providerSessionId: session.providerSessionId, stopReason: session.closed ? 'cancelled' : 'error' });
|
|
157
|
+
} finally {
|
|
158
|
+
session.currentQuery = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { ClaudeAdapter };
|