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.
@@ -54,6 +54,7 @@ const MCP_PREFIXES = [
54
54
  '/agents',
55
55
  '/agent-config',
56
56
  '/agent-bundles',
57
+ '/skills',
57
58
  '/user-preferences',
58
59
  '/browser',
59
60
  ];
@@ -56,6 +56,7 @@ const MCP_PREFIXES = [
56
56
  '/agents',
57
57
  '/agent-config',
58
58
  '/agent-bundles',
59
+ '/skills',
59
60
  '/user-preferences',
60
61
  '/browser',
61
62
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.130",
3
+ "version": "0.1.131",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -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
- return unpacked && fs.existsSync(unpacked) ? unpacked : null;
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 = cleanString(value);
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 = cleanString(installUrl);
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 { buildCanonicalSkillRoots, classifySkillRoot, normalizePath } = require('./roots');
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 key = cleanString(skill?.source?.path);
135
- if (!key || byPath.has(key)) continue;
136
- byPath.set(key, skill);
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 native = path.join(root, 'app.asar.unpacked', 'node_modules', 'agent-browser', 'bin', `agent-browser-${os.platform()}-${os.arch()}`);
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, 2);
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',
@@ -163,6 +163,7 @@ const MCP_PREFIXES = [
163
163
  '/agents',
164
164
  '/agent-config',
165
165
  '/agent-bundles',
166
+ '/skills',
166
167
  '/user-preferences',
167
168
  '/browser',
168
169
  ];