amalgm 0.1.130 → 0.1.131-canary.20260625234755
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/cli.js +4 -1
- package/lib/paths.js +2 -1
- package/lib/tunnel-chat.js +7 -2
- 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/config.js +15 -3
- package/runtime/scripts/amalgm-mcp/index.js +1 -1
- 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/chat-core/contract.js +2 -2
- package/runtime/scripts/chat-core/server.js +2 -2
- package/runtime/scripts/chat-server/config.js +1 -0
- package/runtime/scripts/chat-server.js +2 -1
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/proxy-token-store.js +13 -1
package/lib/cli.js
CHANGED
|
@@ -1051,7 +1051,10 @@ async function status() {
|
|
|
1051
1051
|
if (record?.webhook_url) console.log(`Events: ${record.webhook_url}`);
|
|
1052
1052
|
if (record?.tunnel_url) console.log(`Tunnel: ${record.tunnel_url}`);
|
|
1053
1053
|
if (record?.tunnel_token) {
|
|
1054
|
-
|
|
1054
|
+
const defaultChatTunnel = AMALGM_RUNTIME_LABEL === 'main'
|
|
1055
|
+
? 'wss://amalgm-chat-gateway.fly.dev/wire'
|
|
1056
|
+
: 'wss://chat.preview.amalgm.ai/wire';
|
|
1057
|
+
console.log(`Chat tunnel: ${record.chat_tunnel_url || defaultChatTunnel}`);
|
|
1055
1058
|
}
|
|
1056
1059
|
console.log(`HMAC proxy: ${proxyTokenFromRecord(record) ? 'configured' : 'not configured'}`);
|
|
1057
1060
|
|
package/lib/paths.js
CHANGED
|
@@ -49,7 +49,8 @@ const SERVICE_PID_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'amalgm-service.pid
|
|
|
49
49
|
const SERVICE_STATE_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'service-state.json');
|
|
50
50
|
const SERVICE_STOP_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'amalgm-service.stop');
|
|
51
51
|
const SHUTDOWN_REASON_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'shutdown-reason.json');
|
|
52
|
-
const DEFAULT_APP_URL = process.env.AMALGM_APP_URL
|
|
52
|
+
const DEFAULT_APP_URL = process.env.AMALGM_APP_URL
|
|
53
|
+
|| (AMALGM_RUNTIME_LABEL === 'main' ? 'https://amalgm.ai' : 'https://preview.amalgm.ai');
|
|
53
54
|
|
|
54
55
|
module.exports = {
|
|
55
56
|
AMALGM_BRANCH,
|
package/lib/tunnel-chat.js
CHANGED
|
@@ -16,7 +16,11 @@ const DEFAULT_PORT_MONITOR_PORT = runtimeServiceByName('port-monitor').defaultPo
|
|
|
16
16
|
const DEFAULT_MCP_PORT = runtimeServiceByName('amalgm-mcp').defaultPort;
|
|
17
17
|
const DEFAULT_CHAT_SERVER_PORT = runtimeServiceByName('chat-server').defaultPort;
|
|
18
18
|
const DEFAULT_TARGET_PORT = DEFAULT_CHAT_SERVER_PORT;
|
|
19
|
-
|
|
19
|
+
function defaultChatTunnelUrl() {
|
|
20
|
+
return AMALGM_RUNTIME_LABEL === 'main'
|
|
21
|
+
? 'wss://amalgm-chat-gateway.fly.dev/wire'
|
|
22
|
+
: 'wss://chat.preview.amalgm.ai/wire';
|
|
23
|
+
}
|
|
20
24
|
const CONNECT_TIMEOUT_MS = 20_000;
|
|
21
25
|
const WATCHDOG_INTERVAL_MS = 15_000;
|
|
22
26
|
const HEARTBEAT_TIMEOUT_MS = 75_000;
|
|
@@ -54,6 +58,7 @@ const MCP_PREFIXES = [
|
|
|
54
58
|
'/agents',
|
|
55
59
|
'/agent-config',
|
|
56
60
|
'/agent-bundles',
|
|
61
|
+
'/skills',
|
|
57
62
|
'/user-preferences',
|
|
58
63
|
'/browser',
|
|
59
64
|
];
|
|
@@ -72,7 +77,7 @@ const HOP_BY_HOP_HEADERS = new Set([
|
|
|
72
77
|
]);
|
|
73
78
|
|
|
74
79
|
function tunnelUrl(rawUrl) {
|
|
75
|
-
const url = new URL(rawUrl ||
|
|
80
|
+
const url = new URL(rawUrl || defaultChatTunnelUrl());
|
|
76
81
|
url.searchParams.delete('token');
|
|
77
82
|
return url.toString();
|
|
78
83
|
}
|
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) {
|
|
@@ -52,9 +52,21 @@ const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
|
|
|
52
52
|
const AMALGM_USER_ID = process.env.AMALGM_USER_ID || '';
|
|
53
53
|
const AMALGM_COMPUTER_ID = process.env.AMALGM_COMPUTER_ID || process.env.AMALGM_CONTAINER_ID || '';
|
|
54
54
|
|
|
55
|
+
function usesPreviewInfraDefaults() {
|
|
56
|
+
const raw = String(
|
|
57
|
+
process.env.AMALGM_RUNTIME_LABEL
|
|
58
|
+
|| process.env.AMALGM_CHANNEL
|
|
59
|
+
|| process.env.AMALGM_BRANCH
|
|
60
|
+
|| '',
|
|
61
|
+
).trim().toLowerCase();
|
|
62
|
+
return ['preview', 'canary', 'native-chat', 'staging', 'local', 'dev', 'development'].includes(raw);
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
const PROXY_TOKEN = process.env.AMALGM_PROXY_TOKEN
|
|
56
66
|
|| (process.env.AMALGM_RUNTIME_SOURCE === 'npm' ? '' : process.env.ANTHROPIC_API_KEY || '');
|
|
57
|
-
const DEFAULT_PROXY_BASE_URL =
|
|
67
|
+
const DEFAULT_PROXY_BASE_URL = usesPreviewInfraDefaults()
|
|
68
|
+
? 'https://api.preview.amalgm.ai'
|
|
69
|
+
: 'https://amalgm-api-proxy-v2.fly.dev';
|
|
58
70
|
const PROXY_BASE_URL = process.env.AMALGM_PROXY_URL || (PROXY_TOKEN ? DEFAULT_PROXY_BASE_URL : '');
|
|
59
71
|
const SUPABASE_PROXY_URL = PROXY_BASE_URL ? `${PROXY_BASE_URL}/supabase` : '';
|
|
60
72
|
|
|
@@ -79,14 +91,14 @@ const APPS_DOMAIN = (
|
|
|
79
91
|
|| process.env.NEXT_PUBLIC_APPS_DOMAIN
|
|
80
92
|
|| process.env.AMALGM_ARTIFACTS_DOMAIN
|
|
81
93
|
|| process.env.NEXT_PUBLIC_ARTIFACTS_DOMAIN
|
|
82
|
-
|| 'apps.amalgm.ai'
|
|
94
|
+
|| (usesPreviewInfraDefaults() ? 'apps.preview.amalgm.ai' : 'apps.amalgm.ai')
|
|
83
95
|
);
|
|
84
96
|
const APP_PORT_MIN = parseInt(process.env.AMALGM_APP_PORT_MIN || process.env.AMALGM_ARTIFACT_PORT_MIN || '9100', 10);
|
|
85
97
|
const APP_PORT_MAX = parseInt(process.env.AMALGM_APP_PORT_MAX || process.env.AMALGM_ARTIFACT_PORT_MAX || '9199', 10);
|
|
86
98
|
const GATEWAY_BASE_URL = (
|
|
87
99
|
process.env.AMALGM_GATEWAY_INTERNAL_URL
|
|
88
100
|
|| process.env.AMALGM_GATEWAY_URL
|
|
89
|
-
|| 'https://wire.events.amalgm.ai'
|
|
101
|
+
|| (usesPreviewInfraDefaults() ? 'https://wire.events.preview.amalgm.ai' : 'https://wire.events.amalgm.ai')
|
|
90
102
|
).replace(/\/$/, '');
|
|
91
103
|
const GATEWAY_INTERNAL_SECRET =
|
|
92
104
|
process.env.AMALGM_GATEWAY_INTERNAL_SECRET
|
|
@@ -56,7 +56,7 @@ async function ensureProxyToken() {
|
|
|
56
56
|
const adminSecret = process.env.PROXY_ADMIN_SECRET || process.env.ADMIN_SECRET;
|
|
57
57
|
if (!adminSecret) return;
|
|
58
58
|
|
|
59
|
-
const proxyUrl = process.env.AMALGM_PROXY_URL ||
|
|
59
|
+
const proxyUrl = process.env.AMALGM_PROXY_URL || DEFAULT_PROXY_BASE_URL;
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
62
|
const res = await fetch(`${proxyUrl}/internal/token`, {
|
|
@@ -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',
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { AMALGM_DIR, DEFAULT_CWD, PROXY_BASE_URL, PROXY_TOKEN } = require('../chat-server/config');
|
|
6
|
+
const { AMALGM_DIR, DEFAULT_CWD, DEFAULT_PROXY_BASE_URL, PROXY_BASE_URL, PROXY_TOKEN } = require('../chat-server/config');
|
|
7
7
|
const { canonicalProjectPath } = require('../lib/project-paths');
|
|
8
8
|
const { authEnvelope, coerceAuth, fingerprint } = require('./auth');
|
|
9
9
|
const { runtimePort } = require('../../lib/runtime-manifest');
|
|
@@ -427,7 +427,7 @@ function createContract(payload, options = {}) {
|
|
|
427
427
|
sessionId,
|
|
428
428
|
localBaseUrl,
|
|
429
429
|
proxyToken: options.proxyToken || PROXY_TOKEN,
|
|
430
|
-
proxyBaseUrl: options.proxyBaseUrl || PROXY_BASE_URL ||
|
|
430
|
+
proxyBaseUrl: options.proxyBaseUrl || PROXY_BASE_URL || DEFAULT_PROXY_BASE_URL,
|
|
431
431
|
amalgmDir: options.amalgmDir || AMALGM_DIR || path.join(process.env.HOME || '.', '.amalgm'),
|
|
432
432
|
userId,
|
|
433
433
|
credentialId: payload.credentialId || payload.byokCredentialId || null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const http = require('http');
|
|
4
|
-
const { PORT, PROXY_BASE_URL, ensureFreshProxyToken } = require('../chat-server/config');
|
|
4
|
+
const { PORT, DEFAULT_PROXY_BASE_URL, PROXY_BASE_URL, ensureFreshProxyToken } = require('../chat-server/config');
|
|
5
5
|
const db = require('../chat-server/db');
|
|
6
6
|
const { CodexAdapter } = require('./adapters/codex');
|
|
7
7
|
const { ClaudeAdapter } = require('./adapters/claude');
|
|
@@ -107,7 +107,7 @@ function createServer(core = createCore()) {
|
|
|
107
107
|
res,
|
|
108
108
|
contract,
|
|
109
109
|
turn: core.turns.active(route.sessionId),
|
|
110
|
-
proxyBaseUrl: PROXY_BASE_URL ||
|
|
110
|
+
proxyBaseUrl: PROXY_BASE_URL || DEFAULT_PROXY_BASE_URL,
|
|
111
111
|
proxyToken,
|
|
112
112
|
upstreamPath: `${route.upstreamPath}${route.search || ''}`,
|
|
113
113
|
});
|
|
@@ -31,7 +31,8 @@ async function boot() {
|
|
|
31
31
|
}
|
|
32
32
|
const adminSecret = process.env.PROXY_ADMIN_SECRET || process.env.ADMIN_SECRET;
|
|
33
33
|
if (adminSecret) {
|
|
34
|
-
const
|
|
34
|
+
const { DEFAULT_PROXY_BASE_URL } = require('./proxy-token-store');
|
|
35
|
+
const proxyUrl = process.env.AMALGM_PROXY_URL || DEFAULT_PROXY_BASE_URL;
|
|
35
36
|
try {
|
|
36
37
|
const userId = process.env.AMALGM_USER_ID || process.env.USER_ID || process.env.NEXT_PUBLIC_AMALGM_USER_ID || '';
|
|
37
38
|
const containerId = process.env.AMALGM_CONTAINER_ID || 'standalone';
|
|
@@ -4,7 +4,19 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function usesPreviewInfraDefaults() {
|
|
8
|
+
const raw = String(
|
|
9
|
+
process.env.AMALGM_RUNTIME_LABEL
|
|
10
|
+
|| process.env.AMALGM_CHANNEL
|
|
11
|
+
|| process.env.AMALGM_BRANCH
|
|
12
|
+
|| '',
|
|
13
|
+
).trim().toLowerCase();
|
|
14
|
+
return ['preview', 'canary', 'native-chat', 'staging', 'local', 'dev', 'development'].includes(raw);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_PROXY_BASE_URL = usesPreviewInfraDefaults()
|
|
18
|
+
? 'https://api.preview.amalgm.ai'
|
|
19
|
+
: 'https://amalgm-api-proxy-v2.fly.dev';
|
|
8
20
|
const REFRESH_BUFFER_MS = 10 * 60 * 1000;
|
|
9
21
|
|
|
10
22
|
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|