amalgm 0.1.61 → 0.1.63

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.
@@ -231,6 +231,7 @@ function createEventTunnel({ record, foreground = false }) {
231
231
  let connectStartedAt = 0;
232
232
  let lastGatewayFrameAt = 0;
233
233
  const upstreamSockets = new Map();
234
+ const upstreamStreams = new Map();
234
235
 
235
236
  function log(message) {
236
237
  const line = `[event-tunnel] ${message}`;
@@ -386,6 +387,71 @@ function createEventTunnel({ record, foreground = false }) {
386
387
  req.end(body);
387
388
  }
388
389
 
390
+ function forwardStream(frame) {
391
+ const targetPort = resolveTargetPort(frame);
392
+ if (!targetPort) {
393
+ send({
394
+ type: 'stream_res_error',
395
+ req_id: frame.req_id,
396
+ status: 403,
397
+ message: 'Target port is not allowed',
398
+ });
399
+ return;
400
+ }
401
+ const body = frame.body_b64 ? Buffer.from(frame.body_b64, 'base64') : Buffer.alloc(0);
402
+ const req = http.request(
403
+ {
404
+ hostname: '127.0.0.1',
405
+ port: targetPort,
406
+ method: frame.method || 'GET',
407
+ path: frame.path || '/',
408
+ headers: localHeaders(frame.headers),
409
+ },
410
+ (res) => {
411
+ send({
412
+ type: 'stream_res_start',
413
+ req_id: frame.req_id,
414
+ status: res.statusCode || 200,
415
+ headers: responseHeaders(res.headers),
416
+ });
417
+ res.on('data', (chunk) => {
418
+ send({
419
+ type: 'stream_res_data',
420
+ req_id: frame.req_id,
421
+ chunk_b64: Buffer.from(chunk).toString('base64'),
422
+ });
423
+ });
424
+ res.on('end', () => {
425
+ upstreamStreams.delete(frame.req_id);
426
+ send({ type: 'stream_res_end', req_id: frame.req_id });
427
+ });
428
+ },
429
+ );
430
+
431
+ upstreamStreams.set(frame.req_id, req);
432
+
433
+ req.on('error', (error) => {
434
+ if (!upstreamStreams.has(frame.req_id)) return;
435
+ upstreamStreams.delete(frame.req_id);
436
+ send({
437
+ type: 'stream_res_error',
438
+ req_id: frame.req_id,
439
+ status: 502,
440
+ message: error.message || 'Local stream request failed',
441
+ });
442
+ });
443
+
444
+ if (body.length > 0) req.write(body);
445
+ req.end();
446
+ }
447
+
448
+ function cancelStream(frame) {
449
+ const req = upstreamStreams.get(frame.req_id);
450
+ if (!req) return;
451
+ upstreamStreams.delete(frame.req_id);
452
+ req.destroy(new Error('stream canceled'));
453
+ }
454
+
389
455
  function openPreviewSocket(frame) {
390
456
  const targetPort = resolveTargetPort(frame);
391
457
  if (!targetPort) {
@@ -436,6 +502,14 @@ function createEventTunnel({ record, foreground = false }) {
436
502
  handleRequest(frame);
437
503
  return;
438
504
  }
505
+ if (frame.type === 'stream_req') {
506
+ forwardStream(frame);
507
+ return;
508
+ }
509
+ if (frame.type === 'stream_cancel') {
510
+ cancelStream(frame);
511
+ return;
512
+ }
439
513
  if (frame.type === 'ws_open') {
440
514
  openPreviewSocket(frame);
441
515
  return;
@@ -518,6 +592,14 @@ function createEventTunnel({ record, foreground = false }) {
518
592
  }
519
593
  }
520
594
  upstreamSockets.clear();
595
+ for (const stream of upstreamStreams.values()) {
596
+ try {
597
+ stream.destroy(new Error('event tunnel stopped'));
598
+ } catch {
599
+ // noop
600
+ }
601
+ }
602
+ upstreamStreams.clear();
521
603
  void postPresence(record, false);
522
604
  if (ws) {
523
605
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -338,12 +338,16 @@ function buildCodexConfig(contract, existingConfig, syncInfo) {
338
338
  ].filter(Boolean).join('\n\n') + '\n';
339
339
  }
340
340
 
341
+ function baseNativeConfigForContract(contract, syncInfo) {
342
+ return readTextFile(syncInfo?.sourceConfigPath);
343
+ }
344
+
341
345
  function writeConfig(contract) {
342
346
  const home = contract.auth.runtimeHome;
343
347
  if (!home) return;
344
348
  fs.mkdirSync(home, { recursive: true });
345
349
  const syncInfo = syncCodexNativeConfig(home);
346
- const nativeHookTrust = hookTrustToml(readTextFile(syncInfo?.sourceConfigPath));
350
+ const nativeConfig = baseNativeConfigForContract(contract, syncInfo);
347
351
  const configPath = path.join(home, 'config.toml');
348
352
  if (contract.authMethod === 'provider_auth') {
349
353
  const sourceAuth = path.join(syncInfo?.sourceDir || path.join(os.homedir(), '.codex'), 'auth.json');
@@ -352,10 +356,10 @@ function writeConfig(contract) {
352
356
  fs.copyFileSync(sourceAuth, targetAuth);
353
357
  fs.chmodSync(targetAuth, 0o600);
354
358
  }
355
- fs.writeFileSync(configPath, buildCodexConfig(contract, nativeHookTrust, syncInfo), { mode: 0o600 });
359
+ fs.writeFileSync(configPath, buildCodexConfig(contract, nativeConfig, syncInfo), { mode: 0o600 });
356
360
  return;
357
361
  }
358
- fs.writeFileSync(configPath, buildCodexConfig(contract, nativeHookTrust, syncInfo), { mode: 0o600 });
362
+ fs.writeFileSync(configPath, buildCodexConfig(contract, nativeConfig, syncInfo), { mode: 0o600 });
359
363
  fs.writeFileSync(path.join(home, 'auth.json'), JSON.stringify({
360
364
  auth_mode: 'apikey',
361
365
  OPENAI_API_KEY: contract.auth.tokenRef,
@@ -37,6 +37,8 @@ test('codex native sync copies hook support without bulk runtime state', () => {
37
37
  fs.mkdirSync(path.join(source, 'worktrees'), { recursive: true });
38
38
  fs.mkdirSync(path.join(source, 'plugins'), { recursive: true });
39
39
  fs.mkdirSync(path.join(source, 'supermemory'), { recursive: true });
40
+ fs.writeFileSync(path.join(source, 'config.toml'), 'model = "gpt-5.5"');
41
+ fs.writeFileSync(path.join(source, 'preferences.json'), '{"theme":"dark"}');
40
42
  fs.writeFileSync(path.join(source, 'hooks.json'), '{"hooks":{}}');
41
43
  fs.writeFileSync(path.join(source, 'supermemory.json'), '{"projectContainerTag":"test"}');
42
44
  fs.writeFileSync(path.join(source, 'supermemory', 'recall.js'), 'console.log("recall")');
@@ -51,6 +53,8 @@ test('codex native sync copies hook support without bulk runtime state', () => {
51
53
  const result = syncCodexNativeConfig(runtimeHome);
52
54
 
53
55
  assert.equal(result.sourceDir, source);
56
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'config.toml')), true);
57
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'preferences.json')), true);
54
58
  assert.equal(fs.existsSync(path.join(runtimeHome, 'hooks.json')), true);
55
59
  assert.equal(fs.existsSync(path.join(runtimeHome, 'supermemory.json')), true);
56
60
  assert.equal(fs.existsSync(path.join(runtimeHome, 'supermemory', 'recall.js')), true);
@@ -61,7 +65,7 @@ test('codex native sync copies hook support without bulk runtime state', () => {
61
65
  });
62
66
  });
63
67
 
64
- test('codex provider config keeps hooks but drops native model and mcp config', () => {
68
+ test('codex provider config keeps native mcp config and overrides managed model provider', () => {
65
69
  withNativeHome((home) => {
66
70
  const source = path.join(home, '.codex');
67
71
  fs.mkdirSync(source, { recursive: true });
@@ -72,7 +76,10 @@ test('codex provider config keeps hooks but drops native model and mcp config',
72
76
  'model_provider = "native-provider"',
73
77
  '',
74
78
  '[mcp_servers.native]',
75
- 'command = "native-mcp"',
79
+ 'url = "https://example.com/mcp"',
80
+ '',
81
+ '[mcp_servers.native.http_headers]',
82
+ 'Authorization = "Bearer test"',
76
83
  '',
77
84
  `[hooks.state.${JSON.stringify(`${hooksPath}:UserPromptSubmit:0`)}]`,
78
85
  'trusted = true',
@@ -90,7 +97,47 @@ test('codex provider config keeps hooks but drops native model and mcp config',
90
97
  assert.match(config, /model_provider = "openai"/);
91
98
  assert.match(config, /codex_hooks = true/);
92
99
  assert.doesNotMatch(config, /native-provider/);
93
- assert.doesNotMatch(config, /mcp_servers\.native/);
100
+ assert.match(config, /\[mcp_servers\.native\]/);
101
+ assert.match(config, /\[mcp_servers\.native\.http_headers\]/);
102
+ assert.match(config, /Authorization = "Bearer test"/);
103
+ assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), true);
104
+ });
105
+ });
106
+
107
+ test('codex amalgm config keeps native mcp config and overrides managed model provider', () => {
108
+ withNativeHome((home) => {
109
+ const source = path.join(home, '.codex');
110
+ fs.mkdirSync(source, { recursive: true });
111
+ const hooksPath = path.join(source, 'hooks.json');
112
+ fs.writeFileSync(hooksPath, '{"hooks":{"UserPromptSubmit":[]}}');
113
+ fs.writeFileSync(path.join(source, 'config.toml'), [
114
+ 'model_provider = "native-provider"',
115
+ '',
116
+ '[mcp_servers.native]',
117
+ 'url = "https://example.com/mcp"',
118
+ '',
119
+ '[mcp_servers.native.http_headers]',
120
+ 'Authorization = "Bearer test"',
121
+ '',
122
+ `[hooks.state.${JSON.stringify(`${hooksPath}:UserPromptSubmit:0`)}]`,
123
+ 'trusted = true',
124
+ '',
125
+ ].join('\n'));
126
+
127
+ const runtimeHome = path.join(home, 'runtime-home');
128
+ codexPrivate.writeConfig({
129
+ authMethod: 'amalgm',
130
+ auth: { runtimeHome, baseUrl: 'https://amalgm.example/v1', tokenRef: 'test-token' },
131
+ mcpServers: [],
132
+ });
133
+
134
+ const config = fs.readFileSync(path.join(runtimeHome, 'config.toml'), 'utf8');
135
+ assert.match(config, /model_provider = "amalgm"/);
136
+ assert.match(config, /codex_hooks = true/);
137
+ assert.doesNotMatch(config, /native-provider/);
138
+ assert.match(config, /\[mcp_servers\.native\]/);
139
+ assert.match(config, /\[mcp_servers\.native\.http_headers\]/);
140
+ assert.match(config, /Authorization = "Bearer test"/);
94
141
  assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), true);
95
142
  });
96
143
  });
@@ -179,7 +179,10 @@ function syncCodexNativeConfig(runtimeHome) {
179
179
  if (!exists(sourceDir)) return null;
180
180
 
181
181
  pruneLegacyCodexRuntimeHome(runtimeHome);
182
+ const nativeConfig = copyDirBounded(sourceDir, runtimeHome);
182
183
  const copiedFiles = [
184
+ copyFileIfPresent(path.join(sourceDir, 'config.toml'), path.join(runtimeHome, 'config.toml')),
185
+ copyFileIfPresent(path.join(sourceDir, 'auth.json'), path.join(runtimeHome, 'auth.json')),
183
186
  copyFileIfPresent(path.join(sourceDir, 'hooks.json'), path.join(runtimeHome, 'hooks.json')),
184
187
  copyFileIfPresent(path.join(sourceDir, 'supermemory.json'), path.join(runtimeHome, 'supermemory.json')),
185
188
  ].filter(Boolean).length;
@@ -191,8 +194,8 @@ function syncCodexNativeConfig(runtimeHome) {
191
194
  sourceDir,
192
195
  runtimeHome,
193
196
  sourceConfigPath: path.join(sourceDir, 'config.toml'),
194
- copied: copiedFiles + (supermemory.copied ? 1 : 0) > 0,
195
- truncated: supermemory.truncated,
197
+ copied: nativeConfig.copied || copiedFiles + (supermemory.copied ? 1 : 0) > 0,
198
+ truncated: nativeConfig.truncated || supermemory.truncated,
196
199
  };
197
200
  }
198
201