symbiot 0.17.0 → 0.20.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 +98 -16
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -112,7 +112,7 @@ function completedInRepo(repoPath) {
|
|
|
112
112
|
}
|
|
113
113
|
function repoPathMap() {
|
|
114
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(
|
|
115
|
+
const src = (LAST_MAP && LAST_MAP.nodes) ? LAST_MAP.nodes.filter((n) => n.type === "repo" || n.type === "folder").map((n) => ({ name: n.label, path: n.meta && n.meta.path })) : findAllRepos();
|
|
116
116
|
for (const r of src) if (r.path && !byName[r.name]) byName[r.name] = r.path;
|
|
117
117
|
return byName;
|
|
118
118
|
}
|
|
@@ -169,7 +169,7 @@ function pushTasks(filter) {
|
|
|
169
169
|
// Resolve repo name -> path from the already-scanned map when we have it
|
|
170
170
|
// (avoids a fresh full scan); fall back to a scan only if needed.
|
|
171
171
|
const byName = {};
|
|
172
|
-
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(
|
|
172
|
+
const src = (LAST_MAP && LAST_MAP.nodes) ? LAST_MAP.nodes.filter((n) => n.type === "repo" || n.type === "folder").map((n) => ({ name: n.label, path: n.meta && n.meta.path })) : findAllRepos();
|
|
173
173
|
for (const r of src) if (r.path && !byName[r.name]) byName[r.name] = r.path;
|
|
174
174
|
const groups = {}; for (const t of tasks) { const k = t.repo || ""; (groups[k] = groups[k] || []).push(t); }
|
|
175
175
|
const written = [], unresolved = [];
|
|
@@ -260,8 +260,8 @@ function sh(cmd) {
|
|
|
260
260
|
// crawling into them — this is the big speedup for the map scan.
|
|
261
261
|
function findGitDirs(base, limit) {
|
|
262
262
|
return sh(
|
|
263
|
-
`find ${JSON.stringify(base)} -maxdepth
|
|
264
|
-
`\\( -name node_modules -o -name .cache -o -name .local -o -name .npm -o -name venv -o -name .venv -o -name .git-crypt \\) -prune ` +
|
|
263
|
+
`find ${JSON.stringify(base)} -maxdepth 7 ` +
|
|
264
|
+
`\\( -name node_modules -o -name .cache -o -name .local -o -name .npm -o -name venv -o -name .venv -o -name .gradle -o -name Pods -o -name .git-crypt \\) -prune ` +
|
|
265
265
|
`-o -name .git -print 2>/dev/null | head -${limit}`,
|
|
266
266
|
).split("\n").filter(Boolean);
|
|
267
267
|
}
|
|
@@ -372,10 +372,30 @@ function reportFooter(repoPath, auth, state, rd) {
|
|
|
372
372
|
if (state.behind) b.push(`${state.behind} behind upstream`);
|
|
373
373
|
return b.join(" · ");
|
|
374
374
|
}
|
|
375
|
+
function expandRoot(p) { p = String(p || "").trim(); return p.startsWith("~") ? join(homedir(), p.slice(1)) : p; }
|
|
376
|
+
// Where to look for repos: configured folders, or --dir, else your home folder.
|
|
377
|
+
function scanRoots() {
|
|
378
|
+
if (flag("dir", null)) return [BASE];
|
|
379
|
+
const r = loadConfig().scanRoots;
|
|
380
|
+
const roots = (Array.isArray(r) ? r : []).map(expandRoot).filter((x) => { try { return existsSync(x); } catch { return false; } });
|
|
381
|
+
return roots.length ? roots : [homedir()];
|
|
382
|
+
}
|
|
383
|
+
function addScanRoot(p) {
|
|
384
|
+
p = expandRoot(p); if (!p) return { error: "empty" };
|
|
385
|
+
try { if (!existsSync(p)) return { error: "folder not found: " + p }; } catch { return { error: "can't read: " + p }; }
|
|
386
|
+
const cfg = loadConfig();
|
|
387
|
+
let list = Array.isArray(cfg.scanRoots) && cfg.scanRoots.length ? cfg.scanRoots : [homedir()]; // keep home when adding the first extra folder
|
|
388
|
+
if (!list.includes(p)) list.push(p);
|
|
389
|
+
cfg.scanRoots = list; saveConfig(cfg); LAST_MAP = null; return { ok: true, roots: cfg.scanRoots };
|
|
390
|
+
}
|
|
391
|
+
function removeScanRoot(p) {
|
|
392
|
+
const cfg = loadConfig(); cfg.scanRoots = (Array.isArray(cfg.scanRoots) ? cfg.scanRoots : []).filter((x) => x !== p);
|
|
393
|
+
if (!cfg.scanRoots.length) delete cfg.scanRoots; saveConfig(cfg); LAST_MAP = null; return { ok: true, roots: cfg.scanRoots || [] };
|
|
394
|
+
}
|
|
375
395
|
function findRepos(base, sinceDays) {
|
|
376
|
-
const repos = [];
|
|
377
|
-
for (const g of findGitDirs(
|
|
378
|
-
const repo = g.replace(/\/\.git$/, "");
|
|
396
|
+
const repos = []; const seen = new Set();
|
|
397
|
+
for (const root of scanRoots()) for (const g of findGitDirs(root, 200)) {
|
|
398
|
+
const repo = g.replace(/\/\.git$/, ""); if (seen.has(repo)) continue; seen.add(repo);
|
|
379
399
|
const n = Number(sh(`git -C ${JSON.stringify(repo)} log --since="${sinceDays} days ago" --oneline 2>/dev/null | wc -l`).trim());
|
|
380
400
|
const last = Number(sh(`git -C ${JSON.stringify(repo)} log -1 --format=%ct 2>/dev/null`).trim()) || 0;
|
|
381
401
|
if (n > 0) repos.push({ path: repo, name: repo.split("/").pop(), recency: last });
|
|
@@ -574,7 +594,12 @@ function recommendModels(hw) {
|
|
|
574
594
|
// gnome-terminal --working-directory={dir} -- claude "{prompt}"
|
|
575
595
|
// Not tied to any one tool — you decide what runs.
|
|
576
596
|
const IDE_LIST = [["code", "VS Code"], ["cursor", "Cursor"], ["windsurf", "Windsurf"], ["zed", "Zed"], ["subl", "Sublime Text"], ["idea", "IntelliJ IDEA"], ["nvim", "Neovim"]];
|
|
577
|
-
const AGENT_LIST = [
|
|
597
|
+
const AGENT_LIST = [
|
|
598
|
+
["claude", "Claude Code — make changes", 'claude -p "{prompt}" --permission-mode acceptEdits'],
|
|
599
|
+
["claude", "Claude Code — plan only (asks first)", 'claude -p "{prompt}"'],
|
|
600
|
+
["aider", "Aider — make changes", 'aider --message "{prompt}" --yes'],
|
|
601
|
+
["cursor-agent", "Cursor agent", 'cursor-agent -p "{prompt}"'],
|
|
602
|
+
];
|
|
578
603
|
function detectHandoffs() {
|
|
579
604
|
const editors = IDE_LIST.filter(([cmd]) => sh(`command -v ${cmd} 2>/dev/null`).trim()).map(([cmd, label]) => ({ label, tmpl: `${cmd} {dir}`, kind: "editor" }));
|
|
580
605
|
const agents = AGENT_LIST.filter(([cmd]) => sh(`command -v ${cmd} 2>/dev/null`).trim()).map(([cmd, label, tmpl]) => ({ label, tmpl, kind: "agent" }));
|
|
@@ -779,7 +804,7 @@ function driftRepo(p, opts = {}) {
|
|
|
779
804
|
}
|
|
780
805
|
function computeDrift(opts = {}) {
|
|
781
806
|
const deploys = loadDeploys();
|
|
782
|
-
const repos = findAllRepos(
|
|
807
|
+
const repos = findAllRepos().slice(0, 20).map((r) => driftRepo(r.path, { deploys, ci: opts.ci, fetch: opts.fetch }));
|
|
783
808
|
return { repos, ci: !!opts.ci };
|
|
784
809
|
}
|
|
785
810
|
function cmdDrift() {
|
|
@@ -812,9 +837,10 @@ const MANIFEST_TOOL = {
|
|
|
812
837
|
"terraform.tf": "Terraform", "kubernetes.yml": "Kubernetes", ".github": "GitHub Actions",
|
|
813
838
|
};
|
|
814
839
|
function findAllRepos(base) {
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
840
|
+
const roots = base ? [base] : scanRoots();
|
|
841
|
+
const repos = []; const seen = new Set();
|
|
842
|
+
for (const root of roots) for (const g of findGitDirs(root, 300)) {
|
|
843
|
+
const repo = g.replace(/\/\.git$/, ""); if (seen.has(repo)) continue; seen.add(repo);
|
|
818
844
|
const last = Number(sh(`git -C ${JSON.stringify(repo)} log -1 --format=%ct 2>/dev/null`).trim()) || 0;
|
|
819
845
|
if (last) repos.push({ path: repo, name: repo.split("/").pop(), recency: last });
|
|
820
846
|
}
|
|
@@ -825,7 +851,7 @@ function findAllRepos(base) {
|
|
|
825
851
|
const key = cd.startsWith("/") ? cd : join(r.path, cd);
|
|
826
852
|
if (!byCommon[key] || byCommon[key].recency < r.recency) byCommon[key] = r;
|
|
827
853
|
}
|
|
828
|
-
return Object.values(byCommon).sort((a, b) => b.recency - a.recency).slice(0,
|
|
854
|
+
return Object.values(byCommon).sort((a, b) => b.recency - a.recency).slice(0, 60);
|
|
829
855
|
}
|
|
830
856
|
function detectRepo(r) {
|
|
831
857
|
const files = sh(`git -C ${JSON.stringify(r.path)} ls-files 2>/dev/null | head -3000`).split("\n").filter(Boolean);
|
|
@@ -843,9 +869,31 @@ function detectRepo(r) {
|
|
|
843
869
|
const last = sh(`git -C ${JSON.stringify(r.path)} log -1 --format=%cd --date=short 2>/dev/null`).trim();
|
|
844
870
|
return { ...r, langs, tools: [...tools], mine, files: files.length, branch, last };
|
|
845
871
|
}
|
|
872
|
+
// Non-git PROJECT folders inside your scan roots (you added them = consent):
|
|
873
|
+
// a directory with a manifest but no .git — "not everything is a repo".
|
|
874
|
+
function findProjectFolders(root) {
|
|
875
|
+
const manifests = sh(
|
|
876
|
+
`find ${JSON.stringify(root)} -maxdepth 3 ` +
|
|
877
|
+
`\\( -name node_modules -o -name .cache -o -name .local -o -name .npm -o -name venv -o -name .venv -o -name .gradle -o -name Pods -o -name .git \\) -prune ` +
|
|
878
|
+
`-o -type f \\( -name package.json -o -name requirements.txt -o -name pyproject.toml -o -name go.mod -o -name Cargo.toml -o -name pom.xml -o -name build.gradle -o -name Gemfile -o -name composer.json -o -name Dockerfile -o -name pubspec.yaml -o -name CMakeLists.txt \\) -print 2>/dev/null | head -200`,
|
|
879
|
+
).split("\n").filter(Boolean);
|
|
880
|
+
const dirs = new Set(); for (const m of manifests) dirs.add(m.replace(/\/[^/]+$/, ""));
|
|
881
|
+
const out = [];
|
|
882
|
+
for (const d of dirs) {
|
|
883
|
+
if (sh(`git -C ${JSON.stringify(d)} rev-parse --is-inside-work-tree 2>/dev/null`).trim() === "true") continue; // inside a repo already
|
|
884
|
+
out.push(d); if (out.length >= 24) break;
|
|
885
|
+
}
|
|
886
|
+
return out;
|
|
887
|
+
}
|
|
888
|
+
function detectFolder(path) {
|
|
889
|
+
const files = sh(`find ${JSON.stringify(path)} -maxdepth 2 \\( -name node_modules -o -name .git \\) -prune -o -type f -print 2>/dev/null | head -2000`).split("\n").filter(Boolean);
|
|
890
|
+
const count = {}; const tools = new Set();
|
|
891
|
+
for (const f of files) { const base = f.split("/").pop(); if (MANIFEST_TOOL[base]) tools.add(MANIFEST_TOOL[base]); const ext = (base.includes(".") ? base.split(".").pop() : "").toLowerCase(); if (EXT_LANG[ext]) count[EXT_LANG[ext]] = (count[EXT_LANG[ext]] || 0) + 1; }
|
|
892
|
+
return { path, name: path.split("/").pop(), langs: Object.entries(count).sort((a, b) => b[1] - a[1]).map((x) => x[0]), tools: [...tools], files: files.length };
|
|
893
|
+
}
|
|
846
894
|
function buildMap() {
|
|
847
895
|
const who = me();
|
|
848
|
-
const repos = findAllRepos(
|
|
896
|
+
const repos = findAllRepos().map(detectRepo);
|
|
849
897
|
const nodes = []; const edges = []; const have = new Set();
|
|
850
898
|
const add = (n) => { if (!have.has(n.id)) { have.add(n.id); nodes.push(n); } };
|
|
851
899
|
add({ id: "me", type: "person", label: who.name || "You", weight: 22 });
|
|
@@ -857,6 +905,17 @@ function buildMap() {
|
|
|
857
905
|
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 }); }
|
|
858
906
|
for (const T of r.tools) { const id = "tool:" + T; add({ id, type: "tool", label: T, weight: 12 }); edges.push({ source: rid, target: id }); }
|
|
859
907
|
}
|
|
908
|
+
// Non-git project folders in the scan roots (capped; deduped against repos).
|
|
909
|
+
try {
|
|
910
|
+
const repoPaths = new Set(repos.map((r) => r.path)); let folders = 0;
|
|
911
|
+
for (const root of scanRoots()) { for (const d of findProjectFolders(root)) {
|
|
912
|
+
if (repoPaths.has(d) || have.has("folder:" + d) || folders >= 20) continue;
|
|
913
|
+
const det = detectFolder(d); folders++;
|
|
914
|
+
add({ id: "folder:" + d, type: "folder", label: det.name, weight: 10, meta: { path: d, langs: det.langs.slice(0, 3), tools: det.tools, files: det.files } });
|
|
915
|
+
edges.push({ source: "me", target: "folder:" + d });
|
|
916
|
+
for (const L of det.langs.slice(0, 3)) { const id = "lang:" + L; add({ id, type: "lang", label: L, weight: 15 }); edges.push({ source: "folder:" + d, target: id }); }
|
|
917
|
+
} }
|
|
918
|
+
} catch {}
|
|
860
919
|
// What you build WITH (person-level, not per-repo): detected agents/editors
|
|
861
920
|
// and the AI currently powering Symbiot — so the initial scan shows the whole
|
|
862
921
|
// setup, not just code. All local detection, nothing invasive.
|
|
@@ -872,6 +931,7 @@ function buildMap() {
|
|
|
872
931
|
} catch {}
|
|
873
932
|
const out = { nodes, edges, stats: {
|
|
874
933
|
repos: repos.length,
|
|
934
|
+
folders: nodes.filter((n) => n.type === "folder").length,
|
|
875
935
|
languages: nodes.filter((n) => n.type === "lang").length,
|
|
876
936
|
tools: nodes.filter((n) => n.type === "tool").length,
|
|
877
937
|
agents: nodes.filter((n) => n.type === "agent").length,
|
|
@@ -905,6 +965,10 @@ function nodeDetail(id) {
|
|
|
905
965
|
const n = map.nodes.find((x) => x.id === id);
|
|
906
966
|
return { type: id.startsWith("agent:") ? "agent" : "ai", label: n ? n.label : id.split(":")[1], meta: (n && n.meta) || {} };
|
|
907
967
|
}
|
|
968
|
+
if (id.startsWith("folder:")) {
|
|
969
|
+
const path = id.slice(7); const n = map.nodes.find((x) => x.id === id); const m = (n && n.meta) || {};
|
|
970
|
+
return { type: "folder", label: n ? n.label : path.split("/").pop(), path, langs: m.langs || [], tools: m.tools || [], files: m.files || 0 };
|
|
971
|
+
}
|
|
908
972
|
return { error: "unknown node" };
|
|
909
973
|
}
|
|
910
974
|
// AI review of one repo: what it does, what it's for, possible upgrades.
|
|
@@ -1250,6 +1314,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1250
1314
|
<span class="lg"><i style="background:#6bb3ff"></i>tools</span>
|
|
1251
1315
|
<span class="lg"><i style="background:#c58af9"></i>agents</span>
|
|
1252
1316
|
<span class="lg"><i style="background:#5fe3b0"></i>AI</span>
|
|
1317
|
+
<span class="lg"><i style="background:#b7a98c"></i>folders (no git)</span>
|
|
1253
1318
|
<span class="muted" style="margin-left:8px">scroll to zoom · drag to pan · click a node</span>
|
|
1254
1319
|
<button class="ghost" id="remap" style="margin-left:auto">Rescan</button>
|
|
1255
1320
|
</div>
|
|
@@ -1297,6 +1362,12 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1297
1362
|
<div class="row" style="margin-top:16px"><button class="act" id="save">Save & connect</button>
|
|
1298
1363
|
<span class="note" id="saveMsg"></span></div>
|
|
1299
1364
|
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1365
|
+
<label>Folders to scan for repos</label>
|
|
1366
|
+
<div id="scanroots"></div>
|
|
1367
|
+
<div class="row" style="margin-top:6px"><input id="newroot" placeholder="/path/to/folder (or ~/work) — where your projects live" style="flex:1"><button class="ghost" id="addroot">Add folder</button></div>
|
|
1368
|
+
<div class="note muted" id="scanrootnote">Point Symbiot at where your work lives — inside or outside your home folder. Defaults to your home folder.</div>
|
|
1369
|
+
</div>
|
|
1370
|
+
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1300
1371
|
<label>Hand off to your agent when you "Send to repos"</label>
|
|
1301
1372
|
<input id="agentcmd" type="text" placeholder="e.g. claude -p "{prompt}" · code {dir} · leave blank to just write the file">
|
|
1302
1373
|
<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>
|
|
@@ -1318,7 +1389,7 @@ var KEYURL={anthropic:'https://console.anthropic.com/settings/keys',openai:'http
|
|
|
1318
1389
|
var DEFMODEL={anthropic:'claude-opus-5-5',openai:'gpt-4o-mini',gemini:'gemini-1.5-flash',ollama:'llama3.1'};
|
|
1319
1390
|
function $(id){return document.getElementById(id);}
|
|
1320
1391
|
var current='map';var mapLoaded=false;var driftLoaded=false;
|
|
1321
|
-
var COLORS={person:'#3DDC97',repo:'#F4F1EA',lang:'#F2A541',tool:'#6bb3ff',agent:'#c58af9',ai:'#5fe3b0'};
|
|
1392
|
+
var COLORS={person:'#3DDC97',repo:'#F4F1EA',lang:'#F2A541',tool:'#6bb3ff',agent:'#c58af9',ai:'#5fe3b0',folder:'#b7a98c'};
|
|
1322
1393
|
function tabs(){return document.querySelectorAll('.tab');}
|
|
1323
1394
|
function setTab(tab){current=tab;tabs().forEach(function(t){t.classList.toggle('active',t.dataset.tab===tab);});
|
|
1324
1395
|
var isMap=tab==='map',isSet=tab==='settings',isTasks=tab==='tasks',isDrift=tab==='drift',isAgents=tab==='agents',isRun=(tab==='week'||tab==='standup'||tab==='todo');
|
|
@@ -1374,6 +1445,7 @@ document.getElementById('suggest').addEventListener('click',function(){var o=doc
|
|
|
1374
1445
|
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;}
|
|
1375
1446
|
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
1447
|
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;}
|
|
1448
|
+
if(d.type==='folder'){var chips='';(d.langs||[]).concat(d.tools||[]).forEach(function(x){chips+="<span class='tag'>"+esc(x)+"</span>";});el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>project folder · not a git repo · "+(d.files||0)+" files</div><div class='chips'>"+chips+"</div><div class='k' style='font-family:ui-monospace,monospace'>"+esc(d.path||'')+"</div><div class='k'>Tasks can be sent here too — the agent runs in this folder.</div>";return;}
|
|
1377
1449
|
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;}}
|
|
1378
1450
|
var reviewCache={};var IDEAS=[];var IREPO="";
|
|
1379
1451
|
function hideReview(){var el=document.getElementById('review');el.classList.add('hidden');el.innerHTML='';}
|
|
@@ -1470,6 +1542,11 @@ if(!chips.length){box.innerHTML="<span class='muted' style='font-size:12px'>Noth
|
|
|
1470
1542
|
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
1543
|
box.querySelectorAll('.preset').forEach(function(btn){btn.addEventListener('click',function(){document.getElementById('agentcmd').value=chips[+btn.getAttribute('data-i')].tmpl;saveAgent();});});});}
|
|
1472
1544
|
function saveAgent(){api('/api/agentcmd',{cmd:document.getElementById('agentcmd').value});}
|
|
1545
|
+
function loadScanRoots(){api('/api/scanroots').then(function(d){var box=document.getElementById('scanroots');var roots=d.effective||[];
|
|
1546
|
+
box.innerHTML=roots.map(function(r){var custom=(d.roots||[]).indexOf(r)>=0;return "<div class='task' data-p='"+esc(r)+"'><span class='t' style='font-family:ui-monospace,monospace;font-size:12px'>"+esc(r)+"</span>"+(r===d.home?"<span class='rp'>home</span>":"")+(custom?"<button class='rm rmroot' title='remove'>×</button>":"")+"</div>";}).join("");
|
|
1547
|
+
box.querySelectorAll('.rmroot').forEach(function(btn){btn.addEventListener('click',function(){api('/api/scanroots/remove',{path:btn.closest('.task').getAttribute('data-p')}).then(function(){loadScanRoots();mapLoaded=false;driftLoaded=false;});});});});}
|
|
1548
|
+
function addRootUI(){var i=document.getElementById('newroot');var v=(i.value||'').trim();if(!v)return;var n=document.getElementById('scanrootnote');
|
|
1549
|
+
api('/api/scanroots/add',{path:v}).then(function(r){if(r.error){n.innerHTML="<span class='err'>"+esc(r.error)+"</span>";return;}i.value='';n.textContent='Added — the Map/Drift will rescan.';loadScanRoots();mapLoaded=false;driftLoaded=false;});}
|
|
1473
1550
|
function setupLocalUI(){var o=document.getElementById('setupout');o.innerHTML="Setting up a local model…";
|
|
1474
1551
|
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
1552
|
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>";});}
|
|
@@ -1502,13 +1579,15 @@ h+="</div>";out.innerHTML=h;});}
|
|
|
1502
1579
|
document.getElementById('remap').addEventListener('click',loadMap);
|
|
1503
1580
|
document.getElementById('recbtn').addEventListener('click',loadRec);
|
|
1504
1581
|
document.getElementById('setuplocal').addEventListener('click',setupLocalUI);
|
|
1582
|
+
document.getElementById('addroot').addEventListener('click',addRootUI);
|
|
1583
|
+
document.getElementById('newroot').addEventListener('keydown',function(e){if(e.key==='Enter')addRootUI();});
|
|
1505
1584
|
document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
|
|
1506
1585
|
document.getElementById('agentsrefresh').addEventListener('click',loadAgents);
|
|
1507
1586
|
document.getElementById('addtask').addEventListener('click',addTaskUI);
|
|
1508
1587
|
document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
|
|
1509
1588
|
document.getElementById('agentcmd').addEventListener('change',saveAgent);
|
|
1510
1589
|
document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
|
|
1511
|
-
initGraphEvents();syncP();refresh();loadMap();loadAgentCfg();
|
|
1590
|
+
initGraphEvents();syncP();refresh();loadMap();loadAgentCfg();loadScanRoots();
|
|
1512
1591
|
</script></body></html>`;
|
|
1513
1592
|
|
|
1514
1593
|
function readBody(req) {
|
|
@@ -1574,6 +1653,9 @@ async function cmdApp() {
|
|
|
1574
1653
|
if (u.pathname === "/api/tasks/restore" && req.method === "POST") { const b = await readBody(req); return json(res, restoreTask(String(b.id || ""))); }
|
|
1575
1654
|
if (u.pathname === "/api/tasks/sync" && req.method === "POST") return json(res, syncTasks());
|
|
1576
1655
|
if (u.pathname === "/api/tasks/push" && req.method === "POST") { const b = await readBody(req); return json(res, pushTasks(b)); }
|
|
1656
|
+
if (u.pathname === "/api/scanroots") return json(res, { roots: loadConfig().scanRoots || [], effective: scanRoots(), home: homedir() });
|
|
1657
|
+
if (u.pathname === "/api/scanroots/add" && req.method === "POST") { const b = await readBody(req); return json(res, addScanRoot(String(b.path || ""))); }
|
|
1658
|
+
if (u.pathname === "/api/scanroots/remove" && req.method === "POST") { const b = await readBody(req); return json(res, removeScanRoot(String(b.path || ""))); }
|
|
1577
1659
|
if (u.pathname === "/api/agentcfg") { const d = detectHandoffs(); return json(res, { cmd: handoffCmd(), agents: d.agents, editors: d.editors }); }
|
|
1578
1660
|
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
1661
|
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 : "" }); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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": {
|