@drakulavich/zapara 0.7.1 → 0.7.3
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 +23 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/analyze.ts +3 -2
- package/src/index.ts +42 -12
- package/src/parse.ts +21 -1
- package/src/report.ts +33 -7
- package/src/scan.ts +14 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project are documented here. The format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.3] - 2026-09-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `--verbose` prints timings and counts to stderr after the output, with no
|
|
12
|
+
path in them: files scanned and read, megabytes, milliseconds per stage,
|
|
13
|
+
versions, platform and CPU count. It is meant for diagnosing a slow run on
|
|
14
|
+
someone else's machine.
|
|
15
|
+
- `zapara card` says `drawing the card…` on a terminal's stderr while the
|
|
16
|
+
browser engine renders the picture, which takes a few seconds.
|
|
17
|
+
|
|
18
|
+
## [0.7.2] - 2026-09-26
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Transcripts are read sixteen at a time instead of one by one. On a machine
|
|
22
|
+
with 1.6 GB of transcripts in a 14-day window, reading took 0.9 s instead
|
|
23
|
+
of 5.4 s.
|
|
24
|
+
- Transcript lines older than the window's three-hour look-back are skipped
|
|
25
|
+
before parsing, except assistant replies, whose request ids dedupe output
|
|
26
|
+
tokens. A long session's file holds its whole history, and on one machine
|
|
27
|
+
59% of what a 14-day window read was such history. Results are unchanged.
|
|
28
|
+
Together with parallel reads, `zapara card --json` there went from 5.6 s to
|
|
29
|
+
about 2 s.
|
|
30
|
+
|
|
8
31
|
## [0.7.1] - 2026-09-25
|
|
9
32
|
|
|
10
33
|
### Added
|
package/README.md
CHANGED
|
@@ -135,12 +135,13 @@ If your card told you something about your week, star [the repository](https://g
|
|
|
135
135
|
| `--projects <dir>` | Read this directory instead of `~/.claude/projects`. |
|
|
136
136
|
| `--out <path>` | Where `card` writes instead of `~/Downloads`; the extension picks the format. |
|
|
137
137
|
| `--no-color` | Plain glyphs and peaks with no ANSI codes. `NO_COLOR` in the environment does the same. |
|
|
138
|
+
| `--verbose` | After the output, prints to stderr where the time went: files scanned and read, megabytes, and milliseconds for scanning, reading, analysis and the card's render, plus the zapara and Bun versions, platform and CPU count. Numbers only, no path, so the lines are safe to paste into an issue when zapara is slow on your machine. |
|
|
138
139
|
| `-h`, `--help` | Usage, exit 0. |
|
|
139
140
|
| `-V`, `--version` | The version from `package.json`, exit 0. |
|
|
140
141
|
|
|
141
142
|
Levels: calm 0–29, warming 30–59, heating 60–84, fried 85–100.
|
|
142
143
|
|
|
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.
|
|
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` asks to open the picture only when stdin and stdout are both a terminal, and never on Windows. While the browser engine draws a picture, a terminal's stderr shows `drawing the card…`, cleared before the result. `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.
|
|
144
145
|
|
|
145
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, `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`.
|
|
146
147
|
|
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
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Argument parsing, the clock, stdout and exit codes live here; everything else is pure.
|
|
3
3
|
import { readFileSync, statSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
4
|
+
import { cpus, 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
9
|
import { loadAssets, openCard, renderCard } from "./image.ts";
|
|
10
10
|
import { renderDay, renderJson, renderWeek } from "./render.ts";
|
|
11
|
-
import { report } from "./report.ts";
|
|
11
|
+
import { READERS, report, type Timing } from "./report.ts";
|
|
12
12
|
import { renderStatus, statusOf } from "./status.ts";
|
|
13
13
|
import { writeStatus } from "./statusfile.ts";
|
|
14
14
|
import type { Day } from "./types.ts";
|
|
@@ -40,6 +40,7 @@ options:
|
|
|
40
40
|
--json the same data as JSON; a pipe gets JSON without asking
|
|
41
41
|
--projects <dir> read this directory instead of ~/.claude/projects
|
|
42
42
|
--no-color no ANSI colors; NO_COLOR does the same
|
|
43
|
+
--verbose timings and counts on stderr, for a slow run
|
|
43
44
|
-h, --help -V, --version
|
|
44
45
|
|
|
45
46
|
levels: calm 0-29 warming 30-59 heating 60-84 fried 85-100
|
|
@@ -47,7 +48,7 @@ levels: calm 0-29 warming 30-59 heating 60-84 fried 85-100
|
|
|
47
48
|
bugs, ideas and a star: github.com/drakulavich/zapara`;
|
|
48
49
|
const HINT = "run 'zapara --help' for usage";
|
|
49
50
|
|
|
50
|
-
type Args = { command: "grid" | "day" | "card" | "status"; to: string; days: number; explain: boolean; json: boolean; out: string | null; projects: string; color: boolean };
|
|
51
|
+
type Args = { command: "grid" | "day" | "card" | "status"; to: string; days: number; explain: boolean; json: boolean; out: string | null; projects: string; color: boolean; verbose: boolean };
|
|
51
52
|
|
|
52
53
|
class UsageError extends Error {}
|
|
53
54
|
// Thrown only at a flag position, never for a token consumed as another flag's
|
|
@@ -98,7 +99,7 @@ function windowOf(days: string | null, from: string | null, to: string | null, d
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
function parseArgs(argv: string[], now: Date, env: NodeJS.ProcessEnv, isTTY: boolean): Args {
|
|
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 };
|
|
102
|
+
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, verbose: false };
|
|
102
103
|
let days: string | null = null;
|
|
103
104
|
let from: string | null = null;
|
|
104
105
|
let to: string | null = null;
|
|
@@ -134,6 +135,7 @@ function parseArgs(argv: string[], now: Date, env: NodeJS.ProcessEnv, isTTY: boo
|
|
|
134
135
|
case "--json": bare(); jsonFlag = true; break;
|
|
135
136
|
case "--explain": bare(); a.explain = true; break;
|
|
136
137
|
case "--no-color": bare(); a.color = false; break;
|
|
138
|
+
case "--verbose": bare(); a.verbose = true; break;
|
|
137
139
|
case "--projects": a.projects = value(); break;
|
|
138
140
|
case "--from": from = value(); break;
|
|
139
141
|
case "--to": to = value(); break;
|
|
@@ -173,9 +175,14 @@ async function main(): Promise<number> {
|
|
|
173
175
|
const argv = process.argv.slice(2);
|
|
174
176
|
const now = new Date();
|
|
175
177
|
const a = parseArgs(argv, now, process.env, process.stdout.isTTY === true);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
const timing = a.verbose ? ({} as Timing) : undefined;
|
|
179
|
+
const code = a.command === "card" ? await card(a, timing) : a.command === "status" ? await status(a, now, timing) : await table(a, now, timing);
|
|
180
|
+
if (timing) process.stderr.write(timingLines(timing));
|
|
181
|
+
return code;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function table(a: Args, now: Date, timing?: Timing): Promise<number> {
|
|
185
|
+
const days: Day[] = await report({ projects: a.projects, to: a.to, days: a.days, now }, timing);
|
|
179
186
|
const data = a.command === "day" ? days[0] : days;
|
|
180
187
|
if (a.json) console.log(renderJson(data!));
|
|
181
188
|
else if (a.command === "day") console.log(renderDay(days[0]!, { explain: a.explain, color: a.color }));
|
|
@@ -184,8 +191,8 @@ async function main(): Promise<number> {
|
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
// The write comes first: a caller never reads a line that was not saved.
|
|
187
|
-
async function status(a: Args, now: Date): Promise<number> {
|
|
188
|
-
const days: Day[] = await report({ projects: a.projects, to: a.to, days: a.days, now });
|
|
194
|
+
async function status(a: Args, now: Date, timing?: Timing): Promise<number> {
|
|
195
|
+
const days: Day[] = await report({ projects: a.projects, to: a.to, days: a.days, now }, timing);
|
|
189
196
|
const line = renderStatus(statusOf(days[0]!, now));
|
|
190
197
|
await writeStatus(line, process.env);
|
|
191
198
|
process.stdout.write(line);
|
|
@@ -202,8 +209,8 @@ function cardTarget(out: string | null): { path: string; label: string } {
|
|
|
202
209
|
return { path: join(dir, "zapara-card.png"), label: "zapara-card.png to Downloads" };
|
|
203
210
|
}
|
|
204
211
|
|
|
205
|
-
async function card(a: Args): Promise<number> {
|
|
206
|
-
const days: Day[] = await report({ projects: a.projects, to: a.to, days: a.days });
|
|
212
|
+
async function card(a: Args, timing?: Timing): Promise<number> {
|
|
213
|
+
const days: Day[] = await report({ projects: a.projects, to: a.to, days: a.days }, timing);
|
|
207
214
|
const data = cardData(days, { days: a.days });
|
|
208
215
|
if (data === null) throw new Error(`no activity in the last ${a.days} days`);
|
|
209
216
|
if (a.json) {
|
|
@@ -218,7 +225,16 @@ async function card(a: Args): Promise<number> {
|
|
|
218
225
|
return 0;
|
|
219
226
|
}
|
|
220
227
|
const target = cardTarget(a.out);
|
|
221
|
-
|
|
228
|
+
// The browser engine takes seconds; a person at a terminal is told why it waits.
|
|
229
|
+
const note = process.stderr.isTTY && !/\.html$/i.test(target.path);
|
|
230
|
+
if (note) process.stderr.write("drawing the card…");
|
|
231
|
+
const t = performance.now();
|
|
232
|
+
try {
|
|
233
|
+
await renderCard(cardHtml(data, await loadAssets()), target.path);
|
|
234
|
+
} finally {
|
|
235
|
+
if (note) process.stderr.write("\r\x1b[K");
|
|
236
|
+
}
|
|
237
|
+
if (timing) timing.render = { format: target.path.slice(target.path.lastIndexOf(".") + 1).toLowerCase(), ms: performance.now() - t };
|
|
222
238
|
console.log(`${data.name}: ${sentenceText(data.sentence)}\nwrote ${target.label}`);
|
|
223
239
|
if (process.stdin.isTTY && process.stdout.isTTY && process.platform !== "win32") {
|
|
224
240
|
process.stdout.write("open it? [Y/n] ");
|
|
@@ -229,6 +245,20 @@ async function card(a: Args): Promise<number> {
|
|
|
229
245
|
return 0;
|
|
230
246
|
}
|
|
231
247
|
|
|
248
|
+
// Numbers only: someone pastes these from another machine, and no path may leave it.
|
|
249
|
+
function timingLines(t: Timing): string {
|
|
250
|
+
let ver = "?";
|
|
251
|
+
try { ver = version(); } catch {}
|
|
252
|
+
const row = (label: string, what: string, ms: number) => `${label.padEnd(8)}${what.padEnd(44)}${String(Math.round(ms)).padStart(7)} ms\n`;
|
|
253
|
+
const plural = (n: number, one: string) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
254
|
+
return `zapara ${ver} · bun ${Bun.version} · ${process.platform} ${process.arch} · ${cpus().length} cpus\n`
|
|
255
|
+
+ row("scan", `${plural(t.files, "file")}, ${t.inWindow} in window, ${plural(t.tailChecks, "tail check")}`, t.scanMs)
|
|
256
|
+
+ row("read", `${plural(t.read, "file")}, ${(t.bytes / 1e6).toFixed(1)} MB, ${READERS} at a time`, t.readMs)
|
|
257
|
+
+ row("analyze", "", t.analyzeMs)
|
|
258
|
+
+ (t.render ? row("render", t.render.format, t.render.ms) : "")
|
|
259
|
+
+ row("total", "", performance.now());
|
|
260
|
+
}
|
|
261
|
+
|
|
232
262
|
if (import.meta.main) {
|
|
233
263
|
main().then((code) => process.exit(code), (e: unknown) => {
|
|
234
264
|
if (e instanceof HelpRequested) { console.log(USAGE); process.exit(0); }
|
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;
|
package/src/report.ts
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { analyze } from "./analyze.ts";
|
|
3
3
|
import { windowBounds } from "./derive.ts";
|
|
4
|
-
import { scan } from "./scan.ts";
|
|
4
|
+
import { scan, type ScanStats } from "./scan.ts";
|
|
5
5
|
import type { Day, Transcript } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
export type ReportOptions = { projects: string; to: string; days: number; now?: Date };
|
|
8
|
+
// What --verbose prints about a run: counts and milliseconds, never a path.
|
|
9
|
+
export type Timing = ScanStats & { inWindow: number; read: number; bytes: number; scanMs: number; readMs: number; analyzeMs: number; render?: { format: string; ms: number } };
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
// Reading one file at a time left the disk idle between files: 1.6 GB took 5.4 s
|
|
12
|
+
// sequentially and 0.9 s in parallel. The cap keeps open files well under the limit.
|
|
13
|
+
export const READERS = 16;
|
|
14
|
+
|
|
15
|
+
export async function report(o: ReportOptions, timing?: Timing): Promise<Day[]> {
|
|
10
16
|
const window = { to: o.to, days: o.days, now: o.now };
|
|
11
|
-
const
|
|
17
|
+
const stats: ScanStats = { files: 0, tailChecks: 0 };
|
|
18
|
+
let t = performance.now();
|
|
19
|
+
const paths = await scan(o.projects, windowBounds(window).cutoffMs, stats);
|
|
20
|
+
const scanMs = performance.now() - t;
|
|
21
|
+
t = performance.now();
|
|
22
|
+
const texts: (string | null)[] = new Array(paths.length).fill(null);
|
|
23
|
+
let next = 0;
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
const reader = async (): Promise<void> => {
|
|
26
|
+
while (next < paths.length) {
|
|
27
|
+
const i = next++;
|
|
28
|
+
try {
|
|
29
|
+
const buf = await readFile(paths[i]!);
|
|
30
|
+
bytes += buf.length;
|
|
31
|
+
texts[i] = buf.toString("utf8");
|
|
32
|
+
} catch { /* vanished or unreadable: skip */ }
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
await Promise.all(Array.from({ length: Math.min(READERS, paths.length) }, reader));
|
|
36
|
+
const readMs = performance.now() - t;
|
|
12
37
|
const transcripts: Transcript[] = [];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
38
|
+
paths.forEach((path, i) => { if (texts[i] !== null) transcripts.push({ path, text: texts[i]! }); });
|
|
39
|
+
t = performance.now();
|
|
40
|
+
const days = analyze(transcripts, window);
|
|
41
|
+
if (timing) Object.assign(timing, { ...stats, inWindow: paths.length, read: transcripts.length, bytes, scanMs, readMs, analyzeMs: performance.now() - t });
|
|
42
|
+
return days;
|
|
17
43
|
}
|
package/src/scan.ts
CHANGED
|
@@ -2,8 +2,12 @@ import type { Dirent } from "node:fs";
|
|
|
2
2
|
import { open, readdir, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
+
// What --verbose reports about the walk: transcripts seen, and how many of them
|
|
6
|
+
// were opened only to read the tail of a file older than the window by mtime.
|
|
7
|
+
export type ScanStats = { files: number; tailChecks: number };
|
|
8
|
+
|
|
5
9
|
// A `subagents` directory holds the parent agent's conversation, not the human's.
|
|
6
|
-
export async function scan(projects: string, cutoffMs: number): Promise<string[]> {
|
|
10
|
+
export async function scan(projects: string, cutoffMs: number, stats: ScanStats = { files: 0, tailChecks: 0 }): Promise<string[]> {
|
|
7
11
|
// No path in either message: the CLI never prints one.
|
|
8
12
|
let root;
|
|
9
13
|
try {
|
|
@@ -15,23 +19,28 @@ export async function scan(projects: string, cutoffMs: number): Promise<string[]
|
|
|
15
19
|
throw new Error("projects directory not found (pass --projects <dir>)");
|
|
16
20
|
}
|
|
17
21
|
const out: string[] = [];
|
|
18
|
-
await collect(projects, root, cutoffMs, out);
|
|
22
|
+
await collect(projects, root, cutoffMs, out, stats);
|
|
19
23
|
return out.sort();
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
// One unreadable directory costs only itself. Symlinks are not followed: Claude
|
|
23
27
|
// Code never writes one, and following one is how a loop or $HOME would get in.
|
|
24
|
-
async function collect(dir: string, entries: Dirent[], cutoffMs: number, out: string[]): Promise<void> {
|
|
28
|
+
async function collect(dir: string, entries: Dirent[], cutoffMs: number, out: string[], stats: ScanStats): Promise<void> {
|
|
25
29
|
for (const entry of entries) {
|
|
26
30
|
const full = join(dir, entry.name);
|
|
27
31
|
if (entry.isDirectory()) {
|
|
28
32
|
if (entry.name === "subagents") continue;
|
|
29
33
|
try {
|
|
30
|
-
await collect(full, await readdir(full, { withFileTypes: true }), cutoffMs, out);
|
|
34
|
+
await collect(full, await readdir(full, { withFileTypes: true }), cutoffMs, out, stats);
|
|
31
35
|
} catch {}
|
|
32
36
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
37
|
+
stats.files++;
|
|
33
38
|
try {
|
|
34
|
-
if ((await stat(full)).mtimeMs >= cutoffMs
|
|
39
|
+
if ((await stat(full)).mtimeMs >= cutoffMs) out.push(full);
|
|
40
|
+
else {
|
|
41
|
+
stats.tailChecks++;
|
|
42
|
+
if ((await lastTimestampMs(full)) >= cutoffMs) out.push(full);
|
|
43
|
+
}
|
|
35
44
|
} catch {}
|
|
36
45
|
}
|
|
37
46
|
}
|