nanobazaar-cli 2.0.1 → 2.0.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to `nanobazaar-cli` are documented in this file.
4
4
 
5
5
  This project follows Semantic Versioning.
6
6
 
7
+ ## [2.0.3] - 2026-02-09
8
+
9
+ ### Changed
10
+ - `nanobazaar watch` now wakes only on relay wake events; removed the safety interval.
11
+
12
+ ## [2.0.2] - 2026-02-08
13
+
14
+ ### Fixed
15
+ - `job mark-paid` now defaults to an idempotency key derived from the request payload (prevents `409 idempotency collision` when retrying with updated evidence).
16
+
17
+ ### Added
18
+ - `NBR_IDEMPOTENCY_KEY` env override for commands that accept `--idempotency-key` (`job charge|mark-paid|deliver|reissue-charge`).
19
+
7
20
  ## [2.0.1] - 2026-02-08
8
21
 
9
22
  ### Changed
package/bin/nanobazaar CHANGED
@@ -74,6 +74,35 @@ function sha256Hex(buffer) {
74
74
  return crypto.createHash('sha256').update(buffer).digest('hex');
75
75
  }
76
76
 
77
+ function stableJsonNormalize(value) {
78
+ if (value === undefined) {
79
+ return null;
80
+ }
81
+ if (value === null || typeof value !== 'object') {
82
+ return value;
83
+ }
84
+ if (typeof value.toJSON === 'function') {
85
+ return stableJsonNormalize(value.toJSON());
86
+ }
87
+ if (Array.isArray(value)) {
88
+ return value.map((entry) => stableJsonNormalize(entry));
89
+ }
90
+ const out = {};
91
+ for (const key of Object.keys(value).sort()) {
92
+ const entry = value[key];
93
+ // Match JSON.stringify: omit undefined values in objects.
94
+ if (entry === undefined) {
95
+ continue;
96
+ }
97
+ out[key] = stableJsonNormalize(entry);
98
+ }
99
+ return out;
100
+ }
101
+
102
+ function stableJsonStringify(value) {
103
+ return JSON.stringify(stableJsonNormalize(value));
104
+ }
105
+
77
106
  function loadState(filePath) {
78
107
  try {
79
108
  const raw = fs.readFileSync(filePath, 'utf8');
@@ -289,6 +318,17 @@ function getEnvValue(name) {
289
318
  return value && value.trim() ? value.trim() : '';
290
319
  }
291
320
 
321
+ function resolveIdempotencyKey(flags, fallback) {
322
+ if (flags && flags.idempotencyKey) {
323
+ return String(flags.idempotencyKey);
324
+ }
325
+ const envKey = getEnvValue('NBR_IDEMPOTENCY_KEY');
326
+ if (envKey) {
327
+ return envKey;
328
+ }
329
+ return fallback ? String(fallback) : '';
330
+ }
331
+
292
332
  function expandHomePath(value) {
293
333
  if (!value) {
294
334
  return value;
@@ -1235,10 +1275,10 @@ Commands:
1235
1275
  Poll events and optionally ack
1236
1276
  poll ack --up-to-event-id <id>
1237
1277
  Advance the server-side poll cursor (used for 410 resync)
1238
- watch [--stream-path /v0/stream] [--safety-wake-interval <seconds>]
1278
+ watch [--stream-path /v0/stream]
1239
1279
  [--state-path <path>] [--openclaw-bin <bin>] [--event-text <text>]
1240
1280
  [--mode now|next] [--no-openclaw]
1241
- Maintain SSE connection; wake OpenClaw on relay wake events + on a safety interval.
1281
+ Maintain SSE connection; wake OpenClaw on relay wake events.
1242
1282
  Does not poll or ack (OpenClaw should run /nanobazaar poll).
1243
1283
 
1244
1284
  Global flags:
@@ -1990,7 +2030,7 @@ async function runJobCharge(argv) {
1990
2030
  method: 'POST',
1991
2031
  path: `/v0/jobs/${jobId}/charge`,
1992
2032
  body: payload,
1993
- idempotencyKey: String(flags.idempotencyKey || chargeId),
2033
+ idempotencyKey: resolveIdempotencyKey(flags, chargeId),
1994
2034
  relayUrl: config.relay_url,
1995
2035
  keys,
1996
2036
  identity,
@@ -2060,11 +2100,15 @@ async function runJobMarkPaid(argv) {
2060
2100
  const {keys} = requireKeys(state);
2061
2101
  const identity = deriveIdentity(keys);
2062
2102
 
2103
+ const requestBody = payload && Object.keys(payload).length > 0 ? payload : undefined;
2104
+ const bodyHash = sha256Hex(stableJsonStringify(requestBody)).slice(0, 16);
2105
+ const defaultIdempotencyKey = `mark_paid:${jobId}:${bodyHash}`;
2106
+
2063
2107
  const result = await signedRequest({
2064
2108
  method: 'POST',
2065
2109
  path: `/v0/jobs/${jobId}/mark_paid`,
2066
- body: payload && Object.keys(payload).length > 0 ? payload : undefined,
2067
- idempotencyKey: String(flags.idempotencyKey || `mark_paid:${jobId}`),
2110
+ body: requestBody,
2111
+ idempotencyKey: resolveIdempotencyKey(flags, defaultIdempotencyKey),
2068
2112
  relayUrl: config.relay_url,
2069
2113
  keys,
2070
2114
  identity,
@@ -2144,7 +2188,7 @@ async function runJobDeliver(argv) {
2144
2188
  method: 'POST',
2145
2189
  path: `/v0/jobs/${jobId}/deliver`,
2146
2190
  body: {payload: built.envelope},
2147
- idempotencyKey: String(flags.idempotencyKey || payloadId),
2191
+ idempotencyKey: resolveIdempotencyKey(flags, payloadId),
2148
2192
  relayUrl: config.relay_url,
2149
2193
  keys,
2150
2194
  identity,
@@ -2272,7 +2316,7 @@ async function runJobReissueCharge(argv) {
2272
2316
  method: 'POST',
2273
2317
  path: `/v0/jobs/${jobId}/charge/reissue`,
2274
2318
  body: payload,
2275
- idempotencyKey: String(flags.idempotencyKey || payload.charge_id),
2319
+ idempotencyKey: resolveIdempotencyKey(flags, payload.charge_id),
2276
2320
  relayUrl: config.relay_url,
2277
2321
  keys,
2278
2322
  identity,
@@ -2991,11 +3035,9 @@ async function runWatch(argv) {
2991
3035
  throw new Error('watch no longer polls. Remove polling flags (--no-ack/--no-fetch-payloads/--limit/--types/--print-polls/--output) and retry.');
2992
3036
  }
2993
3037
 
2994
- const safetyIntervalSeconds = flags.safetyWakeInterval
2995
- ? parsePositiveInt(flags.safetyWakeInterval, '--safety-wake-interval')
2996
- : flags.safetyPollInterval
2997
- ? parsePositiveInt(flags.safetyPollInterval, '--safety-poll-interval')
2998
- : 180;
3038
+ if (flags.safetyWakeInterval !== undefined || flags.safetyPollInterval !== undefined) {
3039
+ throw new Error('Safety wake interval has been removed. Remove --safety-wake-interval/--safety-poll-interval and retry.');
3040
+ }
2999
3041
 
3000
3042
  const openclawBin = String(flags.openclawBin || 'openclaw');
3001
3043
  const mode = String(flags.mode || 'now');
@@ -3006,7 +3048,6 @@ async function runWatch(argv) {
3006
3048
  relay_url: config.relay_url,
3007
3049
  state_path: statePath,
3008
3050
  stream_path: streamPath,
3009
- safety_wake_interval_seconds: safetyIntervalSeconds,
3010
3051
  openclaw_enabled: openclawEnabled,
3011
3052
  });
3012
3053
 
@@ -3047,7 +3088,7 @@ async function runWatch(argv) {
3047
3088
  const label = queuedReason || reason;
3048
3089
  queuedReason = null;
3049
3090
 
3050
- // Best-effort: waking OpenClaw can be retried on the next wake/safety interval.
3091
+ // Best-effort: waking OpenClaw can be retried on the next wake event.
3051
3092
  const didWake = triggerOpenclawWake(label);
3052
3093
  console.error(`[watch] wake ${didWake ? 'ok' : 'skipped'} (${label})`);
3053
3094
  }
@@ -3070,17 +3111,6 @@ async function runWatch(argv) {
3070
3111
  console.error(`[watch] relay=${config.relay_url}`);
3071
3112
  console.error(`[watch] state_path=${statePath}`);
3072
3113
  console.error(`[watch] stream_path=${streamPath}`);
3073
- console.error(`[watch] safety_wake_interval_seconds=${safetyIntervalSeconds}`);
3074
-
3075
- const safetyTimer = setInterval(() => {
3076
- if (signal.aborted) {
3077
- return;
3078
- }
3079
- void runWakeLoop('safety');
3080
- }, safetyIntervalSeconds * 1000);
3081
-
3082
- // Kick off an initial wake so the agent can poll even if wakeups are missed.
3083
- void runWakeLoop('startup');
3084
3114
 
3085
3115
  let attempt = 0;
3086
3116
  function isWakeEvent(evt) {
@@ -3157,7 +3187,6 @@ async function runWatch(argv) {
3157
3187
  }
3158
3188
  }
3159
3189
 
3160
- clearInterval(safetyTimer);
3161
3190
  }
3162
3191
 
3163
3192
  const DISPATCH_BOOL_FLAGS = new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanobazaar-cli",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "NanoBazaar CLI for the NanoBazaar Relay and OpenClaw skill.",
5
5
  "homepage": "https://github.com/nanobazaar/nanobazaar/tree/main/packages/nanobazaar-cli#readme",
6
6
  "license": "UNLICENSED",