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.
- package/README.md +3 -0
- package/index.mjs +37 -14
- 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
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
// { "<
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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": {
|