nanobazaar-cli 1.0.13 → 1.0.15
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 +51 -0
- package/README.md +1 -0
- package/bin/nanobazaar +773 -16
- package/package.json +13 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `nanobazaar-cli` are documented in this file.
|
|
4
|
+
|
|
5
|
+
This project follows Semantic Versioning.
|
|
6
|
+
|
|
7
|
+
## [1.0.15] - 2026-02-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `nanobazaar payload list` to list payload metadata for the current bot.
|
|
11
|
+
- `nanobazaar payload fetch` to fetch, decrypt, verify, and cache payloads locally.
|
|
12
|
+
- Automatic payload fetch/decrypt/verify/cache during `nanobazaar poll` and `nanobazaar watch` (disable via `--no-fetch-payloads`).
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- `payload fetch --job-id ...` now falls back to querying the relay when local state/event logs are missing or truncated.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Avoid rewriting the state file when content is unchanged (prevents mtime-only updates that can spam local wakeups).
|
|
19
|
+
|
|
20
|
+
## [1.0.14] - 2026-02-05
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `nanobazaar poll ack --up-to-event-id <id>` helper to advance the server-side poll cursor (used for 410 resync scenarios).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- `nanobazaar poll` now defaults to relying on the relay's server-side cursor unless `--since-event-id` is explicitly provided.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Prevented cursor regressions when multiple processes update local state (server-side `last_acked_event_id` wins).
|
|
30
|
+
|
|
31
|
+
## [1.0.13] - 2026-02-05
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- `--debug` (or `NBR_DEBUG=1`) to enable verbose logging for `poll` and `watch`.
|
|
35
|
+
- Support for global flags appearing before the command.
|
|
36
|
+
|
|
37
|
+
## [1.0.12] - 2026-02-05
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- Unified watcher behavior so `nanobazaar watch` can optionally use `fswatch` for local wakeups when available, while continuing to work in SSE-only mode.
|
|
41
|
+
|
|
42
|
+
## [1.0.11] - 2026-02-05
|
|
43
|
+
|
|
44
|
+
### Removed
|
|
45
|
+
- CLI cron helpers and related docs.
|
|
46
|
+
|
|
47
|
+
## [1.0.10] - 2026-02-05
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Version bump for npm release.
|
|
51
|
+
|
package/README.md
CHANGED
package/bin/nanobazaar
CHANGED
|
@@ -153,7 +153,29 @@ function withStateLock(filePath, fn) {
|
|
|
153
153
|
|
|
154
154
|
function saveState(filePath, state) {
|
|
155
155
|
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
156
|
-
|
|
156
|
+
|
|
157
|
+
// Avoid touching mtime (and triggering fswatch/OpenClaw wakeups) when state
|
|
158
|
+
// content did not actually change.
|
|
159
|
+
const json = JSON.stringify(state, null, 2);
|
|
160
|
+
try {
|
|
161
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
162
|
+
if (existing === json) {
|
|
163
|
+
// Still enforce file permissions without rewriting the file.
|
|
164
|
+
try {
|
|
165
|
+
const mode = fs.statSync(filePath).mode & 0o777;
|
|
166
|
+
if (mode !== 0o600) {
|
|
167
|
+
fs.chmodSync(filePath, 0o600);
|
|
168
|
+
}
|
|
169
|
+
} catch (_) {
|
|
170
|
+
// ignore stat/chmod errors on unsupported platforms
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
} catch (_) {
|
|
175
|
+
// ignore read errors; we'll (re)create the file below
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fs.writeFileSync(filePath, json);
|
|
157
179
|
try {
|
|
158
180
|
fs.chmodSync(filePath, 0o600);
|
|
159
181
|
} catch (_) {
|
|
@@ -700,6 +722,387 @@ async function signedRequest({method, path, query, body, idempotencyKey, relayUr
|
|
|
700
722
|
return {response, data, text};
|
|
701
723
|
}
|
|
702
724
|
|
|
725
|
+
function isPlainObject(value) {
|
|
726
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function decodeBase64url(value, label) {
|
|
730
|
+
try {
|
|
731
|
+
return Buffer.from(String(value), 'base64url');
|
|
732
|
+
} catch (err) {
|
|
733
|
+
throw new Error(`Invalid base64url for ${label}: ${err.message}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const PAYLOAD_CACHE_DIRNAME = 'payloads';
|
|
738
|
+
const PAYLOAD_CIPHERTEXT_MAX_BYTES = 64 * 1024; // best-effort cap; relay should enforce on ingest.
|
|
739
|
+
|
|
740
|
+
function ensurePrivateDir(dirPath) {
|
|
741
|
+
fs.mkdirSync(dirPath, {recursive: true});
|
|
742
|
+
try {
|
|
743
|
+
fs.chmodSync(dirPath, 0o700);
|
|
744
|
+
} catch (_) {
|
|
745
|
+
// ignore chmod errors on unsupported platforms
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function ensurePrivateFile(filePath) {
|
|
750
|
+
try {
|
|
751
|
+
fs.chmodSync(filePath, 0o600);
|
|
752
|
+
} catch (_) {
|
|
753
|
+
// ignore chmod errors on unsupported platforms
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function resolvePayloadCacheDir(statePath) {
|
|
758
|
+
const dir = path.join(path.dirname(statePath), PAYLOAD_CACHE_DIRNAME);
|
|
759
|
+
ensurePrivateDir(dir);
|
|
760
|
+
return dir;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function safePayloadCacheFilename(payloadId) {
|
|
764
|
+
const raw = String(payloadId);
|
|
765
|
+
const trimmed = raw.trim();
|
|
766
|
+
if (trimmed && trimmed.length <= 160 && /^[A-Za-z0-9._-]+$/.test(trimmed)) {
|
|
767
|
+
return `${trimmed}.json`;
|
|
768
|
+
}
|
|
769
|
+
return `payload_${sha256Hex(raw)}.json`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function readJsonFile(filePath, label) {
|
|
773
|
+
try {
|
|
774
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
775
|
+
return JSON.parse(raw);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
throw new Error(`Failed to read ${label} (${filePath}): ${err.message}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function writeJsonFile(filePath, data, compact) {
|
|
782
|
+
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
783
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, compact ? 0 : 2));
|
|
784
|
+
ensurePrivateFile(filePath);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function fetchBot({botId, config, keys, identity}) {
|
|
788
|
+
const result = await signedRequest({
|
|
789
|
+
method: 'GET',
|
|
790
|
+
path: `/v0/bots/${botId}`,
|
|
791
|
+
relayUrl: config.relay_url,
|
|
792
|
+
keys,
|
|
793
|
+
identity,
|
|
794
|
+
});
|
|
795
|
+
if (!result.response.ok) {
|
|
796
|
+
throw new Error(`Failed to fetch bot (${result.response.status}): ${result.text || result.response.statusText}`);
|
|
797
|
+
}
|
|
798
|
+
if (!isPlainObject(result.data)) {
|
|
799
|
+
throw new Error('Bot response was not an object.');
|
|
800
|
+
}
|
|
801
|
+
return result.data;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function fetchPayloadEnvelope({payloadId, config, keys, identity}) {
|
|
805
|
+
const result = await signedRequest({
|
|
806
|
+
method: 'GET',
|
|
807
|
+
path: `/v0/payloads/${payloadId}`,
|
|
808
|
+
relayUrl: config.relay_url,
|
|
809
|
+
keys,
|
|
810
|
+
identity,
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
if (!result.response.ok) {
|
|
814
|
+
throw new Error(`Payload fetch failed (${result.response.status}): ${result.text || result.response.statusText}`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (!isPlainObject(result.data)) {
|
|
818
|
+
throw new Error('Payload response was not an object.');
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const envelope = result.data;
|
|
822
|
+
if (envelope.payload_id && String(envelope.payload_id) !== String(payloadId)) {
|
|
823
|
+
throw new Error(`Payload response payload_id mismatch: expected=${payloadId} got=${envelope.payload_id}`);
|
|
824
|
+
}
|
|
825
|
+
if (envelope.enc_alg && String(envelope.enc_alg) !== ENC_ALG) {
|
|
826
|
+
throw new Error(`Unsupported enc_alg: ${envelope.enc_alg}`);
|
|
827
|
+
}
|
|
828
|
+
return envelope;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function listPayloadMetadata({status, jobId, limit, cursor, config, keys, identity}) {
|
|
832
|
+
const result = await signedRequest({
|
|
833
|
+
method: 'GET',
|
|
834
|
+
path: '/v0/payloads',
|
|
835
|
+
query: {
|
|
836
|
+
status,
|
|
837
|
+
job_id: jobId,
|
|
838
|
+
limit,
|
|
839
|
+
cursor,
|
|
840
|
+
},
|
|
841
|
+
relayUrl: config.relay_url,
|
|
842
|
+
keys,
|
|
843
|
+
identity,
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
if (!result.response.ok) {
|
|
847
|
+
throw new Error(`Payload list failed (${result.response.status}): ${result.text || result.response.statusText}`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (!isPlainObject(result.data)) {
|
|
851
|
+
throw new Error('Payload list response was not an object.');
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return result.data;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function decryptPayloadCiphertext({ciphertextB64, keys}) {
|
|
858
|
+
const ciphertext = decodeBase64url(ciphertextB64, 'ciphertext_b64');
|
|
859
|
+
if (ciphertext.length > PAYLOAD_CIPHERTEXT_MAX_BYTES) {
|
|
860
|
+
throw new Error(`ciphertext_b64 too large (${ciphertext.length} bytes decoded).`);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const sodium = await loadSodium();
|
|
864
|
+
const publicKey = decodeBase64url(keys.encryption_public_key_b64url, 'encryption_public_key_b64url');
|
|
865
|
+
const privateKey = decodeBase64url(keys.encryption_private_key_b64url, 'encryption_private_key_b64url');
|
|
866
|
+
const plaintext = sodium.crypto_box_seal_open(ciphertext, publicKey, privateKey);
|
|
867
|
+
if (!plaintext) {
|
|
868
|
+
throw new Error('Decryption failed. Are you the intended recipient?');
|
|
869
|
+
}
|
|
870
|
+
return Buffer.from(plaintext);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function parseDecryptedPayload(plaintext) {
|
|
874
|
+
const raw = Buffer.from(plaintext).toString('utf8');
|
|
875
|
+
try {
|
|
876
|
+
const parsed = JSON.parse(raw);
|
|
877
|
+
if (!isPlainObject(parsed)) {
|
|
878
|
+
throw new Error('decrypted payload was not an object');
|
|
879
|
+
}
|
|
880
|
+
return parsed;
|
|
881
|
+
} catch (err) {
|
|
882
|
+
const preview = raw.length > 200 ? raw.slice(0, 200) + '...' : raw;
|
|
883
|
+
throw new Error(`Invalid decrypted payload JSON: ${err.message}. Preview: ${preview}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function canonicalPayloadString(inner) {
|
|
888
|
+
if (!inner || typeof inner !== 'object') {
|
|
889
|
+
throw new Error('Missing inner payload.');
|
|
890
|
+
}
|
|
891
|
+
if (inner.prefix !== 'NBR1') {
|
|
892
|
+
throw new Error(`Invalid payload prefix: ${inner.prefix}`);
|
|
893
|
+
}
|
|
894
|
+
if (typeof inner.body !== 'string') {
|
|
895
|
+
throw new Error('Invalid payload body (expected string).');
|
|
896
|
+
}
|
|
897
|
+
const bodyHash = sha256Hex(inner.body);
|
|
898
|
+
return `NBR1|${inner.payload_id}|${inner.job_id}|${inner.payload_kind}|${inner.sender_bot_id}|${inner.recipient_bot_id}|${inner.created_at}|${bodyHash}`;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function verifyPayloadInner({inner, envelope, senderSigningPubkeyEd25519}) {
|
|
902
|
+
if (!isPlainObject(inner)) {
|
|
903
|
+
throw new Error('Invalid decrypted payload (expected object).');
|
|
904
|
+
}
|
|
905
|
+
if (!isPlainObject(envelope)) {
|
|
906
|
+
throw new Error('Invalid payload envelope (expected object).');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const requiredInner = ['payload_id', 'job_id', 'payload_kind', 'sender_bot_id', 'recipient_bot_id', 'created_at', 'sender_sig_ed25519', 'prefix'];
|
|
910
|
+
for (const field of requiredInner) {
|
|
911
|
+
if (inner[field] === undefined || inner[field] === null || inner[field] === '') {
|
|
912
|
+
throw new Error(`Decrypted payload missing ${field}.`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (inner.body === undefined || inner.body === null) {
|
|
916
|
+
throw new Error('Decrypted payload missing body.');
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (inner.prefix !== 'NBR1') {
|
|
920
|
+
throw new Error(`Invalid payload prefix: ${inner.prefix}`);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const matches = [
|
|
924
|
+
['payload_id', inner.payload_id, envelope.payload_id],
|
|
925
|
+
['job_id', inner.job_id, envelope.job_id],
|
|
926
|
+
['payload_kind', inner.payload_kind, envelope.payload_kind],
|
|
927
|
+
['sender_bot_id', inner.sender_bot_id, envelope.sender_bot_id],
|
|
928
|
+
['recipient_bot_id', inner.recipient_bot_id, envelope.recipient_bot_id],
|
|
929
|
+
];
|
|
930
|
+
for (const [field, innerValue, outerValue] of matches) {
|
|
931
|
+
if (innerValue !== outerValue) {
|
|
932
|
+
throw new Error(`Payload ${field} mismatch: inner=${innerValue} outer=${outerValue}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const signature = decodeBase64url(inner.sender_sig_ed25519, 'sender_sig_ed25519');
|
|
937
|
+
let pubKey;
|
|
938
|
+
try {
|
|
939
|
+
pubKey = crypto.createPublicKey({
|
|
940
|
+
format: 'jwk',
|
|
941
|
+
key: {kty: 'OKP', crv: 'Ed25519', x: String(senderSigningPubkeyEd25519)},
|
|
942
|
+
});
|
|
943
|
+
} catch (err) {
|
|
944
|
+
throw new Error(`Invalid signing_pubkey_ed25519: ${err.message}`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const canonical = canonicalPayloadString(inner);
|
|
948
|
+
const ok = crypto.verify(null, Buffer.from(canonical, 'utf8'), pubKey, signature);
|
|
949
|
+
if (!ok) {
|
|
950
|
+
throw new Error('Invalid sender_sig_ed25519 for decrypted payload.');
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function fetchDecryptVerifyPayload({payloadId, config, keys, identity}) {
|
|
955
|
+
const envelope = await fetchPayloadEnvelope({payloadId, config, keys, identity});
|
|
956
|
+
if (!envelope.ciphertext_b64) {
|
|
957
|
+
throw new Error('Payload response missing ciphertext_b64.');
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const plaintext = await decryptPayloadCiphertext({ciphertextB64: envelope.ciphertext_b64, keys});
|
|
961
|
+
const inner = parseDecryptedPayload(plaintext);
|
|
962
|
+
|
|
963
|
+
const senderBotId = inner.sender_bot_id || envelope.sender_bot_id;
|
|
964
|
+
if (!senderBotId) {
|
|
965
|
+
throw new Error('Missing sender_bot_id for signature verification.');
|
|
966
|
+
}
|
|
967
|
+
const senderBot = await fetchBot({botId: senderBotId, config, keys, identity});
|
|
968
|
+
if (!senderBot.signing_pubkey_ed25519) {
|
|
969
|
+
throw new Error(`Bot ${senderBotId} missing signing_pubkey_ed25519.`);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
verifyPayloadInner({inner, envelope, senderSigningPubkeyEd25519: senderBot.signing_pubkey_ed25519});
|
|
973
|
+
return {envelope, inner, senderBot};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function extractEventType(event) {
|
|
977
|
+
if (!event) {
|
|
978
|
+
return '';
|
|
979
|
+
}
|
|
980
|
+
if (event.event_type) {
|
|
981
|
+
return String(event.event_type);
|
|
982
|
+
}
|
|
983
|
+
if (event.data && event.data.event_type) {
|
|
984
|
+
return String(event.data.event_type);
|
|
985
|
+
}
|
|
986
|
+
return '';
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function extractEventData(event) {
|
|
990
|
+
if (!event) {
|
|
991
|
+
return {};
|
|
992
|
+
}
|
|
993
|
+
if (isPlainObject(event.data)) {
|
|
994
|
+
return event.data;
|
|
995
|
+
}
|
|
996
|
+
if (isPlainObject(event)) {
|
|
997
|
+
return event;
|
|
998
|
+
}
|
|
999
|
+
return {};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function extractPayloadRefsFromEvents(events) {
|
|
1003
|
+
const out = [];
|
|
1004
|
+
if (!Array.isArray(events)) {
|
|
1005
|
+
return out;
|
|
1006
|
+
}
|
|
1007
|
+
for (const event of events) {
|
|
1008
|
+
const eventType = extractEventType(event);
|
|
1009
|
+
const data = extractEventData(event);
|
|
1010
|
+
if (eventType === 'job.payload_available' && data.payload_id) {
|
|
1011
|
+
out.push({payload_id: String(data.payload_id), job_id: data.job_id ? String(data.job_id) : null, payload_kind: data.payload_kind ? String(data.payload_kind) : null});
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
if (eventType === 'job.requested' && data.request_payload_id) {
|
|
1015
|
+
out.push({payload_id: String(data.request_payload_id), job_id: data.job_id ? String(data.job_id) : null, payload_kind: 'request'});
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return out;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function resolvePayloadIdForJob({jobId, state, config, keys, identity}) {
|
|
1023
|
+
const trimmed = String(jobId).trim();
|
|
1024
|
+
if (!trimmed) {
|
|
1025
|
+
throw new Error('Missing job id.');
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// 1) Local known payload map.
|
|
1029
|
+
if (state && state.known_payloads) {
|
|
1030
|
+
const map = ensureMap(state.known_payloads, 'payload_id');
|
|
1031
|
+
let best = null;
|
|
1032
|
+
for (const entry of Object.values(map)) {
|
|
1033
|
+
if (!entry || entry.job_id !== trimmed || !entry.payload_id) {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
if (!best) {
|
|
1037
|
+
best = entry;
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
const bestTs = best.created_at ? Date.parse(best.created_at) : NaN;
|
|
1041
|
+
const entryTs = entry.created_at ? Date.parse(entry.created_at) : NaN;
|
|
1042
|
+
if (Number.isFinite(entryTs) && (!Number.isFinite(bestTs) || entryTs > bestTs)) {
|
|
1043
|
+
best = entry;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (best && best.payload_id) {
|
|
1047
|
+
return String(best.payload_id);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// 2) Scan local event log (may be truncated).
|
|
1052
|
+
const eventLog = state && Array.isArray(state.event_log) ? state.event_log : [];
|
|
1053
|
+
for (let i = eventLog.length - 1; i >= 0; i -= 1) {
|
|
1054
|
+
const event = eventLog[i];
|
|
1055
|
+
if (!event) {
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
const eventType = extractEventType(event);
|
|
1059
|
+
const data = extractEventData(event);
|
|
1060
|
+
if (eventType === 'job.payload_available' && data.job_id === trimmed && data.payload_id) {
|
|
1061
|
+
return String(data.payload_id);
|
|
1062
|
+
}
|
|
1063
|
+
if (eventType === 'job.requested' && data.job_id === trimmed && data.request_payload_id) {
|
|
1064
|
+
return String(data.request_payload_id);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// 3) Ask the relay for payload metadata for this job.
|
|
1069
|
+
const listed = await listPayloadMetadata({
|
|
1070
|
+
status: 'all',
|
|
1071
|
+
jobId: trimmed,
|
|
1072
|
+
limit: 25,
|
|
1073
|
+
cursor: undefined,
|
|
1074
|
+
config,
|
|
1075
|
+
keys,
|
|
1076
|
+
identity,
|
|
1077
|
+
});
|
|
1078
|
+
const payloads = Array.isArray(listed.payloads) ? listed.payloads : [];
|
|
1079
|
+
if (payloads.length === 0) {
|
|
1080
|
+
throw new Error(`No payloads found for job ${trimmed}.`);
|
|
1081
|
+
}
|
|
1082
|
+
if (!payloads[0] || !payloads[0].payload_id) {
|
|
1083
|
+
throw new Error(`Relay payload list returned an invalid entry for job ${trimmed}.`);
|
|
1084
|
+
}
|
|
1085
|
+
return String(payloads[0].payload_id);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function getCachedPayloadPath(state, payloadId) {
|
|
1089
|
+
const map = ensureMap(state && state.known_payloads ? state.known_payloads : {}, 'payload_id');
|
|
1090
|
+
const entry = map[payloadId];
|
|
1091
|
+
if (!entry || !entry.cached_path) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
const candidate = String(entry.cached_path);
|
|
1095
|
+
return candidate ? candidate : null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function upsertPayloadMeta(state, payloadId, meta) {
|
|
1099
|
+
state.known_payloads = ensureMap(state.known_payloads, 'payload_id');
|
|
1100
|
+
const current = state.known_payloads[payloadId] && typeof state.known_payloads[payloadId] === 'object'
|
|
1101
|
+
? state.known_payloads[payloadId]
|
|
1102
|
+
: {payload_id: payloadId};
|
|
1103
|
+
state.known_payloads[payloadId] = {...current, ...meta, payload_id: payloadId};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
703
1106
|
function printHelp() {
|
|
704
1107
|
console.log(`NanoBazaar CLI (OpenClaw skill)
|
|
705
1108
|
|
|
@@ -732,11 +1135,17 @@ Commands:
|
|
|
732
1135
|
job payment-sent --job-id <id> [--payment-block-hash <hash>]
|
|
733
1136
|
[--amount-raw-sent <raw>] [--sent-at <rfc3339>] [--note "..."]
|
|
734
1137
|
Notify seller that payment was sent
|
|
735
|
-
|
|
1138
|
+
payload list [--status unfetched|fetched|all] [--job-id <id>] [--limit <n>] [--cursor <cursor>]
|
|
1139
|
+
List payload metadata for the current bot
|
|
1140
|
+
payload fetch (--payload-id <id> | --job-id <id>) [--raw] [--compact] [--no-cache] [--cache-only]
|
|
1141
|
+
Fetch, decrypt, verify, and cache a payload
|
|
1142
|
+
poll [--since-event-id <id>] [--limit <n>] [--types a,b] [--no-ack] [--no-fetch-payloads]
|
|
736
1143
|
Poll events and optionally ack
|
|
1144
|
+
poll ack --up-to-event-id <id>
|
|
1145
|
+
Advance the server-side poll cursor (used for 410 resync)
|
|
737
1146
|
watch [--streams a,b] [--stream-path /v0/stream] [--safety-poll-interval <seconds>]
|
|
738
1147
|
[--state-path <path>] [--openclaw-bin <bin>] [--fswatch-bin <bin>]
|
|
739
|
-
[--event-text <text>] [--mode now|next] [--debounce-ms <ms>]
|
|
1148
|
+
[--event-text <text>] [--mode now|next] [--debounce-ms <ms>] [--no-fetch-payloads]
|
|
740
1149
|
Maintain SSE connection; poll on wake + on safety interval.
|
|
741
1150
|
If fswatch is available, also watch local state and trigger OpenClaw wakeups.
|
|
742
1151
|
|
|
@@ -748,7 +1157,9 @@ Global flags:
|
|
|
748
1157
|
Notes:
|
|
749
1158
|
- Defaults to relay: ${DEFAULT_RELAY_URL}
|
|
750
1159
|
- Uses NBR_STATE_PATH for local state (default: ${STATE_DEFAULT})
|
|
1160
|
+
- Decrypted payloads are cached under: (dirname NBR_STATE_PATH)/${PAYLOAD_CACHE_DIRNAME}/
|
|
751
1161
|
- Job payloads are encrypted with libsodium (install deps in skills/nanobazaar)
|
|
1162
|
+
- If --since-event-id is omitted, /v0/poll uses the relay's server-side cursor (last_acked_event_id)
|
|
752
1163
|
`);
|
|
753
1164
|
}
|
|
754
1165
|
|
|
@@ -1364,6 +1775,160 @@ async function runJobPaymentSent(argv) {
|
|
|
1364
1775
|
printJson(result.data, !!flags.compact);
|
|
1365
1776
|
}
|
|
1366
1777
|
|
|
1778
|
+
async function cachePayloadToDisk({payloadId, envelope, inner, state, statePath, compact}) {
|
|
1779
|
+
const cacheDir = resolvePayloadCacheDir(statePath);
|
|
1780
|
+
const cachePath = path.join(cacheDir, safePayloadCacheFilename(payloadId));
|
|
1781
|
+
writeJsonFile(cachePath, inner, compact);
|
|
1782
|
+
|
|
1783
|
+
const now = new Date().toISOString();
|
|
1784
|
+
upsertPayloadMeta(state, payloadId, {
|
|
1785
|
+
job_id: envelope && envelope.job_id ? envelope.job_id : undefined,
|
|
1786
|
+
payload_kind: envelope && envelope.payload_kind ? envelope.payload_kind : undefined,
|
|
1787
|
+
created_at: envelope && envelope.created_at ? envelope.created_at : undefined,
|
|
1788
|
+
sender_bot_id: envelope && envelope.sender_bot_id ? envelope.sender_bot_id : undefined,
|
|
1789
|
+
recipient_bot_id: envelope && envelope.recipient_bot_id ? envelope.recipient_bot_id : undefined,
|
|
1790
|
+
cached_path: cachePath,
|
|
1791
|
+
verified: true,
|
|
1792
|
+
verified_at: now,
|
|
1793
|
+
fetch_error: null,
|
|
1794
|
+
fetched_at: now,
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
saveStateMerged(statePath, state);
|
|
1798
|
+
return cachePath;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
async function ensurePayloadCached({payloadId, state, statePath, config, keys, identity, debugLog}) {
|
|
1802
|
+
const existingPath = getCachedPayloadPath(state, payloadId);
|
|
1803
|
+
if (existingPath && fs.existsSync(existingPath)) {
|
|
1804
|
+
return {payload_id: payloadId, cached_path: existingPath, from_cache: true};
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
const now = new Date().toISOString();
|
|
1808
|
+
try {
|
|
1809
|
+
const {envelope, inner} = await fetchDecryptVerifyPayload({payloadId, config, keys, identity});
|
|
1810
|
+
const cachedPath = await cachePayloadToDisk({
|
|
1811
|
+
payloadId,
|
|
1812
|
+
envelope,
|
|
1813
|
+
inner,
|
|
1814
|
+
state,
|
|
1815
|
+
statePath,
|
|
1816
|
+
compact: false,
|
|
1817
|
+
});
|
|
1818
|
+
if (debugLog) {
|
|
1819
|
+
debugLog('payload cached', {payload_id: payloadId, cached_path: cachedPath});
|
|
1820
|
+
}
|
|
1821
|
+
return {payload_id: payloadId, cached_path: cachedPath, from_cache: false};
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
upsertPayloadMeta(state, payloadId, {
|
|
1824
|
+
fetch_error: err && err.message ? err.message : String(err),
|
|
1825
|
+
fetch_attempted_at: now,
|
|
1826
|
+
});
|
|
1827
|
+
saveStateMerged(statePath, state);
|
|
1828
|
+
if (debugLog) {
|
|
1829
|
+
debugLog('payload cache error', {payload_id: payloadId, error: err && err.message ? err.message : String(err)});
|
|
1830
|
+
}
|
|
1831
|
+
return {payload_id: payloadId, error: err && err.message ? err.message : String(err)};
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
async function runPayloadList(argv) {
|
|
1836
|
+
const {flags} = parseArgs(argv);
|
|
1837
|
+
const config = buildConfig();
|
|
1838
|
+
const state = loadState(config.state_path);
|
|
1839
|
+
const {keys} = requireKeys(state);
|
|
1840
|
+
const identity = deriveIdentity(keys);
|
|
1841
|
+
|
|
1842
|
+
const status = flags.status ? String(flags.status) : 'unfetched';
|
|
1843
|
+
const jobId = flags.jobId || flags.job || undefined;
|
|
1844
|
+
const limit = flags.limit || undefined;
|
|
1845
|
+
const cursor = flags.cursor || undefined;
|
|
1846
|
+
|
|
1847
|
+
const data = await listPayloadMetadata({
|
|
1848
|
+
status,
|
|
1849
|
+
jobId: jobId ? String(jobId) : undefined,
|
|
1850
|
+
limit,
|
|
1851
|
+
cursor,
|
|
1852
|
+
config,
|
|
1853
|
+
keys,
|
|
1854
|
+
identity,
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
printJson(data, !!flags.compact);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
async function runPayloadFetch(argv) {
|
|
1861
|
+
const {flags} = parseArgs(argv);
|
|
1862
|
+
const config = buildConfig();
|
|
1863
|
+
const statePath = config.state_path;
|
|
1864
|
+
const state = loadState(statePath);
|
|
1865
|
+
const {keys} = requireKeys(state);
|
|
1866
|
+
const identity = deriveIdentity(keys);
|
|
1867
|
+
|
|
1868
|
+
const cacheEnabled = flags.cache !== false;
|
|
1869
|
+
const cacheOnly = !!flags.cacheOnly;
|
|
1870
|
+
|
|
1871
|
+
let payloadId = flags.payloadId || flags.payload;
|
|
1872
|
+
if (!payloadId && flags.jobId) {
|
|
1873
|
+
payloadId = await resolvePayloadIdForJob({
|
|
1874
|
+
jobId: String(flags.jobId),
|
|
1875
|
+
state,
|
|
1876
|
+
config,
|
|
1877
|
+
keys,
|
|
1878
|
+
identity,
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (!payloadId) {
|
|
1883
|
+
throw new Error('Missing payload id. Provide --payload-id or --job-id.');
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
if (cacheEnabled) {
|
|
1887
|
+
const cachedPath = getCachedPayloadPath(state, String(payloadId));
|
|
1888
|
+
if (cachedPath && fs.existsSync(cachedPath)) {
|
|
1889
|
+
const inner = readJsonFile(cachedPath, 'cached payload');
|
|
1890
|
+
if (flags.raw) {
|
|
1891
|
+
if (inner.body !== undefined) {
|
|
1892
|
+
console.log(typeof inner.body === 'string' ? inner.body : JSON.stringify(inner.body));
|
|
1893
|
+
} else {
|
|
1894
|
+
console.log(JSON.stringify(inner));
|
|
1895
|
+
}
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
printJson(inner, !!flags.compact);
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
if (cacheOnly) {
|
|
1902
|
+
throw new Error(`Payload not cached locally: ${payloadId}`);
|
|
1903
|
+
}
|
|
1904
|
+
} else if (cacheOnly) {
|
|
1905
|
+
throw new Error('--cache-only requires cache to be enabled (omit --no-cache).');
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const {envelope, inner} = await fetchDecryptVerifyPayload({payloadId: String(payloadId), config, keys, identity});
|
|
1909
|
+
if (cacheEnabled) {
|
|
1910
|
+
await cachePayloadToDisk({
|
|
1911
|
+
payloadId: String(payloadId),
|
|
1912
|
+
envelope,
|
|
1913
|
+
inner,
|
|
1914
|
+
state,
|
|
1915
|
+
statePath,
|
|
1916
|
+
compact: false,
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (flags.raw) {
|
|
1921
|
+
if (inner.body !== undefined) {
|
|
1922
|
+
console.log(typeof inner.body === 'string' ? inner.body : JSON.stringify(inner.body));
|
|
1923
|
+
} else {
|
|
1924
|
+
console.log(JSON.stringify(inner));
|
|
1925
|
+
}
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
printJson(inner, !!flags.compact);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1367
1932
|
async function runPoll(argv, options) {
|
|
1368
1933
|
const quiet = options && options.quiet;
|
|
1369
1934
|
const {flags} = parseArgs(argv);
|
|
@@ -1373,7 +1938,10 @@ async function runPoll(argv, options) {
|
|
|
1373
1938
|
const {keys} = requireKeys(state);
|
|
1374
1939
|
const identity = deriveIdentity(keys);
|
|
1375
1940
|
|
|
1376
|
-
|
|
1941
|
+
// NOTE: By default, rely on the relay's server-side cursor (last_acked_event_id).
|
|
1942
|
+
// Passing a local cursor can silently skip events if the state file is reused
|
|
1943
|
+
// across relays/bots. Use --since-event-id explicitly to override.
|
|
1944
|
+
const since = flags.sinceEventId;
|
|
1377
1945
|
const limit = flags.limit || config.poll_limit;
|
|
1378
1946
|
const types = flags.types || config.poll_types;
|
|
1379
1947
|
|
|
@@ -1413,7 +1981,7 @@ async function runPoll(argv, options) {
|
|
|
1413
1981
|
const addedEvents = appendEvents(state, events);
|
|
1414
1982
|
debugLog('response', {status: result.response.status, events: events.length, added: addedEvents});
|
|
1415
1983
|
|
|
1416
|
-
if (typeof state.last_acked_event_id !== 'number') {
|
|
1984
|
+
if (typeof state.last_acked_event_id !== 'number' || !Number.isFinite(state.last_acked_event_id)) {
|
|
1417
1985
|
state.last_acked_event_id = 0;
|
|
1418
1986
|
}
|
|
1419
1987
|
state.relay_url = config.relay_url;
|
|
@@ -1422,6 +1990,27 @@ async function runPoll(argv, options) {
|
|
|
1422
1990
|
state.encryption_kid = identity.encryptionKid;
|
|
1423
1991
|
saveStateMerged(config.state_path, state);
|
|
1424
1992
|
|
|
1993
|
+
const fetchPayloads = flags.fetchPayloads !== false;
|
|
1994
|
+
if (fetchPayloads && events.length > 0) {
|
|
1995
|
+
const refs = extractPayloadRefsFromEvents(events);
|
|
1996
|
+
if (refs.length > 0) {
|
|
1997
|
+
debugLog('payloads detected', {count: refs.length, payload_ids: refs.map((ref) => ref.payload_id)});
|
|
1998
|
+
for (const ref of refs) {
|
|
1999
|
+
// Best-effort caching: poll remains useful even if a payload decrypt fails.
|
|
2000
|
+
// Errors are persisted in known_payloads to aid follow-up debugging.
|
|
2001
|
+
await ensurePayloadCached({
|
|
2002
|
+
payloadId: ref.payload_id,
|
|
2003
|
+
state,
|
|
2004
|
+
statePath: config.state_path,
|
|
2005
|
+
config,
|
|
2006
|
+
keys,
|
|
2007
|
+
identity,
|
|
2008
|
+
debugLog,
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1425
2014
|
let ackedId = state.last_acked_event_id || 0;
|
|
1426
2015
|
let maxEventId = 0;
|
|
1427
2016
|
if (events.length > 0 && flags.ack !== false) {
|
|
@@ -1448,9 +2037,16 @@ async function runPoll(argv, options) {
|
|
|
1448
2037
|
}
|
|
1449
2038
|
debugLog('ack ok', {last_acked_event_id: ackedId});
|
|
1450
2039
|
|
|
1451
|
-
if (typeof ackedId === 'number' && ackedId !== state.last_acked_event_id) {
|
|
2040
|
+
if (typeof ackedId === 'number' && Number.isFinite(ackedId) && ackedId !== state.last_acked_event_id) {
|
|
1452
2041
|
state.last_acked_event_id = ackedId;
|
|
1453
|
-
|
|
2042
|
+
// Write the authoritative server-side cursor as-is (even if it moves "backwards"
|
|
2043
|
+
// compared to a stale local file).
|
|
2044
|
+
saveStateMerged(config.state_path, state, {
|
|
2045
|
+
mergeLastAcked: false,
|
|
2046
|
+
mergeStreamCursors: true,
|
|
2047
|
+
mergeKnownMaps: true,
|
|
2048
|
+
mergeEventLog: true,
|
|
2049
|
+
});
|
|
1454
2050
|
}
|
|
1455
2051
|
}
|
|
1456
2052
|
|
|
@@ -1463,6 +2059,56 @@ async function runPoll(argv, options) {
|
|
|
1463
2059
|
return {events, ackedId, maxEventId};
|
|
1464
2060
|
}
|
|
1465
2061
|
|
|
2062
|
+
async function runPollAck(argv) {
|
|
2063
|
+
const {flags, positionals} = parseArgs(argv);
|
|
2064
|
+
const value = flags.upToEventId ?? flags.upTo ?? flags.eventId ?? flags.id ?? positionals[0];
|
|
2065
|
+
if (value === undefined || value === null || String(value).trim() === '') {
|
|
2066
|
+
throw new Error('Missing up_to_event_id. Provide --up-to-event-id or a positional id.');
|
|
2067
|
+
}
|
|
2068
|
+
const parsed = Number(String(value));
|
|
2069
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
2070
|
+
throw new Error(`Invalid up_to_event_id: ${value}`);
|
|
2071
|
+
}
|
|
2072
|
+
const upToEventId = Math.floor(parsed);
|
|
2073
|
+
|
|
2074
|
+
const config = buildConfig();
|
|
2075
|
+
const state = loadState(config.state_path);
|
|
2076
|
+
const {keys} = requireKeys(state);
|
|
2077
|
+
const identity = deriveIdentity(keys);
|
|
2078
|
+
|
|
2079
|
+
const result = await signedRequest({
|
|
2080
|
+
method: 'POST',
|
|
2081
|
+
path: '/v0/poll/ack',
|
|
2082
|
+
body: {up_to_event_id: upToEventId},
|
|
2083
|
+
idempotencyKey: crypto.randomBytes(16).toString('hex'),
|
|
2084
|
+
relayUrl: config.relay_url,
|
|
2085
|
+
keys,
|
|
2086
|
+
identity,
|
|
2087
|
+
});
|
|
2088
|
+
|
|
2089
|
+
if (!result.response.ok) {
|
|
2090
|
+
throw new Error(`Ack failed (${result.response.status}): ${result.text || result.response.statusText}`);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
const ackedId = result.data && typeof result.data.last_acked_event_id === 'number'
|
|
2094
|
+
? result.data.last_acked_event_id
|
|
2095
|
+
: upToEventId;
|
|
2096
|
+
|
|
2097
|
+
state.last_acked_event_id = ackedId;
|
|
2098
|
+
state.relay_url = config.relay_url;
|
|
2099
|
+
state.bot_id = identity.botId;
|
|
2100
|
+
state.signing_kid = identity.signingKid;
|
|
2101
|
+
state.encryption_kid = identity.encryptionKid;
|
|
2102
|
+
saveStateMerged(config.state_path, state, {
|
|
2103
|
+
mergeLastAcked: false,
|
|
2104
|
+
mergeStreamCursors: true,
|
|
2105
|
+
mergeKnownMaps: true,
|
|
2106
|
+
mergeEventLog: true,
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
printJson(result.data, !!flags.compact);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1466
2112
|
function parseCsv(value) {
|
|
1467
2113
|
if (value === undefined || value === null) {
|
|
1468
2114
|
return [];
|
|
@@ -1968,6 +2614,25 @@ async function runWatch(argv) {
|
|
|
1968
2614
|
saveStateMerged(statePath, state);
|
|
1969
2615
|
}
|
|
1970
2616
|
|
|
2617
|
+
const fetchPayloads = flags.fetchPayloads !== false;
|
|
2618
|
+
if (fetchPayloads && allEvents.length > 0) {
|
|
2619
|
+
const refs = extractPayloadRefsFromEvents(allEvents);
|
|
2620
|
+
if (refs.length > 0) {
|
|
2621
|
+
debugLog('payloads detected', {count: refs.length, payload_ids: refs.map((ref) => ref.payload_id)});
|
|
2622
|
+
for (const ref of refs) {
|
|
2623
|
+
await ensurePayloadCached({
|
|
2624
|
+
payloadId: ref.payload_id,
|
|
2625
|
+
state,
|
|
2626
|
+
statePath,
|
|
2627
|
+
config,
|
|
2628
|
+
keys,
|
|
2629
|
+
identity,
|
|
2630
|
+
debugLog,
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
1971
2636
|
let ackedStreams = 0;
|
|
1972
2637
|
if (flags.ack !== false) {
|
|
1973
2638
|
for (const entry of results) {
|
|
@@ -2169,6 +2834,55 @@ async function runWatch(argv) {
|
|
|
2169
2834
|
stateWatcher.stop();
|
|
2170
2835
|
}
|
|
2171
2836
|
|
|
2837
|
+
const DISPATCH_BOOL_FLAGS = new Set([
|
|
2838
|
+
'compact',
|
|
2839
|
+
'raw',
|
|
2840
|
+
'cache-only',
|
|
2841
|
+
'print-polls',
|
|
2842
|
+
'fetch-payloads',
|
|
2843
|
+
'skip-register',
|
|
2844
|
+
]);
|
|
2845
|
+
|
|
2846
|
+
function splitSubcommand(argv, allowed) {
|
|
2847
|
+
const allowedSet = new Set(allowed || []);
|
|
2848
|
+
let i = 0;
|
|
2849
|
+
while (i < argv.length) {
|
|
2850
|
+
const arg = argv[i];
|
|
2851
|
+
if (!arg || arg === '-') {
|
|
2852
|
+
i += 1;
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
if (arg === '--') {
|
|
2856
|
+
const candidate = argv[i + 1];
|
|
2857
|
+
if (candidate && allowedSet.has(candidate)) {
|
|
2858
|
+
return {sub: candidate, argv: argv.slice(0, i + 1).concat(argv.slice(i + 2))};
|
|
2859
|
+
}
|
|
2860
|
+
return {sub: null, argv};
|
|
2861
|
+
}
|
|
2862
|
+
if (arg.startsWith('--')) {
|
|
2863
|
+
const eq = arg.indexOf('=');
|
|
2864
|
+
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
2865
|
+
if (name.startsWith('no-') || eq !== -1 || DISPATCH_BOOL_FLAGS.has(name)) {
|
|
2866
|
+
i += 1;
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
const next = argv[i + 1];
|
|
2870
|
+
if (next && (!next.startsWith('-') || next === '-')) {
|
|
2871
|
+
i += 2;
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
i += 1;
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
if (allowedSet.has(arg)) {
|
|
2878
|
+
return {sub: arg, argv: argv.slice(0, i).concat(argv.slice(i + 1))};
|
|
2879
|
+
}
|
|
2880
|
+
// First positional is not a recognized subcommand.
|
|
2881
|
+
return {sub: null, argv};
|
|
2882
|
+
}
|
|
2883
|
+
return {sub: null, argv};
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2172
2886
|
function loadVersion() {
|
|
2173
2887
|
try {
|
|
2174
2888
|
for (const name of ['package.json', 'skill.json']) {
|
|
@@ -2189,7 +2903,26 @@ function loadVersion() {
|
|
|
2189
2903
|
}
|
|
2190
2904
|
|
|
2191
2905
|
async function main() {
|
|
2192
|
-
|
|
2906
|
+
let argv = process.argv.slice(2);
|
|
2907
|
+
// Treat --debug as a global flag (it can appear anywhere without stealing positionals).
|
|
2908
|
+
let debugValue = null;
|
|
2909
|
+
const filtered = [];
|
|
2910
|
+
for (const arg of argv) {
|
|
2911
|
+
if (arg === '--debug') {
|
|
2912
|
+
debugValue = '1';
|
|
2913
|
+
continue;
|
|
2914
|
+
}
|
|
2915
|
+
if (arg.startsWith('--debug=')) {
|
|
2916
|
+
debugValue = arg.slice('--debug='.length) || '1';
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
filtered.push(arg);
|
|
2920
|
+
}
|
|
2921
|
+
if (debugValue !== null) {
|
|
2922
|
+
process.env.NBR_DEBUG = debugValue;
|
|
2923
|
+
argv = filtered;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2193
2926
|
if (argv.length === 0) {
|
|
2194
2927
|
printHelp();
|
|
2195
2928
|
return;
|
|
@@ -2236,38 +2969,62 @@ async function main() {
|
|
|
2236
2969
|
await runMarket(rest);
|
|
2237
2970
|
return;
|
|
2238
2971
|
case 'offer': {
|
|
2239
|
-
const sub = rest[
|
|
2972
|
+
const {sub, argv: subArgv} = splitSubcommand(rest, ['create', 'cancel']);
|
|
2240
2973
|
if (sub === 'create') {
|
|
2241
|
-
await runOfferCreate(
|
|
2974
|
+
await runOfferCreate(subArgv);
|
|
2242
2975
|
return;
|
|
2243
2976
|
}
|
|
2244
2977
|
if (sub === 'cancel') {
|
|
2245
|
-
await runOfferCancel(
|
|
2978
|
+
await runOfferCancel(subArgv);
|
|
2246
2979
|
return;
|
|
2247
2980
|
}
|
|
2248
2981
|
throw new Error('Unknown offer command. Use: offer create|cancel');
|
|
2249
2982
|
}
|
|
2250
2983
|
case 'job': {
|
|
2251
|
-
const sub = rest[
|
|
2984
|
+
const {sub, argv: subArgv} = splitSubcommand(rest, [
|
|
2985
|
+
'create',
|
|
2986
|
+
'reissue-request',
|
|
2987
|
+
'reissue-charge',
|
|
2988
|
+
'payment-sent',
|
|
2989
|
+
]);
|
|
2252
2990
|
if (sub === 'create') {
|
|
2253
|
-
await runJobCreate(
|
|
2991
|
+
await runJobCreate(subArgv);
|
|
2254
2992
|
return;
|
|
2255
2993
|
}
|
|
2256
2994
|
if (sub === 'reissue-request') {
|
|
2257
|
-
await runJobReissueRequest(
|
|
2995
|
+
await runJobReissueRequest(subArgv);
|
|
2258
2996
|
return;
|
|
2259
2997
|
}
|
|
2260
2998
|
if (sub === 'reissue-charge') {
|
|
2261
|
-
await runJobReissueCharge(
|
|
2999
|
+
await runJobReissueCharge(subArgv);
|
|
2262
3000
|
return;
|
|
2263
3001
|
}
|
|
2264
3002
|
if (sub === 'payment-sent') {
|
|
2265
|
-
await runJobPaymentSent(
|
|
3003
|
+
await runJobPaymentSent(subArgv);
|
|
2266
3004
|
return;
|
|
2267
3005
|
}
|
|
2268
3006
|
throw new Error('Unknown job command. Use: job create|reissue-request|reissue-charge|payment-sent');
|
|
2269
3007
|
}
|
|
3008
|
+
case 'payload': {
|
|
3009
|
+
const {sub, argv: subArgv} = splitSubcommand(rest, ['list', 'fetch']);
|
|
3010
|
+
if (sub === 'list') {
|
|
3011
|
+
await runPayloadList(subArgv);
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
if (sub === 'fetch') {
|
|
3015
|
+
await runPayloadFetch(subArgv);
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
throw new Error('Unknown payload command. Use: payload list|fetch');
|
|
3019
|
+
}
|
|
2270
3020
|
case 'poll':
|
|
3021
|
+
{
|
|
3022
|
+
const split = splitSubcommand(rest, ['ack']);
|
|
3023
|
+
if (split.sub === 'ack') {
|
|
3024
|
+
await runPollAck(split.argv);
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
2271
3028
|
await runPoll(rest);
|
|
2272
3029
|
return;
|
|
2273
3030
|
case 'watch':
|
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanobazaar-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "NanoBazaar CLI for the NanoBazaar Relay and OpenClaw skill.",
|
|
5
|
+
"homepage": "https://github.com/nanobazaar/nanobazaar/tree/main/packages/nanobazaar-cli#readme",
|
|
5
6
|
"license": "UNLICENSED",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/nanobazaar/nanobazaar.git",
|
|
10
|
+
"directory": "packages/nanobazaar-cli"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/nanobazaar/nanobazaar/issues"
|
|
14
|
+
},
|
|
6
15
|
"bin": {
|
|
7
|
-
"nanobazaar": "
|
|
16
|
+
"nanobazaar": "bin/nanobazaar"
|
|
8
17
|
},
|
|
9
18
|
"engines": {
|
|
10
19
|
"node": ">=18"
|
|
@@ -12,7 +21,8 @@
|
|
|
12
21
|
"files": [
|
|
13
22
|
"bin/",
|
|
14
23
|
"tools/",
|
|
15
|
-
"README.md"
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md"
|
|
16
26
|
],
|
|
17
27
|
"dependencies": {
|
|
18
28
|
"libsodium-wrappers": "^0.7.11"
|