symbiot 0.28.3 → 0.29.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 +25 -1
- package/package.json +1 -1
- package/ui.mjs +7 -6
package/index.mjs
CHANGED
|
@@ -1322,6 +1322,30 @@ async function repoSuggest(path) {
|
|
|
1322
1322
|
const text = await write(system, prompt);
|
|
1323
1323
|
return { text: text || "(couldn't reach the model)", footer };
|
|
1324
1324
|
}
|
|
1325
|
+
// Overview + suggestions for a NON-git project folder (no commits to read), from
|
|
1326
|
+
// its file tree, manifest and README. The folder-node equivalent of repoSuggest.
|
|
1327
|
+
async function folderSuggest(path) {
|
|
1328
|
+
if (!resolveProvider()) return { error: "not-connected" };
|
|
1329
|
+
if (!path) return { error: "no path" };
|
|
1330
|
+
const det = detectFolder(path);
|
|
1331
|
+
const files = sh(`find ${JSON.stringify(path)} -maxdepth 2 \\( -name node_modules -o -name .git \\) -prune -o -type f -print 2>/dev/null | head -200`).split("\n").filter(Boolean);
|
|
1332
|
+
const rels = files.map((f) => f.slice(path.length + 1));
|
|
1333
|
+
const pick = (names) => { for (const n of names) { const hit = files.find((f) => f.split("/").pop().toLowerCase() === n); if (hit) { try { return readFileSync(hit, "utf8").slice(0, 2000); } catch {} } } return ""; };
|
|
1334
|
+
const manifest = pick(["package.json", "requirements.txt", "pyproject.toml", "go.mod", "cargo.toml", "pom.xml", "composer.json", "gemfile", "pubspec.yaml"]);
|
|
1335
|
+
const readme = pick(["readme.md", "readme.txt", "readme"]);
|
|
1336
|
+
const stack = [...det.langs, ...det.tools].join(", ") || "unknown";
|
|
1337
|
+
const system =
|
|
1338
|
+
`You are a pragmatic senior engineer looking at a project FOLDER that is NOT under version control. ` +
|
|
1339
|
+
`From its files, write exactly three short sections with headings: "Overview" (what this project is, 1-2 sentences), ` +
|
|
1340
|
+
`"Next steps" (3-5 concrete actions — a strong first one is often "git init" if this looks like real work), ` +
|
|
1341
|
+
`"Ideas" (2-3 that fit where it's heading). Ground every point in the actual files/manifest/README shown. No preamble.`;
|
|
1342
|
+
const prompt =
|
|
1343
|
+
`Folder: ${det.name}\nStack: ${stack}\nFiles (${det.files}):\n${rels.slice(0, 120).join("\n")}\n\n` +
|
|
1344
|
+
(manifest ? `Manifest:\n${manifest}\n\n` : "") + (readme ? `README excerpt:\n${readme}\n\n` : "") + `Write the overview and suggestions.`;
|
|
1345
|
+
const text = await write(system, prompt);
|
|
1346
|
+
const footer = `symbiot ${VERSION} · folder · ${det.files} files · ${stack} · not a git repo`;
|
|
1347
|
+
return { text: text || "(couldn't reach the model)", footer };
|
|
1348
|
+
}
|
|
1325
1349
|
|
|
1326
1350
|
// Build a write-up for a command; returns { text, sub, error? } without printing.
|
|
1327
1351
|
// Shared by the CLI (cmdRun) and the web UI (symbiot app).
|
|
@@ -1543,7 +1567,7 @@ async function cmdApp() {
|
|
|
1543
1567
|
if (u.pathname === "/api/models") { const hw = detectHardware(); return json(res, { hardware: hw, rec: recommendModels(hw) }); }
|
|
1544
1568
|
if (u.pathname === "/api/drift") return json(res, computeDrift({ ci: u.searchParams.get("ci") === "1", fetch: u.searchParams.get("fetch") === "1" }));
|
|
1545
1569
|
if (u.pathname === "/api/node") return json(res, await nodeDetail(u.searchParams.get("id") || "")); // local
|
|
1546
|
-
if (u.pathname === "/api/suggest" && req.method === "POST") { const b = await readBody(req);
|
|
1570
|
+
if (u.pathname === "/api/suggest" && req.method === "POST") { const b = await readBody(req); const p = String(b.path || ""); const isRepo = p && existsSync(join(p, ".git")); return json(res, await (isRepo ? repoSuggest(p) : folderSuggest(p))); }
|
|
1547
1571
|
if (u.pathname === "/api/review" && req.method === "POST") { const b = await readBody(req); return json(res, await repoReview(String(b.path || ""))); }
|
|
1548
1572
|
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) }))); }
|
|
1549
1573
|
if (u.pathname === "/api/tasks/add" && req.method === "POST") { const b = await readBody(req); return json(res, addTask(b.text, b.repo)); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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": {
|
package/ui.mjs
CHANGED
|
@@ -149,7 +149,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
149
149
|
<div class="rfoot" id="outfoot" style="display:none"></div>
|
|
150
150
|
</section>
|
|
151
151
|
<section id="panel-tasks" class="hidden">
|
|
152
|
-
<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>
|
|
152
|
+
<div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><select id="newtaskrepo" title="Which repo this task is for (needed to send it to an agent)" style="flex:0 0 auto;max-width:180px"><option value="">repo…</option></select><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>
|
|
153
153
|
<div class="note muted" style="margin-top:2px">To give tasks to your agent, use <b>Send to repos</b> — filter by tag below to choose which. As the agent finishes each one it lands in <b>Awaiting your review</b>: <b>Approve</b> commits it on a branch and opens a PR, <b>↶</b> sends it back. Ticking a task yourself marks it done (it auto-archives).</div>
|
|
154
154
|
<div id="pushout"></div>
|
|
155
155
|
<div id="reviewout"></div>
|
|
@@ -224,7 +224,7 @@ $('panel-drift').classList.toggle('hidden',!isDrift);
|
|
|
224
224
|
$('panel-agents').classList.toggle('hidden',!isAgents);
|
|
225
225
|
if(isRun){$('what').textContent=tab;$('out').textContent='Nothing yet - hit the button.';$('out').classList.add('muted');$('copy').classList.add('hidden');}
|
|
226
226
|
if(isMap&&!mapLoaded)loadMap();
|
|
227
|
-
if(isTasks)loadTasks();
|
|
227
|
+
if(isTasks){fillTaskRepos();loadTasks();}
|
|
228
228
|
if(isDrift&&!driftLoaded)loadDrift();
|
|
229
229
|
if(isAgents)loadAgents(); else stopAgentsPoll();}
|
|
230
230
|
tabs().forEach(function(t){t.addEventListener('click',function(){setTab(t.dataset.tab);});});
|
|
@@ -268,7 +268,7 @@ document.getElementById('suggest').addEventListener('click',function(){var o=doc
|
|
|
268
268
|
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;}
|
|
269
269
|
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;}
|
|
270
270
|
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;}
|
|
271
|
-
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;}
|
|
271
|
+
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><button class='act' id='suggest' style='margin-top:12px'>Overview & suggestions</button><div id='sugout'></div><div class='k' style='margin-top:8px'>Tasks can be sent here too — the agent runs in this folder.</div>";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 an overview - 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;}
|
|
272
272
|
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;}}
|
|
273
273
|
var reviewCache={};var IDEAS=[];var IREPO="";
|
|
274
274
|
function hideReview(){var el=document.getElementById('review');el.classList.add('hidden');el.innerHTML='';}
|
|
@@ -377,14 +377,15 @@ var clean=repos.filter(function(r){return !r.flags.length;}).map(function(r){ret
|
|
|
377
377
|
if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clean.join(", "))+"</div>";
|
|
378
378
|
if(!repos.length)h+="<div class='muted'>No repos found under your home folder.</div>";
|
|
379
379
|
out.innerHTML=h;});}
|
|
380
|
-
function
|
|
380
|
+
function fillTaskRepos(){var sel=document.getElementById('newtaskrepo');if(!sel||!GRAPH)return;var cur=sel.value;var names=GRAPH.nodes.filter(function(n){return n.type==='repo'||n.type==='folder';}).map(function(n){return n.label;}).sort();sel.innerHTML="<option value=''>repo…</option>"+names.map(function(n){return "<option value='"+esc(n)+"'"+(n===cur?" selected":"")+">"+esc(n)+"</option>";}).join("");}
|
|
381
|
+
function addTaskUI(){var i=document.getElementById('newtask');var v=(i.value||'').trim();if(!v)return;var repo=(document.getElementById('newtaskrepo')||{}).value||'';api('/api/tasks/add',{text:v,repo:repo}).then(function(){i.value='';TFILTER={type:'',repo:''};loadTasks();});}
|
|
381
382
|
function pushTasksUI(){var o=document.getElementById('pushout');var btn=document.getElementById('pushtasks');btn.disabled=true;
|
|
382
383
|
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>";
|
|
383
384
|
api('/api/tasks/push',{type:TFILTER.type||'',repo:TFILTER.repo||''}).then(function(r){btn.disabled=false;
|
|
384
385
|
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;}
|
|
385
386
|
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>";
|
|
386
387
|
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>";}
|
|
387
|
-
if(r.unresolved&&r.unresolved.length){h+="<div class='dd' style='margin-top:6px'>⚠ not
|
|
388
|
+
if(r.unresolved&&r.unresolved.length){var names=r.unresolved.map(function(u){return u.name;}).join(", ");var hasNoRepo=r.unresolved.some(function(u){return u.name==='(no repo)';});h+="<div class='dd' style='margin-top:6px'>⚠ not sent: "+esc(names)+". "+(hasNoRepo?"Pick a repo in the dropdown next to <b>Add</b> so the task has somewhere to go.":"That repo isn't in the map — add its folder in Settings.")+"</div>";}
|
|
388
389
|
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);}
|
|
389
390
|
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>";}
|
|
390
391
|
h+="</div>";
|
|
@@ -423,7 +424,7 @@ function scanLine(s){return 'Mapping your work… '+s.phase+(s.total?' '+s.done+
|
|
|
423
424
|
function loadMap(){var p=document.getElementById("profile");p.textContent="Mapping your work...";document.getElementById("graph").innerHTML="";sel=null;hideDetail();hideReview();var done=false;
|
|
424
425
|
function poll(){if(done)return;api('/api/scan').then(function(s){if(done)return;if(s&&s.active&&s.phase)p.textContent=scanLine(s);setTimeout(poll,600);}).catch(function(){});}
|
|
425
426
|
setTimeout(poll,400);
|
|
426
|
-
api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;layout(g.nodes,g.edges);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"");render();});}
|
|
427
|
+
api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;fillTaskRepos();layout(g.nodes,g.edges);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"");render();});}
|
|
427
428
|
function fitBadge(m){return m.fits?"<span class='tag'>fits your RAM</span>":"<span class='tag' style='background:#3a2a12;border-color:#6b4a1f;color:#F2A541'>needs more RAM</span>";}
|
|
428
429
|
function loadRec(){var out=document.getElementById('recout');out.innerHTML="<div class='muted'>Reading your hardware...</div>";
|
|
429
430
|
api('/api/models').then(function(d){var hw=d.hardware,rec=d.rec;
|