@drakulavich/zapara 0.7.0 → 0.7.2
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/CHANGELOG.md +24 -0
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/analyze.ts +3 -2
- package/src/index.ts +3 -1
- package/src/parse.ts +22 -1
- package/src/report.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ All notable changes to this project are documented here. The format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.2] - 2026-09-26
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Transcripts are read sixteen at a time instead of one by one. On a machine
|
|
12
|
+
with 1.6 GB of transcripts in a 14-day window, reading took 0.9 s instead
|
|
13
|
+
of 5.4 s.
|
|
14
|
+
- Transcript lines older than the window's three-hour look-back are skipped
|
|
15
|
+
before parsing, except assistant replies, whose request ids dedupe output
|
|
16
|
+
tokens. A long session's file holds its whole history, and on one machine
|
|
17
|
+
59% of what a 14-day window read was such history. Results are unchanged.
|
|
18
|
+
Together with parallel reads, `zapara card --json` there went from 5.6 s to
|
|
19
|
+
about 2 s.
|
|
20
|
+
|
|
21
|
+
## [0.7.1] - 2026-09-25
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `zapara --help` ends with where to report bugs, share ideas and star the
|
|
25
|
+
project.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Sessions started by a script through `claude -p` or the Agent SDK no longer
|
|
29
|
+
count as yours. A batch of 200 of them showed up as "204 sessions at once"
|
|
30
|
+
and inflated that hour's prompts, context switches and index.
|
|
31
|
+
|
|
8
32
|
## [0.7.0] - 2026-09-23
|
|
9
33
|
|
|
10
34
|
### Added
|
package/README.md
CHANGED
|
@@ -112,6 +112,8 @@ open it? [Y/n]
|
|
|
112
112
|
|
|
113
113
|
This one comes from the same `busy-week` fixture as the pictures above. A headless browser that Bun drives takes the picture: WebKit on macOS, an installed Google Chrome elsewhere. On Linux or Windows, install Chrome or write `--out card.html` and open the page in any browser.
|
|
114
114
|
|
|
115
|
+
If your card told you something about your week, star [the repository](https://github.com/drakulavich/zapara) so other people can find zapara.
|
|
116
|
+
|
|
115
117
|
## Usage
|
|
116
118
|
|
|
117
119
|
| Command | What it does |
|
|
@@ -223,7 +225,7 @@ Weights and norms live in one exported constant in `src/score.ts`, so a recalibr
|
|
|
223
225
|
| Column | What it counts |
|
|
224
226
|
|---|---|
|
|
225
227
|
| `sess` | Distinct session ids with at least one user or assistant record in the hour. |
|
|
226
|
-
| `prompts` | Messages the human typed. A `user` record whose text is neither an interrupt marker nor an agent-message marker; `isMeta` and
|
|
228
|
+
| `prompts` | Messages the human typed. A `user` record whose text is neither an interrupt marker nor an agent-message marker; `isMeta`, sidechain, `claude -p` and Agent SDK records are excluded. |
|
|
227
229
|
| `rep` | Inbound messages from subagents, other sessions and background tasks. A `user` record whose text starts with one of the agent-message markers, which is something to read and react to rather than something typed. |
|
|
228
230
|
| `intr` | Interrupts. A `user` record whose text block starts with `[Request interrupted by user`, covering both the plain and the tool-use form. |
|
|
229
231
|
| `rej` | Tool rejections. A `tool_result` block saying the user did not want to proceed with that tool use. |
|
package/package.json
CHANGED
package/src/analyze.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { compareStrings, derive } from "./derive.ts";
|
|
1
|
+
import { compareStrings, derive, windowBounds } from "./derive.ts";
|
|
2
2
|
import { parseTranscript } from "./parse.ts";
|
|
3
3
|
import type { Day, Transcript, Window } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
// Sorted by path so file discovery order cannot change a result.
|
|
6
6
|
export function analyze(transcripts: Transcript[], w: Window): Day[] {
|
|
7
|
+
const { cutoffMs } = windowBounds(w);
|
|
7
8
|
const events = [...transcripts]
|
|
8
9
|
.sort((a, b) => compareStrings(a.path, b.path))
|
|
9
|
-
.flatMap((t) => parseTranscript(t.text));
|
|
10
|
+
.flatMap((t) => parseTranscript(t.text, cutoffMs));
|
|
10
11
|
return derive(events, w);
|
|
11
12
|
}
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,9 @@ options:
|
|
|
42
42
|
--no-color no ANSI colors; NO_COLOR does the same
|
|
43
43
|
-h, --help -V, --version
|
|
44
44
|
|
|
45
|
-
levels: calm 0-29 warming 30-59 heating 60-84 fried 85-100
|
|
45
|
+
levels: calm 0-29 warming 30-59 heating 60-84 fried 85-100
|
|
46
|
+
|
|
47
|
+
bugs, ideas and a star: github.com/drakulavich/zapara`;
|
|
46
48
|
const HINT = "run 'zapara --help' for usage";
|
|
47
49
|
|
|
48
50
|
type Args = { command: "grid" | "day" | "card" | "status"; to: string; days: number; explain: boolean; json: boolean; out: string | null; projects: string; color: boolean };
|
package/src/parse.ts
CHANGED
|
@@ -28,7 +28,26 @@ const firstText = (content: unknown): string | null => {
|
|
|
28
28
|
return null;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// A record older than the look-back yields only events derive() drops, so it is
|
|
32
|
+
// not parsed at all. Every "timestamp" in the line must be old, since a nested
|
|
33
|
+
// one can differ from the record's own. The skip waits for a first timestamped
|
|
34
|
+
// record, which the mode switches before it attach to; lines naming the two
|
|
35
|
+
// tools whose result the human writes are always parsed, for their ids.
|
|
36
|
+
// One pass over the line: it costs a fifth of JSON.parse.
|
|
37
|
+
const SKIP_SCAN = new RegExp(`"timestamp":"([^"]{20,40})"|${QUESTION_TOOL}|${PLAN_TOOL}`, "g");
|
|
38
|
+
function olderThan(line: string, cutoffMs: number): boolean {
|
|
39
|
+
// Parsing an assistant record can update seenRequestIds even when its events
|
|
40
|
+
// are outside the window. Preserve that deduplication state across the skip.
|
|
41
|
+
if (line.includes('"requestId"')) return false;
|
|
42
|
+
let any = false;
|
|
43
|
+
for (const m of line.matchAll(SKIP_SCAN)) {
|
|
44
|
+
if (m[1] === undefined || !(Date.parse(m[1]) < cutoffMs)) return false;
|
|
45
|
+
any = true;
|
|
46
|
+
}
|
|
47
|
+
return any;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseTranscript(text: string, cutoffMs = -Infinity): Event[] {
|
|
32
51
|
const events: Event[] = [];
|
|
33
52
|
let lastTs: number | null = null;
|
|
34
53
|
let lastMode: string | null = null;
|
|
@@ -42,6 +61,7 @@ export function parseTranscript(text: string): Event[] {
|
|
|
42
61
|
|
|
43
62
|
for (const line of text.split("\n")) {
|
|
44
63
|
if (line.trim() === "") continue;
|
|
64
|
+
if (lastTs !== null && lastTs < cutoffMs && olderThan(line, cutoffMs)) continue;
|
|
45
65
|
let rec: unknown;
|
|
46
66
|
try { rec = JSON.parse(line); } catch { continue; }
|
|
47
67
|
if (!isObj(rec)) continue;
|
|
@@ -49,6 +69,7 @@ export function parseTranscript(text: string): Event[] {
|
|
|
49
69
|
const sessionId = str(rec.sessionId);
|
|
50
70
|
if (type === null || sessionId === null) continue;
|
|
51
71
|
if (rec.isSidechain === true) continue;
|
|
72
|
+
if (str(rec.entrypoint)?.startsWith("sdk-")) continue; // a script ran `claude -p` or the Agent SDK
|
|
52
73
|
|
|
53
74
|
if (type === "permission-mode") {
|
|
54
75
|
const m = str(rec.permissionMode);
|
package/src/report.ts
CHANGED
|
@@ -6,12 +6,23 @@ import type { Day, Transcript } from "./types.ts";
|
|
|
6
6
|
|
|
7
7
|
export type ReportOptions = { projects: string; to: string; days: number; now?: Date };
|
|
8
8
|
|
|
9
|
+
// Reading one file at a time left the disk idle between files: 1.6 GB took 5.4 s
|
|
10
|
+
// sequentially and 0.9 s in parallel. The cap keeps open files well under the limit.
|
|
11
|
+
const READERS = 16;
|
|
12
|
+
|
|
9
13
|
export async function report(o: ReportOptions): Promise<Day[]> {
|
|
10
14
|
const window = { to: o.to, days: o.days, now: o.now };
|
|
11
15
|
const paths = await scan(o.projects, windowBounds(window).cutoffMs);
|
|
16
|
+
const texts: (string | null)[] = new Array(paths.length).fill(null);
|
|
17
|
+
let next = 0;
|
|
18
|
+
const reader = async (): Promise<void> => {
|
|
19
|
+
while (next < paths.length) {
|
|
20
|
+
const i = next++;
|
|
21
|
+
try { texts[i] = await readFile(paths[i]!, "utf8"); } catch { /* vanished or unreadable: skip */ }
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
await Promise.all(Array.from({ length: Math.min(READERS, paths.length) }, reader));
|
|
12
25
|
const transcripts: Transcript[] = [];
|
|
13
|
-
|
|
14
|
-
try { transcripts.push({ path, text: await readFile(path, "utf8") }); } catch { /* vanished or unreadable: skip */ }
|
|
15
|
-
}
|
|
26
|
+
paths.forEach((path, i) => { if (texts[i] !== null) transcripts.push({ path, text: texts[i]! }); });
|
|
16
27
|
return analyze(transcripts, window);
|
|
17
28
|
}
|