symbiot 0.20.0 → 0.22.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.
Files changed (2) hide show
  1. package/index.mjs +56 -12
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -593,21 +593,57 @@ function recommendModels(hw) {
593
593
  // claude -p "{prompt}" · aider --message "{prompt}" · code {dir}
594
594
  // gnome-terminal --working-directory={dir} -- claude "{prompt}"
595
595
  // Not tied to any one tool — you decide what runs.
596
- const IDE_LIST = [["code", "VS Code"], ["cursor", "Cursor"], ["windsurf", "Windsurf"], ["zed", "Zed"], ["subl", "Sublime Text"], ["idea", "IntelliJ IDEA"], ["nvim", "Neovim"]];
596
+ // [cmd, label, macAppName] — macApp used to launch GUI editors on macOS where
597
+ // the CLI isn't on PATH (they're .app bundles).
598
+ const IDE_LIST = [["code", "VS Code", "Visual Studio Code"], ["cursor", "Cursor", "Cursor"], ["windsurf", "Windsurf", "Windsurf"], ["zed", "Zed", "Zed"], ["subl", "Sublime Text", "Sublime Text"], ["idea", "IntelliJ IDEA", "IntelliJ IDEA"], ["nvim", "Neovim", ""]];
597
599
  const AGENT_LIST = [
598
600
  ["claude", "Claude Code — make changes", 'claude -p "{prompt}" --permission-mode acceptEdits'],
599
601
  ["claude", "Claude Code — plan only (asks first)", 'claude -p "{prompt}"'],
602
+ ["codex", "Codex (OpenAI) — make changes", 'codex exec --full-auto "{prompt}"'],
600
603
  ["aider", "Aider — make changes", 'aider --message "{prompt}" --yes'],
601
604
  ["cursor-agent", "Cursor agent", 'cursor-agent -p "{prompt}"'],
605
+ ["gemini", "Gemini CLI", 'gemini -p "{prompt}"'],
602
606
  ];
607
+ // Cross-platform "is this command available?" (command -v on posix, where on win).
608
+ function hasCmd(cmd) { try { return !!sh(process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`).trim(); } catch { return false; } }
609
+ function macApp(name) { if (process.platform !== "darwin" || !name) return ""; for (const base of ["/Applications", join(homedir(), "Applications")]) { try { if (existsSync(join(base, name + ".app"))) return name; } catch {} } return ""; }
610
+ // Find the Orca IDE CLI across OSes (known locations, then a bounded search).
611
+ let ORCA_CLI; // cached per process: undefined=unchecked, ""=none, string=path
612
+ function findOrcaCli() {
613
+ if (ORCA_CLI !== undefined) return ORCA_CLI;
614
+ ORCA_CLI = _findOrcaCli();
615
+ return ORCA_CLI;
616
+ }
617
+ function _findOrcaCli() {
618
+ const home = homedir(); const cands = [];
619
+ if (process.platform === "linux") cands.push(join(home, ".local/share/orca-ide/app/resources/bin/orca-ide"));
620
+ if (process.platform === "darwin") { cands.push(join(home, "Library/Application Support/orca-ide/app/resources/bin/orca-ide"), "/Applications/Orca.app/Contents/Resources/app/resources/bin/orca-ide", join(home, "Applications/Orca.app/Contents/Resources/app/resources/bin/orca-ide")); }
621
+ if (process.platform === "win32") { const la = process.env.LOCALAPPDATA || ""; cands.push(join(la, "orca-ide", "app", "resources", "bin", "orca-ide"), join(la, "Programs", "orca-ide", "resources", "app", "resources", "bin", "orca-ide")); }
622
+ for (const c of cands) { try { if (existsSync(c)) return c; } catch {} }
623
+ const roots = process.platform === "darwin" ? [join(home, "Library/Application Support"), "/Applications", join(home, "Applications")]
624
+ : process.platform === "win32" ? [process.env.LOCALAPPDATA || "", process.env.PROGRAMFILES || ""]
625
+ : [join(home, ".local/share"), "/opt", join(home, ".config")];
626
+ for (const r of roots) { if (!r) continue; const hit = sh(`find ${JSON.stringify(r)} -maxdepth 6 -name orca-ide -type f 2>/dev/null | head -1`).trim(); if (hit) return hit; }
627
+ return "";
628
+ }
603
629
  function detectHandoffs() {
604
- const editors = IDE_LIST.filter(([cmd]) => sh(`command -v ${cmd} 2>/dev/null`).trim()).map(([cmd, label]) => ({ label, tmpl: `${cmd} {dir}`, kind: "editor" }));
605
- const agents = AGENT_LIST.filter(([cmd]) => sh(`command -v ${cmd} 2>/dev/null`).trim()).map(([cmd, label, tmpl]) => ({ label, tmpl, kind: "agent" }));
606
- // Orca IDE: its own CLI (not the ~/.local/bin GPU wrapper, which ignores args).
607
- // Registers the repo, then opens a terminal tab running the agent — Orca shows
608
- // the live session itself.
609
- const orca = join(homedir(), ".local/share/orca-ide/app/resources/bin/orca-ide");
610
- 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" });
630
+ const editors = [];
631
+ for (const [cmd, label, app] of IDE_LIST) {
632
+ if (hasCmd(cmd)) editors.push({ label, tmpl: `${cmd} {dir}`, kind: "editor" });
633
+ else { const a = macApp(app); if (a) editors.push({ label, tmpl: `open -a ${JSON.stringify(a)} {dir}`, kind: "editor" }); }
634
+ }
635
+ const agents = AGENT_LIST.filter(([cmd]) => hasCmd(cmd)).map(([cmd, label, tmpl]) => ({ label, tmpl, kind: "agent" }));
636
+ // Orca IDE (any OS): register the repo + open a terminal tab. Two variants —
637
+ // one that just opens the repo (use Orca's own agent, e.g. GPT), and one that
638
+ // runs Claude in the tab for Claude users. Not Claude-only.
639
+ const orca = findOrcaCli();
640
+ if (orca) {
641
+ const q = JSON.stringify(orca);
642
+ agents.unshift(
643
+ { label: "Orca IDE — open repo (use your Orca agent)", tmpl: `${q} repo add --path {dir}; ${q} terminal create --worktree path:{dir} --focus`, kind: "agent" },
644
+ { label: "Orca IDE — run Claude in a tab", tmpl: `${q} repo add --path {dir}; ${q} terminal create --worktree path:{dir} --command "claude {prompt}" --focus`, kind: "agent" },
645
+ );
646
+ }
611
647
  return { agents, editors };
612
648
  }
613
649
  const shSingle = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
@@ -1049,10 +1085,16 @@ async function repoSuggest(path) {
1049
1085
 
1050
1086
  // Build a write-up for a command; returns { text, sub, error? } without printing.
1051
1087
  // Shared by the CLI (cmdRun) and the web UI (symbiot app).
1088
+ // The SAME repo set the Map uses — reuse its cached scan when present, else do
1089
+ // the identical discovery. So week/standup/todo/drift all agree with the Map.
1090
+ function discoveredRepos() {
1091
+ if (LAST_MAP && LAST_MAP.nodes) return LAST_MAP.nodes.filter((n) => n.type === "repo" && n.meta && n.meta.path).map((n) => ({ path: n.meta.path, name: n.label, recency: 0 }));
1092
+ return findAllRepos();
1093
+ }
1052
1094
  async function produce(cmd) {
1053
1095
  if (!resolveProvider()) return { error: "not-connected" };
1054
1096
  if (cmd === "todo") {
1055
- const repos = findRepos(BASE, 60);
1097
+ const repos = discoveredRepos();
1056
1098
  const open = openWork(repos);
1057
1099
  if (!open.length) return { text: "Nothing outstanding found (no TODOs or uncommitted work).", sub: "0 open items · todo" };
1058
1100
  const system =
@@ -1065,8 +1107,10 @@ async function produce(cmd) {
1065
1107
  const label = cmd === "standup" ? "standup" : "week";
1066
1108
  const days = cmd === "standup" ? 2 : SINCE_WEEK;
1067
1109
  const who = me();
1068
- const repos = findRepos(BASE, days);
1069
- if (!repos.length) return { text: `No git activity in the last ${days} days under ${BASE}.\nPoint it at your repos with --dir.`, sub: "no activity" };
1110
+ // Discover the Map's repo set, then keep only those with commits in the window.
1111
+ const all = discoveredRepos();
1112
+ const repos = all.filter((r) => sh(`git -C ${JSON.stringify(r.path)} log --since="${days} days ago" --oneline -1 2>/dev/null`).trim());
1113
+ if (!repos.length) return { text: `No commits in the last ${days} days across your ${all.length} repos.\nAdd folders to scan in Settings, or check your git identity.`, sub: "no activity" };
1070
1114
  let cs = commits(repos, `${days} days ago`, !has("all"));
1071
1115
  if (!cs.length) cs = commits(repos, `${days} days ago`, false); // fall back to all if none matched you
1072
1116
  const open = label === "week" ? openWork(repos) : [];
@@ -1085,7 +1129,7 @@ async function produce(cmd) {
1085
1129
  `Write the ${label === "standup" ? "standup" : "update"}.`;
1086
1130
 
1087
1131
  const text = await write(system, prompt);
1088
- return { text: text || "(couldn't reach the model)", sub: `${cs.length} commits across ${new Set(cs.map((x) => x.repo)).size} repos · ${label}`, footer: `symbiot ${VERSION} · ${repos.length} repos scanned · ${cs.length} commits (yours) in last ${days}d` };
1132
+ return { text: text || "(couldn't reach the model)", sub: `${cs.length} commits across ${new Set(cs.map((x) => x.repo)).size} repos · ${label}`, footer: `symbiot ${VERSION} · ${all.length} repos (same as the Map) · ${repos.length} active · ${cs.length} commits in last ${days}d` };
1089
1133
  }
1090
1134
 
1091
1135
  async function cmdRun(cmd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.20.0",
3
+ "version": "0.22.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": {