@drakulavich/zapara 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project are documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.7.1] - 2026-09-25
9
+
10
+ ### Added
11
+ - `zapara --help` ends with where to report bugs, share ideas and star the
12
+ project.
13
+
14
+ ### Fixed
15
+ - Sessions started by a script through `claude -p` or the Agent SDK no longer
16
+ count as yours. A batch of 200 of them showed up as "204 sessions at once"
17
+ and inflated that hour's prompts, context switches and index.
18
+
19
+ ## [0.7.0] - 2026-09-23
20
+
21
+ ### Added
22
+ - In a terminal, `zapara card` asks `open it? [Y/n]` and opens the picture in
23
+ the default viewer on Enter. Pipes, scripts and Windows are never asked.
24
+
25
+ ### Changed
26
+ - `zapara card` writes to `~/Downloads/zapara-card.png` instead of the current
27
+ directory, and says `wrote zapara-card.png to Downloads`. Without that
28
+ folder it asks for `--out`.
29
+
8
30
  ## [0.6.0] - 2026-09-22
9
31
 
10
32
  ### Changed
package/README.md CHANGED
@@ -96,25 +96,24 @@ The grid is a fixed 98 columns wide, 100 with its hour header, and does not refl
96
96
  `zapara card` turns your last two weeks into one picture: a character named after the kind of load that dominates your hours, the sentence behind it, the peak hour, the share of calm, warming, heating and fried hours, and three highlights. It carries no dates and no hour totals, so it does not read as a timesheet.
97
97
 
98
98
  ```bash
99
- zapara card # writes zapara-card.png in the current directory
99
+ zapara card # writes zapara-card.png to ~/Downloads
100
100
  zapara card --out card.webp # WebP instead; --out card.html writes the page itself
101
101
  ```
102
102
 
103
- On macOS, one more command puts the picture on the clipboard, ready to paste into a chat:
104
-
105
- ```bash
106
- zapara card && osascript -e 'set the clipboard to (read (POSIX file "zapara-card.png") as «class PNGf»)'
107
- ```
103
+ In a terminal it then asks `open it? [Y/n]`: Enter opens the picture in the default viewer, where ⌘C copies it for a chat. A pipe or a script is never asked.
108
104
 
109
105
  ```
110
106
  The Marathoner: Longest streak 7h53m without a break, 68% of your hours calm.
111
- wrote zapara-card.png
107
+ wrote zapara-card.png to Downloads
108
+ open it? [Y/n]
112
109
  ```
113
110
 
114
111
  <p align="center"><img src="https://raw.githubusercontent.com/drakulavich/zapara/main/assets/card.webp" alt="zapara card: The Marathoner, longest streak 7h53m, 68% of hours calm" width="800"></p>
115
112
 
116
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.
117
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
+
118
117
  ## Usage
119
118
 
120
119
  | Command | What it does |
@@ -124,7 +123,7 @@ This one comes from the same `busy-week` fixture as the pictures above. A headle
124
123
  | `zapara --from 2026-09-01 --to 2026-09-14` | Any window, both days inclusive, at most 90 days. `--to` alone is 7 days ending there, `--days 30 --to 2026-09-14` is 30 days ending there. |
125
124
  | `zapara today` | Today, one row per hour that had activity. `yesterday` likewise. |
126
125
  | `zapara 2026-09-14 --explain` | One day, with the six weighted components behind each index. |
127
- | `zapara card` | The last 14 days as one shareable picture, `zapara-card.png` in the current directory. |
126
+ | `zapara card` | The last 14 days as one shareable picture, `zapara-card.png` in `~/Downloads`. |
128
127
  | `zapara card --days 30 --out me.webp` | Any window from 1 to 90 days; `.png`, `.webp` or `.html` by extension. `--json` prints the card's data instead. |
129
128
  | `zapara status` | Writes today's load to `~/.claude/zapara/status.json` for a status line to read, and prints the same line. See [Status line](#status-line). |
130
129
 
