nanobazaar-cli 1.0.13 → 1.0.14

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.
Files changed (2) hide show
  1. package/bin/nanobazaar +71 -4
  2. package/package.json +1 -1
package/bin/nanobazaar CHANGED
@@ -734,6 +734,8 @@ Commands:
734
734
  Notify seller that payment was sent
735
735
  poll [--since-event-id <id>] [--limit <n>] [--types a,b] [--no-ack]
736
736
  Poll events and optionally ack
737
+ poll ack --up-to-event-id <id>
738
+ Advance the server-side poll cursor (used for 410 resync)
737
739
  watch [--streams a,b] [--stream-path /v0/stream] [--safety-poll-interval <seconds>]
738
740
  [--state-path <path>] [--openclaw-bin <bin>] [--fswatch-bin <bin>]
739
741
  [--event-text <text>] [--mode now|next] [--debounce-ms <ms>]
@@ -749,6 +751,7 @@ Notes:
749
751
  - Defaults to relay: ${DEFAULT_RELAY_URL}
750
752
  - Uses NBR_STATE_PATH for local state (default: ${STATE_DEFAULT})
751
753
  - Job payloads are encrypted with libsodium (install deps in skills/nanobazaar)
754
+ - If --since-event-id is omitted, /v0/poll uses the relay's server-side cursor (last_acked_event_id)
752
755
  `);
753
756
  }
754
757
 
@@ -1373,7 +1376,10 @@ async function runPoll(argv, options) {
1373
1376
  const {keys} = requireKeys(state);
1374
1377
  const identity = deriveIdentity(keys);
1375
1378
 
1376
- const since = flags.sinceEventId || state.last_acked_event_id;
1379
+ // NOTE: By default, rely on the relay's server-side cursor (last_acked_event_id).
1380
+ // Passing a local cursor can silently skip events if the state file is reused
1381
+ // across relays/bots. Use --since-event-id explicitly to override.
1382
+ const since = flags.sinceEventId;
1377
1383
  const limit = flags.limit || config.poll_limit;
1378
1384
  const types = flags.types || config.poll_types;
1379
1385
 
@@ -1413,7 +1419,7 @@ async function runPoll(argv, options) {
1413
1419
  const addedEvents = appendEvents(state, events);
1414
1420
  debugLog('response', {status: result.response.status, events: events.length, added: addedEvents});
1415
1421
 
1416
- if (typeof state.last_acked_event_id !== 'number') {
1422
+ if (typeof state.last_acked_event_id !== 'number' || !Number.isFinite(state.last_acked_event_id)) {
1417
1423
  state.last_acked_event_id = 0;
1418
1424
  }
1419
1425
  state.relay_url = config.relay_url;
@@ -1448,9 +1454,16 @@ async function runPoll(argv, options) {
1448
1454
  }
1449
1455
  debugLog('ack ok', {last_acked_event_id: ackedId});
1450
1456
 
1451
- if (typeof ackedId === 'number' && ackedId !== state.last_acked_event_id) {
1457
+ if (typeof ackedId === 'number' && Number.isFinite(ackedId) && ackedId !== state.last_acked_event_id) {
1452
1458
  state.last_acked_event_id = ackedId;
1453
- saveStateMerged(config.state_path, state);
1459
+ // Write the authoritative server-side cursor as-is (even if it moves "backwards"
1460
+ // compared to a stale local file).
1461
+ saveStateMerged(config.state_path, state, {
1462
+ mergeLastAcked: false,
1463
+ mergeStreamCursors: true,
1464
+ mergeKnownMaps: true,
1465
+ mergeEventLog: true,
1466
+ });
1454
1467
  }
1455
1468
  }
1456
1469
 
@@ -1463,6 +1476,56 @@ async function runPoll(argv, options) {
1463
1476
  return {events, ackedId, maxEventId};
1464
1477
  }
1465
1478
 
1479
+ async function runPollAck(argv) {
1480
+ const {flags, positionals} = parseArgs(argv);
1481
+ const value = flags.upToEventId ?? flags.upTo ?? flags.eventId ?? flags.id ?? positionals[0];
1482
+ if (value === undefined || value === null || String(value).trim() === '') {
1483
+ throw new Error('Missing up_to_event_id. Provide --up-to-event-id or a positional id.');
1484
+ }
1485
+ const parsed = Number(String(value));
1486
+ if (!Number.isFinite(parsed) || parsed < 0) {
1487
+ throw new Error(`Invalid up_to_event_id: ${value}`);
1488
+ }
1489
+ const upToEventId = Math.floor(parsed);
1490
+
1491
+ const config = buildConfig();
1492
+ const state = loadState(config.state_path);
1493
+ const {keys} = requireKeys(state);
1494
+ const identity = deriveIdentity(keys);
1495
+
1496
+ const result = await signedRequest({
1497
+ method: 'POST',
1498
+ path: '/v0/poll/ack',
1499
+ body: {up_to_event_id: upToEventId},
1500
+ idempotencyKey: crypto.randomBytes(16).toString('hex'),
1501
+ relayUrl: config.relay_url,
1502
+ keys,
1503
+ identity,
1504
+ });
1505
+
1506
+ if (!result.response.ok) {
1507
+ throw new Error(`Ack failed (${result.response.status}): ${result.text || result.response.statusText}`);
1508
+ }
1509
+
1510
+ const ackedId = result.data && typeof result.data.last_acked_event_id === 'number'
1511
+ ? result.data.last_acked_event_id
1512
+ : upToEventId;
1513
+
1514
+ state.last_acked_event_id = ackedId;
1515
+ state.relay_url = config.relay_url;
1516
+ state.bot_id = identity.botId;
1517
+ state.signing_kid = identity.signingKid;
1518
+ state.encryption_kid = identity.encryptionKid;
1519
+ saveStateMerged(config.state_path, state, {
1520
+ mergeLastAcked: false,
1521
+ mergeStreamCursors: true,
1522
+ mergeKnownMaps: true,
1523
+ mergeEventLog: true,
1524
+ });
1525
+
1526
+ printJson(result.data, !!flags.compact);
1527
+ }
1528
+
1466
1529
  function parseCsv(value) {
1467
1530
  if (value === undefined || value === null) {
1468
1531
  return [];
@@ -2268,6 +2331,10 @@ async function main() {
2268
2331
  throw new Error('Unknown job command. Use: job create|reissue-request|reissue-charge|payment-sent');
2269
2332
  }
2270
2333
  case 'poll':
2334
+ if (rest[0] === 'ack') {
2335
+ await runPollAck(rest.slice(1));
2336
+ return;
2337
+ }
2271
2338
  await runPoll(rest);
2272
2339
  return;
2273
2340
  case 'watch':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanobazaar-cli",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "NanoBazaar CLI for the NanoBazaar Relay and OpenClaw skill.",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {