@hyperengineering/recall 1.2.0

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/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # @hyperengineering/recall
2
+
3
+ CLI for managing experiential lore from AI agent workflows.
4
+
5
+ This npm package provides a convenient way to install and use the `recall` CLI tool. The package automatically downloads the correct binary for your platform during installation.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @hyperengineering/recall
11
+ ```
12
+
13
+ Or with yarn:
14
+
15
+ ```bash
16
+ yarn add @hyperengineering/recall
17
+ ```
18
+
19
+ Or with pnpm:
20
+
21
+ ```bash
22
+ pnpm add @hyperengineering/recall
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ After installation, the `recall` command is available:
28
+
29
+ ```bash
30
+ npx recall --help
31
+ ```
32
+
33
+ Or if installed globally:
34
+
35
+ ```bash
36
+ npm install -g @hyperengineering/recall
37
+ recall --help
38
+ ```
39
+
40
+ ## Supported Platforms
41
+
42
+ | OS | Architecture | Supported |
43
+ |----|--------------|-----------|
44
+ | macOS | x64 (Intel) | Yes |
45
+ | macOS | arm64 (Apple Silicon) | Yes |
46
+ | Linux | x64 | Yes |
47
+ | Linux | arm64 | Yes |
48
+ | Windows | x64 | Yes |
49
+ | Windows | arm64 | No |
50
+
51
+ ## Environment Variables
52
+
53
+ ### `RECALL_SKIP_DOWNLOAD`
54
+
55
+ Set to `1` to skip the binary download during installation. Useful when:
56
+ - You want to install the binary manually
57
+ - You're caching the binary in CI/CD
58
+ - The download is blocked by network restrictions
59
+
60
+ ```bash
61
+ RECALL_SKIP_DOWNLOAD=1 npm install @hyperengineering/recall
62
+ ```
63
+
64
+ ### `RECALL_BINARY_PATH`
65
+
66
+ Point to a custom binary location instead of the downloaded one:
67
+
68
+ ```bash
69
+ export RECALL_BINARY_PATH=/usr/local/bin/recall
70
+ npx recall --help
71
+ ```
72
+
73
+ ## CI/CD Usage
74
+
75
+ ### GitHub Actions
76
+
77
+ ```yaml
78
+ - name: Install recall
79
+ run: npm install @hyperengineering/recall
80
+
81
+ - name: Use recall
82
+ run: npx recall version
83
+ ```
84
+
85
+ With caching:
86
+
87
+ ```yaml
88
+ - name: Cache recall binary
89
+ uses: actions/cache@v4
90
+ with:
91
+ path: node_modules/@hyperengineering/recall/vendor
92
+ key: recall-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package-lock.json') }}
93
+
94
+ - name: Install recall
95
+ run: npm install @hyperengineering/recall
96
+ ```
97
+
98
+ ### GitLab CI
99
+
100
+ ```yaml
101
+ install:
102
+ script:
103
+ - npm install @hyperengineering/recall
104
+ - npx recall version
105
+ cache:
106
+ paths:
107
+ - node_modules/
108
+ ```
109
+
110
+ ### Skip Download (Pre-installed Binary)
111
+
112
+ If you have recall installed via Homebrew or another method:
113
+
114
+ ```yaml
115
+ - name: Install recall via Homebrew
116
+ run: brew install hyperengineering/tap/recall
117
+
118
+ - name: Install npm package (skip download)
119
+ run: RECALL_SKIP_DOWNLOAD=1 npm install @hyperengineering/recall
120
+ env:
121
+ RECALL_BINARY_PATH: /usr/local/bin/recall
122
+ ```
123
+
124
+ ## Troubleshooting
125
+
126
+ ### Binary not found after installation
127
+
128
+ The binary may have failed to download. Try:
129
+
130
+ 1. Run `npm rebuild @hyperengineering/recall` to re-download
131
+ 2. Check for network issues (firewalls, proxies)
132
+ 3. Install manually using one of the alternatives below
133
+
134
+ ### Download fails in restricted environments
135
+
136
+ Some environments block downloads during npm install. Solutions:
137
+
138
+ 1. Pre-download the binary and use `RECALL_BINARY_PATH`
139
+ 2. Use the Homebrew tap: `brew install hyperengineering/tap/recall`
140
+ 3. Download from [GitHub Releases](https://github.com/hyperengineering/recall/releases)
141
+
142
+ ### Permission denied on Unix
143
+
144
+ The binary should have executable permissions set automatically. If not:
145
+
146
+ ```bash
147
+ chmod +x node_modules/@hyperengineering/recall/vendor/recall
148
+ ```
149
+
150
+ ## Alternative Installation Methods
151
+
152
+ If npm installation doesn't work for your use case:
153
+
154
+ ### Homebrew (macOS/Linux)
155
+
156
+ ```bash
157
+ brew install hyperengineering/tap/recall
158
+ ```
159
+
160
+ ### Direct Download
161
+
162
+ Download the binary for your platform from [GitHub Releases](https://github.com/hyperengineering/recall/releases).
163
+
164
+ ### Go Install
165
+
166
+ ```bash
167
+ go install github.com/hyperengineering/recall/cmd/recall@latest
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT
173
+
174
+ ## Links
175
+
176
+ - [GitHub Repository](https://github.com/hyperengineering/recall)
177
+ - [Documentation](https://github.com/hyperengineering/recall#readme)
178
+ - [Issues](https://github.com/hyperengineering/recall/issues)
package/bin/recall.cmd ADDED
@@ -0,0 +1,6 @@
1
+ @echo off
2
+
3
+ :: Wrapper script for recall binary on Windows
4
+ :: This is a compatibility shim for npm on Windows
5
+
6
+ node "%~dp0recall.js" %*
package/bin/recall.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wrapper script for recall binary.
5
+ * Executes the downloaded binary with all arguments.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { spawn } = require('child_process');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+
14
+ const SCRIPT_DIR = __dirname;
15
+ const VENDOR_DIR = path.join(SCRIPT_DIR, '..', 'vendor');
16
+
17
+ function getBinaryPath() {
18
+ // Check for custom binary path
19
+ if (process.env.RECALL_BINARY_PATH) {
20
+ return process.env.RECALL_BINARY_PATH;
21
+ }
22
+
23
+ // Determine binary name based on platform
24
+ const binaryName = process.platform === 'win32' ? 'recall.exe' : 'recall';
25
+ const binaryPath = path.join(VENDOR_DIR, binaryName);
26
+
27
+ return binaryPath;
28
+ }
29
+
30
+ function main() {
31
+ const binaryPath = getBinaryPath();
32
+
33
+ if (!fs.existsSync(binaryPath)) {
34
+ console.error(`Error: recall binary not found at ${binaryPath}`);
35
+ console.error('');
36
+ console.error('Run one of the following to download the binary:');
37
+ console.error(' npm rebuild @hyperengineering/recall');
38
+ console.error('');
39
+ console.error('Or set RECALL_BINARY_PATH to point to your recall installation');
40
+ process.exit(1);
41
+ }
42
+
43
+ // Pass through all arguments to the binary
44
+ const args = process.argv.slice(2);
45
+
46
+ const child = spawn(binaryPath, args, {
47
+ stdio: 'inherit',
48
+ windowsHide: true
49
+ });
50
+
51
+ child.on('error', (err) => {
52
+ console.error(`Error executing recall: ${err.message}`);
53
+ process.exit(1);
54
+ });
55
+
56
+ child.on('exit', (code, signal) => {
57
+ if (signal) {
58
+ process.exit(1);
59
+ }
60
+ process.exit(code ?? 0);
61
+ });
62
+ }
63
+
64
+ main();
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Binary download with retry logic.
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const https = require('https');
8
+ const http = require('http');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const MAX_RETRIES = 3;
13
+ const RETRY_DELAY_MS = 1000;
14
+
15
+ /**
16
+ * Download a file from URL to destination path.
17
+ * @param {string} url - Download URL
18
+ * @param {string} destPath - Destination file path
19
+ * @param {object} options - Options
20
+ * @param {number} [options.retries=3] - Number of retries
21
+ * @param {function} [options.onProgress] - Progress callback (received, total)
22
+ * @returns {Promise<void>}
23
+ */
24
+ async function download(url, destPath, options = {}) {
25
+ const { retries = MAX_RETRIES, onProgress } = options;
26
+
27
+ for (let attempt = 1; attempt <= retries; attempt++) {
28
+ try {
29
+ await downloadOnce(url, destPath, onProgress);
30
+ return;
31
+ } catch (err) {
32
+ if (attempt === retries) {
33
+ throw err;
34
+ }
35
+ const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1);
36
+ console.log(`Download failed (attempt ${attempt}/${retries}), retrying in ${delay}ms...`);
37
+ await sleep(delay);
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Download a file once (no retries).
44
+ * @param {string} url - Download URL
45
+ * @param {string} destPath - Destination file path
46
+ * @param {function} [onProgress] - Progress callback
47
+ * @returns {Promise<void>}
48
+ */
49
+ function downloadOnce(url, destPath, onProgress) {
50
+ return new Promise((resolve, reject) => {
51
+ const protocol = url.startsWith('https') ? https : http;
52
+
53
+ const request = protocol.get(url, {
54
+ headers: { 'User-Agent': 'npm-@hyperengineering/recall' }
55
+ }, (response) => {
56
+ // Handle redirects
57
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
58
+ downloadOnce(response.headers.location, destPath, onProgress)
59
+ .then(resolve)
60
+ .catch(reject);
61
+ return;
62
+ }
63
+
64
+ if (response.statusCode !== 200) {
65
+ reject(new Error(`Download failed: HTTP ${response.statusCode}`));
66
+ return;
67
+ }
68
+
69
+ const totalBytes = parseInt(response.headers['content-length'], 10) || 0;
70
+ let receivedBytes = 0;
71
+
72
+ const dir = path.dirname(destPath);
73
+ if (!fs.existsSync(dir)) {
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ }
76
+
77
+ const fileStream = fs.createWriteStream(destPath);
78
+
79
+ response.on('data', (chunk) => {
80
+ receivedBytes += chunk.length;
81
+ if (onProgress && totalBytes > 0) {
82
+ onProgress(receivedBytes, totalBytes);
83
+ }
84
+ });
85
+
86
+ response.pipe(fileStream);
87
+
88
+ fileStream.on('finish', () => {
89
+ fileStream.close();
90
+ resolve();
91
+ });
92
+
93
+ fileStream.on('error', (err) => {
94
+ fs.unlink(destPath, () => {}); // Clean up partial file
95
+ reject(err);
96
+ });
97
+ });
98
+
99
+ request.on('error', (err) => {
100
+ // Clean up partial file if it exists
101
+ if (fs.existsSync(destPath)) {
102
+ fs.unlinkSync(destPath);
103
+ }
104
+ reject(err);
105
+ });
106
+
107
+ request.on('timeout', () => {
108
+ request.destroy();
109
+ reject(new Error('Download timed out'));
110
+ });
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Sleep for a given number of milliseconds.
116
+ * @param {number} ms - Milliseconds to sleep
117
+ * @returns {Promise<void>}
118
+ */
119
+ function sleep(ms) {
120
+ return new Promise(resolve => setTimeout(resolve, ms));
121
+ }
122
+
123
+ module.exports = { download };
package/lib/extract.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Archive extraction for tar.gz and zip files.
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Extract archive to destination directory.
12
+ * @param {string} archivePath - Path to archive file
13
+ * @param {string} destDir - Destination directory
14
+ * @param {{ ext: string, os?: string }} platform - Platform info with extension
15
+ * @returns {Promise<void>}
16
+ */
17
+ async function extract(archivePath, destDir, platform) {
18
+ if (!fs.existsSync(destDir)) {
19
+ fs.mkdirSync(destDir, { recursive: true });
20
+ }
21
+
22
+ if (platform.ext === 'zip') {
23
+ await extractZip(archivePath, destDir);
24
+ } else {
25
+ await extractTarGz(archivePath, destDir);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Extract tar.gz archive, filtering for recall binary only.
31
+ * @param {string} archivePath - Path to tar.gz file
32
+ * @param {string} destDir - Destination directory
33
+ * @returns {Promise<void>}
34
+ */
35
+ async function extractTarGz(archivePath, destDir) {
36
+ const tar = require('tar');
37
+
38
+ await tar.extract({
39
+ file: archivePath,
40
+ cwd: destDir,
41
+ filter: (entryPath) => {
42
+ // Only extract the recall binary, skip README etc.
43
+ const basename = path.basename(entryPath);
44
+ return basename === 'recall' || basename === 'recall.exe';
45
+ }
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Extract zip archive, filtering for recall binary only.
51
+ * @param {string} archivePath - Path to zip file
52
+ * @param {string} destDir - Destination directory
53
+ * @returns {Promise<void>}
54
+ */
55
+ async function extractZip(archivePath, destDir) {
56
+ const AdmZip = require('adm-zip');
57
+ const zip = new AdmZip(archivePath);
58
+
59
+ zip.getEntries().forEach((entry) => {
60
+ const basename = path.basename(entry.entryName);
61
+ if (basename === 'recall' || basename === 'recall.exe') {
62
+ zip.extractEntryTo(entry, destDir, false, true);
63
+ }
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Set executable permissions on the binary (Unix only).
69
+ * @param {string} binaryPath - Path to binary
70
+ */
71
+ function setExecutable(binaryPath) {
72
+ if (process.platform !== 'win32') {
73
+ fs.chmodSync(binaryPath, 0o755);
74
+ }
75
+ }
76
+
77
+ module.exports = { extract, setExecutable };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Platform detection for binary downloads.
3
+ * Maps Node.js platform/arch to GoReleaser naming conventions.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const PLATFORM_MAP = {
9
+ darwin: 'darwin',
10
+ linux: 'linux',
11
+ win32: 'windows'
12
+ };
13
+
14
+ const ARCH_MAP = {
15
+ x64: 'amd64',
16
+ arm64: 'arm64'
17
+ };
18
+
19
+ /**
20
+ * Get the current platform info for binary download.
21
+ * @returns {{ os: string, arch: string, ext: string }}
22
+ */
23
+ function getPlatform() {
24
+ const platform = process.platform;
25
+ const arch = process.arch;
26
+
27
+ const os = PLATFORM_MAP[platform];
28
+ if (!os) {
29
+ throw new Error(`Unsupported platform: ${platform}. Supported: darwin, linux, win32`);
30
+ }
31
+
32
+ const goArch = ARCH_MAP[arch];
33
+ if (!goArch) {
34
+ throw new Error(`Unsupported architecture: ${arch}. Supported: x64, arm64`);
35
+ }
36
+
37
+ // Windows arm64 is not supported by GoReleaser config
38
+ if (os === 'windows' && goArch === 'arm64') {
39
+ throw new Error('Windows ARM64 is not currently supported. Please use x64.');
40
+ }
41
+
42
+ const ext = os === 'windows' ? 'zip' : 'tar.gz';
43
+
44
+ return { os, arch: goArch, ext };
45
+ }
46
+
47
+ /**
48
+ * Construct the GitHub release download URL.
49
+ * @param {string} version - Package version (e.g., "1.2.3")
50
+ * @param {{ os: string, arch: string, ext: string }} platform
51
+ * @returns {string}
52
+ */
53
+ function getDownloadUrl(version, platform) {
54
+ const { os, arch, ext } = platform;
55
+ const filename = `recall_${version}_${os}_${arch}.${ext}`;
56
+ return `https://github.com/hyperengineering/recall/releases/download/v${version}/${filename}`;
57
+ }
58
+
59
+ /**
60
+ * Get the binary filename for the current platform.
61
+ * @param {{ os: string }} platform
62
+ * @returns {string}
63
+ */
64
+ function getBinaryName(platform) {
65
+ return platform.os === 'windows' ? 'recall.exe' : 'recall';
66
+ }
67
+
68
+ module.exports = { getPlatform, getDownloadUrl, getBinaryName };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Postinstall helper functions.
3
+ * Provides testable utilities for the postinstall script.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+
10
+ /**
11
+ * Check if download should be skipped via environment variable.
12
+ * @returns {boolean}
13
+ */
14
+ function shouldSkipDownload() {
15
+ return process.env.RECALL_SKIP_DOWNLOAD === '1';
16
+ }
17
+
18
+ /**
19
+ * Get custom binary path from environment variable.
20
+ * @returns {string|null}
21
+ */
22
+ function getCustomBinaryPath() {
23
+ return process.env.RECALL_BINARY_PATH || null;
24
+ }
25
+
26
+ /**
27
+ * Check if binary exists at the given path.
28
+ * @param {string} binaryPath - Path to check
29
+ * @returns {boolean}
30
+ */
31
+ function binaryExists(binaryPath) {
32
+ return fs.existsSync(binaryPath);
33
+ }
34
+
35
+ /**
36
+ * Print manual installation instructions.
37
+ * @param {Error} err - The error that occurred
38
+ */
39
+ function printManualInstructions(err) {
40
+ console.error('\n');
41
+ console.error('Failed to install recall binary:');
42
+ console.error(err.message);
43
+ console.error('\n');
44
+ console.error('Manual installation options:');
45
+ console.error(' 1. Homebrew (macOS/Linux): brew install hyperengineering/tap/recall');
46
+ console.error(' 2. Download from: https://github.com/hyperengineering/recall/releases');
47
+ console.error(' 3. Go install: go install github.com/hyperengineering/recall/cmd/recall@latest');
48
+ console.error('\n');
49
+ console.error('To skip this download, set RECALL_SKIP_DOWNLOAD=1');
50
+ console.error('To use a custom binary, set RECALL_BINARY_PATH=/path/to/recall');
51
+ console.error('\n');
52
+ }
53
+
54
+ /**
55
+ * Format download progress as percentage.
56
+ * @param {number} received - Bytes received
57
+ * @param {number} total - Total bytes
58
+ * @returns {number} - Percentage (0-100)
59
+ */
60
+ function formatProgress(received, total) {
61
+ if (total === 0) return 0;
62
+ return Math.round((received / total) * 100);
63
+ }
64
+
65
+ module.exports = {
66
+ shouldSkipDownload,
67
+ getCustomBinaryPath,
68
+ binaryExists,
69
+ printManualInstructions,
70
+ formatProgress
71
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@hyperengineering/recall",
3
+ "version": "1.2.0",
4
+ "description": "CLI for managing experiential lore from AI agent workflows",
5
+ "keywords": [
6
+ "recall",
7
+ "lore",
8
+ "ai",
9
+ "mcp",
10
+ "cli"
11
+ ],
12
+ "homepage": "https://github.com/hyperengineering/recall",
13
+ "bugs": {
14
+ "url": "https://github.com/hyperengineering/recall/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/hyperengineering/recall.git",
19
+ "directory": "npm"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Hyperengineering",
23
+ "bin": {
24
+ "recall": "./bin/recall.js"
25
+ },
26
+ "scripts": {
27
+ "postinstall": "node scripts/postinstall.js",
28
+ "test": "node --test test/"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "os": [
34
+ "darwin",
35
+ "linux",
36
+ "win32"
37
+ ],
38
+ "cpu": [
39
+ "x64",
40
+ "arm64"
41
+ ],
42
+ "files": [
43
+ "bin/",
44
+ "lib/",
45
+ "scripts/",
46
+ "vendor/",
47
+ "README.md"
48
+ ],
49
+ "dependencies": {
50
+ "adm-zip": "^0.5.16",
51
+ "tar": "^7.4.3"
52
+ }
53
+ }
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script for @hyperengineering/recall.
5
+ * Downloads and installs the recall binary for the current platform.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { getPlatform, getDownloadUrl, getBinaryName } = require('../lib/platform');
13
+ const { download } = require('../lib/download');
14
+ const { extract, setExecutable } = require('../lib/extract');
15
+ const {
16
+ shouldSkipDownload,
17
+ getCustomBinaryPath,
18
+ binaryExists,
19
+ printManualInstructions,
20
+ formatProgress
21
+ } = require('../lib/postinstall');
22
+
23
+ const packageJson = require('../package.json');
24
+ const VERSION = packageJson.version;
25
+
26
+ const VENDOR_DIR = path.join(__dirname, '..', 'vendor');
27
+ const CACHE_DIR = path.join(__dirname, '..', '.cache');
28
+
29
+ async function main() {
30
+ // Check for skip flag
31
+ if (shouldSkipDownload()) {
32
+ console.log('RECALL_SKIP_DOWNLOAD is set, skipping binary download.');
33
+ return;
34
+ }
35
+
36
+ // Check for custom binary path
37
+ const customPath = getCustomBinaryPath();
38
+ if (customPath) {
39
+ console.log(`Using custom binary path: ${customPath}`);
40
+ return;
41
+ }
42
+
43
+ try {
44
+ const platform = getPlatform();
45
+ const binaryName = getBinaryName(platform);
46
+ const binaryPath = path.join(VENDOR_DIR, binaryName);
47
+
48
+ // Check if binary already exists
49
+ if (binaryExists(binaryPath)) {
50
+ console.log(`recall binary already exists at ${binaryPath}`);
51
+ return;
52
+ }
53
+
54
+ console.log(`Installing recall v${VERSION} for ${platform.os}/${platform.arch}...`);
55
+
56
+ // Create directories
57
+ if (!fs.existsSync(VENDOR_DIR)) {
58
+ fs.mkdirSync(VENDOR_DIR, { recursive: true });
59
+ }
60
+ if (!fs.existsSync(CACHE_DIR)) {
61
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
62
+ }
63
+
64
+ // Download archive
65
+ const downloadUrl = getDownloadUrl(VERSION, platform);
66
+ const archiveName = `recall_${VERSION}_${platform.os}_${platform.arch}.${platform.ext}`;
67
+ const archivePath = path.join(CACHE_DIR, archiveName);
68
+
69
+ console.log(`Downloading from ${downloadUrl}...`);
70
+ await download(downloadUrl, archivePath, {
71
+ onProgress: (received, total) => {
72
+ const percent = formatProgress(received, total);
73
+ process.stdout.write(`\rDownloading: ${percent}%`);
74
+ }
75
+ });
76
+ console.log('\nDownload complete.');
77
+
78
+ // Extract binary
79
+ console.log('Extracting binary...');
80
+ await extract(archivePath, VENDOR_DIR, platform);
81
+
82
+ // Set executable permissions
83
+ setExecutable(binaryPath);
84
+
85
+ // Verify binary exists
86
+ if (!binaryExists(binaryPath)) {
87
+ throw new Error(`Binary not found after extraction: ${binaryPath}`);
88
+ }
89
+
90
+ // Clean up archive
91
+ try {
92
+ fs.unlinkSync(archivePath);
93
+ } catch {
94
+ // Ignore cleanup errors
95
+ }
96
+
97
+ console.log(`recall v${VERSION} installed successfully!`);
98
+ console.log(`Binary location: ${binaryPath}`);
99
+
100
+ } catch (err) {
101
+ printManualInstructions(err);
102
+
103
+ // Don't fail npm install - just warn
104
+ process.exit(0);
105
+ }
106
+ }
107
+
108
+ main();
File without changes