@@ -134,16 +133,16 @@ This one comes from the same `busy-week` fixture as the pictures above. A headle
134
133
  | `--explain` | With a day: the six weighted parts behind each index. |
135
134
  | `--json` | Print the whole window as one JSON document instead of a table. |
136
135
  | `--projects <dir>` | Read this directory instead of `~/.claude/projects`. |
137
- | `--out <path>` | Where `card` writes; the extension picks the format. |
136
+ | `--out <path>` | Where `card` writes instead of `~/Downloads`; the extension picks the format. |
138
137
  | `--no-color` | Plain glyphs and peaks with no ANSI codes. `NO_COLOR` in the environment does the same. |
139
138
  | `-h`, `--help` | Usage, exit 0. |
140
139
  | `-V`, `--version` | The version from `package.json`, exit 0. |
141
140
 
142
141
  Levels: calm 0–29, warming 30–59, heating 60–84, fried 85–100.
143
142
 
144
- The grid and the day print a text table when stdout is a terminal and JSON otherwise, so `zapara | cat` prints JSON; no flag forces text in a pipe yet. `card` and `status` write their file and print their lines whether piped or not. `card --json` is the exception: it prints the card's data and writes no file. `--json` changes nothing for `status`, whose line is already JSON and whose file is written either way.
143
+ The grid and the day print a text table when stdout is a terminal and JSON otherwise, so `zapara | cat` prints JSON; no flag forces text in a pipe yet. `card` and `status` write their file and print their lines whether piped or not. `card` asks to open the picture only when stdin and stdout are both a terminal, and never on Windows. `card --json` is the exception: it prints the card's data and writes no file. `--json` changes nothing for `status`, whose line is already JSON and whose file is written either way.
145
144
 
146
- A run that works exits 0, and so does a window with no data, which prints an empty grid. Exit 1 is a failure zapara can name, printed as one line to stderr that never contains a path: the projects directory missing or unreadable, `status` unable to write its file, `card` unable to write its picture or to find a browser engine, and whatever else goes wrong below the command line. Exit 2 is a usage error, such as a bad date, an unknown flag or a value flag given twice; it prints one line and a hint to `--help`.
145
+ A run that works exits 0, and so does a window with no data, which prints an empty grid. Exit 1 is a failure zapara can name, printed as one line to stderr that never contains a path: the projects directory missing or unreadable, `status` unable to write its file, `card` unable to write its picture or to find a browser engine, `card` without `--out` on a machine with no `~/Downloads` folder, and whatever else goes wrong below the command line. Exit 2 is a usage error, such as a bad date, an unknown flag or a value flag given twice; it prints one line and a hint to `--help`.
147
146
 
148
147
  ## Status line
149
148
 
@@ -226,7 +225,7 @@ Weights and norms live in one exported constant in `src/score.ts`, so a recalibr
226
225
  | Column | What it counts |
227
226
  |---|---|
228
227
  | `sess` | Distinct session ids with at least one user or assistant record in the hour. |
229
- | `prompts` | Messages the human typed. A `user` record whose text is neither an interrupt marker nor an agent-message marker; `isMeta` and sidechain records are excluded. |
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. |
230
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. |
231
230
  | `intr` | Interrupts. A `user` record whose text block starts with `[Request interrupted by user`, covering both the plain and the tool-use form. |
232
231
  | `rej` | Tool rejections. A `tool_result` block saying the user did not want to proceed with that tool use. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/zapara",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Cognitive load index for people driving Claude Code, computed locally from transcripts",
5
5
  "license": "MIT",
6
6
  "author": "Anton Yakutovich",
package/src/image.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // The only module that reads the card assets, opens a Bun.WebView or Bun.Image,
2
- // and writes the card.
2
+ // writes the card, and starts another program: the opener that shows it.
3
3
  import { readFile, writeFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
4
5
  import type { CardAssets } from "./cardhtml.ts";
5
6
 
6
7
  const ASSETS = new URL("../assets/", import.meta.url);
@@ -72,3 +73,12 @@ async function write(out: string, data: string | Uint8Array): Promise<void> {
72
73
  throw new Error(WRITE_LINE);
73
74
  }
74
75
  }
