nanobazaar-cli 1.0.10 → 1.0.12

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 +131 -175
  2. package/package.json +1 -1
package/bin/nanobazaar CHANGED
@@ -667,17 +667,10 @@ Commands:
667
667
  poll [--since-event-id <id>] [--limit <n>] [--types a,b] [--no-ack]
668
668
  Poll events and optionally ack
669
669
  watch [--streams a,b] [--stream-path /v0/stream] [--safety-poll-interval <seconds>]
670
- Maintain SSE connection; poll on wake + on safety interval
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)
678
- cron enable [--schedule "*/5 * * * *"]
679
- Install cron entry to run poll
680
- cron disable Remove the cron entry
670
+ [--state-path <path>] [--openclaw-bin <bin>] [--fswatch-bin <bin>]
671
+ [--event-text <text>] [--mode now|next] [--debounce-ms <ms>]
672
+ Maintain SSE connection; poll on wake + on safety interval.
673
+ If fswatch is available, also watch local state and trigger OpenClaw wakeups.
681
674
 
682
675
  Global flags:
683
676
  --help Show this help
@@ -1638,12 +1631,116 @@ function deriveDefaultStreams({keys, state}) {
1638
1631
  return uniqStrings(streams);
1639
1632
  }
1640
1633
 
1634
+ function startStateWatcher(options) {
1635
+ const opts = options || {};
1636
+ const statePath = String(opts.statePath || '');
1637
+ const fswatchBin = String(opts.fswatchBin || 'fswatch');
1638
+ const openclawBin = String(opts.openclawBin || 'openclaw');
1639
+ const mode = String(opts.mode || 'now');
1640
+ const eventText = String(opts.eventText || 'NanoBazaar state changed');
1641
+ const debounceMs = typeof opts.debounceMs === 'number' ? opts.debounceMs : 250;
1642
+ const signal = opts.signal;
1643
+
1644
+ if (!statePath) {
1645
+ return {stop: () => {}};
1646
+ }
1647
+
1648
+ if (!fs.existsSync(statePath)) {
1649
+ console.error(`[watch] state file not found at ${statePath}. Waiting for it to appear...`);
1650
+ }
1651
+
1652
+ let watcher = null;
1653
+ let buffer = '';
1654
+ let lastWake = 0;
1655
+ let openclawMissing = false;
1656
+ let disabled = false;
1657
+
1658
+ function disable(message) {
1659
+ if (disabled) {
1660
+ return;
1661
+ }
1662
+ disabled = true;
1663
+ if (message) {
1664
+ console.error(`[watch] ${message}`);
1665
+ }
1666
+ if (watcher && !watcher.killed) {
1667
+ watcher.kill('SIGTERM');
1668
+ }
1669
+ }
1670
+
1671
+ function triggerWake() {
1672
+ if (openclawMissing) {
1673
+ return;
1674
+ }
1675
+ const now = Date.now();
1676
+ if (debounceMs && now - lastWake < debounceMs) {
1677
+ return;
1678
+ }
1679
+ lastWake = now;
1680
+ const result = spawnSync(openclawBin, ['system', 'event', '--text', eventText, '--mode', mode], {stdio: 'inherit'});
1681
+ if (result.error) {
1682
+ openclawMissing = true;
1683
+ console.error(`[watch] Failed to run ${openclawBin}: ${result.error.message}. Local wakeups disabled.`);
1684
+ }
1685
+ }
1686
+
1687
+ try {
1688
+ watcher = spawn(fswatchBin, ['-0', '-o', statePath], {stdio: ['ignore', 'pipe', 'inherit']});
1689
+ } catch (err) {
1690
+ disable(`Failed to start fswatch: ${err && err.message ? err.message : String(err)}. Running SSE polling only.`);
1691
+ return {stop: () => {}};
1692
+ }
1693
+
1694
+ watcher.on('error', (err) => {
1695
+ if (disabled) {
1696
+ return;
1697
+ }
1698
+ if (err && err.code === 'ENOENT') {
1699
+ disable('fswatch not found. Install it to enable local wakeups (macOS: brew install fswatch, Ubuntu: apt install fswatch).');
1700
+ return;
1701
+ }
1702
+ disable(`Failed to start fswatch: ${err && err.message ? err.message : String(err)}. Running SSE polling only.`);
1703
+ });
1704
+
1705
+ watcher.stdout.on('data', (chunk) => {
1706
+ buffer += chunk.toString('utf8');
1707
+ const parts = buffer.split('\0');
1708
+ buffer = parts.pop();
1709
+ if (parts.length === 0) {
1710
+ return;
1711
+ }
1712
+ triggerWake();
1713
+ });
1714
+
1715
+ watcher.on('close', (code) => {
1716
+ if (disabled || (signal && signal.aborted)) {
1717
+ return;
1718
+ }
1719
+ if (code === 0) {
1720
+ disable();
1721
+ return;
1722
+ }
1723
+ disable(`fswatch exited with code ${code}. Running SSE polling only.`);
1724
+ });
1725
+
1726
+ if (signal) {
1727
+ if (signal.aborted) {
1728
+ disable();
1729
+ } else {
1730
+ signal.addEventListener('abort', () => disable());
1731
+ }
1732
+ }
1733
+
1734
+ return {stop: () => disable()};
1735
+ }
1736
+
1641
1737
  async function runWatch(argv) {
1642
1738
  requireFetch();
1643
1739
 
1644
1740
  const {flags} = parseArgs(argv);
1645
1741
  const config = buildConfig();
1646
- const state = loadState(config.state_path);
1742
+ const statePath = expandHomePath(String(flags.statePath || config.state_path));
1743
+ const state = loadState(statePath);
1647
1744
  const {keys} = requireKeys(state);
1648
1745
  const identity = deriveIdentity(keys);
1649
1746
 
@@ -1663,6 +1760,14 @@ async function runWatch(argv) {
1663
1760
  const pollLimit = parseOptionalPositiveInt(flags.limit, '--limit')
1664
1761
  ?? parseOptionalPositiveInt(config.poll_limit, 'NBR_POLL_LIMIT');
1665
1762
 
1763
+ const fswatchBin = String(flags.fswatchBin || 'fswatch');
1764
+ const openclawBin = String(flags.openclawBin || 'openclaw');
1765
+ const mode = String(flags.mode || 'now');
1766
+ const eventText = String(flags.eventText || 'NanoBazaar state changed');
1767
+ const debounceMs = flags.debounceMs
1768
+ ? parsePositiveInt(flags.debounceMs, '--debounce-ms')
1769
+ : 250;
1770
+
1666
1771
  ensureStreamCursorMap(state);
1667
1772
  const streamSet = new Set(streams);
1668
1773
 
@@ -1734,7 +1839,7 @@ async function runWatch(argv) {
1734
1839
 
1735
1840
  const addedEvents = appendEvents(state, allEvents);
1736
1841
  if (addedEvents > 0) {
1737
- saveStateMerged(config.state_path, state);
1842
+ saveStateMerged(statePath, state);
1738
1843
  }
1739
1844
 
1740
1845
  let ackedStreams = 0;
@@ -1764,7 +1869,7 @@ async function runWatch(argv) {
1764
1869
  }
1765
1870
 
1766
1871
  setStreamCursor(state, entry.stream, next);
1767
- saveStateMerged(config.state_path, state);
1872
+ saveStateMerged(statePath, state);
1768
1873
  ackedStreams += 1;
1769
1874
  }
1770
1875
  }
@@ -1819,11 +1924,21 @@ async function runWatch(argv) {
1819
1924
  process.on('SIGTERM', stop);
1820
1925
 
1821
1926
  console.error(`[watch] relay=${config.relay_url}`);
1822
- console.error(`[watch] state_path=${config.state_path}`);
1927
+ console.error(`[watch] state_path=${statePath}`);
1823
1928
  console.error(`[watch] stream_path=${streamPath}`);
1824
1929
  console.error(`[watch] streams=${streams.join(',')}`);
1825
1930
  console.error(`[watch] safety_poll_interval_seconds=${safetyIntervalSeconds}`);
1826
1931
 
1932
+ const stateWatcher = startStateWatcher({
1933
+ statePath,
1934
+ fswatchBin,
1935
+ openclawBin,
1936
+ mode,
1937
+ eventText,
1938
+ debounceMs,
1939
+ signal,
1940
+ });
1941
+
1827
1942
  const safetyTimer = setInterval(() => {
1828
1943
  if (signal.aborted) {
1829
1944
  return;
@@ -1916,148 +2031,7 @@ async function runWatch(argv) {
1916
2031
  }
1917
2032
 
1918
2033
  clearInterval(safetyTimer);
1919
- }
1920
-
1921
- async function runWatchState(argv) {
1922
- const {flags} = parseArgs(argv);
1923
- const config = buildConfig();
1924
- const statePath = expandHomePath(String(flags.statePath || config.state_path));
1925
- const fswatchBin = String(flags.fswatchBin || 'fswatch');
1926
- const openclawBin = String(flags.openclawBin || 'openclaw');
1927
- const mode = String(flags.mode || 'now');
1928
- const eventText = String(flags.eventText || 'NanoBazaar state changed');
1929
- const debounceMs = flags.debounceMs
1930
- ? parsePositiveInt(flags.debounceMs, '--debounce-ms')
1931
- : 250;
1932
-
1933
- if (!fs.existsSync(statePath)) {
1934
- console.error(`State file not found at ${statePath}. Waiting for it to appear...`);
1935
- }
1936
-
1937
- let buffer = '';
1938
- let lastWake = 0;
1939
- let openclawMissing = false;
1940
-
1941
- function triggerWake() {
1942
- if (openclawMissing) {
1943
- return;
1944
- }
1945
- const now = Date.now();
1946
- if (debounceMs && now - lastWake < debounceMs) {
1947
- return;
1948
- }
1949
- lastWake = now;
1950
- const result = spawnSync(openclawBin, ['system', 'event', '--text', eventText, '--mode', mode], {stdio: 'inherit'});
1951
- if (result.error) {
1952
- openclawMissing = true;
1953
- console.error(`Failed to run ${openclawBin}: ${result.error.message}`);
1954
- }
1955
- }
1956
-
1957
- return new Promise((resolve, reject) => {
1958
- const watcher = spawn(fswatchBin, ['-0', '-o', statePath], {stdio: ['ignore', 'pipe', 'inherit']});
1959
-
1960
- watcher.on('error', (err) => {
1961
- if (err && err.code === 'ENOENT') {
1962
- reject(new Error(`fswatch not found. Install it (macOS: brew install fswatch).`));
1963
- return;
1964
- }
1965
- reject(new Error(`Failed to start fswatch: ${err && err.message ? err.message : String(err)}`));
1966
- });
1967
-
1968
- watcher.stdout.on('data', (chunk) => {
1969
- buffer += chunk.toString('utf8');
1970
- const parts = buffer.split('\0');
1971
- buffer = parts.pop();
1972
- if (parts.length === 0) {
1973
- return;
1974
- }
1975
- triggerWake();
1976
- });
1977
-
1978
- const forwardSignal = (signal) => {
1979
- if (!watcher.killed) {
1980
- watcher.kill(signal);
1981
- }
1982
- };
1983
- process.on('SIGINT', () => forwardSignal('SIGINT'));
1984
- process.on('SIGTERM', () => forwardSignal('SIGTERM'));
1985
-
1986
- watcher.on('close', (code) => {
1987
- if (code === 0) {
1988
- resolve();
1989
- return;
1990
- }
1991
- reject(new Error(`fswatch exited with code ${code}`));
1992
- });
1993
- });
1994
- }
1995
-
1996
- async function runWatchAll(argv) {
1997
- await Promise.all([runWatch(argv), runWatchState(argv)]);
1998
- }
1999
-
2000
- function runCronEnable(argv) {
2001
- const {flags} = parseArgs(argv);
2002
- const config = buildConfig();
2003
- const schedule = flags.schedule || '*/5 * * * *';
2004
- const nodeBin = process.execPath;
2005
- const cliPath = path.resolve(__filename);
2006
-
2007
- const envPairs = [];
2008
- if (getEnvValue('NBR_RELAY_URL')) {
2009
- envPairs.push(`NBR_RELAY_URL=${shellEscape(getEnvValue('NBR_RELAY_URL'))}`);
2010
- }
2011
- if (getEnvValue('NBR_STATE_PATH')) {
2012
- envPairs.push(`NBR_STATE_PATH=${shellEscape(expandHomePath(getEnvValue('NBR_STATE_PATH')))}`);
2013
- }
2014
- if (getEnvValue('NBR_POLL_LIMIT')) {
2015
- envPairs.push(`NBR_POLL_LIMIT=${shellEscape(getEnvValue('NBR_POLL_LIMIT'))}`);
2016
- }
2017
- if (getEnvValue('NBR_POLL_TYPES')) {
2018
- envPairs.push(`NBR_POLL_TYPES=${shellEscape(getEnvValue('NBR_POLL_TYPES'))}`);
2019
- }
2020
-
2021
- const envPrefix = envPairs.length > 0 ? `${envPairs.join(' ')} ` : '';
2022
- const command = `${shellEscape(nodeBin)} ${shellEscape(cliPath)} poll`;
2023
- const cronLine = `${schedule} ${envPrefix}${command} # nanobazaar-cli`;
2024
-
2025
- const listResult = spawnSync('crontab', ['-l'], {encoding: 'utf8'});
2026
- const current = listResult.status === 0 ? listResult.stdout : '';
2027
- const filtered = current
2028
- .split(/\r?\n/)
2029
- .filter((line) => line.trim() && !line.includes('# nanobazaar-cli'))
2030
- .join('\n');
2031
-
2032
- const next = [filtered, cronLine].filter(Boolean).join('\n') + '\n';
2033
- const installResult = spawnSync('crontab', ['-'], {input: next, encoding: 'utf8'});
2034
- if (installResult.status !== 0) {
2035
- throw new Error(`Failed to install cron entry: ${installResult.stderr || 'unknown error'}`);
2036
- }
2037
-
2038
- console.log('Cron enabled.');
2039
- console.log(`Schedule: ${schedule}`);
2040
- console.log(`Command: ${envPrefix}${command}`);
2041
- console.log(`State path: ${config.state_path}`);
2042
- }
2043
-
2044
- function runCronDisable() {
2045
- const listResult = spawnSync('crontab', ['-l'], {encoding: 'utf8'});
2046
- if (listResult.status !== 0) {
2047
- console.log('No crontab entries found.');
2048
- return;
2049
- }
2050
- const filtered = listResult.stdout
2051
- .split(/\r?\n/)
2052
- .filter((line) => line.trim() && !line.includes('# nanobazaar-cli'))
2053
- .join('\n');
2054
-
2055
- const next = filtered ? `${filtered}\n` : '';
2056
- const installResult = spawnSync('crontab', ['-'], {input: next, encoding: 'utf8'});
2057
- if (installResult.status !== 0) {
2058
- throw new Error(`Failed to remove cron entry: ${installResult.stderr || 'unknown error'}`);
2059
- }
2060
- console.log('Cron disabled.');
2034
+ stateWatcher.stop();
2061
2035
  }
2062
2036
 
2063
2037
  function loadVersion() {
@@ -2159,24 +2133,6 @@ async function main() {
2159
2133
  case 'watch':
2160
2134
  await runWatch(rest);
2161
2135
  return;
2162
- case 'watch-state':
2163
- await runWatchState(rest);
2164
- return;
2165
- case 'watch-all':
2166
- await runWatchAll(rest);
2167
- return;
2168
- case 'cron': {
2169
- const sub = rest[0];
2170
- if (sub === 'enable') {
2171
- runCronEnable(rest.slice(1));
2172
- return;
2173
- }
2174
- if (sub === 'disable') {
2175
- runCronDisable();
2176
- return;
2177
- }
2178
- throw new Error('Unknown cron command. Use: cron enable|disable');
2179
- }
2180
2136
  default:
2181
2137
  throw new Error(`Unknown command: ${command}`);
2182
2138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanobazaar-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "NanoBazaar CLI for the NanoBazaar Relay and OpenClaw skill.",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {