amalgm 0.0.1 → 0.0.33
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 +4 -0
- package/lib/cli.js +133 -1
- package/lib/supervisor.js +10 -1
- package/package.json +2 -1
- package/runtime/lib/chatInput.js +9 -0
- package/runtime/lib/local/amalgmStore.js +10 -2
- package/runtime/scripts/amalgm-mcp/agents/rest.js +60 -81
- package/runtime/scripts/amalgm-mcp/agents/store.js +589 -58
- package/runtime/scripts/amalgm-mcp/agents/talk.js +10 -4
- package/runtime/scripts/amalgm-mcp/agents/tools.js +12 -1
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +19 -3
- package/runtime/scripts/amalgm-mcp/config.js +2 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +16 -1
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +85 -37
- package/runtime/scripts/amalgm-mcp/local/rest.js +7 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +83 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +42 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +28 -17
- package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
- package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
- package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +16 -1
- package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
- package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +116 -8
- package/runtime/scripts/chat-core/adapters/claude.js +2 -0
- package/runtime/scripts/chat-core/contract.js +2 -0
- package/runtime/scripts/chat-core/engine.js +77 -19
- package/runtime/scripts/credential-adapter.js +4 -2
- package/runtime/scripts/local-gateway.js +2 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { textResult } = require('../lib/tool-result');
|
|
4
|
+
const {
|
|
5
|
+
deleteTool,
|
|
6
|
+
deleteToolAction,
|
|
7
|
+
getTool,
|
|
8
|
+
getToolAction,
|
|
9
|
+
readToolbox,
|
|
10
|
+
updateTool,
|
|
11
|
+
updateToolAction,
|
|
12
|
+
upsertTool,
|
|
13
|
+
upsertToolAction,
|
|
14
|
+
} = require('./store');
|
|
15
|
+
|
|
16
|
+
function compactTool(tool, actionCount) {
|
|
17
|
+
return {
|
|
18
|
+
id: tool.id,
|
|
19
|
+
name: tool.name,
|
|
20
|
+
type: tool.type,
|
|
21
|
+
owner: tool.owner,
|
|
22
|
+
origin: tool.origin,
|
|
23
|
+
status: tool.status,
|
|
24
|
+
discovery: tool.discovery,
|
|
25
|
+
actionCount,
|
|
26
|
+
source: tool.source,
|
|
27
|
+
guide: tool.guide,
|
|
28
|
+
display: tool.display,
|
|
29
|
+
policy: tool.policy,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function listPayload(args = {}) {
|
|
34
|
+
const toolbox = readToolbox();
|
|
35
|
+
const actionsByTool = new Map();
|
|
36
|
+
for (const action of Object.values(toolbox.toolActions || {})) {
|
|
37
|
+
const existing = actionsByTool.get(action.toolId) || [];
|
|
38
|
+
existing.push(action);
|
|
39
|
+
actionsByTool.set(action.toolId, existing);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tools = Object.values(toolbox.tools || {})
|
|
43
|
+
.filter((tool) => !args.type || tool.type === args.type)
|
|
44
|
+
.filter((tool) => !args.owner || tool.owner === args.owner)
|
|
45
|
+
.filter((tool) => !args.origin || tool.origin === args.origin)
|
|
46
|
+
.map((tool) => compactTool(tool, actionsByTool.get(tool.id)?.length || 0));
|
|
47
|
+
|
|
48
|
+
const payload = {
|
|
49
|
+
version: toolbox.version,
|
|
50
|
+
tools,
|
|
51
|
+
...(args.include_actions
|
|
52
|
+
? { toolActions: toolbox.toolActions }
|
|
53
|
+
: {}),
|
|
54
|
+
};
|
|
55
|
+
return payload;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = [
|
|
59
|
+
{
|
|
60
|
+
name: 'toolbox_tools_list',
|
|
61
|
+
description: 'List registered Amalgm Toolbox connectors/tools. Use this to see persistent user tools available across sessions.',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
type: { type: 'string', enum: ['mcp', 'api', 'cli'], description: 'Optional tool type filter.' },
|
|
66
|
+
owner: { type: 'string', description: 'Optional owner filter, e.g. amalgm, claude, cursor, codex, or an agent harness id.' },
|
|
67
|
+
origin: { type: 'string', enum: ['system', 'user', 'catalog'], description: 'Optional origin filter.' },
|
|
68
|
+
include_actions: { type: 'boolean', description: 'Include full tool action records. Defaults false.' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
async handler(args) {
|
|
72
|
+
return textResult(JSON.stringify(listPayload(args || {}), null, 2));
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'toolbox_tool_get',
|
|
77
|
+
description: 'Read one registered Toolbox connector/tool by id, optionally including its actions.',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
id: { type: 'string', description: 'Tool id to read.' },
|
|
82
|
+
include_actions: { type: 'boolean', description: 'Include actions for this tool. Defaults true.' },
|
|
83
|
+
},
|
|
84
|
+
required: ['id'],
|
|
85
|
+
},
|
|
86
|
+
async handler(args) {
|
|
87
|
+
const payload = getTool(args?.id, { includeActions: args?.include_actions !== false });
|
|
88
|
+
return textResult(JSON.stringify(payload, null, 2));
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'toolbox_tool_register',
|
|
93
|
+
description: [
|
|
94
|
+
'Register or update a persistent Toolbox connector/tool. This is how agents save custom tools for future sessions.',
|
|
95
|
+
'For CLI tools, provide type="cli" and source.command. A default <tool>.run action is created automatically unless actions are provided.',
|
|
96
|
+
'Optional skillPath/guidePath attaches usage instructions to the tool without becoming a separate callable action.',
|
|
97
|
+
].join('\n'),
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
id: { type: 'string', description: 'Stable tool id, e.g. acme.executor.' },
|
|
102
|
+
name: { type: 'string', description: 'Human-readable connector/tool name.' },
|
|
103
|
+
type: { type: 'string', enum: ['mcp', 'api', 'cli'] },
|
|
104
|
+
owner: { type: 'string', description: 'Tool owner. Defaults to amalgm. Use a harness or agent id for externally owned tools.' },
|
|
105
|
+
origin: { type: 'string', enum: ['user', 'catalog'], description: 'Defaults to user.' },
|
|
106
|
+
status: { type: 'string', enum: ['enabled', 'disabled', 'error'] },
|
|
107
|
+
source: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
description: 'Type-specific source config. CLI: {command,args,cwd,inputMode,outputMode}. MCP: {transport,url|command}. API: {baseUrl,specUrl,specPath,endpoints}.',
|
|
110
|
+
additionalProperties: true,
|
|
111
|
+
},
|
|
112
|
+
command: { type: 'string', description: 'CLI shortcut for source.command.' },
|
|
113
|
+
args: { type: 'array', items: { type: 'string' }, description: 'CLI/MCP stdio args shortcut.' },
|
|
114
|
+
cwd: { type: 'string', description: 'Working directory shortcut.' },
|
|
115
|
+
inputMode: { type: 'string', enum: ['json-stdin', 'argv', 'terminal', 'none'] },
|
|
116
|
+
outputMode: { type: 'string', enum: ['json', 'text'] },
|
|
117
|
+
skillPath: { type: 'string', description: 'Optional local SKILL.md path with usage guidance.' },
|
|
118
|
+
guidePath: { type: 'string', description: 'Optional local guide path with usage guidance.' },
|
|
119
|
+
display: { type: 'object', additionalProperties: true },
|
|
120
|
+
policy: { type: 'object', additionalProperties: true },
|
|
121
|
+
actions: {
|
|
122
|
+
type: 'array',
|
|
123
|
+
description: 'Optional static action records to create/update with the tool.',
|
|
124
|
+
items: { type: 'object', additionalProperties: true },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ['id', 'name', 'type'],
|
|
128
|
+
},
|
|
129
|
+
async handler(args) {
|
|
130
|
+
const saved = upsertTool(args || {});
|
|
131
|
+
return textResult(JSON.stringify({ ok: true, ...saved }, null, 2));
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'toolbox_tool_update',
|
|
136
|
+
description: [
|
|
137
|
+
'Partially update an existing Toolbox connector/tool without replacing unspecified fields.',
|
|
138
|
+
'Use this for modifications like status, display metadata, policy, guide/skill path, owner, or source patches.',
|
|
139
|
+
'System tools cannot be modified.',
|
|
140
|
+
].join('\n'),
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
id: { type: 'string', description: 'Tool id to update.' },
|
|
145
|
+
name: { type: 'string', description: 'Optional new human-readable name.' },
|
|
146
|
+
type: { type: 'string', enum: ['mcp', 'api', 'cli'], description: 'Optional type update.' },
|
|
147
|
+
owner: { type: 'string', description: 'Optional owner update.' },
|
|
148
|
+
origin: { type: 'string', enum: ['user', 'catalog'], description: 'Optional origin update.' },
|
|
149
|
+
status: { type: 'string', enum: ['enabled', 'disabled', 'error'] },
|
|
150
|
+
source: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
description: 'Partial source patch. CLI: {command,args,cwd,inputMode,outputMode}. MCP: {transport,url|command}. API: {baseUrl,specUrl,specPath,endpoints}.',
|
|
153
|
+
additionalProperties: true,
|
|
154
|
+
},
|
|
155
|
+
command: { type: 'string', description: 'CLI shortcut for source.command.' },
|
|
156
|
+
args: { type: 'array', items: { type: 'string' }, description: 'CLI/MCP stdio args shortcut.' },
|
|
157
|
+
cwd: { type: 'string', description: 'Working directory shortcut.' },
|
|
158
|
+
inputMode: { type: 'string', enum: ['json-stdin', 'argv', 'terminal', 'none'] },
|
|
159
|
+
outputMode: { type: 'string', enum: ['json', 'text'] },
|
|
160
|
+
discovery: { type: 'object', additionalProperties: true },
|
|
161
|
+
skillPath: { type: 'string', description: 'Optional local SKILL.md path with usage guidance.' },
|
|
162
|
+
guidePath: { type: 'string', description: 'Optional local guide path with usage guidance.' },
|
|
163
|
+
guide: { type: 'object', additionalProperties: true },
|
|
164
|
+
display: { type: 'object', additionalProperties: true },
|
|
165
|
+
policy: { type: 'object', additionalProperties: true },
|
|
166
|
+
},
|
|
167
|
+
required: ['id'],
|
|
168
|
+
},
|
|
169
|
+
async handler(args) {
|
|
170
|
+
const saved = updateTool(args || {});
|
|
171
|
+
return textResult(JSON.stringify({ ok: true, ...saved }, null, 2));
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'toolbox_action_get',
|
|
176
|
+
description: 'Read one registered Toolbox action by id.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
id: { type: 'string', description: 'Tool action id to read.' },
|
|
181
|
+
},
|
|
182
|
+
required: ['id'],
|
|
183
|
+
},
|
|
184
|
+
async handler(args) {
|
|
185
|
+
const payload = getToolAction(args?.id);
|
|
186
|
+
return textResult(JSON.stringify(payload, null, 2));
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'toolbox_action_register',
|
|
191
|
+
description: 'Register or update a persistent action under an existing Toolbox tool. Use this for richer CLI/API schemas beyond the default run action.',
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
id: { type: 'string', description: 'Stable action id, e.g. acme.executor.run_job.' },
|
|
196
|
+
toolId: { type: 'string', description: 'Parent tool id.' },
|
|
197
|
+
name: { type: 'string', description: 'Action name, e.g. run_job.' },
|
|
198
|
+
displayName: { type: 'string' },
|
|
199
|
+
description: { type: 'string' },
|
|
200
|
+
inputSchema: { type: 'object', additionalProperties: true },
|
|
201
|
+
outputSchema: { type: 'object', additionalProperties: true },
|
|
202
|
+
status: { type: 'string', enum: ['enabled', 'disabled', 'error'] },
|
|
203
|
+
sourceAction: { type: 'object', additionalProperties: true },
|
|
204
|
+
skillPath: { type: 'string', description: 'Optional local SKILL.md path with usage guidance.' },
|
|
205
|
+
guidePath: { type: 'string', description: 'Optional local guide path with usage guidance.' },
|
|
206
|
+
policy: { type: 'object', additionalProperties: true },
|
|
207
|
+
},
|
|
208
|
+
required: ['toolId', 'name'],
|
|
209
|
+
},
|
|
210
|
+
async handler(args) {
|
|
211
|
+
const action = upsertToolAction(args || {});
|
|
212
|
+
return textResult(JSON.stringify({ ok: true, action }, null, 2));
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'toolbox_action_update',
|
|
217
|
+
description: 'Partially update an existing Toolbox action without replacing unspecified fields. System tool actions cannot be modified.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
id: { type: 'string', description: 'Tool action id to update.' },
|
|
222
|
+
name: { type: 'string', description: 'Optional action name update.' },
|
|
223
|
+
displayName: { type: 'string' },
|
|
224
|
+
description: { type: 'string' },
|
|
225
|
+
inputSchema: { type: 'object', additionalProperties: true },
|
|
226
|
+
outputSchema: { type: 'object', additionalProperties: true },
|
|
227
|
+
status: { type: 'string', enum: ['enabled', 'disabled', 'error'] },
|
|
228
|
+
sourceAction: { type: 'object', additionalProperties: true },
|
|
229
|
+
skillPath: { type: 'string', description: 'Optional local SKILL.md path with usage guidance.' },
|
|
230
|
+
guidePath: { type: 'string', description: 'Optional local guide path with usage guidance.' },
|
|
231
|
+
guide: { type: 'object', additionalProperties: true },
|
|
232
|
+
policy: { type: 'object', additionalProperties: true },
|
|
233
|
+
},
|
|
234
|
+
required: ['id'],
|
|
235
|
+
},
|
|
236
|
+
async handler(args) {
|
|
237
|
+
const action = updateToolAction(args || {});
|
|
238
|
+
return textResult(JSON.stringify({ ok: true, action }, null, 2));
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'toolbox_tool_delete',
|
|
243
|
+
description: 'Delete a user/catalog Toolbox tool and its actions. System tools cannot be deleted.',
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
properties: {
|
|
247
|
+
id: { type: 'string', description: 'Tool id to delete.' },
|
|
248
|
+
},
|
|
249
|
+
required: ['id'],
|
|
250
|
+
},
|
|
251
|
+
async handler(args) {
|
|
252
|
+
return textResult(JSON.stringify(deleteTool(args?.id), null, 2));
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'toolbox_action_delete',
|
|
257
|
+
description: 'Delete a user/catalog Toolbox action. System tool actions cannot be deleted.',
|
|
258
|
+
inputSchema: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
properties: {
|
|
261
|
+
id: { type: 'string', description: 'Tool action id to delete.' },
|
|
262
|
+
},
|
|
263
|
+
required: ['id'],
|
|
264
|
+
},
|
|
265
|
+
async handler(args) {
|
|
266
|
+
return textResult(JSON.stringify(deleteToolAction(args?.id), null, 2));
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
];
|
|
@@ -7,12 +7,55 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const { execFile } = require('child_process');
|
|
9
9
|
const { promisify } = require('util');
|
|
10
|
-
const { DEFAULT_CWD } = require('../config');
|
|
10
|
+
const { AMALGM_DIR, DEFAULT_CWD } = require('../config');
|
|
11
11
|
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
14
14
|
const WORKTREE_SUFFIX_RE = /-amalgm-wt-[a-z0-9]+$/;
|
|
15
15
|
|
|
16
|
+
function isWithin(root, target) {
|
|
17
|
+
const relative = path.relative(root, target);
|
|
18
|
+
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sanitizeProjectName(value) {
|
|
22
|
+
const fallback = 'project';
|
|
23
|
+
return String(value || fallback)
|
|
24
|
+
.trim()
|
|
25
|
+
.replace(/[\/\\:\0]/g, '-')
|
|
26
|
+
.replace(/^\.+$/, fallback)
|
|
27
|
+
.replace(/^\.+/, '')
|
|
28
|
+
.slice(0, 120) || fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getWorkspaceRoot() {
|
|
32
|
+
if (process.env.AMALGM_WORKSPACES_DIR) return path.resolve(process.env.AMALGM_WORKSPACES_DIR);
|
|
33
|
+
if (process.env.AMALGM_PROJECTS_DIR) return path.resolve(process.env.AMALGM_PROJECTS_DIR);
|
|
34
|
+
if (process.env.AMALGM_LOCAL_MODE === 'true') return path.join(AMALGM_DIR, 'workspaces');
|
|
35
|
+
return path.join(DEFAULT_CWD, 'workspaces');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ensureWorkspaceRoot() {
|
|
39
|
+
const workspaceRoot = getWorkspaceRoot();
|
|
40
|
+
await fs.promises.mkdir(workspaceRoot, { recursive: true });
|
|
41
|
+
return workspaceRoot;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function uniqueProjectPath(workspaceRoot, requestedName) {
|
|
45
|
+
const name = sanitizeProjectName(requestedName);
|
|
46
|
+
let candidate = path.join(workspaceRoot, name);
|
|
47
|
+
let suffix = 2;
|
|
48
|
+
while (true) {
|
|
49
|
+
try {
|
|
50
|
+
await fs.promises.access(candidate);
|
|
51
|
+
candidate = path.join(workspaceRoot, `${name}-${suffix}`);
|
|
52
|
+
suffix += 1;
|
|
53
|
+
} catch {
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
16
59
|
function isValidWorkspacePath(workspacePath) {
|
|
17
60
|
return typeof workspacePath === 'string' && workspacePath.length > 0 && path.isAbsolute(workspacePath);
|
|
18
61
|
}
|
|
@@ -94,7 +137,7 @@ function getLocalWorktreePath(workspacePath, shortId) {
|
|
|
94
137
|
|
|
95
138
|
async function handleProjects(sendJson) {
|
|
96
139
|
try {
|
|
97
|
-
const rootDir =
|
|
140
|
+
const rootDir = await ensureWorkspaceRoot();
|
|
98
141
|
const dirents = await fs.promises.readdir(rootDir, { withFileTypes: true }).catch(() => []);
|
|
99
142
|
const projects = [];
|
|
100
143
|
|
|
@@ -112,10 +155,73 @@ async function handleProjects(sendJson) {
|
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
115
|
-
sendJson(200, { projects });
|
|
158
|
+
sendJson(200, { projects, workspaceRoot: rootDir });
|
|
159
|
+
} catch (error) {
|
|
160
|
+
sendJson(500, {
|
|
161
|
+
error: error instanceof Error ? error.message : 'Failed to list workspaces',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handleImport(body, sendJson) {
|
|
167
|
+
const sourcePath = body?.source_path;
|
|
168
|
+
const projectName = body?.project_name;
|
|
169
|
+
|
|
170
|
+
if (!sourcePath || typeof sourcePath !== 'string') {
|
|
171
|
+
sendJson(400, { error: 'source_path is required' });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!path.isAbsolute(sourcePath)) {
|
|
176
|
+
sendJson(400, { error: 'source_path must be absolute' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const sourceStats = await fs.promises.stat(sourcePath);
|
|
182
|
+
if (!sourceStats.isDirectory()) {
|
|
183
|
+
sendJson(400, { error: 'source_path must be a folder' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const workspaceRoot = await ensureWorkspaceRoot();
|
|
188
|
+
const [realSource, realWorkspaceRoot] = await Promise.all([
|
|
189
|
+
fs.promises.realpath(sourcePath),
|
|
190
|
+
fs.promises.realpath(workspaceRoot),
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
if (isWithin(realWorkspaceRoot, realSource)) {
|
|
194
|
+
sendJson(200, {
|
|
195
|
+
success: true,
|
|
196
|
+
path: realSource,
|
|
197
|
+
name: path.basename(realSource),
|
|
198
|
+
workspaceRoot,
|
|
199
|
+
alreadyInWorkspace: true,
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const targetPath = await uniqueProjectPath(
|
|
205
|
+
workspaceRoot,
|
|
206
|
+
projectName || path.basename(realSource),
|
|
207
|
+
);
|
|
208
|
+
await fs.promises.cp(realSource, targetPath, {
|
|
209
|
+
recursive: true,
|
|
210
|
+
force: false,
|
|
211
|
+
errorOnExist: true,
|
|
212
|
+
dereference: false,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
sendJson(200, {
|
|
216
|
+
success: true,
|
|
217
|
+
path: targetPath,
|
|
218
|
+
name: path.basename(targetPath),
|
|
219
|
+
workspaceRoot,
|
|
220
|
+
alreadyInWorkspace: false,
|
|
221
|
+
});
|
|
116
222
|
} catch (error) {
|
|
117
223
|
sendJson(500, {
|
|
118
|
-
error: error instanceof Error ? error.message : 'Failed to
|
|
224
|
+
error: error instanceof Error ? error.message : 'Failed to import project',
|
|
119
225
|
});
|
|
120
226
|
}
|
|
121
227
|
}
|
|
@@ -135,12 +241,13 @@ async function handleClone(body, sendJson) {
|
|
|
135
241
|
return;
|
|
136
242
|
}
|
|
137
243
|
|
|
138
|
-
const
|
|
244
|
+
const workspaceRoot = await ensureWorkspaceRoot();
|
|
245
|
+
const targetPath = path.join(workspaceRoot, sanitizeProjectName(repoName));
|
|
139
246
|
|
|
140
247
|
try {
|
|
141
248
|
await fs.promises.access(targetPath);
|
|
142
249
|
if (await isGitRepo(targetPath)) {
|
|
143
|
-
sendJson(200, { success: true, path: targetPath, alreadyExists: true });
|
|
250
|
+
sendJson(200, { success: true, path: targetPath, workspaceRoot, alreadyExists: true });
|
|
144
251
|
return;
|
|
145
252
|
}
|
|
146
253
|
sendJson(409, {
|
|
@@ -158,7 +265,7 @@ async function handleClone(body, sendJson) {
|
|
|
158
265
|
}
|
|
159
266
|
cloneArgs.push('clone', cloneUrl, targetPath);
|
|
160
267
|
|
|
161
|
-
const cloneResult = await runGit(cloneArgs,
|
|
268
|
+
const cloneResult = await runGit(cloneArgs, workspaceRoot);
|
|
162
269
|
if (cloneResult.exitCode !== 0) {
|
|
163
270
|
sendJson(500, {
|
|
164
271
|
error: cloneResult.stderr.trim() || cloneResult.stdout.trim() || 'Clone failed',
|
|
@@ -166,7 +273,7 @@ async function handleClone(body, sendJson) {
|
|
|
166
273
|
return;
|
|
167
274
|
}
|
|
168
275
|
|
|
169
|
-
sendJson(200, { success: true, path: targetPath, alreadyExists: false });
|
|
276
|
+
sendJson(200, { success: true, path: targetPath, workspaceRoot, alreadyExists: false });
|
|
170
277
|
}
|
|
171
278
|
|
|
172
279
|
async function handleBranches(query, sendJson) {
|
|
@@ -379,6 +486,7 @@ async function handleWorktreeDelete(body, sendJson) {
|
|
|
379
486
|
|
|
380
487
|
module.exports = {
|
|
381
488
|
handleProjects,
|
|
489
|
+
handleImport,
|
|
382
490
|
handleClone,
|
|
383
491
|
handleBranches,
|
|
384
492
|
handleCheckout,
|
|
@@ -36,6 +36,8 @@ class ClaudeAdapter {
|
|
|
36
36
|
cwd: contract.cwd,
|
|
37
37
|
env: runtimeEnv(contract),
|
|
38
38
|
model: contract.cliModel,
|
|
39
|
+
...(contract.reasoningEffort ? { effort: contract.reasoningEffort } : {}),
|
|
40
|
+
...(contract.fastMode ? { settings: { fastMode: true, fastModePerSessionOptIn: true } } : {}),
|
|
39
41
|
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
|
|
40
42
|
...(systemPrompt ? { systemPrompt: { type: 'preset', preset: 'claude_code', append: systemPrompt } } : {}),
|
|
41
43
|
mcpServers: toClaudeMcpServers(contract),
|
|
@@ -266,6 +266,7 @@ function createContract(payload, options = {}) {
|
|
|
266
266
|
const authMethod = coerceAuth(harness, payload.authMethod || 'amalgm');
|
|
267
267
|
const modelId = canonicalModel(payload.modelId || payload.cliModel, harness);
|
|
268
268
|
const reasoningEffort = payload.reasoningEffort || null;
|
|
269
|
+
const fastMode = payload.fastMode === true;
|
|
269
270
|
const cliModel = cliModelFor({ harness, modelId, cliModel: payload.cliModel, reasoningEffort });
|
|
270
271
|
const cwd = resolveCwd(payload.cwd, options);
|
|
271
272
|
const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port || process.env.CHAT_SERVER_PORT || 8084}`;
|
|
@@ -294,6 +295,7 @@ function createContract(payload, options = {}) {
|
|
|
294
295
|
providerSessionId: nullableString(payload.providerSessionId || payload.provider_session_id),
|
|
295
296
|
cliModel,
|
|
296
297
|
reasoningEffort,
|
|
298
|
+
fastMode,
|
|
297
299
|
contextWindow: positiveOrNull(payload.contextWindow),
|
|
298
300
|
maxTokens: positiveOrNull(payload.maxTokens),
|
|
299
301
|
cwd,
|
|
@@ -7,9 +7,39 @@ const { normalizeInput } = require('./input');
|
|
|
7
7
|
const { frameFor, titleFrame } = require('./sse');
|
|
8
8
|
const { beginProxyTurn, logTurnUsage } = require('./usage');
|
|
9
9
|
|
|
10
|
-
function
|
|
10
|
+
function warnIfDbNotSaved(result, label) {
|
|
11
11
|
if (result === false || result === null) {
|
|
12
|
-
|
|
12
|
+
console.warn(`[ChatCore] ${label} was not saved; continuing turn with temp/raw stream for reconnect`);
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function bestEffortDbSave(label, fn) {
|
|
19
|
+
try {
|
|
20
|
+
return warnIfDbNotSaved(await fn(), label);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.warn(`[ChatCore] ${label} save failed: ${err.message}; continuing turn with temp/raw stream for reconnect`);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function withDeadline(label, promise, ms) {
|
|
28
|
+
if (!ms || ms <= 0) return promise;
|
|
29
|
+
let timeout = null;
|
|
30
|
+
try {
|
|
31
|
+
return await Promise.race([
|
|
32
|
+
promise,
|
|
33
|
+
new Promise((resolve) => {
|
|
34
|
+
timeout = setTimeout(() => {
|
|
35
|
+
console.warn(`[ChatCore] ${label} timed out after ${ms}ms; continuing without it`);
|
|
36
|
+
resolve(null);
|
|
37
|
+
}, ms);
|
|
38
|
+
timeout.unref?.();
|
|
39
|
+
}),
|
|
40
|
+
]);
|
|
41
|
+
} finally {
|
|
42
|
+
if (timeout) clearTimeout(timeout);
|
|
13
43
|
}
|
|
14
44
|
}
|
|
15
45
|
|
|
@@ -20,6 +50,7 @@ class ChatCore {
|
|
|
20
50
|
this.turns = turns;
|
|
21
51
|
this.options = options;
|
|
22
52
|
this.beginTurn = options.beginTurn || beginProxyTurn;
|
|
53
|
+
this.historyLoadTimeoutMs = Number(options.historyLoadTimeoutMs ?? process.env.AMALGM_HISTORY_LOAD_TIMEOUT_MS ?? 750);
|
|
23
54
|
this.contracts = new Map();
|
|
24
55
|
}
|
|
25
56
|
|
|
@@ -70,10 +101,14 @@ class ChatCore {
|
|
|
70
101
|
assistantMessageId: contract.assistantMessageId,
|
|
71
102
|
modelId: contract.usageModelId || contract.modelId,
|
|
72
103
|
};
|
|
73
|
-
|
|
104
|
+
const userMessageSave = bestEffortDbSave('User message', () => this.db.saveUserMessage(msgConfig));
|
|
74
105
|
if (isReturningToSession && !runtimeWasWarm && !contract.providerSessionId && typeof this.db.loadConversationHistory === 'function') {
|
|
75
106
|
try {
|
|
76
|
-
const history = await
|
|
107
|
+
const history = await withDeadline(
|
|
108
|
+
'Cold-resume history load',
|
|
109
|
+
this.db.loadConversationHistory(contract.sessionId, contract.userMessageId),
|
|
110
|
+
this.historyLoadTimeoutMs,
|
|
111
|
+
);
|
|
77
112
|
if (history) {
|
|
78
113
|
const text = `${history}${input.text || ''}`.trim();
|
|
79
114
|
runtimeInput = {
|
|
@@ -86,8 +121,11 @@ class ChatCore {
|
|
|
86
121
|
console.warn('[ChatCore] Failed to load cold-resume conversation history:', err.message);
|
|
87
122
|
}
|
|
88
123
|
}
|
|
89
|
-
|
|
90
|
-
|
|
124
|
+
const assistantPlaceholderSave = bestEffortDbSave(
|
|
125
|
+
'Assistant placeholder',
|
|
126
|
+
() => this.db.ensureAssistantMessage(msgConfig),
|
|
127
|
+
);
|
|
128
|
+
this.db.mergeSessionMetadata(contract.sessionId, {
|
|
91
129
|
contract: {
|
|
92
130
|
userId: contract.userId,
|
|
93
131
|
computerId: contract.computerId,
|
|
@@ -121,6 +159,11 @@ class ChatCore {
|
|
|
121
159
|
assistantMessageId: contract.assistantMessageId,
|
|
122
160
|
userMessageId: contract.userMessageId,
|
|
123
161
|
});
|
|
162
|
+
entry.persistence = {
|
|
163
|
+
userMessageSaved: null,
|
|
164
|
+
assistantPlaceholderSaved: null,
|
|
165
|
+
assistantMessageSaved: null,
|
|
166
|
+
};
|
|
124
167
|
|
|
125
168
|
if (typeof this.db.generateAndSaveTitle === 'function' && input.text) {
|
|
126
169
|
this.db.generateAndSaveTitle(contract.sessionId, input.text, (title) => {
|
|
@@ -173,20 +216,35 @@ class ChatCore {
|
|
|
173
216
|
|
|
174
217
|
const savedParts = parts.finalize();
|
|
175
218
|
this.turns.setParts(turnId, savedParts);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
219
|
+
this.turns.mark(turnId, 'persisting');
|
|
220
|
+
const finalizePersistence = async () => {
|
|
221
|
+
const [userMessageSaved, assistantPlaceholderSaved, assistantMessageSaved] = await Promise.all([
|
|
222
|
+
userMessageSave,
|
|
223
|
+
assistantPlaceholderSave,
|
|
224
|
+
bestEffortDbSave('Assistant message', () => this.db.saveAssistantMessage(msgConfig, savedParts, providerSessionId, usage)),
|
|
225
|
+
]);
|
|
226
|
+
if (entry.persistence) {
|
|
227
|
+
entry.persistence.userMessageSaved = userMessageSaved;
|
|
228
|
+
entry.persistence.assistantPlaceholderSaved = assistantPlaceholderSaved;
|
|
229
|
+
entry.persistence.assistantMessageSaved = assistantMessageSaved;
|
|
230
|
+
}
|
|
231
|
+
await logTurnUsage({
|
|
232
|
+
db: this.db,
|
|
233
|
+
contract,
|
|
234
|
+
messageId: contract.assistantMessageId,
|
|
235
|
+
usage,
|
|
236
|
+
usageRecords,
|
|
237
|
+
});
|
|
238
|
+
if (!assistantMessageSaved || !userMessageSaved || !assistantPlaceholderSaved) {
|
|
239
|
+
this.turns.mark(turnId, 'save_failed');
|
|
240
|
+
} else {
|
|
241
|
+
this.turns.mark(turnId, stopReason === 'error' ? 'error' : stopReason === 'cancelled' ? 'cancelled' : 'complete');
|
|
242
|
+
this.turns.clear(turnId);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
finalizePersistence().catch((err) => {
|
|
246
|
+
console.warn('[ChatCore] Persistence finalization failed:', err.message);
|
|
179
247
|
this.turns.mark(turnId, 'save_failed');
|
|
180
|
-
throw saveErr;
|
|
181
|
-
}
|
|
182
|
-
this.turns.mark(turnId, stopReason === 'error' ? 'error' : stopReason === 'cancelled' ? 'cancelled' : 'complete');
|
|
183
|
-
this.turns.clear(turnId);
|
|
184
|
-
await logTurnUsage({
|
|
185
|
-
db: this.db,
|
|
186
|
-
contract,
|
|
187
|
-
messageId: contract.assistantMessageId,
|
|
188
|
-
usage,
|
|
189
|
-
usageRecords,
|
|
190
248
|
});
|
|
191
249
|
return { providerSessionId, usage, parts: savedParts, stopReason };
|
|
192
250
|
}
|
|
@@ -13,12 +13,13 @@ const fs = require('fs');
|
|
|
13
13
|
const os = require('os');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
|
|
16
|
-
const VALID_HARNESS_IDS = ['claude_code', 'codex', 'opencode'];
|
|
16
|
+
const VALID_HARNESS_IDS = ['claude_code', 'codex', 'opencode', 'pi'];
|
|
17
17
|
const VALID_AUTH_MODES = ['amalgm', 'provider_auth', 'byok'];
|
|
18
18
|
const SUPPORTED_AUTH_BY_HARNESS = {
|
|
19
19
|
claude_code: ['amalgm', 'byok', 'provider_auth'],
|
|
20
20
|
codex: ['amalgm', 'byok', 'provider_auth'],
|
|
21
|
-
opencode: ['amalgm'],
|
|
21
|
+
opencode: ['amalgm', 'byok'],
|
|
22
|
+
pi: ['amalgm', 'byok', 'provider_auth'],
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
@@ -30,6 +31,7 @@ function defaultAuthModes() {
|
|
|
30
31
|
claude_code: 'amalgm',
|
|
31
32
|
codex: 'amalgm',
|
|
32
33
|
opencode: 'amalgm',
|
|
34
|
+
pi: 'amalgm',
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
|