symbiot 0.13.0 → 0.17.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 +210 -28
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import Anthropic from "@anthropic-ai/sdk";
|
|
|
21
21
|
import { execSync, spawn } from "node:child_process";
|
|
22
22
|
import { homedir, totalmem, cpus as oscpus } from "node:os";
|
|
23
23
|
import { join } from "node:path";
|
|
24
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, statSync, realpathSync } from "node:fs";
|
|
24
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, statSync, realpathSync, openSync, writeSync } from "node:fs";
|
|
25
25
|
import { createInterface } from "node:readline";
|
|
26
26
|
import { createServer } from "node:http";
|
|
27
27
|
import { randomBytes } from "node:crypto";
|
|
@@ -56,6 +56,7 @@ const has = (name) => argv.includes(`--${name}`);
|
|
|
56
56
|
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
|
+
const HANDOFFS = []; // live registry of agents Symbiot has handed work to
|
|
59
60
|
|
|
60
61
|
// ---- colour + spinner ------------------------------------------------------
|
|
61
62
|
const c = PLAIN
|
|
@@ -103,6 +104,31 @@ function addTask(text, repo) {
|
|
|
103
104
|
}
|
|
104
105
|
function toggleTask(id) { const t = loadTasks(); const it = t.find((x) => x.id === id); if (it) { it.done = !it.done; saveTasks(t); } return it || { error: "not found" }; }
|
|
105
106
|
function removeTask(id) { saveTasks(loadTasks().filter((x) => x.id !== id)); return { ok: true }; }
|
|
107
|
+
function restoreTask(id) { const t = loadTasks(); const it = t.find((x) => x.id === id); if (it) { it.archived = false; it.done = false; delete it.archivedAt; saveTasks(t); } return it || { error: "not found" }; }
|
|
108
|
+
// Which task texts the agent checked off in a repo's .symbiot/TASKS.md
|
|
109
|
+
function completedInRepo(repoPath) {
|
|
110
|
+
try { return readFileSync(join(repoPath, ".symbiot", "TASKS.md"), "utf8").split("\n").filter((l) => /^\s*-\s*\[x\]/i.test(l)).map((l) => l.replace(/^\s*-\s*\[x\]\s*/i, "").trim().toLowerCase()); }
|
|
111
|
+
catch { return []; }
|
|
112
|
+
}
|
|
113
|
+
function repoPathMap() {
|
|
114
|
+
const byName = {};
|
|
115
|
+
const src = (LAST_MAP && LAST_MAP.nodes) ? LAST_MAP.nodes.filter((n) => n.type === "repo").map((n) => ({ name: n.label, path: n.meta && n.meta.path })) : findAllRepos(BASE);
|
|
116
|
+
for (const r of src) if (r.path && !byName[r.name]) byName[r.name] = r.path;
|
|
117
|
+
return byName;
|
|
118
|
+
}
|
|
119
|
+
// "Check what was handed out, see what's completed, then archive it": mark done
|
|
120
|
+
// any task the agent ticked in TASKS.md, then auto-archive every done task.
|
|
121
|
+
function syncTasks() {
|
|
122
|
+
const t = loadTasks(); const map = repoPathMap(); let completed = 0, archived = 0; const checkedByRepo = {};
|
|
123
|
+
for (const x of t) {
|
|
124
|
+
if (x.archived || x.done || !x.repo) continue;
|
|
125
|
+
if (!(x.repo in checkedByRepo)) checkedByRepo[x.repo] = map[x.repo] ? completedInRepo(map[x.repo]) : [];
|
|
126
|
+
if (checkedByRepo[x.repo].includes(x.text.toLowerCase())) { x.done = true; completed++; }
|
|
127
|
+
}
|
|
128
|
+
for (const x of t) { if (x.done && !x.archived) { x.archived = true; x.archivedAt = Date.now(); archived++; } }
|
|
129
|
+
saveTasks(t);
|
|
130
|
+
return { completed, archived };
|
|
131
|
+
}
|
|
106
132
|
// Deterministic task classification (no model) so tasks batch by kind instead
|
|
107
133
|
// of arriving as a flat, mixed pile. Order matters (most specific first).
|
|
108
134
|
const TASK_ORDER = ["Fixes", "Tests & CI", "Security", "Performance", "Refactor", "UI/UX", "Docs", "Features & other"];
|
|
@@ -135,8 +161,10 @@ function buildTasksMd(name, ctx, list) {
|
|
|
135
161
|
L.push("---", 'To action these, tell your coding agent: "Read `.symbiot/TASKS.md` and implement the unchecked items in this repo, using the context above. Confirm with me before anything destructive."');
|
|
136
162
|
return L.join("\n") + "\n";
|
|
137
163
|
}
|
|
138
|
-
function pushTasks() {
|
|
139
|
-
|
|
164
|
+
function pushTasks(filter) {
|
|
165
|
+
let tasks = loadTasks().filter((t) => !t.done && !t.archived);
|
|
166
|
+
if (filter && filter.type) tasks = tasks.filter((t) => taskType(t.text) === filter.type);
|
|
167
|
+
if (filter && filter.repo) tasks = tasks.filter((t) => t.repo === filter.repo);
|
|
140
168
|
if (!tasks.length) return { empty: true, written: [], unresolved: [] };
|
|
141
169
|
// Resolve repo name -> path from the already-scanned map when we have it
|
|
142
170
|
// (avoids a fresh full scan); fall back to a scan only if needed.
|
|
@@ -453,7 +481,10 @@ async function callOllama(r, system, prompt) {
|
|
|
453
481
|
body: JSON.stringify({ model: r.model, stream: false, messages: [{ role: "system", content: system }, { role: "user", content: prompt }] }),
|
|
454
482
|
});
|
|
455
483
|
const text = await res.text();
|
|
456
|
-
if (!res.ok)
|
|
484
|
+
if (!res.ok) {
|
|
485
|
+
if (/not found|no such model|try pulling/i.test(text)) throw new Error(`the model "${r.model}" isn't downloaded yet — run symbiot setup-local --model ${r.model} (or click "Set up a free local model" in Settings)`);
|
|
486
|
+
throw new Error(`Ollama ${res.status}: ${text.slice(0, 200)}`);
|
|
487
|
+
}
|
|
457
488
|
return (JSON.parse(text).message?.content || "").trim();
|
|
458
489
|
}
|
|
459
490
|
|
|
@@ -547,19 +578,74 @@ const AGENT_LIST = [["claude", "Claude Code", 'claude -p "{prompt}"'], ["aider",
|
|
|
547
578
|
function detectHandoffs() {
|
|
548
579
|
const editors = IDE_LIST.filter(([cmd]) => sh(`command -v ${cmd} 2>/dev/null`).trim()).map(([cmd, label]) => ({ label, tmpl: `${cmd} {dir}`, kind: "editor" }));
|
|
549
580
|
const agents = AGENT_LIST.filter(([cmd]) => sh(`command -v ${cmd} 2>/dev/null`).trim()).map(([cmd, label, tmpl]) => ({ label, tmpl, kind: "agent" }));
|
|
581
|
+
// Orca IDE: its own CLI (not the ~/.local/bin GPU wrapper, which ignores args).
|
|
582
|
+
// Registers the repo, then opens a terminal tab running the agent — Orca shows
|
|
583
|
+
// the live session itself.
|
|
584
|
+
const orca = join(homedir(), ".local/share/orca-ide/app/resources/bin/orca-ide");
|
|
585
|
+
if (existsSync(orca)) agents.unshift({ label: "Orca IDE (terminal tab)", tmpl: `${orca} repo add --path {dir}; ${orca} terminal create --worktree path:{dir} --command "claude {prompt}" --focus`, kind: "agent" });
|
|
550
586
|
return { agents, editors };
|
|
551
587
|
}
|
|
552
588
|
const shSingle = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
553
589
|
const escDq = (s) => String(s).replace(/[\\"$`]/g, "\\$&");
|
|
554
590
|
const HANDOFF_PROMPT = "Read .symbiot/TASKS.md and implement the unchecked items in this repo. Confirm before anything destructive.";
|
|
555
591
|
function handoffCmd() { const cfg = loadConfig(); return cfg.agentCmd || (cfg.ide ? `${cfg.ide} {dir}` : ""); } // ide = legacy
|
|
556
|
-
|
|
592
|
+
// Run a shell command as a tracked, logged background job that shows up live in
|
|
593
|
+
// the Agents tab. Shared by the agent handoff and the local-model setup.
|
|
594
|
+
function track(name, cmd, cwd, onExit) {
|
|
557
595
|
try {
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
596
|
+
const dir = join(cwd, ".symbiot"); mkdirSync(dir, { recursive: true });
|
|
597
|
+
const logp = join(dir, "agent.log");
|
|
598
|
+
let fd = "ignore"; try { fd = openSync(logp, "a"); writeSync(fd, `\n=== ${name} ${new Date().toISOString()} ===\n$ ${cmd}\n`); } catch {}
|
|
599
|
+
const entry = { id: randomBytes(4).toString("hex"), name, path: cwd, log: logp, startedAt: Date.now(), status: "running", exitCode: null, endedAt: null };
|
|
600
|
+
const child = spawn(cmd, { shell: true, cwd, detached: true, stdio: ["ignore", fd === "ignore" ? "ignore" : fd, fd === "ignore" ? "ignore" : fd] });
|
|
601
|
+
entry.pid = child.pid;
|
|
602
|
+
child.on("exit", (code) => { entry.status = code === 0 ? "done" : "failed"; entry.exitCode = code; entry.endedAt = Date.now(); if (onExit) try { onExit(code); } catch {} });
|
|
603
|
+
child.on("error", () => { entry.status = "failed"; entry.endedAt = Date.now(); });
|
|
604
|
+
child.unref();
|
|
605
|
+
HANDOFFS.unshift(entry);
|
|
606
|
+
if (HANDOFFS.length > 30) HANDOFFS.length = 30;
|
|
607
|
+
return entry;
|
|
608
|
+
} catch { return null; }
|
|
609
|
+
}
|
|
610
|
+
function runHandoff(repoPath) {
|
|
611
|
+
const tmpl = handoffCmd(); if (!tmpl || !repoPath) return null;
|
|
612
|
+
const cmd = tmpl.replace(/\{dir\}/g, shSingle(repoPath)).replace(/\{prompt\}/g, escDq(HANDOFF_PROMPT));
|
|
613
|
+
return track(repoPath.split("/").pop(), cmd, repoPath);
|
|
614
|
+
}
|
|
615
|
+
// ---- local model one-command setup (Ollama), OS-aware ---------------------
|
|
616
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
617
|
+
function hasOllama() { return !!sh("ollama --version").trim(); } // cross-platform (not POSIX command -v)
|
|
618
|
+
function ollamaInstall() {
|
|
619
|
+
if (process.platform === "darwin") return { cmd: "brew install ollama", alt: "https://ollama.com/download" };
|
|
620
|
+
if (process.platform === "win32") return { cmd: "winget install Ollama.Ollama", alt: "https://ollama.com/download" };
|
|
621
|
+
return { cmd: "curl -fsSL https://ollama.com/install.sh | sh", alt: "https://ollama.com/download" };
|
|
622
|
+
}
|
|
623
|
+
async function ollamaUp() { try { const r = await fetch("http://127.0.0.1:11434/api/tags"); return r.ok; } catch { return false; } }
|
|
624
|
+
async function ensureOllama() {
|
|
625
|
+
if (await ollamaUp()) return true;
|
|
626
|
+
try { spawn("ollama", ["serve"], { detached: true, stdio: "ignore" }).unref(); } catch {}
|
|
627
|
+
for (let i = 0; i < 16; i++) { await sleep(500); if (await ollamaUp()) return true; }
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
function useOllamaModel(model) { const cfg = loadConfig(); cfg.provider = "ollama"; cfg.ollama = { baseUrl: "http://localhost:11434", model }; delete cfg.apiKey; saveConfig(cfg); }
|
|
631
|
+
async function cmdSetupLocal() {
|
|
632
|
+
const model = flag("model", null) || recommendModels(detectHardware()).best;
|
|
633
|
+
if (!hasOllama()) {
|
|
634
|
+
const inst = ollamaInstall();
|
|
635
|
+
console.log("\n" + c.y("Ollama isn't installed") + c.d(" — the free, private local-model runner.") + "\n");
|
|
636
|
+
console.log(` On ${process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux"}, install it then re-run ${c.b("symbiot setup-local")}:`);
|
|
637
|
+
console.log(" " + c.b(inst.cmd));
|
|
638
|
+
console.log(c.d(" or download: " + inst.alt));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
process.stdout.write(c.d("Starting Ollama… "));
|
|
642
|
+
if (!(await ensureOllama())) { console.log(c.y("couldn't reach it — start it with ") + c.b("ollama serve")); return; }
|
|
643
|
+
console.log(c.g("ready"));
|
|
644
|
+
console.log("\n" + c.b("Pulling " + model) + c.d(" (first time downloads a few GB — progress below)\n"));
|
|
645
|
+
const code = await new Promise((res) => { const ch = spawn("ollama", ["pull", model], { stdio: "inherit" }); ch.on("close", res); ch.on("error", () => res(1)); });
|
|
646
|
+
if (code !== 0) { console.log("\n" + c.y("Pull failed.") + c.d(" Try a smaller model: ") + c.b("symbiot setup-local --model llama3.2:3b")); return; }
|
|
647
|
+
useOllamaModel(model);
|
|
648
|
+
console.log("\n" + c.g("✓ ") + `Symbiot now runs on ${model} locally — free & private. Try: ` + c.b("symbiot week"));
|
|
563
649
|
}
|
|
564
650
|
function cmdModels() {
|
|
565
651
|
const hw = detectHardware(); const rec = recommendModels(hw);
|
|
@@ -771,10 +857,24 @@ function buildMap() {
|
|
|
771
857
|
for (const L of r.langs.slice(0, 3)) { const id = "lang:" + L; add({ id, type: "lang", label: L, weight: 15 }); edges.push({ source: rid, target: id }); }
|
|
772
858
|
for (const T of r.tools) { const id = "tool:" + T; add({ id, type: "tool", label: T, weight: 12 }); edges.push({ source: rid, target: id }); }
|
|
773
859
|
}
|
|
860
|
+
// What you build WITH (person-level, not per-repo): detected agents/editors
|
|
861
|
+
// and the AI currently powering Symbiot — so the initial scan shows the whole
|
|
862
|
+
// setup, not just code. All local detection, nothing invasive.
|
|
863
|
+
try {
|
|
864
|
+
const hands = detectHandoffs();
|
|
865
|
+
for (const a of [...hands.agents, ...hands.editors]) {
|
|
866
|
+
const id = "agent:" + a.label;
|
|
867
|
+
add({ id, type: "agent", label: a.label.replace(/\s*\(.*\)$/, ""), weight: 11, meta: { cmd: a.tmpl, kind: a.kind } });
|
|
868
|
+
edges.push({ source: "me", target: id });
|
|
869
|
+
}
|
|
870
|
+
const prov = resolveProvider();
|
|
871
|
+
if (prov) { const id = "ai:" + prov.provider; add({ id, type: "ai", label: PROVIDERS[prov.provider].label, weight: 14, meta: { model: prov.model, source: prov.source } }); edges.push({ source: "me", target: id }); }
|
|
872
|
+
} catch {}
|
|
774
873
|
const out = { nodes, edges, stats: {
|
|
775
874
|
repos: repos.length,
|
|
776
875
|
languages: nodes.filter((n) => n.type === "lang").length,
|
|
777
876
|
tools: nodes.filter((n) => n.type === "tool").length,
|
|
877
|
+
agents: nodes.filter((n) => n.type === "agent").length,
|
|
778
878
|
commits: repos.reduce((s, r) => s + r.mine, 0),
|
|
779
879
|
files: repos.reduce((s, r) => s + (r.files || 0), 0),
|
|
780
880
|
base: BASE,
|
|
@@ -801,6 +901,10 @@ function nodeDetail(id) {
|
|
|
801
901
|
const repos = map.edges.filter((e) => e.target === id).map((e) => { const r = map.nodes.find((x) => x.id === e.source); return r ? r.label : null; }).filter(Boolean);
|
|
802
902
|
return { type: id.startsWith("lang:") ? "lang" : "tool", label: n ? n.label : id.split(":")[1], repos };
|
|
803
903
|
}
|
|
904
|
+
if (id.startsWith("agent:") || id.startsWith("ai:")) {
|
|
905
|
+
const n = map.nodes.find((x) => x.id === id);
|
|
906
|
+
return { type: id.startsWith("agent:") ? "agent" : "ai", label: n ? n.label : id.split(":")[1], meta: (n && n.meta) || {} };
|
|
907
|
+
}
|
|
804
908
|
return { error: "unknown node" };
|
|
805
909
|
}
|
|
806
910
|
// AI review of one repo: what it does, what it's for, possible upgrades.
|
|
@@ -1090,6 +1194,11 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1090
1194
|
.task .rm:hover{color:var(--amber)}
|
|
1091
1195
|
.tgroup{margin:14px 0 4px;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--green);font-weight:700}
|
|
1092
1196
|
.tcount{color:var(--faint);font-weight:400}
|
|
1197
|
+
.taskfilter{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:10px 0 2px}
|
|
1198
|
+
.fl{font-size:10px;color:var(--faint);text-transform:uppercase;letter-spacing:.12em;margin:0 2px 0 8px}
|
|
1199
|
+
.fchip{font-size:12px;padding:4px 10px;border-radius:999px;border:1px solid var(--line);background:var(--ink3);color:var(--text);cursor:pointer;font:inherit}
|
|
1200
|
+
.fchip.on{background:var(--green-dim);border-color:#2a6b52;color:var(--green)}
|
|
1201
|
+
.task .rm.restore{color:var(--green);font-size:15px}
|
|
1093
1202
|
.drift{border:1px solid var(--line);border-radius:10px;margin-top:10px;padding:12px 14px;background:var(--ink2)}
|
|
1094
1203
|
.drift .dh{display:flex;gap:8px;align-items:center}
|
|
1095
1204
|
.drift .dn{color:var(--bone);font-weight:600}
|
|
@@ -1100,6 +1209,15 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1100
1209
|
.drift .ev{color:var(--faint);font-family:ui-monospace,Menlo,monospace;font-size:11px}
|
|
1101
1210
|
.dot-w{width:9px;height:9px;border-radius:50%;background:var(--amber);display:inline-block}
|
|
1102
1211
|
.dot-c{width:9px;height:9px;border-radius:50%;background:var(--green);display:inline-block}
|
|
1212
|
+
.agent{border:1px solid var(--line);border-radius:10px;margin-top:10px;padding:12px 14px;background:var(--ink2)}
|
|
1213
|
+
.adot{width:10px;height:10px;border-radius:50%;display:inline-block;flex:none}
|
|
1214
|
+
.adot.run{background:var(--amber);animation:pulse 1s ease-in-out infinite}
|
|
1215
|
+
.adot.ok{background:var(--green)} .adot.fail{background:#E5695B}
|
|
1216
|
+
@keyframes pulse{0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(242,165,65,.5)}50%{opacity:.4;box-shadow:0 0 0 5px rgba(242,165,65,0)}}
|
|
1217
|
+
.alogout{white-space:pre-wrap;background:var(--ink);border:1px solid var(--line);border-radius:8px;padding:10px;margin-top:10px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;color:var(--text);max-height:260px;overflow:auto}
|
|
1218
|
+
.bar{height:3px;background:var(--green-dim);border-radius:2px;overflow:hidden;margin-top:8px}
|
|
1219
|
+
.bar > i{display:block;height:100%;width:35%;background:var(--green);border-radius:2px;animation:slide 1.3s ease-in-out infinite}
|
|
1220
|
+
@keyframes slide{0%{margin-left:-35%}100%{margin-left:100%}}
|
|
1103
1221
|
.out2{white-space:pre-wrap;background:var(--ink);border:1px solid var(--line);border-radius:9px;padding:10px;margin-top:10px;font-size:13px;line-height:1.55;color:var(--bone)}
|
|
1104
1222
|
.legend{display:flex;gap:14px;align-items:center;margin-top:10px;font-size:12px;color:var(--faint);flex-wrap:wrap}
|
|
1105
1223
|
.lg{display:inline-flex;gap:6px;align-items:center}
|
|
@@ -1114,6 +1232,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1114
1232
|
<button class="tab" data-tab="standup">Standup</button>
|
|
1115
1233
|
<button class="tab" data-tab="todo">Todo</button>
|
|
1116
1234
|
<button class="tab" data-tab="tasks">Tasks</button>
|
|
1235
|
+
<button class="tab" data-tab="agents">Agents</button>
|
|
1117
1236
|
<button class="tab" data-tab="settings">Settings</button>
|
|
1118
1237
|
</div>
|
|
1119
1238
|
<main>
|
|
@@ -1129,6 +1248,8 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1129
1248
|
<span class="lg"><i style="background:var(--bone)"></i>repos</span>
|
|
1130
1249
|
<span class="lg"><i style="background:var(--amber)"></i>languages</span>
|
|
1131
1250
|
<span class="lg"><i style="background:#6bb3ff"></i>tools</span>
|
|
1251
|
+
<span class="lg"><i style="background:#c58af9"></i>agents</span>
|
|
1252
|
+
<span class="lg"><i style="background:#5fe3b0"></i>AI</span>
|
|
1132
1253
|
<span class="muted" style="margin-left:8px">scroll to zoom · drag to pan · click a node</span>
|
|
1133
1254
|
<button class="ghost" id="remap" style="margin-left:auto">Rescan</button>
|
|
1134
1255
|
</div>
|
|
@@ -1143,9 +1264,15 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1143
1264
|
</section>
|
|
1144
1265
|
<section id="panel-tasks" class="hidden">
|
|
1145
1266
|
<div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><button class="act" id="addtask">Add</button><button class="ghost" id="pushtasks" title="Write .symbiot/TASKS.md into each repo for your coding agent">Send to repos</button></div>
|
|
1267
|
+
<div class="note muted" style="margin-top:2px">The checkbox marks a task <b>done</b> (it auto-archives). To give tasks to your agent, use <b>Send to repos</b> — filter by tag below to choose which.</div>
|
|
1146
1268
|
<div id="pushout"></div>
|
|
1269
|
+
<div id="taskfilter" class="taskfilter"></div>
|
|
1147
1270
|
<div id="tasklist"></div>
|
|
1148
1271
|
</section>
|
|
1272
|
+
<section id="panel-agents" class="hidden">
|
|
1273
|
+
<div class="row"><span class="muted">Agents Symbiot has handed work to — live status and output.</span><button class="ghost" id="agentsrefresh" style="margin-left:auto">Refresh</button></div>
|
|
1274
|
+
<div id="agentslist"></div>
|
|
1275
|
+
</section>
|
|
1149
1276
|
<section id="panel-drift" class="hidden">
|
|
1150
1277
|
<div class="row"><span class="muted">What's out of sync, stuck or at risk across your repos — local git facts.</span>
|
|
1151
1278
|
<label class="muted" style="margin-left:auto"><input type="checkbox" id="driftfetch"> fetch latest</label>
|
|
@@ -1177,6 +1304,8 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1177
1304
|
</div>
|
|
1178
1305
|
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1179
1306
|
<button class="ghost" id="recbtn">Recommend models for my machine</button>
|
|
1307
|
+
<button class="ghost" id="setuplocal" style="margin-left:8px">Set up a free local model</button>
|
|
1308
|
+
<div id="setupout" class="note muted" style="margin-top:10px"></div>
|
|
1180
1309
|
<div id="recout" style="margin-top:12px"></div>
|
|
1181
1310
|
</div>
|
|
1182
1311
|
</section>
|
|
@@ -1189,19 +1318,21 @@ var KEYURL={anthropic:'https://console.anthropic.com/settings/keys',openai:'http
|
|
|
1189
1318
|
var DEFMODEL={anthropic:'claude-opus-5-5',openai:'gpt-4o-mini',gemini:'gemini-1.5-flash',ollama:'llama3.1'};
|
|
1190
1319
|
function $(id){return document.getElementById(id);}
|
|
1191
1320
|
var current='map';var mapLoaded=false;var driftLoaded=false;
|
|
1192
|
-
var COLORS={person:'#3DDC97',repo:'#F4F1EA',lang:'#F2A541',tool:'#6bb3ff'};
|
|
1321
|
+
var COLORS={person:'#3DDC97',repo:'#F4F1EA',lang:'#F2A541',tool:'#6bb3ff',agent:'#c58af9',ai:'#5fe3b0'};
|
|
1193
1322
|
function tabs(){return document.querySelectorAll('.tab');}
|
|
1194
1323
|
function setTab(tab){current=tab;tabs().forEach(function(t){t.classList.toggle('active',t.dataset.tab===tab);});
|
|
1195
|
-
var isMap=tab==='map',isSet=tab==='settings',isTasks=tab==='tasks',isDrift=tab==='drift',isRun=(tab==='week'||tab==='standup'||tab==='todo');
|
|
1324
|
+
var isMap=tab==='map',isSet=tab==='settings',isTasks=tab==='tasks',isDrift=tab==='drift',isAgents=tab==='agents',isRun=(tab==='week'||tab==='standup'||tab==='todo');
|
|
1196
1325
|
$('panel-map').classList.toggle('hidden',!isMap);
|
|
1197
1326
|
$('panel-run').classList.toggle('hidden',!isRun);
|
|
1198
1327
|
$('panel-settings').classList.toggle('hidden',!isSet);
|
|
1199
1328
|
$('panel-tasks').classList.toggle('hidden',!isTasks);
|
|
1200
1329
|
$('panel-drift').classList.toggle('hidden',!isDrift);
|
|
1330
|
+
$('panel-agents').classList.toggle('hidden',!isAgents);
|
|
1201
1331
|
if(isRun){$('what').textContent=tab;$('out').textContent='Nothing yet - hit the button.';$('out').classList.add('muted');$('copy').classList.add('hidden');}
|
|
1202
1332
|
if(isMap&&!mapLoaded)loadMap();
|
|
1203
1333
|
if(isTasks)loadTasks();
|
|
1204
|
-
if(isDrift&&!driftLoaded)loadDrift();
|
|
1334
|
+
if(isDrift&&!driftLoaded)loadDrift();
|
|
1335
|
+
if(isAgents)loadAgents(); else stopAgentsPoll();}
|
|
1205
1336
|
tabs().forEach(function(t){t.addEventListener('click',function(){setTab(t.dataset.tab);});});
|
|
1206
1337
|
function refresh(){api('/api/status').then(function(s){$('status').textContent=s.connected?s.line:'Not connected - open Settings';});}
|
|
1207
1338
|
$('write').addEventListener('click',function(){$('out').textContent='Writing...';$('out').classList.add('muted');$('copy').classList.add('hidden');
|
|
@@ -1241,7 +1372,9 @@ if(d.type==='repo'){var chips="";['branch: '+(d.branch||'?'),d.commits+' commits
|
|
|
1241
1372
|
el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='chips'>"+chips+"</div><button class='act' id='suggest' style='margin-top:12px'>Suggest next steps</button><div id='sugout'></div>";
|
|
1242
1373
|
document.getElementById('suggest').addEventListener('click',function(){var o=document.getElementById('sugout');o.innerHTML="<div class='out2'>Thinking...</div>";api('/api/suggest',{path:d.path}).then(function(r){if(r.error==='not-connected'){o.innerHTML="<div class='out2'>Connect a model in Settings to get suggestions - Ollama is free and runs locally.</div>";return;}o.innerHTML="<div class='out2'>"+esc(r.text||'(no output)')+"</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");});});return;}
|
|
1243
1374
|
if(d.type==='lang'||d.type==='tool'){var lis=(d.repos||[]).map(function(r){return "<li>"+esc(r)+"</li>";}).join("");el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>Used in "+((d.repos||[]).length)+" repos</div><ul>"+lis+"</ul>";return;}
|
|
1244
|
-
if(d.type==='
|
|
1375
|
+
if(d.type==='agent'){var m=d.meta||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+(m.kind==='agent'?'AI coding agent you have installed':'editor you have installed')+"</div>"+(m.cmd?"<div class='out2' style='font-size:11px'>"+esc(m.cmd)+"</div>":"");return;}
|
|
1376
|
+
if(d.type==='ai'){var m=d.meta||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>Currently powering Symbiot · model <b>"+esc(m.model||'?')+"</b>"+(m.source?" · via "+esc(m.source):"")+"</div>";return;}
|
|
1377
|
+
if(d.type==='person'){var st=d.stats||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+st.repos+" repos · "+st.commits+" commits · "+st.languages+" languages · "+st.tools+" tools"+(st.agents?" · "+st.agents+" agents/editors":"")+"</div>";return;}}
|
|
1245
1378
|
var reviewCache={};var IDEAS=[];var IREPO="";
|
|
1246
1379
|
function hideReview(){var el=document.getElementById('review');el.classList.add('hidden');el.innerHTML='';}
|
|
1247
1380
|
function reviewHtml(name,body,verdict,ideas,tasks,footer){IDEAS=ideas||[];IREPO=name;
|
|
@@ -1264,17 +1397,48 @@ el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h
|
|
|
1264
1397
|
Promise.all([api('/api/review',{path:path}),api('/api/tasks')]).then(function(res){var r=res[0]||{},tasks=res[1]||[];
|
|
1265
1398
|
if(r.error==='not-connected'){el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>Connect a model in Settings to get a review - Ollama is free and runs locally.</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");return;}
|
|
1266
1399
|
var body=r.text||'(no output)';reviewCache[path]={text:body,verdict:r.verdict||"",ideas:r.ideas||[],footer:r.footer};el.innerHTML=reviewHtml(name,body,r.verdict||"",r.ideas||[],tasks,r.footer);wireIdeas();});}
|
|
1267
|
-
|
|
1400
|
+
var ALLTASKS=[];var TFILTER={type:'',repo:''};var TARCH=false;
|
|
1401
|
+
var TORDER=['Fixes','Tests & CI','Security','Performance','Refactor','UI/UX','Docs','Features & other'];
|
|
1402
|
+
function taskRow(t,arch){return "<div class='task"+(t.done?" done":"")+"' data-id='"+esc(t.id)+"'>"+(arch?"":"<input type='checkbox' class='taskchk' title='mark done (auto-archives on sync)'"+(t.done?" checked":"")+">")+"<span class='t'>"+esc(t.text)+"</span>"+(t.repo?"<span class='rp'>"+esc(t.repo)+"</span>":"")+(arch?"<button class='rm restore' title='restore to active'>↶</button>":"<button class='rm' title='remove'>×</button>")+"</div>";}
|
|
1268
1403
|
function wireTaskRows(el){el.querySelectorAll('.task').forEach(function(row){var id=row.getAttribute('data-id');
|
|
1269
|
-
row.querySelector('.taskchk').addEventListener('change',function(){api('/api/tasks/toggle',{id:id}).then(function(){row.classList.toggle('done');});});
|
|
1270
|
-
row.querySelector('.rm').addEventListener('click',function(){api('/api/tasks/
|
|
1271
|
-
|
|
1272
|
-
var
|
|
1404
|
+
var cb=row.querySelector('.taskchk');if(cb)cb.addEventListener('change',function(){api('/api/tasks/toggle',{id:id}).then(function(){row.classList.toggle('done');});});
|
|
1405
|
+
var rm=row.querySelector('.rm');if(rm&&rm.classList.contains('restore'))rm.addEventListener('click',function(){api('/api/tasks/restore',{id:id}).then(loadArchived);});
|
|
1406
|
+
else if(rm)rm.addEventListener('click',function(){api('/api/tasks/remove',{id:id}).then(function(){row.remove();});});});}
|
|
1407
|
+
function fchip(dim,val,label){var on=(TFILTER[dim]||'')===val;return "<button class='fchip"+(on?' on':'')+"' data-dim='"+dim+"' data-val='"+esc(val)+"'>"+esc(label)+"</button>";}
|
|
1408
|
+
function renderFilter(){var box=document.getElementById('taskfilter');
|
|
1409
|
+
if(TARCH){box.innerHTML="<button class='fchip on' id='archtoggle'>↩ back to active</button>";document.getElementById('archtoggle').addEventListener('click',function(){loadTasks();});return;}
|
|
1410
|
+
var types={},repos={};ALLTASKS.forEach(function(t){types[t.type||'Features & other']=1;if(t.repo)repos[t.repo]=1;});
|
|
1411
|
+
var fb="<span class='fl'>Type</span>"+fchip('type','','All')+Object.keys(types).sort(function(a,b){return TORDER.indexOf(a)-TORDER.indexOf(b);}).map(function(t){return fchip('type',t,t);}).join('');
|
|
1412
|
+
fb+="<span class='fl'>Repo</span>"+fchip('repo','','All')+Object.keys(repos).sort().map(function(r){return fchip('repo',r,r);}).join('');
|
|
1413
|
+
fb+="<button class='fchip' id='archtoggle' style='margin-left:auto'>🗃 archived</button>";
|
|
1414
|
+
box.innerHTML=fb;
|
|
1415
|
+
box.querySelectorAll('.fchip[data-dim]').forEach(function(c){c.addEventListener('click',function(){TFILTER[c.getAttribute('data-dim')]=c.getAttribute('data-val');renderTasks();});});
|
|
1416
|
+
document.getElementById('archtoggle').addEventListener('click',loadArchived);}
|
|
1417
|
+
function renderTasks(){var el=document.getElementById('tasklist');TARCH=false;renderFilter();
|
|
1418
|
+
var list=ALLTASKS.filter(function(t){return (!TFILTER.type||(t.type||'Features & other')===TFILTER.type)&&(!TFILTER.repo||t.repo===TFILTER.repo);});
|
|
1419
|
+
if(!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>"+(ALLTASKS.length?"Nothing matches this filter.":"No tasks yet. Tick ideas in a repo review, or add one above.")+"</div>";return;}
|
|
1273
1420
|
var groups={};list.forEach(function(t){var ty=t.type||'Features & other';(groups[ty]=groups[ty]||[]).push(t);});
|
|
1274
|
-
var keys=Object.keys(groups).sort(function(a,b){var ia=
|
|
1275
|
-
var h="";keys.forEach(function(ty){h+="<div class='tgroup'>"+esc(ty)+" <span class='tcount'>"+groups[ty].length+"</span></div>";groups[ty].forEach(function(t){h+=taskRow(t);});});
|
|
1421
|
+
var keys=Object.keys(groups).sort(function(a,b){var ia=TORDER.indexOf(a),ib=TORDER.indexOf(b);return (ia<0?99:ia)-(ib<0?99:ib);});
|
|
1422
|
+
var h="";keys.forEach(function(ty){h+="<div class='tgroup'>"+esc(ty)+" <span class='tcount'>"+groups[ty].length+"</span></div>";groups[ty].forEach(function(t){h+=taskRow(t,false);});});
|
|
1276
1423
|
el.innerHTML=h;wireTaskRows(el);}
|
|
1277
|
-
function loadTasks(){api('/api/tasks').then(renderTasks);}
|
|
1424
|
+
function loadTasks(){TARCH=false;api('/api/tasks/sync',{}).then(function(){api('/api/tasks').then(function(list){ALLTASKS=list;renderTasks();});});}
|
|
1425
|
+
function loadArchived(){TARCH=true;api('/api/tasks?archived=1').then(function(list){ALLTASKS=list;renderFilter();var el=document.getElementById('tasklist');
|
|
1426
|
+
if(!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>Nothing archived yet. Completed tasks land here.</div>";return;}
|
|
1427
|
+
var h="<div class='tgroup'>Archived <span class='tcount'>"+list.length+"</span></div>";list.forEach(function(t){h+=taskRow(t,true);});el.innerHTML=h;wireTaskRows(el);});}
|
|
1428
|
+
var agentsTimer=null;
|
|
1429
|
+
function stopAgentsPoll(){if(agentsTimer){clearTimeout(agentsTimer);agentsTimer=null;}}
|
|
1430
|
+
function fmtE(ms){var s=Math.floor(ms/1000);if(s<60)return s+'s';var m=Math.floor(s/60);return m+'m '+(s%60)+'s';}
|
|
1431
|
+
function loadAgents(){api('/api/agents').then(function(list){var el=document.getElementById('agentslist');
|
|
1432
|
+
if(!list||!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>No agents yet. In <b>Tasks</b>, tick ideas and hit <b>Send to repos</b> (with an agent command set in Settings) — you'll watch it work here.</div>";stopAgentsPoll();return;}
|
|
1433
|
+
el.innerHTML=list.map(function(a){var cls=a.status==='running'?'run':(a.status==='done'?'ok':'fail');
|
|
1434
|
+
var st=a.status==='running'?('working · '+fmtE(a.elapsed)):(esc(a.status)+' · '+fmtE(a.elapsed)+(a.exitCode!=null?' · exit '+a.exitCode:''));
|
|
1435
|
+
var b="<div class='dh'><span class='adot "+cls+"'></span><span class='dn'>"+esc(a.name)+"</span><span class='dd'>"+st+"</span></div>";
|
|
1436
|
+
if(a.status==='running')b+="<div class='bar'><i></i></div>";
|
|
1437
|
+
b+="<pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(waiting for output…)')+"</pre>";
|
|
1438
|
+
return "<div class='agent'>"+b+"</div>";}).join("");
|
|
1439
|
+
el.querySelectorAll('.alogout').forEach(function(p){p.scrollTop=p.scrollHeight;});
|
|
1440
|
+
var anyRunning=list.some(function(a){return a.status==='running';});
|
|
1441
|
+
stopAgentsPoll();if(anyRunning&¤t==='agents')agentsTimer=setTimeout(loadAgents,2000);});}
|
|
1278
1442
|
function loadDrift(){var out=document.getElementById('driftout');out.innerHTML="<div class='muted' style='margin-top:12px'>Reading your repos…</div>";
|
|
1279
1443
|
var ci=document.getElementById('driftci').checked?'1':'0';var ft=document.getElementById('driftfetch').checked?'1':'0';
|
|
1280
1444
|
api('/api/drift?ci='+ci+'&fetch='+ft).then(function(d){driftLoaded=true;var repos=d.repos||[];var risky=repos.filter(function(r){return r.flags.some(function(f){return f.level==='warn';});});
|
|
@@ -1290,13 +1454,13 @@ out.innerHTML=h;});}
|
|
|
1290
1454
|
function addTaskUI(){var i=document.getElementById('newtask');var v=(i.value||'').trim();if(!v)return;api('/api/tasks/add',{text:v,repo:''}).then(function(){i.value='';loadTasks();});}
|
|
1291
1455
|
function pushTasksUI(){var o=document.getElementById('pushout');var btn=document.getElementById('pushtasks');btn.disabled=true;
|
|
1292
1456
|
o.innerHTML="<div class='muted' style='margin-top:10px'><span class='dot-c' style='background:var(--amber)'></span> Writing .symbiot/TASKS.md into your repos…</div>";
|
|
1293
|
-
api('/api/tasks/push',{}).then(function(r){btn.disabled=false;
|
|
1457
|
+
api('/api/tasks/push',{type:TFILTER.type||'',repo:TFILTER.repo||''}).then(function(r){btn.disabled=false;
|
|
1294
1458
|
if(r.empty){o.innerHTML="<div class='muted' style='margin-top:10px'>No open tasks to send. Tick ideas in a repo review, or add tasks above.</div>";return;}
|
|
1295
1459
|
var n=(r.written||[]).length;var h="<div class='drift' style='margin-top:10px'><div class='dh'><span class='dot-c'></span><span class='dn'>Done — wrote "+n+" file"+(n===1?"":"s")+"</span></div>";
|
|
1296
1460
|
if(n){h+="<ul>";r.written.forEach(function(w){h+="<li class='info'>✓ <b>"+esc(w.name)+"</b> <span class='ev'>"+esc(w.file)+" ("+w.count+" task"+(w.count===1?"":"s")+")</span></li>";});h+="</ul>";}
|
|
1297
1461
|
if(r.unresolved&&r.unresolved.length){h+="<div class='dd' style='margin-top:6px'>⚠ not written (repo not found under your home folder): "+esc(r.unresolved.map(function(u){return u.name;}).join(", "))+"</div>";}
|
|
1298
|
-
if(r.handoff&&r.written&&r.written.length){r.written.forEach(function(w){api('/api/open',{path:w.path});});h+="<div class='dd' style='margin-top:8px'>🤖
|
|
1299
|
-
else{h+="<div class='dd' style='margin-top:8px'>Set an <b>agent command</b> in Settings to auto-run it on send. For now, tell your agent: <b>“Read .symbiot/TASKS.md and implement the unchecked items.”</b></div>";}
|
|
1462
|
+
if(r.handoff&&r.written&&r.written.length){r.written.forEach(function(w){api('/api/open',{path:w.path});});h+="<div class='dd' style='margin-top:8px'>🤖 Handed "+n+" repo(s) to your agent — opening the <b>Agents</b> tab to watch it work…</div>";setTimeout(function(){setTab('agents');},500);}
|
|
1463
|
+
else{h+="<div class='dd' style='margin-top:8px'>Set an <b>agent command</b> in Settings to auto-run it on send (and watch it in the Agents tab). For now, tell your agent: <b>“Read .symbiot/TASKS.md and implement the unchecked items.”</b></div>";}
|
|
1300
1464
|
h+="</div>";
|
|
1301
1465
|
o.innerHTML=h;}).catch(function(e){btn.disabled=false;o.innerHTML="<div class='err' style='margin-top:10px'>Couldn't write tasks: "+esc(String((e&&e.message)||e))+"</div>";});}
|
|
1302
1466
|
function loadAgentCfg(){api('/api/agentcfg').then(function(d){document.getElementById('agentcmd').value=d.cmd||'';
|
|
@@ -1306,6 +1470,9 @@ if(!chips.length){box.innerHTML="<span class='muted' style='font-size:12px'>Noth
|
|
|
1306
1470
|
box.innerHTML="<span class='muted' style='font-size:12px'>Detected — click to use:</span><br>"+chips.map(function(c,i){return "<button class='ghost preset' data-i='"+i+"' style='padding:4px 10px;font-size:12px;margin:5px 5px 0 0'>"+esc(c.label)+"</button>";}).join("");
|
|
1307
1471
|
box.querySelectorAll('.preset').forEach(function(btn){btn.addEventListener('click',function(){document.getElementById('agentcmd').value=chips[+btn.getAttribute('data-i')].tmpl;saveAgent();});});});}
|
|
1308
1472
|
function saveAgent(){api('/api/agentcmd',{cmd:document.getElementById('agentcmd').value});}
|
|
1473
|
+
function setupLocalUI(){var o=document.getElementById('setupout');o.innerHTML="Setting up a local model…";
|
|
1474
|
+
api('/api/setup-local',{}).then(function(r){if(r.error==='not-installed'){var i=r.install||{};o.innerHTML="Ollama isn't installed. Run this once, then click again:<div class='out2'>"+esc(i.cmd||'')+"</div><div class='muted' style='font-size:11px'>or download: "+esc(i.alt||'https://ollama.com/download')+"</div>";return;}
|
|
1475
|
+
o.innerHTML="Pulling <b>"+esc(r.model)+"</b> — watch it in the <b>Agents</b> tab. Symbiot switches to it automatically when the download finishes.";setTimeout(function(){setTab('agents');},700);}).catch(function(e){o.innerHTML="<span class='err'>Setup failed: "+esc(String((e&&e.message)||e))+"</span>";});}
|
|
1309
1476
|
function selectNode(id){sel=id;render();updateBar(id);var n=nodeById(id);api('/api/node?id='+encodeURIComponent(id)).then(showDetail);
|
|
1310
1477
|
if(n&&n.type==='repo'&&n.meta&&n.meta.path){loadReview(n.label,n.meta.path);}else{hideReview();}}
|
|
1311
1478
|
function screenToGraph(el,ev){var rc=el.getBoundingClientRect();var mx=(ev.clientX-rc.left)/rc.width*GW;var my=(ev.clientY-rc.top)/rc.height*GH;return {x:(mx-view.x)/view.k,y:(my-view.y)/view.k};}
|
|
@@ -1334,7 +1501,9 @@ rec.paid.forEach(function(p){h+="<div class='idea'><span><b>"+esc(p.provider)+"<
|
|
|
1334
1501
|
h+="</div>";out.innerHTML=h;});}
|
|
1335
1502
|
document.getElementById('remap').addEventListener('click',loadMap);
|
|
1336
1503
|
document.getElementById('recbtn').addEventListener('click',loadRec);
|
|
1504
|
+
document.getElementById('setuplocal').addEventListener('click',setupLocalUI);
|
|
1337
1505
|
document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
|
|
1506
|
+
document.getElementById('agentsrefresh').addEventListener('click',loadAgents);
|
|
1338
1507
|
document.getElementById('addtask').addEventListener('click',addTaskUI);
|
|
1339
1508
|
document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
|
|
1340
1509
|
document.getElementById('agentcmd').addEventListener('change',saveAgent);
|
|
@@ -1398,14 +1567,25 @@ async function cmdApp() {
|
|
|
1398
1567
|
if (u.pathname === "/api/node") return json(res, nodeDetail(u.searchParams.get("id") || "")); // local
|
|
1399
1568
|
if (u.pathname === "/api/suggest" && req.method === "POST") { const b = await readBody(req); return json(res, await repoSuggest(String(b.path || ""))); }
|
|
1400
1569
|
if (u.pathname === "/api/review" && req.method === "POST") { const b = await readBody(req); return json(res, await repoReview(String(b.path || ""))); }
|
|
1401
|
-
if (u.pathname === "/api/tasks" && req.method !== "POST") return json(res, loadTasks().map((t) => ({ ...t, type: taskType(t.text) })));
|
|
1570
|
+
if (u.pathname === "/api/tasks" && req.method !== "POST") { const arch = u.searchParams.get("archived") === "1"; return json(res, loadTasks().filter((x) => !!x.archived === arch).map((t) => ({ ...t, type: taskType(t.text) }))); }
|
|
1402
1571
|
if (u.pathname === "/api/tasks/add" && req.method === "POST") { const b = await readBody(req); return json(res, addTask(b.text, b.repo)); }
|
|
1403
1572
|
if (u.pathname === "/api/tasks/toggle" && req.method === "POST") { const b = await readBody(req); return json(res, toggleTask(String(b.id || ""))); }
|
|
1404
1573
|
if (u.pathname === "/api/tasks/remove" && req.method === "POST") { const b = await readBody(req); return json(res, removeTask(String(b.id || ""))); }
|
|
1405
|
-
if (u.pathname === "/api/tasks/
|
|
1574
|
+
if (u.pathname === "/api/tasks/restore" && req.method === "POST") { const b = await readBody(req); return json(res, restoreTask(String(b.id || ""))); }
|
|
1575
|
+
if (u.pathname === "/api/tasks/sync" && req.method === "POST") return json(res, syncTasks());
|
|
1576
|
+
if (u.pathname === "/api/tasks/push" && req.method === "POST") { const b = await readBody(req); return json(res, pushTasks(b)); }
|
|
1406
1577
|
if (u.pathname === "/api/agentcfg") { const d = detectHandoffs(); return json(res, { cmd: handoffCmd(), agents: d.agents, editors: d.editors }); }
|
|
1407
1578
|
if (u.pathname === "/api/agentcmd" && req.method === "POST") { const b = await readBody(req); const cfg = loadConfig(); const v = String(b.cmd || "").trim(); if (v) cfg.agentCmd = v; else delete cfg.agentCmd; delete cfg.ide; saveConfig(cfg); return json(res, { ok: true, cmd: cfg.agentCmd || "" }); }
|
|
1408
|
-
if (u.pathname === "/api/open" && req.method === "POST") { const b = await readBody(req);
|
|
1579
|
+
if (u.pathname === "/api/open" && req.method === "POST") { const b = await readBody(req); const e = runHandoff(String(b.path || "")); return json(res, { opened: !!e, id: e ? e.id : "" }); }
|
|
1580
|
+
if (u.pathname === "/api/setup-local" && req.method === "POST") {
|
|
1581
|
+
const b = await readBody(req);
|
|
1582
|
+
if (!hasOllama()) return json(res, { error: "not-installed", install: ollamaInstall() });
|
|
1583
|
+
const model = String(b.model || "") || recommendModels(detectHardware()).best;
|
|
1584
|
+
await ensureOllama();
|
|
1585
|
+
const e = track("ollama pull " + model, "ollama pull " + shSingle(model), homedir(), (code) => { if (code === 0) useOllamaModel(model); });
|
|
1586
|
+
return json(res, { started: true, model, id: e ? e.id : "" });
|
|
1587
|
+
}
|
|
1588
|
+
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 }; }));
|
|
1409
1589
|
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)); }
|
|
1410
1590
|
if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
|
|
1411
1591
|
if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
|
|
@@ -1431,6 +1611,7 @@ ${c.b("Usage")}
|
|
|
1431
1611
|
symbiot drift what's out of sync / at risk across repos
|
|
1432
1612
|
symbiot push [--open] write tasks into each repo (and open your IDE)
|
|
1433
1613
|
symbiot models recommend AI models for your hardware
|
|
1614
|
+
symbiot setup-local [--model X] install/run a free local model (Ollama)
|
|
1434
1615
|
symbiot login connect it to an AI (once)
|
|
1435
1616
|
symbiot whoami show how it's connected
|
|
1436
1617
|
symbiot logout forget saved credentials
|
|
@@ -1460,6 +1641,7 @@ async function main() {
|
|
|
1460
1641
|
if (cmd === "whoami" || cmd === "status") return cmdWhoami();
|
|
1461
1642
|
if (cmd === "app" || cmd === "ui") return cmdApp();
|
|
1462
1643
|
if (cmd === "models" || cmd === "hardware") return cmdModels();
|
|
1644
|
+
if (cmd === "setup-local" || cmd === "setup-ollama") return cmdSetupLocal();
|
|
1463
1645
|
if (cmd === "drift") return cmdDrift();
|
|
1464
1646
|
if (cmd === "push") return cmdPush();
|
|
1465
1647
|
if (cmd === "week") return cmdRun("week");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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": {
|