amalgm 0.0.0 → 0.0.32

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 (116) hide show
  1. package/README.md +41 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1132 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +476 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +30 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +315 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +136 -0
  14. package/runtime/lib/local/credentialResolver.js +425 -0
  15. package/runtime/lib/mcpApps/registry.js +619 -0
  16. package/runtime/package.json +5 -0
  17. package/runtime/scripts/amalgm-mcp/agents/rest.js +144 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -0
  21. package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
  22. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
  23. package/runtime/scripts/amalgm-mcp/artifacts/store.js +157 -0
  24. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
  25. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
  26. package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
  27. package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
  28. package/runtime/scripts/amalgm-mcp/config.js +140 -0
  29. package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
  30. package/runtime/scripts/amalgm-mcp/deps.js +40 -0
  31. package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
  32. package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
  33. package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
  34. package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
  35. package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
  36. package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
  37. package/runtime/scripts/amalgm-mcp/events/store.js +113 -0
  38. package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
  39. package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
  40. package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
  41. package/runtime/scripts/amalgm-mcp/index.js +100 -0
  42. package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
  43. package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
  44. package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
  45. package/runtime/scripts/amalgm-mcp/lib/prefs.js +441 -0
  46. package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
  47. package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
  48. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
  49. package/runtime/scripts/amalgm-mcp/local/rest.js +87 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
  53. package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
  54. package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
  55. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  56. package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
  57. package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
  58. package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
  59. package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  61. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  62. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  63. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  64. package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
  65. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  66. package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
  67. package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
  68. package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
  69. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
  70. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  71. package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
  72. package/runtime/scripts/chat-core/adapters/claude.js +165 -0
  73. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  74. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  75. package/runtime/scripts/chat-core/auth.js +177 -0
  76. package/runtime/scripts/chat-core/contract.js +328 -0
  77. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  78. package/runtime/scripts/chat-core/egress.js +87 -0
  79. package/runtime/scripts/chat-core/engine.js +253 -0
  80. package/runtime/scripts/chat-core/event-schema.js +231 -0
  81. package/runtime/scripts/chat-core/events.js +190 -0
  82. package/runtime/scripts/chat-core/index.js +11 -0
  83. package/runtime/scripts/chat-core/input.js +50 -0
  84. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  85. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  86. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  87. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  88. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  89. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  90. package/runtime/scripts/chat-core/parts.js +253 -0
  91. package/runtime/scripts/chat-core/recorder.js +65 -0
  92. package/runtime/scripts/chat-core/runtime.js +86 -0
  93. package/runtime/scripts/chat-core/server.js +163 -0
  94. package/runtime/scripts/chat-core/sse.js +196 -0
  95. package/runtime/scripts/chat-core/stores.js +100 -0
  96. package/runtime/scripts/chat-core/tool-display.js +149 -0
  97. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  98. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  99. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  100. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  101. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  102. package/runtime/scripts/chat-core/usage.js +343 -0
  103. package/runtime/scripts/chat-server/config.js +110 -0
  104. package/runtime/scripts/chat-server/db.js +529 -0
  105. package/runtime/scripts/chat-server/index.js +33 -0
  106. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  107. package/runtime/scripts/chat-server.js +75 -0
  108. package/runtime/scripts/credential-adapter.js +131 -0
  109. package/runtime/scripts/fs-watcher.js +888 -0
  110. package/runtime/scripts/local-gateway.js +854 -0
  111. package/runtime/scripts/platform-context.txt +246 -0
  112. package/runtime/scripts/port-monitor.js +175 -0
  113. package/runtime/scripts/proxy-token-store.js +162 -0
  114. package/runtime/scripts/runtime-auth.js +163 -0
  115. package/runtime/scripts/test-claude-code-models.js +87 -0
  116. package/runtime/tsconfig.json +15 -0
