@vibe-cafe/vibe-usage 0.10.20 → 0.10.21

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
@@ -64,7 +64,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
64
64
  | OpenCode | `~/.local/share/opencode/opencode.db` (SQLite, `json_extract` query) |
65
65
  | OpenClaw | `~/.openclaw/agents/`, `~/.openclaw-<profile>/agents/` (profile deployments); cache-creation/cache-write tokens are included in input usage |
66
66
  | Oh My Pi | `~/.omp/agent/sessions/`, `~/.omp/profiles/*/agent/sessions/`, and `$XDG_DATA_HOME/omp/{sessions,profiles/*/sessions}`; recognizes OMP's `$PI_CODING_AGENT_DIR`, current v3 title slots and path/hashed session directories, deduplicates copied records, includes cache writes in input, and splits reasoning from OMP's inclusive output count |
67
- | pi | `~/.pi/agent/sessions/` or `$PI_CODING_AGENT_DIR/sessions/`, plus the session directory Pi itself was pointed at via `PI_CODING_AGENT_SESSION_DIR` or `sessionDir` in `~/.pi/agent/settings.json` (fixture/relocation override: `VIBE_USAGE_PI_SESSION_DIRS`). Cache writes are included in input usage; reasoning is read from Pi's `usage.reasoning` (legacy `usage.reasoningTokens` still accepted) and split out of the inclusive output total |
67
+ | pi | `~/.pi/agent/sessions/` or `$PI_CODING_AGENT_DIR/sessions/`, plus the session directory Pi itself was pointed at via `PI_CODING_AGENT_SESSION_DIR` or `sessionDir` in `~/.pi/agent/settings.json`, plus explicitly added `pi-coding-agent` roots for stores only reachable through `pi --session <file>` (fixture/relocation override: `VIBE_USAGE_PI_SESSION_DIRS`). Cache writes are included in input usage; reasoning is read from Pi's `usage.reasoning` (legacy `usage.reasoningTokens` still accepted) and split out of the inclusive output total |
68
68
  | Qwen Code | `~/.qwen/tmp/` |
69
69
  | Kimi Code | Current `~/.kimi-code/sessions/wd_<slug>_<hash>/session_<id>/agents/<agent>/wire.jsonl` (`usage.record` deltas, including retry/compaction scope and cache creation; main/subagent wires form one session), data root resolved via `$KIMI_CODE_HOME` like the CLI itself, with project names from `session_index.jsonl`; legacy `~/.kimi/sessions/` is parsed alongside (`kimi migrate` never carries usage over, so both stores are always merged) |
70
70
  | MiniMax Code (mcode) | `$MCODE_HOME/v2/sqlite/runtime-state.sqlite` (default `~/.minimax/v2/sqlite/runtime-state.sqlite`; fixture override: `VIBE_USAGE_MCODE_DB`). Reads only allow-listed token ledger fields and session workspace/project paths, uses basename-only projects, folds cache writes into input, keeps cache reads and reasoning separate, and never selects raw/message JSON payloads. WAL/lock reads use a disposable snapshot; malformed or incompatible databases are skipped to preserve incremental state. |
@@ -158,7 +158,7 @@ Config stored at `~/.vibe-usage/config.json` (dev: `config.dev.json`).
158
158
  | `apiUrl` | Server URL (default: `https://vibecafe.ai`) |
159
159
  | `hostname` | Stable device name for usage tracking (set at init, reused across syncs) |
160
160
  | `codexExtraHome` | Optional additional Codex Home scanned together with `$CODEX_HOME` / `~/.codex` |
161
- | `extraRoots` | Tool-specific additional roots managed by the commands below; currently supports `codex`, `grok`, and `antigravity` |
161
+ | `extraRoots` | Tool-specific additional roots managed by the commands below; currently supports `codex`, `grok`, `antigravity`, and `pi-coding-agent` |
162
162
 
163
163
  The `hostname` is captured once during `init` and reused for all future syncs. This prevents macOS mDNS hostname changes (e.g., `MacBook-Pro` → `MacBook-Pro-2`) from creating duplicate device entries. To change it manually:
164
164
 
@@ -179,6 +179,12 @@ npx @vibe-cafe/vibe-usage config add-root grok /path/to/grok-home
179
179
  # Antigravity expects an alternate HOME containing .gemini/antigravity*/conversations/.
