symbiot 0.11.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 +280 -27
- 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,45 @@ 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
|
+
}
|
|
132
|
+
// Deterministic task classification (no model) so tasks batch by kind instead
|
|
133
|
+
// of arriving as a flat, mixed pile. Order matters (most specific first).
|
|
134
|
+
const TASK_ORDER = ["Fixes", "Tests & CI", "Security", "Performance", "Refactor", "UI/UX", "Docs", "Features & other"];
|
|
135
|
+
function taskType(text) {
|
|
136
|
+
const s = String(text || "").toLowerCase();
|
|
137
|
+
if (/\b(test|tests|spec|coverage|smoke|fixture|ci\b)/.test(s)) return "Tests & CI";
|
|
138
|
+
if (/\b(secur|auth|token|secret|vulnerab|permission|sanitiz|escap|injection)/.test(s)) return "Security";
|
|
139
|
+
if (/\b(readme|docs?|document|changelog|comment|guide)/.test(s)) return "Docs";
|
|
140
|
+
if (/\b(perf|performance|slow|optimi|latency|throughput|speed up|memory leak)/.test(s)) return "Performance";
|
|
141
|
+
if (/\b(refactor|simplify|split|extract|clean ?up|consolidat|dedupe|modular|rename)/.test(s)) return "Refactor";
|
|
142
|
+
if (/\b(bug|fix|broken|error|crash|regress|hang|race|leak)/.test(s)) return "Fixes";
|
|
143
|
+
if (/\b(ui|ux|design|layout|style|button|screen|view\b|page\b|responsive)/.test(s)) return "UI/UX";
|
|
144
|
+
return "Features & other";
|
|
145
|
+
}
|
|
106
146
|
// Push tasks into the repos as an agent-readable brief (.symbiot/TASKS.md):
|
|
107
147
|
// a checklist plus the deterministic context an agent needs to get oriented.
|
|
108
148
|
function buildTasksMd(name, ctx, list) {
|
|
@@ -115,12 +155,16 @@ function buildTasksMd(name, ctx, list) {
|
|
|
115
155
|
if (ctx.open && ctx.open.length) { L.push("- **Open markers (TODO/FIXME + uncommitted):**"); for (const o of ctx.open) L.push(` - ${o}`); }
|
|
116
156
|
if (ctx.drift && ctx.drift.length) { L.push("- **Current drift / risk:**"); for (const d of ctx.drift) L.push(` - ${d}`); }
|
|
117
157
|
L.push("", "## Tasks");
|
|
118
|
-
for (const t of list)
|
|
119
|
-
|
|
158
|
+
const byType = {}; for (const t of list) { const ty = taskType(t.text); (byType[ty] = byType[ty] || []).push(t); }
|
|
159
|
+
const keys = Object.keys(byType).sort((a, b) => TASK_ORDER.indexOf(a) - TASK_ORDER.indexOf(b));
|
|
160
|
+
for (const ty of keys) { L.push(`### ${ty}`); for (const t of byType[ty]) L.push(`- [ ] ${t.text}`); L.push(""); }
|
|
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."');
|
|
120
162
|
return L.join("\n") + "\n";
|
|
121
163
|
}
|
|
122
|
-
function pushTasks() {
|
|
123
|
-
|
|
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);
|
|
124
168
|
if (!tasks.length) return { empty: true, written: [], unresolved: [] };
|
|
125
169
|
// Resolve repo name -> path from the already-scanned map when we have it
|
|
126
170
|
// (avoids a fresh full scan); fall back to a scan only if needed.
|
|
@@ -147,17 +191,22 @@ function pushTasks() {
|
|
|
147
191
|
const dir = join(path, ".symbiot"); mkdirSync(dir, { recursive: true });
|
|
148
192
|
const file = join(dir, "TASKS.md");
|
|
149
193
|
writeFileSync(file, buildTasksMd(name, { branch: st.branch, commits, open, drift: risk, stack }, list));
|
|
150
|
-
written.push({ name, file, count: list.length });
|
|
194
|
+
written.push({ name, file, path, count: list.length });
|
|
151
195
|
} catch (e) { unresolved.push({ name, count: list.length, error: String((e && e.message) || e) }); }
|
|
152
196
|
}
|
|
153
|
-
return { empty: false, written, unresolved };
|
|
197
|
+
return { empty: false, written, unresolved, handoff: handoffCmd() };
|
|
154
198
|
}
|
|
155
199
|
function cmdPush() {
|
|
156
200
|
const r = pushTasks();
|
|
157
201
|
if (r.empty) { console.log(c.y("No open tasks to push.") + c.d(" Add some in `symbiot app` — a repo review's ideas, or the Tasks tab.")); return; }
|
|
158
202
|
if (r.written.length) { console.log("\n" + c.g("●") + " " + c.b("Pushed tasks into repos:")); for (const w of r.written) console.log(` ${c.g("✓")} ${w.name} ${c.d(w.file + " (" + w.count + " task" + (w.count > 1 ? "s" : "") + ")")}`); }
|
|
159
203
|
if (r.unresolved.length) { console.log("\n" + c.y(`Not written (repo not found under ${BASE}):`)); for (const u of r.unresolved) console.log(` · ${u.name} (${u.count})`); }
|
|
160
|
-
|
|
204
|
+
if (has("open")) {
|
|
205
|
+
if (!r.handoff) console.log("\n" + c.y("No agent command set.") + c.d(" Set one in `symbiot app` Settings, or `agentCmd` in ~/.config/symbiot/config.json (use {dir} and {prompt})."));
|
|
206
|
+
else { for (const w of r.written) runHandoff(w.path); console.log("\n" + c.g("→ ") + `Handed ${r.written.length} repo(s) to your agent (${r.handoff}).`); }
|
|
207
|
+
} else {
|
|
208
|
+
console.log("\n" + c.d("Point your agent at .symbiot/TASKS.md in each repo. (add --open to run your configured agent command)"));
|
|
209
|
+
}
|
|
161
210
|
}
|
|
162
211
|
function envKey(provider) {
|
|
163
212
|
for (const e of (PROVIDERS[provider].env || [])) if (process.env[e]) return process.env[e];
|
|
@@ -432,7 +481,10 @@ async function callOllama(r, system, prompt) {
|
|
|
432
481
|
body: JSON.stringify({ model: r.model, stream: false, messages: [{ role: "system", content: system }, { role: "user", content: prompt }] }),
|
|
433
482
|
});
|
|
434
483
|
const text = await res.text();
|
|
435
|
-
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
|
+
}
|
|
436
488
|
return (JSON.parse(text).message?.content || "").trim();
|
|
437
489
|
}
|
|
438
490
|
|
|
@@ -515,6 +567,86 @@ function recommendModels(hw) {
|
|
|
515
567
|
const best = (local.slice().reverse().find((m) => m.fits) || local[0]).model;
|
|
516
568
|
return { local, paid, best };
|
|
517
569
|
}
|
|
570
|
+
// ---- hand a repo (+ its tasks) to the user's agent — generic, settings-based
|
|
571
|
+
// The handoff is a command TEMPLATE the user configures, with {dir} (repo path)
|
|
572
|
+
// and {prompt} (the task instruction). Works for any agent/editor:
|
|
573
|
+
// claude -p "{prompt}" · aider --message "{prompt}" · code {dir}
|
|
574
|
+
// gnome-terminal --working-directory={dir} -- claude "{prompt}"
|
|
575
|
+
// Not tied to any one tool — you decide what runs.
|
|
576
|
+
const IDE_LIST = [["code", "VS Code"], ["cursor", "Cursor"], ["windsurf", "Windsurf"], ["zed", "Zed"], ["subl", "Sublime Text"], ["idea", "IntelliJ IDEA"], ["nvim", "Neovim"]];
|
|
577
|
+
const AGENT_LIST = [["claude", "Claude Code", 'claude -p "{prompt}"'], ["aider", "Aider", 'aider --message "{prompt}"'], ["cursor-agent", "Cursor agent", 'cursor-agent -p "{prompt}"']];
|
|
578
|
+
function detectHandoffs() {
|
|
579
|
+
const editors = IDE_LIST.filter(([cmd]) => sh(`command -v ${cmd} 2>/dev/null`).trim()).map(([cmd, label]) => ({ label, tmpl: `${cmd} {dir}`, kind: "editor" }));
|
|
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" });
|
|
586
|
+
return { agents, editors };
|
|
587
|
+
}
|
|
588
|
+
const shSingle = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
589
|
+
const escDq = (s) => String(s).replace(/[\\"$`]/g, "\\$&");
|
|
590
|
+
const HANDOFF_PROMPT = "Read .symbiot/TASKS.md and implement the unchecked items in this repo. Confirm before anything destructive.";
|
|
591
|
+
function handoffCmd() { const cfg = loadConfig(); return cfg.agentCmd || (cfg.ide ? `${cfg.ide} {dir}` : ""); } // ide = legacy
|
|
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) {
|
|
595
|
+
try {
|
|
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"));
|
|
649
|
+
}
|
|
518
650
|
function cmdModels() {
|
|
519
651
|
const hw = detectHardware(); const rec = recommendModels(hw);
|
|
520
652
|
const pad = (s, n) => String(s).padEnd(n);
|
|
@@ -725,10 +857,24 @@ function buildMap() {
|
|
|
725
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 }); }
|
|
726
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 }); }
|
|
727
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 {}
|
|
728
873
|
const out = { nodes, edges, stats: {
|
|
729
874
|
repos: repos.length,
|
|
730
875
|
languages: nodes.filter((n) => n.type === "lang").length,
|
|
731
876
|
tools: nodes.filter((n) => n.type === "tool").length,
|
|
877
|
+
agents: nodes.filter((n) => n.type === "agent").length,
|
|
732
878
|
commits: repos.reduce((s, r) => s + r.mine, 0),
|
|
733
879
|
files: repos.reduce((s, r) => s + (r.files || 0), 0),
|
|
734
880
|
base: BASE,
|
|
@@ -755,6 +901,10 @@ function nodeDetail(id) {
|
|
|
755
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);
|
|
756
902
|
return { type: id.startsWith("lang:") ? "lang" : "tool", label: n ? n.label : id.split(":")[1], repos };
|
|
757
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
|
+
}
|
|
758
908
|
return { error: "unknown node" };
|
|
759
909
|
}
|
|
760
910
|
// AI review of one repo: what it does, what it's for, possible upgrades.
|
|
@@ -1042,6 +1192,13 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1042
1192
|
.task .rp{font-size:11px;color:var(--faint);background:var(--ink3);border:1px solid var(--line);border-radius:999px;padding:2px 8px}
|
|
1043
1193
|
.task .rm{background:none;border:0;color:var(--faint);cursor:pointer;font-size:18px;line-height:1}
|
|
1044
1194
|
.task .rm:hover{color:var(--amber)}
|
|
1195
|
+
.tgroup{margin:14px 0 4px;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--green);font-weight:700}
|
|
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}
|
|
1045
1202
|
.drift{border:1px solid var(--line);border-radius:10px;margin-top:10px;padding:12px 14px;background:var(--ink2)}
|
|
1046
1203
|
.drift .dh{display:flex;gap:8px;align-items:center}
|
|
1047
1204
|
.drift .dn{color:var(--bone);font-weight:600}
|
|
@@ -1052,6 +1209,15 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1052
1209
|
.drift .ev{color:var(--faint);font-family:ui-monospace,Menlo,monospace;font-size:11px}
|
|
1053
1210
|
.dot-w{width:9px;height:9px;border-radius:50%;background:var(--amber);display:inline-block}
|
|
1054
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%}}
|
|
1055
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)}
|
|
1056
1222
|
.legend{display:flex;gap:14px;align-items:center;margin-top:10px;font-size:12px;color:var(--faint);flex-wrap:wrap}
|
|
1057
1223
|
.lg{display:inline-flex;gap:6px;align-items:center}
|
|
@@ -1066,6 +1232,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1066
1232
|
<button class="tab" data-tab="standup">Standup</button>
|
|
1067
1233
|
<button class="tab" data-tab="todo">Todo</button>
|
|
1068
1234
|
<button class="tab" data-tab="tasks">Tasks</button>
|
|
1235
|
+
<button class="tab" data-tab="agents">Agents</button>
|
|
1069
1236
|
<button class="tab" data-tab="settings">Settings</button>
|
|
1070
1237
|
</div>
|
|
1071
1238
|
<main>
|
|
@@ -1081,6 +1248,8 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1081
1248
|
<span class="lg"><i style="background:var(--bone)"></i>repos</span>
|
|
1082
1249
|
<span class="lg"><i style="background:var(--amber)"></i>languages</span>
|
|
1083
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>
|
|
1084
1253
|
<span class="muted" style="margin-left:8px">scroll to zoom · drag to pan · click a node</span>
|
|
1085
1254
|
<button class="ghost" id="remap" style="margin-left:auto">Rescan</button>
|
|
1086
1255
|
</div>
|
|
@@ -1095,9 +1264,15 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1095
1264
|
</section>
|
|
1096
1265
|
<section id="panel-tasks" class="hidden">
|
|
1097
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>
|
|
1098
1268
|
<div id="pushout"></div>
|
|
1269
|
+
<div id="taskfilter" class="taskfilter"></div>
|
|
1099
1270
|
<div id="tasklist"></div>
|
|
1100
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>
|
|
1101
1276
|
<section id="panel-drift" class="hidden">
|
|
1102
1277
|
<div class="row"><span class="muted">What's out of sync, stuck or at risk across your repos — local git facts.</span>
|
|
1103
1278
|
<label class="muted" style="margin-left:auto"><input type="checkbox" id="driftfetch"> fetch latest</label>
|
|
@@ -1122,7 +1297,15 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1122
1297
|
<div class="row" style="margin-top:16px"><button class="act" id="save">Save & connect</button>
|
|
1123
1298
|
<span class="note" id="saveMsg"></span></div>
|
|
1124
1299
|
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1300
|
+
<label>Hand off to your agent when you "Send to repos"</label>
|
|
1301
|
+
<input id="agentcmd" type="text" placeholder="e.g. claude -p "{prompt}" · code {dir} · leave blank to just write the file">
|
|
1302
|
+
<div class="note muted">Runs in each repo after tasks are written. Use <b>{dir}</b> = repo path, <b>{prompt}</b> = the task instruction. Works with any agent or editor — it's your command.</div>
|
|
1303
|
+
<div id="agentpresets" style="margin-top:8px"></div>
|
|
1304
|
+
</div>
|
|
1305
|
+
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1125
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>
|
|
1126
1309
|
<div id="recout" style="margin-top:12px"></div>
|
|
1127
1310
|
</div>
|
|
1128
1311
|
</section>
|
|
@@ -1135,19 +1318,21 @@ var KEYURL={anthropic:'https://console.anthropic.com/settings/keys',openai:'http
|
|
|
1135
1318
|
var DEFMODEL={anthropic:'claude-opus-5-5',openai:'gpt-4o-mini',gemini:'gemini-1.5-flash',ollama:'llama3.1'};
|
|
1136
1319
|
function $(id){return document.getElementById(id);}
|
|
1137
1320
|
var current='map';var mapLoaded=false;var driftLoaded=false;
|
|
1138
|
-
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'};
|
|
1139
1322
|
function tabs(){return document.querySelectorAll('.tab');}
|
|
1140
1323
|
function setTab(tab){current=tab;tabs().forEach(function(t){t.classList.toggle('active',t.dataset.tab===tab);});
|
|
1141
|
-
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');
|
|
1142
1325
|
$('panel-map').classList.toggle('hidden',!isMap);
|
|
1143
1326
|
$('panel-run').classList.toggle('hidden',!isRun);
|
|
1144
1327
|
$('panel-settings').classList.toggle('hidden',!isSet);
|
|
1145
1328
|
$('panel-tasks').classList.toggle('hidden',!isTasks);
|
|
1146
1329
|
$('panel-drift').classList.toggle('hidden',!isDrift);
|
|
1330
|
+
$('panel-agents').classList.toggle('hidden',!isAgents);
|
|
1147
1331
|
if(isRun){$('what').textContent=tab;$('out').textContent='Nothing yet - hit the button.';$('out').classList.add('muted');$('copy').classList.add('hidden');}
|
|
1148
1332
|
if(isMap&&!mapLoaded)loadMap();
|
|
1149
1333
|
if(isTasks)loadTasks();
|
|
1150
|
-
if(isDrift&&!driftLoaded)loadDrift();
|
|
1334
|
+
if(isDrift&&!driftLoaded)loadDrift();
|
|
1335
|
+
if(isAgents)loadAgents(); else stopAgentsPoll();}
|
|
1151
1336
|
tabs().forEach(function(t){t.addEventListener('click',function(){setTab(t.dataset.tab);});});
|
|
1152
1337
|
function refresh(){api('/api/status').then(function(s){$('status').textContent=s.connected?s.line:'Not connected - open Settings';});}
|
|
1153
1338
|
$('write').addEventListener('click',function(){$('out').textContent='Writing...';$('out').classList.add('muted');$('copy').classList.add('hidden');
|
|
@@ -1187,7 +1372,9 @@ if(d.type==='repo'){var chips="";['branch: '+(d.branch||'?'),d.commits+' commits
|
|
|
1187
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>";
|
|
1188
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;}
|
|
1189
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;}
|
|
1190
|
-
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;}}
|
|
1191
1378
|
var reviewCache={};var IDEAS=[];var IREPO="";
|
|
1192
1379
|
function hideReview(){var el=document.getElementById('review');el.classList.add('hidden');el.innerHTML='';}
|
|
1193
1380
|
function reviewHtml(name,body,verdict,ideas,tasks,footer){IDEAS=ideas||[];IREPO=name;
|
|
@@ -1210,13 +1397,48 @@ el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h
|
|
|
1210
1397
|
Promise.all([api('/api/review',{path:path}),api('/api/tasks')]).then(function(res){var r=res[0]||{},tasks=res[1]||[];
|
|
1211
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;}
|
|
1212
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();});}
|
|
1213
|
-
|
|
1214
|
-
var
|
|
1215
|
-
|
|
1216
|
-
el.querySelectorAll('.task').forEach(function(row){var id=row.getAttribute('data-id');
|
|
1217
|
-
row.querySelector('.taskchk').addEventListener('change',function(){api('/api/tasks/toggle',{id:id}).then(function(){row.classList.toggle('done');});});
|
|
1218
|
-
row.querySelector('.rm').addEventListener('click',function(){api('/api/tasks/
|
|
1219
|
-
|
|
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>";}
|
|
1403
|
+
function wireTaskRows(el){el.querySelectorAll('.task').forEach(function(row){var id=row.getAttribute('data-id');
|
|
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;}
|
|
1420
|
+
var groups={};list.forEach(function(t){var ty=t.type||'Features & other';(groups[ty]=groups[ty]||[]).push(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);});});
|
|
1423
|
+
el.innerHTML=h;wireTaskRows(el);}
|
|
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);});}
|
|
1220
1442
|
function loadDrift(){var out=document.getElementById('driftout');out.innerHTML="<div class='muted' style='margin-top:12px'>Reading your repos…</div>";
|
|
1221
1443
|
var ci=document.getElementById('driftci').checked?'1':'0';var ft=document.getElementById('driftfetch').checked?'1':'0';
|
|
1222
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';});});
|
|
@@ -1232,13 +1454,25 @@ out.innerHTML=h;});}
|
|
|
1232
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();});}
|
|
1233
1455
|
function pushTasksUI(){var o=document.getElementById('pushout');var btn=document.getElementById('pushtasks');btn.disabled=true;
|
|
1234
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>";
|
|
1235
|
-
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;
|
|
1236
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;}
|
|
1237
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>";
|
|
1238
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>";}
|
|
1239
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>";}
|
|
1240
|
-
h+="<div class='dd' style='margin-top:8px'
|
|
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>";}
|
|
1464
|
+
h+="</div>";
|
|
1241
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>";});}
|
|
1466
|
+
function loadAgentCfg(){api('/api/agentcfg').then(function(d){document.getElementById('agentcmd').value=d.cmd||'';
|
|
1467
|
+
var chips=[];(d.agents||[]).forEach(function(a){chips.push(a);});(d.editors||[]).forEach(function(e){chips.push(e);});
|
|
1468
|
+
var box=document.getElementById('agentpresets');
|
|
1469
|
+
if(!chips.length){box.innerHTML="<span class='muted' style='font-size:12px'>Nothing detected on PATH — type your own command above.</span>";return;}
|
|
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("");
|
|
1471
|
+
box.querySelectorAll('.preset').forEach(function(btn){btn.addEventListener('click',function(){document.getElementById('agentcmd').value=chips[+btn.getAttribute('data-i')].tmpl;saveAgent();});});});}
|
|
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>";});}
|
|
1242
1476
|
function selectNode(id){sel=id;render();updateBar(id);var n=nodeById(id);api('/api/node?id='+encodeURIComponent(id)).then(showDetail);
|
|
1243
1477
|
if(n&&n.type==='repo'&&n.meta&&n.meta.path){loadReview(n.label,n.meta.path);}else{hideReview();}}
|
|
1244
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};}
|
|
@@ -1267,11 +1501,14 @@ rec.paid.forEach(function(p){h+="<div class='idea'><span><b>"+esc(p.provider)+"<
|
|
|
1267
1501
|
h+="</div>";out.innerHTML=h;});}
|
|
1268
1502
|
document.getElementById('remap').addEventListener('click',loadMap);
|
|
1269
1503
|
document.getElementById('recbtn').addEventListener('click',loadRec);
|
|
1504
|
+
document.getElementById('setuplocal').addEventListener('click',setupLocalUI);
|
|
1270
1505
|
document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
|
|
1506
|
+
document.getElementById('agentsrefresh').addEventListener('click',loadAgents);
|
|
1271
1507
|
document.getElementById('addtask').addEventListener('click',addTaskUI);
|
|
1272
1508
|
document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
|
|
1509
|
+
document.getElementById('agentcmd').addEventListener('change',saveAgent);
|
|
1273
1510
|
document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
|
|
1274
|
-
initGraphEvents();syncP();refresh();loadMap();
|
|
1511
|
+
initGraphEvents();syncP();refresh();loadMap();loadAgentCfg();
|
|
1275
1512
|
</script></body></html>`;
|
|
1276
1513
|
|
|
1277
1514
|
function readBody(req) {
|
|
@@ -1330,11 +1567,25 @@ async function cmdApp() {
|
|
|
1330
1567
|
if (u.pathname === "/api/node") return json(res, nodeDetail(u.searchParams.get("id") || "")); // local
|
|
1331
1568
|
if (u.pathname === "/api/suggest" && req.method === "POST") { const b = await readBody(req); return json(res, await repoSuggest(String(b.path || ""))); }
|
|
1332
1569
|
if (u.pathname === "/api/review" && req.method === "POST") { const b = await readBody(req); return json(res, await repoReview(String(b.path || ""))); }
|
|
1333
|
-
if (u.pathname === "/api/tasks" && req.method !== "POST") return json(res, loadTasks());
|
|
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) }))); }
|
|
1334
1571
|
if (u.pathname === "/api/tasks/add" && req.method === "POST") { const b = await readBody(req); return json(res, addTask(b.text, b.repo)); }
|
|
1335
1572
|
if (u.pathname === "/api/tasks/toggle" && req.method === "POST") { const b = await readBody(req); return json(res, toggleTask(String(b.id || ""))); }
|
|
1336
1573
|
if (u.pathname === "/api/tasks/remove" && req.method === "POST") { const b = await readBody(req); return json(res, removeTask(String(b.id || ""))); }
|
|
1337
|
-
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)); }
|
|
1577
|
+
if (u.pathname === "/api/agentcfg") { const d = detectHandoffs(); return json(res, { cmd: handoffCmd(), agents: d.agents, editors: d.editors }); }
|
|
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 || "" }); }
|
|
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 }; }));
|
|
1338
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)); }
|
|
1339
1590
|
if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
|
|
1340
1591
|
if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
|
|
@@ -1358,8 +1609,9 @@ ${c.b("Usage")}
|
|
|
1358
1609
|
symbiot todo what's still on your plate
|
|
1359
1610
|
symbiot app open the visual app in your browser
|
|
1360
1611
|
symbiot drift what's out of sync / at risk across repos
|
|
1361
|
-
symbiot push
|
|
1612
|
+
symbiot push [--open] write tasks into each repo (and open your IDE)
|
|
1362
1613
|
symbiot models recommend AI models for your hardware
|
|
1614
|
+
symbiot setup-local [--model X] install/run a free local model (Ollama)
|
|
1363
1615
|
symbiot login connect it to an AI (once)
|
|
1364
1616
|
symbiot whoami show how it's connected
|
|
1365
1617
|
symbiot logout forget saved credentials
|
|
@@ -1389,6 +1641,7 @@ async function main() {
|
|
|
1389
1641
|
if (cmd === "whoami" || cmd === "status") return cmdWhoami();
|
|
1390
1642
|
if (cmd === "app" || cmd === "ui") return cmdApp();
|
|
1391
1643
|
if (cmd === "models" || cmd === "hardware") return cmdModels();
|
|
1644
|
+
if (cmd === "setup-local" || cmd === "setup-ollama") return cmdSetupLocal();
|
|
1392
1645
|
if (cmd === "drift") return cmdDrift();
|
|
1393
1646
|
if (cmd === "push") return cmdPush();
|
|
1394
1647
|
if (cmd === "week") return cmdRun("week");
|
|
@@ -1406,4 +1659,4 @@ const isMain = (() => {
|
|
|
1406
1659
|
})();
|
|
1407
1660
|
if (isMain) main();
|
|
1408
1661
|
|
|
1409
|
-
export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, buildTasksMd, EMBEDDED_UI };
|
|
1662
|
+
export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, buildTasksMd, taskType, EMBEDDED_UI };
|
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": {
|