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.
Files changed (28) hide show
  1. package/package.json +2 -2
  2. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +29 -2
  3. package/runtime/scripts/amalgm-mcp/events/executor.js +6 -0
  4. package/runtime/scripts/amalgm-mcp/events/rest.js +13 -0
  5. package/runtime/scripts/amalgm-mcp/events/tools.js +13 -0
  6. package/runtime/scripts/amalgm-mcp/fs/rest.js +6 -0
  7. package/runtime/scripts/amalgm-mcp/server/http.js +9 -0
  8. package/runtime/scripts/amalgm-mcp/state/db.js +25 -0
  9. package/runtime/scripts/amalgm-mcp/state/snapshot.js +10 -0
  10. package/runtime/scripts/amalgm-mcp/tasks/executor.js +9 -3
  11. package/runtime/scripts/amalgm-mcp/tasks/tools.js +13 -0
  12. package/runtime/scripts/amalgm-mcp/tests/workspace-store.test.js +84 -0
  13. package/runtime/scripts/amalgm-mcp/toolbox/store.js +15 -0
  14. package/runtime/scripts/amalgm-mcp/workspace/rest.js +162 -22
  15. package/runtime/scripts/amalgm-mcp/workspace/store.js +278 -0
  16. package/runtime/scripts/chat-core/adapters/claude.js +2 -1
  17. package/runtime/scripts/chat-core/adapters/codex.js +44 -4
  18. package/runtime/scripts/chat-core/adapters/opencode.js +2 -1
  19. package/runtime/scripts/chat-core/contract.js +57 -0
  20. package/runtime/scripts/chat-core/engine.js +5 -5
  21. package/runtime/scripts/chat-core/server.js +17 -4
  22. package/runtime/scripts/chat-core/sse.js +8 -1
  23. package/runtime/scripts/chat-core/stores.js +3 -1
  24. package/runtime/scripts/chat-core/tooling/active-memory.js +396 -0
  25. package/runtime/scripts/chat-core/tooling/package-import.js +108 -0
  26. package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -0
  27. package/runtime/scripts/chat-server/db.js +38 -9
  28. 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 titleEndpoints() {
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 && PROXY_TOKEN) {
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: PROXY_TOKEN,
184
+ token: proxyToken,
173
185
  model: gatewayTitleModel(TITLE_MODEL),
186
+ usesProxyToken: true,
174
187
  });
175
188
  }
176
- if (OPENAI_PROXY_URL && PROXY_TOKEN) {
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: PROXY_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
- import('ai'),
200
- import('@ai-sdk/gateway'),
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 requestTitle(endpoint, text, controller.signal);
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;