nanobazaar-cli 2.0.0 → 2.0.2
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 +14 -0
- package/bin/nanobazaar +83 -65
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.2] - 2026-02-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `job mark-paid` now defaults to an idempotency key derived from the request payload (prevents `409 idempotency collision` when retrying with updated evidence).
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `NBR_IDEMPOTENCY_KEY` env override for commands that accept `--idempotency-key` (`job charge|mark-paid|deliver|reissue-charge`).
|
|
14
|
+
|
|
15
|
+
## [2.0.1] - 2026-02-08
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `nanobazaar watch` is now notifier-only: it keeps an SSE connection and wakes OpenClaw on relay wake events + a safety interval, but does not poll or ack.
|
|
19
|
+
- Added `--safety-wake-interval` (alias for `--safety-poll-interval`) to reflect notifier semantics.
|
|
20
|
+
|
|
7
21
|
## [2.0.0] - 2026-02-08
|
|
8
22
|
|
|
9
23
|
### Added
|
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,11 +1275,11 @@ 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-
|
|
1278
|
+
watch [--stream-path /v0/stream] [--safety-wake-interval <seconds>]
|
|
1239
1279
|
[--state-path <path>] [--openclaw-bin <bin>] [--event-text <text>]
|
|
1240
|
-
[--mode now|next] [--no-openclaw]
|
|
1241
|
-
Maintain SSE connection;
|
|
1242
|
-
|
|
1280
|
+
[--mode now|next] [--no-openclaw]
|
|
1281
|
+
Maintain SSE connection; wake OpenClaw on relay wake events + on a safety interval.
|
|
1282
|
+
Does not poll or ack (OpenClaw should run /nanobazaar poll).
|
|
1243
1283
|
|
|
1244
1284
|
Global flags:
|
|
1245
1285
|
--help Show this help
|
|
@@ -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:
|
|
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:
|
|
2067
|
-
idempotencyKey:
|
|
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:
|
|
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:
|
|
2319
|
+
idempotencyKey: resolveIdempotencyKey(flags, payload.charge_id),
|
|
2276
2320
|
relayUrl: config.relay_url,
|
|
2277
2321
|
keys,
|
|
2278
2322
|
identity,
|
|
@@ -2987,14 +3031,15 @@ async function runWatch(argv) {
|
|
|
2987
3031
|
throw new Error('Local fswatch wakeups are removed. Remove --fswatch-bin/--debounce-ms and retry.');
|
|
2988
3032
|
}
|
|
2989
3033
|
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3034
|
+
if (flags.ack !== undefined || flags.fetchPayloads !== undefined || flags.limit !== undefined || flags.output !== undefined || flags.printPolls !== undefined || flags.types !== undefined) {
|
|
3035
|
+
throw new Error('watch no longer polls. Remove polling flags (--no-ack/--no-fetch-payloads/--limit/--types/--print-polls/--output) and retry.');
|
|
3036
|
+
}
|
|
2993
3037
|
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3038
|
+
const safetyIntervalSeconds = flags.safetyWakeInterval
|
|
3039
|
+
? parsePositiveInt(flags.safetyWakeInterval, '--safety-wake-interval')
|
|
3040
|
+
: flags.safetyPollInterval
|
|
3041
|
+
? parsePositiveInt(flags.safetyPollInterval, '--safety-poll-interval')
|
|
3042
|
+
: 180;
|
|
2998
3043
|
|
|
2999
3044
|
const openclawBin = String(flags.openclawBin || 'openclaw');
|
|
3000
3045
|
const mode = String(flags.mode || 'now');
|
|
@@ -3005,22 +3050,18 @@ async function runWatch(argv) {
|
|
|
3005
3050
|
relay_url: config.relay_url,
|
|
3006
3051
|
state_path: statePath,
|
|
3007
3052
|
stream_path: streamPath,
|
|
3008
|
-
|
|
3009
|
-
ack: flags.ack !== false,
|
|
3010
|
-
poll_limit: pollLimit ?? null,
|
|
3011
|
-
poll_types: pollTypes ?? null,
|
|
3012
|
-
print_polls: printPolls,
|
|
3053
|
+
safety_wake_interval_seconds: safetyIntervalSeconds,
|
|
3013
3054
|
openclaw_enabled: openclawEnabled,
|
|
3014
3055
|
});
|
|
3015
3056
|
|
|
3016
|
-
let pollInFlight = false;
|
|
3017
|
-
let pollQueued = false;
|
|
3018
|
-
let queuedReason = null;
|
|
3019
3057
|
let openclawMissing = false;
|
|
3058
|
+
let wakeInFlight = false;
|
|
3059
|
+
let wakeQueued = false;
|
|
3060
|
+
let queuedReason = null;
|
|
3020
3061
|
|
|
3021
3062
|
function triggerOpenclawWake(reason) {
|
|
3022
3063
|
if (!openclawEnabled || openclawMissing) {
|
|
3023
|
-
return;
|
|
3064
|
+
return false;
|
|
3024
3065
|
}
|
|
3025
3066
|
if (debugLog) {
|
|
3026
3067
|
debugLog('openclaw wake', {reason, openclaw_bin: openclawBin, mode, event_text: eventText});
|
|
@@ -3032,53 +3073,30 @@ async function runWatch(argv) {
|
|
|
3032
3073
|
if (debugLog) {
|
|
3033
3074
|
debugLog('openclaw wake failed', result.error && result.error.message ? result.error.message : String(result.error));
|
|
3034
3075
|
}
|
|
3076
|
+
return false;
|
|
3035
3077
|
}
|
|
3078
|
+
return true;
|
|
3036
3079
|
}
|
|
3037
3080
|
|
|
3038
|
-
async function
|
|
3081
|
+
async function runWakeLoop(reason) {
|
|
3039
3082
|
queuedReason = reason;
|
|
3040
|
-
|
|
3041
|
-
if (
|
|
3083
|
+
wakeQueued = true;
|
|
3084
|
+
if (wakeInFlight) {
|
|
3042
3085
|
return;
|
|
3043
3086
|
}
|
|
3044
|
-
|
|
3087
|
+
wakeInFlight = true;
|
|
3045
3088
|
try {
|
|
3046
|
-
while (
|
|
3047
|
-
|
|
3089
|
+
while (wakeQueued) {
|
|
3090
|
+
wakeQueued = false;
|
|
3048
3091
|
const label = queuedReason || reason;
|
|
3049
3092
|
queuedReason = null;
|
|
3050
|
-
try {
|
|
3051
|
-
const {response, events, addedEvents, ackedId} = await pollOnce({
|
|
3052
|
-
config,
|
|
3053
|
-
state,
|
|
3054
|
-
statePath,
|
|
3055
|
-
keys,
|
|
3056
|
-
identity,
|
|
3057
|
-
since: undefined,
|
|
3058
|
-
limit: pollLimit,
|
|
3059
|
-
types: pollTypes,
|
|
3060
|
-
ack: flags.ack !== false,
|
|
3061
|
-
fetchPayloads: flags.fetchPayloads !== false,
|
|
3062
|
-
quiet: true,
|
|
3063
|
-
compact: !!flags.compact,
|
|
3064
|
-
debugLog,
|
|
3065
|
-
});
|
|
3066
3093
|
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
console.error(`[watch] poll ok (${label}): events=${events.length} added=${addedEvents} ack=${flags.ack === false ? 'disabled' : ackedId}`);
|
|
3072
|
-
if (addedEvents > 0) {
|
|
3073
|
-
triggerOpenclawWake(label);
|
|
3074
|
-
}
|
|
3075
|
-
} catch (err) {
|
|
3076
|
-
console.error(`[watch] poll error (${label}): ${err && err.message ? err.message : String(err)}`);
|
|
3077
|
-
// Keep running; polling is idempotent and can be retried on the next wake/safety interval.
|
|
3078
|
-
}
|
|
3094
|
+
// Best-effort: waking OpenClaw can be retried on the next wake/safety interval.
|
|
3095
|
+
const didWake = triggerOpenclawWake(label);
|
|
3096
|
+
console.error(`[watch] wake ${didWake ? 'ok' : 'skipped'} (${label})`);
|
|
3079
3097
|
}
|
|
3080
3098
|
} finally {
|
|
3081
|
-
|
|
3099
|
+
wakeInFlight = false;
|
|
3082
3100
|
}
|
|
3083
3101
|
}
|
|
3084
3102
|
|
|
@@ -3096,17 +3114,17 @@ async function runWatch(argv) {
|
|
|
3096
3114
|
console.error(`[watch] relay=${config.relay_url}`);
|
|
3097
3115
|
console.error(`[watch] state_path=${statePath}`);
|
|
3098
3116
|
console.error(`[watch] stream_path=${streamPath}`);
|
|
3099
|
-
console.error(`[watch]
|
|
3117
|
+
console.error(`[watch] safety_wake_interval_seconds=${safetyIntervalSeconds}`);
|
|
3100
3118
|
|
|
3101
3119
|
const safetyTimer = setInterval(() => {
|
|
3102
3120
|
if (signal.aborted) {
|
|
3103
3121
|
return;
|
|
3104
3122
|
}
|
|
3105
|
-
void
|
|
3123
|
+
void runWakeLoop('safety');
|
|
3106
3124
|
}, safetyIntervalSeconds * 1000);
|
|
3107
3125
|
|
|
3108
|
-
// Kick off an initial
|
|
3109
|
-
void
|
|
3126
|
+
// Kick off an initial wake so the agent can poll even if wakeups are missed.
|
|
3127
|
+
void runWakeLoop('startup');
|
|
3110
3128
|
|
|
3111
3129
|
let attempt = 0;
|
|
3112
3130
|
function isWakeEvent(evt) {
|
|
@@ -3160,7 +3178,7 @@ async function runWatch(argv) {
|
|
|
3160
3178
|
});
|
|
3161
3179
|
if (isWakeEvent(evt)) {
|
|
3162
3180
|
debugLog('wake detected', {});
|
|
3163
|
-
void
|
|
3181
|
+
void runWakeLoop('wake');
|
|
3164
3182
|
}
|
|
3165
3183
|
},
|
|
3166
3184
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanobazaar-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
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",
|