@m13v/s4l 1.7.4-rc.38 → 1.7.4-rc.39

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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.4-rc.38",
3
- "installedAt": "2026-07-14T01:01:14.735Z"
2
+ "version": "1.7.4-rc.39",
3
+ "installedAt": "2026-07-14T01:27:42.097Z"
4
4
  }
package/mcp/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "social-autoposter",
4
4
  "display_name": "S4L",
5
- "version": "1.7.4-rc.38",
5
+ "version": "1.7.4-rc.39",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts.",
7
7
  "long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L plugin end to end**\n\n2\\. Quit with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
8
8
  "author": {
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l-mcp",
3
- "version": "1.7.4-rc.38",
3
+ "version": "1.7.4-rc.39",
4
4
  "private": true,
5
5
  "description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l",
3
- "version": "1.7.4-rc.38",
3
+ "version": "1.7.4-rc.39",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js",
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ """Shared zero-progress deadline for browser-touching pipeline steps.
3
+
4
+ Any step that relays CDP to the harness Chrome (scan queries, thread media
5
+ capture, ...) can hang FOREVER against a half-wedged browser or a dead
6
+ renderer: the call never returns, the step holds the browser lock, and the
7
+ cycle-entry health checks that could heal Chrome can't run because the next
8
+ cycle can't start. 2026-07-13: a scan hung 23 min at zero queries; the same
9
+ night media capture hung 34+ min at candidate 24/92 and only the watchdog's
10
+ 90-min lock-liveness net was ever going to reap it.
11
+
12
+ One implementation, used by every step (twitter_scan.py Phase-1 queries,
13
+ twitter_browser.py media capture). Usage:
14
+
15
+ from stall_guard import stall_guard
16
+ with stall_guard("thread_media", url): # default deadline 240s
17
+ <one bounded unit of browser work>
18
+
19
+ If the guarded block does not finish inside the deadline the guard:
20
+ 1. prints a loud machine-greppable marker to stderr
21
+ ([stall_guard] STALL_ABORT step=<step> ...)
22
+ 2. stamps the port's wedge strike file, so the next cycle's two-strike
23
+ gate in skill/lib/twitter-backend.sh converges to a kill+relaunch if
24
+ Chrome is genuinely sick
25
+ 3. hard-exits the process with EX_TEMPFAIL (75) so the cycle fails fast
26
+ and releases the browser lock + cycle slot.
27
+
28
+ A completed block cancels its timer; a healthy pipeline never notices the
29
+ guard exists. Deadline override: S4L_BROWSER_OP_DEADLINE_S (seconds).
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import contextlib
34
+ import os
35
+ import pathlib
36
+ import sys
37
+ import threading
38
+
39
+ DEFAULT_DEADLINE_S = float(os.environ.get("S4L_BROWSER_OP_DEADLINE_S", "240"))
40
+
41
+
42
+ def _strike_file(port: int) -> str:
43
+ # Must match the two-strike gate in skill/lib/twitter-backend.sh.
44
+ return f"/tmp/s4l_cdp_wedge_strike_{port}"
45
+
46
+
47
+ @contextlib.contextmanager
48
+ def stall_guard(step: str, detail: str = "", deadline_s: float | None = None, port: int = 9555):
49
+ limit = DEFAULT_DEADLINE_S if deadline_s is None else float(deadline_s)
50
+
51
+ def _fire():
52
+ sys.stderr.write(
53
+ f"[stall_guard] STALL_ABORT step={step} detail={detail!r} "
54
+ f"deadline={limit:.0f}s — browser call hung (wedged Chrome or dead "
55
+ "renderer); stamping wedge strike and aborting so the lock frees\n"
56
+ )
57
+ sys.stderr.flush()
58
+ try:
59
+ pathlib.Path(_strike_file(port)).touch()
60
+ except OSError:
61
+ pass
62
+ os._exit(75) # EX_TEMPFAIL
63
+
64
+ t = threading.Timer(limit, _fire)
65
+ t.daemon = True
66
+ t.start()
67
+ try:
68
+ yield
69
+ finally:
70
+ t.cancel()
@@ -2732,6 +2732,19 @@ def _anchor_repost_from_tweets(tweets, anchor_tweet_id):
2732
2732
 
2733
2733
 
2734
2734
  def scrape_thread_media(thread_url, scroll_count=1):
2735
+ """Deadline wrapper: media capture relays CDP to the harness Chrome, and
2736
+ against a half-wedged browser or dead renderer a single evaluate can hang
2737
+ FOREVER (2026-07-13: hung 34+ min at candidate 24/92 holding the browser
2738
+ lock; only the watchdog's 90-min lock-liveness net would have reaped it).
2739
+ stall_guard aborts the process fast, stamps the wedge strike, and lets the
2740
+ cycle-entry heal chain converge. Shared implementation — see
2741
+ scripts/stall_guard.py; do NOT re-implement per step."""
2742
+ from stall_guard import stall_guard
2743
+ with stall_guard("thread_media", thread_url):
2744
+ return _scrape_thread_media_inner(thread_url, scroll_count)
2745
+
2746
+
2747
+ def _scrape_thread_media_inner(thread_url, scroll_count=1):
2735
2748
  """Navigate to a tweet's permalink and return the media of the anchor tweet.
2736
2749
 
2737
2750
  Deterministic, model-free media capture for the MAIN posting cycle: the
@@ -56,7 +56,6 @@ import os
56
56
  import pathlib
57
57
  import re
58
58
  import sys
59
- import threading
60
59
  import time
61
60
  import urllib.parse
62
61
 
@@ -247,38 +246,13 @@ def _navigate(url: str) -> None:
247
246
  new_tab(url)
248
247
 
249
248
 
250
- # Per-query stall guard (2026-07-13). The daemon helpers (goto_url/js) relay
251
- # CDP through the harness daemon; against a half-wedged Chrome those calls can
252
- # hang far past any useful budget, and one stuck query froze a Phase 1 scan at
253
- # "scan 0 · 23m" while holding the browser lock — the entry wedge check can't
254
- # help because the NEXT cycle can't start until this one dies. Self-heal from
255
- # inside instead: if a single scan() call exceeds the deadline, print a loud
256
- # marker, stamp the wedge strike file (so the next cycle's two-strike gate in
257
- # twitter-backend.sh converges to a kill+relaunch if Chrome is still sick),
258
- # and hard-exit the scan process so the cycle fails fast and releases the
259
- # lock. A healthy query completes in well under a minute; 240s is generous.
260
- _SCAN_QUERY_DEADLINE_S = float(os.environ.get("S4L_SCAN_QUERY_DEADLINE_S", "240"))
261
- _WEDGE_STRIKE_FILE = "/tmp/s4l_cdp_wedge_strike_9555" # matches twitter-backend.sh
262
-
263
-
264
- def _arm_stall_guard(query: str):
265
- def _fire():
266
- sys.stderr.write(
267
- f"[twitter_scan] SCAN_STALL_ABORT query={query!r} "
268
- f"deadline={_SCAN_QUERY_DEADLINE_S:.0f}s — CDP call hung (wedged "
269
- "Chrome); stamping wedge strike and aborting the scan process\n"
270
- )
271
- sys.stderr.flush()
272
- try:
273
- pathlib.Path(_WEDGE_STRIKE_FILE).touch()
274
- except OSError:
275
- pass
276
- os._exit(75) # EX_TEMPFAIL: cycle fails fast, lock + slot free up
249
+ # Per-query stall guard: shared implementation (scripts/stall_guard.py) used
250
+ # by every browser-touching pipeline step — do NOT re-implement per step.
251
+ # S4L_SCAN_QUERY_DEADLINE_S kept as a scan-specific override for continuity
252
+ # with rc.36-38; unset, the shared default applies.
253
+ from stall_guard import stall_guard # noqa: E402
277
254
 
278
- t = threading.Timer(_SCAN_QUERY_DEADLINE_S, _fire)
279
- t.daemon = True
280
- t.start()
281
- return t
255
+ _SCAN_DEADLINE_OVERRIDE = os.environ.get("S4L_SCAN_QUERY_DEADLINE_S")
282
256
 
283
257
 
284
258
  def scan(
@@ -293,11 +267,12 @@ def scan(
293
267
  ###TWEETS_BEGIN###/###TWEETS_END### sentinels for the scan model to relay
294
268
  into StructuredOutput; also returns the kept list so direct callers (tests,
295
269
  future shell-driven invocations) can consume it without parsing stdout."""
296
- _stall_guard = _arm_stall_guard(query)
297
- try:
270
+ with stall_guard(
271
+ "scan_query",
272
+ query,
273
+ deadline_s=float(_SCAN_DEADLINE_OVERRIDE) if _SCAN_DEADLINE_OVERRIDE else None,
274
+ ):
298
275
  return _scan_inner(query, project, search_topic, freshness_hours, skip_ids, settle_seconds)
299
- finally:
300
- _stall_guard.cancel()
301
276
 
302
277
 
303
278
  def _scan_inner(
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # browser-launch.sh — the ONE way any backend spawns a harness Chrome.
3
+ #
4
+ # Consolidates what twitter-backend.sh and linkedin-backend.sh grew as
5
+ # hand-rolled near-duplicates on 2026-07-13 (and reddit-backend.sh never got
6
+ # at all). Source this from a backend lib and call:
7
+ #
8
+ # launch_harness_chrome "$_chrome_bin" "$_prof_dir" <chrome arg>...
9
+ #
10
+ # What every launch gets, uniformly:
11
+ # 1. CLEAN-EXIT STAMP: mark the profile's last exit as clean first, so a
12
+ # SIGKILLed Chrome (wedge heal, stall abort) doesn't session-restore a
13
+ # crashed "Aw, Snap" corpse tab on relaunch.
14
+ # 2. NO FOCUS STEAL (macOS + .app bundle): `open -n -g` launches a NEW
15
+ # instance (-n: the user's personal Chrome may already be running;
16
+ # without it LaunchServices pokes that instance and drops our --args)
17
+ # without activation (-g). A directly-exec'd Chrome always activates
18
+ # itself and planted a window over the user's work on every relaunch.
19
+ # LaunchServices also parents the process outside the caller's launchd
20
+ # job process group — independent cover for the pgroup-reaping bug.
21
+ # (Tested alternatives that DON'T work: `-j` — Chrome unhides and
22
+ # activates itself anyway; `--no-startup-window` — the activation just
23
+ # moves to first-tab creation.)
24
+ # 3. DETACHED FALLBACK (Linux / bare chromium binaries): direct exec in a
25
+ # NEW SESSION via os.setsid, so a transient launchd job's exit can't
26
+ # SIGKILL Chrome with the job's process group (2026-07-12 root cause of
27
+ # the kill-relaunch-foreground loop).
28
+ launch_harness_chrome() {
29
+ local _bl_chrome_bin="$1"; shift
30
+ local _bl_prof_dir="$1"; shift
31
+ "${S4L_PYTHON:-python3}" -c 'import json, os, sys
32
+ p = os.path.join(sys.argv[1], "Default", "Preferences")
33
+ try:
34
+ d = json.load(open(p))
35
+ except Exception:
36
+ raise SystemExit(0)
37
+ prof = d.setdefault("profile", {})
38
+ prof["exit_type"] = "Normal"
39
+ prof["exited_cleanly"] = True
40
+ json.dump(d, open(p, "w"))' "$_bl_prof_dir" 2>/dev/null || true
41
+ local _bl_app_bundle=""
42
+ case "$_bl_chrome_bin" in
43
+ *.app/Contents/MacOS/*) _bl_app_bundle="${_bl_chrome_bin%%/Contents/MacOS/*}" ;;
44
+ esac
45
+ if [ "$(uname -s)" = "Darwin" ] && [ -n "$_bl_app_bundle" ] && [ -d "$_bl_app_bundle" ]; then
46
+ open -n -g -a "$_bl_app_bundle" --args "$@" >/dev/null 2>&1 || true
47
+ else
48
+ "${S4L_PYTHON:-python3}" -c 'import os,sys
49
+ os.setsid()
50
+ os.execv(sys.argv[1], sys.argv[1:])' \
51
+ "$_bl_chrome_bin" "$@" >/dev/null 2>&1 &
52
+ disown
53
+ fi
54
+ }
@@ -333,52 +333,21 @@ ensure_linkedin_browser_for_backend() {
333
333
  # The occlusion/backgrounding flags matter: the window sits offscreen,
334
334
  # and without them Chrome stops laying out SPA-rendered content, so
335
335
  # every element measures 0x0 and clicks become impossible (2026-07-03).
336
- # Mark the profile's last exit as clean BEFORE launching, so a
337
- # SIGKILLed Chrome doesn't session-restore a crashed "Aw, Snap" tab on
338
- # relaunch (same fix + rationale as twitter-backend.sh, 2026-07-13).
339
- "${S4L_PYTHON:-python3}" -c 'import json, os, sys
340
- p = os.path.join(sys.argv[1], "Default", "Preferences")
341
- try:
342
- d = json.load(open(p))
343
- except Exception:
344
- raise SystemExit(0)
345
- prof = d.setdefault("profile", {})
346
- prof["exit_type"] = "Normal"
347
- prof["exited_cleanly"] = True
348
- json.dump(d, open(p, "w"))' "$_prof_dir" 2>/dev/null || true
349
- # Launch flags shared by both spawn paths below.
350
- local _li_launch_args=(
351
- --remote-debugging-port=9556
352
- --user-data-dir="$HOME/.claude/browser-profiles/browser-harness-linkedin"
353
- --no-first-run --no-default-browser-check
354
- --disable-features=ChromeWhatsNewUI
355
- --disable-backgrounding-occluded-windows
356
- --disable-renderer-backgrounding
357
- --disable-background-timer-throttling
358
- "${_extra[@]}"
336
+ # Spawn via the SHARED launcher (skill/lib/browser-launch.sh) — see
337
+ # that file for the clean-exit stamp + no-focus-steal + detach
338
+ # rationale. Do NOT hand-roll launch logic per backend.
339
+ # shellcheck disable=SC1091
340
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
341
+ launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
342
+ --remote-debugging-port=9556 \
343
+ --user-data-dir="$HOME/.claude/browser-profiles/browser-harness-linkedin" \
344
+ --no-first-run --no-default-browser-check \
345
+ --disable-features=ChromeWhatsNewUI \
346
+ --disable-backgrounding-occluded-windows \
347
+ --disable-renderer-backgrounding \
348
+ --disable-background-timer-throttling \
349
+ "${_extra[@]}" \
359
350
  about:blank
360
- )
361
- # macOS + .app-bundled Chrome: `open -n -g` launches a NEW instance
362
- # WITHOUT stealing the user's focus (a directly-exec'd Chrome always
363
- # activates itself), and LaunchServices parents it outside this launchd
364
- # job's process group. Same fix + rationale as twitter-backend.sh
365
- # (2026-07-13). Fallback: direct exec in a new session — os.setsid so
366
- # Chrome escapes the job's process group; launchd SIGKILLs a transient
367
- # job's whole pgroup when the shell exits, and `disown` does not change
368
- # the pgid (2026-07-12).
369
- local _li_app_bundle=""
370
- case "$_chrome_bin" in
371
- *.app/Contents/MacOS/*) _li_app_bundle="${_chrome_bin%%/Contents/MacOS/*}" ;;
372
- esac
373
- if [ "$(uname -s)" = "Darwin" ] && [ -n "$_li_app_bundle" ] && [ -d "$_li_app_bundle" ]; then
374
- open -n -g -a "$_li_app_bundle" --args "${_li_launch_args[@]}" >/dev/null 2>&1 || true
375
- else
376
- "${S4L_PYTHON:-python3}" -c 'import os,sys
377
- os.setsid()
378
- os.execv(sys.argv[1], sys.argv[1:])' \
379
- "$_chrome_bin" "${_li_launch_args[@]}" >/dev/null 2>&1 &
380
- disown
381
- fi
382
351
  for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
383
352
  curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null && break
384
353
  sleep 1
@@ -203,14 +203,20 @@ ensure_reddit_browser_for_backend() {
203
203
  [ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
204
204
  rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
205
205
  fi
206
- "$_chrome_bin" \
206
+ # Spawn via the SHARED launcher (skill/lib/browser-launch.sh): clean-
207
+ # exit stamp (no Aw-Snap restore), `open -n -g` on macOS (no focus
208
+ # steal, out of this job's pgroup), setsid exec fallback elsewhere.
209
+ # This backend previously used a bare exec and had NONE of those
210
+ # protections (2026-07-13 consolidation).
211
+ # shellcheck disable=SC1091
212
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
213
+ launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
207
214
  --remote-debugging-port=9557 \
208
215
  --user-data-dir="$HOME/.claude/browser-profiles/reddit-harness" \
209
216
  --no-first-run --no-default-browser-check \
210
217
  --disable-features=ChromeWhatsNewUI \
211
218
  "${_extra[@]}" \
212
- about:blank >/dev/null 2>&1 &
213
- disown
219
+ about:blank
214
220
  for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
215
221
  curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null && break
216
222
  sleep 1
@@ -334,64 +334,21 @@ ensure_twitter_browser_for_backend() {
334
334
  [ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
335
335
  rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
336
336
  fi
337
- # Mark the profile's last exit as clean BEFORE launching. A SIGKILLed
338
- # Chrome (wedge heal, stall abort) records exit_type=Crashed, and the
339
- # next launch then session-restores the dead session's tab, which
340
- # renders as a lingering "Aw, Snap" corpse the user keeps seeing
341
- # (2026-07-13, twice). With the profile marked clean, Chrome opens
342
- # ONLY our startup URL. Best-effort: a missing/corrupt Preferences
343
- # file just skips.
344
- "${S4L_PYTHON:-python3}" -c 'import json, os, sys
345
- p = os.path.join(sys.argv[1], "Default", "Preferences")
346
- try:
347
- d = json.load(open(p))
348
- except Exception:
349
- raise SystemExit(0)
350
- prof = d.setdefault("profile", {})
351
- prof["exit_type"] = "Normal"
352
- prof["exited_cleanly"] = True
353
- json.dump(d, open(p, "w"))' "$_prof_dir" 2>/dev/null || true
354
- # Launch flags shared by both spawn paths below.
355
- local _launch_args=(
356
- --remote-debugging-port=9555
357
- --user-data-dir="$HOME/.claude/browser-profiles/browser-harness"
358
- --no-first-run --no-default-browser-check
359
- --password-store=basic --use-mock-keychain
360
- --disable-features=ChromeWhatsNewUI,CalculateNativeWinOcclusion
361
- --disable-backgrounding-occluded-windows
362
- "${_extra[@]}"
337
+ # Spawn via the SHARED launcher (skill/lib/browser-launch.sh): clean-
338
+ # exit stamp (no Aw-Snap restore), `open -n -g` on macOS (no focus
339
+ # steal, out of this job's pgroup), setsid exec fallback elsewhere.
340
+ # Do NOT hand-roll launch logic per backend.
341
+ # shellcheck disable=SC1091
342
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
343
+ launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
344
+ --remote-debugging-port=9555 \
345
+ --user-data-dir="$HOME/.claude/browser-profiles/browser-harness" \
346
+ --no-first-run --no-default-browser-check \
347
+ --password-store=basic --use-mock-keychain \
348
+ --disable-features=ChromeWhatsNewUI,CalculateNativeWinOcclusion \
349
+ --disable-backgrounding-occluded-windows \
350
+ "${_extra[@]}" \
363
351
  "${BH_LAUNCH_URL:-https://x.com}"
364
- )
365
- # macOS + .app-bundled Chrome: launch via `open -n -g` so the window
366
- # NEVER steals the user's focus. A directly-exec'd Chrome always
367
- # activates itself on launch — every legitimate relaunch (wedge heal,
368
- # user quit, crash) popped a window over the user's work (the
369
- # foreground-telemetry `cause:launched` events, 2026-07-13). -n forces
370
- # a NEW instance (the user's personal Chrome may already be running;
371
- # without -n LaunchServices would just poke it and drop our --args),
372
- # -g launches WITHOUT activation. LaunchServices also parents the
373
- # process outside this launchd job's process group, which independently
374
- # covers the 2026-07-12 pgroup-reaping bug the setsid fallback fixes.
375
- local _app_bundle=""
376
- case "$_chrome_bin" in
377
- *.app/Contents/MacOS/*) _app_bundle="${_chrome_bin%%/Contents/MacOS/*}" ;;
378
- esac
379
- if [ "$(uname -s)" = "Darwin" ] && [ -n "$_app_bundle" ] && [ -d "$_app_bundle" ]; then
380
- open -n -g -a "$_app_bundle" --args "${_launch_args[@]}" >/dev/null 2>&1 || true
381
- else
382
- # Fallback (Linux, bare chromium binaries): direct exec in a NEW
383
- # SESSION. os.setsid: Chrome must escape THIS job's process group —
384
- # launchd SIGKILLs a transient job's whole process group the moment
385
- # the shell exits (no AbandonProcessGroup), and `disown` does not
386
- # change the pgid, so a plainly-backgrounded Chrome died on every
387
- # cycle completion and the NEXT cycle's relaunch stole the user's
388
- # focus (2026-07-12 root cause).
389
- "${S4L_PYTHON:-python3}" -c 'import os,sys
390
- os.setsid()
391
- os.execv(sys.argv[1], sys.argv[1:])' \
392
- "$_chrome_bin" "${_launch_args[@]}" >/dev/null 2>&1 &
393
- disown
394
- fi
395
352
  for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
396
353
  curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null && break
397
354
  sleep 1