crispy-recall 0.1.1 → 0.1.3

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.
@@ -4594,6 +4594,11 @@ async function startServer() {
4594
4594
  "-ub",
4595
4595
  "8192",
4596
4596
  // micro-batch (ubatch) — also defaults to 512, must be raised
4597
+ // Flash Attention: the attention compute buffer is otherwise O(n²) in the
4598
+ // 8192-token batch (~3.7 GB on GPU), which OOMs cards <6 GB and forces CPU.
4599
+ // -fa makes it O(n) (~0.8 GB → ~2.9 GB total), so 4 GB cards run on GPU,
4600
+ // with numerically identical embeddings (verified cosine 1.0 vs non-FA).
4601
+ "-fa",
4597
4602
  // nomic-embed-text-v1.5 is trained at 2048 ctx and uses Dynamic NTK-aware
4598
4603
  // RoPE scaling to extend to 8192. Without these flags, newer llama.cpp
4599
4604
  // (b9253+) refuses inputs >2048 tokens, and older versions silently
@@ -4801,6 +4806,9 @@ async function embedViaProcess(texts, modelPath) {
4801
4806
  "array",
4802
4807
  "-c",
4803
4808
  "8192",
4809
+ // Flash Attention — see startServer: O(n) attention buffer instead of
4810
+ // O(n²), so the one-shot path also fits cards <6 GB. Identical embeddings.
4811
+ "-fa",
4804
4812
  // Match the YaRN flags on the llama-server batch path — nomic-embed-text-v1.5
4805
4813
  // is trained at 2048 ctx and extends to 8192 via Dynamic NTK-aware RoPE.
4806
4814
  // Without these, newer llama.cpp (b9253+) refuses inputs >2048 tokens here
@@ -5552,6 +5560,9 @@ function normalizePath(p) {
5552
5560
  return normalized;
5553
5561
  }
5554
5562
 
5563
+ // src/recall/message-ingest.ts
5564
+ init_log();
5565
+
5555
5566
  // src/adapters/claude/jsonl-reader.ts
5556
5567
  var fs = __toESM(require("fs"));
5557
5568
  init_log();
@@ -5962,8 +5973,7 @@ function emitFunctionCall(payload, base, outputIndex, _counter) {
5962
5973
  const outputRecord = outputIndex.get(callId);
5963
5974
  if (outputRecord) {
5964
5975
  const outputPayload = outputRecord.payload;
5965
- const rawOutput = outputPayload.output;
5966
- const { exitCode, body } = parseExecOutputHeader(rawOutput);
5976
+ const { exitCode, body } = parseExecOutputHeader(outputPayload.output);
5967
5977
  const isError = exitCode !== 0;
5968
5978
  const toolResult = {
5969
5979
  type: "tool_result",
@@ -6101,7 +6111,7 @@ function emitApplyPatch(callId, input, base, outputIndex) {
6101
6111
  return entries;
6102
6112
  }
6103
6113
  function buildCustomToolResult(parentUuid, callId, outputRecord, base) {
6104
- const rawOutput = outputRecord.payload.output;
6114
+ const rawOutput = coerceOutputText(outputRecord.payload.output);
6105
6115
  let content;
6106
6116
  let isError = false;
6107
6117
  try {
@@ -6182,7 +6192,7 @@ function emitOrphanedOutput(payload, subtype, base, outputIndex, _counter) {
6182
6192
  return [];
6183
6193
  if (!outputIndex.has(callId))
6184
6194
  return [];
6185
- const rawOutput = payload.output;
6195
+ const rawOutput = coerceOutputText(payload.output);
6186
6196
  let content;
6187
6197
  let isError = false;
6188
6198
  if (subtype === "function_call_output") {
@@ -6295,7 +6305,18 @@ function mapFunctionCall(name, args) {
6295
6305
  return { toolName: name, toolInput: args };
6296
6306
  }
6297
6307
  }
6298
- function parseExecOutputHeader(output) {
6308
+ function coerceOutputText(output) {
6309
+ if (typeof output === "string")
6310
+ return output;
6311
+ if (Array.isArray(output)) {
6312
+ return output.map(
6313
+ (item) => item && typeof item === "object" && typeof item.text === "string" ? item.text : ""
6314
+ ).filter(Boolean).join("\n");
6315
+ }
6316
+ return "";
6317
+ }
6318
+ function parseExecOutputHeader(rawOutput) {
6319
+ const output = coerceOutputText(rawOutput);
6299
6320
  if (!output)
6300
6321
  return { exitCode: 0, body: "" };
6301
6322
  const currentMatch = output.match(/Process exited with code (\d+)/);
@@ -6460,6 +6481,39 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
6460
6481
  }
6461
6482
  var MAX_EMBED_CHARS = 14e3;
6462
6483
  var MAX_EMBED_BATCH = 10;
6484
+ var SAFE_EMBED_CHARS = 6e3;
6485
+ async function embedRowsResilient(rows) {
6486
+ const { embedBatch: embedBatch2 } = await Promise.resolve().then(() => (init_embedder(), embedder_exports));
6487
+ const { quantizeToQ8: quantizeToQ82, computeNorm: computeNorm2 } = await Promise.resolve().then(() => (init_quantize(), quantize_exports));
6488
+ const toRecord = (messageId, f32) => {
6489
+ const { q8, scale } = quantizeToQ82(f32);
6490
+ return { messageId, embeddingQ8: q8, norm: computeNorm2(f32), quantScale: scale };
6491
+ };
6492
+ try {
6493
+ const vectors = await embedBatch2(rows.map((r) => r.text));
6494
+ return rows.map((r, j) => toRecord(r.messageId, vectors[j]));
6495
+ } catch {
6496
+ const records = [];
6497
+ for (const r of rows) {
6498
+ try {
6499
+ const [v] = await embedBatch2([r.text]);
6500
+ records.push(toRecord(r.messageId, v));
6501
+ } catch {
6502
+ try {
6503
+ const [v] = await embedBatch2([r.text.slice(0, SAFE_EMBED_CHARS)]);
6504
+ records.push(toRecord(r.messageId, v));
6505
+ } catch (err) {
6506
+ log({
6507
+ source: "recall:embed",
6508
+ level: "warn",
6509
+ summary: `skipped message ${r.messageId} (embed failed even at ${SAFE_EMBED_CHARS} chars): ${err.message}`
6510
+ });
6511
+ }
6512
+ }
6513
+ }
6514
+ return records;
6515
+ }
6516
+ }
6463
6517
  async function embedSessionMessages(sessionId, force) {
6464
6518
  const d = getDb(dbPath());
6465
6519
  const rows = d.all(
@@ -6484,22 +6538,7 @@ async function embedSessionMessages(sessionId, force) {
6484
6538
  if (validRows.length > MAX_EMBED_BATCH) {
6485
6539
  validRows.length = MAX_EMBED_BATCH;
6486
6540
  }
6487
- const { embedBatch: embedBatch2 } = await Promise.resolve().then(() => (init_embedder(), embedder_exports));
6488
- const { quantizeToQ8: quantizeToQ82, computeNorm: computeNorm2 } = await Promise.resolve().then(() => (init_quantize(), quantize_exports));
6489
- const texts = validRows.map((r) => r.text);
6490
- const vectors = await embedBatch2(texts);
6491
- const records = [];
6492
- for (let j = 0; j < validRows.length; j++) {
6493
- const f32 = vectors[j];
6494
- const { q8, scale } = quantizeToQ82(f32);
6495
- const norm = computeNorm2(f32);
6496
- records.push({
6497
- messageId: validRows[j].messageId,
6498
- embeddingQ8: q8,
6499
- norm,
6500
- quantScale: scale
6501
- });
6502
- }
6541
+ const records = await embedRowsResilient(validRows);
6503
6542
  insertMessageVectors(records);
6504
6543
  return records.length;
6505
6544
  }
@@ -6518,22 +6557,7 @@ async function embedMessageBatch(messages) {
6518
6557
  }
6519
6558
  if (truncated.length === 0)
6520
6559
  return 0;
6521
- const { embedBatch: embedBatch2 } = await Promise.resolve().then(() => (init_embedder(), embedder_exports));
6522
- const { quantizeToQ8: quantizeToQ82, computeNorm: computeNorm2 } = await Promise.resolve().then(() => (init_quantize(), quantize_exports));
6523
- const texts = truncated.map((r) => r.text);
6524
- const vectors = await embedBatch2(texts);
6525
- const records = [];
6526
- for (let j = 0; j < truncated.length; j++) {
6527
- const f32 = vectors[j];
6528
- const { q8, scale } = quantizeToQ82(f32);
6529
- const norm = computeNorm2(f32);
6530
- records.push({
6531
- messageId: truncated[j].messageId,
6532
- embeddingQ8: q8,
6533
- norm,
6534
- quantScale: scale
6535
- });
6536
- }
6560
+ const records = await embedRowsResilient(truncated);
6537
6561
  insertMessageVectors(records);
6538
6562
  return records.length;
6539
6563
  }
package/dist/recall.js CHANGED
@@ -3268,6 +3268,11 @@ async function startServer() {
3268
3268
  "-ub",
3269
3269
  "8192",
3270
3270
  // micro-batch (ubatch) — also defaults to 512, must be raised
3271
+ // Flash Attention: the attention compute buffer is otherwise O(n²) in the
3272
+ // 8192-token batch (~3.7 GB on GPU), which OOMs cards <6 GB and forces CPU.
3273
+ // -fa makes it O(n) (~0.8 GB → ~2.9 GB total), so 4 GB cards run on GPU,
3274
+ // with numerically identical embeddings (verified cosine 1.0 vs non-FA).
3275
+ "-fa",
3271
3276
  // nomic-embed-text-v1.5 is trained at 2048 ctx and uses Dynamic NTK-aware
3272
3277
  // RoPE scaling to extend to 8192. Without these flags, newer llama.cpp
3273
3278
  // (b9253+) refuses inputs >2048 tokens, and older versions silently
@@ -3475,6 +3480,9 @@ async function embedViaProcess(texts, modelPath) {
3475
3480
  "array",
3476
3481
  "-c",
3477
3482
  "8192",
3483
+ // Flash Attention — see startServer: O(n) attention buffer instead of
3484
+ // O(n²), so the one-shot path also fits cards <6 GB. Identical embeddings.
3485
+ "-fa",
3478
3486
  // Match the YaRN flags on the llama-server batch path — nomic-embed-text-v1.5
3479
3487
  // is trained at 2048 ctx and extends to 8192 via Dynamic NTK-aware RoPE.
3480
3488
  // Without these, newer llama.cpp (b9253+) refuses inputs >2048 tokens here
@@ -13157,8 +13165,7 @@ function emitFunctionCall(payload, base, outputIndex, _counter) {
13157
13165
  const outputRecord = outputIndex.get(callId);
13158
13166
  if (outputRecord) {
13159
13167
  const outputPayload = outputRecord.payload;
13160
- const rawOutput = outputPayload.output;
13161
- const { exitCode, body } = parseExecOutputHeader(rawOutput);
13168
+ const { exitCode, body } = parseExecOutputHeader(outputPayload.output);
13162
13169
  const isError = exitCode !== 0;
13163
13170
  const toolResult = {
13164
13171
  type: "tool_result",
@@ -13296,7 +13303,7 @@ function emitApplyPatch(callId, input, base, outputIndex) {
13296
13303
  return entries;
13297
13304
  }
13298
13305
  function buildCustomToolResult(parentUuid, callId, outputRecord, base) {
13299
- const rawOutput = outputRecord.payload.output;
13306
+ const rawOutput = coerceOutputText(outputRecord.payload.output);
13300
13307
  let content;
13301
13308
  let isError = false;
13302
13309
  try {
@@ -13377,7 +13384,7 @@ function emitOrphanedOutput(payload, subtype, base, outputIndex, _counter) {
13377
13384
  return [];
13378
13385
  if (!outputIndex.has(callId))
13379
13386
  return [];
13380
- const rawOutput = payload.output;
13387
+ const rawOutput = coerceOutputText(payload.output);
13381
13388
  let content;
13382
13389
  let isError = false;
13383
13390
  if (subtype === "function_call_output") {
@@ -13490,7 +13497,18 @@ function mapFunctionCall(name, args) {
13490
13497
  return { toolName: name, toolInput: args };
13491
13498
  }
13492
13499
  }
13493
- function parseExecOutputHeader(output) {
13500
+ function coerceOutputText(output) {
13501
+ if (typeof output === "string")
13502
+ return output;
13503
+ if (Array.isArray(output)) {
13504
+ return output.map(
13505
+ (item) => item && typeof item === "object" && typeof item.text === "string" ? item.text : ""
13506
+ ).filter(Boolean).join("\n");
13507
+ }
13508
+ return "";
13509
+ }
13510
+ function parseExecOutputHeader(rawOutput) {
13511
+ const output = coerceOutputText(rawOutput);
13494
13512
  if (!output)
13495
13513
  return { exitCode: 0, body: "" };
13496
13514
  const currentMatch = output.match(/Process exited with code (\d+)/);
@@ -13658,6 +13676,38 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
13658
13676
  skipped: false
13659
13677
  };
13660
13678
  }
13679
+ async function embedRowsResilient(rows) {
13680
+ const { embedBatch: embedBatch2 } = await Promise.resolve().then(() => (init_embedder(), embedder_exports));
13681
+ const { quantizeToQ8: quantizeToQ82, computeNorm: computeNorm2 } = await Promise.resolve().then(() => (init_quantize(), quantize_exports));
13682
+ const toRecord = (messageId, f32) => {
13683
+ const { q8, scale } = quantizeToQ82(f32);
13684
+ return { messageId, embeddingQ8: q8, norm: computeNorm2(f32), quantScale: scale };
13685
+ };
13686
+ try {
13687
+ const vectors = await embedBatch2(rows.map((r2) => r2.text));
13688
+ return rows.map((r2, j2) => toRecord(r2.messageId, vectors[j2]));
13689
+ } catch {
13690
+ const records = [];
13691
+ for (const r2 of rows) {
13692
+ try {
13693
+ const [v2] = await embedBatch2([r2.text]);
13694
+ records.push(toRecord(r2.messageId, v2));
13695
+ } catch {
13696
+ try {
13697
+ const [v2] = await embedBatch2([r2.text.slice(0, SAFE_EMBED_CHARS)]);
13698
+ records.push(toRecord(r2.messageId, v2));
13699
+ } catch (err) {
13700
+ log({
13701
+ source: "recall:embed",
13702
+ level: "warn",
13703
+ summary: `skipped message ${r2.messageId} (embed failed even at ${SAFE_EMBED_CHARS} chars): ${err.message}`
13704
+ });
13705
+ }
13706
+ }
13707
+ }
13708
+ return records;
13709
+ }
13710
+ }
13661
13711
  async function embedMessageBatch(messages) {
13662
13712
  if (messages.length === 0)
13663
13713
  return 0;
@@ -13673,26 +13723,11 @@ async function embedMessageBatch(messages) {
13673
13723
  }
13674
13724
  if (truncated.length === 0)
13675
13725
  return 0;
13676
- const { embedBatch: embedBatch2 } = await Promise.resolve().then(() => (init_embedder(), embedder_exports));
13677
- const { quantizeToQ8: quantizeToQ82, computeNorm: computeNorm2 } = await Promise.resolve().then(() => (init_quantize(), quantize_exports));
13678
- const texts = truncated.map((r2) => r2.text);
13679
- const vectors = await embedBatch2(texts);
13680
- const records = [];
13681
- for (let j2 = 0; j2 < truncated.length; j2++) {
13682
- const f32 = vectors[j2];
13683
- const { q8, scale } = quantizeToQ82(f32);
13684
- const norm = computeNorm2(f32);
13685
- records.push({
13686
- messageId: truncated[j2].messageId,
13687
- embeddingQ8: q8,
13688
- norm,
13689
- quantScale: scale
13690
- });
13691
- }
13726
+ const records = await embedRowsResilient(truncated);
13692
13727
  insertMessageVectors(records);
13693
13728
  return records.length;
13694
13729
  }
13695
- var MAX_EMBED_CHARS;
13730
+ var MAX_EMBED_CHARS, SAFE_EMBED_CHARS;
13696
13731
  var init_message_ingest = __esm({
13697
13732
  "src/recall/message-ingest.ts"() {
13698
13733
  "use strict";
@@ -13701,11 +13736,13 @@ var init_message_ingest = __esm({
13701
13736
  init_db();
13702
13737
  init_paths();
13703
13738
  init_url_path_resolver();
13739
+ init_log();
13704
13740
  init_jsonl_reader();
13705
13741
  init_claude_entry_adapter();
13706
13742
  init_codex_jsonl_reader();
13707
13743
  init_codex_jsonl_adapter();
13708
13744
  MAX_EMBED_CHARS = 14e3;
13745
+ SAFE_EMBED_CHARS = 6e3;
13709
13746
  }
13710
13747
  });
13711
13748
 
@@ -14924,8 +14961,8 @@ function cudaAssetUrl() {
14924
14961
  const tag = version ? `download/v${version}` : "latest/download";
14925
14962
  return `https://github.com/${owner}/${repo}/releases/${tag}/${CUDA_ASSET_NAME}`;
14926
14963
  }
14927
- function binCudaDir() {
14928
- return (0, import_node_path9.join)(recallRoot(), "bin-cuda");
14964
+ function cudaBackendLib() {
14965
+ return (0, import_node_path9.join)(binDir(), "libggml-cuda.so");
14929
14966
  }
14930
14967
  async function readVram() {
14931
14968
  try {
@@ -14957,7 +14994,7 @@ async function detectGpu(opts = {}) {
14957
14994
  if (p === "win32" && a3 === "x64") {
14958
14995
  return { ...base, cudaAvailable: "prebuilt", plannedMode: "gpu" };
14959
14996
  }
14960
- if (p === "linux" && a3 === "x64" && (0, import_node_fs9.existsSync)(binCudaDir())) {
14997
+ if (p === "linux" && a3 === "x64" && (0, import_node_fs9.existsSync)(cudaBackendLib())) {
14961
14998
  return { ...base, cudaAvailable: "reuse-existing", plannedMode: "gpu" };
14962
14999
  }
14963
15000
  if (p === "linux" && a3 === "x64") {
@@ -14966,10 +15003,7 @@ async function detectGpu(opts = {}) {
14966
15003
  return { ...base, cudaAvailable: "none", plannedMode: "cpu" };
14967
15004
  }
14968
15005
  function stderrIndicatesOffload(stderr) {
14969
- if (/ggml_cuda_init/.test(stderr))
14970
- return true;
14971
- const m2 = /offloaded\s+(\d+)\s*\/\s*\d+\s+layers?\s+to\s+GPU/i.exec(stderr);
14972
- return m2 ? Number(m2[1]) > 0 : false;
15006
+ return /ggml_cuda_init|found\s+\d+\s+CUDA\s+devices|loaded CUDA backend|using device CUDA|CUDA0[^\n]*buffer/i.test(stderr);
14973
15007
  }
14974
15008
  async function defaultProbe(args) {
14975
15009
  const { binaryPath: binaryPath2, modelPath, libDir, ngl, platform: platform2 } = args;
@@ -14990,6 +15024,12 @@ async function defaultProbe(args) {
14990
15024
  "array",
14991
15025
  "-c",
14992
15026
  "8192",
15027
+ // Probe with the SAME flags the real embed server uses (esp. -fa). Without
15028
+ // Flash Attention the probe allocates the ~3.7 GB O(n²) attention buffer and
15029
+ // OOMs on a 4 GB card — failing the probe and falling back to CPU even
15030
+ // though the actual -fa server (~2.9 GB) would have fit. Probe must mirror
15031
+ // runtime to gate GPU adoption correctly.
15032
+ "-fa",
14993
15033
  "--rope-scaling",
14994
15034
  "yarn",
14995
15035
  "--rope-freq-scale",
@@ -15044,7 +15084,7 @@ async function runGpuPhase(opts = {}) {
15044
15084
  return { mode, libDir: libDir2, ngl: mode === "gpu" ? DEFAULT_NGL : 0, cudaAvailable: info.cudaAvailable, ...reason ? { reason } : {} };
15045
15085
  };
15046
15086
  if (info.plannedMode === "cpu") {
15047
- const reason = info.detected && info.vendor === "nvidia" ? "NVIDIA GPU detected but no usable CUDA libs (no prebuilt for this platform; no ~/.recall/bin-cuda/ build present)" : void 0;
15087
+ const reason = info.detected && info.vendor === "nvidia" ? "NVIDIA GPU detected but no usable CUDA libs (no prebuilt for this platform; no ~/.recall/bin/libggml-cuda.so present)" : void 0;
15048
15088
  return persist("cpu", null, reason);
15049
15089
  }
15050
15090
  if (info.cudaAvailable === "metal") {
@@ -15052,7 +15092,7 @@ async function runGpuPhase(opts = {}) {
15052
15092
  }
15053
15093
  let libDir;
15054
15094
  if (info.cudaAvailable === "reuse-existing") {
15055
- libDir = binCudaDir();
15095
+ libDir = binDir();
15056
15096
  } else if (info.cudaAvailable === "prebuilt" && p === "win32") {
15057
15097
  libDir = binDir();
15058
15098
  } else if (info.cudaAvailable === "prebuilt") {
@@ -15087,8 +15127,8 @@ async function runGpuPhase(opts = {}) {
15087
15127
  }
15088
15128
  }
15089
15129
  async function defaultStage(args) {
15090
- const target = binCudaDir();
15091
- const libPath = (0, import_node_path9.join)(target, "libggml-cuda.so");
15130
+ const target = binDir();
15131
+ const libPath = cudaBackendLib();
15092
15132
  if ((0, import_node_fs9.existsSync)(libPath))
15093
15133
  return target;
15094
15134
  if (args.offline)
package/dist/stop-hook.js CHANGED
@@ -1807,6 +1807,9 @@ function normalizePath(p) {
1807
1807
  return normalized;
1808
1808
  }
1809
1809
 
1810
+ // src/recall/message-ingest.ts
1811
+ init_log();
1812
+
1810
1813
  // src/adapters/claude/jsonl-reader.ts
1811
1814
  var fs = __toESM(require("fs"));
1812
1815
  init_log();
@@ -2217,8 +2220,7 @@ function emitFunctionCall(payload, base, outputIndex, _counter) {
2217
2220
  const outputRecord = outputIndex.get(callId);
2218
2221
  if (outputRecord) {
2219
2222
  const outputPayload = outputRecord.payload;
2220
- const rawOutput = outputPayload.output;
2221
- const { exitCode, body } = parseExecOutputHeader(rawOutput);
2223
+ const { exitCode, body } = parseExecOutputHeader(outputPayload.output);
2222
2224
  const isError = exitCode !== 0;
2223
2225
  const toolResult = {
2224
2226
  type: "tool_result",
@@ -2356,7 +2358,7 @@ function emitApplyPatch(callId, input, base, outputIndex) {
2356
2358
  return entries;
2357
2359
  }
2358
2360
  function buildCustomToolResult(parentUuid, callId, outputRecord, base) {
2359
- const rawOutput = outputRecord.payload.output;
2361
+ const rawOutput = coerceOutputText(outputRecord.payload.output);
2360
2362
  let content;
2361
2363
  let isError = false;
2362
2364
  try {
@@ -2437,7 +2439,7 @@ function emitOrphanedOutput(payload, subtype, base, outputIndex, _counter) {
2437
2439
  return [];
2438
2440
  if (!outputIndex.has(callId))
2439
2441
  return [];
2440
- const rawOutput = payload.output;
2442
+ const rawOutput = coerceOutputText(payload.output);
2441
2443
  let content;
2442
2444
  let isError = false;
2443
2445
  if (subtype === "function_call_output") {
@@ -2550,7 +2552,18 @@ function mapFunctionCall(name, args) {
2550
2552
  return { toolName: name, toolInput: args };
2551
2553
  }
2552
2554
  }
2553
- function parseExecOutputHeader(output) {
2555
+ function coerceOutputText(output) {
2556
+ if (typeof output === "string")
2557
+ return output;
2558
+ if (Array.isArray(output)) {
2559
+ return output.map(
2560
+ (item) => item && typeof item === "object" && typeof item.text === "string" ? item.text : ""
2561
+ ).filter(Boolean).join("\n");
2562
+ }
2563
+ return "";
2564
+ }
2565
+ function parseExecOutputHeader(rawOutput) {
2566
+ const output = coerceOutputText(rawOutput);
2554
2567
  if (!output)
2555
2568
  return { exitCode: 0, body: "" };
2556
2569
  const currentMatch = output.match(/Process exited with code (\d+)/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crispy-recall",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local session transcript memory for Claude Code and Codex — search past sessions with FTS5 + semantic vectors.",
5
5
  "license": "MIT",
6
6
  "author": "Sylvester Wong",