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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { PLATFORM_CONTEXT } = require('../../chat-server/config');
|
|
4
|
+
const { activeMemoryContextBlock } = require('./active-memory');
|
|
4
5
|
|
|
5
6
|
function trimBlock(value) {
|
|
6
7
|
return String(value || '').trim();
|
|
@@ -10,7 +11,9 @@ function composeSystemPrompt(contract) {
|
|
|
10
11
|
const parts = [];
|
|
11
12
|
const platform = trimBlock(PLATFORM_CONTEXT);
|
|
12
13
|
const custom = trimBlock(contract?.systemPrompt);
|
|
14
|
+
const active = contract?.providerSessionId ? '' : trimBlock(activeMemoryContextBlock(contract));
|
|
13
15
|
if (platform) parts.push(platform);
|
|
16
|
+
if (active) parts.push(active);
|
|
14
17
|
if (custom) parts.push(custom);
|
|
15
18
|
return parts.join('\n\n');
|
|
16
19
|
}
|
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
OPENAI_PROXY_URL,
|
|
18
18
|
AI_GATEWAY_PROXY_URL,
|
|
19
19
|
} = require('./config');
|
|
20
|
+
const { importPackage } = require('../chat-core/tooling/package-import');
|
|
20
21
|
|
|
21
22
|
// ── Supabase primitives ─────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -153,8 +154,19 @@ function gatewaySdkBaseUrl(baseUrl) {
|
|
|
153
154
|
return clean.endsWith('/v1/ai') ? clean : `${clean}/v1/ai`;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
function
|
|
157
|
+
async function currentProxyToken({ forceRefresh = false } = {}) {
|
|
158
|
+
if (!AI_GATEWAY_PROXY_URL && !OPENAI_PROXY_URL) return '';
|
|
159
|
+
try {
|
|
160
|
+
return await ensureFreshProxyToken?.({ force: forceRefresh, logger: console }) || readProxyToken?.() || PROXY_TOKEN || '';
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.warn('[DB] Proxy token refresh failed for title generation:', err.message);
|
|
163
|
+
return readProxyToken?.() || PROXY_TOKEN || '';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function titleEndpoints({ forceRefresh = false } = {}) {
|
|
157
168
|
const endpoints = [];
|
|
169
|
+
const proxyToken = await currentProxyToken({ forceRefresh });
|
|
158
170
|
if (AI_GATEWAY_API_KEY_TITLE) {
|
|
159
171
|
endpoints.push({
|
|
160
172
|
name: 'vercel_ai_sdk_gateway',
|
|
@@ -164,22 +176,24 @@ function titleEndpoints() {
|
|
|
164
176
|
model: gatewayTitleModel(TITLE_MODEL),
|
|
165
177
|
});
|
|
166
178
|
}
|
|
167
|
-
if (AI_GATEWAY_PROXY_URL &&
|
|
179
|
+
if (AI_GATEWAY_PROXY_URL && proxyToken) {
|
|
168
180
|
endpoints.push({
|
|
169
181
|
name: 'ai_gateway_proxy',
|
|
170
182
|
kind: 'openai_compat',
|
|
171
183
|
url: `${AI_GATEWAY_PROXY_URL}/chat/completions`,
|
|
172
|
-
token:
|
|
184
|
+
token: proxyToken,
|
|
173
185
|
model: gatewayTitleModel(TITLE_MODEL),
|
|
186
|
+
usesProxyToken: true,
|
|
174
187
|
});
|
|
175
188
|
}
|
|
176
|
-
if (OPENAI_PROXY_URL &&
|
|
189
|
+
if (OPENAI_PROXY_URL && proxyToken) {
|
|
177
190
|
endpoints.push({
|
|
178
191
|
name: 'openai_proxy',
|
|
179
192
|
kind: 'openai_compat',
|
|
180
193
|
url: `${OPENAI_PROXY_URL}/v1/chat/completions`,
|
|
181
|
-
token:
|
|
194
|
+
token: proxyToken,
|
|
182
195
|
model: openAiTitleModel(TITLE_MODEL),
|
|
196
|
+
usesProxyToken: true,
|
|
183
197
|
});
|
|
184
198
|
}
|
|
185
199
|
if (OPENAI_API_KEY_TITLE) {
|
|
@@ -196,8 +210,8 @@ function titleEndpoints() {
|
|
|
196
210
|
|
|
197
211
|
async function requestTitleWithAiSdkGateway(endpoint, prompt, signal) {
|
|
198
212
|
const [{ generateText }, { createGatewayProvider }] = await Promise.all([
|
|
199
|
-
|
|
200
|
-
|
|
213
|
+
importPackage('ai'),
|
|
214
|
+
importPackage('@ai-sdk/gateway'),
|
|
201
215
|
]);
|
|
202
216
|
const gateway = createGatewayProvider({
|
|
203
217
|
apiKey: endpoint.token,
|
|
@@ -240,11 +254,26 @@ async function requestTitle(endpoint, prompt, signal) {
|
|
|
240
254
|
return cleanGeneratedTitle(extractResponseText(await res.json()));
|
|
241
255
|
}
|
|
242
256
|
|
|
257
|
+
function isProxyAuthFailure(error) {
|
|
258
|
+
return /\b(401|403)\b|invalid or expired token|unauthorized/i.test(String(error?.message || error || ''));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function requestTitleWithProxyRetry(endpoint, prompt, signal) {
|
|
262
|
+
try {
|
|
263
|
+
return await requestTitle(endpoint, prompt, signal);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (!endpoint.usesProxyToken || !isProxyAuthFailure(err)) throw err;
|
|
266
|
+
const refreshedToken = await currentProxyToken({ forceRefresh: true });
|
|
267
|
+
if (!refreshedToken || refreshedToken === endpoint.token) throw err;
|
|
268
|
+
return requestTitle({ ...endpoint, token: refreshedToken }, prompt, signal);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
243
272
|
async function generateTitle(prompt) {
|
|
244
273
|
const text = String(prompt || '').trim();
|
|
245
274
|
if (!text) return '';
|
|
246
275
|
|
|
247
|
-
const endpoints = titleEndpoints();
|
|
276
|
+
const endpoints = await titleEndpoints();
|
|
248
277
|
if (endpoints.length === 0) {
|
|
249
278
|
console.warn('[DB] Title generation skipped: no gateway/proxy credentials');
|
|
250
279
|
return '';
|
|
@@ -254,7 +283,7 @@ async function generateTitle(prompt) {
|
|
|
254
283
|
const controller = new AbortController();
|
|
255
284
|
const timeout = setTimeout(() => controller.abort(), TITLE_TIMEOUT_MS);
|
|
256
285
|
try {
|
|
257
|
-
const title = await
|
|
286
|
+
const title = await requestTitleWithProxyRetry(endpoint, text, controller.signal);
|
|
258
287
|
if (title) return title;
|
|
259
288
|
} catch (err) {
|
|
260
289
|
console.warn('[DB] Title generation endpoint failed:', err.message);
|
|
@@ -46,12 +46,34 @@ const pty = loadPty();
|
|
|
46
46
|
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
47
47
|
const STATE_FILE = path.join(AMALGM_DIR, 'runtime-state.json');
|
|
48
48
|
const ARTIFACTS_FILE = path.join(AMALGM_DIR, 'artifacts.json');
|
|
49
|
+
const LOG_DIR = path.join(AMALGM_DIR, 'logs');
|
|
49
50
|
const BIND_HOST = process.env.AMALGM_BIND_HOST || '127.0.0.1';
|
|
50
51
|
const OWNER = process.env.AMALGM_RUNTIME_SOURCE || 'local';
|
|
51
52
|
const VERSION = process.env.npm_package_version || process.env.AMALGM_RUNTIME_VERSION || '';
|
|
52
53
|
const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
|
|
53
54
|
const PORT = Number.parseInt(process.env.AMALGM_GATEWAY_PORT || '28781', 10);
|
|
54
55
|
const RUNTIME_TOKEN_HEADER = 'x-amalgm-runtime-token';
|
|
56
|
+
const DEFAULT_DIAGNOSTIC_LOG_TAIL_BYTES = 256 * 1024;
|
|
57
|
+
const MAX_DIAGNOSTIC_LOG_TAIL_BYTES = 2 * 1024 * 1024;
|
|
58
|
+
|
|
59
|
+
const DIAGNOSTIC_LOG_FILES = Object.freeze({
|
|
60
|
+
daemon: 'daemon.log',
|
|
61
|
+
'chat-server': 'chat-server.log',
|
|
62
|
+
'local-gateway': 'local-gateway.log',
|
|
63
|
+
'amalgm-mcp': 'amalgm-mcp.log',
|
|
64
|
+
'fs-watcher': 'fs-watcher.log',
|
|
65
|
+
'port-monitor': 'port-monitor.log',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const DIAGNOSTIC_LOG_ALIASES = Object.freeze({
|
|
69
|
+
chat: 'chat-server',
|
|
70
|
+
mcp: 'amalgm-mcp',
|
|
71
|
+
gateway: 'local-gateway',
|
|
72
|
+
events: 'daemon',
|
|
73
|
+
'event-tunnel': 'daemon',
|
|
74
|
+
'chat-tunnel': 'daemon',
|
|
75
|
+
tunnel: 'daemon',
|
|
76
|
+
});
|
|
55
77
|
|
|
56
78
|
const SERVICE_PORTS = {
|
|
57
79
|
gateway: PORT,
|
|
@@ -314,6 +336,137 @@ function sendJson(res, statusCode, payload) {
|
|
|
314
336
|
res.end(JSON.stringify(payload));
|
|
315
337
|
}
|
|
316
338
|
|
|
339
|
+
function diagnosticLogServices() {
|
|
340
|
+
return Object.keys(DIAGNOSTIC_LOG_FILES);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function normalizeDiagnosticLogService(value) {
|
|
344
|
+
let service = String(value || 'daemon').trim().toLowerCase();
|
|
345
|
+
if (service.endsWith('.log')) service = service.slice(0, -4);
|
|
346
|
+
service = service.replace(/[^a-z0-9_-]/g, '');
|
|
347
|
+
return DIAGNOSTIC_LOG_ALIASES[service] || service;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function parseDiagnosticTailBytes(value) {
|
|
351
|
+
const parsed = Number.parseInt(String(value || ''), 10);
|
|
352
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_DIAGNOSTIC_LOG_TAIL_BYTES;
|
|
353
|
+
return Math.min(parsed, MAX_DIAGNOSTIC_LOG_TAIL_BYTES);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function parseDiagnosticLineLimit(value) {
|
|
357
|
+
const parsed = Number.parseInt(String(value || ''), 10);
|
|
358
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
|
359
|
+
return Math.min(parsed, 5000);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function redactDiagnosticLogContent(content) {
|
|
363
|
+
return String(content)
|
|
364
|
+
.replace(/(authorization\s*[:=]\s*)(?:Bearer\s+)?[A-Za-z0-9._~+/=-]{8,}/gi, '$1[REDACTED]')
|
|
365
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/gi, 'Bearer [REDACTED]')
|
|
366
|
+
.replace(/\b(?:sk|pk|rk|ghp|github_pat|glpat|xox[baprs])[-_][A-Za-z0-9_./+=-]{16,}\b/g, '[REDACTED_TOKEN]')
|
|
367
|
+
.replace(
|
|
368
|
+
/((?:api[_-]?key|apikey|token|secret|password|cookie|set-cookie)[^:=\n]{0,32}[:=]\s*["']?)[^\s"',;}]+/gi,
|
|
369
|
+
'$1[REDACTED]',
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function applyDiagnosticLineLimit(content, lineLimit) {
|
|
374
|
+
if (!lineLimit) return { content, lineTruncated: false };
|
|
375
|
+
const lines = String(content).split(/\r?\n/);
|
|
376
|
+
if (lines.length <= lineLimit) return { content, lineTruncated: false };
|
|
377
|
+
return {
|
|
378
|
+
content: lines.slice(-lineLimit).join('\n'),
|
|
379
|
+
lineTruncated: true,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function readDiagnosticLogTail(logPath, maxBytes) {
|
|
384
|
+
const stats = await fs.promises.stat(logPath);
|
|
385
|
+
if (!stats.isFile()) {
|
|
386
|
+
const error = new Error('Diagnostic log path is not a file');
|
|
387
|
+
error.statusCode = 400;
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const bytes = Math.min(stats.size, maxBytes);
|
|
392
|
+
if (bytes === 0) {
|
|
393
|
+
return {
|
|
394
|
+
stats,
|
|
395
|
+
bytes,
|
|
396
|
+
content: '',
|
|
397
|
+
byteTruncated: false,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const handle = await fs.promises.open(logPath, 'r');
|
|
402
|
+
try {
|
|
403
|
+
const buffer = Buffer.alloc(bytes);
|
|
404
|
+
await handle.read(buffer, 0, bytes, stats.size - bytes);
|
|
405
|
+
return {
|
|
406
|
+
stats,
|
|
407
|
+
bytes,
|
|
408
|
+
content: buffer.toString('utf8'),
|
|
409
|
+
byteTruncated: stats.size > bytes,
|
|
410
|
+
};
|
|
411
|
+
} finally {
|
|
412
|
+
await handle.close();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function handleDiagnosticLogs(req, res, url) {
|
|
417
|
+
if (req.method !== 'GET') {
|
|
418
|
+
res.writeHead(405, { Allow: 'GET' });
|
|
419
|
+
res.end();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const service = normalizeDiagnosticLogService(
|
|
424
|
+
url.searchParams.get('service') || url.searchParams.get('name'),
|
|
425
|
+
);
|
|
426
|
+
const filename = DIAGNOSTIC_LOG_FILES[service];
|
|
427
|
+
if (!filename) {
|
|
428
|
+
sendJson(res, 400, {
|
|
429
|
+
error: 'Unknown diagnostic log service',
|
|
430
|
+
availableServices: diagnosticLogServices(),
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const tailBytes = parseDiagnosticTailBytes(url.searchParams.get('tailBytes'));
|
|
436
|
+
const lineLimit = parseDiagnosticLineLimit(url.searchParams.get('lines'));
|
|
437
|
+
const logPath = path.join(LOG_DIR, filename);
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const tail = await readDiagnosticLogTail(logPath, tailBytes);
|
|
441
|
+
const lineLimited = applyDiagnosticLineLimit(tail.content, lineLimit);
|
|
442
|
+
sendJson(res, 200, {
|
|
443
|
+
service,
|
|
444
|
+
path: logPath,
|
|
445
|
+
exists: true,
|
|
446
|
+
size: tail.stats.size,
|
|
447
|
+
mtime: tail.stats.mtime.toISOString(),
|
|
448
|
+
tailBytes: tail.bytes,
|
|
449
|
+
truncated: tail.byteTruncated || lineLimited.lineTruncated,
|
|
450
|
+
content: redactDiagnosticLogContent(lineLimited.content),
|
|
451
|
+
});
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (error && error.code === 'ENOENT') {
|
|
454
|
+
sendJson(res, 404, {
|
|
455
|
+
service,
|
|
456
|
+
path: logPath,
|
|
457
|
+
exists: false,
|
|
458
|
+
error: 'Diagnostic log file not found',
|
|
459
|
+
});
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const status = typeof error?.statusCode === 'number' ? error.statusCode : 500;
|
|
463
|
+
sendJson(res, status, {
|
|
464
|
+
service,
|
|
465
|
+
error: error instanceof Error ? error.message : 'Failed to read diagnostic log',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
317
470
|
async function readBody(req) {
|
|
318
471
|
const chunks = [];
|
|
319
472
|
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
@@ -760,6 +913,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
760
913
|
return;
|
|
761
914
|
}
|
|
762
915
|
|
|
916
|
+
if (url.pathname === '/diagnostics/logs') {
|
|
917
|
+
await handleDiagnosticLogs(req, res, url);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
763
921
|
if (url.pathname === '/pty/session' || url.pathname === '/pty/resize') {
|
|
764
922
|
await handlePtyHttp(req, res, url.pathname);
|
|
765
923
|
return;
|