mnemospark 0.3.0 → 0.5.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/dist/index.js CHANGED
@@ -377,6 +377,34 @@ function normalizePaymentRequired(headers) {
377
377
  function normalizePaymentResponse(headers) {
378
378
  return headers.get("PAYMENT-RESPONSE") ?? headers.get("x-payment-response") ?? void 0;
379
379
  }
380
+ var BYTE_SI_UNITS = ["B", "KB", "MB", "GB", "TB"];
381
+ var BYTE_SI_BASE = 1e3;
382
+ function formatBytesForDisplay(bytes) {
383
+ if (!Number.isInteger(bytes) || bytes < 0 || !Number.isFinite(bytes)) {
384
+ throw new Error("formatBytesForDisplay expects a non-negative integer");
385
+ }
386
+ if (bytes === 0) {
387
+ return "0 B";
388
+ }
389
+ let value = bytes;
390
+ let unitIndex = 0;
391
+ while (value >= BYTE_SI_BASE && unitIndex < BYTE_SI_UNITS.length - 1) {
392
+ value /= BYTE_SI_BASE;
393
+ unitIndex += 1;
394
+ }
395
+ if (unitIndex === 0) {
396
+ return `${bytes} B`;
397
+ }
398
+ const nearestInt = Math.round(value);
399
+ const pickInt = nearestInt > 0 && Math.abs(value - nearestInt) / Math.max(value, 1e-9) <= 0.01;
400
+ let rounded = pickInt ? nearestInt : Math.round(value * 10) / 10;
401
+ if (rounded >= BYTE_SI_BASE && unitIndex < BYTE_SI_UNITS.length - 1) {
402
+ rounded = Math.round(rounded / BYTE_SI_BASE * 10) / 10;
403
+ unitIndex += 1;
404
+ }
405
+ const text = Number.isInteger(rounded) ? String(rounded) : String(rounded).replace(/\.0$/, "");
406
+ return `${text} ${BYTE_SI_UNITS[unitIndex]}`;
407
+ }
380
408
 
381
409
  // src/wallet-signature.ts
382
410
  function normalizeWalletSignature(value) {
@@ -1193,6 +1221,9 @@ function parseStoredAes256Key(raw, errorMessage = "Invalid key file format") {
1193
1221
  var STORAGE_LS_PROXY_PATH = "/mnemospark/storage/ls";
1194
1222
  var STORAGE_DOWNLOAD_PROXY_PATH = "/mnemospark/storage/download";
1195
1223
  var STORAGE_DELETE_PROXY_PATH = "/mnemospark/storage/delete";
1224
+ function isStorageLsListResponse(r) {
1225
+ return r.mode === "list";
1226
+ }
1196
1227
  var AES_GCM_TAG_BYTES = 16;
1197
1228
  function asBooleanOrDefault(value, defaultValue) {
1198
1229
  if (typeof value === "boolean") {
@@ -1280,7 +1311,7 @@ async function decryptDownloadBytes(encryptedBytes, wrappedDekBase64, walletAddr
1280
1311
  }
1281
1312
  return decryptAesGcm(encryptedBytes, dek);
1282
1313
  }
1283
- async function requestJsonViaProxy(proxyPath, request, parser, options = {}) {
1314
+ async function requestJsonViaProxy(proxyPath, jsonBody, parser, options = {}) {
1284
1315
  const fetchImpl = options.fetchImpl ?? fetch;
1285
1316
  const baseUrl = normalizeBaseUrl(
1286
1317
  options.proxyBaseUrl ?? `http://127.0.0.1:${PROXY_PORT.toString()}`
@@ -1293,7 +1324,7 @@ async function requestJsonViaProxy(proxyPath, request, parser, options = {}) {
1293
1324
  },
1294
1325
  options.correlation
1295
1326
  ),
1296
- body: JSON.stringify(request)
1327
+ body: JSON.stringify(jsonBody)
1297
1328
  });
1298
1329
  const bodyText = await response.text();
1299
1330
  if (!response.ok) {
@@ -1307,7 +1338,7 @@ async function requestJsonViaProxy(proxyPath, request, parser, options = {}) {
1307
1338
  }
1308
1339
  return parser(payload);
1309
1340
  }
1310
- async function forwardStorageToBackend(path, method, request, options = {}) {
1341
+ async function forwardStorageToBackend(path, method, jsonBody, options = {}) {
1311
1342
  const fetchImpl = options.fetchImpl ?? fetch;
1312
1343
  const backendBaseUrl = (options.backendBaseUrl ?? "").trim();
1313
1344
  const walletSignature = normalizeWalletSignature(options.walletSignature);
@@ -1326,7 +1357,7 @@ async function forwardStorageToBackend(path, method, request, options = {}) {
1326
1357
  "Content-Type": "application/json",
1327
1358
  "X-Wallet-Signature": walletSignature
1328
1359
  },
1329
- body: JSON.stringify(request)
1360
+ body: JSON.stringify(jsonBody)
1330
1361
  });
1331
1362
  const bodyBuffer = Buffer.from(await response.arrayBuffer());
1332
1363
  return {
@@ -1356,11 +1387,97 @@ function parseStorageObjectRequest(payload) {
1356
1387
  location
1357
1388
  };
1358
1389
  }
1390
+ function jsonBodyForObjectRequest(request) {
1391
+ const o = {
1392
+ wallet_address: request.wallet_address,
1393
+ object_key: request.object_key
1394
+ };
1395
+ if (request.location) {
1396
+ o.location = request.location;
1397
+ }
1398
+ return o;
1399
+ }
1400
+ function jsonBodyForLsRequest(request) {
1401
+ const o = { wallet_address: request.wallet_address };
1402
+ if (request.object_key) {
1403
+ o.object_key = request.object_key;
1404
+ }
1405
+ if (request.location) {
1406
+ o.location = request.location;
1407
+ }
1408
+ if (request.continuation_token) {
1409
+ o.continuation_token = request.continuation_token;
1410
+ }
1411
+ if (typeof request.max_keys === "number") {
1412
+ o.max_keys = request.max_keys;
1413
+ }
1414
+ if (request.prefix) {
1415
+ o.prefix = request.prefix;
1416
+ }
1417
+ return o;
1418
+ }
1419
+ function parseStorageLsRequestPayload(payload) {
1420
+ const record = asRecord(payload);
1421
+ if (!record) {
1422
+ return null;
1423
+ }
1424
+ const walletAddress = asNonEmptyString(record.wallet_address);
1425
+ if (!walletAddress) {
1426
+ return null;
1427
+ }
1428
+ const objectKey = asNonEmptyString(record.object_key) ?? void 0;
1429
+ const location = asNonEmptyString(record.location) ?? void 0;
1430
+ const continuation_token = asNonEmptyString(record.continuation_token) ?? void 0;
1431
+ const maxRaw = asNumber(record.max_keys);
1432
+ const max_keys = maxRaw !== null && Number.isInteger(maxRaw) && maxRaw >= 1 ? maxRaw : void 0;
1433
+ const prefix = asNonEmptyString(record.prefix) ?? void 0;
1434
+ return {
1435
+ wallet_address: walletAddress,
1436
+ ...objectKey ? { object_key: objectKey } : {},
1437
+ ...location ? { location } : {},
1438
+ ...continuation_token ? { continuation_token } : {},
1439
+ ...typeof max_keys === "number" ? { max_keys } : {},
1440
+ ...prefix ? { prefix } : {}
1441
+ };
1442
+ }
1359
1443
  function parseStorageLsResponse(payload) {
1360
1444
  const record = asRecord(payload);
1361
1445
  if (!record) {
1362
1446
  throw new Error("Invalid ls response payload");
1363
1447
  }
1448
+ if (record.list_mode === true) {
1449
+ const bucket2 = asNonEmptyString(record.bucket) ?? asNonEmptyString(record.bucket_name);
1450
+ const rawObjects = record.objects;
1451
+ if (!bucket2 || !Array.isArray(rawObjects)) {
1452
+ throw new Error("ls list response is missing required fields");
1453
+ }
1454
+ const objects = [];
1455
+ for (const item of rawObjects) {
1456
+ const row = asRecord(item);
1457
+ if (!row) {
1458
+ continue;
1459
+ }
1460
+ const key2 = asNonEmptyString(row.key);
1461
+ const sizeBytes2 = asNumber(row.size_bytes);
1462
+ if (!key2 || sizeBytes2 === null || !Number.isInteger(sizeBytes2) || sizeBytes2 < 0) {
1463
+ continue;
1464
+ }
1465
+ const last_modified = asNonEmptyString(row.last_modified) ?? void 0;
1466
+ objects.push({ key: key2, size_bytes: sizeBytes2, last_modified });
1467
+ }
1468
+ const is_truncated = asBooleanOrDefault(record.is_truncated, false);
1469
+ const nextRaw = record.next_continuation_token;
1470
+ const next_continuation_token = nextRaw === void 0 || nextRaw === null ? null : String(nextRaw);
1471
+ return {
1472
+ mode: "list",
1473
+ success: asBooleanOrDefault(record.success, true),
1474
+ list_mode: true,
1475
+ bucket: bucket2,
1476
+ objects,
1477
+ is_truncated,
1478
+ next_continuation_token
1479
+ };
1480
+ }
1364
1481
  const key = asNonEmptyString(record.key) ?? asNonEmptyString(record.object_key);
1365
1482
  const sizeBytes = asNumber(record.size_bytes);
1366
1483
  const bucket = asNonEmptyString(record.bucket) ?? asNonEmptyString(record.bucket_name);
@@ -1368,7 +1485,11 @@ function parseStorageLsResponse(payload) {
1368
1485
  if (!key || sizeBytes === null || !bucket) {
1369
1486
  throw new Error("ls response is missing required fields");
1370
1487
  }
1488
+ if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {
1489
+ throw new Error("ls response has invalid size_bytes; expected non-negative integer");
1490
+ }
1371
1491
  return {
1492
+ mode: "stat",
1372
1493
  success: asBooleanOrDefault(record.success, true),
1373
1494
  key,
1374
1495
  size_bytes: sizeBytes,
@@ -1411,12 +1532,17 @@ function parseStorageDownloadProxyResponse(payload) {
1411
1532
  };
1412
1533
  }
1413
1534
  async function requestStorageLsViaProxy(request, options = {}) {
1414
- return requestJsonViaProxy(STORAGE_LS_PROXY_PATH, request, parseStorageLsResponse, options);
1535
+ return requestJsonViaProxy(
1536
+ STORAGE_LS_PROXY_PATH,
1537
+ jsonBodyForLsRequest(request),
1538
+ parseStorageLsResponse,
1539
+ options
1540
+ );
1415
1541
  }
1416
1542
  async function requestStorageDownloadViaProxy(request, options = {}) {
1417
1543
  return requestJsonViaProxy(
1418
1544
  STORAGE_DOWNLOAD_PROXY_PATH,
1419
- request,
1545
+ jsonBodyForObjectRequest(request),
1420
1546
  parseStorageDownloadProxyResponse,
1421
1547
  options
1422
1548
  );
@@ -1424,19 +1550,29 @@ async function requestStorageDownloadViaProxy(request, options = {}) {
1424
1550
  async function requestStorageDeleteViaProxy(request, options = {}) {
1425
1551
  return requestJsonViaProxy(
1426
1552
  STORAGE_DELETE_PROXY_PATH,
1427
- request,
1553
+ jsonBodyForObjectRequest(request),
1428
1554
  parseStorageDeleteResponse,
1429
1555
  options
1430
1556
  );
1431
1557
  }
1432
1558
  async function forwardStorageLsToBackend(request, options = {}) {
1433
- return forwardStorageToBackend("/storage/ls", "POST", request, options);
1559
+ return forwardStorageToBackend("/storage/ls", "POST", jsonBodyForLsRequest(request), options);
1434
1560
  }
1435
1561
  async function forwardStorageDownloadToBackend(request, options = {}) {
1436
- return forwardStorageToBackend("/storage/download", "POST", request, options);
1562
+ return forwardStorageToBackend(
1563
+ "/storage/download",
1564
+ "POST",
1565
+ jsonBodyForObjectRequest(request),
1566
+ options
1567
+ );
1437
1568
  }
1438
1569
  async function forwardStorageDeleteToBackend(request, options = {}) {
1439
- return forwardStorageToBackend("/storage/delete", "POST", request, options);
1570
+ return forwardStorageToBackend(
1571
+ "/storage/delete",
1572
+ "POST",
1573
+ jsonBodyForObjectRequest(request),
1574
+ options
1575
+ );
1440
1576
  }
1441
1577
  async function downloadStorageToDisk(request, backendResponse, options = {}) {
1442
1578
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -2272,13 +2408,13 @@ async function startProxy(options) {
2272
2408
  });
2273
2409
  return;
2274
2410
  }
2275
- const requestPayload = parseStorageObjectRequest(payload);
2411
+ const requestPayload = parseStorageLsRequestPayload(payload);
2276
2412
  if (!requestPayload) {
2277
2413
  logProxyEvent("warn", "proxy_ls_missing_fields");
2278
2414
  emitProxyTerminalFromStatus(correlation, 400, { reason: "missing_fields" });
2279
2415
  sendJson(res, 400, {
2280
2416
  error: "Bad request",
2281
- message: "Missing required fields: wallet_address, object_key"
2417
+ message: "Missing required field: wallet_address"
2282
2418
  });
2283
2419
  return;
2284
2420
  }
@@ -2800,6 +2936,259 @@ import { homedir as homedir6 } from "os";
2800
2936
  import { basename as basename2, dirname as dirname5, join as join8, resolve as resolve2 } from "path";
2801
2937
  import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
2802
2938
 
2939
+ // src/cloud-ls-format.ts
2940
+ import { CronExpressionParser } from "cron-parser";
2941
+ var LS_NAME_DISPLAY_MAX = 72;
2942
+ var LS_PAY_DISPLAY_MAX = 28;
2943
+ var LS_CRON_ID_MAX = 14;
2944
+ var LS_TIME_FIELD_WIDTH = 12;
2945
+ var MONTHS_SHORT = [
2946
+ "Jan",
2947
+ "Feb",
2948
+ "Mar",
2949
+ "Apr",
2950
+ "May",
2951
+ "Jun",
2952
+ "Jul",
2953
+ "Aug",
2954
+ "Sep",
2955
+ "Oct",
2956
+ "Nov",
2957
+ "Dec"
2958
+ ];
2959
+ function formatLsTimeFieldUtc(iso, now) {
2960
+ const placeholder = " - ".slice(0, LS_TIME_FIELD_WIDTH);
2961
+ if (!iso) {
2962
+ return placeholder.padEnd(LS_TIME_FIELD_WIDTH, " ");
2963
+ }
2964
+ const d = new Date(iso);
2965
+ if (Number.isNaN(d.getTime())) {
2966
+ return placeholder.padEnd(LS_TIME_FIELD_WIDTH, " ");
2967
+ }
2968
+ const mon = MONTHS_SHORT[d.getUTCMonth()] ?? "???";
2969
+ const day = String(d.getUTCDate()).padStart(2, " ");
2970
+ const y = d.getUTCFullYear();
2971
+ const nowY = now.getUTCFullYear();
2972
+ let core;
2973
+ if (y === nowY) {
2974
+ const hh = String(d.getUTCHours()).padStart(2, "0");
2975
+ const mm = String(d.getUTCMinutes()).padStart(2, "0");
2976
+ core = `${mon} ${day} ${hh}:${mm}`;
2977
+ } else {
2978
+ core = `${mon} ${day} ${y}`;
2979
+ }
2980
+ return core.padEnd(LS_TIME_FIELD_WIDTH, " ");
2981
+ }
2982
+ function truncateEnd(value, max) {
2983
+ if (value.length <= max) {
2984
+ return value;
2985
+ }
2986
+ if (max <= 1) {
2987
+ return "\u2026";
2988
+ }
2989
+ return `${value.slice(0, max - 1)}\u2026`;
2990
+ }
2991
+ function truncateMiddle(value, max, suffixMin) {
2992
+ if (value.length <= max) {
2993
+ return value;
2994
+ }
2995
+ if (max < suffixMin + 5) {
2996
+ return truncateEnd(value, max);
2997
+ }
2998
+ const suffixLen = Math.min(suffixMin, max - 5);
2999
+ const prefixLen = max - 3 - suffixLen;
3000
+ return `${value.slice(0, prefixLen)} \u2026 ${value.slice(-suffixLen)}`;
3001
+ }
3002
+ function formatCronIdCell(cronId, width) {
3003
+ if (!cronId) {
3004
+ return " - ".slice(0, width).padStart(width, " ");
3005
+ }
3006
+ const t = truncateEnd(cronId, width);
3007
+ return t.padStart(width, " ");
3008
+ }
3009
+ function formatPaymentCell(amount, network, maxWidth) {
3010
+ if (amount === null) {
3011
+ return " - ".slice(0, maxWidth).padStart(maxWidth, " ");
3012
+ }
3013
+ let s = amount.toFixed(6).replace(/\.?0+$/, "");
3014
+ if (network && network.trim()) {
3015
+ s = `${s} (${network.trim()})`;
3016
+ }
3017
+ return truncateEnd(s, maxWidth).padStart(maxWidth, " ");
3018
+ }
3019
+ function formatNextCronUtc(schedule, cronStatus, now) {
3020
+ const blank = " - ".slice(0, LS_TIME_FIELD_WIDTH).padEnd(LS_TIME_FIELD_WIDTH, " ");
3021
+ if (cronStatus !== "active") {
3022
+ return blank;
3023
+ }
3024
+ try {
3025
+ const expr = CronExpressionParser.parse(schedule, { tz: "UTC", currentDate: now });
3026
+ const next = expr.next().toDate();
3027
+ return formatLsTimeFieldUtc(next.toISOString(), now);
3028
+ } catch {
3029
+ return "?".padEnd(LS_TIME_FIELD_WIDTH, " ");
3030
+ }
3031
+ }
3032
+ async function prepareRows(objects, walletAddress, datastore, now) {
3033
+ const sorted = [...objects].sort((a, b) => {
3034
+ const ta = a.last_modified ? Date.parse(a.last_modified) : Number.NaN;
3035
+ const tb = b.last_modified ? Date.parse(b.last_modified) : Number.NaN;
3036
+ const aOk = Number.isFinite(ta);
3037
+ const bOk = Number.isFinite(tb);
3038
+ if (aOk && bOk && tb !== ta) {
3039
+ return tb - ta;
3040
+ }
3041
+ if (aOk && !bOk) {
3042
+ return -1;
3043
+ }
3044
+ if (!aOk && bOk) {
3045
+ return 1;
3046
+ }
3047
+ return a.key.localeCompare(b.key);
3048
+ });
3049
+ const rows = [];
3050
+ for (const obj of sorted) {
3051
+ const friendly = await datastore.findLatestFriendlyNameForObjectKey(walletAddress, obj.key);
3052
+ const cp = await datastore.findCronAndPaymentForObjectKey(walletAddress, obj.key);
3053
+ const sizeStr = formatBytesForDisplay(obj.size_bytes);
3054
+ const s3time = formatLsTimeFieldUtc(obj.last_modified, now);
3055
+ let cronIdDisp = null;
3056
+ let nextRun = " - ".slice(0, LS_TIME_FIELD_WIDTH).padEnd(LS_TIME_FIELD_WIDTH, " ");
3057
+ let payCell = "";
3058
+ if (cp) {
3059
+ cronIdDisp = cp.cronId;
3060
+ nextRun = formatNextCronUtc(cp.schedule, cp.cronStatus, now);
3061
+ payCell = formatPaymentCell(cp.amount, cp.network, LS_PAY_DISPLAY_MAX);
3062
+ } else {
3063
+ payCell = formatPaymentCell(null, null, LS_PAY_DISPLAY_MAX);
3064
+ }
3065
+ const nameRaw = friendly ? `${friendly} (${obj.key})` : obj.key;
3066
+ rows.push({
3067
+ perm: "----------",
3068
+ ln: " 1",
3069
+ user: "- ",
3070
+ grp: "- ",
3071
+ sizeStr,
3072
+ s3time,
3073
+ cronIdRaw: cronIdDisp,
3074
+ nextRun,
3075
+ payRaw: payCell,
3076
+ nameRaw: truncateMiddle(nameRaw, LS_NAME_DISPLAY_MAX, 8)
3077
+ });
3078
+ }
3079
+ return rows;
3080
+ }
3081
+ function columnWidths(rows) {
3082
+ let sizeW = 4;
3083
+ let cronW = 4;
3084
+ let payW = 3;
3085
+ for (const r of rows) {
3086
+ sizeW = Math.max(sizeW, r.sizeStr.length);
3087
+ const cid = r.cronIdRaw ? truncateEnd(r.cronIdRaw, LS_CRON_ID_MAX) : "";
3088
+ cronW = Math.max(cronW, cid.length || 1);
3089
+ payW = Math.max(payW, r.payRaw.length);
3090
+ }
3091
+ cronW = Math.min(Math.max(cronW, 4), LS_CRON_ID_MAX);
3092
+ payW = Math.min(Math.max(payW, 3), LS_PAY_DISPLAY_MAX);
3093
+ return { sizeW, cronW, payW };
3094
+ }
3095
+ function renderRow(r, w) {
3096
+ const cronPadded = formatCronIdCell(r.cronIdRaw, w.cronW);
3097
+ const sizePadded = r.sizeStr.padStart(w.sizeW, " ");
3098
+ const payPadded = r.payRaw.padStart(w.payW, " ");
3099
+ return [
3100
+ r.perm,
3101
+ r.ln,
3102
+ r.user,
3103
+ r.grp,
3104
+ sizePadded,
3105
+ r.s3time,
3106
+ cronPadded,
3107
+ r.nextRun,
3108
+ payPadded,
3109
+ r.nameRaw
3110
+ ].join(" ");
3111
+ }
3112
+ function renderHeader(w) {
3113
+ return [
3114
+ "PERM ",
3115
+ "LN",
3116
+ "USER ",
3117
+ "GRP ",
3118
+ "SIZE".padStart(w.sizeW, " "),
3119
+ "S3_TIME ".slice(0, LS_TIME_FIELD_WIDTH).padEnd(LS_TIME_FIELD_WIDTH, " "),
3120
+ "CRON".padStart(w.cronW, " "),
3121
+ "NEXT ".slice(0, LS_TIME_FIELD_WIDTH).padEnd(LS_TIME_FIELD_WIDTH, " "),
3122
+ "PAY".padStart(w.payW, " "),
3123
+ "NAME"
3124
+ ].join(" ");
3125
+ }
3126
+ async function buildMnemosparkLsMessage(result, ctx) {
3127
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
3128
+ if (isStorageLsListResponse(result)) {
3129
+ const disclaimer2 = "Names, cron, and payment columns come from this machine's SQLite catalog when available; `-` means unknown locally. S3 is authoritative for which keys exist.";
3130
+ const legend2 = "Legend: S3_TIME and NEXT are UTC. NEXT is the next cron fire from the stored expression.";
3131
+ const bucketLine = `bucket: ${result.bucket}`;
3132
+ const sortLine = "sorted by: S3 last_modified descending (missing dates last), then key ascending.";
3133
+ if (result.objects.length === 0) {
3134
+ const lines = [disclaimer2, "", bucketLine, "", "No objects in this bucket."];
3135
+ return lines.join("\n");
3136
+ }
3137
+ const rows = await prepareRows(result.objects, ctx.walletAddress, ctx.datastore, now);
3138
+ const w2 = columnWidths(rows);
3139
+ const header2 = renderHeader(w2);
3140
+ const bodyLines = rows.map((r) => renderRow(r, w2));
3141
+ const totalLine = `total ${String(result.objects.length)}`;
3142
+ const truncLine = result.is_truncated ? "List truncated; more objects in bucket." : null;
3143
+ const prose2 = [disclaimer2, legend2, bucketLine, sortLine, totalLine, truncLine].filter((x) => Boolean(x)).join("\n");
3144
+ const fence2 = ["```", [header2, ...bodyLines].join("\n"), "```"].join("\n");
3145
+ return `${prose2}
3146
+
3147
+ ${fence2}`;
3148
+ }
3149
+ const friendly = await ctx.datastore.findLatestFriendlyNameForObjectKey(
3150
+ ctx.walletAddress,
3151
+ result.key
3152
+ );
3153
+ const cp = await ctx.datastore.findCronAndPaymentForObjectKey(ctx.walletAddress, result.key);
3154
+ const sizeStr = formatBytesForDisplay(result.size_bytes);
3155
+ const s3time = formatLsTimeFieldUtc(void 0, now);
3156
+ let payCell = formatPaymentCell(null, null, LS_PAY_DISPLAY_MAX);
3157
+ let cronIdDisp = null;
3158
+ let nextRun = " - ".slice(0, LS_TIME_FIELD_WIDTH).padEnd(LS_TIME_FIELD_WIDTH, " ");
3159
+ if (cp) {
3160
+ cronIdDisp = cp.cronId;
3161
+ nextRun = formatNextCronUtc(cp.schedule, cp.cronStatus, now);
3162
+ payCell = formatPaymentCell(cp.amount, cp.network, LS_PAY_DISPLAY_MAX);
3163
+ }
3164
+ const nameShown = truncateMiddle(
3165
+ friendly ? `${friendly} (${result.key})` : result.key,
3166
+ LS_NAME_DISPLAY_MAX,
3167
+ 8
3168
+ );
3169
+ const prep = {
3170
+ perm: "----------",
3171
+ ln: " 1",
3172
+ user: "- ",
3173
+ grp: "- ",
3174
+ sizeStr,
3175
+ s3time,
3176
+ cronIdRaw: cronIdDisp,
3177
+ nextRun,
3178
+ payRaw: payCell,
3179
+ nameRaw: nameShown
3180
+ };
3181
+ const w = columnWidths([prep]);
3182
+ const header = renderHeader(w);
3183
+ const line = renderRow(prep, w);
3184
+ const disclaimer = "Names, cron, and payment columns come from this machine's SQLite catalog when available; `-` means unknown locally.";
3185
+ const legend = "Legend: S3_TIME and NEXT are UTC.";
3186
+ const prose = [disclaimer, legend, `bucket: ${result.bucket}`, ""].join("\n");
3187
+ const fence = ["```", [header, line].join("\n"), "```"].join("\n");
3188
+ return `${prose}
3189
+ ${fence}`;
3190
+ }
3191
+
2803
3192
  // src/cloud-datastore.ts
2804
3193
  import { randomUUID as randomUUID2 } from "crypto";
2805
3194
  import { mkdir as mkdir4 } from "fs/promises";
@@ -2835,6 +3224,7 @@ async function createCloudDatastore(homeDir) {
2835
3224
  const nextDb = new DatabaseSyncCtor(dbPath);
2836
3225
  nextDb.exec("PRAGMA journal_mode=WAL;");
2837
3226
  nextDb.exec("PRAGMA foreign_keys=ON;");
3227
+ nextDb.exec("PRAGMA busy_timeout=5000;");
2838
3228
  nextDb.exec(`
2839
3229
  CREATE TABLE IF NOT EXISTS schema_migrations (
2840
3230
  version INTEGER PRIMARY KEY,
@@ -2883,6 +3273,7 @@ async function createCloudDatastore(homeDir) {
2883
3273
  updated_at TEXT NOT NULL
2884
3274
  );
2885
3275
  CREATE INDEX IF NOT EXISTS idx_cron_jobs_object_key ON cron_jobs(object_key);
3276
+ CREATE INDEX IF NOT EXISTS idx_cron_jobs_quote_id ON cron_jobs(quote_id);
2886
3277
 
2887
3278
  CREATE TABLE IF NOT EXISTS operations (
2888
3279
  operation_id TEXT PRIMARY KEY,
@@ -3083,6 +3474,38 @@ async function createCloudDatastore(homeDir) {
3083
3474
  if (!row) return null;
3084
3475
  return { cronId: row.cron_id, objectId: row.object_id };
3085
3476
  }, null),
3477
+ findCronByQuoteId: async (quoteId) => safe(() => {
3478
+ const row = db.prepare(
3479
+ `SELECT cron_id, object_id, object_key, quote_id, schedule, command, status
3480
+ FROM cron_jobs WHERE quote_id = ? ORDER BY updated_at DESC LIMIT 1`
3481
+ ).get(quoteId);
3482
+ if (!row) return null;
3483
+ return {
3484
+ cron_id: row.cron_id,
3485
+ object_id: row.object_id,
3486
+ object_key: row.object_key,
3487
+ quote_id: row.quote_id,
3488
+ schedule: row.schedule,
3489
+ command: row.command,
3490
+ status: row.status
3491
+ };
3492
+ }, null),
3493
+ findPaymentByQuoteId: async (quoteId) => safe(() => {
3494
+ const row = db.prepare(
3495
+ `SELECT quote_id, wallet_address, trans_id, amount, network, status, settled_at
3496
+ FROM payments WHERE quote_id = ? LIMIT 1`
3497
+ ).get(quoteId);
3498
+ if (!row) return null;
3499
+ return {
3500
+ quote_id: row.quote_id,
3501
+ wallet_address: row.wallet_address,
3502
+ trans_id: row.trans_id,
3503
+ amount: row.amount,
3504
+ network: row.network,
3505
+ status: row.status,
3506
+ settled_at: row.settled_at
3507
+ };
3508
+ }, null),
3086
3509
  upsertOperation: async (row) => {
3087
3510
  await safe(() => {
3088
3511
  const ts = nowIso();
@@ -3209,7 +3632,63 @@ async function createCloudDatastore(homeDir) {
3209
3632
  WHERE wallet_address = ? AND friendly_name = ? AND is_active = 1`
3210
3633
  ).get(normalizedWalletAddress, friendlyName);
3211
3634
  return Number(row?.cnt ?? 0);
3212
- }, 0)
3635
+ }, 0),
3636
+ findLatestFriendlyNameForObjectKey: async (walletAddress, objectKey) => safe(() => {
3637
+ const w = normalizeWalletAddress(walletAddress);
3638
+ const byKey = db.prepare(
3639
+ `SELECT friendly_name
3640
+ FROM friendly_names
3641
+ WHERE wallet_address = ? AND object_key = ? AND is_active = 1
3642
+ ORDER BY created_at DESC
3643
+ LIMIT 1`
3644
+ ).get(w, objectKey);
3645
+ if (byKey) {
3646
+ return byKey.friendly_name;
3647
+ }
3648
+ const obj = db.prepare(
3649
+ `SELECT object_id FROM objects WHERE wallet_address = ? AND object_key = ? LIMIT 1`
3650
+ ).get(w, objectKey);
3651
+ if (!obj) {
3652
+ return null;
3653
+ }
3654
+ const byObj = db.prepare(
3655
+ `SELECT friendly_name
3656
+ FROM friendly_names
3657
+ WHERE wallet_address = ? AND object_id = ? AND is_active = 1
3658
+ ORDER BY created_at DESC
3659
+ LIMIT 1`
3660
+ ).get(w, obj.object_id);
3661
+ return byObj?.friendly_name ?? null;
3662
+ }, null),
3663
+ findCronAndPaymentForObjectKey: async (walletAddress, objectKey) => safe(() => {
3664
+ const w = normalizeWalletAddress(walletAddress);
3665
+ const cron = db.prepare(
3666
+ `SELECT c.cron_id, c.quote_id, c.schedule, c.status
3667
+ FROM cron_jobs c
3668
+ INNER JOIN objects o ON o.object_id = c.object_id
3669
+ WHERE c.object_key = ? AND o.wallet_address = ?
3670
+ ORDER BY c.updated_at DESC
3671
+ LIMIT 1`
3672
+ ).get(objectKey, w);
3673
+ if (!cron) {
3674
+ return null;
3675
+ }
3676
+ const pay = db.prepare(
3677
+ `SELECT amount, network, status
3678
+ FROM payments
3679
+ WHERE quote_id = ? AND wallet_address = ?
3680
+ LIMIT 1`
3681
+ ).get(cron.quote_id, w);
3682
+ return {
3683
+ cronId: cron.cron_id,
3684
+ schedule: cron.schedule,
3685
+ quoteId: cron.quote_id,
3686
+ cronStatus: cron.status,
3687
+ amount: pay?.amount ?? null,
3688
+ network: pay?.network ?? null,
3689
+ paymentStatus: pay?.status ?? null
3690
+ };
3691
+ }, null)
3213
3692
  };
3214
3693
  }
3215
3694
 
@@ -3229,7 +3708,16 @@ var CRON_LOG_ROW_PREFIX = "cron";
3229
3708
  var TAR_OVERHEAD_BYTES = 10 * 1024 * 1024;
3230
3709
  var REQUIRED_PRICE_STORAGE = "--wallet-address, --object-id, --object-id-hash, --gb, --provider, --region";
3231
3710
  var REQUIRED_UPLOAD = "--quote-id, --wallet-address, --object-id, --object-id-hash";
3711
+ var REQUIRED_PAYMENT_SETTLE = "--quote-id and --wallet-address";
3232
3712
  var REQUIRED_STORAGE_OBJECT = "--wallet-address and one of (--object-key | --name [--latest|--at])";
3713
+ var REQUIRED_LS = "--wallet-address (for one object add --object-key or --name [--latest|--at]; omit both to list the bucket)";
3714
+ var PAYMENT_SETTLE_FLAG_NAMES = /* @__PURE__ */ new Set([
3715
+ "quote-id",
3716
+ "wallet-address",
3717
+ "object-id",
3718
+ "object-key",
3719
+ "storage-price"
3720
+ ]);
3233
3721
  var BOOLEAN_SELECTOR_FLAGS = /* @__PURE__ */ new Set(["latest"]);
3234
3722
  var BOOLEAN_ASYNC_FLAGS = /* @__PURE__ */ new Set(["async"]);
3235
3723
  var BOOLEAN_OP_STATUS_FLAGS = /* @__PURE__ */ new Set(["cancel"]);
@@ -3262,9 +3750,13 @@ var CLOUD_HELP_TEXT = [
3262
3750
  " Purpose: upload an encrypted object using a valid quote-id.",
3263
3751
  " Required: " + REQUIRED_UPLOAD,
3264
3752
  "",
3265
- "\u2022 `/mnemospark_cloud ls --wallet-address <addr> [--object-key <object-key> | --name <friendly-name>] [--latest|--at <timestamp>]`",
3266
- " Purpose: look up remote object metadata.",
3267
- " Required: " + REQUIRED_STORAGE_OBJECT,
3753
+ "\u2022 `/mnemospark_cloud payment-settle --quote-id <quote-id> --wallet-address <addr> [--object-id <id>] [--object-key <key>] [--storage-price <n>]`",
3754
+ " Purpose: settle storage payment for a quote (e.g. monthly cron). Uses the same proxy + x402 path as upload pre-settlement.",
3755
+ " Required: --quote-id, --wallet-address (wallet private key must match the address).",
3756
+ "",
3757
+ "\u2022 `/mnemospark_cloud ls --wallet-address <addr> [--object-key <key> | --name <friendly-name> | omit both to list bucket] [--latest|--at <timestamp>]`",
3758
+ " Purpose: stat one object or list all keys in the wallet bucket (S3).",
3759
+ " Required: " + REQUIRED_LS,
3268
3760
  "",
3269
3761
  "\u2022 `/mnemospark_cloud download --wallet-address <addr> [--object-key <object-key> | --name <friendly-name>] [--latest|--at <timestamp>] [--async] [--orchestrator <inline|subagent>] [--timeout-seconds <n>]`",
3270
3762
  " Purpose: fetch an object to local disk.",
@@ -3296,7 +3788,7 @@ var CLOUD_HELP_TEXT = [
3296
3788
  "\u2022 `/mnemospark_cloud op-status --operation-id <id>`",
3297
3789
  "\u2022 `/mnemospark_cloud op-status --operation-id <id> --cancel`",
3298
3790
  "",
3299
- "Backup creates a tar+gzip object in ~/.openclaw/mnemospark/backup and appends object metadata to ~/.openclaw/mnemospark/object.log. Upload appends storage rows and cron-tracking rows to object.log, and keeps job entries in ~/.openclaw/mnemospark/crontab.txt. All storage commands (price-storage, upload, ls, download, delete) require --wallet-address."
3791
+ "Backup creates a tar+gzip object in ~/.openclaw/mnemospark/backup and appends object metadata to ~/.openclaw/mnemospark/object.log. Upload appends storage rows and cron-tracking rows to object.log, and keeps job entries in ~/.openclaw/mnemospark/crontab.txt. Commands price-storage, upload, ls, download, delete, and payment-settle require --wallet-address."
3300
3792
  ].join("\n");
3301
3793
  var UnsupportedBackupPlatformError = class extends Error {
3302
3794
  constructor(platform) {
@@ -3367,6 +3859,18 @@ function parseObjectSelector(flags) {
3367
3859
  if (objectKey) return { objectKey };
3368
3860
  return { nameSelector: { name, latest, at } };
3369
3861
  }
3862
+ function parseLsObjectSelector(flags) {
3863
+ const objectKey = flags["object-key"]?.trim();
3864
+ const name = flags.name?.trim();
3865
+ const latest = flags.latest === "true";
3866
+ const at = flags.at?.trim();
3867
+ if (objectKey && name) return null;
3868
+ if (latest && at) return null;
3869
+ if (!objectKey && !name && (latest || at)) return null;
3870
+ if (objectKey) return { objectKey };
3871
+ if (name) return { nameSelector: { name, latest, at } };
3872
+ return {};
3873
+ }
3370
3874
  function parseStorageObjectRequestInput(flags, selector) {
3371
3875
  const walletAddress = flags["wallet-address"]?.trim();
3372
3876
  if (!walletAddress) {
@@ -3537,20 +4041,76 @@ function parseCloudArgs(args) {
3537
4041
  }
3538
4042
  };
3539
4043
  }
4044
+ if (subcommand === "payment-settle") {
4045
+ const flags = parseNamedFlags(rest);
4046
+ if (!flags) {
4047
+ return { mode: "payment-settle-invalid" };
4048
+ }
4049
+ for (const key of Object.keys(flags)) {
4050
+ if (!PAYMENT_SETTLE_FLAG_NAMES.has(key)) {
4051
+ return { mode: "payment-settle-invalid" };
4052
+ }
4053
+ }
4054
+ const quoteId = flags["quote-id"]?.trim();
4055
+ const walletAddress = flags["wallet-address"]?.trim();
4056
+ if (!quoteId || !walletAddress) {
4057
+ return { mode: "payment-settle-invalid" };
4058
+ }
4059
+ let storagePrice;
4060
+ if (flags["storage-price"] !== void 0 && flags["storage-price"] !== "") {
4061
+ const raw = flags["storage-price"]?.trim() ?? "";
4062
+ const n = Number.parseFloat(raw);
4063
+ if (!Number.isFinite(n) || n < 0) {
4064
+ return { mode: "payment-settle-invalid" };
4065
+ }
4066
+ storagePrice = n;
4067
+ }
4068
+ return {
4069
+ mode: "payment-settle",
4070
+ paymentSettleRequest: {
4071
+ quote_id: quoteId,
4072
+ wallet_address: walletAddress,
4073
+ object_id: flags["object-id"]?.trim() || void 0,
4074
+ object_key: flags["object-key"]?.trim() || void 0,
4075
+ storage_price: storagePrice
4076
+ }
4077
+ };
4078
+ }
3540
4079
  if (subcommand === "ls") {
3541
4080
  const flags = parseNamedFlags(rest, BOOLEAN_SELECTOR_FLAGS);
3542
4081
  if (!flags) {
3543
4082
  return { mode: "ls-invalid" };
3544
4083
  }
3545
- const selector = parseObjectSelector(flags);
3546
- if (!selector) {
4084
+ const walletAddress = flags["wallet-address"]?.trim() ?? flags["wallet_address"]?.trim() ?? "";
4085
+ if (!walletAddress) {
3547
4086
  return { mode: "ls-invalid" };
3548
4087
  }
3549
- const request = parseStorageObjectRequestInput(flags, selector);
3550
- if (!request) {
4088
+ const selector = parseLsObjectSelector(flags);
4089
+ if (!selector) {
3551
4090
  return { mode: "ls-invalid" };
3552
4091
  }
3553
- return { mode: "ls", storageObjectRequest: request, nameSelector: selector.nameSelector };
4092
+ const location = flags.location?.trim() || flags.region?.trim() || void 0;
4093
+ if (selector.nameSelector) {
4094
+ return {
4095
+ mode: "ls",
4096
+ storageObjectRequest: { wallet_address: walletAddress, location },
4097
+ nameSelector: selector.nameSelector
4098
+ };
4099
+ }
4100
+ if (selector.objectKey) {
4101
+ return {
4102
+ mode: "ls",
4103
+ storageObjectRequest: {
4104
+ wallet_address: walletAddress,
4105
+ object_key: selector.objectKey,
4106
+ location
4107
+ }
4108
+ };
4109
+ }
4110
+ return {
4111
+ mode: "ls",
4112
+ storageObjectRequest: { wallet_address: walletAddress, location }
4113
+ };
3554
4114
  }
3555
4115
  if (subcommand === "download") {
3556
4116
  const flags = parseNamedFlags(rest, BOOLEAN_SELECTOR_AND_ASYNC_FLAGS);
@@ -3897,7 +4457,8 @@ function quoteCronArgument(value) {
3897
4457
  }
3898
4458
  function buildStoragePaymentCronCommand(job) {
3899
4459
  return [
3900
- "mnemospark-pay-storage",
4460
+ "/mnemospark_cloud",
4461
+ "payment-settle",
3901
4462
  "--quote-id",
3902
4463
  quoteCronArgument(job.quoteId),
3903
4464
  "--wallet-address",
@@ -4186,16 +4747,28 @@ function extractUploadErrorMessage(error) {
4186
4747
  }
4187
4748
  return message;
4188
4749
  }
4750
+ function extractLsErrorMessage(error) {
4751
+ if (!(error instanceof Error)) {
4752
+ return null;
4753
+ }
4754
+ const message = error.message.trim();
4755
+ if (!message) {
4756
+ return null;
4757
+ }
4758
+ if (message.startsWith("ls response") || message.startsWith("ls list response") || message.startsWith("Invalid ls response payload")) {
4759
+ return `Cannot list storage object: ${message}`;
4760
+ }
4761
+ if (message === "formatBytesForDisplay expects a non-negative integer") {
4762
+ return "Cannot list storage object: ls response has invalid size_bytes; expected non-negative integer";
4763
+ }
4764
+ return null;
4765
+ }
4189
4766
  function formatPriceStorageUserMessage(quote) {
4190
4767
  return [
4191
4768
  `Your storage quote \`${quote.quote_id}\` is valid for 1 hour, the storage price is \`${quote.storage_price}\` for \`${quote.object_id}\` with file size of \`${quote.object_size_gb}\` in \`${quote.provider}\` \`${quote.location}\``,
4192
4769
  `If you accept this quote run the command /mnemospark_cloud upload --quote-id \`${quote.quote_id}\` --wallet-address \`${quote.addr}\` --object-id \`${quote.object_id}\` --object-id-hash \`${quote.object_id_hash}\``
4193
4770
  ].join("\n");
4194
4771
  }
4195
- function formatStorageLsUserMessage(result, requestedObjectKey) {
4196
- const objectId = result.object_id ?? result.key;
4197
- return `${objectId} with ${requestedObjectKey} is ${result.size_bytes} in ${result.bucket}`;
4198
- }
4199
4772
  function createInProcessSubagentOrchestrator() {
4200
4773
  const sessions = /* @__PURE__ */ new Map();
4201
4774
  const completeSession = async (sessionId, handler) => {
@@ -4319,10 +4892,12 @@ function createCloudCommand(options = {}) {
4319
4892
  requestStorageLsFn: options.requestStorageLsFn ?? requestStorageLsViaProxy,
4320
4893
  requestStorageDownloadFn: options.requestStorageDownloadFn ?? requestStorageDownloadViaProxy,
4321
4894
  requestStorageDeleteFn: options.requestStorageDeleteFn ?? requestStorageDeleteViaProxy,
4895
+ requestPaymentSettleViaProxyFn: options.requestPaymentSettleViaProxyFn ?? requestPaymentSettleViaProxy,
4322
4896
  objectLogHomeDir: options.objectLogHomeDir ?? options.backupOptions?.homeDir,
4323
4897
  backupOptions: options.backupOptions,
4324
4898
  proxyQuoteOptions: options.proxyQuoteOptions,
4325
4899
  proxyUploadOptions: options.proxyUploadOptions,
4900
+ proxySettleOptions: options.proxySettleOptions,
4326
4901
  proxyUploadConfirmOptions: options.proxyUploadConfirmOptions,
4327
4902
  subagentOrchestrator,
4328
4903
  proxyStorageOptions: options.proxyStorageOptions
@@ -4371,7 +4946,23 @@ async function resolveFriendlyNameFromManifest(params, homeDir) {
4371
4946
  }
4372
4947
  async function resolveNameSelectorIfNeeded(datastore, request, selector, homeDir) {
4373
4948
  if (!selector) {
4374
- const parsedRequest2 = parseStorageObjectRequest(request);
4949
+ const walletAddress = request.wallet_address?.trim();
4950
+ if (!walletAddress) {
4951
+ return { error: "Cannot resolve storage object request." };
4952
+ }
4953
+ const objectKey = request.object_key?.trim();
4954
+ if (!objectKey) {
4955
+ const listRequest = { wallet_address: walletAddress };
4956
+ if (request.location) {
4957
+ listRequest.location = request.location;
4958
+ }
4959
+ return { request: listRequest };
4960
+ }
4961
+ const parsedRequest2 = parseStorageObjectRequest({
4962
+ wallet_address: walletAddress,
4963
+ object_key: objectKey,
4964
+ location: request.location
4965
+ });
4375
4966
  if (!parsedRequest2) {
4376
4967
  return { error: "Cannot resolve storage object request." };
4377
4968
  }
@@ -4432,6 +5023,21 @@ async function resolveNameSelectorIfNeeded(datastore, request, selector, homeDir
4432
5023
  degradedWarning
4433
5024
  };
4434
5025
  }
5026
+ function toStorageObjectRequestOrError(request, missingKeyMessage) {
5027
+ const key = request.object_key?.trim();
5028
+ if (!key) {
5029
+ return { ok: false, error: missingKeyMessage };
5030
+ }
5031
+ const parsed = parseStorageObjectRequest({
5032
+ wallet_address: request.wallet_address,
5033
+ object_key: key,
5034
+ location: request.location
5035
+ });
5036
+ if (!parsed) {
5037
+ return { ok: false, error: "Cannot resolve storage object request." };
5038
+ }
5039
+ return { ok: true, request: parsed };
5040
+ }
4435
5041
  async function emitCloudEvent(eventType, details, homeDir) {
4436
5042
  await appendJsonlEvent(
4437
5043
  "events.jsonl",
@@ -4486,6 +5092,117 @@ function buildRequestCorrelation(forcedOperationId, forcedTraceId) {
4486
5092
  const traceId = forcedTraceId?.trim() || randomUUID3();
4487
5093
  return { operationId, traceId };
4488
5094
  }
5095
+ function parseTransIdFromPaymentSettleBody(bodyText) {
5096
+ const trimmed = bodyText.trim();
5097
+ if (!trimmed.startsWith("{")) {
5098
+ return null;
5099
+ }
5100
+ try {
5101
+ const parsed = JSON.parse(trimmed);
5102
+ const tid = parsed.trans_id;
5103
+ return typeof tid === "string" && tid.trim() ? tid.trim() : null;
5104
+ } catch {
5105
+ return null;
5106
+ }
5107
+ }
5108
+ async function resolveAmountForPaymentSettle(quoteId, storagePriceFromFlag, datastore, homeDir) {
5109
+ if (storagePriceFromFlag !== void 0 && Number.isFinite(storagePriceFromFlag)) {
5110
+ return storagePriceFromFlag;
5111
+ }
5112
+ const quoteLookup = await datastore.findQuoteById(quoteId);
5113
+ if (quoteLookup && Number.isFinite(quoteLookup.storagePrice)) {
5114
+ return quoteLookup.storagePrice;
5115
+ }
5116
+ const logged = await findLoggedPriceStorageQuote(quoteId, homeDir);
5117
+ if (logged && Number.isFinite(logged.storagePrice)) {
5118
+ return logged.storagePrice;
5119
+ }
5120
+ const payment = await datastore.findPaymentByQuoteId(quoteId);
5121
+ if (payment && Number.isFinite(payment.amount)) {
5122
+ return payment.amount;
5123
+ }
5124
+ return 0;
5125
+ }
5126
+ async function emitPaymentSettleClientObservationBestEffort(params) {
5127
+ try {
5128
+ const {
5129
+ phase,
5130
+ correlation,
5131
+ quoteId,
5132
+ walletAddress,
5133
+ objectId,
5134
+ objectKey,
5135
+ httpStatus,
5136
+ outcomeStatus,
5137
+ homeDir
5138
+ } = params;
5139
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
5140
+ if (phase === "start") {
5141
+ await emitCloudEventBestEffort(
5142
+ "payment-settle.started",
5143
+ {
5144
+ operation_id: correlation.operationId,
5145
+ trace_id: correlation.traceId,
5146
+ quote_id: quoteId,
5147
+ wallet_address: walletAddress,
5148
+ object_id: objectId,
5149
+ object_key: objectKey,
5150
+ status: "running"
5151
+ },
5152
+ homeDir
5153
+ );
5154
+ await appendJsonlEvent(
5155
+ "proxy-events.jsonl",
5156
+ {
5157
+ ts,
5158
+ event_type: "payment.settle",
5159
+ status: "start",
5160
+ trace_id: correlation.traceId,
5161
+ operation_id: correlation.operationId,
5162
+ quote_id: quoteId,
5163
+ wallet_address: walletAddress,
5164
+ object_id: objectId ?? null,
5165
+ object_key: objectKey ?? null,
5166
+ details: { source: "client" }
5167
+ },
5168
+ homeDir
5169
+ );
5170
+ return;
5171
+ }
5172
+ const terminal = outcomeStatus ?? "failed";
5173
+ await emitCloudEventBestEffort(
5174
+ "payment-settle.completed",
5175
+ {
5176
+ operation_id: correlation.operationId,
5177
+ trace_id: correlation.traceId,
5178
+ quote_id: quoteId,
5179
+ wallet_address: walletAddress,
5180
+ object_id: objectId,
5181
+ object_key: objectKey,
5182
+ status: terminal === "succeeded" ? "succeeded" : "failed",
5183
+ http_status: httpStatus
5184
+ },
5185
+ homeDir
5186
+ );
5187
+ await appendJsonlEvent(
5188
+ "proxy-events.jsonl",
5189
+ {
5190
+ ts,
5191
+ event_type: "payment.settle",
5192
+ status: "result",
5193
+ trace_id: correlation.traceId,
5194
+ operation_id: correlation.operationId,
5195
+ quote_id: quoteId,
5196
+ wallet_address: walletAddress,
5197
+ object_id: objectId ?? null,
5198
+ object_key: objectKey ?? null,
5199
+ details: { http_status: httpStatus ?? null, source: "client" }
5200
+ },
5201
+ homeDir
5202
+ );
5203
+ } catch {
5204
+ }
5205
+ }
4489
5206
  async function runCloudCommandHandler(ctx, options, executionContext = {}) {
4490
5207
  const parsed = parseCloudArgs(ctx.args);
4491
5208
  const objectLogHomeDir = options.objectLogHomeDir;
@@ -4501,6 +5218,7 @@ async function runCloudCommandHandler(ctx, options, executionContext = {}) {
4501
5218
  const requestStorageLs = options.requestStorageLsFn;
4502
5219
  const requestStorageDownload = options.requestStorageDownloadFn;
4503
5220
  const requestStorageDelete = options.requestStorageDeleteFn;
5221
+ const requestPaymentSettleViaProxy2 = options.requestPaymentSettleViaProxyFn;
4504
5222
  const subagentOrchestrator = options.subagentOrchestrator;
4505
5223
  if (parsed.mode === "help" || parsed.mode === "unknown") {
4506
5224
  return {
@@ -4538,9 +5256,15 @@ async function runCloudCommandHandler(ctx, options, executionContext = {}) {
4538
5256
  isError: true
4539
5257
  };
4540
5258
  }
5259
+ if (parsed.mode === "payment-settle-invalid") {
5260
+ return {
5261
+ text: `Cannot settle payment: required arguments are ${REQUIRED_PAYMENT_SETTLE}. Optional: --object-id, --object-key, --storage-price.`,
5262
+ isError: true
5263
+ };
5264
+ }
4541
5265
  if (parsed.mode === "ls-invalid") {
4542
5266
  return {
4543
- text: `Cannot list storage object: required arguments are ${REQUIRED_STORAGE_OBJECT}.`,
5267
+ text: `Cannot list storage object: required arguments are ${REQUIRED_LS}.`,
4544
5268
  isError: true
4545
5269
  };
4546
5270
  }
@@ -4682,6 +5406,142 @@ async function runCloudCommandHandler(ctx, options, executionContext = {}) {
4682
5406
  }
4683
5407
  return formatOperationStatus(operation);
4684
5408
  }
5409
+ if (parsed.mode === "payment-settle") {
5410
+ const req = parsed.paymentSettleRequest;
5411
+ let walletKey;
5412
+ try {
5413
+ walletKey = await resolveWalletKey(objectLogHomeDir);
5414
+ } catch (err) {
5415
+ const message = err instanceof Error ? err.message : String(err);
5416
+ return { text: message.trim() || "Cannot resolve wallet key.", isError: true };
5417
+ }
5418
+ const walletAccount = privateKeyToAccount5(walletKey);
5419
+ if (walletAccount.address.toLowerCase() !== req.wallet_address.toLowerCase()) {
5420
+ return {
5421
+ text: `Cannot settle payment: wallet key address ${walletAccount.address} does not match --wallet-address ${req.wallet_address}.`,
5422
+ isError: true
5423
+ };
5424
+ }
5425
+ const correlation = buildRequestCorrelation();
5426
+ const settleFetch = createPayment(walletKey).fetch;
5427
+ const objectId = req.object_id;
5428
+ const objectKey = req.object_key;
5429
+ await emitPaymentSettleClientObservationBestEffort({
5430
+ phase: "start",
5431
+ correlation,
5432
+ quoteId: req.quote_id,
5433
+ walletAddress: req.wallet_address,
5434
+ objectId,
5435
+ objectKey,
5436
+ homeDir: objectLogHomeDir
5437
+ });
5438
+ let settleResult;
5439
+ try {
5440
+ settleResult = await requestPaymentSettleViaProxy2(req.quote_id, req.wallet_address, {
5441
+ ...options.proxySettleOptions,
5442
+ correlation,
5443
+ fetchImpl: (input, init) => settleFetch(input, init)
5444
+ });
5445
+ } catch (err) {
5446
+ const amountErr = await resolveAmountForPaymentSettle(
5447
+ req.quote_id,
5448
+ req.storage_price,
5449
+ datastore,
5450
+ objectLogHomeDir
5451
+ );
5452
+ await datastore.upsertPayment({
5453
+ quote_id: req.quote_id,
5454
+ wallet_address: req.wallet_address,
5455
+ trans_id: null,
5456
+ amount: amountErr,
5457
+ network: null,
5458
+ status: "settle_failed",
5459
+ settled_at: null
5460
+ });
5461
+ const cronErr = await datastore.findCronByQuoteId(req.quote_id);
5462
+ if (cronErr) {
5463
+ await datastore.upsertCronJob({ ...cronErr, status: "active" });
5464
+ }
5465
+ const msg = err instanceof Error ? err.message : String(err);
5466
+ await emitPaymentSettleClientObservationBestEffort({
5467
+ phase: "result",
5468
+ correlation,
5469
+ quoteId: req.quote_id,
5470
+ walletAddress: req.wallet_address,
5471
+ objectId,
5472
+ objectKey,
5473
+ outcomeStatus: "failed",
5474
+ homeDir: objectLogHomeDir
5475
+ });
5476
+ return { text: `Payment settle failed: ${msg}`, isError: true };
5477
+ }
5478
+ const amount = await resolveAmountForPaymentSettle(
5479
+ req.quote_id,
5480
+ req.storage_price,
5481
+ datastore,
5482
+ objectLogHomeDir
5483
+ );
5484
+ const transId = settleResult.status === 200 ? parseTransIdFromPaymentSettleBody(settleResult.bodyText ?? "") : null;
5485
+ if (settleResult.status === 200) {
5486
+ await datastore.upsertPayment({
5487
+ quote_id: req.quote_id,
5488
+ wallet_address: req.wallet_address,
5489
+ trans_id: transId,
5490
+ amount,
5491
+ network: null,
5492
+ status: "settled",
5493
+ settled_at: (/* @__PURE__ */ new Date()).toISOString()
5494
+ });
5495
+ const cronRow = await datastore.findCronByQuoteId(req.quote_id);
5496
+ if (cronRow) {
5497
+ await datastore.upsertCronJob({ ...cronRow, status: "active" });
5498
+ }
5499
+ await emitPaymentSettleClientObservationBestEffort({
5500
+ phase: "result",
5501
+ correlation,
5502
+ quoteId: req.quote_id,
5503
+ walletAddress: req.wallet_address,
5504
+ objectId,
5505
+ objectKey,
5506
+ httpStatus: settleResult.status,
5507
+ outcomeStatus: "succeeded",
5508
+ homeDir: objectLogHomeDir
5509
+ });
5510
+ return {
5511
+ text: transId ? `Payment settled for quote ${req.quote_id} (trans_id: ${transId}).` : `Payment settled for quote ${req.quote_id}.`
5512
+ };
5513
+ }
5514
+ await datastore.upsertPayment({
5515
+ quote_id: req.quote_id,
5516
+ wallet_address: req.wallet_address,
5517
+ trans_id: transId,
5518
+ amount,
5519
+ network: null,
5520
+ status: "settle_failed",
5521
+ settled_at: null
5522
+ });
5523
+ const cronRowFailed = await datastore.findCronByQuoteId(req.quote_id);
5524
+ if (cronRowFailed) {
5525
+ await datastore.upsertCronJob({ ...cronRowFailed, status: "active" });
5526
+ }
5527
+ await emitPaymentSettleClientObservationBestEffort({
5528
+ phase: "result",
5529
+ correlation,
5530
+ quoteId: req.quote_id,
5531
+ walletAddress: req.wallet_address,
5532
+ objectId,
5533
+ objectKey,
5534
+ httpStatus: settleResult.status,
5535
+ outcomeStatus: "failed",
5536
+ homeDir: objectLogHomeDir
5537
+ });
5538
+ const bodySnippet = settleResult.bodyText?.trim();
5539
+ const detail = bodySnippet && bodySnippet.length > 500 ? `${bodySnippet.slice(0, 500)}\u2026` : bodySnippet;
5540
+ return {
5541
+ text: detail ? `Payment settle failed (HTTP ${settleResult.status}): ${detail}` : `Payment settle failed with HTTP ${settleResult.status}.`,
5542
+ isError: true
5543
+ };
5544
+ }
4685
5545
  if ((parsed.mode === "backup" || parsed.mode === "upload" || parsed.mode === "download") && parsed.async) {
4686
5546
  const asyncCorrelation = buildRequestCorrelation();
4687
5547
  const operationId = asyncCorrelation.operationId;
@@ -5192,7 +6052,7 @@ operation-id: ${operationId}`,
5192
6052
  const shouldSettleBeforeUpload = requestStorageUpload !== requestStorageUploadViaProxy;
5193
6053
  if (shouldSettleBeforeUpload) {
5194
6054
  const paymentFetch = createPayment(walletKey).fetch;
5195
- const settleResult = await requestPaymentSettleViaProxy(
6055
+ const settleResult = await requestPaymentSettleViaProxy2(
5196
6056
  parsed.uploadRequest.quote_id,
5197
6057
  parsed.uploadRequest.wallet_address,
5198
6058
  {
@@ -5399,15 +6259,17 @@ operation-id: ${operationId}`,
5399
6259
  "name_resolution.degraded",
5400
6260
  {
5401
6261
  wallet_address: resolvedRequest.wallet_address,
5402
- object_key: resolvedRequest.object_key,
6262
+ object_key: resolvedRequest.object_key ?? null,
5403
6263
  warning: resolved.degradedWarning
5404
6264
  },
5405
6265
  objectLogHomeDir
5406
6266
  );
5407
6267
  }
6268
+ const objectKeyForLs = resolvedRequest.object_key?.trim();
6269
+ const isBucketList = !objectKeyForLs;
5408
6270
  const correlation = buildRequestCorrelation();
5409
6271
  const operationId = correlation.operationId;
5410
- const knownObject = await datastore.findObjectByObjectKey(resolvedRequest.object_key);
6272
+ const knownObject = isBucketList ? null : await datastore.findObjectByObjectKey(objectKeyForLs);
5411
6273
  const operationObjectId = knownObject?.object_id ?? null;
5412
6274
  await datastore.upsertOperation({
5413
6275
  operation_id: operationId,
@@ -5441,17 +6303,23 @@ operation-id: ${operationId}`,
5441
6303
  operation_id: operationId,
5442
6304
  trace_id: correlation.traceId,
5443
6305
  wallet_address: resolvedRequest.wallet_address,
5444
- object_key: resolvedRequest.object_key,
5445
- status: "succeeded"
6306
+ object_key: resolvedRequest.object_key ?? null,
6307
+ status: "succeeded",
6308
+ list_mode: isBucketList
5446
6309
  },
5447
6310
  objectLogHomeDir
5448
6311
  );
5449
- const lsText = formatStorageLsUserMessage(lsResult, resolvedRequest.object_key);
6312
+ const lsText = await buildMnemosparkLsMessage(lsResult, {
6313
+ walletAddress: resolvedRequest.wallet_address,
6314
+ datastore
6315
+ });
5450
6316
  return {
5451
6317
  text: resolved.degradedWarning ? `${resolved.degradedWarning}
6318
+
5452
6319
  ${lsText}` : lsText
5453
6320
  };
5454
- } catch {
6321
+ } catch (error) {
6322
+ const lsErrorMessage = extractLsErrorMessage(error) ?? "Cannot list storage object";
5455
6323
  await datastore.upsertOperation({
5456
6324
  operation_id: operationId,
5457
6325
  type: "ls",
@@ -5459,7 +6327,7 @@ ${lsText}` : lsText
5459
6327
  quote_id: null,
5460
6328
  status: "failed",
5461
6329
  error_code: "LS_FAILED",
5462
- error_message: "Cannot list storage object"
6330
+ error_message: lsErrorMessage
5463
6331
  });
5464
6332
  await emitCloudEventBestEffort(
5465
6333
  "ls.completed",
@@ -5467,13 +6335,14 @@ ${lsText}` : lsText
5467
6335
  operation_id: operationId,
5468
6336
  trace_id: correlation.traceId,
5469
6337
  wallet_address: resolvedRequest.wallet_address,
5470
- object_key: resolvedRequest.object_key,
5471
- status: "failed"
6338
+ object_key: resolvedRequest.object_key ?? null,
6339
+ status: "failed",
6340
+ list_mode: isBucketList
5472
6341
  },
5473
6342
  objectLogHomeDir
5474
6343
  );
5475
6344
  return {
5476
- text: "Cannot list storage object",
6345
+ text: lsErrorMessage,
5477
6346
  isError: true
5478
6347
  };
5479
6348
  }
@@ -5488,7 +6357,14 @@ ${lsText}` : lsText
5488
6357
  if (resolved.error || !resolved.request) {
5489
6358
  return { text: resolved.error ?? "Cannot resolve storage object request.", isError: true };
5490
6359
  }
5491
- const resolvedRequest = resolved.request;
6360
+ const narrowed = toStorageObjectRequestOrError(
6361
+ resolved.request,
6362
+ `Cannot download file: required arguments are ${REQUIRED_STORAGE_OBJECT}.`
6363
+ );
6364
+ if (!narrowed.ok) {
6365
+ return { text: narrowed.error, isError: true };
6366
+ }
6367
+ const resolvedRequest = narrowed.request;
5492
6368
  if (resolved.degradedWarning) {
5493
6369
  await emitCloudEventBestEffort(
5494
6370
  "name_resolution.degraded",
@@ -5586,7 +6462,14 @@ ${downloadText}` : downloadText
5586
6462
  if (resolved.error || !resolved.request) {
5587
6463
  return { text: resolved.error ?? "Cannot resolve storage object request.", isError: true };
5588
6464
  }
5589
- const resolvedRequest = resolved.request;
6465
+ const narrowedDelete = toStorageObjectRequestOrError(
6466
+ resolved.request,
6467
+ `Cannot delete file: required arguments are ${REQUIRED_STORAGE_OBJECT}.`
6468
+ );
6469
+ if (!narrowedDelete.ok) {
6470
+ return { text: narrowedDelete.error, isError: true };
6471
+ }
6472
+ const resolvedRequest = narrowedDelete.request;
5590
6473
  if (resolved.degradedWarning) {
5591
6474
  await emitCloudEventBestEffort(
5592
6475
  "name_resolution.degraded",