@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 +178 -0
- package/bin/recall.cmd +6 -0
- package/bin/recall.js +64 -0
- package/lib/download.js +123 -0
- package/lib/extract.js +77 -0
- package/lib/platform.js +68 -0
- package/lib/postinstall.js +71 -0
- package/package.json +53 -0
- package/scripts/postinstall.js +108 -0
- package/vendor/.gitkeep +0 -0
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
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();
|
package/lib/download.js
ADDED
|
@@ -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 };
|
package/lib/platform.js
ADDED
|
@@ -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();
|
package/vendor/.gitkeep
ADDED
|
File without changes
|