180
180
  npx @vibe-cafe/vibe-usage config add-root antigravity /path/to/alternate-home
181
181
 
182
+ # Pi accepts a directory that holds session .jsonl files directly, or a Pi
183
+ # agent directory containing sessions/. Use this when a harness starts Pi with
184
+ # `pi --session <file>`: that path is recorded nowhere Pi's own settings can
185
+ # report, so it is invisible to discovery.
186
+ npx @vibe-cafe/vibe-usage config add-root pi-coding-agent /path/to/pi-sessions
187
+
182
188
  npx @vibe-cafe/vibe-usage config roots
183
189
  npx @vibe-cafe/vibe-usage config remove-root grok /path/to/grok-home
184
190
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.10.20",
3
+ "version": "0.10.21",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,9 +1,17 @@
1
- import { accessSync, constants, readdirSync, statSync } from 'node:fs';
1
+ import { accessSync, closeSync, constants, openSync, readSync, readdirSync, statSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { basename, join, resolve } from 'node:path';
4
4
  import { codexSessionDirs } from './codex-roots.js';
5
5
 
6
- export const EXTRA_ROOT_SOURCES = ['antigravity', 'codex', 'grok'];
6
+ export const EXTRA_ROOT_SOURCES = ['antigravity', 'codex', 'grok', 'pi-coding-agent'];
7
+
8
+ // Probing a candidate Pi store has three outcomes, never two: a confirmed
9
+ // session, a directory proven to hold none, and one that could not be read.
10
+ // Collapsing the last two into a single false is what let an unreadable subtree
11
+ // be treated as an empty one.
12
+ const PI_SESSIONS_FOUND = 'found';
13
+ const PI_SESSIONS_ABSENT = 'absent';
14
+ const PI_SESSIONS_UNREADABLE = 'unreadable';
7
15
 
8
16
  export function extraRootList(value) {
9
17
  return Array.isArray(value) ? value.filter(root => typeof root === 'string' && root.trim()) : [];
@@ -81,6 +89,194 @@ export function antigravityConversationDirs(value) {
81
89
  ];
82
90
  }
83
91
 
92
+ // Pi's own discoverable settings (PI_CODING_AGENT_SESSION_DIR, `sessionDir` in
93
+ // settings.json) name a sessions directory directly, and a harness that calls
94
+ // `pi --session <file>` writes bare session trees with no agent home above
95
+ // them. Accept either shape, but resolve to exactly one directory per root:
96
+ // the parser walks nested directories, so returning both a root and its
97
+ // `sessions/` child would read every file twice. Canonical-path dedup in the
98
+ // parser makes the overlap harmless even for a mixed root that holds sessions
99
+ // directly *and* under `sessions/`.
100
+ //
101
+ // Every candidate is confirmed by content, never by name alone. A readable but
102
+ // unconfirmed `sessions/` child used to win unconditionally, so creating an
103
+ // empty `<root>/sessions` was enough to redirect the scan away from a bare
104
+ // store's own files — a successful sync reporting zero, which then pruned the
105
+ // source's incremental state.
106
+ //
107
+ // The shape is re-resolved on every run rather than remembered from add-root
108
+ // time, and an unresolvable root returns null instead of falling back to the
109
+ // root itself. Falling back would turn an agent home that lost its `sessions/`
110
+ // child into a readable directory holding no sessions, i.e. exactly the silent
111
+ // zero this feature exists to prevent.
112
+ //
113
+ // Probing is tri-state on purpose. A boolean collapsed "holds no sessions" into
114
+ // "could not be read", so making one sibling store unreadable was enough to
115
+ // make a container look like an agent home and narrow the scan to `sessions/`,
116
+ // dropping every readable sibling with no `skipped` flag. Absence has to be
117
+ // proven; where it is only assumed, resolution gives up and the caller skips.
118
+ export function piSessionsDir(value) {
119
+ const root = normalizeExtraRoot(value);
120
+ if (!isReadableDirectory(root)) return null;
121
+ // A bare store is identified by the session files it holds directly, and
122
+ // outranks any `sessions/` child: those files are what the user configured.
123
+ const direct = probePiSessions(root, 0);
124
+ if (direct === PI_SESSIONS_FOUND) return root;
125
+
126
+ const outside = probePiSessionsOutsideNested(root);
127
+ // Both decisions below rest on absence: that the root holds no sessions
128
+ // directly, and that no sibling of `sessions/` holds any. An unreadable
129
+ // candidate proves neither, so stop rather than narrow past it.
130
+ if (direct === PI_SESSIONS_UNREADABLE || outside === PI_SESSIONS_UNREADABLE) return null;
131
+
132
+ const nested = join(root, 'sessions');
133
+ // Agent-home shape: narrowing to the child is only safe when every confirmed
134
+ // session lives below it. A container whose per-task stores happen to include
135
+ // one named `sessions` is still a container, and resolving it to that child
136
+ // would drop all its siblings — the same silent undercount as above.
137
+ if (
138
+ outside === PI_SESSIONS_ABSENT
139
+ && isReadableDirectory(nested)
140
+ && probePiSessions(nested) === PI_SESSIONS_FOUND
141
+ ) {
142
+ return nested;
143
+ }
144
+ // A configured container holding per-task stores somewhere below it. Anything
145
+ // else — no sessions at all, or a subtree that could not be read — resolves
146
+ // to null, which the parser reports as skipped instead of as an empty sync.
147
+ return probePiSessions(root) === PI_SESSIONS_FOUND ? root : null;
148
+ }
149
+
150
+ // Confirmed sessions in some child other than `sessions/`. The per-child depth
151
+ // is one less than the root scan's own so both reach the same files.
152
+ function probePiSessionsOutsideNested(root) {
153
+ let children;
154
+ try {
155
+ children = readdirSync(root, { withFileTypes: true });
156
+ } catch {
157
+ return PI_SESSIONS_UNREADABLE;
158
+ }
159
+ let unreadable = false;
160
+ for (const child of children) {
161
+ if (!child.isDirectory() || child.name === 'sessions') continue;
162
+ const probe = probePiSessions(join(root, child.name), 1);
163
+ if (probe === PI_SESSIONS_FOUND) return PI_SESSIONS_FOUND;
164
+ if (probe === PI_SESSIONS_UNREADABLE) unreadable = true;
165
+ }
166
+ return unreadable ? PI_SESSIONS_UNREADABLE : PI_SESSIONS_ABSENT;
167
+ }
168
+
169
+ const PI_PROBE_BYTES = 16 * 1024;
170
+ const PI_PROBE_LINES = 10;
171
+ // Roles the Pi parser turns into events; anything else contributes nothing.
172
+ const PI_MESSAGE_ROLES = new Set(['user', 'assistant', 'toolResult']);
173
+
174
+ function isNonEmptyString(value) {
175
+ return typeof value === 'string' && value.trim() !== '';
176
+ }
177
+
178
+ // Pi's session format opens a store with a full SessionHeader. `version` is
179
+ // written as both a number and a string across real stores, so only its
180
+ // presence is required.
181
+ function isPiSessionHeader(obj) {
182
+ return obj.type === 'session'
183
+ && obj.version !== undefined
184
+ && obj.version !== null
185
+ && isNonEmptyString(obj.id)
186
+ && isNonEmptyString(obj.timestamp)
187
+ && typeof obj.cwd === 'string';
188
+ }
189
+
190
+ // A store an external harness appends to may carry no header inside the probed
191
+ // prefix, so a message record alone can confirm the directory — but only a
192
+ // complete one. Matching on `type` and an object-valued `message` accepted
193
+ // `{"type":"message","message":{}}`, which the parser reads to exactly zero.
194
+ function isPiSessionMessage(obj) {
195
+ return obj.type === 'message'
196
+ && isNonEmptyString(obj.id)
197
+ && 'parentId' in obj
198
+ && isNonEmptyString(obj.timestamp)
199
+ && Boolean(obj.message)
200
+ && typeof obj.message === 'object'
201
+ && PI_MESSAGE_ROLES.has(obj.message.role);
202
+ }
203
+
204
+ // A `.jsonl` extension proves nothing: an unrelated log would validate an
205
+ // entirely wrong directory, which the parser then ignores without complaining.
206
+ // Neither does a bare `type` name — validation has to require the fields the
207
+ // parser reads, or a malformed lookalike is accepted and still syncs zero.
208
+ // A file that cannot be opened is reported as unreadable, not as "not a
209
+ // session": it may well be the store the user configured.
210
+ function probePiSessionFile(filePath) {
211
+ let text;
212
+ let fd;
213
+ try {
214
+ fd = openSync(filePath, 'r');
215
+ const buffer = Buffer.alloc(PI_PROBE_BYTES);
216
+ const read = readSync(fd, buffer, 0, PI_PROBE_BYTES, 0);
217
+ text = buffer.subarray(0, read).toString('utf8');
218
+ // A prefix read can cut the final line in half; drop the partial tail.
219
+ if (read === PI_PROBE_BYTES) text = text.slice(0, text.lastIndexOf('\n') + 1);
220
+ } catch {
221
+ return PI_SESSIONS_UNREADABLE;
222
+ } finally {
223
+ if (fd !== undefined) {
224
+ try {
225
+ closeSync(fd);
226
+ } catch { /* already closed */ }
227
+ }
228
+ }
229
+
230
+ let checked = 0;
231
+ for (const line of text.split('\n')) {
232
+ if (!line.trim()) continue;
233
+ if (++checked > PI_PROBE_LINES) return PI_SESSIONS_ABSENT;
234
+ let obj;
235
+ try {
236
+ obj = JSON.parse(line);
237
+ } catch {
238
+ continue;
239
+ }
240
+ if (!obj || typeof obj !== 'object') continue;
241
+ if (isPiSessionHeader(obj) || isPiSessionMessage(obj)) return PI_SESSIONS_FOUND;
242
+ }
243
+ return PI_SESSIONS_ABSENT;
244
+ }
245
+
246
+ // A session store is only recognizable by the Pi session files in it. Stay
247
+ // shallow: a configured root may sit next to large unrelated trees.
248
+ //
249
+ // `found` outranks `unreadable`: one confirmed session is enough to resolve the
250
+ // shape, and the shared parser reports whatever it cannot read on the way in.
251
+ // `unreadable` outranks `absent`, so a caller never mistakes a subtree it could
252
+ // not open for one it proved empty.
253
+ function probePiSessions(dir, depth = 2) {
254
+ let children;
255
+ try {
256
+ children = readdirSync(dir, { withFileTypes: true });
257
+ } catch {
258
+ return PI_SESSIONS_UNREADABLE;
259
+ }
260
+ let unreadable = false;
261
+ // Dirent#isDirectory is false for symbolic links, matching the parser's own
262
+ // walk: linked session files are read, linked directories are not entered.
263
+ for (const child of children) {
264
+ if (child.isDirectory() || !child.name.endsWith('.jsonl')) continue;
265
+ const probe = probePiSessionFile(join(dir, child.name));
266
+ if (probe === PI_SESSIONS_FOUND) return PI_SESSIONS_FOUND;
267
+ if (probe === PI_SESSIONS_UNREADABLE) unreadable = true;
268
+ }
269
+ if (depth > 0) {
270
+ for (const child of children) {
271
+ if (!child.isDirectory()) continue;
272
+ const probe = probePiSessions(join(dir, child.name), depth - 1);
273
+ if (probe === PI_SESSIONS_FOUND) return PI_SESSIONS_FOUND;
274
+ if (probe === PI_SESSIONS_UNREADABLE) unreadable = true;
275
+ }
276
+ }
277
+ return unreadable ? PI_SESSIONS_UNREADABLE : PI_SESSIONS_ABSENT;
278
+ }
279
+
84
280
  export function validateExtraRoot(source, value) {
85
281
  if (!EXTRA_ROOT_SOURCES.includes(source)) {
86
282
  return { ok: false, path: value, reason: `不支持的工具: ${source}` };
@@ -94,6 +290,15 @@ export function validateExtraRoot(source, value) {
94
290
  reason: '需要是 Codex Home,或包含 */*/codex-home 的 Multica 容器',
95
291
  };
96
292
  }
293
+ if (source === 'pi-coding-agent') {
294
+ // piSessionsDir only returns a directory it has already confirmed by
295
+ // content, so there is nothing left to re-check here.
296
+ return {
297
+ ok: piSessionsDir(path) !== null,
298
+ path,
299
+ reason: '需要是直接包含 Pi 会话 .jsonl 的目录,或包含 sessions/ 的 Pi agent 目录',
300
+ };
301
+ }
97
302
  const dirs = source === 'grok'
98
303
  ? [grokSessionsDir(path)]
99
304
  : antigravityConversationDirs(path);
package/src/index.js CHANGED
@@ -121,7 +121,7 @@ function handleConfig(args) {
121
121
  const source = args[1];
122
122
  const value = args[2];
123
123
  if (!source || value === undefined) {
124
- console.error('Usage: vibe-usage config add-root <codex|grok|antigravity> <path>');
124
+ console.error(`Usage: vibe-usage config add-root <${EXTRA_ROOT_SOURCES.join('|')}> <path>`);
125
125
  process.exit(1);
126
126
  }
127
127
  const validation = validateExtraRoot(source, value);
@@ -142,7 +142,7 @@ function handleConfig(args) {
142
142
  const source = args[1];
143
143
  const value = args[2];
144
144
  if (!EXTRA_ROOT_SOURCES.includes(source) || value === undefined) {
145
- console.error('Usage: vibe-usage config remove-root <codex|grok|antigravity> <path>');
145
+ console.error(`Usage: vibe-usage config remove-root <${EXTRA_ROOT_SOURCES.join('|')}> <path>`);
146
146
  process.exit(1);
147
147
  }
148
148
  const config = loadConfig() || {};
@@ -297,7 +297,7 @@ export async function run(rawArgs) {
297
297
  npx @vibe-cafe/vibe-usage config get <key> Get a config value
298
298
  npx @vibe-cafe/vibe-usage config set <key> <value> Set a config value
299
299
  npx @vibe-cafe/vibe-usage config set codexExtraHome <path> Persist another Codex Home
300
- npx @vibe-cafe/vibe-usage config add-root <tool> <path> Add a Codex, Grok, or Antigravity data root
300
+ npx @vibe-cafe/vibe-usage config add-root <tool> <path> Add a Codex, Grok, Antigravity, or Pi data root
301
301
  npx @vibe-cafe/vibe-usage config remove-root <tool> <path> Remove an added data root
302
302
  npx @vibe-cafe/vibe-usage config roots Show added data roots as JSON
303
303
  npx @vibe-cafe/vibe-usage help Show this help
@@ -1,12 +1,27 @@
1
+ import { normalizeExtraRoot, piSessionsDir } from '../extra-roots.js';
1
2
  import { getPiSessionDirs } from '../pi-roots.js';
2
3
  import { mergeCindyHarnessUsage, readCindyHarnessUsage } from './cindy-ledger.js';
3
4
  import { parsePiSessionJsonl } from './pi-session-jsonl.js';
4
5
 
5
6
  /** Parse the official Pi agent's Pi-compatible JSONL sessions. */
6
- export async function parse() {
7
+ export async function parse({ extraRoots = [] } = {}) {
8
+ for (const root of extraRoots) {
9
+ // piSessionsDir re-resolves the root's shape, so an agent home that lost
10
+ // its `sessions/` child is caught here too, not just a root that vanished.
11
+ if (piSessionsDir(root) !== null) continue;
12
+ // An explicitly configured root that is momentarily unreadable is not
13
+ // proof that its usage disappeared, so skip instead of reporting empty.
14
+ return {
15
+ buckets: [],
16
+ sessions: [],
17
+ skipped: true,
18
+ warnings: [`pi-coding-agent: 额外根目录不可用,已跳过本次 Pi 同步: ${normalizeExtraRoot(root)}`],
19
+ };
20
+ }
21
+
7
22
  const nativeResult = await parsePiSessionJsonl({
8
23
  source: 'pi-coding-agent',
9
- sessionsDirs: getPiSessionDirs(),
24
+ sessionsDirs: getPiSessionDirs(extraRoots),
10
25
  });
11
26
  return mergeCindyHarnessUsage(nativeResult, readCindyHarnessUsage('pi'));
12
27
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
2
2
  import { basename, join, relative } from 'node:path';
3
3
  import { aggregateToBuckets, extractSessions } from './aggregate.js';
4
4
  import { projectFromCwd, toCount } from './fs-utils.js';
@@ -38,6 +38,19 @@ export function projectFromFirstDir(filePath, sessionsDir) {
38
38
  return first.split('-').filter(Boolean).at(-1) || 'unknown';
39
39
  }
40
40
 
41
+ // Configured stores can overlap: an ancestor and its descendant, or two paths
42
+ // that resolve to the same place through a symlink. Record-level dedup only
43
+ // covers entries carrying an `id`, so the same anonymous record would be
44
+ // counted once per path that reaches it. Collapse on the canonical file path
45
+ // instead, which also folds symlinked duplicates of a single file.
46
+ function canonicalFilePath(filePath) {
47
+ try {
48
+ return realpathSync.native(filePath);
49
+ } catch {
50
+ return filePath;
51
+ }
52
+ }
53
+
41
54
  export async function parsePiSessionJsonl({
42
55
  source,
43
56
  sessionsDirs,
@@ -49,9 +62,14 @@ export async function parsePiSessionJsonl({
49
62
  const anonymousEntries = [];
50
63
  const eventsById = new Map();
51
64
  const anonymousEvents = [];
65
+ const seenFiles = new Set();
52
66
 
53
67
  for (const sessionsDir of sessionsDirs) {
54
68
  for (const filePath of findJsonlFiles(sessionsDir, includeFile, ctx)) {
69
+ const canonical = canonicalFilePath(filePath);
70
+ if (seenFiles.has(canonical)) continue;
71
+ seenFiles.add(canonical);
72
+
55
73
  let content;
56
74
  try {
57
75
  content = readFileSync(filePath, 'utf8');
package/src/pi-roots.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { delimiter, isAbsolute, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
+ import { piSessionsDir } from './extra-roots.js';
4
5
 
5
6
  function expandHome(value) {
6
7
  const trimmed = value.trim();
@@ -60,22 +61,30 @@ function settingsSessionDir(agentDir) {
60
61
  }
61
62
  }
62
63
 
63
- export function getPiSessionDirs() {
64
+ export function getPiSessionDirs(extraRoots = []) {
65
+ // Explicitly configured roots are user intent, not a fixture: they are always
66
+ // scanned, and they never replace the default store. A root whose shape no
67
+ // longer resolves drops out here; the parser reports that as skipped.
68
+ const extraDirs = extraRoots.map(piSessionsDir).filter(dir => dir !== null);
69
+
64
70
  const override = process.env.VIBE_USAGE_PI_SESSION_DIRS?.trim();
65
- if (override) return uniqueExistingDirs(override.split(delimiter));
71
+ if (override) return uniqueExistingDirs([...override.split(delimiter), ...extraDirs]);
66
72
 
67
73
  const envAgentDir = process.env.PI_CODING_AGENT_DIR?.trim();
68
74
  const agentDir = envAgentDir ? expandHome(envAgentDir) : join(homedir(), '.pi', 'agent');
69
75
  // OMP inherits PI_CODING_AGENT_DIR from Pi. Do not parse an identifiable
70
76
  // OMP store again as source=pi-coding-agent.
71
- if (envAgentDir && looksLikeOmpAgentDir(agentDir)) return [];
77
+ const isOmpStore = Boolean(envAgentDir) && looksLikeOmpAgentDir(agentDir);
72
78
 
73
- const dirs = [join(agentDir, 'sessions')];
74
- const envSessionDir = process.env.PI_CODING_AGENT_SESSION_DIR?.trim();
75
- if (envSessionDir) dirs.push(expandHome(envSessionDir));
76
- const configured = settingsSessionDir(agentDir);
77
- if (configured) dirs.push(configured);
78
- return uniqueExistingDirs(dirs);
79
+ const dirs = [];
80
+ if (!isOmpStore) {
81
+ dirs.push(join(agentDir, 'sessions'));
82
+ const envSessionDir = process.env.PI_CODING_AGENT_SESSION_DIR?.trim();
83
+ if (envSessionDir) dirs.push(expandHome(envSessionDir));
84
+ const configured = settingsSessionDir(agentDir);
85
+ if (configured) dirs.push(configured);
86
+ }
87
+ return uniqueExistingDirs([...dirs, ...extraDirs]);
79
88
  }
80
89
 
81
90
  export function getOmpSessionDirs() {
package/src/tools.js CHANGED
@@ -320,7 +320,9 @@ export const TOOLS = [
320
320
  name: 'pi',
321
321
  id: 'pi-coding-agent',
322
322
  dataDir: join(homedir(), '.pi', 'agent', 'sessions'),
323
- detectDataDirs: findPiDataDirs,
323
+ detectDataDirs: ({ extraRoots } = {}) => (
324
+ findPiDataDirs(extraRootList(extraRoots?.['pi-coding-agent']))
325
+ ),
324
326
  },
325
327
  {
326
328
  name: 'Qwen Code',