@@ -0,0 +1,608 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ function amalgmDir() {
9
+ return process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
10
+ }
11
+
12
+ function nativeNodeModulesDirs() {
13
+ return [
14
+ process.env.AMALGM_NATIVE_NODE_MODULES,
15
+ path.join(amalgmDir(), 'native', 'node_modules'),
16
+ ].filter(Boolean);
17
+ }
18
+
19
+ function executableExists(file) {
20
+ try {
21
+ fs.accessSync(file, fs.constants.X_OK);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function findPackageJson(packageName) {
29
+ const candidateDirs = [
30
+ ...nativeNodeModulesDirs(),
31
+ ...(require.resolve.paths(packageName) || []),
32
+ ];
33
+ for (const dir of candidateDirs) {
34
+ const candidate = path.join(dir, packageName, 'package.json');
35
+ if (fs.existsSync(candidate)) return candidate;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function packageRoot(packageName) {
41
+ const manifest = findPackageJson(packageName);
42
+ if (manifest) return path.dirname(manifest);
43
+ try {
44
+ return path.dirname(require.resolve(`${packageName}/package.json`));
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function findOnPath(command) {
51
+ if (!command || command.includes('/') || command.includes('\\')) return null;
52
+ const binaryNames = process.platform === 'win32' && !path.extname(command)
53
+ ? unique([
54
+ command,
55
+ ...String(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
56
+ .split(';')
57
+ .filter(Boolean)
58
+ .map((ext) => `${command}${ext.toLowerCase()}`),
59
+ ])
60
+ : [command];
61
+ for (const dir of String(process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
62
+ for (const binaryName of binaryNames) {
63
+ const candidate = path.join(dir, binaryName);
64
+ if (executableExists(candidate)) return candidate;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+
70
+ function packageBin(packageName, binName) {
71
+ const root = packageRoot(packageName);
72
+ if (!root) return null;
73
+ try {
74
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
75
+ const bin = typeof manifest.bin === 'string'
76
+ ? manifest.bin
77
+ : manifest.bin?.[binName];
78
+ if (!bin) return null;
79
+ const candidate = path.join(root, bin);
80
+ return executableExists(candidate) ? candidate : null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function chmodExecutableBestEffort(file) {
87
+ if (!file || process.platform === 'win32') return;
88
+ try {
89
+ const stat = fs.statSync(file);
90
+ fs.chmodSync(file, stat.mode | 0o111);
91
+ } catch {
92
+ // chmod is best-effort.
93
+ }
94
+ }
95
+
96
+ function sameRealPath(a, b) {
97
+ try {
98
+ return fs.realpathSync(a) === fs.realpathSync(b);
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ function unique(values) {
105
+ const seen = new Set();
106
+ return values.filter((value) => {
107
+ if (!value || seen.has(value)) return false;
108
+ seen.add(value);
109
+ return true;
110
+ });
111
+ }
112
+
113
+ function nearestPackageManifest() {
114
+ let current = __dirname;
115
+ for (let i = 0; i < 12; i += 1) {
116
+ const candidate = path.join(current, 'package.json');
117
+ if (fs.existsSync(candidate)) {
118
+ try {
119
+ const manifest = JSON.parse(fs.readFileSync(candidate, 'utf8'));
120
+ if (manifest.dependencies || manifest.optionalDependencies) return manifest;
121
+ } catch {
122
+ // Keep walking.
123
+ }
124
+ }
125
+ const next = path.dirname(current);
126
+ if (next === current) break;
127
+ current = next;
128
+ }
129
+ return {};
130
+ }
131
+
132
+ function dependencySpec(packageName, fallback = 'latest') {
133
+ const manifest = nearestPackageManifest();
134
+ return manifest.dependencies?.[packageName]
135
+ || manifest.optionalDependencies?.[packageName]
136
+ || fallback;
137
+ }
138
+
139
+ function linuxLibcSuffix() {
140
+ if (process.platform !== 'linux') return '';
141
+ const glibc = process.report?.getReport?.().header?.glibcVersionRuntime;
142
+ return glibc ? '' : '-musl';
143
+ }
144
+
145
+ function nativePackageSuffix() {
146
+ if (!['linux', 'darwin', 'win32'].includes(process.platform)) return null;
147
+ if (!['x64', 'arm64'].includes(process.arch)) return null;
148
+ return `${process.platform}-${process.arch}${linuxLibcSuffix()}`;
149
+ }
150
+
151
+ function claudeTargetPackage() {
152
+ const suffix = nativePackageSuffix();
153
+ if (!suffix) return null;
154
+ return `@anthropic-ai/claude-agent-sdk-${suffix}`;
155
+ }
156
+
157
+ function bundledClaudeBinary() {
158
+ const packageName = claudeTargetPackage();
159
+ if (!packageName) return null;
160
+ const root = packageRoot(packageName);
161
+ if (!root) return null;
162
+ const candidate = path.join(root, process.platform === 'win32' ? 'claude.exe' : 'claude');
163
+ return executableExists(candidate) ? candidate : null;
164
+ }
165
+
166
+ function resolveClaudeBinary() {
167
+ const candidates = [
168
+ process.env.CLAUDE_CODE_BINARY,
169
+ bundledClaudeBinary(),
170
+ path.join(os.homedir(), '.npm-global', 'bin', process.platform === 'win32' ? 'claude.exe' : 'claude'),
171
+ '/opt/homebrew/bin/claude',
172
+ '/usr/local/bin/claude',
173
+ findOnPath('claude'),
174
+ ].filter(Boolean);
175
+
176
+ for (const candidate of candidates) {
177
+ if (executableExists(candidate)) return candidate;
178
+ }
179
+ return null;
180
+ }
181
+
182
+ function codexTarget() {
183
+ const binaryName = process.platform === 'win32' ? 'codex.exe' : 'codex';
184
+ if (process.platform === 'linux' && process.arch === 'x64') {
185
+ return { packageName: '@openai/codex-linux-x64', triple: 'x86_64-unknown-linux-musl', binaryName };
186
+ }
187
+ if (process.platform === 'linux' && process.arch === 'arm64') {
188
+ return { packageName: '@openai/codex-linux-arm64', triple: 'aarch64-unknown-linux-musl', binaryName };
189
+ }
190
+ if (process.platform === 'darwin' && process.arch === 'x64') {
191
+ return { packageName: '@openai/codex-darwin-x64', triple: 'x86_64-apple-darwin', binaryName };
192
+ }
193
+ if (process.platform === 'darwin' && process.arch === 'arm64') {
194
+ return { packageName: '@openai/codex-darwin-arm64', triple: 'aarch64-apple-darwin', binaryName };
195
+ }
196
+ if (process.platform === 'win32' && process.arch === 'x64') {
197
+ return { packageName: '@openai/codex-win32-x64', triple: 'x86_64-pc-windows-msvc', binaryName };
198
+ }
199
+ if (process.platform === 'win32' && process.arch === 'arm64') {
200
+ return { packageName: '@openai/codex-win32-arm64', triple: 'aarch64-pc-windows-msvc', binaryName };
201
+ }
202
+ return null;
203
+ }
204
+
205
+ function codexInstallPackageName() {
206
+ return codexTarget()?.packageName || null;
207
+ }
208
+
209
+ function codexVendorRoot() {
210
+ const target = codexTarget();
211
+ if (!target) return null;
212
+ const platformRoot = packageRoot(target.packageName);
213
+ if (platformRoot) return path.join(platformRoot, 'vendor');
214
+ const wrapperRoot = packageRoot('@openai/codex');
215
+ if (wrapperRoot) {
216
+ const localVendorRoot = path.join(wrapperRoot, 'vendor');
217
+ if (fs.existsSync(localVendorRoot)) return localVendorRoot;
218
+ }
219
+ return null;
220
+ }
221
+
222
+ function bundledCodexBinary() {
223
+ const target = codexTarget();
224
+ const vendorRoot = codexVendorRoot();
225
+ if (target && vendorRoot) {
226
+ const candidate = path.join(vendorRoot, target.triple, 'codex', target.binaryName);
227
+ if (executableExists(candidate)) return candidate;
228
+ }
229
+ return packageBin('@openai/codex', 'codex');
230
+ }
231
+
232
+ function bundledCodexPathDirs() {
233
+ const target = codexTarget();
234
+ const vendorRoot = codexVendorRoot();
235
+ if (!target || !vendorRoot) return [];
236
+ const candidate = path.join(vendorRoot, target.triple, 'path');
237
+ return executableExists(path.join(candidate, process.platform === 'win32' ? 'rg.exe' : 'rg')) ? [candidate] : [];
238
+ }
239
+
240
+ function openCodePlatform() {
241
+ if (process.platform === 'darwin') return 'darwin';
242
+ if (process.platform === 'linux') return 'linux';
243
+ if (process.platform === 'win32') return 'windows';
244
+ return process.platform;
245
+ }
246
+
247
+ function supportsAvx2() {
248
+ if (process.arch !== 'x64') return false;
249
+ if (process.platform === 'linux') {
250
+ try {
251
+ return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync('/proc/cpuinfo', 'utf8'));
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
256
+ if (process.platform === 'darwin') {
257
+ try {
258
+ const { spawnSync } = require('child_process');
259
+ const res = spawnSync('sysctl', ['-n', 'hw.optional.avx2_0'], { encoding: 'utf8', timeout: 1500 });
260
+ return res.status === 0 && String(res.stdout || '').trim() === '1';
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+ return false;
266
+ }
267
+
268
+ function openCodePackageCandidates() {
269
+ if (!['linux', 'darwin', 'win32'].includes(process.platform)) return [];
270
+ if (!['x64', 'arm64', 'arm'].includes(process.arch)) return [];
271
+ const platform = openCodePlatform();
272
+ const arch = process.arch;
273
+ const base = `opencode-${platform}-${arch}`;
274
+ if (process.platform !== 'linux') return [base];
275
+
276
+ const musl = linuxLibcSuffix() === '-musl';
277
+ if (arch !== 'x64') return musl ? [`${base}-musl`, base] : [base, `${base}-musl`];
278
+
279
+ const preferred = supportsAvx2() ? [base, `${base}-baseline`] : [`${base}-baseline`, base];
280
+ const suffixes = musl ? ['-musl', ''] : ['', '-musl'];
281
+ return preferred.flatMap((name) => suffixes.map((suffix) => `${name}${suffix}`));
282
+ }
283
+
284
+ function openCodeInstallPackageNames() {
285
+ return openCodePackageCandidates();
286
+ }
287
+
288
+ function bundledOpenCodeBinary() {
289
+ const binaryName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
290
+ for (const packageName of openCodePackageCandidates()) {
291
+ const root = packageRoot(packageName);
292
+ if (!root) continue;
293
+ const candidate = path.join(root, 'bin', binaryName);
294
+ if (executableExists(candidate)) return candidate;
295
+ }
296
+ const wrapperRoot = packageRoot('opencode-ai');
297
+ if (wrapperRoot) {
298
+ const cached = path.join(wrapperRoot, 'bin', '.opencode');
299
+ if (executableExists(cached)) return cached;
300
+ }
301
+ return packageBin('opencode-ai', 'opencode');
302
+ }
303
+
304
+ function binaryStatus() {
305
+ const claudeBinary = resolveClaudeBinary();
306
+ const codexBinary = bundledCodexBinary() || findOnPath('codex');
307
+ const openCodeBinary = bundledOpenCodeBinary() || findOnPath('opencode');
308
+ return [
309
+ {
310
+ id: 'claude_code',
311
+ name: 'Claude Code',
312
+ ok: Boolean(claudeBinary),
313
+ path: claudeBinary,
314
+ packageName: claudeTargetPackage(),
315
+ },
316
+ {
317
+ id: 'codex',
318
+ name: 'Codex',
319
+ ok: Boolean(codexBinary),
320
+ path: codexBinary,
321
+ packageName: codexInstallPackageName(),
322
+ },
323
+ {
324
+ id: 'opencode',
325
+ name: 'OpenCode',
326
+ ok: Boolean(openCodeBinary),
327
+ path: openCodeBinary,
328
+ packageName: openCodeInstallPackageNames()[0] || null,
329
+ },
330
+ ];
331
+ }
332
+
333
+ function commandTargets() {
334
+ return [
335
+ {
336
+ command: 'claude',
337
+ name: 'Claude Code',
338
+ path: resolveClaudeBinary(),
339
+ },
340
+ {
341
+ command: 'codex',
342
+ name: 'Codex',
343
+ path: packageBin('@openai/codex', 'codex') || bundledCodexBinary(),
344
+ },
345
+ {
346
+ command: 'opencode',
347
+ name: 'OpenCode',
348
+ path: packageBin('opencode-ai', 'opencode') || bundledOpenCodeBinary(),
349
+ },
350
+ ];
351
+ }
352
+
353
+ function installRoot() {
354
+ return path.join(amalgmDir(), 'native');
355
+ }
356
+
357
+ function npmCommand() {
358
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
359
+ }
360
+
361
+ function npmGlobalBinDir() {
362
+ try {
363
+ const result = spawnSync(npmCommand(), ['prefix', '-g'], {
364
+ encoding: 'utf8',
365
+ timeout: 5000,
366
+ stdio: ['ignore', 'pipe', 'ignore'],
367
+ shell: process.platform === 'win32',
368
+ windowsHide: true,
369
+ });
370
+ if (result.status !== 0) return null;
371
+ const prefix = String(result.stdout || '').trim();
372
+ if (!prefix) return null;
373
+ return process.platform === 'win32' ? prefix : path.join(prefix, 'bin');
374
+ } catch {
375
+ return null;
376
+ }
377
+ }
378
+
379
+ function commandShimDirs() {
380
+ return unique([
381
+ process.env.AMALGM_AGENT_BIN_DIR,
382
+ findOnPath('amalgm') ? path.dirname(findOnPath('amalgm')) : null,
383
+ npmGlobalBinDir(),
384
+ ]);
385
+ }
386
+
387
+ function entryStat(file) {
388
+ try {
389
+ return fs.lstatSync(file);
390
+ } catch {
391
+ return null;
392
+ }
393
+ }
394
+
395
+ function ensurePosixCommandShim(command, target, binDir, options = {}) {
396
+ fs.mkdirSync(binDir, { recursive: true });
397
+ chmodExecutableBestEffort(target);
398
+
399
+ const linkPath = path.join(binDir, command);
400
+ const existing = entryStat(linkPath);
401
+ if (existing) {
402
+ if (sameRealPath(linkPath, target)) {
403
+ return { ok: true, command, path: linkPath, target, status: 'exists' };
404
+ }
405
+ if (!existing.isSymbolicLink() && !options.force) {
406
+ return { ok: true, command, path: linkPath, target, status: 'kept-existing' };
407
+ }
408
+ try {
409
+ fs.unlinkSync(linkPath);
410
+ } catch (error) {
411
+ return { ok: false, command, path: linkPath, target, status: 'failed', error };
412
+ }
413
+ }
414
+
415
+ try {
416
+ fs.symlinkSync(target, linkPath);
417
+ return { ok: true, command, path: linkPath, target, status: 'created' };
418
+ } catch (error) {
419
+ return { ok: false, command, path: linkPath, target, status: 'failed', error };
420
+ }
421
+ }
422
+
423
+ function ensureWindowsCommandShim(command, target, binDir) {
424
+ fs.mkdirSync(binDir, { recursive: true });
425
+ const shimPath = path.join(binDir, `${command}.cmd`);
426
+ const usesNode = /\.m?js$/i.test(target);
427
+ const runner = usesNode ? process.execPath : target;
428
+ const args = usesNode ? `"${target}" %*` : '%*';
429
+ try {
430
+ fs.writeFileSync(shimPath, `@echo off\r\n"${runner}" ${args}\r\n`);
431
+ return { ok: true, command, path: shimPath, target, status: 'created' };
432
+ } catch (error) {
433
+ return { ok: false, command, path: shimPath, target, status: 'failed', error };
434
+ }
435
+ }
436
+
437
+ function ensureCommandShim(command, target, binDir, options = {}) {
438
+ if (!target || !fs.existsSync(target)) {
439
+ return { ok: false, command, path: null, target, status: 'missing-target' };
440
+ }
441
+ if (process.platform === 'win32') {
442
+ return ensureWindowsCommandShim(command, target, binDir);
443
+ }
444
+ return ensurePosixCommandShim(command, target, binDir, options);
445
+ }
446
+
447
+ function ensureAgentCommandShims(options = {}) {
448
+ const binDirs = commandShimDirs();
449
+ const targets = commandTargets();
450
+ const logger = options.logger || console;
451
+ const results = [];
452
+
453
+ if (binDirs.length === 0) {
454
+ return {
455
+ ok: false,
456
+ shims: targets.map((target) => ({
457
+ command: target.command,
458
+ ok: false,
459
+ status: 'missing-bin-dir',
460
+ })),
461
+ error: new Error('Could not find a global bin directory for agent commands'),
462
+ };
463
+ }
464
+
465
+ for (const target of targets) {
466
+ if (!target.path) {
467
+ results.push({
468
+ command: target.command,
469
+ name: target.name,
470
+ ok: false,
471
+ status: 'missing-target',
472
+ });
473
+ continue;
474
+ }
475
+
476
+ let last = null;
477
+ for (const binDir of binDirs) {
478
+ last = ensureCommandShim(target.command, target.path, binDir, options);
479
+ if (last.ok) break;
480
+ }
481
+ results.push({ ...last, name: target.name });
482
+ }
483
+
484
+ const created = results.filter((result) => result.ok && result.status === 'created');
485
+ if (created.length > 0 && !options.quiet) {
486
+ logger.log?.(`Agent commands are ready: ${created.map((result) => result.command).join(', ')}`);
487
+ }
488
+
489
+ const failures = results.filter((result) => !result.ok);
490
+ return {
491
+ ok: failures.length === 0,
492
+ shims: results,
493
+ error: failures.length > 0
494
+ ? new Error(`Could not expose agent commands: ${failures.map((result) => result.command).join(', ')}`)
495
+ : null,
496
+ };
497
+ }
498
+
499
+ function agentCommandShimStatus() {
500
+ const binDirs = commandShimDirs();
501
+ return commandTargets().map((target) => {
502
+ const commandPath = findOnPath(target.command)
503
+ || binDirs
504
+ .map((binDir) => path.join(binDir, process.platform === 'win32' ? `${target.command}.cmd` : target.command))
505
+ .find((candidate) => fs.existsSync(candidate))
506
+ || null;
507
+ return {
508
+ command: target.command,
509
+ name: target.name,
510
+ ok: Boolean(commandPath),
511
+ path: commandPath,
512
+ target: target.path,
513
+ status: commandPath
514
+ ? (target.path && sameRealPath(commandPath, target.path) ? 'linked' : 'available')
515
+ : (target.path ? 'missing-command' : 'missing-target'),
516
+ };
517
+ });
518
+ }
519
+
520
+ function packageInstallSpecs() {
521
+ return [
522
+ `@anthropic-ai/claude-agent-sdk@${dependencySpec('@anthropic-ai/claude-agent-sdk', 'latest')}`,
523
+ `@openai/codex@${dependencySpec('@openai/codex', 'latest')}`,
524
+ `opencode-ai@${dependencySpec('opencode-ai', 'latest')}`,
525
+ ];
526
+ }
527
+
528
+ function ensureNativeInstallPackageJson(root) {
529
+ fs.mkdirSync(root, { recursive: true });
530
+ const manifest = path.join(root, 'package.json');
531
+ if (!fs.existsSync(manifest)) {
532
+ fs.writeFileSync(manifest, `${JSON.stringify({ private: true }, null, 2)}\n`);
533
+ }
534
+ }
535
+
536
+ function ensureNativeBinaries(options = {}) {
537
+ const before = binaryStatus();
538
+ const missing = before.filter((item) => !item.ok);
539
+ if (missing.length === 0) {
540
+ return { ok: true, installed: false, before, after: before };
541
+ }
542
+
543
+ if (process.env.AMALGM_SKIP_NATIVE_INSTALL === '1') {
544
+ return {
545
+ ok: false,
546
+ installed: false,
547
+ before,
548
+ after: before,
549
+ error: new Error(`Missing agent binaries: ${missing.map((item) => item.name).join(', ')}`),
550
+ };
551
+ }
552
+
553
+ const root = installRoot();
554
+ ensureNativeInstallPackageJson(root);
555
+ const args = [
556
+ 'install',
557
+ '--prefix',
558
+ root,
559
+ '--no-audit',
560
+ '--no-fund',
561
+ '--include=optional',
562
+ ...packageInstallSpecs(),
563
+ ];
564
+ const logger = options.logger || console;
565
+ if (!options.quiet) {
566
+ logger.log?.(`Installing missing agent binaries: ${missing.map((item) => item.name).join(', ')}`);
567
+ }
568
+
569
+ const result = spawnSync(npmCommand(), args, {
570
+ stdio: options.quiet ? 'pipe' : 'inherit',
571
+ encoding: 'utf8',
572
+ shell: process.platform === 'win32',
573
+ windowsHide: true,
574
+ env: {
575
+ ...process.env,
576
+ npm_config_audit: 'false',
577
+ npm_config_fund: 'false',
578
+ npm_config_optional: 'true',
579
+ },
580
+ timeout: Number(process.env.AMALGM_NATIVE_INSTALL_TIMEOUT_MS || 180000),
581
+ });
582
+
583
+ const after = binaryStatus();
584
+ if (result.status === 0 && after.every((item) => item.ok)) {
585
+ return { ok: true, installed: true, before, after };
586
+ }
587
+
588
+ const stderr = String(result.stderr || '').trim();
589
+ return {
590
+ ok: false,
591
+ installed: result.status === 0,
592
+ before,
593
+ after,
594
+ error: new Error(stderr || `npm install exited with status ${result.status ?? result.signal ?? 'unknown'}`),
595
+ };
596
+ }
597
+
598
+ module.exports = {
599
+ binaryStatus,
600
+ bundledCodexPathDirs,
601
+ bundledClaudeBinary: resolveClaudeBinary,
602
+ bundledCodexBinary,
603
+ bundledOpenCodeBinary,
604
+ agentCommandShimStatus,
605
+ ensureAgentCommandShims,
606
+ ensureNativeBinaries,
607
+ executableExists,
608
+ };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const { PLATFORM_CONTEXT } = require('../../chat-server/config');
4
+
5
+ function trimBlock(value) {
6
+ return String(value || '').trim();
7
+ }
8
+
9
+ function composeSystemPrompt(contract) {
10
+ const parts = [];
11
+ const platform = trimBlock(PLATFORM_CONTEXT);
12
+ const custom = trimBlock(contract?.systemPrompt);
13
+ if (platform) parts.push(platform);
14
+ if (custom) parts.push(custom);
15
+ return parts.join('\n\n');
16
+ }
17
+
18
+ module.exports = {
19
+ composeSystemPrompt,
20
+ };