portable-agent-layer 0.104.0 → 0.104.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.104.0",
3
+ "version": "0.104.1",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,10 +34,12 @@ interface PreserveLocations {
34
34
 
35
35
  type Manifest = Record<string, string>;
36
36
 
37
+ export const transcriptArchive = () => resolve(paths.content(), "claude");
38
+
37
39
  function defaultLocations(): PreserveLocations {
38
40
  return {
39
41
  source: resolve(platform.claudeDir(), "projects"),
40
- target: resolve(paths.content(), "claude"),
42
+ target: transcriptArchive(),
41
43
  manifest: resolve(paths.state(), "transcript-copies.json"),
42
44
  };
43
45
  }
@@ -4,8 +4,8 @@
4
4
  * rather than finding it themselves, which is what makes them testable.
5
5
  */
6
6
 
7
- import { existsSync, readdirSync, readFileSync } from "node:fs";
8
- import { resolve } from "node:path";
7
+ import { type Dirent, existsSync, readdirSync, readFileSync } from "node:fs";
8
+ import { relative, resolve } from "node:path";
9
9
  import type { LoggedEvent } from "../../hooks/lib/events";
10
10
  import { costOfUsage } from "../../hooks/lib/models";
11
11
  import { cacheWritesOf, finalPerMessage, type TranscriptUsage } from "./transcript-usage";
@@ -194,10 +194,18 @@ interface TranscriptLine {
194
194
  * wrote — those sit one level down and are billed to the same project, so
195
195
  * missing them undercounts every session that spawned one.
196
196
  */
197
- function transcriptsIn(projPath: string): string[] {
197
+ function entriesOf(dir: string): Dirent[] {
198
+ try {
199
+ return readdirSync(dir, { withFileTypes: true });
200
+ } catch {
201
+ return [];
202
+ }
203
+ }
204
+
205
+ function transcriptsIn(projPath: string, extension = ".jsonl"): string[] {
198
206
  const files: string[] = [];
199
- for (const entry of readdirSync(projPath, { withFileTypes: true })) {
200
- if (entry.isFile() && entry.name.endsWith(".jsonl")) {
207
+ for (const entry of entriesOf(projPath)) {
208
+ if (entry.isFile() && entry.name.endsWith(extension)) {
201
209
  files.push(resolve(projPath, entry.name));
202
210
  continue;
203
211
  }
@@ -205,7 +213,7 @@ function transcriptsIn(projPath: string): string[] {
205
213
  const subagentsDir = resolve(projPath, entry.name, "subagents");
206
214
  try {
207
215
  for (const sub of readdirSync(subagentsDir)) {
208
- if (sub.endsWith(".jsonl")) files.push(resolve(subagentsDir, sub));
216
+ if (sub.endsWith(extension)) files.push(resolve(subagentsDir, sub));
209
217
  }
210
218
  } catch {
211
219
  /* no subagents dir */
@@ -256,30 +264,64 @@ function pricedCallOf(line: string): PricedCall | null {
256
264
  };
257
265
  }
258
266
 
267
+ const projectDirsIn = (dir: string) =>
268
+ entriesOf(dir)
269
+ .filter((entry) => entry.isDirectory())
270
+ .map((entry) => entry.name);
271
+
272
+ const GZIP = ".gz";
273
+
274
+ /** Claude Code deletes a transcript after cleanupPeriodDays; PAL's gzipped copy outlives it. */
275
+ function deletedTranscriptsIn(archivedProject: string, liveProject: string): string[] {
276
+ return transcriptsIn(archivedProject, `.jsonl${GZIP}`).filter((copy) => {
277
+ const original = relative(archivedProject, copy).slice(0, -GZIP.length);
278
+ return !existsSync(resolve(liveProject, original));
279
+ });
280
+ }
281
+
282
+ function transcriptText(filepath: string): string | null {
283
+ try {
284
+ const raw = readFileSync(filepath);
285
+ return filepath.endsWith(GZIP)
286
+ ? new TextDecoder().decode(Bun.gunzipSync(raw))
287
+ : raw.toString("utf-8");
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+
293
+ function projectTranscripts(
294
+ claudeDir: string,
295
+ archiveDir: string | undefined,
296
+ dirName: string
297
+ ) {
298
+ const live = resolve(claudeDir, dirName);
299
+ return [
300
+ ...transcriptsIn(live),
301
+ ...(archiveDir ? deletedTranscriptsIn(resolve(archiveDir, dirName), live) : []),
302
+ ];
303
+ }
304
+
259
305
  export function readClaudeCode(
260
306
  claudeDir: string,
261
307
  projectFilter?: string,
262
- now: Date = new Date()
308
+ now: Date = new Date(),
309
+ archiveDir?: string
263
310
  ): AgentUsage {
264
311
  const tally = usageTally(projectFilter, now);
265
- if (!existsSync(claudeDir)) return tally.usage;
266
312
  const calls: (PricedCall & { project: string })[] = [];
267
-
268
- const projectDirs = readdirSync(claudeDir, { withFileTypes: true })
269
- .filter((entry) => entry.isDirectory())
270
- .map((entry) => entry.name);
313
+ const projectDirs = new Set([
314
+ ...projectDirsIn(claudeDir),
315
+ ...(archiveDir ? projectDirsIn(archiveDir) : []),
316
+ ]);
271
317
 
272
318
  for (const dirName of projectDirs) {
273
319
  const projName = projectNameOf(dirName);
274
320
  if (typeof projectFilter === "string" && !projName.includes(projectFilter)) continue;
275
321
 
276
- for (const filepath of transcriptsIn(resolve(claudeDir, dirName))) {
277
- let content: string;
278
- try {
279
- content = readFileSync(filepath, "utf-8");
280
- } catch {
281
- continue;
282
- }
322
+ for (const filepath of projectTranscripts(claudeDir, archiveDir, dirName)) {
323
+ const content = transcriptText(filepath);
324
+ if (content === null) continue;
283
325
 
284
326
  for (const line of content.split("\n")) {
285
327
  // Cheap reject before the parse: most lines are not model turns.
@@ -8,6 +8,7 @@ import { resolve } from "node:path";
8
8
  import { AGENT_REGISTRY, type AgentName } from "../hooks/lib/agent-registry";
9
9
  import { eventsSince } from "../hooks/lib/events";
10
10
  import { platform } from "../hooks/lib/paths";
11
+ import { transcriptArchive } from "../hooks/lib/transcript-preserve";
11
12
  import { findBinaryOnPath } from "../hooks/lib/which";
12
13
  import {
13
14
  opencodeDatabase,
@@ -32,7 +33,12 @@ function agentReports(project: string | undefined): AgentReport[] {
32
33
  });
33
34
  return [
34
35
  report("claude", () =>
35
- readClaudeCode(resolve(platform.claudeDir(), "projects"), project)
36
+ readClaudeCode(
37
+ resolve(platform.claudeDir(), "projects"),
38
+ project,
39
+ new Date(),
40
+ transcriptArchive()
41
+ )
36
42
  ),
37
43
  report("codex", () => readCodex(platform.codexDir(), project)),
38
44
  report("opencode", () => readOpencode(opencodeDatabase(), project)),