@synkro-sh/cli 1.3.26 → 1.3.28

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/dist/bootstrap.js CHANGED
@@ -1677,7 +1677,7 @@ Commands:
1677
1677
  status - print "running"/"stopped"
1678
1678
  """
1679
1679
 
1680
- import os, sys, json, socket, time, signal, fcntl, re
1680
+ import os, sys, json, socket, time, signal, fcntl, re, select
1681
1681
  import subprocess, threading
1682
1682
  from pathlib import Path
1683
1683
 
@@ -1707,27 +1707,49 @@ def log(msg):
1707
1707
  pass
1708
1708
 
1709
1709
 
1710
+ STALL_TIMEOUT_SEC = int(os.environ.get("SYNKRO_DAEMON_STALL_TIMEOUT", "10"))
1711
+
1710
1712
  def _read_response(proc, timeout=45):
1711
1713
  acc = []
1712
1714
  deadline = time.time() + timeout
1715
+ last_data = time.time()
1716
+ fd = proc.stdout.fileno()
1717
+ buf = ""
1713
1718
  while True:
1714
- if time.time() > deadline:
1719
+ remaining = deadline - time.time()
1720
+ if remaining <= 0:
1715
1721
  log("read timeout")
1716
1722
  return ""
1717
- line = proc.stdout.readline()
1718
- if not line:
1723
+ if time.time() - last_data > STALL_TIMEOUT_SEC:
1724
+ log(f"stall timeout: no data for {STALL_TIMEOUT_SEC}s")
1719
1725
  return ""
1720
- try:
1721
- obj = json.loads(line)
1722
- except json.JSONDecodeError:
1726
+ ready, _, _ = select.select([fd], [], [], min(remaining, 2.0))
1727
+ if not ready:
1728
+ if proc.poll() is not None:
1729
+ log("process exited during read")
1730
+ return ""
1723
1731
  continue
1724
- t = obj.get("type")
1725
- if t == "assistant":
1726
- for c in obj.get("message", {}).get("content", []):
1727
- if c.get("type") == "text":
1728
- acc.append(c["text"])
1729
- elif t == "result":
1730
- return "".join(acc)
1732
+ chunk = os.read(fd, 65536)
1733
+ if not chunk:
1734
+ return ""
1735
+ last_data = time.time()
1736
+ buf += chunk.decode("utf-8", errors="replace")
1737
+ while "\\n" in buf:
1738
+ line, buf = buf.split("\\n", 1)
1739
+ line = line.strip()
1740
+ if not line:
1741
+ continue
1742
+ try:
1743
+ obj = json.loads(line)
1744
+ except json.JSONDecodeError:
1745
+ continue
1746
+ t = obj.get("type")
1747
+ if t == "assistant":
1748
+ for c in obj.get("message", {}).get("content", []):
1749
+ if c.get("type") == "text":
1750
+ acc.append(c["text"])
1751
+ elif t == "result":
1752
+ return "".join(acc)
1731
1753
 
1732
1754
 
1733
1755
  def _send_msg(proc, text):
@@ -1757,6 +1779,7 @@ class WarmGrader:
1757
1779
  self._warm_thread = None
1758
1780
  self._lock = threading.Lock()
1759
1781
  self._total_grades = 0
1782
+ self._prewarm_ok = True
1760
1783
  self._start_prewarm()
1761
1784
 
1762
1785
  def _make_proc(self):
@@ -1786,27 +1809,32 @@ class WarmGrader:
1786
1809
  log("pre-warming process")
1787
1810
  proc = self._make_proc()
1788
1811
  _send_msg(proc, "Ready")
1789
- resp = _read_response(proc, timeout=30)
1812
+ resp = _read_response(proc, timeout=15)
1790
1813
  if resp:
1791
1814
  with self._lock:
1792
1815
  old = self._warm_proc
1793
1816
  self._warm_proc = proc
1817
+ self._prewarm_ok = True
1794
1818
  if old:
1795
1819
  self._kill_proc(old)
1796
1820
  log(f"pre-warm ready ({len(resp)} chars)")
1797
1821
  else:
1798
1822
  log("pre-warm response empty")
1799
1823
  self._kill_proc(proc)
1824
+ self._prewarm_ok = False
1800
1825
  except Exception as e:
1801
1826
  log(f"pre-warm failed: {e}")
1827
+ self._prewarm_ok = False
1802
1828
 
1803
1829
  def _start_prewarm(self):
1804
1830
  self._warm_thread = threading.Thread(target=self._prewarm, daemon=True)
1805
1831
  self._warm_thread.start()
1806
1832
 
1807
1833
  def grade(self, prompt):
1808
- if self._warm_thread:
1809
- self._warm_thread.join(timeout=60)
1834
+ if self._warm_thread and self._prewarm_ok:
1835
+ self._warm_thread.join(timeout=8)
1836
+ elif self._warm_thread and not self._prewarm_ok:
1837
+ log("skipping prewarm join (last prewarm failed)")
1810
1838
 
1811
1839
  with self._lock:
1812
1840
  proc = self._warm_proc
@@ -1817,11 +1845,17 @@ class WarmGrader:
1817
1845
  log("no warm process, cold fallback")
1818
1846
  proc = self._make_proc()
1819
1847
  warm = False
1848
+ elif proc.stdin.closed:
1849
+ log("warm process stdin closed, cold fallback")
1850
+ self._kill_proc(proc)
1851
+ proc = self._make_proc()
1852
+ warm = False
1820
1853
 
1854
+ wall_limit = int(os.environ.get("SYNKRO_DAEMON_WALL_TIMEOUT", "12"))
1821
1855
  t0 = time.time()
1822
1856
  try:
1823
1857
  _send_msg(proc, prompt)
1824
- resp = _read_response(proc, timeout=GRADE_TIMEOUT_SEC)
1858
+ resp = _read_response(proc, timeout=min(GRADE_TIMEOUT_SEC, wall_limit))
1825
1859
  except Exception as e:
1826
1860
  log(f"grade error: {e}")
1827
1861
  resp = ""
@@ -2339,7 +2373,7 @@ async function refreshToken() {
2339
2373
  const creds = loadCredentials();
2340
2374
  if (!creds?.refresh_token) return false;
2341
2375
  try {
2342
- const response = await fetch(`${SYNKRO_WEB_AUTH_URL}/api/auth/refresh`, {
2376
+ const response = await fetch(`${SYNKRO_API_URL}/api/auth/refresh`, {
2343
2377
  method: "POST",
2344
2378
  headers: { "Content-Type": "application/json" },
2345
2379
  body: JSON.stringify({ refresh_token: creds.refresh_token })
@@ -2348,6 +2382,7 @@ async function refreshToken() {
2348
2382
  const data = await response.json();
2349
2383
  if (data.access_token) {
2350
2384
  saveCredentials({
2385
+ ...creds,
2351
2386
  access_token: data.access_token,
2352
2387
  refresh_token: data.refresh_token || creds.refresh_token
2353
2388
  });
@@ -2379,7 +2414,7 @@ function clearCredentials() {
2379
2414
  unlinkSync2(AUTH_FILE);
2380
2415
  }
2381
2416
  }
2382
- var PORT, RAW_WEB_AUTH_URL, SYNKRO_WEB_AUTH_URL, AUTH_FILE, ERROR_HTML, refreshPromise;
2417
+ var PORT, RAW_WEB_AUTH_URL, SYNKRO_WEB_AUTH_URL, AUTH_FILE, RAW_API_URL, SYNKRO_API_URL, ERROR_HTML, refreshPromise;
2383
2418
  var init_stub = __esm({
2384
2419
  "cli/auth/stub.ts"() {
2385
2420
  "use strict";
@@ -2387,6 +2422,8 @@ var init_stub = __esm({
2387
2422
  RAW_WEB_AUTH_URL = process.env.SYNKRO_WEB_AUTH_URL;
2388
2423
  SYNKRO_WEB_AUTH_URL = RAW_WEB_AUTH_URL && /^https?:\/\//.test(RAW_WEB_AUTH_URL) ? RAW_WEB_AUTH_URL : "https://app.synkro.sh";
2389
2424
  AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join3(homedir3(), ".synkro", "credentials.json");
2425
+ RAW_API_URL = process.env.SYNKRO_CRUD_URL || process.env.SYNKRO_API_URL;
2426
+ SYNKRO_API_URL = RAW_API_URL && /^https?:\/\//.test(RAW_API_URL) ? RAW_API_URL : "https://api.synkro.sh";
2390
2427
  ERROR_HTML = `
2391
2428
  <!DOCTYPE html>
2392
2429
  <html>
@@ -2448,6 +2485,7 @@ async function callApi(method, endpoint, body) {
2448
2485
  throw new Error("API URL not configured. Run `synkro install` first.");
2449
2486
  }
2450
2487
  const url = `${API_URL}${endpoint}`;
2488
+ await ensureValidToken();
2451
2489
  const accessToken = getAccessToken();
2452
2490
  const headers = {
2453
2491
  "Content-Type": "application/json"
@@ -3333,7 +3371,7 @@ function writeConfigEnv(opts) {
3333
3371
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3334
3372
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3335
3373
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3336
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.26")}`
3374
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.28")}`
3337
3375
  ];
3338
3376
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3339
3377
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -4000,6 +4038,7 @@ async function statusCommand() {
4000
4038
  const gatewayUrl = (config.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/^['"]|['"]$/g, "");
4001
4039
  let serverTier = config.SYNKRO_TIER || "(unset)";
4002
4040
  let serverInference = config.SYNKRO_INFERENCE || "fast";
4041
+ await ensureValidToken();
4003
4042
  const token = getAccessToken();
4004
4043
  if (token) {
4005
4044
  try {