symbiot 0.40.2 → 0.40.3

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 (3) hide show
  1. package/README.md +3 -0
  2. package/index.mjs +37 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -136,6 +136,9 @@ symbiot app # opens in your phone's browser
136
136
  `proot-distro`. To reach
137
137
  the phone's shared storage, run `termux-setup-storage` and add `~/storage/shared`
138
138
  (or a folder in it) under Settings → Folders to scan.
139
+ - Claude Code's history is read from Termux's home and from those Linuxes'
140
+ homes, so projects your agent worked on inside `proot-distro login debian` get
141
+ their "agent" badge on the Map, as they do on a computer.
139
142
  - **Write my week and notify me** sends an Android notification when the
140
143
  `termux-api` package and the Termux:API app are installed.
141
144
  - **Start Symbiot in the background when the phone starts** writes a
package/index.mjs CHANGED
@@ -732,16 +732,22 @@ function storageBlocked() {
732
732
  // home folders in $PREFIX/var/lib/proot-distro/installed-rootfs/<distro>, outside
733
733
  // Termux's home, and that's where projects worked on in it live. Symbiot in
734
734
  // Termux reads them there: /root and /home/<user> of each installed distro.
735
- function prootHomes() {
735
+ const subdirs = (p) => { try { return readdirSync(p, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join(p, d.name)); } catch { return []; } };
736
+ // The folder each installed distro sees as "/".
737
+ function prootDistros() {
736
738
  if (!process.env.PREFIX) return [];
737
- const base = join(process.env.PREFIX, "var", "lib", "proot-distro", "installed-rootfs"), out = [];
738
- const dirs = (p) => { try { return readdirSync(p, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join(p, d.name)); } catch { return []; } };
739
- for (const distro of dirs(base)) {
740
- if (existsSync(join(distro, "root"))) out.push(join(distro, "root"));
741
- out.push(...dirs(join(distro, "home")));
739
+ return subdirs(join(process.env.PREFIX, "var", "lib", "proot-distro", "installed-rootfs"));
740
+ }
741
+ // Each distro's home folders, with the distro they're in.
742
+ function prootHomeDirs() {
743
+ const out = [];
744
+ for (const rootfs of prootDistros()) {
745
+ if (existsSync(join(rootfs, "root"))) out.push({ rootfs, home: join(rootfs, "root") });
746
+ for (const home of subdirs(join(rootfs, "home"))) out.push({ rootfs, home });
742
747
  }
743
748
  return out;
744
749
  }
750
+ function prootHomes() { return prootHomeDirs().map((h) => h.home); }
745
751
  // Where to look when you haven't picked folders: your home folder (and in Termux,
746
752
  // the homes of its proot-distro Linuxes).
747
753
  function scanDefaults() { return [scanHome(), ...prootHomes()]; }
@@ -1118,9 +1124,13 @@ function detectFolder(path) {
1118
1124
  // Projects you've run an AI coding agent on. Claude Code stores one dir per
1119
1125
  // project under ~/.claude/projects, named by the project path with every
1120
1126
  // non-alphanumeric char → "-" (so "/" "." "_" "-" all collide). Returns
1121
- // { "<encoded path>": { agent, last, path } } — last = newest session mtime
1127
+ // { "<key>": { agent, last, path, rootfs, enc } } — last = newest session mtime
1122
1128
  // (epoch secs); path = the real project dir, read from the "cwd" a session
1123
- // records (empty when no session says, then callers fall back to decoding).
1129
+ // records (empty when no session says, then callers fall back to decoding enc).
1130
+ // In Termux it also reads Claude Code's history inside each proot-distro (in
1131
+ // the distro's /root and /home/<user>). Paths there are the distro's own
1132
+ // ("/root/work/proj"), so rootfs is the distro's folder ("" outside one), path
1133
+ // is prefixed with it, and key = agentKey(rootfs, enc).
1124
1134
  // (Gemini's dir is global config, Codex's is empty, so no reliable per-project
1125
1135
  // signal there yet; add them here when there is.)
1126
1136
  const claudeEnc = (p) => String(p || "").replace(/[^a-zA-Z0-9]/g, "-");
@@ -1130,9 +1140,21 @@ function sessionCwd(file) {
1130
1140
  if (!m) return "";
1131
1141
  try { return JSON.parse(`"${m[1]}"`); } catch { return ""; }
1132
1142
  }
1143
+ const agentKey = (rootfs, enc) => (rootfs ? rootfs + "\0" : "") + enc;
1144
+ // The keys a project folder's history could be under: its path, and its path as
1145
+ // seen from inside the proot-distro it's in (if any), each encoded both ways.
1146
+ function agentKeys(path, distros) {
1147
+ const enc = (p) => [claudeEnc(p), p.replace(/\//g, "-")]; // older Claude Code mapped only "/"
1148
+ const rootfs = distros.find((r) => path.startsWith(r + "/"));
1149
+ return [...enc(path), ...(rootfs ? enc(path.slice(rootfs.length)).map((e) => agentKey(rootfs, e)) : [])];
1150
+ }
1133
1151
  function claudeProjects() {
1134
- const base = join(homedir(), ".claude", "projects");
1135
1152
  const out = {};
1153
+ readClaudeProjects(join(homedir(), ".claude", "projects"), "", out);
1154
+ for (const { rootfs, home } of prootHomeDirs()) readClaudeProjects(join(home, ".claude", "projects"), rootfs, out);
1155
+ return out;
1156
+ }
1157
+ function readClaudeProjects(base, rootfs, out) {
1136
1158
  let dirs = [];
1137
1159
  try { dirs = sh(`ls -1 ${JSON.stringify(base)} 2>/dev/null`).split("\n").filter(Boolean); } catch {}
1138
1160
  for (const enc of dirs) {
@@ -1145,9 +1167,10 @@ function claudeProjects() {
1145
1167
  if (cwd && (claudeEnc(cwd) === enc || cwd.replace(/\//g, "-") === enc)) path = cwd; // only trust a cwd that matches this dir
1146
1168
  }
1147
1169
  if (!last) { try { last = Math.floor(statSync(join(base, enc)).mtimeMs / 1000); } catch {} }
1148
- out[enc] = { agent: "Claude Code", last, path };
1170
+ const key = agentKey(rootfs, enc);
1171
+ if (out[key] && out[key].last >= last) continue; // one project worked on from two of the distro's homes: keep the newest
1172
+ out[key] = { agent: "Claude Code", last, path: path && rootfs + path, rootfs, enc };
1149
1173
  }
1150
- return out;
1151
1174
  }
1152
1175
  // Async only to YIELD between repos, so the app server can answer /api/scan
1153
1176
  // (progress) mid-scan; the git calls themselves stay synchronous. Concurrent
@@ -1212,10 +1235,10 @@ async function buildMapScan() {
1212
1235
  // Agent projects: badge the repo/folder nodes an AI coding agent has worked,
1213
1236
  // and surface agent-worked projects the scan missed as their own nodes.
1214
1237
  try {
1215
- const ap = claudeProjects(); const used = new Set();
1238
+ const ap = claudeProjects(), distros = prootDistros(); const used = new Set();
1216
1239
  for (const n of nodes) {
1217
1240
  if ((n.type === "repo" || n.type === "folder") && n.meta && n.meta.path) {
1218
- const e = [claudeEnc(n.meta.path), n.meta.path.replace(/\//g, "-")].find((k) => ap[k]); // older Claude Code mapped only "/"
1241
+ const e = agentKeys(n.meta.path, distros).find((k) => ap[k]);
1219
1242
  if (e) { n.meta.agents = [{ agent: ap[e].agent, last: ap[e].last }]; used.add(e); }
1220
1243
  }
1221
1244
  }
@@ -1223,7 +1246,7 @@ async function buildMapScan() {
1223
1246
  for (const e of Object.keys(ap)) {
1224
1247
  if (used.has(e) || extra >= 20) continue;
1225
1248
  // the session's recorded cwd; else best-effort decode (names with - . _ won't resolve and are skipped)
1226
- const decoded = ap[e].path || e.replace(/-/g, "/");
1249
+ const decoded = ap[e].path || ap[e].rootfs + ap[e].enc.replace(/-/g, "/");
1227
1250
  if (!existsSync(decoded) || !statSync(decoded).isDirectory()) continue;
1228
1251
  if (nodes.some((n) => n.meta && n.meta.path === decoded)) continue;
1229
1252
  const isGit = existsSync(join(decoded, ".git"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.40.2",
3
+ "version": "0.40.3",
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": {