@lloyal-labs/lloyal.node 1.0.3-alpha

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/index.js ADDED
@@ -0,0 +1,121 @@
1
+ const path = require('path');
2
+ const binary = require('node-gyp-build')(path.join(__dirname, '..'));
3
+
4
+ /**
5
+ * liblloyal-node - Thin N-API wrapper over liblloyal
6
+ *
7
+ * Exposes raw llama.cpp inference primitives for Node.js.
8
+ * Primary use case: Integration testing for tsampler.
9
+ *
10
+ * @example
11
+ * ```js
12
+ * const { createContext, withLogits } = require('lloyal.node');
13
+ *
14
+ * const ctx = await createContext({
15
+ * modelPath: './model.gguf',
16
+ * nCtx: 2048,
17
+ * nThreads: 4
18
+ * });
19
+ *
20
+ * // Tokenize
21
+ * const tokens = await ctx.tokenize("Hello world");
22
+ *
23
+ * // Decode
24
+ * await ctx.decode(tokens, 0);
25
+ *
26
+ * // Safe logits access (Runtime Borrow Checker pattern)
27
+ * const entropy = await withLogits(ctx, (logits) => {
28
+ * // logits is valid here - use synchronously only!
29
+ * return computeEntropy(logits);
30
+ * });
31
+ *
32
+ * // Or with native reference implementations (for testing)
33
+ * const nativeEntropy = ctx.computeEntropy();
34
+ * const token = ctx.greedySample();
35
+ *
36
+ * // Cleanup
37
+ * ctx.dispose();
38
+ * ```
39
+ */
40
+
41
+ /**
42
+ * Safe logits access with Runtime Borrow Checker pattern
43
+ *
44
+ * Ensures logits are only accessed synchronously within the callback.
45
+ * The callback MUST NOT:
46
+ * - Store the logits reference
47
+ * - Return a Promise (will throw)
48
+ * - Call decode() (would invalidate logits)
49
+ *
50
+ * This is a "runtime borrow checker" - it prevents async mutations
51
+ * while you're working with borrowed logits.
52
+ *
53
+ * @template T
54
+ * @param {SessionContext} ctx - The session context
55
+ * @param {(logits: Float32Array) => T} fn - Synchronous callback that uses logits
56
+ * @returns {T} The result from the callback
57
+ * @throws {Error} If callback returns a Promise (async usage not allowed)
58
+ *
59
+ * @example
60
+ * ```js
61
+ * // Safe: synchronous computation
62
+ * const entropy = withLogits(ctx, (logits) => {
63
+ * let sum = 0;
64
+ * for (let i = 0; i < logits.length; i++) {
65
+ * sum += Math.exp(logits[i]);
66
+ * }
67
+ * return Math.log(sum);
68
+ * });
69
+ *
70
+ * // ERROR: callback returns Promise (will throw)
71
+ * withLogits(ctx, async (logits) => {
72
+ * await something(); // NOT ALLOWED
73
+ * return logits[0];
74
+ * });
75
+ * ```
76
+ */
77
+ function withLogits(ctx, fn) {
78
+ // Get logits (memoized - same buffer if called twice in same step)
79
+ const logits = ctx.getLogits();
80
+
81
+ // Execute user callback with logits
82
+ const result = fn(logits);
83
+
84
+ // Detect async usage (not allowed - logits would be invalidated)
85
+ if (result && typeof result.then === 'function') {
86
+ throw new Error(
87
+ 'withLogits callback must be synchronous. ' +
88
+ 'Returning a Promise is not allowed because logits become invalid after decode(). ' +
89
+ 'Complete all logits processing synchronously within the callback.'
90
+ );
91
+ }
92
+
93
+ return result;
94
+ }
95
+
96
+ module.exports = {
97
+ /**
98
+ * Create a new inference context
99
+ *
100
+ * @param {Object} options
101
+ * @param {string} options.modelPath - Path to .gguf model file
102
+ * @param {number} [options.nCtx=2048] - Context size
103
+ * @param {number} [options.nThreads=4] - Number of threads
104
+ * @returns {Promise<SessionContext>}
105
+ */
106
+ createContext: async (options) => {
107
+ // For now, createContext is synchronous in C++
108
+ // Wrap in Promise for future async model loading
109
+ return binary.createContext(options);
110
+ },
111
+
112
+ /**
113
+ * Safe logits access with Runtime Borrow Checker pattern
114
+ *
115
+ * Ensures logits are only accessed synchronously within the callback.
116
+ * See function JSDoc for full documentation.
117
+ */
118
+ withLogits,
119
+
120
+ SessionContext: binary.SessionContext
121
+ };
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@lloyal-labs/lloyal.node",
3
+ "version": "1.0.3-alpha",
4
+ "description": "Node.js client for liblloyal+llama.cpp",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "gypfile": false,
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "scripts": {
12
+ "download-models": "bash scripts/download-test-models.sh",
13
+ "install": "node scripts/install.js",
14
+ "build": "node scripts/build.js",
15
+ "build:debug": "cmake-js compile --debug",
16
+ "rebuild": "cmake-js rebuild",
17
+ "clean": "cmake-js clean && rm -rf build_test/",
18
+ "version": "node scripts/sync-versions.js && git add -A",
19
+ "docs": "npx typedoc",
20
+ "test": "npm run test:api && npm run test:e2e",
21
+ "test:api": "node test/api.js",
22
+ "test:e2e": "node test/e2e.js",
23
+ "example": "node examples/chat/chat.mjs"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/lloyal-ai/lloyal.node.git"
28
+ },
29
+ "keywords": [
30
+ "llama",
31
+ "llama.cpp",
32
+ "liblloyal",
33
+ "napi",
34
+ "native",
35
+ "inference",
36
+ "llm"
37
+ ],
38
+ "author": "lloyal.ai",
39
+ "license": "Apache-2.0",
40
+ "type": "commonjs",
41
+ "bugs": {
42
+ "url": "https://github.com/lloyal-ai/lloyal.node/issues"
43
+ },
44
+ "homepage": "https://github.com/lloyal-ai/lloyal.node#readme",
45
+ "dependencies": {
46
+ "node-addon-api": "^8.5.0",
47
+ "node-gyp-build": "^4.8.4"
48
+ },
49
+ "devDependencies": {
50
+ "cmake-js": "^7.4.0",
51
+ "glob": "^11.0.0",
52
+ "typedoc": "^0.27.5"
53
+ },
54
+ "optionalDependencies": {
55
+ "@lloyal-labs/lloyal.node-darwin-arm64": "1.0.3-alpha",
56
+ "@lloyal-labs/lloyal.node-darwin-x64": "1.0.3-alpha",
57
+ "@lloyal-labs/lloyal.node-linux-arm64": "1.0.3-alpha",
58
+ "@lloyal-labs/lloyal.node-linux-arm64-cuda": "1.0.3-alpha",
59
+ "@lloyal-labs/lloyal.node-linux-arm64-vulkan": "1.0.3-alpha",
60
+ "@lloyal-labs/lloyal.node-linux-x64": "1.0.3-alpha",
61
+ "@lloyal-labs/lloyal.node-linux-x64-cuda": "1.0.3-alpha",
62
+ "@lloyal-labs/lloyal.node-linux-x64-vulkan": "1.0.3-alpha",
63
+ "@lloyal-labs/lloyal.node-win32-arm64": "1.0.3-alpha",
64
+ "@lloyal-labs/lloyal.node-win32-arm64-vulkan": "1.0.3-alpha",
65
+ "@lloyal-labs/lloyal.node-win32-x64": "1.0.3-alpha",
66
+ "@lloyal-labs/lloyal.node-win32-x64-cuda": "1.0.3-alpha",
67
+ "@lloyal-labs/lloyal.node-win32-x64-vulkan": "1.0.3-alpha"
68
+ },
69
+ "engines": {
70
+ "node": ">=22.0.0"
71
+ },
72
+ "files": [
73
+ "lib/",
74
+ "scripts/"
75
+ ]
76
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build script for lloyal.node
4
+ *
5
+ * Wraps cmake-js with GPU backend detection from LLOYAL_GPU environment variable.
6
+ *
7
+ * Usage:
8
+ * npm run build # CPU/Metal (auto-detected)
9
+ * LLOYAL_GPU=cuda npm run build # CUDA
10
+ * LLOYAL_GPU=vulkan npm run build # Vulkan
11
+ * LLOYAL_GPU=metal npm run build # Metal (macOS only)
12
+ */
13
+
14
+ const { execSync } = require('child_process');
15
+ const os = require('os');
16
+
17
+ const PLATFORM = process.platform;
18
+ const gpuBackend = process.env.LLOYAL_GPU?.toLowerCase();
19
+
20
+ // Build cmake-js command with appropriate flags
21
+ const cmakeFlags = [];
22
+
23
+ if (gpuBackend === 'cuda') {
24
+ cmakeFlags.push('--CDGGML_CUDA=ON');
25
+ console.log('[lloyal.node] GPU backend: CUDA');
26
+ } else if (gpuBackend === 'vulkan') {
27
+ cmakeFlags.push('--CDGGML_VULKAN=ON');
28
+ console.log('[lloyal.node] GPU backend: Vulkan');
29
+ } else if (gpuBackend === 'metal') {
30
+ cmakeFlags.push('--CDGGML_METAL=ON');
31
+ console.log('[lloyal.node] GPU backend: Metal');
32
+ } else if (PLATFORM === 'darwin') {
33
+ // Metal is auto-enabled on macOS by llama.cpp
34
+ console.log('[lloyal.node] GPU backend: Metal (auto-enabled on macOS)');
35
+ } else {
36
+ console.log('[lloyal.node] GPU backend: CPU only');
37
+ }
38
+
39
+ const buildCmd = `npx cmake-js compile ${cmakeFlags.join(' ')}`.trim();
40
+ console.log(`[lloyal.node] Running: ${buildCmd}`);
41
+
42
+ try {
43
+ execSync(buildCmd, {
44
+ cwd: __dirname + '/..',
45
+ stdio: 'inherit'
46
+ });
47
+ console.log('[lloyal.node] ✅ Build successful!');
48
+ } catch (error) {
49
+ console.error('[lloyal.node] ❌ Build failed');
50
+ process.exit(1);
51
+ }
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Create platform-specific package for prebuilt binaries
4
+ *
5
+ * Usage: node scripts/create-platform-package.js <package-name> <os> <arch>
6
+ * Example: node scripts/create-platform-package.js darwin-arm64 macos-14 arm64
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const [packageName, osRunner, arch] = process.argv.slice(2);
13
+
14
+ if (!packageName || !osRunner || !arch) {
15
+ console.error('Usage: node create-platform-package.js <package-name> <os-runner> <arch>');
16
+ console.error('Example: node create-platform-package.js darwin-arm64 macos-14 arm64');
17
+ process.exit(1);
18
+ }
19
+
20
+ const ROOT = path.join(__dirname, '..');
21
+ const BUILD_DIR = path.join(ROOT, 'build', 'Release');
22
+ const PACKAGES_DIR = path.join(ROOT, 'packages');
23
+ const PKG_DIR = path.join(PACKAGES_DIR, packageName);
24
+ const BIN_DIR = path.join(PKG_DIR, 'bin');
25
+
26
+ // Determine OS and CPU for package.json
27
+ const OS_MAP = {
28
+ 'macos-14': 'darwin',
29
+ 'macos-13': 'darwin',
30
+ 'ubuntu-22.04': 'linux',
31
+ 'windows-2022': 'win32'
32
+ };
33
+
34
+ const osName = OS_MAP[osRunner] || process.platform;
35
+
36
+ console.log(`\n=== Creating platform package: @lloyal-labs/lloyal.node-${packageName} ===\n`);
37
+
38
+ // Create directories
39
+ fs.mkdirSync(BIN_DIR, { recursive: true });
40
+
41
+ // Copy binaries
42
+ console.log('Copying binaries...');
43
+
44
+ // N-API binary
45
+ const nodeBinary = path.join(BUILD_DIR, 'lloyal.node');
46
+ if (!fs.existsSync(nodeBinary)) {
47
+ console.error(`❌ Error: lloyal.node not found at ${nodeBinary}`);
48
+ console.error('Available files in build/Release:');
49
+ if (fs.existsSync(BUILD_DIR)) {
50
+ fs.readdirSync(BUILD_DIR).forEach(f => console.error(` - ${f}`));
51
+ } else {
52
+ console.error(' (build/Release directory does not exist)');
53
+ }
54
+ process.exit(1);
55
+ }
56
+
57
+ fs.copyFileSync(nodeBinary, path.join(BIN_DIR, 'lloyal.node'));
58
+ console.log(` ✓ Copied lloyal.node`);
59
+
60
+ // Shared libraries (platform-specific)
61
+ if (osName === 'darwin') {
62
+ const dylib = path.join(BUILD_DIR, 'libllama.dylib');
63
+ if (fs.existsSync(dylib)) {
64
+ fs.copyFileSync(dylib, path.join(BIN_DIR, 'libllama.dylib'));
65
+ console.log(` ✓ Copied libllama.dylib`);
66
+ } else {
67
+ console.warn(` ⚠️ libllama.dylib not found (optional)`);
68
+ }
69
+ } else if (osName === 'linux') {
70
+ const so = path.join(BUILD_DIR, 'libllama.so');
71
+ if (fs.existsSync(so)) {
72
+ fs.copyFileSync(so, path.join(BIN_DIR, 'libllama.so'));
73
+ console.log(` ✓ Copied libllama.so`);
74
+ } else {
75
+ console.warn(` ⚠️ libllama.so not found (optional)`);
76
+ }
77
+ } else if (osName === 'win32') {
78
+ // Copy all DLLs
79
+ const dlls = fs.readdirSync(BUILD_DIR).filter(f => f.endsWith('.dll'));
80
+ if (dlls.length > 0) {
81
+ dlls.forEach(dll => {
82
+ fs.copyFileSync(
83
+ path.join(BUILD_DIR, dll),
84
+ path.join(BIN_DIR, dll)
85
+ );
86
+ console.log(` ✓ Copied ${dll}`);
87
+ });
88
+ } else {
89
+ console.warn(` ⚠️ No DLLs found in build/Release (optional)`);
90
+ }
91
+ }
92
+
93
+ // Create package.json from template
94
+ console.log('\nGenerating package.json...');
95
+ const mainPackageJson = require(path.join(ROOT, 'package.json'));
96
+ const templatePath = path.join(ROOT, 'packages', 'template', 'package.json');
97
+
98
+ let pkgJson;
99
+ if (fs.existsSync(templatePath)) {
100
+ pkgJson = require(templatePath);
101
+ } else {
102
+ // Fallback template if file doesn't exist yet
103
+ pkgJson = {
104
+ name: '@lloyal-labs/lloyal.node-PLATFORM',
105
+ version: '0.0.0',
106
+ description: 'Lloyal native binary for PLATFORM',
107
+ main: 'index.js',
108
+ files: ['bin/', 'index.js'],
109
+ repository: {
110
+ type: 'git',
111
+ url: 'git+https://github.com/lloyal-ai/lloyal.node.git'
112
+ },
113
+ license: 'Apache-2.0'
114
+ };
115
+ }
116
+
117
+ // Update with actual values
118
+ pkgJson.name = `@lloyal-labs/lloyal.node-${packageName}`;
119
+ pkgJson.version = mainPackageJson.version;
120
+ pkgJson.description = `Lloyal native binary for ${packageName}`;
121
+ pkgJson.os = [osName];
122
+ pkgJson.cpu = [arch];
123
+
124
+ fs.writeFileSync(
125
+ path.join(PKG_DIR, 'package.json'),
126
+ JSON.stringify(pkgJson, null, 2) + '\n'
127
+ );
128
+ console.log(` ✓ Created package.json`);
129
+
130
+ // Create index.js
131
+ console.log('\nGenerating index.js...');
132
+ const indexJs = `// Platform-specific binary package for ${packageName}
133
+ // This file resolves to the native binary in bin/
134
+
135
+ const path = require('path');
136
+
137
+ module.exports = path.join(__dirname, 'bin', 'lloyal.node');
138
+ `;
139
+
140
+ fs.writeFileSync(path.join(PKG_DIR, 'index.js'), indexJs);
141
+ console.log(` ✓ Created index.js`);
142
+
143
+ // Summary
144
+ console.log(`\n✅ Platform package created successfully!`);
145
+ console.log(`\nPackage: @lloyal-labs/lloyal.node-${packageName}@${pkgJson.version}`);
146
+ console.log(`Location: ${PKG_DIR}`);
147
+ console.log(`\nContents:`);
148
+ fs.readdirSync(BIN_DIR).forEach(f => {
149
+ const stats = fs.statSync(path.join(BIN_DIR, f));
150
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
151
+ console.log(` - bin/${f} (${sizeMB} MB)`);
152
+ });
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Downloading test models..."
5
+
6
+ mkdir -p models
7
+ cd models
8
+
9
+ # SmolLM2 1.7B Instruct Q4_K_M (1.0GB)
10
+ if [ ! -f "SmolLM2-1.7B-Instruct-Q4_K_M.gguf" ]; then
11
+ echo " → Downloading SmolLM2-1.7B-Instruct-Q4_K_M.gguf..."
12
+ curl -L -o "SmolLM2-1.7B-Instruct-Q4_K_M.gguf" \
13
+ "https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B-Instruct-GGUF/resolve/main/smollm2-1.7b-instruct-q4_k_m.gguf"
14
+ echo " ✓ Downloaded SmolLM2"
15
+ else
16
+ echo " ✓ SmolLM2 already exists"
17
+ fi
18
+
19
+ # Nomic Embed Text v1.5 Q4_K_M (80MB)
20
+ if [ ! -f "nomic-embed-text-v1.5.Q4_K_M.gguf" ]; then
21
+ echo " → Downloading nomic-embed-text-v1.5.Q4_K_M.gguf..."
22
+ curl -L -o "nomic-embed-text-v1.5.Q4_K_M.gguf" \
23
+ "https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF/resolve/main/nomic-embed-text-v1.5.Q4_K_M.gguf"
24
+ echo " ✓ Downloaded nomic-embed-text"
25
+ else
26
+ echo " ✓ nomic-embed-text already exists"
27
+ fi
28
+
29
+ echo ""
30
+ echo "✅ All test models ready"
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Initialize git submodules during npm install
4
+ *
5
+ * This is necessary because npm doesn't automatically initialize submodules
6
+ * when installing from GitHub URLs. Without this, llama.cpp/ and liblloyal/
7
+ * won't exist, causing build failures.
8
+ */
9
+
10
+ const { execSync } = require('child_process');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const ROOT = path.join(__dirname, '..');
15
+
16
+ console.log('[init-submodules] Checking for git submodules...');
17
+
18
+ // Check if we're in a git repository
19
+ const isGitRepo = fs.existsSync(path.join(ROOT, '.git'));
20
+
21
+ if (!isGitRepo) {
22
+ console.log('[init-submodules] Not a git repository, skipping submodule initialization');
23
+ process.exit(0);
24
+ }
25
+
26
+ // Check if submodules are already initialized
27
+ const llamaCppExists = fs.existsSync(path.join(ROOT, 'llama.cpp/.git'));
28
+ const libloyalExists = fs.existsSync(path.join(ROOT, 'liblloyal/.git'));
29
+
30
+ if (llamaCppExists && libloyalExists) {
31
+ console.log('[init-submodules] ✓ Submodules already initialized');
32
+ process.exit(0);
33
+ }
34
+
35
+ // Initialize submodules
36
+ console.log('[init-submodules] Initializing git submodules...');
37
+ try {
38
+ execSync('git submodule update --init --recursive', {
39
+ cwd: ROOT,
40
+ stdio: 'inherit'
41
+ });
42
+ console.log('[init-submodules] ✓ Submodules initialized successfully');
43
+ } catch (error) {
44
+ console.error('[init-submodules] Failed to initialize submodules:', error.message);
45
+ console.error('[init-submodules] Please run manually: git submodule update --init --recursive');
46
+ process.exit(1);
47
+ }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Smart installer for lloyal.node
4
+ *
5
+ * Strategy:
6
+ * 1. Check if prebuilt binary exists for this platform
7
+ * 2. If yes, copy to build/Release/ and exit
8
+ * 3. If no, show helpful error with build-from-source instructions
9
+ *
10
+ * Respects LLOYAL_GPU environment variable for GPU variant selection
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const PLATFORM = process.platform;
17
+ const ARCH = process.arch;
18
+ const ROOT = __dirname + '/..';
19
+ const BUILD_DIR = path.join(ROOT, 'build', 'Release');
20
+
21
+ // Logging helpers
22
+ const log = (msg) => console.log(`[lloyal.node] ${msg}`);
23
+ const error = (msg) => console.error(`[lloyal.node] ❌ ${msg}`);
24
+
25
+ /**
26
+ * Check if a platform package is installed and has binaries
27
+ */
28
+ function findPrebuilt(packageName) {
29
+ try {
30
+ const pkgPath = require.resolve(packageName);
31
+ const binPath = require(packageName); // index.js exports path to binary
32
+
33
+ if (fs.existsSync(binPath)) {
34
+ const binDir = path.dirname(binPath);
35
+ return binDir;
36
+ }
37
+ } catch (e) {
38
+ // Package not installed or doesn't export binary path
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Copy prebuilt binaries to build/Release/
45
+ */
46
+ function installPrebuilt(binDir, packageName) {
47
+ log(`Found prebuilt binaries in ${packageName}`);
48
+
49
+ try {
50
+ // Create build/Release directory
51
+ fs.mkdirSync(BUILD_DIR, { recursive: true });
52
+
53
+ // Copy all files from bin directory
54
+ const files = fs.readdirSync(binDir);
55
+ files.forEach(file => {
56
+ const src = path.join(binDir, file);
57
+ const dest = path.join(BUILD_DIR, file);
58
+
59
+ if (fs.statSync(src).isFile()) {
60
+ fs.copyFileSync(src, dest);
61
+ log(` ✓ Copied ${file}`);
62
+ }
63
+ });
64
+
65
+ log(`✅ Installed prebuilt binaries successfully`);
66
+ process.exit(0);
67
+ } catch (e) {
68
+ error(`Failed to install prebuilt: ${e.message}`);
69
+ // Don't exit - fall through to source build
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Main installation logic
75
+ */
76
+ function main() {
77
+ log(`Platform: ${PLATFORM}-${ARCH}`);
78
+
79
+ // 1. Check for user-specified GPU variant via environment variable
80
+ if (process.env.LLOYAL_GPU) {
81
+ const gpu = process.env.LLOYAL_GPU.toLowerCase();
82
+ const packageName = `@lloyal-labs/lloyal.node-${PLATFORM}-${ARCH}-${gpu}`;
83
+
84
+ log(`LLOYAL_GPU=${gpu}, looking for ${packageName}...`);
85
+ const binDir = findPrebuilt(packageName);
86
+
87
+ if (binDir) {
88
+ installPrebuilt(binDir, packageName);
89
+ return; // exit(0) called in installPrebuilt
90
+ } else {
91
+ log(` ⚠️ Package ${packageName} not found`);
92
+ }
93
+ }
94
+
95
+ // 2. Check for GPU variants in priority order
96
+ const gpuVariants = ['cuda', 'vulkan'];
97
+ for (const gpu of gpuVariants) {
98
+ const packageName = `@lloyal-labs/lloyal.node-${PLATFORM}-${ARCH}-${gpu}`;
99
+ const binDir = findPrebuilt(packageName);
100
+
101
+ if (binDir) {
102
+ log(`Auto-detected GPU variant: ${gpu}`);
103
+ installPrebuilt(binDir, packageName);
104
+ return; // exit(0) called in installPrebuilt
105
+ }
106
+ }
107
+
108
+ // 3. Check for default platform package (CPU or Metal on macOS)
109
+ const defaultPackage = `@lloyal-labs/lloyal.node-${PLATFORM}-${ARCH}`;
110
+ const binDir = findPrebuilt(defaultPackage);
111
+
112
+ if (binDir) {
113
+ installPrebuilt(binDir, defaultPackage);
114
+ return; // exit(0) called in installPrebuilt
115
+ }
116
+
117
+ // 4. No prebuilt found - error with helpful message
118
+ log('');
119
+ error('No prebuilt binary found for your platform');
120
+ log('');
121
+ log(` Platform: ${PLATFORM}-${ARCH}`);
122
+ log('');
123
+ log(' Options:');
124
+ log(' 1. Install a platform-specific package:');
125
+ log(` npm install @lloyal-labs/lloyal.node-${PLATFORM}-${ARCH}`);
126
+ log('');
127
+ log(' 2. Build from source (requires C++20, CMake 3.18+):');
128
+ log(' git clone --recursive https://github.com/lloyal-ai/lloyal.node.git');
129
+ log(' cd lloyal.node && npm run build');
130
+ log('');
131
+ log(' See: https://github.com/lloyal-ai/lloyal.node#building');
132
+ log('');
133
+
134
+ process.exit(1);
135
+ }
136
+
137
+ // Run installer
138
+ main();
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Synchronize all platform package versions with main package version
4
+ *
5
+ * Ensures optionalDependencies in package.json all reference the current version
6
+ * Run before publishing or after `npm version`
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const ROOT = path.join(__dirname, '..');
13
+ const PKG_JSON_PATH = path.join(ROOT, 'package.json');
14
+
15
+ console.log('[sync-versions] Synchronizing package versions...\n');
16
+
17
+ // Read main package.json
18
+ const pkg = JSON.parse(fs.readFileSync(PKG_JSON_PATH, 'utf8'));
19
+ const version = pkg.version;
20
+
21
+ console.log(`Main package version: ${version}`);
22
+
23
+ // Update optionalDependencies
24
+ if (pkg.optionalDependencies) {
25
+ console.log('\nUpdating optionalDependencies:');
26
+
27
+ Object.keys(pkg.optionalDependencies).forEach(dep => {
28
+ const oldVersion = pkg.optionalDependencies[dep];
29
+ pkg.optionalDependencies[dep] = version;
30
+
31
+ if (oldVersion !== version) {
32
+ console.log(` ${dep}: ${oldVersion} → ${version}`);
33
+ } else {
34
+ console.log(` ${dep}: ${version} (unchanged)`);
35
+ }
36
+ });
37
+
38
+ // Write updated package.json
39
+ fs.writeFileSync(PKG_JSON_PATH, JSON.stringify(pkg, null, 2) + '\n');
40
+ console.log('\n✅ package.json updated');
41
+ } else {
42
+ console.log('\n⚠️ No optionalDependencies found in package.json');
43
+ }
44
+
45
+ // Update any existing platform packages in packages/ directory
46
+ const PACKAGES_DIR = path.join(ROOT, 'packages');
47
+
48
+ if (fs.existsSync(PACKAGES_DIR)) {
49
+ const dirs = fs.readdirSync(PACKAGES_DIR).filter(f => {
50
+ const stat = fs.statSync(path.join(PACKAGES_DIR, f));
51
+ return stat.isDirectory() && f !== 'template';
52
+ });
53
+
54
+ if (dirs.length > 0) {
55
+ console.log('\nUpdating platform packages:');
56
+
57
+ dirs.forEach(dir => {
58
+ const pkgPath = path.join(PACKAGES_DIR, dir, 'package.json');
59
+
60
+ if (fs.existsSync(pkgPath)) {
61
+ const platformPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
62
+ const oldVersion = platformPkg.version;
63
+
64
+ platformPkg.version = version;
65
+ fs.writeFileSync(pkgPath, JSON.stringify(platformPkg, null, 2) + '\n');
66
+
67
+ if (oldVersion !== version) {
68
+ console.log(` ${platformPkg.name}: ${oldVersion} → ${version}`);
69
+ } else {
70
+ console.log(` ${platformPkg.name}: ${version} (unchanged)`);
71
+ }
72
+ }
73
+ });
74
+
75
+ console.log('\n✅ Platform packages updated');
76
+ }
77
+ }
78
+
79
+ console.log('\n✅ Version synchronization complete!');