crispy-recall 0.2.0 → 0.2.1

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 CHANGED
@@ -6,6 +6,22 @@ Searchable memory for your Claude Code and Codex sessions. Local, fast, no daemo
6
6
 
7
7
  A standalone spin-off of the recall feature from [Crispy](https://github.com/TheSylvester/crispy). See the parent project for the broader multi-agent orchestration GUI.
8
8
 
9
+ ## What's new in 0.2.1
10
+
11
+ - **macOS install support.** 0.2.0's native SQLite engine shipped without ever
12
+ running on Apple hardware; 0.2.1 fixes the macOS install path and validates it
13
+ end-to-end on Apple Silicon. Fixes: a pre-flight macOS-version check (the
14
+ bundled llama.cpp embedding binaries need macOS 14+ on arm64, 13.7+ on Intel),
15
+ a Node path that stays valid across `brew upgrade node` during in-place
16
+ upgrades (it previously pinned a Homebrew Cellar path that a later upgrade
17
+ removes), an up-front rejection of Node 23 (no prebuilt SQLite binding) with an
18
+ explanation instead of a late failure, and a load-check of the staged SQLite
19
+ binding before the installer trusts it. Non-macOS installs are unaffected.
20
+ - **`recall install` no longer hangs during setup.** Extraction of the embedding
21
+ binaries is now time-boxed with a fallback to the OS unzip tool
22
+ (`unzip`/`ditto`/`Expand-Archive`), so a stalled extract can no longer wedge
23
+ the installer.
24
+
9
25
  ## What's new in 0.2.0
10
26
 
11
27
  - **Native SQLite engine with real WAL.** The database now runs on
@@ -13,7 +29,8 @@ A standalone spin-off of the recall feature from [Crispy](https://github.com/The
13
29
  binding — which never actually engaged WAL and could silently corrupt the
14
30
  index when multiple processes wrote at once. That failure mode is eliminated
15
31
  at the root. `recall doctor` gains a SQLite-binding health section.
16
- **Node.js ≥ 22.16 is now required.**
32
+ **Node.js 22 LTS (≥ 22.16) or 24+ is now required** (Node 23 has no prebuilt
33
+ SQLite binding).
17
34
  - **Git provenance: `--commit` and `--blame`.** `recall --commit <hash>` lists
18
35
  the session(s) that produced a commit; `recall --blame <path>[:line[-line]]`
19
36
  traces a file or line range back to the conversations responsible. Matching
@@ -65,13 +82,19 @@ Run it in the environment where you actually use Claude Code: **WSL and
65
82
  Windows-native are separate installs.** If you use both, run the install in each
66
83
  — the installer only configures the environment it is invoked in.
67
84
 
85
+ > **macOS: use Node 22 LTS or 24+.** Node 23 has no prebuilt SQLite binding, so
86
+ > `npm install -g` would fall back to compiling it and fail without Xcode
87
+ > Command Line Tools. macOS 14+ (Apple Silicon) / 13.7+ (Intel) is required for
88
+ > the bundled semantic-embedding binaries.
89
+
68
90
  > **Don't install via `npx`.** recall must stay resident. `npx` runs from an
69
91
  > ephemeral cache and leaves no `recall` command on your PATH, so follow-up
70
92
  > commands (`recall status` / `doctor` / `repair` / `uninstall`) and the
71
93
  > installed skill's command contract have nothing to call. Use the global
72
94
  > install above.
73
95
 
74
- Prerequisites: Node ≥ 22.16 and Claude Code installed. If Codex is detected
96
+ Prerequisites: Node 22 LTS (≥ 22.16) or 24+ Node 23 is unsupported (no
97
+ prebuilt SQLite binding) — and Claude Code installed. If Codex is detected
75
98
  (`~/.codex/` exists), recall installs the `recall` skill into Codex (so the
76
99
  agent can search) and you can index your Codex history with
77
100
  `recall backfill --vendor codex`. Real-time per-turn Codex indexing is **not
@@ -3466,10 +3466,18 @@ var require_extract_zip = __commonJS({
3466
3466
  }
3467
3467
  });
3468
3468
 
3469
+ // src/installer/stable-node.ts
3470
+ var init_stable_node = __esm({
3471
+ "src/installer/stable-node.ts"() {
3472
+ "use strict";
3473
+ }
3474
+ });
3475
+
3469
3476
  // src/installer/settings-merge.ts
3470
3477
  var init_settings_merge = __esm({
3471
3478
  "src/installer/settings-merge.ts"() {
3472
3479
  "use strict";
3480
+ init_stable_node();
3473
3481
  }
3474
3482
  });
3475
3483
 
@@ -3513,15 +3521,21 @@ var init_config = __esm({
3513
3521
  // src/recall/embedder.ts
3514
3522
  var embedder_exports = {};
3515
3523
  __export(embedder_exports, {
3524
+ LLAMA_RELEASE_TAG: () => LLAMA_RELEASE_TAG,
3525
+ MACOS_MIN_VERSION: () => MACOS_MIN_VERSION,
3526
+ buildUnzipInvocations: () => buildUnzipInvocations,
3516
3527
  disposeEmbedder: () => disposeEmbedder,
3517
3528
  embed: () => embed,
3518
3529
  embedBatch: () => embedBatch,
3519
3530
  ensureBinary: () => ensureBinary,
3520
3531
  ensureModel: () => ensureModel,
3532
+ extractArchive: () => extractArchive,
3521
3533
  getBinaryPath: () => getBinaryPath,
3522
3534
  getModelPath: () => getModelPath,
3523
3535
  hasNvidiaGpu: () => hasNvidiaGpu,
3524
- initEmbedder: () => initEmbedder
3536
+ initEmbedder: () => initEmbedder,
3537
+ installExtractedBinaries: () => installExtractedBinaries,
3538
+ withTimeout: () => withTimeout
3525
3539
  });
3526
3540
  function gpuRuntime() {
3527
3541
  if (_gpuRuntime === void 0) {
@@ -3695,6 +3709,82 @@ async function performBinaryDownload(binPath) {
3695
3709
  }
3696
3710
  throw new Error("No suitable llama-embedding binary found");
3697
3711
  }
3712
+ function withTimeout(promise, ms, label) {
3713
+ let timer;
3714
+ const timeout = new Promise((_, reject) => {
3715
+ timer = setTimeout(
3716
+ () => reject(new Error(`${label} timed out after ${Math.round(ms / 1e3)}s`)),
3717
+ ms
3718
+ );
3719
+ });
3720
+ promise.catch(() => {
3721
+ });
3722
+ return Promise.race([promise, timeout]).finally(() => {
3723
+ if (timer)
3724
+ clearTimeout(timer);
3725
+ });
3726
+ }
3727
+ function psQuote(p) {
3728
+ return p.replace(/'/g, "''");
3729
+ }
3730
+ function buildUnzipInvocations(plat, archivePath, dir) {
3731
+ if (plat === "win32") {
3732
+ return [{
3733
+ cmd: "powershell",
3734
+ args: [
3735
+ "-NoProfile",
3736
+ "-NonInteractive",
3737
+ "-Command",
3738
+ `Expand-Archive -LiteralPath '${psQuote(archivePath)}' -DestinationPath '${psQuote(dir)}' -Force`
3739
+ ]
3740
+ }];
3741
+ }
3742
+ const unzip = { cmd: "unzip", args: ["-o", "-q", archivePath, "-d", dir] };
3743
+ return plat === "darwin" ? [unzip, { cmd: "ditto", args: ["-x", "-k", archivePath, dir] }] : [unzip];
3744
+ }
3745
+ async function osUnzip(archivePath, dir) {
3746
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
3747
+ const invocations = buildUnzipInvocations((0, import_node_os2.platform)(), archivePath, dir);
3748
+ let lastErr;
3749
+ for (const { cmd, args } of invocations) {
3750
+ try {
3751
+ await execFileAsync(cmd, args, { timeout: EXTRACT_TIMEOUT_MS, windowsHide: true });
3752
+ return;
3753
+ } catch (err) {
3754
+ lastErr = err;
3755
+ }
3756
+ }
3757
+ throw lastErr instanceof Error ? lastErr : new Error(`Native unzip failed for ${archivePath}`);
3758
+ }
3759
+ async function extractArchive(archivePath, baseDir, assetName, timeoutMs = EXTRACT_TIMEOUT_MS) {
3760
+ try {
3761
+ await withTimeout((0, import_extract_zip.default)(archivePath, { dir: baseDir }), timeoutMs, `Extracting ${assetName}`);
3762
+ return baseDir;
3763
+ } catch (err) {
3764
+ const msg = err instanceof Error ? err.message : String(err);
3765
+ log({
3766
+ source: "recall-catchup",
3767
+ level: "warn",
3768
+ summary: `Bundled unzip failed for ${assetName} (${msg}); falling back to system unzip`
3769
+ });
3770
+ const fallbackDir = (0, import_node_path4.join)(baseDir, "__os_unzip__");
3771
+ await osUnzip(archivePath, fallbackDir);
3772
+ return fallbackDir;
3773
+ }
3774
+ }
3775
+ async function installExtractedBinaries(extractedDir, destDir = binDir()) {
3776
+ const buildBinDir = (0, import_node_path4.join)(extractedDir, "build", "bin");
3777
+ const sourceDir = (0, import_node_fs4.existsSync)(buildBinDir) ? buildBinDir : extractedDir;
3778
+ (0, import_node_fs4.mkdirSync)(destDir, { recursive: true });
3779
+ let copiedCount = 0;
3780
+ for (const file of await (0, import_promises.readdir)(sourceDir)) {
3781
+ if (!isWantedFile(file))
3782
+ continue;
3783
+ await (0, import_promises.cp)((0, import_node_path4.join)(sourceDir, file), (0, import_node_path4.join)(destDir, file), { force: true });
3784
+ copiedCount++;
3785
+ }
3786
+ return copiedCount;
3787
+ }
3698
3788
  async function downloadAndExtract(assetName, binPath) {
3699
3789
  const url = `https://github.com/ggml-org/llama.cpp/releases/download/${LLAMA_RELEASE_TAG}/${assetName}`;
3700
3790
  const isGpu = assetName.includes("cuda") || assetName.includes("vulkan");
@@ -3715,18 +3805,8 @@ async function downloadAndExtract(assetName, binPath) {
3715
3805
  const tmpExtractDir = (0, import_node_path4.join)((0, import_node_os2.tmpdir)(), `llama-extract-${Date.now()}`);
3716
3806
  (0, import_node_fs4.mkdirSync)(tmpExtractDir, { recursive: true });
3717
3807
  try {
3718
- await (0, import_extract_zip.default)(archivePath, { dir: tmpExtractDir });
3719
- const buildBinDir = (0, import_node_path4.join)(tmpExtractDir, "build", "bin");
3720
- const sourceDir = (0, import_node_fs4.existsSync)(buildBinDir) ? buildBinDir : tmpExtractDir;
3721
- let copiedCount = 0;
3722
- for (const file of await (0, import_promises.readdir)(sourceDir)) {
3723
- if (!isWantedFile(file))
3724
- continue;
3725
- const src = (0, import_node_path4.join)(sourceDir, file);
3726
- const dest = (0, import_node_path4.join)(binDir(), file);
3727
- await (0, import_promises.cp)(src, dest, { force: true });
3728
- copiedCount++;
3729
- }
3808
+ const extractedDir = await extractArchive(archivePath, tmpExtractDir, assetName);
3809
+ const copiedCount = await installExtractedBinaries(extractedDir);
3730
3810
  if (copiedCount === 0) {
3731
3811
  throw new Error(`No binaries found in archive ${assetName}`);
3732
3812
  }
@@ -4350,7 +4430,7 @@ async function embedBatchInner(texts) {
4350
4430
  async function disposeEmbedder() {
4351
4431
  await killServer();
4352
4432
  }
4353
- var import_node_child_process, import_node_fs4, import_promises, import_node_http, import_node_path4, import_node_os2, import_node_util, import_extract_zip, execFileAsync, _gpuRuntime, MODEL_FILENAME, MODEL_URL, EXPECTED_DIMS, BATCH_SEPARATOR, MAX_ARG_BYTES, LLAMA_RELEASE_TAG, BIN_NAME, SERVER_BIN_NAME, SERVER_SUPPORTED, SERVER_THRESHOLD, IDLE_TIMEOUT_MS, HEALTH_POLL_INTERVAL_MS, HEALTH_POLL_TIMEOUT_MS, HTTP_REQUEST_TIMEOUT_MS, SERVER_KILL_TIMEOUT_MS, SERVER_COOLDOWN_MS, embedQueue, embedRunning, binaryPath, downloadPromise, binaryDownloadPromise, serverProcess, activeSocketPath, idleTimer, serverStartPromise, serverCooldownUntil, serverRetryPromise, activeServerRequests, WANTED_FILES, WANTED_LIB_PATTERNS;
4433
+ var import_node_child_process, import_node_fs4, import_promises, import_node_http, import_node_path4, import_node_os2, import_node_util, import_extract_zip, execFileAsync, _gpuRuntime, MODEL_FILENAME, MODEL_URL, EXPECTED_DIMS, BATCH_SEPARATOR, MAX_ARG_BYTES, LLAMA_RELEASE_TAG, MACOS_MIN_VERSION, BIN_NAME, SERVER_BIN_NAME, SERVER_SUPPORTED, SERVER_THRESHOLD, IDLE_TIMEOUT_MS, HEALTH_POLL_INTERVAL_MS, HEALTH_POLL_TIMEOUT_MS, HTTP_REQUEST_TIMEOUT_MS, SERVER_KILL_TIMEOUT_MS, SERVER_COOLDOWN_MS, EXTRACT_TIMEOUT_MS, embedQueue, embedRunning, binaryPath, downloadPromise, binaryDownloadPromise, serverProcess, activeSocketPath, idleTimer, serverStartPromise, serverCooldownUntil, serverRetryPromise, activeServerRequests, WANTED_FILES, WANTED_LIB_PATTERNS;
4354
4434
  var init_embedder = __esm({
4355
4435
  "src/recall/embedder.ts"() {
4356
4436
  "use strict";
@@ -4372,6 +4452,7 @@ var init_embedder = __esm({
4372
4452
  BATCH_SEPARATOR = "<#sep#>";
4373
4453
  MAX_ARG_BYTES = (0, import_node_os2.platform)() === "win32" ? 0 : 1e5;
4374
4454
  LLAMA_RELEASE_TAG = "b5300";
4455
+ MACOS_MIN_VERSION = { arm64: "14.0", x64: "13.7" };
4375
4456
  BIN_NAME = (0, import_node_os2.platform)() === "win32" ? "llama-embedding.exe" : "llama-embedding";
4376
4457
  SERVER_BIN_NAME = (0, import_node_os2.platform)() === "win32" ? "llama-server.exe" : "llama-server";
4377
4458
  SERVER_SUPPORTED = (0, import_node_os2.platform)() !== "win32";
@@ -4382,6 +4463,7 @@ var init_embedder = __esm({
4382
4463
  HTTP_REQUEST_TIMEOUT_MS = 12e4;
4383
4464
  SERVER_KILL_TIMEOUT_MS = 5e3;
4384
4465
  SERVER_COOLDOWN_MS = 6e4;
4466
+ EXTRACT_TIMEOUT_MS = 12e4;
4385
4467
  embedQueue = [];
4386
4468
  embedRunning = false;
4387
4469
  binaryPath = null;
@@ -4681,7 +4763,10 @@ var BindingLoadError = class extends Error {
4681
4763
  constructor(dbPath2, cause) {
4682
4764
  super(
4683
4765
  `recall: failed to load the better-sqlite3 native binding while opening ${dbPath2}: ${cause.message}
4684
- The binding is ABI-locked to the Node it was built for \u2014 run \`recall doctor\` (usually fixed by re-running \`recall install\`).`
4766
+ The native SQLite binding is ABI-locked to the Node it was built for. This is usually one of:
4767
+ \u2022 recall is running under a different Node than the one npm installed it with (e.g. Homebrew node vs nvm) \u2014 reinstall with \`npm install -g crispy-recall\` using the node on your PATH, then \`recall install\`.
4768
+ \u2022 no prebuilt binary matched your Node version and it could not compile \u2014 use Node 22 LTS or 24+ (Node 23 has no prebuilt SQLite binding), and on macOS make sure Xcode Command Line Tools are installed (\`xcode-select --install\`).
4769
+ Run \`recall doctor\` for details.`
4685
4770
  );
4686
4771
  this.dbPath = dbPath2;
4687
4772
  this.cause = cause;