amalgm 0.1.128 → 0.1.129
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/lib/updater.js +16 -5
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/agent-bundles/bundles.js +0 -2
- package/runtime/scripts/amalgm-mcp/browser/cli.js +15 -1
- package/runtime/scripts/amalgm-mcp/events/desktop-release-runner.js +39 -9
- package/runtime/scripts/amalgm-mcp/tests/agent-bundles.test.js +32 -4
- package/runtime/scripts/amalgm-mcp/tests/browser-cli.test.js +14 -0
- package/runtime/scripts/amalgm-mcp/tests/desktop-release-runner.test.js +31 -0
package/lib/updater.js
CHANGED
|
@@ -84,13 +84,19 @@ function npmEnv(env = process.env) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function npmSpawnOptions(options = {}) {
|
|
88
|
+
return {
|
|
89
|
+
env: npmEnv(options.env),
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
92
|
+
// Node rejects direct .cmd spawning on Windows, so npm.cmd must run via cmd.exe there.
|
|
93
|
+
shell: process.platform === 'win32',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
function runNpm(args, options = {}) {
|
|
88
98
|
return new Promise((resolve, reject) => {
|
|
89
|
-
const child = spawn(npmCommand(), args,
|
|
90
|
-
env: npmEnv(options.env),
|
|
91
|
-
windowsHide: true,
|
|
92
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
93
|
-
});
|
|
99
|
+
const child = spawn(npmCommand(), args, npmSpawnOptions(options));
|
|
94
100
|
let stdout = '';
|
|
95
101
|
let stderr = '';
|
|
96
102
|
let timedOut = false;
|
|
@@ -202,6 +208,11 @@ function shouldAutoUpdate(options = {}) {
|
|
|
202
208
|
}
|
|
203
209
|
|
|
204
210
|
module.exports = {
|
|
211
|
+
_test: {
|
|
212
|
+
npmCommand,
|
|
213
|
+
npmEnv,
|
|
214
|
+
npmSpawnOptions,
|
|
215
|
+
},
|
|
205
216
|
checkForUpdate,
|
|
206
217
|
compareVersions,
|
|
207
218
|
envDisablesAutoUpdate,
|
package/package.json
CHANGED
|
@@ -383,14 +383,12 @@ function collectToolRecords(toolIds, requires) {
|
|
|
383
383
|
|
|
384
384
|
if (tool.origin === 'system') {
|
|
385
385
|
addUnique(requires.systemTools, tool.id);
|
|
386
|
-
continue;
|
|
387
386
|
}
|
|
388
387
|
|
|
389
388
|
if (!tools.has(tool.id)) tools.set(tool.id, portableTool(tool, requires));
|
|
390
389
|
|
|
391
390
|
if (selectedAction) {
|
|
392
391
|
actions.set(selectedAction.id, portableAction(selectedAction, requires));
|
|
393
|
-
continue;
|
|
394
392
|
}
|
|
395
393
|
|
|
396
394
|
for (const action of Object.values(catalog.toolActions || {})) {
|
|
@@ -64,6 +64,8 @@ function ffmpegBinary() {
|
|
|
64
64
|
if (process.env.AMALGM_FFMPEG) return process.env.AMALGM_FFMPEG;
|
|
65
65
|
try {
|
|
66
66
|
const resolved = require('ffmpeg-static');
|
|
67
|
+
const unpacked = asarUnpackedPath(resolved);
|
|
68
|
+
if (unpacked && fs.existsSync(unpacked)) return unpacked;
|
|
67
69
|
if (resolved && fs.existsSync(resolved)) return resolved;
|
|
68
70
|
} catch {}
|
|
69
71
|
return 'ffmpeg';
|
|
@@ -90,7 +92,17 @@ function nativeAgentBrowser(wrapperPath) {
|
|
|
90
92
|
&& (fs.existsSync('/lib/ld-musl-x86_64.so.1') || fs.existsSync('/lib/ld-musl-aarch64.so.1'));
|
|
91
93
|
const name = `agent-browser-${platform}${musl ? '-musl' : ''}-${arch}${platform === 'win32' ? '.exe' : ''}`;
|
|
92
94
|
const candidate = path.join(path.dirname(wrapperPath), name);
|
|
93
|
-
|
|
95
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
96
|
+
const unpacked = asarUnpackedPath(candidate);
|
|
97
|
+
return unpacked && fs.existsSync(unpacked) ? unpacked : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function asarUnpackedPath(candidate) {
|
|
101
|
+
if (!candidate) return null;
|
|
102
|
+
const marker = `${path.sep}app.asar${path.sep}`;
|
|
103
|
+
const index = String(candidate).indexOf(marker);
|
|
104
|
+
if (index === -1) return null;
|
|
105
|
+
return `${String(candidate).slice(0, index)}${path.sep}app.asar.unpacked${path.sep}${String(candidate).slice(index + marker.length)}`;
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
function agentBrowserCommand() {
|
|
@@ -238,11 +250,13 @@ module.exports = {
|
|
|
238
250
|
DEFAULT_SESSION,
|
|
239
251
|
agentBrowserCommand,
|
|
240
252
|
amalgmDir,
|
|
253
|
+
asarUnpackedPath,
|
|
241
254
|
asText,
|
|
242
255
|
browserRoot,
|
|
243
256
|
ensureDir,
|
|
244
257
|
ffmpegBinary,
|
|
245
258
|
ffmpegDir,
|
|
259
|
+
nativeAgentBrowser,
|
|
246
260
|
profileDir,
|
|
247
261
|
readText,
|
|
248
262
|
runCli,
|
|
@@ -1069,6 +1069,27 @@ function uploadRequestWithRetry(params, attempts = Number(params.env?.AMALGM_DES
|
|
|
1069
1069
|
throw lastError;
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
|
+
function parseUploadOffsetMismatch(error) {
|
|
1073
|
+
const message = cleanString(error?.message || error);
|
|
1074
|
+
const match = /Offset mismatch: expected (\d+), received (\d+)/i.exec(message);
|
|
1075
|
+
if (!match) return null;
|
|
1076
|
+
return {
|
|
1077
|
+
expected: Number(match[1]),
|
|
1078
|
+
received: Number(match[2]),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function recoverableUploadOffset(error, offset, bytesRead, size) {
|
|
1083
|
+
const mismatch = parseUploadOffsetMismatch(error);
|
|
1084
|
+
if (!mismatch) return null;
|
|
1085
|
+
if (!Number.isFinite(mismatch.expected) || !Number.isFinite(mismatch.received)) return null;
|
|
1086
|
+
if (mismatch.received !== offset) return null;
|
|
1087
|
+
if (mismatch.expected <= offset) return null;
|
|
1088
|
+
const maxExpected = Math.min(size, offset + bytesRead);
|
|
1089
|
+
if (mismatch.expected > maxExpected) return null;
|
|
1090
|
+
return mismatch.expected;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1072
1093
|
function uploadHttpReleaseFile({ filePath, cwd, env, publishTarget }) {
|
|
1073
1094
|
const token = cleanString(env.AMALGM_DESKTOP_RELEASE_UPLOAD_TOKEN);
|
|
1074
1095
|
if (!token) throw new Error('Missing AMALGM_DESKTOP_RELEASE_UPLOAD_TOKEN for desktop release HTTP upload.');
|
|
@@ -1102,15 +1123,22 @@ function uploadHttpReleaseFile({ filePath, cwd, env, publishTarget }) {
|
|
|
1102
1123
|
fs.writeFileSync(chunkPath, buffer.subarray(0, bytesRead));
|
|
1103
1124
|
const chunkUrl = new URL(uploadUrl);
|
|
1104
1125
|
chunkUrl.searchParams.set('offset', String(offset));
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1126
|
+
try {
|
|
1127
|
+
uploadRequestWithRetry({
|
|
1128
|
+
method: 'PUT',
|
|
1129
|
+
url: chunkUrl.toString(),
|
|
1130
|
+
token,
|
|
1131
|
+
cwd,
|
|
1132
|
+
env,
|
|
1133
|
+
dataFile: chunkPath,
|
|
1134
|
+
});
|
|
1135
|
+
offset += bytesRead;
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
const resumedOffset = recoverableUploadOffset(error, offset, bytesRead, size);
|
|
1138
|
+
if (resumedOffset == null) throw error;
|
|
1139
|
+
console.warn(`desktop release upload resumed ${assetName} at server offset ${resumedOffset}.`);
|
|
1140
|
+
offset = resumedOffset;
|
|
1141
|
+
}
|
|
1114
1142
|
}
|
|
1115
1143
|
|
|
1116
1144
|
uploadRequestWithRetry({
|
|
@@ -1372,8 +1400,10 @@ module.exports = {
|
|
|
1372
1400
|
desktopReleaseArtifactPaths,
|
|
1373
1401
|
envForBuild,
|
|
1374
1402
|
parseEnvFile,
|
|
1403
|
+
parseUploadOffsetMismatch,
|
|
1375
1404
|
publishLegacyGithubBridgeRelease,
|
|
1376
1405
|
publishDesktopRelease,
|
|
1406
|
+
recoverableUploadOffset,
|
|
1377
1407
|
requestFromEnv,
|
|
1378
1408
|
shouldHandleRequest,
|
|
1379
1409
|
};
|
|
@@ -12,7 +12,7 @@ const { upsertAgentConfig, getAgentConfig } = require('../agent-config/store');
|
|
|
12
12
|
const { defaultToolboxService } = require('../toolbox/service');
|
|
13
13
|
const { createAgentBundle, installAgentBundle } = require('../agent-bundles/bundles');
|
|
14
14
|
|
|
15
|
-
test('agent bundle includes recursive subagents, selected user tools, and warnings', () => {
|
|
15
|
+
test('agent bundle includes recursive subagents, selected user tools, system tools, and warnings', () => {
|
|
16
16
|
defaultToolboxService.registerTool({
|
|
17
17
|
id: 'acme.cli',
|
|
18
18
|
name: 'Acme CLI',
|
|
@@ -81,8 +81,9 @@ test('agent bundle includes recursive subagents, selected user tools, and warnin
|
|
|
81
81
|
assert.equal(bundle.kind, 'amalgm.agent.bundle');
|
|
82
82
|
assert.equal(bundle.rootAgentId, root.id);
|
|
83
83
|
assert.deepEqual(bundle.agents.map((entry) => entry.sourceAgentId), [root.id, child.id]);
|
|
84
|
-
assert.deepEqual(bundle.tools.map((tool) => tool.id), ['acme.cli']);
|
|
85
|
-
assert.
|
|
84
|
+
assert.deepEqual(bundle.tools.map((tool) => tool.id), ['acme.cli', 'browser']);
|
|
85
|
+
assert.equal(bundle.toolActions.some((action) => action.id === 'acme.cli.run'), true);
|
|
86
|
+
assert.equal(bundle.toolActions.some((action) => action.toolId === 'browser'), true);
|
|
86
87
|
assert.deepEqual(bundle.headIds, ['agent:bundle-root']);
|
|
87
88
|
assert.equal(bundle.elements.some((element) => element.id === 'agent:bundle-root' && element.type === 'agent'), true);
|
|
88
89
|
assert.equal(bundle.elements.some((element) => element.id === 'agent:bundle-child' && element.type === 'agent'), true);
|
|
@@ -110,10 +111,37 @@ test('agent bundle includes recursive subagents, selected user tools, and warnin
|
|
|
110
111
|
assert.equal(bundle.requires.bindings.includes('tool:acme.cli:cwd'), true);
|
|
111
112
|
assert.equal(bundle.requires.bindings.includes('tool:acme.cli:source.command'), true);
|
|
112
113
|
assert.equal(preview.summary.agents, 2);
|
|
113
|
-
assert.equal(preview.summary.tools,
|
|
114
|
+
assert.equal(preview.summary.tools, 2);
|
|
115
|
+
assert.equal(preview.tools.some((tool) => tool.id === 'browser' && tool.actionCount > 0), true);
|
|
114
116
|
assert.equal(preview.warnings.some((warning) => warning.includes('free-text')), true);
|
|
115
117
|
});
|
|
116
118
|
|
|
119
|
+
test('agent bundle includes every tool action when the loadout references a single action id', () => {
|
|
120
|
+
const catalog = defaultToolboxService.readCatalog();
|
|
121
|
+
const browserActions = Object.values(catalog.toolActions || {}).filter((action) => action?.toolId === 'browser');
|
|
122
|
+
assert.ok(browserActions.length > 1);
|
|
123
|
+
|
|
124
|
+
const root = createAgent({
|
|
125
|
+
id: 'bundle-browser-action-root',
|
|
126
|
+
name: 'Bundle Browser Action Root',
|
|
127
|
+
baseHarnessId: 'codex',
|
|
128
|
+
baseModelId: 'openai/gpt-5.5',
|
|
129
|
+
systemPrompt: 'Browser action root',
|
|
130
|
+
loadout: { toolIds: [browserActions[0].id] },
|
|
131
|
+
authMethod: 'amalgm',
|
|
132
|
+
});
|
|
133
|
+
upsertAgentConfig(root.id, {
|
|
134
|
+
instructions: 'Browser action root',
|
|
135
|
+
loadout: { toolIds: [browserActions[0].id] },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const { bundle, preview } = createAgentBundle(root.id, { bundleId: 'agent-bundle-browser-action-test' });
|
|
139
|
+
|
|
140
|
+
assert.deepEqual(bundle.tools.map((tool) => tool.id), ['browser']);
|
|
141
|
+
assert.equal(bundle.toolActions.filter((action) => action.toolId === 'browser').length, browserActions.length);
|
|
142
|
+
assert.equal(preview.tools.some((tool) => tool.id === 'browser' && tool.actionCount === browserActions.length), true);
|
|
143
|
+
});
|
|
144
|
+
|
|
117
145
|
test('installing an agent bundle creates local copies and rewrites subagent refs', () => {
|
|
118
146
|
const source = createAgent({
|
|
119
147
|
id: 'install-source-root',
|
|
@@ -64,6 +64,20 @@ test('agent-browser spawns the native binary directly when it exists', () => {
|
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
test('agent-browser native path resolves from packaged app.asar to app.asar.unpacked', () => {
|
|
68
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-asar-browser-test-'));
|
|
69
|
+
const wrapper = path.join(root, 'app.asar', 'node_modules', 'agent-browser', 'bin', 'agent-browser.js');
|
|
70
|
+
const native = path.join(root, 'app.asar.unpacked', 'node_modules', 'agent-browser', 'bin', `agent-browser-${os.platform()}-${os.arch()}`);
|
|
71
|
+
fs.mkdirSync(path.dirname(native), { recursive: true });
|
|
72
|
+
fs.writeFileSync(native, '');
|
|
73
|
+
try {
|
|
74
|
+
assert.equal(cli.asarUnpackedPath(wrapper), path.join(root, 'app.asar.unpacked', 'node_modules', 'agent-browser', 'bin', 'agent-browser.js'));
|
|
75
|
+
assert.equal(cli.nativeAgentBrowser(wrapper), native);
|
|
76
|
+
} finally {
|
|
77
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
67
81
|
test('CLI browser sessions stay headless unless headed mode is explicit', async () => {
|
|
68
82
|
await withBrowserEnv({}, () => {
|
|
69
83
|
assert.equal(cli.wantsHeaded(), false);
|
|
@@ -22,7 +22,9 @@ const {
|
|
|
22
22
|
desktopVersion,
|
|
23
23
|
envForBuild,
|
|
24
24
|
parseEnvFile,
|
|
25
|
+
parseUploadOffsetMismatch,
|
|
25
26
|
publishDesktopRelease,
|
|
27
|
+
recoverableUploadOffset,
|
|
26
28
|
requestFromEnv,
|
|
27
29
|
shouldHandleRequest,
|
|
28
30
|
} = require('../events/desktop-release-runner');
|
|
@@ -101,6 +103,35 @@ test('desktop versions stay compatible with existing release ordering', () => {
|
|
|
101
103
|
assert.equal(desktopVersion('0.1.124-preview.1', 'preview', 45), '0.1.124000045-preview.1');
|
|
102
104
|
});
|
|
103
105
|
|
|
106
|
+
test('desktop release upload parser extracts expected and received offsets', () => {
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
parseUploadOffsetMismatch(new Error('Desktop release upload PUT returned HTTP 409: {"ok":false,"error":"Offset mismatch: expected 352452608, received 352321536"}')),
|
|
109
|
+
{ expected: 352452608, received: 352321536 },
|
|
110
|
+
);
|
|
111
|
+
assert.equal(parseUploadOffsetMismatch(new Error('something else')), null);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('desktop release upload can resume from recoverable offset mismatches', () => {
|
|
115
|
+
const error = new Error('Desktop release upload PUT returned HTTP 409: {"ok":false,"error":"Offset mismatch: expected 352452608, received 352321536"}');
|
|
116
|
+
assert.equal(
|
|
117
|
+
recoverableUploadOffset(error, 352321536, 2097152, 369814043),
|
|
118
|
+
352452608,
|
|
119
|
+
);
|
|
120
|
+
assert.equal(
|
|
121
|
+
recoverableUploadOffset(error, 352190464, 2097152, 369814043),
|
|
122
|
+
null,
|
|
123
|
+
);
|
|
124
|
+
assert.equal(
|
|
125
|
+
recoverableUploadOffset(
|
|
126
|
+
new Error('Desktop release upload PUT returned HTTP 409: {"ok":false,"error":"Offset mismatch: expected 354500000, received 352321536"}'),
|
|
127
|
+
352321536,
|
|
128
|
+
2097152,
|
|
129
|
+
369814043,
|
|
130
|
+
),
|
|
131
|
+
null,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
104
135
|
test('desktop release runner loads env-file fallbacks without overriding explicit env', () => {
|
|
105
136
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-desktop-release-env-test-'));
|
|
106
137
|
const envFile = path.join(tempRoot, 'desktop-release.env');
|