symbiot 0.22.0 → 0.23.0

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/index.mjs +57 -3
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -57,6 +57,8 @@ const PLAIN = has("plain") || !process.stdout.isTTY;
57
57
  let SERVING = false; // set while `symbiot app` runs — silences the CLI spinner
58
58
  let LAST_MAP = null; // cached graph so node clicks don't rescan
59
59
  const HANDOFFS = []; // live registry of agents Symbiot has handed work to
60
+ let LATEST_VERSION = ""; // newest symbiot on npm, checked in the background
61
+ async function checkLatest() { try { const r = await fetch("https://registry.npmjs.org/symbiot"); if (!r.ok) return; const j = await r.json(); LATEST_VERSION = (j["dist-tags"] && j["dist-tags"].latest) || ""; } catch {} }
60
62
 
61
63
  // ---- colour + spinner ------------------------------------------------------
62
64
  const c = PLAIN
@@ -1268,6 +1270,10 @@ select,input{width:100%;background:var(--ink);border:1px solid var(--line);borde
1268
1270
  select:focus,input:focus{outline:none;border-color:var(--green)}
1269
1271
  a{color:var(--green);cursor:pointer}.hidden{display:none}
1270
1272
  .note{font-size:12px;margin-top:10px}.ok{color:var(--green)}.err{color:var(--amber)}
1273
+ .updatebar{display:none;align-items:center;gap:12px;padding:9px 16px;font-size:13px;font-weight:600;background:var(--green);color:var(--ink)}
1274
+ .updatebar.show{display:flex}
1275
+ .updatebar.reconnect{background:var(--amber)}
1276
+ .updatebar button{font:inherit;font-weight:700;border:0;border-radius:8px;padding:5px 12px;background:var(--ink);color:var(--bone);cursor:pointer}
1271
1277
  footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
1272
1278
  .profile{font-size:13px;margin-bottom:8px;line-height:1.5}
1273
1279
  .profile b{color:var(--bone)}
@@ -1332,6 +1338,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
1332
1338
  .lg i{width:10px;height:10px;border-radius:50%;display:inline-block}
1333
1339
  @media(max-width:760px){.maprow{flex-direction:column}.detail{width:auto;max-height:none}}
1334
1340
  </style></head><body>
1341
+ <div id="updatebar" class="updatebar"></div>
1335
1342
  <header><span class="dot"></span><span class="brand">Symbiot</span><span class="status" id="status">...</span></header>
1336
1343
  <div class="tabs">
1337
1344
  <button class="tab active" data-tab="map">Map</button>
@@ -1631,6 +1638,24 @@ document.getElementById('addtask').addEventListener('click',addTaskUI);
1631
1638
  document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
1632
1639
  document.getElementById('agentcmd').addEventListener('change',saveAgent);
1633
1640
  document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
1641
+ var SRV_STARTED=null,srvDown=false,updBusy=false;
1642
+ function ubar(){return document.getElementById('updatebar');}
1643
+ function heartbeat(){
1644
+ api('/api/ping').then(function(p){
1645
+ var b=ubar();
1646
+ if(SRV_STARTED===null){SRV_STARTED=p.started;}
1647
+ else if(p.started!==SRV_STARTED){location.reload();return;}
1648
+ if(srvDown){location.reload();return;}
1649
+ if(updBusy)return;
1650
+ if(p.latest&&p.version&&p.latest!==p.version){
1651
+ b.className='updatebar show';
1652
+ b.innerHTML="A new Symbiot ("+esc(p.latest)+") is available &mdash; you're on "+esc(p.version)+". <button id='doupd'>Update &amp; restart</button>";
1653
+ var btn=document.getElementById('doupd');if(btn)btn.onclick=doUpdate;
1654
+ }else{b.className='updatebar';}
1655
+ }).catch(function(){srvDown=true;var b=ubar();b.className='updatebar reconnect show';b.textContent='Reconnecting to Symbiot…';});
1656
+ }
1657
+ function doUpdate(){updBusy=true;var b=ubar();b.className='updatebar show';b.textContent='Updating & restarting… this page will reload itself when it is back.';api('/api/update',{});}
1658
+ setInterval(heartbeat,4000);heartbeat();
1634
1659
  initGraphEvents();syncP();refresh();loadMap();loadAgentCfg();loadScanRoots();
1635
1660
  </script></body></html>`;
1636
1661
 
@@ -1673,7 +1698,13 @@ function openApp(url) {
1673
1698
  }
1674
1699
  async function cmdApp() {
1675
1700
  SERVING = true;
1676
- const TOKEN = randomBytes(16).toString("hex");
1701
+ const SERVER_STARTED = Date.now();
1702
+ // Stable token + port so the URL survives a restart — the open tab can
1703
+ // reconnect and auto-reload itself instead of you closing and reopening it.
1704
+ const cfg0 = loadConfig();
1705
+ let TOKEN = cfg0.appToken;
1706
+ if (!TOKEN) { TOKEN = randomBytes(16).toString("hex"); try { saveConfig({ ...loadConfig(), appToken: TOKEN }); } catch {} }
1707
+ const PORT = Number(process.env.SYMBIOT_PORT || cfg0.appPort) || 7391;
1677
1708
  const json = (res, obj) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
1678
1709
  const server = createServer(async (req, res) => {
1679
1710
  const u = new URL(req.url, "http://127.0.0.1");
@@ -1714,17 +1745,40 @@ async function cmdApp() {
1714
1745
  if (u.pathname === "/api/agents") return json(res, HANDOFFS.map((e) => { let tail = ""; try { tail = readFileSync(e.log, "utf8").slice(-1200); } catch {} return { id: e.id, name: e.name, path: e.path, status: e.status, elapsed: (e.endedAt || Date.now()) - e.startedAt, exitCode: e.exitCode, tail }; }));
1715
1746
  if (u.pathname === "/api/run" && req.method === "POST") { const b = await readBody(req); const cmd = ["week", "standup", "todo"].includes(b.cmd) ? b.cmd : "week"; return json(res, await produce(cmd)); }
1716
1747
  if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
1748
+ if (u.pathname === "/api/ping") return json(res, { version: VERSION, started: SERVER_STARTED, latest: LATEST_VERSION });
1749
+ if (u.pathname === "/api/update" && req.method === "POST") {
1750
+ // Install the newest symbiot, then relaunch this same app (same port +
1751
+ // token => same URL) and exit. The open page's heartbeat reconnects and
1752
+ // reloads to the new version. No terminal, no reopening the window.
1753
+ const inst = process.platform === "win32" ? "npm i -g symbiot@latest" : "npm install -g symbiot@latest";
1754
+ const e = track("symbiot update", inst, homedir(), (code) => {
1755
+ if (code !== 0) return;
1756
+ try { const ch = spawn(process.execPath, process.argv.slice(1), { detached: true, stdio: "ignore", env: process.env }); ch.unref(); } catch {}
1757
+ setTimeout(() => process.exit(0), 1200);
1758
+ });
1759
+ return json(res, { started: true, id: e ? e.id : "" });
1760
+ }
1717
1761
  if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
1718
1762
  } catch (e) { res.writeHead(500, { "content-type": "application/json" }); res.end(JSON.stringify({ error: String((e && e.message) || e) })); return; }
1719
1763
  res.writeHead(404); res.end("not found");
1720
1764
  });
1721
- server.listen(0, "127.0.0.1", () => {
1765
+ let announced = false, tries = 0, opened = false;
1766
+ server.on("listening", () => {
1767
+ if (announced) return; announced = true;
1722
1768
  const url = `http://127.0.0.1:${server.address().port}/?t=${TOKEN}`;
1723
- const how = openApp(url);
1769
+ const how = opened ? "" : openApp(url); opened = true; // only pop a window the first time
1724
1770
  console.log(`\n${c.g("●")} ${c.b("Symbiot")} is running at ${c.b(url)}`);
1725
1771
  console.log(how ? c.d(` Opened in a ${how}.`) : c.d(" Open that URL in your browser."));
1726
1772
  console.log(c.d(" Leave this running; press Ctrl+C to stop (or click Quit in the window)."));
1727
1773
  });
1774
+ server.on("error", (e) => {
1775
+ // Stable port busy (an older instance still exiting during an update, or a
1776
+ // second app): retry briefly, then fall back to a random port.
1777
+ if (e && e.code === "EADDRINUSE" && tries < 8) { tries++; setTimeout(() => { try { server.listen(PORT, "127.0.0.1"); } catch {} }, 500); }
1778
+ else { try { server.listen(0, "127.0.0.1"); } catch {} }
1779
+ });
1780
+ server.listen(PORT, "127.0.0.1");
1781
+ checkLatest(); setInterval(checkLatest, 10 * 60 * 1000).unref(); // background update check
1728
1782
  }
1729
1783
 
1730
1784
  const HELP = `${c.b("symbiot")} — your week, written from your real work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {