amalgm 0.1.130 → 0.1.131
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/tunnel-chat.js +1 -0
- package/lib/tunnel-events.js +1 -0
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/browser/cli.js +5 -2
- package/runtime/scripts/amalgm-mcp/skills/public.js +16 -2
- package/runtime/scripts/amalgm-mcp/skills/roots.js +28 -0
- package/runtime/scripts/amalgm-mcp/skills/scanner.js +32 -4
- package/runtime/scripts/amalgm-mcp/tests/browser-cli.test.js +37 -1
- package/runtime/scripts/amalgm-mcp/tests/skills.test.js +57 -1
- package/runtime/scripts/local-gateway.js +1 -0
package/lib/tunnel-chat.js
CHANGED
package/lib/tunnel-events.js
CHANGED
package/package.json
CHANGED
|
@@ -92,9 +92,12 @@ function nativeAgentBrowser(wrapperPath) {
|
|
|
92
92
|
&& (fs.existsSync('/lib/ld-musl-x86_64.so.1') || fs.existsSync('/lib/ld-musl-aarch64.so.1'));
|
|
93
93
|
const name = `agent-browser-${platform}${musl ? '-musl' : ''}-${arch}${platform === 'win32' ? '.exe' : ''}`;
|
|
94
94
|
const candidate = path.join(path.dirname(wrapperPath), name);
|
|
95
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
96
95
|
const unpacked = asarUnpackedPath(candidate);
|
|
97
|
-
|
|
96
|
+
// Electron's asar-aware fs can report this path as "existing" even though
|
|
97
|
+
// the OS cannot spawn a binary through app.asar. Prefer the real unpacked
|
|
98
|
+
// path whenever it is available.
|
|
99
|
+
if (unpacked && fs.existsSync(unpacked)) return unpacked;
|
|
100
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
function asarUnpackedPath(candidate) {
|
|
@@ -11,19 +11,32 @@ function cleanString(value) {
|
|
|
11
11
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function normalizeInstallSource(value) {
|
|
15
|
+
const source = cleanString(value);
|
|
16
|
+
if (!source) return '';
|
|
17
|
+
if (/^https?:\/\/[^\s]+$/i.test(source)) return source;
|
|
18
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(source)) return source;
|
|
19
|
+
|
|
20
|
+
const host = source.split('/')[0];
|
|
21
|
+
if (/^[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?::\d+)?$/i.test(host)) {
|
|
22
|
+
return `https://${source}`;
|
|
23
|
+
}
|
|
24
|
+
return source;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
function npmCommand() {
|
|
15
28
|
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
function isSupportedInstallSource(value) {
|
|
19
|
-
const source =
|
|
32
|
+
const source = normalizeInstallSource(value);
|
|
20
33
|
if (!source) return false;
|
|
21
34
|
if (/^https?:\/\/[^\s]+$/i.test(source)) return true;
|
|
22
35
|
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(source);
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
function buildInstallSkillArgs({ installUrl, slug }) {
|
|
26
|
-
const source =
|
|
39
|
+
const source = normalizeInstallSource(installUrl);
|
|
27
40
|
const skillSlug = cleanString(slug);
|
|
28
41
|
if (!isSupportedInstallSource(source)) {
|
|
29
42
|
throw new Error('installUrl must be an https URL or owner/repo reference');
|
|
@@ -111,4 +124,5 @@ module.exports = {
|
|
|
111
124
|
buildInstallSkillArgs,
|
|
112
125
|
findInstalledSkillBySlug,
|
|
113
126
|
installPublicSkill,
|
|
127
|
+
normalizeInstallSource,
|
|
114
128
|
};
|
|
@@ -61,6 +61,31 @@ function walkProjectSkillRoots(selectedCwd) {
|
|
|
61
61
|
return roots;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function walkCodexWorktreeSkillRoots(home) {
|
|
65
|
+
const worktreesRoot = existingDirectory(path.join(home, '.codex', 'worktrees'));
|
|
66
|
+
if (!worktreesRoot) return [];
|
|
67
|
+
|
|
68
|
+
let entries = [];
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(worktreesRoot, { withFileTypes: true });
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return entries
|
|
76
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
77
|
+
.map((entry) => existingDirectory(path.join(worktreesRoot, entry.name, '.agents', 'skills')))
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isCodexWorktreeRoot(rootPath, options = {}) {
|
|
82
|
+
const root = normalizePath(rootPath);
|
|
83
|
+
const home = normalizePath(options.homeDir) || homeDir();
|
|
84
|
+
if (!root || !home) return false;
|
|
85
|
+
const worktreesRoot = `${normalizePath(path.join(home, '.codex', 'worktrees'))}${path.sep}`;
|
|
86
|
+
return root.startsWith(worktreesRoot) && root.endsWith(path.normalize(path.join('.agents', 'skills')));
|
|
87
|
+
}
|
|
88
|
+
|
|
64
89
|
function homeDir() {
|
|
65
90
|
return normalizePath(process.env.AMALGM_NATIVE_HOME || os.homedir());
|
|
66
91
|
}
|
|
@@ -75,6 +100,7 @@ function buildCanonicalSkillRoots(options = {}) {
|
|
|
75
100
|
home ? path.join(home, '.agents', 'skills') : '',
|
|
76
101
|
codeXHome ? path.join(codeXHome, 'skills') : '',
|
|
77
102
|
home ? path.join(home, '.codex', 'skills') : '',
|
|
103
|
+
...walkCodexWorktreeSkillRoots(home),
|
|
78
104
|
claudeConfigDir ? path.join(claudeConfigDir, 'skills') : '',
|
|
79
105
|
home ? path.join(home, '.claude', 'skills') : '',
|
|
80
106
|
home ? path.join(home, '.config', 'claude', 'skills') : '',
|
|
@@ -127,6 +153,8 @@ module.exports = {
|
|
|
127
153
|
homeDir,
|
|
128
154
|
normalizePath,
|
|
129
155
|
prefsSelectedCwd,
|
|
156
|
+
isCodexWorktreeRoot,
|
|
130
157
|
uniquePaths,
|
|
158
|
+
walkCodexWorktreeSkillRoots,
|
|
131
159
|
walkProjectSkillRoots,
|
|
132
160
|
};
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
buildCanonicalSkillRoots,
|
|
8
|
+
classifySkillRoot,
|
|
9
|
+
isCodexWorktreeRoot,
|
|
10
|
+
normalizePath,
|
|
11
|
+
} = require('./roots');
|
|
7
12
|
|
|
8
13
|
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---/;
|
|
9
14
|
|
|
@@ -126,14 +131,37 @@ function scanSkillRoot(rootPath, options = {}) {
|
|
|
126
131
|
.filter(Boolean);
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
function skillDuplicateKey(skill) {
|
|
135
|
+
const name = cleanString(skill?.name).toLowerCase();
|
|
136
|
+
const contentHash = cleanString(skill?.contentHash);
|
|
137
|
+
if (!name || !contentHash) return '';
|
|
138
|
+
return `${name}::${contentHash}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
function scanInstalledSkills(options = {}) {
|
|
130
142
|
const roots = buildCanonicalSkillRoots(options);
|
|
131
143
|
const byPath = new Map();
|
|
144
|
+
const byResolvedPath = new Map();
|
|
145
|
+
const bySignature = new Map();
|
|
132
146
|
for (const rootPath of roots) {
|
|
133
147
|
for (const skill of scanSkillRoot(rootPath, options)) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
const pathKey = cleanString(skill?.source?.path);
|
|
149
|
+
const resolvedKey = cleanString(skill?.source?.resolvedPath);
|
|
150
|
+
const signature = skillDuplicateKey(skill);
|
|
151
|
+
|
|
152
|
+
if (!pathKey || byPath.has(pathKey)) continue;
|
|
153
|
+
if (resolvedKey && byResolvedPath.has(resolvedKey)) continue;
|
|
154
|
+
if (
|
|
155
|
+
signature
|
|
156
|
+
&& isCodexWorktreeRoot(skill?.source?.rootPath, options)
|
|
157
|
+
&& bySignature.has(signature)
|
|
158
|
+
) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
byPath.set(pathKey, skill);
|
|
163
|
+
if (resolvedKey) byResolvedPath.set(resolvedKey, skill);
|
|
164
|
+
if (signature) bySignature.set(signature, skill);
|
|
137
165
|
}
|
|
138
166
|
}
|
|
139
167
|
return {
|
|
@@ -8,6 +8,17 @@ const path = require('node:path');
|
|
|
8
8
|
|
|
9
9
|
const cli = require('../browser/cli');
|
|
10
10
|
|
|
11
|
+
function nativeBinaryName() {
|
|
12
|
+
const platforms = { darwin: 'darwin', linux: 'linux', win32: 'win32' };
|
|
13
|
+
const arches = { x64: 'x64', arm64: 'arm64' };
|
|
14
|
+
const platform = platforms[os.platform()];
|
|
15
|
+
const arch = arches[os.arch()];
|
|
16
|
+
if (!platform || !arch) return null;
|
|
17
|
+
const musl = platform === 'linux'
|
|
18
|
+
&& (fs.existsSync('/lib/ld-musl-x86_64.so.1') || fs.existsSync('/lib/ld-musl-aarch64.so.1'));
|
|
19
|
+
return `agent-browser-${platform}${musl ? '-musl' : ''}-${arch}${platform === 'win32' ? '.exe' : ''}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
function withoutBinOverride(fn) {
|
|
12
23
|
const saved = process.env.AMALGM_AGENT_BROWSER_BIN;
|
|
13
24
|
delete process.env.AMALGM_AGENT_BROWSER_BIN;
|
|
@@ -67,7 +78,9 @@ test('agent-browser spawns the native binary directly when it exists', () => {
|
|
|
67
78
|
test('agent-browser native path resolves from packaged app.asar to app.asar.unpacked', () => {
|
|
68
79
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-asar-browser-test-'));
|
|
69
80
|
const wrapper = path.join(root, 'app.asar', 'node_modules', 'agent-browser', 'bin', 'agent-browser.js');
|
|
70
|
-
const
|
|
81
|
+
const binaryName = nativeBinaryName();
|
|
82
|
+
if (!binaryName) return;
|
|
83
|
+
const native = path.join(root, 'app.asar.unpacked', 'node_modules', 'agent-browser', 'bin', binaryName);
|
|
71
84
|
fs.mkdirSync(path.dirname(native), { recursive: true });
|
|
72
85
|
fs.writeFileSync(native, '');
|
|
73
86
|
try {
|
|
@@ -78,6 +91,29 @@ test('agent-browser native path resolves from packaged app.asar to app.asar.unpa
|
|
|
78
91
|
}
|
|
79
92
|
});
|
|
80
93
|
|
|
94
|
+
test('agent-browser prefers app.asar.unpacked even when the asar candidate appears to exist', () => {
|
|
95
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-asar-shim-test-'));
|
|
96
|
+
const wrapper = path.join(root, 'app.asar', 'node_modules', 'agent-browser', 'bin', 'agent-browser.js');
|
|
97
|
+
const binaryName = nativeBinaryName();
|
|
98
|
+
if (!binaryName) return;
|
|
99
|
+
const candidate = path.join(root, 'app.asar', 'node_modules', 'agent-browser', 'bin', binaryName);
|
|
100
|
+
const unpacked = path.join(root, 'app.asar.unpacked', 'node_modules', 'agent-browser', 'bin', binaryName);
|
|
101
|
+
fs.mkdirSync(path.dirname(unpacked), { recursive: true });
|
|
102
|
+
fs.writeFileSync(unpacked, '');
|
|
103
|
+
const originalExistsSync = fs.existsSync;
|
|
104
|
+
fs.existsSync = (value) => {
|
|
105
|
+
const target = String(value);
|
|
106
|
+
if (target === candidate || target === unpacked) return true;
|
|
107
|
+
return originalExistsSync(value);
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
assert.equal(cli.nativeAgentBrowser(wrapper), unpacked);
|
|
111
|
+
} finally {
|
|
112
|
+
fs.existsSync = originalExistsSync;
|
|
113
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
81
117
|
test('CLI browser sessions stay headless unless headed mode is explicit', async () => {
|
|
82
118
|
await withBrowserEnv({}, () => {
|
|
83
119
|
assert.equal(cli.wantsHeaded(), false);
|
|
@@ -38,6 +38,27 @@ test('scanInstalledSkills discovers canonical project/global roots and symlink m
|
|
|
38
38
|
const globalSkillDir = path.join(home, '.codex', 'skills', 'global-review');
|
|
39
39
|
writeSkill(globalSkillDir, 'Global skill');
|
|
40
40
|
|
|
41
|
+
const worktreeSkillDir = path.join(
|
|
42
|
+
home,
|
|
43
|
+
'.codex',
|
|
44
|
+
'worktrees',
|
|
45
|
+
'sandbox-check',
|
|
46
|
+
'.agents',
|
|
47
|
+
'skills',
|
|
48
|
+
'sandbox-agent',
|
|
49
|
+
);
|
|
50
|
+
writeSkill(worktreeSkillDir, 'Worktree skill');
|
|
51
|
+
const duplicateWorktreeSkillDir = path.join(
|
|
52
|
+
home,
|
|
53
|
+
'.codex',
|
|
54
|
+
'worktrees',
|
|
55
|
+
'sandbox-duplicate',
|
|
56
|
+
'.agents',
|
|
57
|
+
'skills',
|
|
58
|
+
'sandbox-agent',
|
|
59
|
+
);
|
|
60
|
+
writeSkill(duplicateWorktreeSkillDir, 'Worktree skill');
|
|
61
|
+
|
|
41
62
|
const realProjectSkillDir = path.join(sourceRoot, 'project-portability');
|
|
42
63
|
writeSkill(realProjectSkillDir, 'Project skill');
|
|
43
64
|
const projectSkillRoot = path.join(project, '.agents', 'skills');
|
|
@@ -50,8 +71,16 @@ test('scanInstalledSkills discovers canonical project/global roots and symlink m
|
|
|
50
71
|
});
|
|
51
72
|
|
|
52
73
|
assert.equal(result.roots.includes(path.join(project, '.agents', 'skills')), true);
|
|
74
|
+
assert.equal(
|
|
75
|
+
result.roots.includes(path.join(home, '.codex', 'worktrees', 'sandbox-check', '.agents', 'skills')),
|
|
76
|
+
true,
|
|
77
|
+
);
|
|
78
|
+
assert.equal(
|
|
79
|
+
result.roots.includes(path.join(home, '.codex', 'worktrees', 'sandbox-duplicate', '.agents', 'skills')),
|
|
80
|
+
true,
|
|
81
|
+
);
|
|
53
82
|
assert.equal(result.roots.includes(path.join(home, '.codex', 'skills')), true);
|
|
54
|
-
assert.equal(result.skills.length,
|
|
83
|
+
assert.equal(result.skills.length, 3);
|
|
55
84
|
|
|
56
85
|
const projectSkill = result.skills.find((skill) => skill.name === 'project-portability');
|
|
57
86
|
assert.equal(projectSkill?.source.scope, 'project');
|
|
@@ -61,6 +90,9 @@ test('scanInstalledSkills discovers canonical project/global roots and symlink m
|
|
|
61
90
|
const globalSkill = result.skills.find((skill) => skill.name === 'global-review');
|
|
62
91
|
assert.equal(globalSkill?.source.provider, 'codex');
|
|
63
92
|
assert.equal(globalSkill?.source.scope, 'global');
|
|
93
|
+
|
|
94
|
+
const worktreeSkill = result.skills.find((skill) => skill.name === 'sandbox-agent');
|
|
95
|
+
assert.equal(worktreeSkill?.source.scope, 'project');
|
|
64
96
|
} finally {
|
|
65
97
|
fs.rmSync(home, { recursive: true, force: true });
|
|
66
98
|
fs.rmSync(project, { recursive: true, force: true });
|
|
@@ -169,6 +201,30 @@ test('buildInstallSkillArgs targets the global Codex skills install flow', () =>
|
|
|
169
201
|
);
|
|
170
202
|
});
|
|
171
203
|
|
|
204
|
+
test('buildInstallSkillArgs normalizes bare well-known sources to https URLs', () => {
|
|
205
|
+
assert.deepEqual(
|
|
206
|
+
buildInstallSkillArgs({
|
|
207
|
+
installUrl: 'open.feishu.cn',
|
|
208
|
+
slug: 'lark-approval',
|
|
209
|
+
}),
|
|
210
|
+
[
|
|
211
|
+
'exec',
|
|
212
|
+
'--yes',
|
|
213
|
+
'--package=skills',
|
|
214
|
+
'--',
|
|
215
|
+
'skills',
|
|
216
|
+
'add',
|
|
217
|
+
'https://open.feishu.cn',
|
|
218
|
+
'--skill',
|
|
219
|
+
'lark-approval',
|
|
220
|
+
'--agent',
|
|
221
|
+
'codex',
|
|
222
|
+
'--global',
|
|
223
|
+
'--yes',
|
|
224
|
+
],
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
172
228
|
test('findInstalledSkillBySlug matches installed public skills by name or directory', () => {
|
|
173
229
|
const installedSkill = {
|
|
174
230
|
id: 'skill:find-skills',
|