symbiot 0.6.1 → 0.7.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/README.md +12 -0
- package/index.mjs +195 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ symbiot your last 7 days, written up (same as: symbiot week)
|
|
|
12
12
|
symbiot standup yesterday + today, for standup
|
|
13
13
|
symbiot todo what's still on your plate
|
|
14
14
|
symbiot app open the visual app in your browser
|
|
15
|
+
symbiot models recommend AI models for your hardware
|
|
15
16
|
symbiot login connect it to an AI (once)
|
|
16
17
|
symbiot whoami show how it's connected
|
|
17
18
|
symbiot help
|
|
@@ -51,6 +52,17 @@ where no Chromium-family browser is found it opens your default browser instead.
|
|
|
51
52
|
npm install -g symbiot # or run without installing: npx symbiot week
|
|
52
53
|
```
|
|
53
54
|
|
|
55
|
+
## Which model? Ask your machine
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
symbiot models
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Reads your RAM / CPU / GPU and recommends **local models by tier** (min / med / max,
|
|
62
|
+
marking which fit your RAM) to run free & private via [Ollama](https://ollama.com),
|
|
63
|
+
plus **paid** options (Claude / OpenAI / Gemini, cheap → top). Also available as
|
|
64
|
+
a button in the app's Settings.
|
|
65
|
+
|
|
54
66
|
## Connect it (once)
|
|
55
67
|
|
|
56
68
|
Pick the AI you want it to write with:
|
package/index.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import Anthropic from "@anthropic-ai/sdk";
|
|
21
21
|
import { execSync, spawn } from "node:child_process";
|
|
22
|
-
import { homedir } from "node:os";
|
|
22
|
+
import { homedir, totalmem, cpus as oscpus } from "node:os";
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
25
25
|
import { createInterface } from "node:readline";
|
|
@@ -86,6 +86,19 @@ function saveConfig(cfg) {
|
|
|
86
86
|
function antProfileExists() {
|
|
87
87
|
try { return existsSync(join(homedir(), ".config", "anthropic")); } catch { return false; }
|
|
88
88
|
}
|
|
89
|
+
// ---- tasks: a persistent checklist (~/.config/symbiot/tasks.json) ---------
|
|
90
|
+
const TASKS_PATH = join(CONFIG_DIR, "tasks.json");
|
|
91
|
+
function loadTasks() { try { return JSON.parse(readFileSync(TASKS_PATH, "utf8")); } catch { return []; } }
|
|
92
|
+
function saveTasks(t) { try { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(TASKS_PATH, JSON.stringify(t, null, 2)); return true; } catch { return false; } }
|
|
93
|
+
function addTask(text, repo) {
|
|
94
|
+
text = String(text || "").trim().slice(0, 300);
|
|
95
|
+
if (!text) return { error: "empty" };
|
|
96
|
+
const t = loadTasks();
|
|
97
|
+
const item = { id: randomBytes(6).toString("hex"), text, repo: repo || "", done: false, ts: Date.now() };
|
|
98
|
+
t.unshift(item); saveTasks(t); return item;
|
|
99
|
+
}
|
|
100
|
+
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" }; }
|
|
101
|
+
function removeTask(id) { saveTasks(loadTasks().filter((x) => x.id !== id)); return { ok: true }; }
|
|
89
102
|
function envKey(provider) {
|
|
90
103
|
for (const e of (PROVIDERS[provider].env || [])) if (process.env[e]) return process.env[e];
|
|
91
104
|
return null;
|
|
@@ -300,6 +313,52 @@ function header(sub) {
|
|
|
300
313
|
const SINCE_WEEK = Number(flag("since", "7"));
|
|
301
314
|
const BASE = flag("dir", homedir());
|
|
302
315
|
|
|
316
|
+
// ---- hardware -> model recommendations ------------------------------------
|
|
317
|
+
function detectGpu() {
|
|
318
|
+
const nv = sh("nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits 2>/dev/null").trim();
|
|
319
|
+
if (nv) { const p = nv.split("\n")[0].split(","); return { name: (p[0] || "NVIDIA GPU").trim(), vramGB: Math.round(Number(p[1]) / 1024) || null, kind: "nvidia" }; }
|
|
320
|
+
if (process.platform === "linux") { const vga = sh("lspci 2>/dev/null | grep -iE 'vga|3d controller|display' | head -1").replace(/^.*?: /, "").trim(); if (vga) return { name: vga, vramGB: null, kind: "other" }; }
|
|
321
|
+
if (process.platform === "darwin") { const chip = sh("sysctl -n machdep.cpu.brand_string 2>/dev/null").trim(); if (/apple/i.test(chip)) return { name: chip + " (unified memory)", vramGB: null, kind: "apple" }; }
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
function detectHardware() {
|
|
325
|
+
const cpus = oscpus() || [];
|
|
326
|
+
return { platform: process.platform, arch: process.arch, ramGB: Math.round(totalmem() / 1073741824), cpuCount: cpus.length, cpuModel: ((cpus[0] && cpus[0].model) || "CPU").trim(), gpu: detectGpu() };
|
|
327
|
+
}
|
|
328
|
+
function recommendModels(hw) {
|
|
329
|
+
const ram = hw.ramGB || 8;
|
|
330
|
+
const local = [
|
|
331
|
+
{ tier: "min", model: "llama3.2:3b", needGB: 6, note: "fast & light — fine for summaries" },
|
|
332
|
+
{ tier: "med", model: "llama3.1:8b", needGB: 10, note: "solid all-rounder" },
|
|
333
|
+
{ tier: "max", model: "qwen2.5:14b", needGB: 18, note: "stronger reasoning" },
|
|
334
|
+
];
|
|
335
|
+
if (ram >= 40) local.push({ tier: "max+", model: "llama3.1:70b", needGB: 48, note: "top local quality — big machine/GPU" });
|
|
336
|
+
local.forEach((m) => { m.fits = ram >= m.needGB; });
|
|
337
|
+
const paid = [
|
|
338
|
+
{ provider: "anthropic", tier: "cheap", model: "claude-haiku-4-5", note: "cheapest Claude" },
|
|
339
|
+
{ provider: "anthropic", tier: "top", model: "claude-opus-5-5", note: "best Claude (default)" },
|
|
340
|
+
{ provider: "openai", tier: "cheap", model: "gpt-4o-mini", note: "cheap OpenAI" },
|
|
341
|
+
{ provider: "openai", tier: "top", model: "gpt-4o", note: "stronger OpenAI" },
|
|
342
|
+
{ provider: "gemini", tier: "cheap", model: "gemini-1.5-flash", note: "cheap Google" },
|
|
343
|
+
{ provider: "gemini", tier: "top", model: "gemini-1.5-pro", note: "stronger Google" },
|
|
344
|
+
];
|
|
345
|
+
const best = (local.slice().reverse().find((m) => m.fits) || local[0]).model;
|
|
346
|
+
return { local, paid, best };
|
|
347
|
+
}
|
|
348
|
+
function cmdModels() {
|
|
349
|
+
const hw = detectHardware(); const rec = recommendModels(hw);
|
|
350
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
351
|
+
console.log("\n" + c.b("Your machine"));
|
|
352
|
+
console.log(` ${c.b(hw.ramGB + " GB")} RAM · ${hw.cpuCount}-core ${hw.cpuModel} · ${hw.platform}/${hw.arch}`);
|
|
353
|
+
if (hw.gpu) console.log(` GPU: ${hw.gpu.name}${hw.gpu.vramGB ? ` (${hw.gpu.vramGB} GB VRAM)` : ""}`);
|
|
354
|
+
console.log("\n" + c.b("Local models") + c.d(" (free & private via Ollama — install: ollama pull <model>)"));
|
|
355
|
+
rec.local.forEach((m) => console.log(` ${m.fits ? c.g("✓") : c.d("·")} ${pad(m.tier, 5)} ${pad(m.model, 16)} ${c.d("~" + m.needGB + "GB " + m.note + (m.fits ? "" : " (needs more RAM)"))}`));
|
|
356
|
+
console.log(c.d(` Best fit for you: `) + c.b(rec.best) + c.d(` → ollama pull ${rec.best} then symbiot login (choose Ollama)`));
|
|
357
|
+
console.log("\n" + c.b("Paid models") + c.d(" (bring an API key — symbiot login)"));
|
|
358
|
+
rec.paid.forEach((p) => console.log(` ${pad(p.provider, 10)} ${pad(p.tier, 6)} ${pad(p.model, 20)} ${c.d(p.note)}`));
|
|
359
|
+
console.log("");
|
|
360
|
+
}
|
|
361
|
+
|
|
303
362
|
// ---- work map: a node graph of your repos, languages, and tools -----------
|
|
304
363
|
const EXT_LANG = {
|
|
305
364
|
ts: "TypeScript", tsx: "TypeScript", js: "JavaScript", jsx: "JavaScript", mjs: "JavaScript",
|
|
@@ -338,7 +397,9 @@ function detectRepo(r) {
|
|
|
338
397
|
const langs = Object.entries(count).sort((a, b) => b[1] - a[1]).map((x) => x[0]);
|
|
339
398
|
const email = sh(`git -C ${JSON.stringify(r.path)} config user.email`).trim();
|
|
340
399
|
const mine = Number(sh(`git -C ${JSON.stringify(r.path)} log ${email ? `--author=${JSON.stringify(email)}` : ""} --oneline 2>/dev/null | wc -l`).trim()) || 0;
|
|
341
|
-
|
|
400
|
+
const branch = sh(`git -C ${JSON.stringify(r.path)} rev-parse --abbrev-ref HEAD 2>/dev/null`).trim();
|
|
401
|
+
const last = sh(`git -C ${JSON.stringify(r.path)} log -1 --format=%cd --date=short 2>/dev/null`).trim();
|
|
402
|
+
return { ...r, langs, tools: [...tools], mine, files: files.length, branch, last };
|
|
342
403
|
}
|
|
343
404
|
function buildMap() {
|
|
344
405
|
const who = me();
|
|
@@ -349,7 +410,7 @@ function buildMap() {
|
|
|
349
410
|
for (const r of repos) {
|
|
350
411
|
const rid = "repo:" + r.path;
|
|
351
412
|
add({ id: rid, type: "repo", label: r.name, weight: Math.min(9 + Math.log2(1 + r.mine) * 3, 26),
|
|
352
|
-
meta: { commits: r.mine, langs: r.langs.slice(0, 3), tools: r.tools } });
|
|
413
|
+
meta: { commits: r.mine, langs: r.langs.slice(0, 3), tools: r.tools, files: r.files, branch: r.branch, last: r.last, path: r.path } });
|
|
353
414
|
edges.push({ source: "me", target: rid });
|
|
354
415
|
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 }); }
|
|
355
416
|
for (const T of r.tools) { const id = "tool:" + T; add({ id, type: "tool", label: T, weight: 12 }); edges.push({ source: rid, target: id }); }
|
|
@@ -359,6 +420,8 @@ function buildMap() {
|
|
|
359
420
|
languages: nodes.filter((n) => n.type === "lang").length,
|
|
360
421
|
tools: nodes.filter((n) => n.type === "tool").length,
|
|
361
422
|
commits: repos.reduce((s, r) => s + r.mine, 0),
|
|
423
|
+
files: repos.reduce((s, r) => s + (r.files || 0), 0),
|
|
424
|
+
base: BASE,
|
|
362
425
|
} };
|
|
363
426
|
LAST_MAP = out;
|
|
364
427
|
return out;
|
|
@@ -384,6 +447,39 @@ function nodeDetail(id) {
|
|
|
384
447
|
}
|
|
385
448
|
return { error: "unknown node" };
|
|
386
449
|
}
|
|
450
|
+
// AI review of one repo: what it does, what it's for, possible upgrades.
|
|
451
|
+
async function repoReview(path) {
|
|
452
|
+
if (!resolveProvider()) return { error: "not-connected" };
|
|
453
|
+
if (!path) return { error: "no repo" };
|
|
454
|
+
const name = path.split("/").pop();
|
|
455
|
+
let readme = "";
|
|
456
|
+
for (const f of ["README.md", "README.MD", "Readme.md", "readme.md", "README.txt", "README"]) {
|
|
457
|
+
try { const p = join(path, f); if (existsSync(p)) { readme = readFileSync(p, "utf8").slice(0, 2500); break; } } catch {}
|
|
458
|
+
}
|
|
459
|
+
const files = sh(`git -C ${JSON.stringify(path)} ls-files 2>/dev/null | head -60`).trim();
|
|
460
|
+
const recent = sh(`git -C ${JSON.stringify(path)} log --oneline -12 2>/dev/null`).trim();
|
|
461
|
+
const system =
|
|
462
|
+
`You are reviewing one software project for its owner. Respond with ONLY a JSON object, no markdown fences, no prose outside it, shaped exactly: ` +
|
|
463
|
+
`{"review": string, "ideas": string[]}. "review" is 3-6 sentences covering what it does and what it's meant for (who'd use it, why). ` +
|
|
464
|
+
`"ideas" is 3-6 short, concrete upgrade or next-feature items, each a single actionable phrase (something you could put on a to-do list). ` +
|
|
465
|
+
`Be specific and grounded in the README, files, and commits — infer from real evidence, don't invent.`;
|
|
466
|
+
const prompt =
|
|
467
|
+
`Project: ${name}\nPath: ${path}\n\n` +
|
|
468
|
+
(readme ? `README (excerpt):\n${readme}\n\n` : "(no README found)\n\n") +
|
|
469
|
+
`Files:\n${files || "(none)"}\n\nRecent commits:\n${recent || "(none)"}\n\nReturn the JSON.`;
|
|
470
|
+
const raw = await write(system, prompt);
|
|
471
|
+
if (!raw) return { text: "(couldn't reach the model)", ideas: [] };
|
|
472
|
+
const j = extractJson(raw);
|
|
473
|
+
if (j && (j.review || j.ideas)) return { text: String(j.review || "").trim() || raw, ideas: Array.isArray(j.ideas) ? j.ideas.map((x) => String(x).trim()).filter(Boolean).slice(0, 8) : [] };
|
|
474
|
+
return { text: raw, ideas: [] }; // model didn't return clean JSON — show prose, no checkboxes
|
|
475
|
+
}
|
|
476
|
+
function extractJson(s) {
|
|
477
|
+
if (!s) return null;
|
|
478
|
+
let t = String(s).trim().replace(/^```(?:json)?/i, "").replace(/```$/,"").trim();
|
|
479
|
+
const a = t.indexOf("{"), b = t.lastIndexOf("}");
|
|
480
|
+
if (a >= 0 && b > a) { try { return JSON.parse(t.slice(a, b + 1)); } catch {} }
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
387
483
|
// AI "value" suggestions for one repo, from its recent commits + open work.
|
|
388
484
|
async function repoSuggest(path) {
|
|
389
485
|
if (!resolveProvider()) return { error: "not-connected" };
|
|
@@ -592,6 +688,22 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
592
688
|
.detail ul{margin:6px 0 0;padding-left:18px}.detail li{margin:2px 0}
|
|
593
689
|
.detail .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
594
690
|
.tag{display:inline-flex;font-size:11px;padding:4px 9px;border-radius:999px;background:var(--green-dim);border:1px solid #2a6b52;color:var(--green)}
|
|
691
|
+
.mapbar{margin-top:8px;font-size:12.5px;color:var(--text);background:var(--ink3);border:1px solid var(--line);border-radius:9px;padding:9px 12px;min-height:18px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
692
|
+
.mapbar b{color:var(--bone)}
|
|
693
|
+
.review{margin-top:12px;background:var(--ink2);border:1px solid var(--line);border-radius:12px;padding:16px;min-height:120px}
|
|
694
|
+
.review h4{margin:0 0 8px;color:var(--green);font-size:11px;letter-spacing:.15em;text-transform:uppercase;font-weight:700}
|
|
695
|
+
.review .body{color:var(--text);line-height:1.65;font-size:14px;white-space:pre-wrap}
|
|
696
|
+
.review .rname{color:var(--bone);font-weight:600}
|
|
697
|
+
.ideas{margin-top:14px}
|
|
698
|
+
.ideas h4{margin:0 0 6px;color:var(--amber);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700}
|
|
699
|
+
.idea{display:flex;align-items:flex-start;gap:9px;padding:5px 0;font-size:13.5px;color:var(--text)}
|
|
700
|
+
.idea input{margin-top:3px;flex:none;width:15px;height:15px;cursor:pointer}
|
|
701
|
+
.task{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px solid var(--line);border-radius:9px;margin-top:8px;background:var(--ink2)}
|
|
702
|
+
.task input[type=checkbox]{width:16px;height:16px;flex:none;cursor:pointer}
|
|
703
|
+
.task .t{flex:1}.task.done .t{color:var(--faint);text-decoration:line-through}
|
|
704
|
+
.task .rp{font-size:11px;color:var(--faint);background:var(--ink3);border:1px solid var(--line);border-radius:999px;padding:2px 8px}
|
|
705
|
+
.task .rm{background:none;border:0;color:var(--faint);cursor:pointer;font-size:18px;line-height:1}
|
|
706
|
+
.task .rm:hover{color:var(--amber)}
|
|
595
707
|
.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)}
|
|
596
708
|
.legend{display:flex;gap:14px;align-items:center;margin-top:10px;font-size:12px;color:var(--faint);flex-wrap:wrap}
|
|
597
709
|
.lg{display:inline-flex;gap:6px;align-items:center}
|
|
@@ -604,6 +716,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
604
716
|
<button class="tab" data-tab="week">Week</button>
|
|
605
717
|
<button class="tab" data-tab="standup">Standup</button>
|
|
606
718
|
<button class="tab" data-tab="todo">Todo</button>
|
|
719
|
+
<button class="tab" data-tab="tasks">Tasks</button>
|
|
607
720
|
<button class="tab" data-tab="settings">Settings</button>
|
|
608
721
|
</div>
|
|
609
722
|
<main>
|
|
@@ -613,6 +726,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
613
726
|
<svg id="graph" viewBox="0 0 960 620" preserveAspectRatio="xMidYMid meet"></svg>
|
|
614
727
|
<aside id="detail" class="detail hidden"></aside>
|
|
615
728
|
</div>
|
|
729
|
+
<div id="mapbar" class="mapbar">Hover a node for a summary · click for details · drag to move</div>
|
|
616
730
|
<div class="legend">
|
|
617
731
|
<span class="lg"><i style="background:var(--green)"></i>you</span>
|
|
618
732
|
<span class="lg"><i style="background:var(--bone)"></i>repos</span>
|
|
@@ -621,6 +735,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
621
735
|
<span class="muted" style="margin-left:8px">scroll to zoom · drag to pan · click a node</span>
|
|
622
736
|
<button class="ghost" id="remap" style="margin-left:auto">Rescan</button>
|
|
623
737
|
</div>
|
|
738
|
+
<div id="review" class="review hidden"></div>
|
|
624
739
|
</section>
|
|
625
740
|
<section id="panel-run" class="hidden">
|
|
626
741
|
<div class="row"><button class="act" id="write">Write my <span id="what">week</span></button>
|
|
@@ -628,6 +743,10 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
628
743
|
<span class="muted">Reads your local git and writes it up.</span></div>
|
|
629
744
|
<div class="out muted" id="out">Nothing yet - hit the button.</div>
|
|
630
745
|
</section>
|
|
746
|
+
<section id="panel-tasks" class="hidden">
|
|
747
|
+
<div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><button class="act" id="addtask">Add</button></div>
|
|
748
|
+
<div id="tasklist"></div>
|
|
749
|
+
</section>
|
|
631
750
|
<section id="panel-settings" class="hidden">
|
|
632
751
|
<label>Which AI should Symbiot write with?</label>
|
|
633
752
|
<select id="provider">
|
|
@@ -644,6 +763,10 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
644
763
|
<input id="model" type="text" placeholder="(blank = default)">
|
|
645
764
|
<div class="row" style="margin-top:16px"><button class="act" id="save">Save & connect</button>
|
|
646
765
|
<span class="note" id="saveMsg"></span></div>
|
|
766
|
+
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
767
|
+
<button class="ghost" id="recbtn">Recommend models for my machine</button>
|
|
768
|
+
<div id="recout" style="margin-top:12px"></div>
|
|
769
|
+
</div>
|
|
647
770
|
</section>
|
|
648
771
|
</main>
|
|
649
772
|
<footer><button class="ghost" id="quit" style="margin-left:auto">Quit</button></footer>
|
|
@@ -657,12 +780,14 @@ var current='map';var mapLoaded=false;
|
|
|
657
780
|
var COLORS={person:'#3DDC97',repo:'#F4F1EA',lang:'#F2A541',tool:'#6bb3ff'};
|
|
658
781
|
function tabs(){return document.querySelectorAll('.tab');}
|
|
659
782
|
function setTab(tab){current=tab;tabs().forEach(function(t){t.classList.toggle('active',t.dataset.tab===tab);});
|
|
660
|
-
var isMap=tab==='map',isSet=tab==='settings',isRun
|
|
783
|
+
var isMap=tab==='map',isSet=tab==='settings',isTasks=tab==='tasks',isRun=(tab==='week'||tab==='standup'||tab==='todo');
|
|
661
784
|
$('panel-map').classList.toggle('hidden',!isMap);
|
|
662
785
|
$('panel-run').classList.toggle('hidden',!isRun);
|
|
663
786
|
$('panel-settings').classList.toggle('hidden',!isSet);
|
|
787
|
+
$('panel-tasks').classList.toggle('hidden',!isTasks);
|
|
664
788
|
if(isRun){$('what').textContent=tab;$('out').textContent='Nothing yet - hit the button.';$('out').classList.add('muted');$('copy').classList.add('hidden');}
|
|
665
|
-
if(isMap&&!mapLoaded)loadMap();
|
|
789
|
+
if(isMap&&!mapLoaded)loadMap();
|
|
790
|
+
if(isTasks)loadTasks();}
|
|
666
791
|
tabs().forEach(function(t){t.addEventListener('click',function(){setTab(t.dataset.tab);});});
|
|
667
792
|
function refresh(){api('/api/status').then(function(s){$('status').textContent=s.connected?s.line:'Not connected - open Settings';});}
|
|
668
793
|
$('write').addEventListener('click',function(){$('out').textContent='Writing...';$('out').classList.add('muted');$('copy').classList.add('hidden');
|
|
@@ -691,7 +816,10 @@ GRAPH.nodes.forEach(function(n){var r=Math.max(5,Math.sqrt(n.weight)*2);var col=
|
|
|
691
816
|
s+="<g class='node' data-id='"+esc(n.id)+"' opacity='"+op+"'><circle cx='"+n.x.toFixed(1)+"' cy='"+n.y.toFixed(1)+"' r='"+r.toFixed(1)+"' fill='"+col+"'"+st+"><title>"+title+"</title></circle>";
|
|
692
817
|
var cls=n.type==="person"?"lbl-me":"";s+="<text x='"+n.x.toFixed(1)+"' y='"+(n.y+r+12).toFixed(1)+"' text-anchor='middle' class='"+cls+"'>"+esc(n.label)+"</text></g>";});
|
|
693
818
|
document.getElementById("graph").innerHTML=s+"</g>";}
|
|
694
|
-
function profileLine(g){var repos=g.nodes.filter(function(n){return n.type==="repo";});if(!repos.length)return "No git repositories found under
|
|
819
|
+
function profileLine(g){var repos=g.nodes.filter(function(n){return n.type==="repo";});var base=g.stats.base||"your home folder";if(!repos.length)return "No git repositories found under "+esc(base)+" yet.";var langs=g.nodes.filter(function(n){return n.type==="lang";}).map(function(n){return n.label;});var top=repos.slice().sort(function(a,b){return (b.meta.commits||0)-(a.meta.commits||0);})[0];var s="Scanned <b>"+esc(base)+"</b> · <b>"+g.stats.repos+"</b> repos · <b>"+g.stats.commits+"</b> of your commits · <b>"+(g.stats.files||0)+"</b> files · ";s+=langs.length?("mostly <b>"+esc(langs.slice(0,3).join(", "))+"</b>"):"no languages detected";if(top)s+=" · most active: <b>"+esc(top.label)+"</b>";return s;}
|
|
820
|
+
function nodeById(id){for(var i=0;i<GRAPH.nodes.length;i++)if(GRAPH.nodes[i].id===id)return GRAPH.nodes[i];return null;}
|
|
821
|
+
function summarize(n){if(!n)return "";if(n.type==='repo'){var m=n.meta||{};var bits=[m.commits+' commits'];if(m.branch)bits.push('branch '+m.branch);if(m.last)bits.push('last '+m.last);if(typeof m.files==='number')bits.push(m.files+' files');if(m.langs&&m.langs.length)bits.push(m.langs.join(', '));if(m.tools&&m.tools.length)bits.push(m.tools.join(', '));return "<b>"+esc(n.label)+"</b> "+esc(m.path||'')+" — "+esc(bits.join(' · '));}if(n.type==='lang')return "<b>"+esc(n.label)+"</b> — language (click to see repos)";if(n.type==='tool')return "<b>"+esc(n.label)+"</b> — tool (click to see repos)";if(n.type==='person')return "<b>"+esc(n.label)+"</b> — you";return "<b>"+esc(n.label)+"</b>";}
|
|
822
|
+
function updateBar(id){var bar=document.getElementById('mapbar');if(!bar)return;var n=id?nodeById(id):(sel?nodeById(sel):null);bar.innerHTML=n?summarize(n):"Hover a node for a summary · click for details · drag to move";}
|
|
695
823
|
function hideDetail(){document.getElementById('detail').classList.add('hidden');}
|
|
696
824
|
function showDetail(d){var el=document.getElementById('detail');el.classList.remove('hidden');
|
|
697
825
|
if(d.error){el.innerHTML="<h3>—</h3><div class='k'>"+esc(d.error)+"</div>";return;}
|
|
@@ -700,16 +828,62 @@ el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='chips'>"+chips+"</div><button
|
|
|
700
828
|
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>";});});return;}
|
|
701
829
|
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;}
|
|
702
830
|
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</div>";return;}}
|
|
703
|
-
|
|
704
|
-
function
|
|
831
|
+
var reviewCache={};
|
|
832
|
+
function hideReview(){var el=document.getElementById('review');el.classList.add('hidden');el.innerHTML='';}
|
|
833
|
+
function reviewHtml(name,body,ideas,tasks){
|
|
834
|
+
var h="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>"+esc(body)+"</div>";
|
|
835
|
+
if(ideas&&ideas.length){h+="<div class='ideas'><h4>Upgrade ideas · tick to add to Tasks</h4>";
|
|
836
|
+
ideas.forEach(function(idea){var t=(tasks||[]).filter(function(x){return x.text===idea&&x.repo===name;})[0];var tid=t?t.id:"";
|
|
837
|
+
h+="<label class='idea'><input type='checkbox' class='ideachk' data-text=\""+esc(idea)+"\" data-repo=\""+esc(name)+"\" data-tid='"+tid+"'"+(t?" checked":"")+"><span>"+esc(idea)+"</span></label>";});
|
|
838
|
+
h+="</div>";}
|
|
839
|
+
return h;}
|
|
840
|
+
function wireIdeas(){document.querySelectorAll('.ideachk').forEach(function(cb){cb.addEventListener('change',function(){
|
|
841
|
+
if(cb.checked){api('/api/tasks/add',{text:cb.getAttribute('data-text'),repo:cb.getAttribute('data-repo')}).then(function(it){if(it&&it.id)cb.setAttribute('data-tid',it.id);});}
|
|
842
|
+
else{var id=cb.getAttribute('data-tid');if(id){api('/api/tasks/remove',{id:id}).then(function(){cb.setAttribute('data-tid','');});}}});});}
|
|
843
|
+
function loadReview(name,path){var el=document.getElementById('review');el.classList.remove('hidden');
|
|
844
|
+
if(reviewCache[path]){var c=reviewCache[path];api('/api/tasks').then(function(tasks){el.innerHTML=reviewHtml(name,c.text,c.ideas,tasks);wireIdeas();});return;}
|
|
845
|
+
el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>Reading the project…</div>";
|
|
846
|
+
Promise.all([api('/api/review',{path:path}),api('/api/tasks')]).then(function(res){var r=res[0]||{},tasks=res[1]||[];
|
|
847
|
+
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>";return;}
|
|
848
|
+
var body=r.text||'(no output)';reviewCache[path]={text:body,ideas:r.ideas||[]};el.innerHTML=reviewHtml(name,body,r.ideas||[],tasks);wireIdeas();});}
|
|
849
|
+
function renderTasks(list){var el=document.getElementById('tasklist');if(!list||!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>No tasks yet. Tick an idea in a repo's review, or add one above.</div>";return;}
|
|
850
|
+
var h="";list.forEach(function(t){h+="<div class='task"+(t.done?" done":"")+"' data-id='"+esc(t.id)+"'><input type='checkbox' class='taskchk'"+(t.done?" checked":"")+"><span class='t'>"+esc(t.text)+"</span>"+(t.repo?"<span class='rp'>"+esc(t.repo)+"</span>":"")+"<button class='rm' title='remove'>×</button></div>";});
|
|
851
|
+
el.innerHTML=h;
|
|
852
|
+
el.querySelectorAll('.task').forEach(function(row){var id=row.getAttribute('data-id');
|
|
853
|
+
row.querySelector('.taskchk').addEventListener('change',function(){api('/api/tasks/toggle',{id:id}).then(function(){row.classList.toggle('done');});});
|
|
854
|
+
row.querySelector('.rm').addEventListener('click',function(){api('/api/tasks/remove',{id:id}).then(function(){row.remove();if(!el.querySelector('.task'))renderTasks([]);});});});}
|
|
855
|
+
function loadTasks(){api('/api/tasks').then(renderTasks);}
|
|
856
|
+
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();});}
|
|
857
|
+
function selectNode(id){sel=id;render();updateBar(id);var n=nodeById(id);api('/api/node?id='+encodeURIComponent(id)).then(showDetail);
|
|
858
|
+
if(n&&n.type==='repo'&&n.meta&&n.meta.path){loadReview(n.label,n.meta.path);}else{hideReview();}}
|
|
859
|
+
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};}
|
|
860
|
+
function initGraphEvents(){var el=document.getElementById('graph');var mode=null,moved=0,sx=0,sy=0,ox=0,oy=0,downId=null,dnode=null;
|
|
705
861
|
function nodeAt(ev){var t=ev.target;var g=t&&t.closest?t.closest('.node'):null;return g?g.getAttribute('data-id'):null;}
|
|
706
862
|
el.addEventListener('wheel',function(ev){ev.preventDefault();if(!GRAPH)return;var rc=el.getBoundingClientRect();var mx=(ev.clientX-rc.left)/rc.width*GW;var my=(ev.clientY-rc.top)/rc.height*GH;var nk=Math.max(0.3,Math.min(4,view.k*(ev.deltaY<0?1.12:0.89)));view.x=mx-(mx-view.x)*(nk/view.k);view.y=my-(my-view.y)*(nk/view.k);view.k=nk;render();},{passive:false});
|
|
707
|
-
el.addEventListener('pointerdown',function(ev){
|
|
708
|
-
el.addEventListener('pointermove',function(ev){
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
863
|
+
el.addEventListener('pointerdown',function(ev){if(!GRAPH)return;moved=0;sx=ev.clientX;sy=ev.clientY;ox=view.x;oy=view.y;downId=nodeAt(ev);if(downId){mode='node';dnode=nodeById(downId);}else{mode='pan';}try{el.setPointerCapture(ev.pointerId);}catch(e){}});
|
|
864
|
+
el.addEventListener('pointermove',function(ev){
|
|
865
|
+
if(!mode){updateBar(nodeAt(ev));return;}
|
|
866
|
+
var dx=ev.clientX-sx,dy=ev.clientY-sy;moved+=Math.abs(dx)+Math.abs(dy);if(moved<4)return;
|
|
867
|
+
if(mode==='node'&&dnode){var p=screenToGraph(el,ev);dnode.x=p.x;dnode.y=p.y;render();}
|
|
868
|
+
else if(mode==='pan'){var rc=el.getBoundingClientRect();view.x=ox+dx/rc.width*GW;view.y=oy+dy/rc.height*GH;render();}});
|
|
869
|
+
el.addEventListener('pointerup',function(ev){var id=downId,wasClick=moved<6;mode=null;dnode=null;downId=null;
|
|
870
|
+
if(id){selectNode(id);}else if(wasClick){sel=null;hideDetail();hideReview();render();updateBar(null);}});
|
|
871
|
+
el.addEventListener('mouseleave',function(){if(!mode)updateBar(null);}); }
|
|
872
|
+
function loadMap(){var p=document.getElementById("profile");p.textContent="Mapping your work...";document.getElementById("graph").innerHTML="";sel=null;hideDetail();hideReview();api("/api/map").then(function(g){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);render();});}
|
|
873
|
+
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>";}
|
|
874
|
+
function loadRec(){var out=document.getElementById('recout');out.innerHTML="<div class='muted'>Reading your hardware...</div>";
|
|
875
|
+
api('/api/models').then(function(d){var hw=d.hardware,rec=d.rec;
|
|
876
|
+
var h="<div class='k'><b>"+hw.ramGB+" GB</b> RAM · "+hw.cpuCount+"-core · "+esc(hw.platform)+"/"+esc(hw.arch)+(hw.gpu?" · GPU: "+esc(hw.gpu.name)+(hw.gpu.vramGB?" ("+hw.gpu.vramGB+"GB)":""):"")+"</div>";
|
|
877
|
+
h+="<div class='ideas'><h4>Local models · free via Ollama</h4>";
|
|
878
|
+
rec.local.forEach(function(m){h+="<div class='idea' style='justify-content:space-between'><span><b>"+esc(m.tier)+"</b> · <code>ollama pull "+esc(m.model)+"</code> <span class='muted'>~"+m.needGB+"GB · "+esc(m.note)+"</span></span> "+fitBadge(m)+"</div>";});
|
|
879
|
+
h+="<div class='muted' style='margin-top:6px'>Best fit: <b>"+esc(rec.best)+"</b> — pull it, then pick <b>Local model (Ollama)</b> above.</div></div>";
|
|
880
|
+
h+="<div class='ideas'><h4>Paid models · bring an API key</h4>";
|
|
881
|
+
rec.paid.forEach(function(p){h+="<div class='idea'><span><b>"+esc(p.provider)+"</b> · "+esc(p.tier)+" · <code>"+esc(p.model)+"</code> <span class='muted'>· "+esc(p.note)+"</span></span></div>";});
|
|
882
|
+
h+="</div>";out.innerHTML=h;});}
|
|
712
883
|
document.getElementById('remap').addEventListener('click',loadMap);
|
|
884
|
+
document.getElementById('recbtn').addEventListener('click',loadRec);
|
|
885
|
+
document.getElementById('addtask').addEventListener('click',addTaskUI);
|
|
886
|
+
document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
|
|
713
887
|
initGraphEvents();syncP();refresh();loadMap();
|
|
714
888
|
</script></body></html>`;
|
|
715
889
|
|
|
@@ -764,8 +938,14 @@ async function cmdApp() {
|
|
|
764
938
|
try {
|
|
765
939
|
if (u.pathname === "/api/status") { const r = resolveProvider(); return json(res, r ? { connected: true, line: `${PROVIDERS[r.provider].label} · ${r.model}` } : { connected: false }); }
|
|
766
940
|
if (u.pathname === "/api/map") return json(res, buildMap()); // local git only — no AI key needed
|
|
941
|
+
if (u.pathname === "/api/models") { const hw = detectHardware(); return json(res, { hardware: hw, rec: recommendModels(hw) }); }
|
|
767
942
|
if (u.pathname === "/api/node") return json(res, nodeDetail(u.searchParams.get("id") || "")); // local
|
|
768
943
|
if (u.pathname === "/api/suggest" && req.method === "POST") { const b = await readBody(req); return json(res, await repoSuggest(String(b.path || ""))); }
|
|
944
|
+
if (u.pathname === "/api/review" && req.method === "POST") { const b = await readBody(req); return json(res, await repoReview(String(b.path || ""))); }
|
|
945
|
+
if (u.pathname === "/api/tasks" && req.method !== "POST") return json(res, loadTasks());
|
|
946
|
+
if (u.pathname === "/api/tasks/add" && req.method === "POST") { const b = await readBody(req); return json(res, addTask(b.text, b.repo)); }
|
|
947
|
+
if (u.pathname === "/api/tasks/toggle" && req.method === "POST") { const b = await readBody(req); return json(res, toggleTask(String(b.id || ""))); }
|
|
948
|
+
if (u.pathname === "/api/tasks/remove" && req.method === "POST") { const b = await readBody(req); return json(res, removeTask(String(b.id || ""))); }
|
|
769
949
|
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)); }
|
|
770
950
|
if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
|
|
771
951
|
if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
|
|
@@ -788,6 +968,7 @@ ${c.b("Usage")}
|
|
|
788
968
|
symbiot standup yesterday + today, for standup
|
|
789
969
|
symbiot todo what's still on your plate
|
|
790
970
|
symbiot app open the visual app in your browser
|
|
971
|
+
symbiot models recommend AI models for your hardware
|
|
791
972
|
symbiot login connect it to an AI (once)
|
|
792
973
|
symbiot whoami show how it's connected
|
|
793
974
|
symbiot logout forget saved credentials
|
|
@@ -816,6 +997,7 @@ ${c.d("with a local Ollama model).")}`;
|
|
|
816
997
|
if (cmd === "logout") return cmdLogout();
|
|
817
998
|
if (cmd === "whoami" || cmd === "status") return cmdWhoami();
|
|
818
999
|
if (cmd === "app" || cmd === "ui") return cmdApp();
|
|
1000
|
+
if (cmd === "models" || cmd === "hardware") return cmdModels();
|
|
819
1001
|
if (cmd === "week") return cmdRun("week");
|
|
820
1002
|
if (cmd === "standup") return cmdRun("standup");
|
|
821
1003
|
if (cmd === "todo") return cmdRun("todo");
|
package/package.json
CHANGED