symbiot 0.20.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.
- package/index.mjs +113 -15
- 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
|
|
@@ -593,21 +595,57 @@ function recommendModels(hw) {
|
|
|
593
595
|
// claude -p "{prompt}" · aider --message "{prompt}" · code {dir}
|
|
594
596
|
// gnome-terminal --working-directory={dir} -- claude "{prompt}"
|
|
595
597
|
// Not tied to any one tool — you decide what runs.
|
|
596
|
-
|
|
598
|
+
// [cmd, label, macAppName] — macApp used to launch GUI editors on macOS where
|
|
599
|
+
// the CLI isn't on PATH (they're .app bundles).
|
|
600
|
+
const IDE_LIST = [["code", "VS Code", "Visual Studio Code"], ["cursor", "Cursor", "Cursor"], ["windsurf", "Windsurf", "Windsurf"], ["zed", "Zed", "Zed"], ["subl", "Sublime Text", "Sublime Text"], ["idea", "IntelliJ IDEA", "IntelliJ IDEA"], ["nvim", "Neovim", ""]];
|
|
597
601
|
const AGENT_LIST = [
|
|
598
602
|
["claude", "Claude Code — make changes", 'claude -p "{prompt}" --permission-mode acceptEdits'],
|
|
599
603
|
["claude", "Claude Code — plan only (asks first)", 'claude -p "{prompt}"'],
|
|
604
|
+
["codex", "Codex (OpenAI) — make changes", 'codex exec --full-auto "{prompt}"'],
|
|
600
605
|
["aider", "Aider — make changes", 'aider --message "{prompt}" --yes'],
|
|
601
606
|
["cursor-agent", "Cursor agent", 'cursor-agent -p "{prompt}"'],
|
|
607
|
+
["gemini", "Gemini CLI", 'gemini -p "{prompt}"'],
|
|
602
608
|
];
|
|
609
|
+
// Cross-platform "is this command available?" (command -v on posix, where on win).
|
|
610
|
+
function hasCmd(cmd) { try { return !!sh(process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`).trim(); } catch { return false; } }
|
|
611
|
+
function macApp(name) { if (process.platform !== "darwin" || !name) return ""; for (const base of ["/Applications", join(homedir(), "Applications")]) { try { if (existsSync(join(base, name + ".app"))) return name; } catch {} } return ""; }
|
|
612
|
+
// Find the Orca IDE CLI across OSes (known locations, then a bounded search).
|
|
613
|
+
let ORCA_CLI; // cached per process: undefined=unchecked, ""=none, string=path
|
|
614
|
+
function findOrcaCli() {
|
|
615
|
+
if (ORCA_CLI !== undefined) return ORCA_CLI;
|
|
616
|
+
ORCA_CLI = _findOrcaCli();
|
|
617
|
+
return ORCA_CLI;
|
|
618
|
+
}
|
|
619
|
+
function _findOrcaCli() {
|
|
620
|
+
const home = homedir(); const cands = [];
|
|
621
|
+
if (process.platform === "linux") cands.push(join(home, ".local/share/orca-ide/app/resources/bin/orca-ide"));
|
|
622
|
+
if (process.platform === "darwin") { cands.push(join(home, "Library/Application Support/orca-ide/app/resources/bin/orca-ide"), "/Applications/Orca.app/Contents/Resources/app/resources/bin/orca-ide", join(home, "Applications/Orca.app/Contents/Resources/app/resources/bin/orca-ide")); }
|
|
623
|
+
if (process.platform === "win32") { const la = process.env.LOCALAPPDATA || ""; cands.push(join(la, "orca-ide", "app", "resources", "bin", "orca-ide"), join(la, "Programs", "orca-ide", "resources", "app", "resources", "bin", "orca-ide")); }
|
|
624
|
+
for (const c of cands) { try { if (existsSync(c)) return c; } catch {} }
|
|
625
|
+
const roots = process.platform === "darwin" ? [join(home, "Library/Application Support"), "/Applications", join(home, "Applications")]
|
|
626
|
+
: process.platform === "win32" ? [process.env.LOCALAPPDATA || "", process.env.PROGRAMFILES || ""]
|
|
627
|
+
: [join(home, ".local/share"), "/opt", join(home, ".config")];
|
|
628
|
+
for (const r of roots) { if (!r) continue; const hit = sh(`find ${JSON.stringify(r)} -maxdepth 6 -name orca-ide -type f 2>/dev/null | head -1`).trim(); if (hit) return hit; }
|
|
629
|
+
return "";
|
|
630
|
+
}
|
|
603
631
|
function detectHandoffs() {
|
|
604
|
-
const editors =
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const
|
|
610
|
-
|
|
632
|
+
const editors = [];
|
|
633
|
+
for (const [cmd, label, app] of IDE_LIST) {
|
|
634
|
+
if (hasCmd(cmd)) editors.push({ label, tmpl: `${cmd} {dir}`, kind: "editor" });
|
|
635
|
+
else { const a = macApp(app); if (a) editors.push({ label, tmpl: `open -a ${JSON.stringify(a)} {dir}`, kind: "editor" }); }
|
|
636
|
+
}
|
|
637
|
+
const agents = AGENT_LIST.filter(([cmd]) => hasCmd(cmd)).map(([cmd, label, tmpl]) => ({ label, tmpl, kind: "agent" }));
|
|
638
|
+
// Orca IDE (any OS): register the repo + open a terminal tab. Two variants —
|
|
639
|
+
// one that just opens the repo (use Orca's own agent, e.g. GPT), and one that
|
|
640
|
+
// runs Claude in the tab for Claude users. Not Claude-only.
|
|
641
|
+
const orca = findOrcaCli();
|
|
642
|
+
if (orca) {
|
|
643
|
+
const q = JSON.stringify(orca);
|
|
644
|
+
agents.unshift(
|
|
645
|
+
{ label: "Orca IDE — open repo (use your Orca agent)", tmpl: `${q} repo add --path {dir}; ${q} terminal create --worktree path:{dir} --focus`, kind: "agent" },
|
|
646
|
+
{ label: "Orca IDE — run Claude in a tab", tmpl: `${q} repo add --path {dir}; ${q} terminal create --worktree path:{dir} --command "claude {prompt}" --focus`, kind: "agent" },
|
|
647
|
+
);
|
|
648
|
+
}
|
|
611
649
|
return { agents, editors };
|
|
612
650
|
}
|
|
613
651
|
const shSingle = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
@@ -1049,10 +1087,16 @@ async function repoSuggest(path) {
|
|
|
1049
1087
|
|
|
1050
1088
|
// Build a write-up for a command; returns { text, sub, error? } without printing.
|
|
1051
1089
|
// Shared by the CLI (cmdRun) and the web UI (symbiot app).
|
|
1090
|
+
// The SAME repo set the Map uses — reuse its cached scan when present, else do
|
|
1091
|
+
// the identical discovery. So week/standup/todo/drift all agree with the Map.
|
|
1092
|
+
function discoveredRepos() {
|
|
1093
|
+
if (LAST_MAP && LAST_MAP.nodes) return LAST_MAP.nodes.filter((n) => n.type === "repo" && n.meta && n.meta.path).map((n) => ({ path: n.meta.path, name: n.label, recency: 0 }));
|
|
1094
|
+
return findAllRepos();
|
|
1095
|
+
}
|
|
1052
1096
|
async function produce(cmd) {
|
|
1053
1097
|
if (!resolveProvider()) return { error: "not-connected" };
|
|
1054
1098
|
if (cmd === "todo") {
|
|
1055
|
-
const repos =
|
|
1099
|
+
const repos = discoveredRepos();
|
|
1056
1100
|
const open = openWork(repos);
|
|
1057
1101
|
if (!open.length) return { text: "Nothing outstanding found (no TODOs or uncommitted work).", sub: "0 open items · todo" };
|
|
1058
1102
|
const system =
|
|
@@ -1065,8 +1109,10 @@ async function produce(cmd) {
|
|
|
1065
1109
|
const label = cmd === "standup" ? "standup" : "week";
|
|
1066
1110
|
const days = cmd === "standup" ? 2 : SINCE_WEEK;
|
|
1067
1111
|
const who = me();
|
|
1068
|
-
|
|
1069
|
-
|
|
1112
|
+
// Discover the Map's repo set, then keep only those with commits in the window.
|
|
1113
|
+
const all = discoveredRepos();
|
|
1114
|
+
const repos = all.filter((r) => sh(`git -C ${JSON.stringify(r.path)} log --since="${days} days ago" --oneline -1 2>/dev/null`).trim());
|
|
1115
|
+
if (!repos.length) return { text: `No commits in the last ${days} days across your ${all.length} repos.\nAdd folders to scan in Settings, or check your git identity.`, sub: "no activity" };
|
|
1070
1116
|
let cs = commits(repos, `${days} days ago`, !has("all"));
|
|
1071
1117
|
if (!cs.length) cs = commits(repos, `${days} days ago`, false); // fall back to all if none matched you
|
|
1072
1118
|
const open = label === "week" ? openWork(repos) : [];
|
|
@@ -1085,7 +1131,7 @@ async function produce(cmd) {
|
|
|
1085
1131
|
`Write the ${label === "standup" ? "standup" : "update"}.`;
|
|
1086
1132
|
|
|
1087
1133
|
const text = await write(system, prompt);
|
|
1088
|
-
return { text: text || "(couldn't reach the model)", sub: `${cs.length} commits across ${new Set(cs.map((x) => x.repo)).size} repos · ${label}`, footer: `symbiot ${VERSION} · ${
|
|
1134
|
+
return { text: text || "(couldn't reach the model)", sub: `${cs.length} commits across ${new Set(cs.map((x) => x.repo)).size} repos · ${label}`, footer: `symbiot ${VERSION} · ${all.length} repos (same as the Map) · ${repos.length} active · ${cs.length} commits in last ${days}d` };
|
|
1089
1135
|
}
|
|
1090
1136
|
|
|
1091
1137
|
async function cmdRun(cmd) {
|
|
@@ -1224,6 +1270,10 @@ select,input{width:100%;background:var(--ink);border:1px solid var(--line);borde
|
|
|
1224
1270
|
select:focus,input:focus{outline:none;border-color:var(--green)}
|
|
1225
1271
|
a{color:var(--green);cursor:pointer}.hidden{display:none}
|
|
1226
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}
|
|
1227
1277
|
footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
1228
1278
|
.profile{font-size:13px;margin-bottom:8px;line-height:1.5}
|
|
1229
1279
|
.profile b{color:var(--bone)}
|
|
@@ -1288,6 +1338,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1288
1338
|
.lg i{width:10px;height:10px;border-radius:50%;display:inline-block}
|
|
1289
1339
|
@media(max-width:760px){.maprow{flex-direction:column}.detail{width:auto;max-height:none}}
|
|
1290
1340
|
</style></head><body>
|
|
1341
|
+
<div id="updatebar" class="updatebar"></div>
|
|
1291
1342
|
<header><span class="dot"></span><span class="brand">Symbiot</span><span class="status" id="status">...</span></header>
|
|
1292
1343
|
<div class="tabs">
|
|
1293
1344
|
<button class="tab active" data-tab="map">Map</button>
|
|
@@ -1587,6 +1638,24 @@ document.getElementById('addtask').addEventListener('click',addTaskUI);
|
|
|
1587
1638
|
document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
|
|
1588
1639
|
document.getElementById('agentcmd').addEventListener('change',saveAgent);
|
|
1589
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 — you're on "+esc(p.version)+". <button id='doupd'>Update & 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();
|
|
1590
1659
|
initGraphEvents();syncP();refresh();loadMap();loadAgentCfg();loadScanRoots();
|
|
1591
1660
|
</script></body></html>`;
|
|
1592
1661
|
|
|
@@ -1629,7 +1698,13 @@ function openApp(url) {
|
|
|
1629
1698
|
}
|
|
1630
1699
|
async function cmdApp() {
|
|
1631
1700
|
SERVING = true;
|
|
1632
|
-
const
|
|
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;
|
|
1633
1708
|
const json = (res, obj) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
|
|
1634
1709
|
const server = createServer(async (req, res) => {
|
|
1635
1710
|
const u = new URL(req.url, "http://127.0.0.1");
|
|
@@ -1670,17 +1745,40 @@ async function cmdApp() {
|
|
|
1670
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 }; }));
|
|
1671
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)); }
|
|
1672
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
|
+
}
|
|
1673
1761
|
if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
|
|
1674
1762
|
} catch (e) { res.writeHead(500, { "content-type": "application/json" }); res.end(JSON.stringify({ error: String((e && e.message) || e) })); return; }
|
|
1675
1763
|
res.writeHead(404); res.end("not found");
|
|
1676
1764
|
});
|
|
1677
|
-
|
|
1765
|
+
let announced = false, tries = 0, opened = false;
|
|
1766
|
+
server.on("listening", () => {
|
|
1767
|
+
if (announced) return; announced = true;
|
|
1678
1768
|
const url = `http://127.0.0.1:${server.address().port}/?t=${TOKEN}`;
|
|
1679
|
-
const how = openApp(url);
|
|
1769
|
+
const how = opened ? "" : openApp(url); opened = true; // only pop a window the first time
|
|
1680
1770
|
console.log(`\n${c.g("●")} ${c.b("Symbiot")} is running at ${c.b(url)}`);
|
|
1681
1771
|
console.log(how ? c.d(` Opened in a ${how}.`) : c.d(" Open that URL in your browser."));
|
|
1682
1772
|
console.log(c.d(" Leave this running; press Ctrl+C to stop (or click Quit in the window)."));
|
|
1683
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
|
|
1684
1782
|
}
|
|
1685
1783
|
|
|
1686
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.
|
|
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": {
|