mnemospark 0.8.3 → 0.9.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
@@ -59,9 +59,11 @@ Use via `/mnemospark_cloud ...` in OpenClaw chat.
59
59
  ### Get a storage quote
60
60
 
61
61
  ```text
62
- /mnemospark_cloud price-storage --wallet-address <addr> --object-id <id> --object-id-hash <sha256> --gb <gb> --provider <provider> --region <region>
62
+ /mnemospark_cloud price-storage --wallet-address <addr> --object-id <id> --object-id-hash <sha256> --gb <gb> --provider aws --region us-east-1
63
63
  ```
64
64
 
65
+ Use other regions by changing `--provider` and `--region` (defaults: `aws` / `us-east-1`).
66
+
65
67
  ### Upload using quote
66
68
 
67
69
  ```text
package/dist/cli.js CHANGED
@@ -2973,10 +2973,12 @@ import {
2973
2973
  randomBytes as randomBytesNode,
2974
2974
  randomUUID as randomUUID3
2975
2975
  } from "crypto";
2976
- import { createReadStream as createReadStream2, statfsSync } from "fs";
2976
+ import { createReadStream as createReadStream2, createWriteStream as createWriteStream2, statfsSync } from "fs";
2977
2977
  import { lstat, mkdir as mkdir5, readFile as readFile3, readdir as readdir2, rm, stat as stat2, writeFile as writeFile3 } from "fs/promises";
2978
- import { homedir as homedir6 } from "os";
2978
+ import { homedir as homedir6, tmpdir } from "os";
2979
2979
  import { basename as basename2, dirname as dirname5, join as join8, resolve as resolve2 } from "path";
2980
+ import { Readable } from "stream";
2981
+ import { finished } from "stream/promises";
2980
2982
  import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
2981
2983
 
2982
2984
  // src/cloud-ls-format.ts
@@ -3374,6 +3376,7 @@ async function createCloudDatastore(homeDir) {
3374
3376
  addOperationsColumn("subagent_session_id", "TEXT");
3375
3377
  addOperationsColumn("timeout_seconds", "INTEGER");
3376
3378
  addOperationsColumn("cancel_requested_at", "TEXT");
3379
+ addOperationsColumn("result_text", "TEXT");
3377
3380
  nextDb.prepare(
3378
3381
  `INSERT INTO schema_migrations(version, applied_at)
3379
3382
  VALUES(?, ?)
@@ -3565,8 +3568,8 @@ async function createCloudDatastore(homeDir) {
3565
3568
  const ts = nowIso();
3566
3569
  const terminalStatuses = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "timed_out"]);
3567
3570
  db.prepare(
3568
- `INSERT INTO operations(operation_id, type, object_id, quote_id, trace_id, orchestrator, subagent_session_id, timeout_seconds, cancel_requested_at, status, error_code, error_message, started_at, finished_at, created_at, updated_at)
3569
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3571
+ `INSERT INTO operations(operation_id, type, object_id, quote_id, trace_id, orchestrator, subagent_session_id, timeout_seconds, cancel_requested_at, status, error_code, error_message, result_text, started_at, finished_at, created_at, updated_at)
3572
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3570
3573
  ON CONFLICT(operation_id) DO UPDATE SET
3571
3574
  type=excluded.type,
3572
3575
  object_id=COALESCE(excluded.object_id, operations.object_id),
@@ -3579,6 +3582,7 @@ async function createCloudDatastore(homeDir) {
3579
3582
  status=excluded.status,
3580
3583
  error_code=excluded.error_code,
3581
3584
  error_message=excluded.error_message,
3585
+ result_text=COALESCE(excluded.result_text, operations.result_text),
3582
3586
  started_at=COALESCE(excluded.started_at, operations.started_at),
3583
3587
  finished_at=COALESCE(excluded.finished_at, operations.finished_at),
3584
3588
  updated_at=excluded.updated_at`
@@ -3595,6 +3599,7 @@ async function createCloudDatastore(homeDir) {
3595
3599
  row.status,
3596
3600
  row.error_code,
3597
3601
  row.error_message,
3602
+ row.result_text ?? null,
3598
3603
  row.status === "started" ? ts : null,
3599
3604
  terminalStatuses.has(row.status) ? ts : null,
3600
3605
  ts,
@@ -3604,7 +3609,7 @@ async function createCloudDatastore(homeDir) {
3604
3609
  },
3605
3610
  findOperationById: async (operationId) => safe(() => {
3606
3611
  const row = db.prepare(
3607
- `SELECT operation_id, type, object_id, quote_id, trace_id, orchestrator, subagent_session_id, timeout_seconds, cancel_requested_at, status, error_code, error_message, started_at, finished_at, updated_at
3612
+ `SELECT operation_id, type, object_id, quote_id, trace_id, orchestrator, subagent_session_id, timeout_seconds, cancel_requested_at, status, error_code, error_message, result_text, started_at, finished_at, updated_at
3608
3613
  FROM operations
3609
3614
  WHERE operation_id = ?
3610
3615
  LIMIT 1`
@@ -3763,6 +3768,7 @@ var DEFAULT_BACKUP_DIR = join8(homedir6(), BACKUP_DIR_SUBPATH);
3763
3768
  var BLOCKRUN_WALLET_KEY_SUBPATH = join8(".openclaw", "blockrun", "wallet.key");
3764
3769
  var MNEMOSPARK_WALLET_KEY_SUBPATH = join8(".openclaw", "mnemospark", "wallet", "wallet.key");
3765
3770
  var INLINE_UPLOAD_MAX_BYTES = 45e5;
3771
+ var NODE_FS_MAX_READFILE_BYTES = 2147483648;
3766
3772
  var PAYMENT_CRON_SCHEDULE = "0 0 1 * *";
3767
3773
  var TAR_OVERHEAD_BYTES = 10 * 1024 * 1024;
3768
3774
  var QUOTE_VALIDITY_USER_NOTE = "Quotes are valid for one hour. Please run price-storage again if you need a new quote.";
@@ -3809,8 +3815,8 @@ var CLOUD_HELP_TEXT = [
3809
3815
  " Purpose: create a local tar+gzip archive under ~/.openclaw/mnemospark/backup (filename from sanitized friendly name) and record metadata in SQLite for later price-storage and upload.",
3810
3816
  " Required: " + REQUIRED_BACKUP,
3811
3817
  "",
3812
- "\u2022 `/mnemospark_cloud price-storage --wallet-address <addr> --object-id <id> --object-id-hash <hash> --gb <gb> --provider <provider> --region <region>`",
3813
- " Purpose: request a storage quote before upload.",
3818
+ "\u2022 `/mnemospark_cloud price-storage --wallet-address <addr> --object-id <id> --object-id-hash <hash> --gb <gb> --provider aws --region us-east-1`",
3819
+ " Purpose: request a storage quote before upload (defaults shown; override `--provider` / `--region` for other regions).",
3814
3820
  " Required: " + REQUIRED_PRICE_STORAGE,
3815
3821
  "",
3816
3822
  "\u2022 `/mnemospark_cloud upload --quote-id <quote-id> --wallet-address <addr> --object-id <id> --object-id-hash <hash> [--name <friendly-name>] [--async] [--orchestrator <inline|subagent>] [--timeout-seconds <n>]`",
@@ -4686,6 +4692,39 @@ function encryptAesGcm(plaintext, key, randomFn = randomBytesNode) {
4686
4692
  const tag = cipher.getAuthTag();
4687
4693
  return Buffer.concat([nonce, ciphertext, tag]);
4688
4694
  }
4695
+ async function encryptPlaintextFileToAesGcmPath(plaintextPath, dek, outPath, randomFn = randomBytesNode) {
4696
+ if (dek.length !== 32) {
4697
+ throw new Error("Expected 32-byte AES key");
4698
+ }
4699
+ const nonce = randomFn(AES_GCM_NONCE_BYTES);
4700
+ const cipher = createCipheriv("aes-256-gcm", dek, nonce);
4701
+ const writeStream = createWriteStream2(outPath, { flags: "w" });
4702
+ writeStream.write(nonce);
4703
+ try {
4704
+ for await (const chunk of createReadStream2(plaintextPath)) {
4705
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4706
+ const out = cipher.update(buf);
4707
+ if (out.length) {
4708
+ await new Promise((resolve3, reject) => {
4709
+ writeStream.write(out, (err) => err ? reject(err) : resolve3());
4710
+ });
4711
+ }
4712
+ }
4713
+ const final = cipher.final();
4714
+ const tag = cipher.getAuthTag();
4715
+ await new Promise((resolve3, reject) => {
4716
+ writeStream.write(final, (err) => err ? reject(err) : resolve3());
4717
+ });
4718
+ await new Promise((resolve3, reject) => {
4719
+ writeStream.write(tag, (err) => err ? reject(err) : resolve3());
4720
+ });
4721
+ writeStream.end();
4722
+ await finished(writeStream);
4723
+ } catch (err) {
4724
+ writeStream.destroy();
4725
+ throw err;
4726
+ }
4727
+ }
4689
4728
  async function loadOrCreateKek(walletAddress, homeDir) {
4690
4729
  const keyPath = resolveWalletKekPath(walletAddress, homeDir);
4691
4730
  await mkdir5(dirname5(keyPath), { recursive: true });
@@ -4702,11 +4741,42 @@ async function loadOrCreateKek(walletAddress, homeDir) {
4702
4741
  return { kek: generated, keyPath };
4703
4742
  }
4704
4743
  async function prepareUploadPayload(archivePath, walletAddress, homeDir) {
4705
- const plaintext = await readFile3(archivePath);
4744
+ const archiveStat = await stat2(archivePath);
4745
+ if (!archiveStat.isFile()) {
4746
+ throw new Error(`Cannot read backup archive: not a file (${archivePath}).`);
4747
+ }
4706
4748
  const { kek, keyPath } = await loadOrCreateKek(walletAddress, homeDir);
4707
4749
  const dek = randomBytesNode(32);
4708
- const encryptedContent = encryptAesGcm(plaintext, dek);
4709
4750
  const wrappedDek = encryptAesGcm(dek, kek);
4751
+ if (archiveStat.size >= NODE_FS_MAX_READFILE_BYTES) {
4752
+ const encryptedTempPath = join8(tmpdir(), `mnemospark-upload-${randomUUID3()}.enc`);
4753
+ try {
4754
+ await encryptPlaintextFileToAesGcmPath(archivePath, dek, encryptedTempPath);
4755
+ const encStat = await stat2(encryptedTempPath);
4756
+ const payloadHash2 = await sha256File(encryptedTempPath);
4757
+ const payload2 = {
4758
+ mode: "presigned",
4759
+ content_base64: void 0,
4760
+ content_sha256: payloadHash2,
4761
+ content_length_bytes: encStat.size,
4762
+ wrapped_dek: wrappedDek.toString("base64"),
4763
+ encryption_algorithm: "AES-256-GCM",
4764
+ bucket_name_hint: bucketNameForWallet(walletAddress),
4765
+ key_store_path_hint: keyPath
4766
+ };
4767
+ return {
4768
+ payload: payload2,
4769
+ encryptedContent: null,
4770
+ encryptedTempPath
4771
+ };
4772
+ } catch (err) {
4773
+ await rm(encryptedTempPath, { force: true }).catch(() => {
4774
+ });
4775
+ throw err;
4776
+ }
4777
+ }
4778
+ const plaintext = await readFile3(archivePath);
4779
+ const encryptedContent = encryptAesGcm(plaintext, dek);
4710
4780
  const payloadHash = sha256Buffer(encryptedContent);
4711
4781
  const payload = {
4712
4782
  mode: encryptedContent.length <= INLINE_UPLOAD_MAX_BYTES ? "inline" : "presigned",
@@ -4723,7 +4793,17 @@ async function prepareUploadPayload(archivePath, walletAddress, homeDir) {
4723
4793
  encryptedContent
4724
4794
  };
4725
4795
  }
4726
- async function uploadPresignedObjectIfNeeded(uploadResponse, uploadMode, encryptedContent, fetchImpl = fetch) {
4796
+ function presignedPutBodyInit(encryptedContent, encryptedTempPath) {
4797
+ if (encryptedTempPath?.trim()) {
4798
+ const body = Readable.toWeb(createReadStream2(encryptedTempPath));
4799
+ return { body, duplex: "half" };
4800
+ }
4801
+ if (encryptedContent) {
4802
+ return { body: new Uint8Array(encryptedContent) };
4803
+ }
4804
+ throw new Error("Cannot upload storage object: missing encrypted payload body.");
4805
+ }
4806
+ async function uploadPresignedObjectIfNeeded(uploadResponse, uploadMode, encryptedContent, encryptedTempPath, fetchImpl = fetch) {
4727
4807
  if (!uploadResponse.upload_url) {
4728
4808
  if (uploadMode === "presigned") {
4729
4809
  throw new Error("Cannot upload storage object: missing presigned upload URL.");
@@ -4734,24 +4814,33 @@ async function uploadPresignedObjectIfNeeded(uploadResponse, uploadMode, encrypt
4734
4814
  if (!headers.has("content-type")) {
4735
4815
  headers.set("content-type", "application/octet-stream");
4736
4816
  }
4737
- const putBody = new Uint8Array(encryptedContent);
4738
- const firstAttempt = await fetchImpl(uploadResponse.upload_url, {
4817
+ const { body, duplex } = presignedPutBodyInit(encryptedContent, encryptedTempPath);
4818
+ const firstInit = {
4739
4819
  method: "PUT",
4740
4820
  headers,
4741
- body: putBody,
4821
+ body,
4742
4822
  redirect: "manual"
4743
- });
4823
+ };
4824
+ if (duplex) {
4825
+ firstInit.duplex = duplex;
4826
+ }
4827
+ const firstAttempt = await fetchImpl(uploadResponse.upload_url, firstInit);
4744
4828
  if (firstAttempt.ok) {
4745
4829
  return;
4746
4830
  }
4747
4831
  if ((firstAttempt.status === 307 || firstAttempt.status === 308) && firstAttempt.headers.has("location")) {
4748
4832
  const location = firstAttempt.headers.get("location")?.trim();
4749
4833
  if (location) {
4750
- const redirectedAttempt = await fetchImpl(location, {
4834
+ const retryBody = presignedPutBodyInit(encryptedContent, encryptedTempPath);
4835
+ const retryInit = {
4751
4836
  method: "PUT",
4752
4837
  headers,
4753
- body: putBody
4754
- });
4838
+ body: retryBody.body
4839
+ };
4840
+ if (retryBody.duplex) {
4841
+ retryInit.duplex = retryBody.duplex;
4842
+ }
4843
+ const redirectedAttempt = await fetchImpl(location, retryInit);
4755
4844
  if (redirectedAttempt.ok) {
4756
4845
  return;
4757
4846
  }
@@ -4879,9 +4968,11 @@ function formatPriceStorageUserMessage(quote, localArchiveHint) {
4879
4968
  function quoteLookupMatchesPriceStorageResponse(lookup, quote) {
4880
4969
  return lookup.quoteId === quote.quote_id && lookup.walletAddress.trim().toLowerCase() === quote.addr.trim().toLowerCase() && lookup.objectId === quote.object_id && lookup.objectIdHash.toLowerCase() === quote.object_id_hash.toLowerCase() && lookup.storagePrice === quote.storage_price && lookup.provider === quote.provider && lookup.location === quote.location;
4881
4970
  }
4971
+ var DEFAULT_BACKUP_QUOTE_PROVIDER = "aws";
4972
+ var DEFAULT_BACKUP_QUOTE_REGION = "us-east-1";
4882
4973
  function formatBackupSuccessUserMessage(result, walletAddress, friendlyName) {
4883
4974
  const hash = result.objectIdHash.replace(/\s/g, "");
4884
- const priceStorageLine = `/mnemospark_cloud price-storage --wallet-address \`${walletAddress}\` --object-id \`${result.objectId}\` --object-id-hash \`${hash}\` --gb \`${result.objectSizeGb}\` --provider <provider> --region <region>`;
4975
+ const priceStorageLine = `/mnemospark_cloud price-storage --wallet-address \`${walletAddress}\` --object-id \`${result.objectId}\` --object-id-hash \`${hash}\` --gb \`${result.objectSizeGb}\` --provider ${DEFAULT_BACKUP_QUOTE_PROVIDER} --region ${DEFAULT_BACKUP_QUOTE_REGION}`;
4885
4976
  return [
4886
4977
  `Backup archive: \`${result.archivePath}\``,
4887
4978
  "",
@@ -4890,10 +4981,12 @@ function formatBackupSuccessUserMessage(result, walletAddress, friendlyName) {
4890
4981
  `object-id-hash: ${hash}`,
4891
4982
  `object-size: ${result.objectSizeGb}`,
4892
4983
  "",
4893
- "Next, request a storage quote. Replace `<provider>` and `<region>` (one line):",
4984
+ "Next, request a storage quote.",
4894
4985
  "",
4895
4986
  priceStorageLine,
4896
4987
  "",
4988
+ `The default region is ${DEFAULT_BACKUP_QUOTE_REGION}. Change the command parameters to switch regions (not required).`,
4989
+ "",
4897
4990
  "Region examples (merge into the command above):",
4898
4991
  "North America: `--provider aws --region us-east-1`",
4899
4992
  "Europe: `--provider aws --region eu-north-1`",
@@ -5407,8 +5500,8 @@ async function runCloudCommandHandler(ctx, options, executionContext = {}) {
5407
5500
  const datastore = await createCloudDatastore(mnemosparkHomeDir);
5408
5501
  const terminalOperationStatuses = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "timed_out"]);
5409
5502
  const isTerminalOperationStatus = (status) => terminalOperationStatuses.has(status);
5410
- const formatOperationStatus = (operation) => ({
5411
- text: [
5503
+ const formatOperationStatus = (operation) => {
5504
+ const meta = [
5412
5505
  `operation-id: ${operation.operation_id}`,
5413
5506
  `type: ${operation.type}`,
5414
5507
  `status: ${operation.status}`,
@@ -5419,9 +5512,15 @@ async function runCloudCommandHandler(ctx, options, executionContext = {}) {
5419
5512
  operation.timeout_seconds ? `timeout-seconds: ${operation.timeout_seconds}` : null,
5420
5513
  operation.error_code ? `error-code: ${operation.error_code}` : null,
5421
5514
  operation.error_message ? `error-message: ${operation.error_message}` : null
5422
- ].filter((v) => Boolean(v)).join("\n"),
5423
- isError: operation.status === "failed" || operation.status === "cancelled" || operation.status === "timed_out"
5424
- });
5515
+ ].filter((v) => Boolean(v)).join("\n");
5516
+ const withResult = operation.status === "succeeded" && operation.result_text?.trim() ? `${meta}
5517
+
5518
+ ${operation.result_text}` : meta;
5519
+ return {
5520
+ text: withResult,
5521
+ isError: operation.status === "failed" || operation.status === "cancelled" || operation.status === "timed_out"
5522
+ };
5523
+ };
5425
5524
  if (parsed.mode === "op-status") {
5426
5525
  let operation = await datastore.findOperationById(parsed.operationId);
5427
5526
  if (!operation) {
@@ -5758,7 +5857,7 @@ async function runCloudCommandHandler(ctx, options, executionContext = {}) {
5758
5857
  mnemosparkHomeDir
5759
5858
  );
5760
5859
  },
5761
- onCompleted: async (sessionId) => {
5860
+ onCompleted: async (sessionId, result) => {
5762
5861
  await datastore.upsertOperation({
5763
5862
  operation_id: operationId,
5764
5863
  type: opType,
@@ -5770,7 +5869,8 @@ async function runCloudCommandHandler(ctx, options, executionContext = {}) {
5770
5869
  timeout_seconds: timeoutSeconds,
5771
5870
  status: "succeeded",
5772
5871
  error_code: null,
5773
- error_message: null
5872
+ error_message: null,
5873
+ result_text: result.isError ? null : result.text
5774
5874
  });
5775
5875
  await emitOperationEventBestEffort(
5776
5876
  "operation.completed",
@@ -5947,7 +6047,8 @@ operation-id: ${operationId}`,
5947
6047
  orchestrator: "inline",
5948
6048
  status: result.isError ? "failed" : "succeeded",
5949
6049
  error_code: result.isError ? "ASYNC_FAILED" : null,
5950
- error_message: result.isError ? result.text : null
6050
+ error_message: result.isError ? result.text : null,
6051
+ result_text: result.isError ? null : result.text
5951
6052
  });
5952
6053
  await emitOperationEventBestEffort(
5953
6054
  "operation.completed",
@@ -6027,7 +6128,7 @@ operation-id: ${operationId}`,
6027
6128
  await emitCloudEventBestEffort(
6028
6129
  "backup.completed",
6029
6130
  {
6030
- operation_id: randomUUID3(),
6131
+ operation_id: executionContext.forcedOperationId?.trim() || randomUUID3(),
6031
6132
  object_id: result.objectId,
6032
6133
  status: "succeeded",
6033
6134
  details: {
@@ -6152,6 +6253,7 @@ operation-id: ${operationId}`,
6152
6253
  executionContext.forcedOperationId ?? idempotencyKeyFn(),
6153
6254
  executionContext.forcedTraceId
6154
6255
  );
6256
+ let preparedPayload;
6155
6257
  try {
6156
6258
  const loggedQuote = await datastore.findQuoteById(parsed.uploadRequest.quote_id);
6157
6259
  if (!loggedQuote) {
@@ -6223,7 +6325,7 @@ operation-id: ${operationId}`,
6223
6325
  isError: true
6224
6326
  };
6225
6327
  }
6226
- const preparedPayload = await prepareUploadPayload(
6328
+ preparedPayload = await prepareUploadPayload(
6227
6329
  archivePath,
6228
6330
  parsed.uploadRequest.wallet_address,
6229
6331
  mnemosparkHomeDir
@@ -6267,6 +6369,7 @@ operation-id: ${operationId}`,
6267
6369
  uploadResponse,
6268
6370
  preparedPayload.payload.mode,
6269
6371
  preparedPayload.encryptedContent,
6372
+ preparedPayload.encryptedTempPath,
6270
6373
  fetchImpl
6271
6374
  );
6272
6375
  let finalizedUploadResponse = uploadResponse;
@@ -6404,6 +6507,11 @@ operation-id: ${operationId}`,
6404
6507
  text: uploadErrorMessage ?? "Cannot upload storage object",
6405
6508
  isError: true
6406
6509
  };
6510
+ } finally {
6511
+ if (preparedPayload?.encryptedTempPath) {
6512
+ await rm(preparedPayload.encryptedTempPath, { force: true }).catch(() => {
6513
+ });
6514
+ }
6407
6515
  }
6408
6516
  }
6409
6517
  if (parsed.mode === "ls") {