76
+
77
+ // Absolute, so `-card.html` is never an option. `sh … &` with SIGHUP ignored:
78
+ // a detached Bun.spawn child, or one in a terminal zapara leads, dies with zapara.
79
+ export function openCard(path: string): void {
80
+ const opener = process.platform === "darwin" ? "open" : "xdg-open";
81
+ try {
82
+ Bun.spawnSync(["sh", "-c", 'trap "" HUP; "$0" "$@" </dev/null >/dev/null 2>&1 &', opener, resolve(path)], { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
83
+ } catch {}
84
+ }
package/src/index.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
  // Argument parsing, the clock, stdout and exit codes live here; everything else is pure.
3
- import { readFileSync } from "node:fs";
3
+ import { readFileSync, statSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { cardData, sentenceText } from "./card.ts";
7
7
  import { cardHtml } from "./cardhtml.ts";
8
8
  import { localDate } from "./derive.ts";
9
- import { loadAssets, renderCard } from "./image.ts";
9
+ import { loadAssets, openCard, renderCard } from "./image.ts";
10
10
  import { renderDay, renderJson, renderWeek } from "./render.ts";
11
11
  import { report } from "./report.ts";
12
12
  import { renderStatus, statusOf } from "./status.ts";
@@ -35,16 +35,19 @@ window:
35
35
 
36
36
  options:
37
37
  --explain with a day: the six weighted parts behind each index
38
- --out <path> with card: .png, .webp or .html (default zapara-card.png)
38
+ --out <path> with card: .png, .webp or .html
39
+ (default ~/Downloads/zapara-card.png)
39
40
  --json the same data as JSON; a pipe gets JSON without asking
40
41
  --projects <dir> read this directory instead of ~/.claude/projects
41
42
  --no-color no ANSI colors; NO_COLOR does the same
42
43
  -h, --help -V, --version
43
44
 
44
- 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`;
45
48
  const HINT = "run 'zapara --help' for usage";
46
49
 
47
- type Args = { command: "grid" | "day" | "card" | "status"; to: string; days: number; explain: boolean; json: boolean; out: string; projects: string; color: boolean };
50
+ type Args = { command: "grid" | "day" | "card" | "status"; to: string; days: number; explain: boolean; json: boolean; out: string | null; projects: string; color: boolean };
48
51
 
49
52
  class UsageError extends Error {}
50
53
  // Thrown only at a flag position, never for a token consumed as another flag's
@@ -95,12 +98,11 @@ function windowOf(days: string | null, from: string | null, to: string | null, d
95
98
  }
96
99
 
97
100
  function parseArgs(argv: string[], now: Date, env: NodeJS.ProcessEnv, isTTY: boolean): Args {
98
- const a: Args = { command: "grid", to: localDate(now), days: 7, explain: false, json: false, out: "zapara-card.png", projects: join(homedir(), ".claude", "projects"), color: isTTY && !env.NO_COLOR };
101
+ const a: Args = { command: "grid", to: localDate(now), days: 7, explain: false, json: false, out: null, projects: join(homedir(), ".claude", "projects"), color: isTTY && !env.NO_COLOR };
99
102
  let days: string | null = null;
100
103
  let from: string | null = null;
101
104
  let to: string | null = null;
102
105
  let jsonFlag = false;
103
- let outGiven = false;
104
106
  const positional: string[] = [];
105
107
  // A value flag given twice is a usage error; bare flags are idempotent and untracked.
106
108
  const seen = new Set<string>();
@@ -136,7 +138,7 @@ function parseArgs(argv: string[], now: Date, env: NodeJS.ProcessEnv, isTTY: boo
136
138
  case "--from": from = value(); break;
137
139
  case "--to": to = value(); break;
138
140
  case "--days": days = value(true); break;
139
- case "--out": a.out = value(); outGiven = true; break;
141
+ case "--out": a.out = value(); break;
140
142
  default:
141
143
  if (arg.startsWith("-")) throw new UsageError(`unknown flag${named(arg)}`);
142
144
  positional.push(arg);
@@ -158,10 +160,12 @@ function parseArgs(argv: string[], now: Date, env: NodeJS.ProcessEnv, isTTY: boo
158
160
  // The card is a file either way, so only an explicit --json switches it.
159
161
  a.json = a.command === "card" ? jsonFlag : jsonFlag || !isTTY;
160
162
  if (a.command !== "day" && a.explain) throw new UsageError("--explain applies to a named day only");
161
- if (a.command !== "card" && outGiven) throw new UsageError("--out applies to card only");
162
- // Printed back verbatim in `wrote \u2026`, so it must be one plain line.
163
- if (/[\x00-\x1f\x7f]/.test(a.out)) throw new UsageError("--out must not contain control characters");
164
- if (!/\.(png|webp|html)$/i.test(a.out)) throw new UsageError("--out must end in .png, .webp or .html");
163
+ if (a.command !== "card" && a.out !== null) throw new UsageError("--out applies to card only");
164
+ if (a.out !== null) {
165
+ // Printed back verbatim in `wrote \u2026`, so it must be one plain line.
166
+ if (/[\x00-\x1f\x7f]/.test(a.out)) throw new UsageError("--out must not contain control characters");
167
+ if (!/\.(png|webp|html)$/i.test(a.out)) throw new UsageError("--out must end in .png, .webp or .html");
168
+ }
165
169
  return a;
166
170
  }
167
171
 
@@ -188,6 +192,16 @@ async function status(a: Args, now: Date): Promise<number> {
188
192
  return 0;
189
193
  }
190
194
 
195
+ // The label names the folder, not the path: the CLI never prints a derived path.
196
+ function cardTarget(out: string | null): { path: string; label: string } {
197
+ if (out !== null) return { path: out, label: out };
198
+ const dir = join(homedir(), "Downloads");
199
+ let isDir = false;
200
+ try { isDir = statSync(dir).isDirectory(); } catch {}
201
+ if (!isDir) throw new Error("no Downloads folder: pass --out <path>");
202
+ return { path: join(dir, "zapara-card.png"), label: "zapara-card.png to Downloads" };
203
+ }
204
+
191
205
  async function card(a: Args): Promise<number> {
192
206
  const days: Day[] = await report({ projects: a.projects, to: a.to, days: a.days });
193
207
  const data = cardData(days, { days: a.days });
@@ -203,8 +217,15 @@ async function card(a: Args): Promise<number> {
203
217
  console.log(JSON.stringify(json, null, 2));
204
218
  return 0;
205
219
  }
206
- await renderCard(cardHtml(data, await loadAssets()), a.out);
207
- console.log(`${data.name}: ${sentenceText(data.sentence)}\nwrote ${a.out}`);
220
+ const target = cardTarget(a.out);
221
+ await renderCard(cardHtml(data, await loadAssets()), target.path);
222
+ console.log(`${data.name}: ${sentenceText(data.sentence)}\nwrote ${target.label}`);
223
+ if (process.stdin.isTTY && process.stdout.isTTY && process.platform !== "win32") {
224
+ process.stdout.write("open it? [Y/n] ");
225
+ let answer: string | null = null;
226
+ for await (const line of console) { answer = line; break; }
227
+ if (answer !== null && /^(y|yes)?$/i.test(answer.trim())) openCard(target.path);
228
+ }
208
229
  return 0;
209
230
  }
210
231
 
package/src/parse.ts CHANGED
@@ -49,6 +49,7 @@ export function parseTranscript(text: string): Event[] {
49
49
  const sessionId = str(rec.sessionId);
50
50
  if (type === null || sessionId === null) continue;
51
51
  if (rec.isSidechain === true) continue;
52
+ if (str(rec.entrypoint)?.startsWith("sdk-")) continue; // a script ran `claude -p` or the Agent SDK
52
53
 
53
54
  if (type === "permission-mode") {
54
55
  const m = str(rec.permissionMode);