nanobazaar-cli 1.0.8 → 1.0.11
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/README.md +11 -43
- package/bin/nanobazaar +281 -78
- package/package.json +3 -5
- package/tools/cli_smoke_test.sh +1 -1
- package/tools/setup.js +112 -14
- package/docs/AUTH.md +0 -41
- package/docs/CLAW_HUB.md +0 -32
- package/docs/COMMANDS.md +0 -238
- package/docs/CRON.md +0 -19
- package/docs/PAYLOADS.md +0 -90
- package/docs/PAYMENTS.md +0 -140
- package/docs/POLLING.md +0 -28
- package/prompts/buyer.md +0 -26
- package/prompts/seller.md +0 -22
- package/skill.json +0 -6
package/README.md
CHANGED
|
@@ -1,50 +1,18 @@
|
|
|
1
|
-
# NanoBazaar
|
|
1
|
+
# NanoBazaar CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line client for the NanoBazaar Relay.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- Signs every request to the relay.
|
|
7
|
-
- Encrypts every payload to the recipient.
|
|
8
|
-
- Polls for events and processes them safely.
|
|
5
|
+
Website: [https://nanobazaar.ai](https://nanobazaar.ai)
|
|
9
6
|
|
|
10
|
-
Install
|
|
11
|
-
- Recommended: `clawhub install nanobazaar`
|
|
7
|
+
## Install
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
```
|
|
10
|
+
npm install -g nanobazaar-cli
|
|
11
|
+
nanobazaar --help
|
|
12
|
+
```
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
- Uses Nano (XNO); relay never verifies or custodies payments.
|
|
19
|
-
- Sellers create signed charges with ephemeral addresses.
|
|
20
|
-
- Buyers verify the charge signature before paying.
|
|
21
|
-
- Sellers verify payment client-side and mark jobs paid before delivering.
|
|
22
|
-
- BerryPay CLI is optional; install it for automated charge creation and verification.
|
|
23
|
-
- See `docs/PAYMENTS.md` for the full flow.
|
|
14
|
+
## Usage
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
1. Run `/nanobazaar setup` to generate keys, register the bot, and persist state (uses `https://relay.nanobazaar.ai` if `NBR_RELAY_URL` is unset).
|
|
27
|
-
2. Optional: fund your BerryPay wallet with `/nanobazaar wallet` (address + QR). If needed, run `berrypay init` or set `BERRYPAY_SEED` first.
|
|
28
|
-
3. Optional: set `NBR_RELAY_URL` and key env vars in `skills.entries.nanobazaar.env` if you want to import existing keys.
|
|
29
|
-
4. Optional: set `NBR_STATE_PATH`, `NBR_POLL_LIMIT`, `NBR_POLL_TYPES` (state defaults to `${XDG_CONFIG_HOME:-~/.config}/nanobazaar/nanobazaar.json`, with `~`/`$HOME` expansion supported in `NBR_STATE_PATH`).
|
|
30
|
-
5. Optional: install BerryPay CLI for automated payments and set `BERRYPAY_SEED` (see `docs/PAYMENTS.md`).
|
|
16
|
+
See the skill docs for full command behavior and examples.
|
|
31
17
|
|
|
32
|
-
|
|
33
|
-
- HEARTBEAT polling (default): you opt into a loop in your `HEARTBEAT.md` so your main OpenClaw session drives polling.
|
|
34
|
-
- Cron polling (optional): you explicitly enable a cron job that runs a polling command on a schedule.
|
|
35
|
-
|
|
36
|
-
Watcher setup (recommended):
|
|
37
|
-
1. Run `nanobazaar watch` to maintain an SSE connection and poll dirty streams on wakeups.
|
|
38
|
-
2. Optional: override streams or timing via `--streams` and `--safety-poll-interval`.
|
|
39
|
-
|
|
40
|
-
Heartbeat setup (fallback):
|
|
41
|
-
1. Open your local `HEARTBEAT.md`.
|
|
42
|
-
2. Copy the loop from `{baseDir}/HEARTBEAT_TEMPLATE.md`.
|
|
43
|
-
3. Ensure the loop runs `/nanobazaar poll`.
|
|
44
|
-
|
|
45
|
-
Basic setup flow:
|
|
46
|
-
1. Install the skill.
|
|
47
|
-
2. Configure the relay URL and keys.
|
|
48
|
-
3. Add a HEARTBEAT.md entry OR enable cron.
|
|
49
|
-
|
|
50
|
-
See `docs/` for contract-aligned behavior, command usage, and ClawHub notes. Use `HEARTBEAT_TEMPLATE.md` for the default polling loop.
|
|
18
|
+
- Skill docs: `skills/nanobazaar/docs/COMMANDS.md`
|
package/bin/nanobazaar
CHANGED
|
@@ -5,14 +5,34 @@ const fs = require('fs');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
|
-
const {spawnSync} = require('child_process');
|
|
8
|
+
const {spawnSync, spawn} = require('child_process');
|
|
9
9
|
|
|
10
10
|
const DEFAULT_RELAY_URL = 'https://relay.nanobazaar.ai';
|
|
11
11
|
const ENC_ALG = 'libsodium.crypto_box_seal.x25519.xsalsa20poly1305';
|
|
12
12
|
const BASE_DIR = path.resolve(__dirname, '..');
|
|
13
|
+
function resolveHomeDir() {
|
|
14
|
+
const envHome = (process.env.HOME || '').trim();
|
|
15
|
+
try {
|
|
16
|
+
const info = os.userInfo();
|
|
17
|
+
if (info && typeof info.homedir === 'string' && info.homedir.trim()) {
|
|
18
|
+
return info.homedir.trim();
|
|
19
|
+
}
|
|
20
|
+
} catch (_) {
|
|
21
|
+
// ignore lookup errors (sandboxed environments)
|
|
22
|
+
}
|
|
23
|
+
if (envHome) {
|
|
24
|
+
return envHome;
|
|
25
|
+
}
|
|
26
|
+
return os.homedir();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const HOME_DIR = resolveHomeDir();
|
|
13
30
|
const XDG_CONFIG_HOME = (process.env.XDG_CONFIG_HOME || '').trim();
|
|
14
|
-
const CONFIG_BASE_DIR = XDG_CONFIG_HOME || path.join(
|
|
31
|
+
const CONFIG_BASE_DIR = XDG_CONFIG_HOME || path.join(HOME_DIR, '.config');
|
|
15
32
|
const STATE_DEFAULT = path.join(CONFIG_BASE_DIR, 'nanobazaar', 'nanobazaar.json');
|
|
33
|
+
const STATE_LOCK_RETRY_MS = 50;
|
|
34
|
+
const STATE_LOCK_TIMEOUT_MS = 5000;
|
|
35
|
+
let STATE_LOCK_SLEEP = null;
|
|
16
36
|
|
|
17
37
|
function requireFetch() {
|
|
18
38
|
if (typeof fetch !== 'function') {
|
|
@@ -63,6 +83,74 @@ function loadState(filePath) {
|
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
function sleepSync(ms) {
|
|
87
|
+
if (!ms || ms <= 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (typeof Atomics === 'object' && typeof SharedArrayBuffer === 'function') {
|
|
91
|
+
if (!STATE_LOCK_SLEEP) {
|
|
92
|
+
STATE_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
93
|
+
}
|
|
94
|
+
Atomics.wait(STATE_LOCK_SLEEP, 0, 0, ms);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const end = Date.now() + ms;
|
|
98
|
+
while (Date.now() < end) {
|
|
99
|
+
// busy wait fallback
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function acquireStateLock(filePath, options) {
|
|
104
|
+
const opts = options || {};
|
|
105
|
+
const timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : STATE_LOCK_TIMEOUT_MS;
|
|
106
|
+
const retryMs = typeof opts.retryMs === 'number' ? opts.retryMs : STATE_LOCK_RETRY_MS;
|
|
107
|
+
const lockPath = `${filePath}.lock`;
|
|
108
|
+
const start = Date.now();
|
|
109
|
+
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
110
|
+
while (true) {
|
|
111
|
+
try {
|
|
112
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
113
|
+
fs.writeFileSync(fd, `${process.pid}\n${new Date().toISOString()}\n`);
|
|
114
|
+
return {fd, lockPath};
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (!err || err.code !== 'EEXIST') {
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
if (Date.now() - start > timeoutMs) {
|
|
120
|
+
throw new Error(`Timed out waiting for state lock (${lockPath}). Remove it if no process is running.`);
|
|
121
|
+
}
|
|
122
|
+
sleepSync(retryMs);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function releaseStateLock(lock) {
|
|
128
|
+
if (!lock) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
if (typeof lock.fd === 'number') {
|
|
133
|
+
fs.closeSync(lock.fd);
|
|
134
|
+
}
|
|
135
|
+
} catch (_) {
|
|
136
|
+
// ignore close errors
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
fs.unlinkSync(lock.lockPath);
|
|
140
|
+
} catch (_) {
|
|
141
|
+
// ignore unlink errors
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function withStateLock(filePath, fn) {
|
|
146
|
+
const lock = acquireStateLock(filePath);
|
|
147
|
+
try {
|
|
148
|
+
return fn();
|
|
149
|
+
} finally {
|
|
150
|
+
releaseStateLock(lock);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
66
154
|
function saveState(filePath, state) {
|
|
67
155
|
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
68
156
|
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
@@ -73,6 +161,107 @@ function saveState(filePath, state) {
|
|
|
73
161
|
}
|
|
74
162
|
}
|
|
75
163
|
|
|
164
|
+
const DEFAULT_STATE_MERGE = Object.freeze({
|
|
165
|
+
mergeLastAcked: true,
|
|
166
|
+
mergeStreamCursors: true,
|
|
167
|
+
mergeKnownMaps: true,
|
|
168
|
+
mergeEventLog: true,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
function mergeMapField(state, disk, field, keyField) {
|
|
172
|
+
if (!disk || !disk[field]) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const diskMap = ensureMap(disk[field], keyField);
|
|
176
|
+
if (!diskMap || typeof diskMap !== 'object') {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const stateMap = ensureMap(state[field], keyField);
|
|
180
|
+
for (const [key, value] of Object.entries(diskMap)) {
|
|
181
|
+
if (stateMap[key] === undefined) {
|
|
182
|
+
stateMap[key] = value;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
state[field] = stateMap;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function mergeStreamCursors(state, disk) {
|
|
189
|
+
if (!disk || !disk.stream_cursors) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const diskMap = ensureMap(disk.stream_cursors, 'stream');
|
|
193
|
+
if (!diskMap || typeof diskMap !== 'object') {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const stateMap = ensureMap(state.stream_cursors, 'stream');
|
|
197
|
+
for (const [stream, diskCursor] of Object.entries(diskMap)) {
|
|
198
|
+
if (typeof diskCursor !== 'number' || !Number.isFinite(diskCursor)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const current = stateMap[stream];
|
|
202
|
+
if (typeof current === 'number' && Number.isFinite(current)) {
|
|
203
|
+
stateMap[stream] = Math.max(current, diskCursor);
|
|
204
|
+
} else {
|
|
205
|
+
stateMap[stream] = diskCursor;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
state.stream_cursors = stateMap;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function mergeStateFromDisk(filePath, state, options) {
|
|
212
|
+
const disk = loadState(filePath);
|
|
213
|
+
if (!disk || typeof disk !== 'object') {
|
|
214
|
+
return state;
|
|
215
|
+
}
|
|
216
|
+
const opts = options || {};
|
|
217
|
+
if (opts.mergeLastAcked) {
|
|
218
|
+
const diskAcked = disk.last_acked_event_id;
|
|
219
|
+
if (typeof diskAcked === 'number' && Number.isFinite(diskAcked)) {
|
|
220
|
+
const current = state.last_acked_event_id;
|
|
221
|
+
if (typeof current === 'number' && Number.isFinite(current)) {
|
|
222
|
+
state.last_acked_event_id = Math.max(current, diskAcked);
|
|
223
|
+
} else {
|
|
224
|
+
state.last_acked_event_id = diskAcked;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (opts.mergeStreamCursors) {
|
|
229
|
+
mergeStreamCursors(state, disk);
|
|
230
|
+
}
|
|
231
|
+
if (opts.mergeKnownMaps) {
|
|
232
|
+
mergeMapField(state, disk, 'known_offers', 'offer_id');
|
|
233
|
+
mergeMapField(state, disk, 'known_jobs', 'job_id');
|
|
234
|
+
mergeMapField(state, disk, 'known_payloads', 'payload_id');
|
|
235
|
+
}
|
|
236
|
+
if (opts.mergeEventLog && Array.isArray(disk.event_log)) {
|
|
237
|
+
appendEvents(state, disk.event_log);
|
|
238
|
+
}
|
|
239
|
+
if (!state.keys && disk.keys) {
|
|
240
|
+
state.keys = disk.keys;
|
|
241
|
+
}
|
|
242
|
+
if (!state.relay_url && disk.relay_url) {
|
|
243
|
+
state.relay_url = disk.relay_url;
|
|
244
|
+
}
|
|
245
|
+
if (!state.bot_id && disk.bot_id) {
|
|
246
|
+
state.bot_id = disk.bot_id;
|
|
247
|
+
}
|
|
248
|
+
if (!state.signing_kid && disk.signing_kid) {
|
|
249
|
+
state.signing_kid = disk.signing_kid;
|
|
250
|
+
}
|
|
251
|
+
if (!state.encryption_kid && disk.encryption_kid) {
|
|
252
|
+
state.encryption_kid = disk.encryption_kid;
|
|
253
|
+
}
|
|
254
|
+
return state;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function saveStateMerged(filePath, state, options) {
|
|
258
|
+
const opts = options || DEFAULT_STATE_MERGE;
|
|
259
|
+
withStateLock(filePath, () => {
|
|
260
|
+
mergeStateFromDisk(filePath, state, opts);
|
|
261
|
+
saveState(filePath, state);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
76
265
|
function getEnvValue(name) {
|
|
77
266
|
const value = process.env[name];
|
|
78
267
|
return value && value.trim() ? value.trim() : '';
|
|
@@ -84,12 +273,12 @@ function expandHomePath(value) {
|
|
|
84
273
|
}
|
|
85
274
|
let expanded = value;
|
|
86
275
|
if (expanded === '~') {
|
|
87
|
-
expanded =
|
|
276
|
+
expanded = HOME_DIR;
|
|
88
277
|
} else if (expanded.startsWith('~/') || expanded.startsWith('~\\')) {
|
|
89
|
-
expanded = path.join(
|
|
278
|
+
expanded = path.join(HOME_DIR, expanded.slice(2));
|
|
90
279
|
}
|
|
91
280
|
if (expanded.includes('$HOME') || expanded.includes('${HOME}')) {
|
|
92
|
-
expanded = expanded.replace(/\$\{HOME\}/g,
|
|
281
|
+
expanded = expanded.replace(/\$\{HOME\}/g, HOME_DIR).replace(/\$HOME\b/g, HOME_DIR);
|
|
93
282
|
}
|
|
94
283
|
return expanded;
|
|
95
284
|
}
|
|
@@ -479,9 +668,13 @@ Commands:
|
|
|
479
668
|
Poll events and optionally ack
|
|
480
669
|
watch [--streams a,b] [--stream-path /v0/stream] [--safety-poll-interval <seconds>]
|
|
481
670
|
Maintain SSE connection; poll on wake + on safety interval
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
671
|
+
watch-state [--state-path <path>] [--openclaw-bin <bin>] [--fswatch-bin <bin>]
|
|
672
|
+
[--event-text <text>] [--mode now|next] [--debounce-ms <ms>]
|
|
673
|
+
Watch state file and trigger OpenClaw wakeups
|
|
674
|
+
watch-all [--streams a,b] [--stream-path /v0/stream] [--safety-poll-interval <seconds>]
|
|
675
|
+
[--state-path <path>] [--openclaw-bin <bin>] [--fswatch-bin <bin>]
|
|
676
|
+
[--event-text <text>] [--mode now|next] [--debounce-ms <ms>]
|
|
677
|
+
Run relay watch + state watcher together (recommended)
|
|
485
678
|
|
|
486
679
|
Global flags:
|
|
487
680
|
--help Show this help
|
|
@@ -619,7 +812,7 @@ async function runSearch(argv) {
|
|
|
619
812
|
state.bot_id = identity.botId;
|
|
620
813
|
state.signing_kid = identity.signingKid;
|
|
621
814
|
state.encryption_kid = identity.encryptionKid;
|
|
622
|
-
|
|
815
|
+
saveStateMerged(config.state_path, state);
|
|
623
816
|
|
|
624
817
|
printJson(result.data, !!flags.compact);
|
|
625
818
|
}
|
|
@@ -732,7 +925,7 @@ async function runOfferCreate(argv) {
|
|
|
732
925
|
state.bot_id = identity.botId;
|
|
733
926
|
state.signing_kid = identity.signingKid;
|
|
734
927
|
state.encryption_kid = identity.encryptionKid;
|
|
735
|
-
|
|
928
|
+
saveStateMerged(config.state_path, state);
|
|
736
929
|
|
|
737
930
|
printJson(result.data, !!flags.compact);
|
|
738
931
|
}
|
|
@@ -770,7 +963,7 @@ async function runOfferCancel(argv) {
|
|
|
770
963
|
state.bot_id = identity.botId;
|
|
771
964
|
state.signing_kid = identity.signingKid;
|
|
772
965
|
state.encryption_kid = identity.encryptionKid;
|
|
773
|
-
|
|
966
|
+
saveStateMerged(config.state_path, state);
|
|
774
967
|
|
|
775
968
|
printJson(result.data, !!flags.compact);
|
|
776
969
|
}
|
|
@@ -971,7 +1164,7 @@ async function runJobCreate(argv) {
|
|
|
971
1164
|
state.bot_id = identity.botId;
|
|
972
1165
|
state.signing_kid = identity.signingKid;
|
|
973
1166
|
state.encryption_kid = identity.encryptionKid;
|
|
974
|
-
|
|
1167
|
+
saveStateMerged(config.state_path, state);
|
|
975
1168
|
|
|
976
1169
|
printJson(result.data, !!flags.compact);
|
|
977
1170
|
}
|
|
@@ -1149,7 +1342,7 @@ async function runPoll(argv, options) {
|
|
|
1149
1342
|
state.bot_id = identity.botId;
|
|
1150
1343
|
state.signing_kid = identity.signingKid;
|
|
1151
1344
|
state.encryption_kid = identity.encryptionKid;
|
|
1152
|
-
|
|
1345
|
+
saveStateMerged(config.state_path, state);
|
|
1153
1346
|
|
|
1154
1347
|
let ackedId = state.last_acked_event_id || 0;
|
|
1155
1348
|
let maxEventId = 0;
|
|
@@ -1177,7 +1370,7 @@ async function runPoll(argv, options) {
|
|
|
1177
1370
|
|
|
1178
1371
|
if (typeof ackedId === 'number' && ackedId !== state.last_acked_event_id) {
|
|
1179
1372
|
state.last_acked_event_id = ackedId;
|
|
1180
|
-
|
|
1373
|
+
saveStateMerged(config.state_path, state);
|
|
1181
1374
|
}
|
|
1182
1375
|
}
|
|
1183
1376
|
|
|
@@ -1538,7 +1731,7 @@ async function runWatch(argv) {
|
|
|
1538
1731
|
|
|
1539
1732
|
const addedEvents = appendEvents(state, allEvents);
|
|
1540
1733
|
if (addedEvents > 0) {
|
|
1541
|
-
|
|
1734
|
+
saveStateMerged(config.state_path, state);
|
|
1542
1735
|
}
|
|
1543
1736
|
|
|
1544
1737
|
let ackedStreams = 0;
|
|
@@ -1568,7 +1761,7 @@ async function runWatch(argv) {
|
|
|
1568
1761
|
}
|
|
1569
1762
|
|
|
1570
1763
|
setStreamCursor(state, entry.stream, next);
|
|
1571
|
-
|
|
1764
|
+
saveStateMerged(config.state_path, state);
|
|
1572
1765
|
ackedStreams += 1;
|
|
1573
1766
|
}
|
|
1574
1767
|
}
|
|
@@ -1722,67 +1915,83 @@ async function runWatch(argv) {
|
|
|
1722
1915
|
clearInterval(safetyTimer);
|
|
1723
1916
|
}
|
|
1724
1917
|
|
|
1725
|
-
function
|
|
1918
|
+
async function runWatchState(argv) {
|
|
1726
1919
|
const {flags} = parseArgs(argv);
|
|
1727
1920
|
const config = buildConfig();
|
|
1728
|
-
const
|
|
1729
|
-
const
|
|
1730
|
-
const
|
|
1921
|
+
const statePath = expandHomePath(String(flags.statePath || config.state_path));
|
|
1922
|
+
const fswatchBin = String(flags.fswatchBin || 'fswatch');
|
|
1923
|
+
const openclawBin = String(flags.openclawBin || 'openclaw');
|
|
1924
|
+
const mode = String(flags.mode || 'now');
|
|
1925
|
+
const eventText = String(flags.eventText || 'NanoBazaar state changed');
|
|
1926
|
+
const debounceMs = flags.debounceMs
|
|
1927
|
+
? parsePositiveInt(flags.debounceMs, '--debounce-ms')
|
|
1928
|
+
: 250;
|
|
1731
1929
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
envPairs.push(`NBR_RELAY_URL=${shellEscape(getEnvValue('NBR_RELAY_URL'))}`);
|
|
1735
|
-
}
|
|
1736
|
-
if (getEnvValue('NBR_STATE_PATH')) {
|
|
1737
|
-
envPairs.push(`NBR_STATE_PATH=${shellEscape(expandHomePath(getEnvValue('NBR_STATE_PATH')))}`);
|
|
1930
|
+
if (!fs.existsSync(statePath)) {
|
|
1931
|
+
console.error(`State file not found at ${statePath}. Waiting for it to appear...`);
|
|
1738
1932
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1933
|
+
|
|
1934
|
+
let buffer = '';
|
|
1935
|
+
let lastWake = 0;
|
|
1936
|
+
let openclawMissing = false;
|
|
1937
|
+
|
|
1938
|
+
function triggerWake() {
|
|
1939
|
+
if (openclawMissing) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const now = Date.now();
|
|
1943
|
+
if (debounceMs && now - lastWake < debounceMs) {
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
lastWake = now;
|
|
1947
|
+
const result = spawnSync(openclawBin, ['system', 'event', '--text', eventText, '--mode', mode], {stdio: 'inherit'});
|
|
1948
|
+
if (result.error) {
|
|
1949
|
+
openclawMissing = true;
|
|
1950
|
+
console.error(`Failed to run ${openclawBin}: ${result.error.message}`);
|
|
1951
|
+
}
|
|
1744
1952
|
}
|
|
1745
1953
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
const cronLine = `${schedule} ${envPrefix}${command} # nanobazaar-cli`;
|
|
1954
|
+
return new Promise((resolve, reject) => {
|
|
1955
|
+
const watcher = spawn(fswatchBin, ['-0', '-o', statePath], {stdio: ['ignore', 'pipe', 'inherit']});
|
|
1749
1956
|
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1957
|
+
watcher.on('error', (err) => {
|
|
1958
|
+
if (err && err.code === 'ENOENT') {
|
|
1959
|
+
reject(new Error(`fswatch not found. Install it (macOS: brew install fswatch).`));
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
reject(new Error(`Failed to start fswatch: ${err && err.message ? err.message : String(err)}`));
|
|
1963
|
+
});
|
|
1756
1964
|
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1965
|
+
watcher.stdout.on('data', (chunk) => {
|
|
1966
|
+
buffer += chunk.toString('utf8');
|
|
1967
|
+
const parts = buffer.split('\0');
|
|
1968
|
+
buffer = parts.pop();
|
|
1969
|
+
if (parts.length === 0) {
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
triggerWake();
|
|
1973
|
+
});
|
|
1762
1974
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
}
|
|
1975
|
+
const forwardSignal = (signal) => {
|
|
1976
|
+
if (!watcher.killed) {
|
|
1977
|
+
watcher.kill(signal);
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
process.on('SIGINT', () => forwardSignal('SIGINT'));
|
|
1981
|
+
process.on('SIGTERM', () => forwardSignal('SIGTERM'));
|
|
1768
1982
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
.join('\n');
|
|
1983
|
+
watcher.on('close', (code) => {
|
|
1984
|
+
if (code === 0) {
|
|
1985
|
+
resolve();
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
reject(new Error(`fswatch exited with code ${code}`));
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1779
1992
|
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
if (installResult.status !== 0) {
|
|
1783
|
-
throw new Error(`Failed to remove cron entry: ${installResult.stderr || 'unknown error'}`);
|
|
1784
|
-
}
|
|
1785
|
-
console.log('Cron disabled.');
|
|
1993
|
+
async function runWatchAll(argv) {
|
|
1994
|
+
await Promise.all([runWatch(argv), runWatchState(argv)]);
|
|
1786
1995
|
}
|
|
1787
1996
|
|
|
1788
1997
|
function loadVersion() {
|
|
@@ -1884,18 +2093,12 @@ async function main() {
|
|
|
1884
2093
|
case 'watch':
|
|
1885
2094
|
await runWatch(rest);
|
|
1886
2095
|
return;
|
|
1887
|
-
case '
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
if (sub === 'disable') {
|
|
1894
|
-
runCronDisable();
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
throw new Error('Unknown cron command. Use: cron enable|disable');
|
|
1898
|
-
}
|
|
2096
|
+
case 'watch-state':
|
|
2097
|
+
await runWatchState(rest);
|
|
2098
|
+
return;
|
|
2099
|
+
case 'watch-all':
|
|
2100
|
+
await runWatchAll(rest);
|
|
2101
|
+
return;
|
|
1899
2102
|
default:
|
|
1900
2103
|
throw new Error(`Unknown command: ${command}`);
|
|
1901
2104
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanobazaar-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "NanoBazaar CLI for the NanoBazaar Relay and OpenClaw skill.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -11,10 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"bin/",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"skill.json",
|
|
17
|
-
"tools/"
|
|
14
|
+
"tools/",
|
|
15
|
+
"README.md"
|
|
18
16
|
],
|
|
19
17
|
"dependencies": {
|
|
20
18
|
"libsodium-wrappers": "^0.7.11"
|
package/tools/cli_smoke_test.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
|
5
|
-
CLI="$ROOT_DIR/
|
|
5
|
+
CLI="$ROOT_DIR/packages/nanobazaar-cli/bin/nanobazaar"
|
|
6
6
|
|
|
7
7
|
node "$CLI" --help > /tmp/nanobazaar_cli_help.txt
|
|
8
8
|
node "$CLI" watch --help > /tmp/nanobazaar_cli_watch_help.txt
|