amalgm 0.1.36 → 0.1.38
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/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +29 -2
- package/runtime/scripts/amalgm-mcp/events/executor.js +6 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +13 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +13 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +6 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +9 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +25 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +10 -0
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +9 -3
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +13 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-store.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +15 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +162 -22
- package/runtime/scripts/amalgm-mcp/workspace/store.js +278 -0
- package/runtime/scripts/chat-core/adapters/claude.js +2 -1
- package/runtime/scripts/chat-core/adapters/codex.js +44 -4
- package/runtime/scripts/chat-core/adapters/opencode.js +2 -1
- package/runtime/scripts/chat-core/contract.js +57 -0
- package/runtime/scripts/chat-core/engine.js +5 -5
- package/runtime/scripts/chat-core/server.js +17 -4
- package/runtime/scripts/chat-core/sse.js +8 -1
- package/runtime/scripts/chat-core/stores.js +3 -1
- package/runtime/scripts/chat-core/tooling/active-memory.js +396 -0
- package/runtime/scripts/chat-core/tooling/package-import.js +108 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -0
- package/runtime/scripts/chat-server/db.js +38 -9
- package/runtime/scripts/local-gateway.js +158 -0
|
@@ -8,6 +8,13 @@ const path = require('path');
|
|
|
8
8
|
const { execFile } = require('child_process');
|
|
9
9
|
const { promisify } = require('util');
|
|
10
10
|
const { AMALGM_DIR, DEFAULT_CWD } = require('../config');
|
|
11
|
+
const activeMemory = require('../../chat-core/tooling/active-memory');
|
|
12
|
+
const {
|
|
13
|
+
listWorkspaces,
|
|
14
|
+
removeWorkspace,
|
|
15
|
+
renameWorkspace,
|
|
16
|
+
upsertWorkspace,
|
|
17
|
+
} = require('./store');
|
|
11
18
|
|
|
12
19
|
const execFileAsync = promisify(execFile);
|
|
13
20
|
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
@@ -41,6 +48,16 @@ async function ensureWorkspaceRoot() {
|
|
|
41
48
|
return workspaceRoot;
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
function ensureProjectMemory(workspace, source = 'workspace') {
|
|
52
|
+
if (!workspace) return;
|
|
53
|
+
activeMemory.ensureConstructMemory({
|
|
54
|
+
type: 'project',
|
|
55
|
+
id: workspace.id,
|
|
56
|
+
name: workspace.name,
|
|
57
|
+
projectPath: workspace.path,
|
|
58
|
+
}, { source });
|
|
59
|
+
}
|
|
60
|
+
|
|
44
61
|
async function uniqueProjectPath(workspaceRoot, requestedName) {
|
|
45
62
|
const name = sanitizeProjectName(requestedName);
|
|
46
63
|
let candidate = path.join(workspaceRoot, name);
|
|
@@ -60,6 +77,11 @@ function isValidWorkspacePath(workspacePath) {
|
|
|
60
77
|
return typeof workspacePath === 'string' && workspacePath.length > 0 && path.isAbsolute(workspacePath);
|
|
61
78
|
}
|
|
62
79
|
|
|
80
|
+
function shouldRefreshBranches(query) {
|
|
81
|
+
const value = query?.refresh;
|
|
82
|
+
return value === true || value === 'true' || value === '1' || value === 'yes';
|
|
83
|
+
}
|
|
84
|
+
|
|
63
85
|
async function runGit(args, cwd) {
|
|
64
86
|
try {
|
|
65
87
|
const { stdout, stderr } = await execFileAsync('git', args, {
|
|
@@ -137,28 +159,100 @@ function getLocalWorktreePath(workspacePath, shortId) {
|
|
|
137
159
|
|
|
138
160
|
async function handleProjects(sendJson) {
|
|
139
161
|
try {
|
|
140
|
-
const rootDir =
|
|
141
|
-
const
|
|
142
|
-
|
|
162
|
+
const rootDir = getWorkspaceRoot();
|
|
163
|
+
const projects = await Promise.all(
|
|
164
|
+
listWorkspaces().map(async (workspace) => ({
|
|
165
|
+
...workspace,
|
|
166
|
+
branch: workspace.path ? await readBranch(workspace.path) : null,
|
|
167
|
+
})),
|
|
168
|
+
);
|
|
143
169
|
|
|
144
|
-
|
|
145
|
-
|
|
170
|
+
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
171
|
+
sendJson(200, { projects, workspaceRoot: rootDir });
|
|
172
|
+
} catch (error) {
|
|
173
|
+
sendJson(500, {
|
|
174
|
+
error: error instanceof Error ? error.message : 'Failed to list workspaces',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
146
178
|
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
async function handleProjectRegister(body, sendJson) {
|
|
180
|
+
const workspacePath = body?.path || body?.source_path;
|
|
181
|
+
const name = body?.name || body?.project_name;
|
|
149
182
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
183
|
+
if (!workspacePath || typeof workspacePath !== 'string') {
|
|
184
|
+
sendJson(400, { error: 'path is required' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!path.isAbsolute(workspacePath)) {
|
|
189
|
+
sendJson(400, { error: 'path must be absolute' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const sourceStats = await fs.promises.stat(workspacePath);
|
|
195
|
+
if (!sourceStats.isDirectory()) {
|
|
196
|
+
sendJson(400, { error: 'path must be a folder' });
|
|
197
|
+
return;
|
|
155
198
|
}
|
|
156
199
|
|
|
157
|
-
|
|
158
|
-
|
|
200
|
+
const workspaceRoot = await ensureWorkspaceRoot();
|
|
201
|
+
const workspace = upsertWorkspace({
|
|
202
|
+
path: await fs.promises.realpath(workspacePath),
|
|
203
|
+
name,
|
|
204
|
+
source: body?.source || 'computer',
|
|
205
|
+
}, {
|
|
206
|
+
workspaceRoot,
|
|
207
|
+
source: 'workspace:register',
|
|
208
|
+
});
|
|
209
|
+
ensureProjectMemory(workspace, 'workspace:register');
|
|
210
|
+
|
|
211
|
+
sendJson(200, {
|
|
212
|
+
success: true,
|
|
213
|
+
workspace,
|
|
214
|
+
path: workspace.path,
|
|
215
|
+
name: workspace.name,
|
|
216
|
+
uri: workspace.uri,
|
|
217
|
+
workspaceRoot,
|
|
218
|
+
});
|
|
159
219
|
} catch (error) {
|
|
160
220
|
sendJson(500, {
|
|
161
|
-
error: error instanceof Error ? error.message : 'Failed to
|
|
221
|
+
error: error instanceof Error ? error.message : 'Failed to register workspace',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handleProjectUpdate(body, sendJson) {
|
|
227
|
+
const identifier = body?.id || body?.path;
|
|
228
|
+
if (!identifier || typeof identifier !== 'string') {
|
|
229
|
+
sendJson(400, { error: 'id or path is required' });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const workspace = renameWorkspace(identifier, body?.name, { source: 'workspace:update' });
|
|
235
|
+
ensureProjectMemory(workspace, 'workspace:update');
|
|
236
|
+
sendJson(200, { success: true, workspace, path: workspace.path, name: workspace.name });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const message = error instanceof Error ? error.message : 'Failed to update workspace';
|
|
239
|
+
sendJson(message === 'Workspace not found' ? 404 : 500, { error: message });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function handleProjectDelete(body, sendJson) {
|
|
244
|
+
const identifier = body?.id || body?.path;
|
|
245
|
+
if (!identifier || typeof identifier !== 'string') {
|
|
246
|
+
sendJson(400, { error: 'id or path is required' });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const workspace = removeWorkspace(identifier, { source: 'workspace:delete' });
|
|
252
|
+
sendJson(200, { success: true, workspace });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
sendJson(500, {
|
|
255
|
+
error: error instanceof Error ? error.message : 'Failed to remove workspace',
|
|
162
256
|
});
|
|
163
257
|
}
|
|
164
258
|
}
|
|
@@ -191,10 +285,21 @@ async function handleImport(body, sendJson) {
|
|
|
191
285
|
]);
|
|
192
286
|
|
|
193
287
|
if (isWithin(realWorkspaceRoot, realSource)) {
|
|
288
|
+
const workspace = upsertWorkspace({
|
|
289
|
+
path: realSource,
|
|
290
|
+
name: projectName || path.basename(realSource),
|
|
291
|
+
source: 'managed',
|
|
292
|
+
managed: true,
|
|
293
|
+
}, {
|
|
294
|
+
workspaceRoot,
|
|
295
|
+
source: 'workspace:import',
|
|
296
|
+
});
|
|
297
|
+
ensureProjectMemory(workspace, 'workspace:import');
|
|
194
298
|
sendJson(200, {
|
|
195
299
|
success: true,
|
|
196
|
-
path:
|
|
197
|
-
name:
|
|
300
|
+
path: workspace.path,
|
|
301
|
+
name: workspace.name,
|
|
302
|
+
workspace,
|
|
198
303
|
workspaceRoot,
|
|
199
304
|
alreadyInWorkspace: true,
|
|
200
305
|
});
|
|
@@ -211,11 +316,22 @@ async function handleImport(body, sendJson) {
|
|
|
211
316
|
errorOnExist: true,
|
|
212
317
|
dereference: false,
|
|
213
318
|
});
|
|
319
|
+
const workspace = upsertWorkspace({
|
|
320
|
+
path: targetPath,
|
|
321
|
+
name: path.basename(targetPath),
|
|
322
|
+
source: 'managed',
|
|
323
|
+
managed: true,
|
|
324
|
+
}, {
|
|
325
|
+
workspaceRoot,
|
|
326
|
+
source: 'workspace:import',
|
|
327
|
+
});
|
|
328
|
+
ensureProjectMemory(workspace, 'workspace:import');
|
|
214
329
|
|
|
215
330
|
sendJson(200, {
|
|
216
331
|
success: true,
|
|
217
|
-
path:
|
|
218
|
-
name:
|
|
332
|
+
path: workspace.path,
|
|
333
|
+
name: workspace.name,
|
|
334
|
+
workspace,
|
|
219
335
|
workspaceRoot,
|
|
220
336
|
alreadyInWorkspace: false,
|
|
221
337
|
});
|
|
@@ -247,7 +363,17 @@ async function handleClone(body, sendJson) {
|
|
|
247
363
|
try {
|
|
248
364
|
await fs.promises.access(targetPath);
|
|
249
365
|
if (await isGitRepo(targetPath)) {
|
|
250
|
-
|
|
366
|
+
const workspace = upsertWorkspace({
|
|
367
|
+
path: targetPath,
|
|
368
|
+
name: repoName,
|
|
369
|
+
source: 'github',
|
|
370
|
+
managed: true,
|
|
371
|
+
}, {
|
|
372
|
+
workspaceRoot,
|
|
373
|
+
source: 'workspace:clone',
|
|
374
|
+
});
|
|
375
|
+
ensureProjectMemory(workspace, 'workspace:clone');
|
|
376
|
+
sendJson(200, { success: true, path: workspace.path, workspace, workspaceRoot, alreadyExists: true });
|
|
251
377
|
return;
|
|
252
378
|
}
|
|
253
379
|
sendJson(409, {
|
|
@@ -273,7 +399,18 @@ async function handleClone(body, sendJson) {
|
|
|
273
399
|
return;
|
|
274
400
|
}
|
|
275
401
|
|
|
276
|
-
|
|
402
|
+
const workspace = upsertWorkspace({
|
|
403
|
+
path: targetPath,
|
|
404
|
+
name: repoName,
|
|
405
|
+
source: 'github',
|
|
406
|
+
managed: true,
|
|
407
|
+
}, {
|
|
408
|
+
workspaceRoot,
|
|
409
|
+
source: 'workspace:clone',
|
|
410
|
+
});
|
|
411
|
+
ensureProjectMemory(workspace, 'workspace:clone');
|
|
412
|
+
|
|
413
|
+
sendJson(200, { success: true, path: workspace.path, workspace, workspaceRoot, alreadyExists: false });
|
|
277
414
|
}
|
|
278
415
|
|
|
279
416
|
async function handleBranches(query, sendJson) {
|
|
@@ -290,7 +427,7 @@ async function handleBranches(query, sendJson) {
|
|
|
290
427
|
|
|
291
428
|
const remoteResult = await runGit(['remote'], workspacePath);
|
|
292
429
|
const hasRemote = remoteResult.stdout.trim().length > 0;
|
|
293
|
-
if (hasRemote) {
|
|
430
|
+
if (hasRemote && shouldRefreshBranches(query)) {
|
|
294
431
|
await runGit(['fetch', '--prune'], workspacePath);
|
|
295
432
|
}
|
|
296
433
|
|
|
@@ -486,6 +623,9 @@ async function handleWorktreeDelete(body, sendJson) {
|
|
|
486
623
|
|
|
487
624
|
module.exports = {
|
|
488
625
|
handleProjects,
|
|
626
|
+
handleProjectRegister,
|
|
627
|
+
handleProjectUpdate,
|
|
628
|
+
handleProjectDelete,
|
|
489
629
|
handleImport,
|
|
490
630
|
handleClone,
|
|
491
631
|
handleBranches,
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { AMALGM_COMPUTER_ID } = require('../config');
|
|
7
|
+
const { openLocalDb } = require('../state/db');
|
|
8
|
+
const { insertStateEvent, publishStateEvent } = require('../state/events');
|
|
9
|
+
|
|
10
|
+
function cleanString(value) {
|
|
11
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stableComputerId(input) {
|
|
15
|
+
return cleanString(input) || cleanString(AMALGM_COMPUTER_ID) || 'local-computer';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function encodePathForUri(localPath) {
|
|
19
|
+
return localPath
|
|
20
|
+
.split(path.sep)
|
|
21
|
+
.map((segment) => encodeURIComponent(segment))
|
|
22
|
+
.join('/');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function workspaceUriForPath(localPath, computerId) {
|
|
26
|
+
const normalizedPath = path.resolve(localPath);
|
|
27
|
+
return `computer://${encodeURIComponent(stableComputerId(computerId))}${encodePathForUri(normalizedPath)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function workspaceIdForUri(uri) {
|
|
31
|
+
return `ws_${crypto.createHash('sha256').update(uri).digest('hex').slice(0, 20)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isWithin(root, target) {
|
|
35
|
+
const relative = path.relative(root, target);
|
|
36
|
+
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isManagedWorkspacePath(localPath, workspaceRoot) {
|
|
40
|
+
const root = cleanString(workspaceRoot);
|
|
41
|
+
if (!root) return false;
|
|
42
|
+
return isWithin(path.resolve(root), path.resolve(localPath));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function basename(localPath) {
|
|
46
|
+
return path.basename(String(localPath || '').replace(/\/+$/, '')) || 'workspace';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nowIso() {
|
|
50
|
+
return new Date().toISOString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseJson(value, fallback) {
|
|
54
|
+
if (typeof value !== 'string' || !value) return fallback;
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(value);
|
|
57
|
+
} catch {
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeWorkspaceRecord(input, options = {}) {
|
|
63
|
+
const localPath = cleanString(input.path || input.localPath || input.local_path);
|
|
64
|
+
if (!localPath || !path.isAbsolute(localPath)) {
|
|
65
|
+
throw new Error('Workspace path must be an absolute local path');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolvedPath = path.resolve(localPath);
|
|
69
|
+
const computerId = stableComputerId(input.computerId || input.computer_id || options.computerId);
|
|
70
|
+
const uri = cleanString(input.uri) || workspaceUriForPath(resolvedPath, computerId);
|
|
71
|
+
const id = cleanString(input.id) || workspaceIdForUri(uri);
|
|
72
|
+
const createdAt = cleanString(input.createdAt || input.created_at) || nowIso();
|
|
73
|
+
const updatedAt = nowIso();
|
|
74
|
+
const managed = input.managed !== undefined
|
|
75
|
+
? Boolean(input.managed)
|
|
76
|
+
: isManagedWorkspacePath(resolvedPath, options.workspaceRoot);
|
|
77
|
+
const status = cleanString(input.status) || (fs.existsSync(resolvedPath) ? 'ready' : 'missing');
|
|
78
|
+
const source = cleanString(input.source) || (managed ? 'managed' : 'computer');
|
|
79
|
+
const name = cleanString(input.name) || basename(resolvedPath);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
name,
|
|
84
|
+
uri,
|
|
85
|
+
path: resolvedPath,
|
|
86
|
+
localPath: resolvedPath,
|
|
87
|
+
locationType: 'computer',
|
|
88
|
+
computerId,
|
|
89
|
+
source,
|
|
90
|
+
managed,
|
|
91
|
+
status,
|
|
92
|
+
createdAt,
|
|
93
|
+
updatedAt,
|
|
94
|
+
location: {
|
|
95
|
+
type: 'computer',
|
|
96
|
+
computerId,
|
|
97
|
+
path: resolvedPath,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function rowToWorkspace(row) {
|
|
103
|
+
if (!row) return null;
|
|
104
|
+
const parsed = parseJson(row.workspace_json, {});
|
|
105
|
+
return {
|
|
106
|
+
...parsed,
|
|
107
|
+
id: row.id,
|
|
108
|
+
name: row.name,
|
|
109
|
+
uri: row.uri,
|
|
110
|
+
path: row.path,
|
|
111
|
+
localPath: row.path,
|
|
112
|
+
locationType: row.location_type,
|
|
113
|
+
computerId: row.computer_id || parsed.computerId || null,
|
|
114
|
+
source: row.source,
|
|
115
|
+
managed: row.managed === 1,
|
|
116
|
+
status: row.status,
|
|
117
|
+
createdAt: row.created_at,
|
|
118
|
+
updatedAt: row.updated_at,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function publishWorkspaceEvents(database, op, workspace, source) {
|
|
123
|
+
const events = [
|
|
124
|
+
insertStateEvent(database, {
|
|
125
|
+
resource: 'workspaces',
|
|
126
|
+
op,
|
|
127
|
+
id: workspace.id,
|
|
128
|
+
value: op === 'delete' ? undefined : workspace,
|
|
129
|
+
source,
|
|
130
|
+
}),
|
|
131
|
+
insertStateEvent(database, {
|
|
132
|
+
resource: 'projects',
|
|
133
|
+
op,
|
|
134
|
+
id: workspace.id,
|
|
135
|
+
value: op === 'delete' ? undefined : workspace,
|
|
136
|
+
source,
|
|
137
|
+
}),
|
|
138
|
+
];
|
|
139
|
+
return events.filter(Boolean);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function publishEvents(events) {
|
|
143
|
+
for (const event of events) publishStateEvent(event);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function listWorkspaces() {
|
|
147
|
+
return openLocalDb()
|
|
148
|
+
.prepare('SELECT * FROM workspaces ORDER BY lower(name), path')
|
|
149
|
+
.all()
|
|
150
|
+
.map(rowToWorkspace)
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function upsertWorkspace(input, options = {}) {
|
|
155
|
+
const db = openLocalDb();
|
|
156
|
+
let workspace;
|
|
157
|
+
let events = [];
|
|
158
|
+
|
|
159
|
+
db.transaction(() => {
|
|
160
|
+
workspace = normalizeWorkspaceRecord(input, options);
|
|
161
|
+
const existing = db
|
|
162
|
+
.prepare('SELECT * FROM workspaces WHERE id = ? OR uri = ? OR path = ? LIMIT 1')
|
|
163
|
+
.get(workspace.id, workspace.uri, workspace.path);
|
|
164
|
+
if (existing) {
|
|
165
|
+
const old = rowToWorkspace(existing);
|
|
166
|
+
workspace = {
|
|
167
|
+
...old,
|
|
168
|
+
...workspace,
|
|
169
|
+
id: old.id,
|
|
170
|
+
uri: old.uri || workspace.uri,
|
|
171
|
+
createdAt: old.createdAt || workspace.createdAt,
|
|
172
|
+
updatedAt: nowIso(),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
db.prepare(`
|
|
177
|
+
INSERT INTO workspaces (
|
|
178
|
+
id,
|
|
179
|
+
name,
|
|
180
|
+
uri,
|
|
181
|
+
path,
|
|
182
|
+
location_type,
|
|
183
|
+
computer_id,
|
|
184
|
+
source,
|
|
185
|
+
managed,
|
|
186
|
+
status,
|
|
187
|
+
created_at,
|
|
188
|
+
updated_at,
|
|
189
|
+
workspace_json
|
|
190
|
+
)
|
|
191
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
192
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
193
|
+
name = excluded.name,
|
|
194
|
+
uri = excluded.uri,
|
|
195
|
+
path = excluded.path,
|
|
196
|
+
location_type = excluded.location_type,
|
|
197
|
+
computer_id = excluded.computer_id,
|
|
198
|
+
source = excluded.source,
|
|
199
|
+
managed = excluded.managed,
|
|
200
|
+
status = excluded.status,
|
|
201
|
+
updated_at = excluded.updated_at,
|
|
202
|
+
workspace_json = excluded.workspace_json
|
|
203
|
+
`).run(
|
|
204
|
+
workspace.id,
|
|
205
|
+
workspace.name,
|
|
206
|
+
workspace.uri,
|
|
207
|
+
workspace.path,
|
|
208
|
+
workspace.locationType,
|
|
209
|
+
workspace.computerId,
|
|
210
|
+
workspace.source,
|
|
211
|
+
workspace.managed ? 1 : 0,
|
|
212
|
+
workspace.status,
|
|
213
|
+
workspace.createdAt,
|
|
214
|
+
workspace.updatedAt,
|
|
215
|
+
JSON.stringify(workspace),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
events = publishWorkspaceEvents(db, existing ? 'update' : 'insert', workspace, options.source || 'workspace:upsert');
|
|
219
|
+
})();
|
|
220
|
+
|
|
221
|
+
publishEvents(events);
|
|
222
|
+
return workspace;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function renameWorkspace(identifier, name, options = {}) {
|
|
226
|
+
const trimmedName = cleanString(name);
|
|
227
|
+
if (!trimmedName) throw new Error('Workspace name is required');
|
|
228
|
+
|
|
229
|
+
const db = openLocalDb();
|
|
230
|
+
let workspace = null;
|
|
231
|
+
let events = [];
|
|
232
|
+
|
|
233
|
+
db.transaction(() => {
|
|
234
|
+
const row = db.prepare('SELECT * FROM workspaces WHERE id = ? OR path = ? LIMIT 1').get(identifier, identifier);
|
|
235
|
+
if (!row) throw new Error('Workspace not found');
|
|
236
|
+
workspace = {
|
|
237
|
+
...rowToWorkspace(row),
|
|
238
|
+
name: trimmedName,
|
|
239
|
+
updatedAt: nowIso(),
|
|
240
|
+
};
|
|
241
|
+
db.prepare(`
|
|
242
|
+
UPDATE workspaces
|
|
243
|
+
SET name = ?, updated_at = ?, workspace_json = ?
|
|
244
|
+
WHERE id = ?
|
|
245
|
+
`).run(workspace.name, workspace.updatedAt, JSON.stringify(workspace), workspace.id);
|
|
246
|
+
events = publishWorkspaceEvents(db, 'update', workspace, options.source || 'workspace:rename');
|
|
247
|
+
})();
|
|
248
|
+
|
|
249
|
+
publishEvents(events);
|
|
250
|
+
return workspace;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function removeWorkspace(identifier, options = {}) {
|
|
254
|
+
const db = openLocalDb();
|
|
255
|
+
let workspace = null;
|
|
256
|
+
let events = [];
|
|
257
|
+
|
|
258
|
+
db.transaction(() => {
|
|
259
|
+
const row = db.prepare('SELECT * FROM workspaces WHERE id = ? OR path = ? LIMIT 1').get(identifier, identifier);
|
|
260
|
+
if (!row) return;
|
|
261
|
+
workspace = rowToWorkspace(row);
|
|
262
|
+
db.prepare('DELETE FROM workspaces WHERE id = ?').run(workspace.id);
|
|
263
|
+
events = publishWorkspaceEvents(db, 'delete', workspace, options.source || 'workspace:remove');
|
|
264
|
+
})();
|
|
265
|
+
|
|
266
|
+
publishEvents(events);
|
|
267
|
+
return workspace;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
listWorkspaces,
|
|
272
|
+
normalizeWorkspaceRecord,
|
|
273
|
+
removeWorkspace,
|
|
274
|
+
renameWorkspace,
|
|
275
|
+
upsertWorkspace,
|
|
276
|
+
workspaceIdForUri,
|
|
277
|
+
workspaceUriForPath,
|
|
278
|
+
};
|
|
@@ -8,6 +8,7 @@ const { normalizeClaudeMessage, usageRecordsFromClaudeResult, usageFromClaude }
|
|
|
8
8
|
const { recordNativeEvent } = require('../recorder');
|
|
9
9
|
const { toClaudeMcpServers } = require('../tooling/mcp-bundle');
|
|
10
10
|
const { bundledClaudeBinary } = require('../tooling/native-binaries');
|
|
11
|
+
const { importPackage } = require('../tooling/package-import');
|
|
11
12
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
12
13
|
|
|
13
14
|
function redactSecrets(text) {
|
|
@@ -90,7 +91,7 @@ class ClaudeAdapter {
|
|
|
90
91
|
}
|
|
91
92
|
};
|
|
92
93
|
try {
|
|
93
|
-
const sdk = await
|
|
94
|
+
const sdk = await importPackage('@anthropic-ai/claude-agent-sdk');
|
|
94
95
|
const query = sdk.query({
|
|
95
96
|
prompt: promptText(input, contract),
|
|
96
97
|
options: {
|
|
@@ -19,6 +19,8 @@ class JsonLineRpc {
|
|
|
19
19
|
this.pending = new Map();
|
|
20
20
|
this.handlers = new Set();
|
|
21
21
|
this.buffer = '';
|
|
22
|
+
this.closed = false;
|
|
23
|
+
this.closeError = null;
|
|
22
24
|
this.child = spawn(binary, ['app-server'], {
|
|
23
25
|
cwd,
|
|
24
26
|
env,
|
|
@@ -33,13 +35,32 @@ class JsonLineRpc {
|
|
|
33
35
|
const text = String(chunk || '').trim();
|
|
34
36
|
if (text && process.env.CHAT_CORE_DEBUG) console.warn('[Codex]', text.slice(0, 1000));
|
|
35
37
|
});
|
|
38
|
+
this.child.on('error', (err) => {
|
|
39
|
+
this.failAll(err);
|
|
40
|
+
});
|
|
36
41
|
this.child.on('exit', (code, signal) => {
|
|
37
42
|
const err = new Error(`codex app-server exited (${code ?? signal ?? 'unknown'})`);
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
this.failAll(err);
|
|
44
|
+
});
|
|
45
|
+
this.child.stdin.on('error', (err) => {
|
|
46
|
+
this.failAll(err);
|
|
40
47
|
});
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
failAll(err) {
|
|
51
|
+
this.closed = true;
|
|
52
|
+
this.closeError = err;
|
|
53
|
+
for (const pending of this.pending.values()) pending.reject(err);
|
|
54
|
+
this.pending.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
rejectPending(id, err) {
|
|
58
|
+
const pending = this.pending.get(id);
|
|
59
|
+
if (!pending) return;
|
|
60
|
+
this.pending.delete(id);
|
|
61
|
+
pending.reject(err);
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
onData(chunk) {
|
|
44
65
|
this.buffer += chunk;
|
|
45
66
|
const lines = this.buffer.split('\n');
|
|
@@ -64,8 +85,12 @@ class JsonLineRpc {
|
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
request(method, params = {}, timeoutMs = 180000) {
|
|
88
|
+
if (this.closeError) return Promise.reject(this.closeError);
|
|
89
|
+
if (this.closed || this.child.exitCode !== null || this.child.signalCode !== null) {
|
|
90
|
+
return Promise.reject(new Error('codex app-server is not running'));
|
|
91
|
+
}
|
|
67
92
|
const id = this.id++;
|
|
68
|
-
|
|
93
|
+
const payload = `${JSON.stringify({ id, method, params })}\n`;
|
|
69
94
|
return new Promise((resolve, reject) => {
|
|
70
95
|
const timer = setTimeout(() => {
|
|
71
96
|
this.pending.delete(id);
|
|
@@ -75,11 +100,26 @@ class JsonLineRpc {
|
|
|
75
100
|
resolve: (value) => { clearTimeout(timer); resolve(value); },
|
|
76
101
|
reject: (err) => { clearTimeout(timer); reject(err); },
|
|
77
102
|
});
|
|
103
|
+
try {
|
|
104
|
+
this.child.stdin.write(payload, (err) => {
|
|
105
|
+
if (err) this.rejectPending(id, err);
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
this.rejectPending(id, err);
|
|
109
|
+
}
|
|
78
110
|
});
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
notify(method, params) {
|
|
82
|
-
this.child.
|
|
114
|
+
if (this.closed || this.child.exitCode !== null || this.child.signalCode !== null) return;
|
|
115
|
+
const payload = `${JSON.stringify(params === undefined ? { method } : { method, params })}\n`;
|
|
116
|
+
try {
|
|
117
|
+
this.child.stdin.write(payload, (err) => {
|
|
118
|
+
if (err) this.failAll(err);
|
|
119
|
+
});
|
|
120
|
+
} catch (err) {
|
|
121
|
+
this.failAll(err);
|
|
122
|
+
}
|
|
83
123
|
}
|
|
84
124
|
|
|
85
125
|
on(handler) {
|
|
@@ -12,6 +12,7 @@ const { normalizeOpenCodeEvent, normalizeOpenCodePromptResult } = require('../no
|
|
|
12
12
|
const { recordNativeEvent } = require('../recorder');
|
|
13
13
|
const { toOpenCodeMcpConfig } = require('../tooling/mcp-bundle');
|
|
14
14
|
const { bundledOpenCodeBinary, executableExists } = require('../tooling/native-binaries');
|
|
15
|
+
const { importPackage } = require('../tooling/package-import');
|
|
15
16
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
16
17
|
|
|
17
18
|
function splitModel(model, auth) {
|
|
@@ -204,7 +205,7 @@ function isCompactCommand(text) {
|
|
|
204
205
|
|
|
205
206
|
class OpenCodeAdapter {
|
|
206
207
|
async startInstance(contract) {
|
|
207
|
-
const sdk = await
|
|
208
|
+
const sdk = await importPackage('@opencode-ai/sdk');
|
|
208
209
|
const server = await startOpenCodeServer(contract);
|
|
209
210
|
const client = sdk.createOpencodeClient({ baseUrl: server.url });
|
|
210
211
|
return { client, server, closed: false };
|