amalgm 0.1.68 → 0.1.69
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/automations/store.js +3 -3
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +3 -0
- package/runtime/scripts/chat-core/adapters/claude.js +5 -5
- package/runtime/scripts/chat-core/adapters/codex.js +25 -11
- package/runtime/scripts/chat-core/adapters/opencode.js +12 -6
- package/runtime/scripts/chat-core/tests/native-config.test.js +45 -1
- package/runtime/scripts/chat-core/tooling/native-binaries.js +35 -11
- package/runtime/scripts/chat-core/tooling/runtime-home.js +29 -0
- package/runtime/scripts/platform-context.txt +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
18
18
|
"prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
19
19
|
"pack:dry": "npm pack --dry-run",
|
|
20
|
-
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/state-migration.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
|
|
20
|
+
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/state-migration.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/chat-core/tooling/runtime-home.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|
|
@@ -462,12 +462,12 @@ function upsertAutomationRow(db, automation) {
|
|
|
462
462
|
function workflowStatus(workflow) {
|
|
463
463
|
if (!workflow || workflow.enabled === false) return 'paused';
|
|
464
464
|
if (Array.isArray(workflow.compilerErrors) && workflow.compilerErrors.length > 0) return 'error';
|
|
465
|
-
return '
|
|
465
|
+
return 'active';
|
|
466
466
|
}
|
|
467
467
|
|
|
468
468
|
function triggerStatus(trigger) {
|
|
469
469
|
if (!trigger || trigger.enabled === false) return 'paused';
|
|
470
|
-
return '
|
|
470
|
+
return 'active';
|
|
471
471
|
}
|
|
472
472
|
|
|
473
473
|
function automationStatus(automation, triggers, workflow) {
|
|
@@ -475,7 +475,7 @@ function automationStatus(automation, triggers, workflow) {
|
|
|
475
475
|
if (workflowStatus(workflow) === 'error') return 'error';
|
|
476
476
|
if (triggers.some((trigger) => triggerStatus(trigger) === 'error')) return 'error';
|
|
477
477
|
if (triggers.length > 0 && triggers.every((trigger) => trigger.enabled === false)) return 'paused';
|
|
478
|
-
return '
|
|
478
|
+
return 'active';
|
|
479
479
|
}
|
|
480
480
|
|
|
481
481
|
function composeAutomation(base, triggers, workflow) {
|
|
@@ -44,6 +44,9 @@ test('automation stores trigger plus workflow in local live SQLite resources', (
|
|
|
44
44
|
assert.equal(automation.triggers.length, 1);
|
|
45
45
|
assert.equal(automation.workflows.length, 1);
|
|
46
46
|
assert.equal(automation.triggers[0].kind, 'event');
|
|
47
|
+
assert.equal(automation.status, 'active');
|
|
48
|
+
assert.equal(automation.triggers[0].status, 'active');
|
|
49
|
+
assert.equal(automation.workflows[0].status, 'active');
|
|
47
50
|
|
|
48
51
|
const snapshot = buildSnapshot('automations,triggers,workflows');
|
|
49
52
|
assert.equal(snapshot.resources.automations.some((item) => item.id === 'automation-webhook'), true);
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { done, errorEvent, reasoningStarted, usageFinal } = require('../events');
|
|
5
5
|
const { promptText } = require('../input');
|
|
6
|
-
const { runtimeEnv } = require('../auth');
|
|
7
6
|
const { normalizeClaudeMessage, usageRecordsFromClaudeResult, usageFromClaude } = require('../normalizers/claude');
|
|
8
7
|
const { recordNativeEvent } = require('../recorder');
|
|
9
8
|
const { toClaudeMcpServers } = require('../tooling/mcp-bundle');
|
|
10
9
|
const { bundledClaudeBinary } = require('../tooling/native-binaries');
|
|
11
|
-
const { claudeNativeHookSettings
|
|
10
|
+
const { claudeNativeHookSettings } = require('../tooling/native-config');
|
|
12
11
|
const { importPackage } = require('../tooling/package-import');
|
|
12
|
+
const { prepareHarnessRuntime } = require('../tooling/runtime-home');
|
|
13
13
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
14
14
|
|
|
15
15
|
function redactSecrets(text) {
|
|
@@ -32,7 +32,7 @@ class ClaudeAdapter {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
options(contract, extra = {}) {
|
|
35
|
-
|
|
35
|
+
const runtime = prepareHarnessRuntime(contract);
|
|
36
36
|
const systemPrompt = composeSystemPrompt(contract);
|
|
37
37
|
const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
|
|
38
38
|
const settings = {
|
|
@@ -41,7 +41,7 @@ class ClaudeAdapter {
|
|
|
41
41
|
};
|
|
42
42
|
return {
|
|
43
43
|
cwd: contract.cwd,
|
|
44
|
-
env:
|
|
44
|
+
env: runtime.env,
|
|
45
45
|
model: contract.cliModel,
|
|
46
46
|
...(contract.reasoningEffort ? { effort: contract.reasoningEffort } : {}),
|
|
47
47
|
...(Object.keys(settings).length > 0 ? { settings } : {}),
|
|
@@ -51,7 +51,7 @@ class ClaudeAdapter {
|
|
|
51
51
|
permissionMode: 'bypassPermissions',
|
|
52
52
|
allowDangerouslySkipPermissions: true,
|
|
53
53
|
...(process.env.CHAT_CORE_DEBUG_CLAUDE === '1'
|
|
54
|
-
? { debug: true, debugFile: path.join(
|
|
54
|
+
? { debug: true, debugFile: path.join(runtime.runtimeHome, 'claude-debug.log') }
|
|
55
55
|
: {}),
|
|
56
56
|
settingSources: [],
|
|
57
57
|
strictMcpConfig: false,
|
|
@@ -6,12 +6,12 @@ const os = require('os');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { done, errorEvent } = require('../events');
|
|
8
8
|
const { promptText } = require('../input');
|
|
9
|
-
const { runtimeEnv } = require('../auth');
|
|
10
9
|
const { codexErrorMessage, normalizeCodexNotification } = require('../normalizers/codex');
|
|
11
10
|
const { recordNativeEvent } = require('../recorder');
|
|
12
11
|
const { relayedMcpServers, toCodexMcpToml } = require('../tooling/mcp-bundle');
|
|
13
|
-
const { bundledCodexBinary, bundledCodexPathDirs, executableExists } = require('../tooling/native-binaries');
|
|
12
|
+
const { bundledCodexBinary, bundledCodexPathDirs, executableExists, findOnPath } = require('../tooling/native-binaries');
|
|
14
13
|
const { syncCodexNativeConfig } = require('../tooling/native-config');
|
|
14
|
+
const { prepareHarnessRuntime } = require('../tooling/runtime-home');
|
|
15
15
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
16
16
|
|
|
17
17
|
class JsonLineRpc {
|
|
@@ -139,16 +139,29 @@ class JsonLineRpc {
|
|
|
139
139
|
|
|
140
140
|
function resolveBinary() {
|
|
141
141
|
const home = process.env.HOME || os.homedir();
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
const candidates = [
|
|
143
|
+
process.env.CODEX_BINARY,
|
|
144
|
+
bundledCodexBinary(),
|
|
145
|
+
path.join(home, '.npm-global/bin/codex'),
|
|
146
|
+
'/opt/homebrew/bin/codex',
|
|
147
|
+
'/usr/local/bin/codex',
|
|
148
|
+
findOnPath('codex'),
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
const failures = [];
|
|
151
|
+
for (const candidate of candidates) {
|
|
152
|
+
if (!executableExists(candidate)) {
|
|
153
|
+
failures.push(`${candidate} (missing or not executable)`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
144
156
|
const res = spawnSync(candidate, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
|
|
145
157
|
if (res.status === 0) return candidate;
|
|
158
|
+
failures.push(`${candidate} (--version failed: ${res.error?.message || res.status || res.signal || 'unknown'})`);
|
|
146
159
|
}
|
|
147
|
-
|
|
160
|
+
throw new Error(`Codex CLI binary is not available. Checked: ${failures.join(', ') || 'no candidates'}. Reinstall Amalgm or run amalgm update so the bundled Codex native package is present.`);
|
|
148
161
|
}
|
|
149
162
|
|
|
150
|
-
function codexEnv(
|
|
151
|
-
const env =
|
|
163
|
+
function codexEnv(baseEnv, binary) {
|
|
164
|
+
const env = { ...(baseEnv || {}) };
|
|
152
165
|
if (binary !== bundledCodexBinary()) return env;
|
|
153
166
|
const pathDirs = bundledCodexPathDirs();
|
|
154
167
|
if (pathDirs.length > 0) {
|
|
@@ -346,11 +359,11 @@ function baseNativeConfigForContract(contract, syncInfo) {
|
|
|
346
359
|
return readTextFile(syncInfo?.sourceConfigPath);
|
|
347
360
|
}
|
|
348
361
|
|
|
349
|
-
function writeConfig(contract) {
|
|
362
|
+
function writeConfig(contract, syncInfo) {
|
|
350
363
|
const home = contract.auth.runtimeHome;
|
|
351
364
|
if (!home) return;
|
|
352
365
|
fs.mkdirSync(home, { recursive: true });
|
|
353
|
-
|
|
366
|
+
if (syncInfo === undefined) syncInfo = syncCodexNativeConfig(home);
|
|
354
367
|
const nativeConfig = baseNativeConfigForContract(contract, syncInfo);
|
|
355
368
|
const configPath = path.join(home, 'config.toml');
|
|
356
369
|
if (contract.authMethod === 'provider_auth') {
|
|
@@ -372,9 +385,10 @@ function writeConfig(contract) {
|
|
|
372
385
|
|
|
373
386
|
class CodexAdapter {
|
|
374
387
|
async create(contract) {
|
|
375
|
-
|
|
388
|
+
const runtime = prepareHarnessRuntime(contract);
|
|
389
|
+
writeConfig(contract, runtime.syncInfo);
|
|
376
390
|
const binary = resolveBinary();
|
|
377
|
-
const client = new JsonLineRpc({ binary, cwd: contract.cwd, env: codexEnv(
|
|
391
|
+
const client = new JsonLineRpc({ binary, cwd: contract.cwd, env: codexEnv(runtime.env, binary) });
|
|
378
392
|
await client.request('initialize', { clientInfo: { name: 'amalgm-chat-core', version: '1.0.0' }, capabilities: { experimentalApi: true } });
|
|
379
393
|
client.notify('initialized');
|
|
380
394
|
const threadOptions = {
|
|
@@ -7,12 +7,12 @@ const os = require('os');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const { done, errorEvent, reasoningStarted } = require('../events');
|
|
9
9
|
const { openCodeParts } = require('../input');
|
|
10
|
-
const { runtimeEnv } = require('../auth');
|
|
11
10
|
const { normalizeOpenCodeEvent, normalizeOpenCodePromptResult } = require('../normalizers/opencode');
|
|
12
11
|
const { recordNativeEvent } = require('../recorder');
|
|
13
12
|
const { toOpenCodeMcpConfig } = require('../tooling/mcp-bundle');
|
|
14
|
-
const { bundledOpenCodeBinary, executableExists } = require('../tooling/native-binaries');
|
|
13
|
+
const { bundledOpenCodeBinary, executableExists, findOnPath } = require('../tooling/native-binaries');
|
|
15
14
|
const { importPackage } = require('../tooling/package-import');
|
|
15
|
+
const { prepareHarnessRuntime } = require('../tooling/runtime-home');
|
|
16
16
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
17
17
|
|
|
18
18
|
function splitModel(model, auth) {
|
|
@@ -102,22 +102,28 @@ function resolveBinary() {
|
|
|
102
102
|
path.join(home, '.npm-global/bin/opencode'),
|
|
103
103
|
'/opt/homebrew/bin/opencode',
|
|
104
104
|
'/usr/local/bin/opencode',
|
|
105
|
-
'opencode',
|
|
105
|
+
findOnPath('opencode'),
|
|
106
106
|
].filter(Boolean);
|
|
107
|
+
const failures = [];
|
|
107
108
|
for (const candidate of candidates) {
|
|
108
|
-
if (
|
|
109
|
+
if (!executableExists(candidate)) {
|
|
110
|
+
failures.push(`${candidate} (missing or not executable)`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
109
113
|
const res = spawnSync(candidate, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
|
|
110
114
|
if (res.status === 0) return candidate;
|
|
115
|
+
failures.push(`${candidate} (--version failed: ${res.error?.message || res.status || res.signal || 'unknown'})`);
|
|
111
116
|
}
|
|
112
|
-
|
|
117
|
+
throw new Error(`OpenCode CLI binary is not available. Checked: ${failures.join(', ') || 'no candidates'}. Reinstall Amalgm or run amalgm update so the bundled OpenCode native package is present.`);
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
async function startOpenCodeServer(contract) {
|
|
121
|
+
const runtime = prepareHarnessRuntime(contract);
|
|
116
122
|
const binary = resolveBinary();
|
|
117
123
|
const config = configFor(contract);
|
|
118
124
|
const port = await freePort();
|
|
119
125
|
const env = {
|
|
120
|
-
...
|
|
126
|
+
...runtime.env,
|
|
121
127
|
OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
|
|
122
128
|
};
|
|
123
129
|
const args = ['serve', '--pure', '--hostname=127.0.0.1', `--port=${port}`];
|
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
syncClaudeNativeConfig,
|
|
13
13
|
syncCodexNativeConfig,
|
|
14
14
|
} = require('../tooling/native-config');
|
|
15
|
+
const { prepareHarnessRuntime } = require('../tooling/runtime-home');
|
|
15
16
|
|
|
16
17
|
function withNativeHome(fn) {
|
|
17
18
|
const previousNativeHome = process.env.AMALGM_NATIVE_HOME;
|
|
@@ -66,6 +67,47 @@ test('codex native sync copies config without deleting existing runtime state',
|
|
|
66
67
|
});
|
|
67
68
|
});
|
|
68
69
|
|
|
70
|
+
test('prepared codex runtime uses managed home while importing native config', () => {
|
|
71
|
+
withNativeHome((home) => {
|
|
72
|
+
const source = path.join(home, '.codex');
|
|
73
|
+
fs.mkdirSync(source, { recursive: true });
|
|
74
|
+
fs.writeFileSync(path.join(source, 'config.toml'), 'model = "gpt-5.5"');
|
|
75
|
+
fs.writeFileSync(path.join(source, 'hooks.json'), '{"hooks":{}}');
|
|
76
|
+
|
|
77
|
+
const runtimeHome = path.join(home, 'managed', 'codex-home');
|
|
78
|
+
const prepared = prepareHarnessRuntime({
|
|
79
|
+
harness: 'codex',
|
|
80
|
+
authMethod: 'provider_auth',
|
|
81
|
+
auth: { runtimeHome },
|
|
82
|
+
}, { PATH: '/bin', HOME: home });
|
|
83
|
+
|
|
84
|
+
assert.equal(prepared.runtimeHome, runtimeHome);
|
|
85
|
+
assert.equal(prepared.env.HOME, runtimeHome);
|
|
86
|
+
assert.equal(prepared.env.CODEX_HOME, runtimeHome);
|
|
87
|
+
assert.equal(prepared.syncInfo.runtimeHome, runtimeHome);
|
|
88
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'config.toml')), true);
|
|
89
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'hooks.json')), true);
|
|
90
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, '.codex', 'config.toml')), true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('prepared opencode runtime always has an isolated managed home', () => {
|
|
95
|
+
withNativeHome((home) => {
|
|
96
|
+
const runtimeHome = path.join(home, 'managed', 'opencode-home');
|
|
97
|
+
const prepared = prepareHarnessRuntime({
|
|
98
|
+
harness: 'opencode',
|
|
99
|
+
authMethod: 'amalgm',
|
|
100
|
+
auth: { runtimeHome, tokenRef: 'test-token', baseUrl: 'https://example.test' },
|
|
101
|
+
}, { PATH: '/bin', HOME: home });
|
|
102
|
+
|
|
103
|
+
assert.equal(prepared.runtimeHome, runtimeHome);
|
|
104
|
+
assert.equal(prepared.env.HOME, runtimeHome);
|
|
105
|
+
assert.equal(prepared.env.OPENCODE_HOME, runtimeHome);
|
|
106
|
+
assert.equal(prepared.env.OPENCODE_CONFIG_DIR, runtimeHome);
|
|
107
|
+
assert.equal(fs.existsSync(runtimeHome), true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
69
111
|
test('codex provider config keeps native mcp config and overrides managed model provider', () => {
|
|
70
112
|
withNativeHome((home) => {
|
|
71
113
|
const source = path.join(home, '.codex');
|
|
@@ -161,9 +203,11 @@ test('claude extracts native hooks without enabling full filesystem settings', (
|
|
|
161
203
|
assert.equal(hookSettings.permissions, undefined);
|
|
162
204
|
|
|
163
205
|
const adapter = new ClaudeAdapter();
|
|
206
|
+
const runtimeHome = path.join(home, 'runtime-home');
|
|
164
207
|
const options = adapter.options({
|
|
208
|
+
harness: 'claude_code',
|
|
165
209
|
authMethod: 'provider_auth',
|
|
166
|
-
auth: { runtimeHome
|
|
210
|
+
auth: { runtimeHome },
|
|
167
211
|
cwd: home,
|
|
168
212
|
cliModel: 'anthropic/claude-opus-4.7',
|
|
169
213
|
mcpServers: [],
|
|
@@ -10,21 +10,43 @@ function amalgmDir() {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function nativeNodeModulesDirs() {
|
|
13
|
-
|
|
13
|
+
const resourcesPath = process.resourcesPath || '';
|
|
14
|
+
return unique([
|
|
14
15
|
process.env.AMALGM_NATIVE_NODE_MODULES,
|
|
15
16
|
path.join(amalgmDir(), 'native', 'node_modules'),
|
|
16
|
-
|
|
17
|
+
resourcesPath ? path.join(resourcesPath, 'app.asar.unpacked', 'node_modules') : '',
|
|
18
|
+
resourcesPath ? path.join(resourcesPath, 'app.asar', 'node_modules') : '',
|
|
19
|
+
resourcesPath ? path.join(resourcesPath, 'cli', 'node_modules') : '',
|
|
20
|
+
resourcesPath ? path.join(resourcesPath, 'runtime', 'node_modules') : '',
|
|
21
|
+
].filter(Boolean));
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
function
|
|
24
|
+
function asarUnpackedPath(file) {
|
|
25
|
+
const marker = `${path.sep}app.asar${path.sep}`;
|
|
26
|
+
const value = String(file || '');
|
|
27
|
+
if (!value.includes(marker)) return value;
|
|
28
|
+
return value.replace(marker, `${path.sep}app.asar.unpacked${path.sep}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function existingAsarUnpackedPath(file) {
|
|
32
|
+
const unpacked = asarUnpackedPath(file);
|
|
33
|
+
return unpacked !== file && fs.existsSync(unpacked) ? unpacked : file;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function executablePath(file) {
|
|
37
|
+
const candidate = existingAsarUnpackedPath(file);
|
|
20
38
|
try {
|
|
21
|
-
fs.accessSync(
|
|
22
|
-
return
|
|
39
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
40
|
+
return candidate;
|
|
23
41
|
} catch {
|
|
24
|
-
return
|
|
42
|
+
return null;
|
|
25
43
|
}
|
|
26
44
|
}
|
|
27
45
|
|
|
46
|
+
function executableExists(file) {
|
|
47
|
+
return Boolean(executablePath(file));
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
function findPackageJson(packageName) {
|
|
29
51
|
const candidateDirs = [
|
|
30
52
|
...nativeNodeModulesDirs(),
|
|
@@ -39,9 +61,9 @@ function findPackageJson(packageName) {
|
|
|
39
61
|
|
|
40
62
|
function packageRoot(packageName) {
|
|
41
63
|
const manifest = findPackageJson(packageName);
|
|
42
|
-
if (manifest) return path.dirname(manifest);
|
|
64
|
+
if (manifest) return path.dirname(existingAsarUnpackedPath(manifest));
|
|
43
65
|
try {
|
|
44
|
-
return path.dirname(require.resolve(`${packageName}/package.json`));
|
|
66
|
+
return path.dirname(existingAsarUnpackedPath(require.resolve(`${packageName}/package.json`)));
|
|
45
67
|
} catch {
|
|
46
68
|
return null;
|
|
47
69
|
}
|
|
@@ -77,7 +99,7 @@ function packageBin(packageName, binName) {
|
|
|
77
99
|
: manifest.bin?.[binName];
|
|
78
100
|
if (!bin) return null;
|
|
79
101
|
const candidate = path.join(root, bin);
|
|
80
|
-
return
|
|
102
|
+
return executablePath(candidate);
|
|
81
103
|
} catch {
|
|
82
104
|
return null;
|
|
83
105
|
}
|
|
@@ -160,7 +182,7 @@ function bundledClaudeBinary() {
|
|
|
160
182
|
const root = packageRoot(packageName);
|
|
161
183
|
if (!root) return null;
|
|
162
184
|
const candidate = path.join(root, process.platform === 'win32' ? 'claude.exe' : 'claude');
|
|
163
|
-
return
|
|
185
|
+
return executablePath(candidate);
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
function resolveClaudeBinary() {
|
|
@@ -224,7 +246,8 @@ function bundledCodexBinary() {
|
|
|
224
246
|
const vendorRoot = codexVendorRoot();
|
|
225
247
|
if (target && vendorRoot) {
|
|
226
248
|
const candidate = path.join(vendorRoot, target.triple, 'codex', target.binaryName);
|
|
227
|
-
|
|
249
|
+
const executable = executablePath(candidate);
|
|
250
|
+
if (executable) return executable;
|
|
228
251
|
}
|
|
229
252
|
return packageBin('@openai/codex', 'codex');
|
|
230
253
|
}
|
|
@@ -605,4 +628,5 @@ module.exports = {
|
|
|
605
628
|
ensureAgentCommandShims,
|
|
606
629
|
ensureNativeBinaries,
|
|
607
630
|
executableExists,
|
|
631
|
+
findOnPath,
|
|
608
632
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { runtimeEnv } = require('../auth');
|
|
5
|
+
const { syncNativeHarnessConfig } = require('./native-config');
|
|
6
|
+
|
|
7
|
+
function managedRuntimeHome(contract) {
|
|
8
|
+
const runtimeHome = contract?.auth?.runtimeHome;
|
|
9
|
+
if (typeof runtimeHome !== 'string' || !runtimeHome.trim()) {
|
|
10
|
+
throw new Error(`${contract?.harness || 'Agent'} is missing a managed CLI home`);
|
|
11
|
+
}
|
|
12
|
+
return runtimeHome;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function prepareHarnessRuntime(contract, baseEnv = process.env) {
|
|
16
|
+
const runtimeHome = managedRuntimeHome(contract);
|
|
17
|
+
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
18
|
+
const syncInfo = syncNativeHarnessConfig(contract);
|
|
19
|
+
return {
|
|
20
|
+
runtimeHome,
|
|
21
|
+
syncInfo,
|
|
22
|
+
env: runtimeEnv(contract, baseEnv),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
managedRuntimeHome,
|
|
28
|
+
prepareHarnessRuntime,
|
|
29
|
+
};
|
|
@@ -205,12 +205,12 @@ Types:
|
|
|
205
205
|
|
|
206
206
|
When you reference a file, folder, skill, or agent that exists on the user's local volume, emit it as an `amalgm://` markdown link. The frontend renders these as inline citation chips by default, with a hover card showing the path and (for skills) description. Skills are files too: if you only have the `SKILL.md` path, a normal file citation is valid. This is the same format the user's `@`-menu produces, and it's how agents talk to each other about local resources.
|
|
207
207
|
|
|
208
|
-
Render format: `[label](amalgm://KIND/
|
|
208
|
+
Render format: `[label](amalgm://KIND/ENCODED_PATH_OR_ID)`
|
|
209
209
|
|
|
210
210
|
Kinds:
|
|
211
|
-
- File: `[Component.tsx](amalgm://file
|
|
212
|
-
- Folder: `[components](amalgm://folder
|
|
213
|
-
- Skill: `[email-formatter](amalgm://skill
|
|
211
|
+
- File: `[Component.tsx](amalgm://file/%2Fworkspace%2Fapp%2FComponent.tsx)`
|
|
212
|
+
- Folder: `[components](amalgm://folder/%2Fworkspace%2Fapp%2Fcomponents)`
|
|
213
|
+
- Skill: `[email-formatter](amalgm://skill/%2Fworkspace%2F.agents%2Fskills%2Femail-formatter "Formats outgoing emails with consistent voice")`
|
|
214
214
|
- Agent: `[Helper](amalgm://agent/AGENT_ID)`
|
|
215
215
|
|
|
216
216
|
For skills, append a one-line description as the markdown link title (the part in quotes after the URL). The description teaches downstream agents and the user what the skill does so they can decide whether to load it. Keep the description under ~140 chars.
|
|
@@ -218,11 +218,12 @@ For skills, append a one-line description as the markdown link title (the part i
|
|
|
218
218
|
For files and folders, the path alone is enough — no description needed. The user (and any agent reading your output) sees a chip; clicking it opens the file or folder.
|
|
219
219
|
|
|
220
220
|
IMPORTANT:
|
|
221
|
-
- Use the absolute path the user gave you
|
|
221
|
+
- Use the absolute path the user gave you as the decoded path, but percent-encode it inside the URL. Spaces must be `%20`, slashes in paths should be `%2F`, and literal `)` characters must be encoded. Do not use raw spaces inside markdown link destinations.
|
|
222
|
+
- Don't rewrite, normalize, or shorten the decoded path.
|
|
222
223
|
- Don't wrap citations in backticks. Don't explain the syntax.
|
|
223
224
|
- When the user `@`-mentions something in their prompt, that input arrives as the same `amalgm://` link format — you can echo it back unchanged when referring to that resource.
|
|
224
225
|
|
|
225
|
-
Example response (what you output): "I updated the [chat-server](amalgm://folder
|
|
226
|
+
Example response (what you output): "I updated the [chat-server](amalgm://folder/%2Fworkspace%2Fscripts%2Fchat-server) entrypoint and pulled in the [retry-policy](amalgm://skill/%2Fworkspace%2F.agents%2Fskills%2Fretry-policy "Adds bounded exponential backoff to flaky outbound calls") skill for the new HTTP client."
|
|
226
227
|
|
|
227
228
|
The user sees two clickable chips inline; the agent reading your transcript sees structured references it can act on.
|
|
228
229
|
</amalgm-platform>
|