backpass 0.1.9 → 0.1.10

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 CHANGED
@@ -84,7 +84,7 @@ backpass reads the local transcript stores of seven harnesses directly. No API,
84
84
  | -------------- | ---------------------------------------------- | --------------------------------------------------- |
85
85
  | **claude** | `~/.claude/projects/<munged-cwd>/<uuid>.jsonl` | per-line `cwd` |
86
86
  | **codex** | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` | `cwd` + recorded `git.repository_url` |
87
- | **pi** | `~/.pi/agent/sessions/<escaped-cwd>/*.jsonl` | session-header `cwd` |
87
+ | **pi** | standalone and BB-managed Pi JSONL stores | session-header `cwd` |
88
88
  | **opencode** | `~/.local/share/opencode/opencode.db` (sqlite) | `session.directory` |
89
89
  | **grok** | `~/.grok/sessions/<encoded-cwd>/<uuid>/` | `summary.json` `cwd` + `git_remotes` |
90
90
  | **cursor CLI** | `~/.cursor/chats/<md5(cwd)>/<uuid>/` | `meta.json` `cwd` |
@@ -95,6 +95,12 @@ relocated config dir does not hide its sessions. The variable is read from backp
95
95
  environment: if you reach that profile through an alias that only prefixes `claude`, set it
96
96
  for the backpass run too (`CLAUDE_CONFIG_DIR=~/.claude-work backpass`, or export it).
97
97
 
98
+ Pi collection covers standalone sessions under `~/.pi/agent/sessions/` and BB-managed Pi
99
+ sessions under `~/.bb/pi-bridge-sessions/`. It also honors `PI_CODING_AGENT_DIR`,
100
+ `PI_CODING_AGENT_SESSION_DIR`, `BB_DATA_DIR`, and `BB_PI_BRIDGE_SESSION_DIR` when they are
101
+ set in backpass's environment. When roots overlap, backpass scans every applicable layout
102
+ and reads each JSONL file once.
103
+
98
104
  Hermes collection includes CLI and ACP sessions only. Gateway, cron, and WhatsApp sessions
99
105
  are excluded because their recorded cwd belongs to the shared gateway process, not a project.
100
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backpass",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "description": "Gradient descent for your agent memory - analyzes past agent session transcripts and proposes evidence-backed edits to AGENTS.md / CLAUDE.md",
6
6
  "type": "module",
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
 
3
4
  import {
@@ -13,7 +14,9 @@ import {
13
14
  } from "./shared.js";
14
15
 
15
16
  /**
16
- * pi: ~/.pi/agent/sessions/<escaped-cwd>/<ISO-ts>_<uuid>.jsonl
17
+ * Pi writes standalone sessions under
18
+ * `~/.pi/agent/sessions/<escaped-cwd>/<ISO-ts>_<uuid>.jsonl`. BB's Pi bridge writes the
19
+ * same JSONL shape directly under `<bb-data-dir>/pi-bridge-sessions/`.
17
20
  *
18
21
  * Line 1 is `{type:"session", cwd, id}`. Entries form a parent/child tree but arrive in
19
22
  * order, so a linear read is faithful. `model_change` / `thinking_level_change` records
@@ -26,13 +29,69 @@ export function storeRoot() {
26
29
  return home(".pi", "agent", "sessions");
27
30
  }
28
31
 
32
+ function expandEnvPath(value) {
33
+ const trimmed = value?.trim();
34
+ if (!trimmed) return null;
35
+ if (trimmed === "~") return home();
36
+ if (trimmed.startsWith("~/")) return path.join(home(), trimmed.slice(2));
37
+ return path.resolve(trimmed);
38
+ }
39
+
40
+ function realpathOrResolve(value) {
41
+ try {
42
+ return fs.realpathSync(value);
43
+ } catch {
44
+ return path.resolve(value);
45
+ }
46
+ }
47
+
48
+ function storeSpecs() {
49
+ const specs = [
50
+ { path: storeRoot(), direct: false, nested: true },
51
+ { path: home(".bb", "pi-bridge-sessions"), direct: true, nested: false },
52
+ ];
53
+ const piAgentDir = expandEnvPath(process.env.PI_CODING_AGENT_DIR);
54
+ if (piAgentDir) specs.push({ path: path.join(piAgentDir, "sessions"), direct: false, nested: true });
55
+ const piSessionDir = expandEnvPath(process.env.PI_CODING_AGENT_SESSION_DIR);
56
+ if (piSessionDir) specs.push({ path: piSessionDir, direct: true, nested: false });
57
+ const bbDataDir = expandEnvPath(process.env.BB_DATA_DIR);
58
+ if (bbDataDir) specs.push({ path: path.join(bbDataDir, "pi-bridge-sessions"), direct: true, nested: false });
59
+ const bridgeDir = expandEnvPath(process.env.BB_PI_BRIDGE_SESSION_DIR);
60
+ if (bridgeDir) specs.push({ path: bridgeDir, direct: true, nested: false });
61
+
62
+ const unique = new Map();
63
+ for (const spec of specs) {
64
+ const key = realpathOrResolve(spec.path);
65
+ const existing = unique.get(key);
66
+ if (existing) {
67
+ existing.direct ||= spec.direct;
68
+ existing.nested ||= spec.nested;
69
+ } else {
70
+ unique.set(key, spec);
71
+ }
72
+ }
73
+ return [...unique.values()];
74
+ }
75
+
76
+ export function storeRoots() {
77
+ return storeSpecs().map((spec) => spec.path);
78
+ }
79
+
29
80
  export function enumerate() {
30
81
  const out = [];
31
- for (const dir of listDirs(storeRoot())) {
32
- for (const file of listFiles(dir, ".jsonl")) {
82
+ const seen = new Set();
83
+ for (const spec of storeSpecs()) {
84
+ const files = [
85
+ ...(spec.direct ? listFiles(spec.path, ".jsonl") : []),
86
+ ...(spec.nested ? listDirs(spec.path).flatMap((dir) => listFiles(dir, ".jsonl")) : []),
87
+ ];
88
+ for (const file of files) {
89
+ const key = realpathOrResolve(file);
90
+ if (seen.has(key)) continue;
91
+ seen.add(key);
33
92
  const stat = statOrNull(file);
34
93
  if (!stat) continue;
35
- out.push({ key: file, path: file, mtimeMs: stat.mtimeMs, bytes: stat.size });
94
+ out.push({ key, path: file, mtimeMs: stat.mtimeMs, bytes: stat.size });
36
95
  }
37
96
  }
38
97
  return out;