@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 +22 -0
- package/README.md +11 -12
- package/package.json +1 -1
- package/src/image.ts +11 -1
- package/src/index.ts +35 -14
- package/src/parse.ts +1 -0
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
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
|
|
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
|
|
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:
|
|
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();
|
|
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" &&
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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);
|