parawatch 0.2.1 → 0.3.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/README.md CHANGED
@@ -267,12 +267,43 @@ parawatch watch --from 120 # starting after event 120
267
267
 
268
268
  ## Storage
269
269
 
270
- Everything lives in one append-only file, `~/.local/state/parawatch/events.jsonl` (`$PARAWATCH_HOME`, or `$XDG_STATE_HOME/parawatch`). Writers take a short lock, so several agents can write at once. Next to it: cursors in `cursors/`, running waits in `listeners/`, the last GitHub reading in `github.json`, the last update check in `update.json`, the version each copy (CLI, app) last ran in `clients/`, and the optional `config.json`. Delete the folder to start over.
270
+ Everything lives in one append-only file, `~/.local/state/parawatch/events.jsonl` (`$PARAWATCH_HOME`, or `$XDG_STATE_HOME/parawatch`). Writers take a short lock, so several agents can write at once. Next to it: cursors in `cursors/`, running waits in `listeners/`, the last GitHub reading in `github.json`, the last update check in `update.json`, the version each copy (CLI, app) last ran in `clients/`, and the optional `config.json`. Delete the folder to start over. A shared log lives in the shared folder instead; see [More than one machine](#more-than-one-machine).
271
271
 
272
272
  Every event carries a format number. A copy of parawatch reads a log that a newer copy wrote, skipping what it does not know, but refuses to write to it and says to update. The desktop app's own log is in `~/Library/Logs/Parawatch/main.log` (Help > Show Log File).
273
273
 
274
274
  Desktop notifications for new cards use `osascript` on macOS and `notify-send` on Linux. `PARAWATCH_NOTIFY=0` turns them off. While the desktop app runs it notifies instead, and the CLI stays quiet. To-dos never notify.
275
275
 
276
+ ## More than one machine
277
+
278
+ Agents on a second machine (a desktop PC, a build box) can ask and be answered like the ones on your laptop. Parawatch does not move files between machines itself: point it at a folder that something else keeps in step, and each machine writes only its own log there.
279
+
280
+ ```sh
281
+ # on each machine, once, with its own name
282
+ parawatch sync setup ~/agent-sync/parawatch --device mac --adapter syncthing
283
+ parawatch sync setup ~/agent-sync/parawatch --device pc --adapter syncthing
284
+ parawatch sync # this machine, the others, how the sync tool is doing
285
+ ```
286
+
287
+ - **Nothing moves until you run `sync setup`.** A machine on its own keeps today's single file and format.
288
+ - **`sync setup` moves this machine's log into the folder.** It becomes `events/<device>.jsonl` with every event, id and seq as it was, so cursors and open cards carry on. The old file is kept beside it as `events.jsonl.before-sync`. A copy of parawatch older than 0.3 on that machine stops writing until it is updated.
289
+ - **Each machine reads every log and writes only its own.** The logs merge in time order. Two machines never write one file, so a sync tool has nothing to merge.
290
+ - **Ids carry the machine once a log is shared:** `pc-c3`, `mac-r2`. Commands take the short form (`parawatch close c3`) while only one machine has that number. Cards from before the move keep their ids.
291
+ - **When two machines answer one card before either has seen the other's answer, the earlier answer stands.** The other one stays on the card, marked as not used, and does not wake the agent. An answer given after seeing the first one counts as usual.
292
+ - **Paths follow the machine.** Attachments, things to open and project folders written on another machine are read as the same place here, under its home folder or the shared folder. Name other folders that sit in different places on each machine (a cloud drive, say): `parawatch sync root gdrive "G:\My Drive"` on one machine and `parawatch sync root gdrive ~/Library/CloudStorage/…/My\ Drive` on the other. Pasted pictures are saved in the shared folder, so every machine can open them.
293
+ - **Waits are seen everywhere.** A card on your laptop says whether an agent on the PC is waiting for it.
294
+ - **Every machine reads GitHub for itself.** The same news is delivered once.
295
+
296
+ The shared folder holds `events/`, `devices/` (each machine's paths, version and last sign of life), `listeners/` and `attachments/`. Cursors, the lock, the GitHub reading and settings stay on each machine.
297
+
298
+ **Sync adapters** are how parawatch asks the sync tool how it is doing, for `parawatch sync`, `doctor` and the app's settings. Pick one with `--adapter` (or `"sync": { "adapter": … }` in `config.json`):
299
+
300
+ | Adapter | Reports |
301
+ |---|---|
302
+ | `syncthing` | Asks the local Syncthing through its REST API (address and API key from its `config.xml`; `PARAWATCH_SYNCTHING_KEY` or `"sync": { "syncthing": { "url", "home" } }` override): connected devices, when the folder was last in step, files on the way, errors. |
303
+ | `folder` | Anything else that keeps a folder in step (a cloud drive, a network share): the other machines' last writes and conflict copies. |
304
+
305
+ An adapter is one object with a `status()` method (`src/core/sync.ts`, `registerAdapter`). Another tool, such as a git checkout or a small server, fits the same shape.
306
+
276
307
  ## Not goals
277
308
 
278
309
  - Starting or steering agents. Use whatever you already use; parawatch only watches and carries answers back.
package/dist/bin/cli.js CHANGED
@@ -2,12 +2,13 @@
2
2
  import { execFileSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import { Command } from "commander";
5
- import { UserError, addProject, answerCard, beatRun, claimSession, closeCard, commentCard, consumerName, editCard, endRun, linkPr, listeners, listenersFor, noteRun, openCard, prEvent, readInbox, removeProject, startListening, startRun, unlinkPr, waitInbox, } from "../core/ops.js";
5
+ import { UserError, addProject, answerCard, beatRun, claimSession, closeCard, commentCard, consumerName, editCard, endRun, linkPr, listeners, listenersFor, noteRun, openCard, prEvent, findCard, readInbox, readState, removeProject, startListening, startRun, unlinkPr, waitInbox, } from "../core/ops.js";
6
6
  import { githubConfig, readCache, resolvePrRef, syncGithub, prViews } from "../core/github.js";
7
7
  import { digest, digestText, localDate } from "../core/digest.js";
8
- import { readEvents } from "../core/store.js";
8
+ import { everywhere, readEvents } from "../core/store.js";
9
9
  import { followEvents } from "../core/follow.js";
10
- import { fold } from "../core/state.js";
10
+ import { adapterNames, setRoot, setupSync, syncAdapter, syncStatus, touchDevice } from "../core/sync.js";
11
+ import { syncText } from "../core/format.js";
11
12
  import { collectRuns } from "../core/runs.js";
12
13
  import { askRows, cardDetail, cardLine, cardRows, inboxLine, prRows, runsTree, view } from "../core/format.js";
13
14
  import { hookContext, notify, removeInstructions, writeInstructions } from "../core/integrate.js";
@@ -42,6 +43,7 @@ function binPath() {
42
43
  }
43
44
  }
44
45
  recordClient({ name: "cli", version: VERSION, path: binPath() });
46
+ touchDevice(VERSION);
45
47
  const program = new Command();
46
48
  program
47
49
  .name("parawatch")
@@ -50,7 +52,7 @@ program
50
52
  /** Read GitHub when the last reading is old; never fails the command. */
51
53
  async function refreshGithub(force = false) {
52
54
  try {
53
- await syncGithub({ state: fold(readEvents()), force });
55
+ await syncGithub({ state: readState(), force });
54
56
  }
55
57
  catch (err) {
56
58
  if (force)
@@ -129,7 +131,7 @@ program
129
131
  .option("--copy", "also put a line about the card on the clipboard, to paste into that session")
130
132
  .option("--print", "print the link instead of opening it")
131
133
  .action((target, o) => {
132
- const g = resolveGoto(target, fold(readEvents()));
134
+ const g = resolveGoto(target, readState());
133
135
  if (o.copy && g.card) {
134
136
  const line = cardContextLine(g.card);
135
137
  try {
@@ -171,7 +173,7 @@ program
171
173
  .description("show one card with its answers")
172
174
  .option("--json", "JSON output")
173
175
  .action((id, o) => {
174
- const card = fold(readEvents()).cards.get(id);
176
+ const card = findCard(readState(), id);
175
177
  if (!card)
176
178
  throw new UserError(`no card "${id}"`);
177
179
  if (o.json) {
@@ -193,7 +195,7 @@ program
193
195
  .option("-a, --all", "include closed cards")
194
196
  .option("--json", "JSON output")
195
197
  .action((o) => {
196
- const cards = [...fold(readEvents()).cards.values()].filter((c) => (o.all || c.status !== "closed") && (!o.project || c.project === o.project));
198
+ const cards = [...readState().cards.values()].filter((c) => (o.all || c.status !== "closed") && (!o.project || c.project === o.project));
197
199
  if (o.json)
198
200
  print({ v: SCHEMA_VERSION, cards });
199
201
  else
@@ -239,9 +241,9 @@ inboxOptions(program.command("inbox").description("print unhandled answers witho
239
241
  program
240
242
  .command("watch")
241
243
  .description("stream every new event as one JSON line (for plugins and monitors)")
242
- .option("--from <seq>", "replay events after this sequence number first", int)
244
+ .option("--from <seq>", "replay events after this sequence number first (in every machine's log)", int)
243
245
  .action(async (o) => {
244
- followEvents(print, o.from);
246
+ followEvents(print, o.from === undefined ? undefined : everywhere(o.from, readEvents()));
245
247
  await new Promise(() => { });
246
248
  });
247
249
  // ------------------------------------------------------------------ runs
@@ -256,7 +258,7 @@ program
256
258
  .option("-w, --wide", "show runtime and ids")
257
259
  .option("--json", "JSON output")
258
260
  .action((o) => {
259
- const runs = collectRuns(fold(readEvents()), { sinceHours: o.since, project: o.project, includeEnded: o.all, runtimes: o.runtime, keepRepeats: o.every });
261
+ const runs = collectRuns(readState(), { sinceHours: o.since, project: o.project, includeEnded: o.all, runtimes: o.runtime, keepRepeats: o.every });
260
262
  if (o.json)
261
263
  print({ v: SCHEMA_VERSION, runs });
262
264
  else
@@ -322,7 +324,7 @@ pr.command("list")
322
324
  .action(async (o) => {
323
325
  if (!o.offline)
324
326
  await refreshGithub();
325
- const state = fold(readEvents());
327
+ const state = readState();
326
328
  const runs = collectRuns(state, { sinceHours: 72 });
327
329
  let prs = prViews(readCache(), state, runs);
328
330
  if (o.project)
@@ -335,7 +337,7 @@ pr.command("list")
335
337
  pr.command("sync")
336
338
  .description("read GitHub now")
337
339
  .action(async () => {
338
- const c = await syncGithub({ state: fold(readEvents()), force: true });
340
+ const c = await syncGithub({ state: readState(), force: true });
339
341
  if (c.error)
340
342
  process.stderr.write(`parawatch: ${c.error}\n`);
341
343
  process.stdout.write(`${c.prs.filter((p) => p.state === "open").length} open pull requests on ${(c.hosts ?? []).join(", ") || "no host"}\n`);
@@ -480,7 +482,7 @@ project
480
482
  .command("list")
481
483
  .option("--json", "JSON output")
482
484
  .action((o) => {
483
- const ps = [...fold(readEvents()).projects.values()];
485
+ const ps = [...readState().projects.values()];
484
486
  if (o.json)
485
487
  print({ v: SCHEMA_VERSION, projects: ps });
486
488
  else
@@ -513,6 +515,41 @@ program
513
515
  else
514
516
  process.stdout.write(`${doctorText(checks, view({}).color)}\n`);
515
517
  });
518
+ // ------------------------------------------------------------------ sharing between machines
519
+ const sync = program.command("sync").description("share cards and answers with your other machines through a synced folder");
520
+ sync
521
+ .command("status", { isDefault: true })
522
+ .description("how sharing is doing: this machine, the others, the sync tool")
523
+ .option("--json", "JSON output")
524
+ .action(async (o) => {
525
+ const st = await syncStatus();
526
+ if (o.json)
527
+ print({ v: SCHEMA_VERSION, sync: st ?? null });
528
+ else
529
+ process.stdout.write(syncText(st, view({}).color) + "\n");
530
+ });
531
+ sync
532
+ .command("setup <folder>")
533
+ .description("start writing to a shared folder; this machine's log moves there, nothing is lost")
534
+ .option("-d, --device <name>", "this machine's name in the folder (a-z, 0-9, -); defaults to the host name")
535
+ .option("-a, --adapter <name>", `the tool that carries the folder: ${adapterNames().join(", ")}`)
536
+ .action((folder, o) => {
537
+ const r = setupSync({ dir: folder, device: o.device, adapter: o.adapter, version: VERSION });
538
+ const lines = [
539
+ `This machine writes to ${r.dir} as "${r.device}" (adapter: ${r.adapter}, ${syncAdapter(r.adapter).about}).`,
540
+ r.moved ? `Moved ${r.moved} events from this machine's log; the old file is kept at ${r.backup}.` : "",
541
+ r.others.length ? `Other machines already here: ${r.others.join(", ")}.` : "No other machine has written here yet. Run the same command on the next one with its own --device.",
542
+ "Older parawatch copies (before 0.3) on this machine stop writing until they are updated.",
543
+ ];
544
+ process.stdout.write(lines.filter(Boolean).join("\n") + "\n");
545
+ });
546
+ sync
547
+ .command("root <name> [folder]")
548
+ .description("name a folder on this machine (a cloud drive, say) so paths under it open on the others; no folder removes it")
549
+ .action((name, folder) => {
550
+ const d = setRoot(name, folder, VERSION);
551
+ process.stdout.write(Object.entries(d.roots ?? {}).map(([k, v]) => `${k} ${v}`).join("\n") + (d.roots ? "\n" : "No named folders.\n"));
552
+ });
516
553
  program
517
554
  .command("hook <runtime>")
518
555
  .description("hook entry point; 'claude-code' reads the hook JSON on stdin")
@@ -10,9 +10,10 @@ import { ghHosts, githubConfig, readCache, runGh } from "./github.js";
10
10
  import { instructionState } from "./integrate.js";
11
11
  import { listeners } from "./ops.js";
12
12
  import { fold } from "./state.js";
13
- import { eventsPath, homeDir, readEvents, storeFormat } from "./store.js";
13
+ import { deviceName, eventFiles, homeDir, readEvents, sharedDir, storeFormat } from "./store.js";
14
+ import { readDevice, syncStatus } from "./sync.js";
14
15
  import { skillState, skillTargets } from "./skill.js";
15
- import { SCHEMA_VERSION } from "./types.js";
16
+ import { STORE_FORMAT } from "./types.js";
16
17
  import { updateChecksOn } from "./update.js";
17
18
  function claudeSettingsFiles() {
18
19
  const dir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
@@ -38,17 +39,39 @@ export async function doctor(input) {
38
39
  // the store
39
40
  const events = readEvents();
40
41
  let size = 0;
41
- try {
42
- size = fs.statSync(eventsPath()).size;
43
- }
44
- catch {
45
- // no events yet
42
+ for (const f of eventFiles()) {
43
+ try {
44
+ size += fs.statSync(f.file).size;
45
+ }
46
+ catch {
47
+ // no events yet
48
+ }
46
49
  }
47
50
  const format = storeFormat(events);
48
- if (format > SCHEMA_VERSION)
49
- add("store", "warn", `${eventsPath()} has format ${format}, newer than this copy reads (${SCHEMA_VERSION})`, CLI_UPDATE);
51
+ const shared = sharedDir();
52
+ const where = shared ? `${shared} (shared) · this machine ${homeDir()}` : homeDir();
53
+ if (format > STORE_FORMAT)
54
+ add("store", "warn", `${where} has format ${format}, newer than this copy reads (${STORE_FORMAT})`, CLI_UPDATE);
50
55
  else
51
- add("store", "ok", `${homeDir()} · ${events.length} events, ${bytes(size)}`);
56
+ add("store", "ok", `${where} · ${events.length} events, ${bytes(size)}`);
57
+ // other machines
58
+ const st = input.sync === undefined ? await syncStatus().catch(() => undefined) : input.sync ?? undefined;
59
+ if (!shared)
60
+ add("sync", "off", "this machine only", "to share with another machine: parawatch sync setup <folder> --device <name>");
61
+ else {
62
+ const me = deviceName();
63
+ const mine = readDevice(me);
64
+ if (mine && mine.host !== `${os.hostname()}:${os.homedir()}`)
65
+ add("sync", "warn", `another machine (${mine.host}) also writes as "${me}"`, "give each machine its own name: parawatch sync setup <folder> --device <name>");
66
+ if (!st)
67
+ add("sync", "info", `${me} · ${shared}`);
68
+ else {
69
+ const peers = st.peers.map((p) => `${p.name} ${p.connected === true ? "connected" : p.connected === false ? "not connected" : "seen"}`).join(", ");
70
+ add("sync", st.error || !st.ok ? "warn" : "ok", `${me} · ${st.adapter} · ${peers || "no other machine yet"}${st.error ? ` · ${st.error}` : ""}`);
71
+ if (st.conflicts.length)
72
+ add("sync", "warn", `${st.conflicts.length} conflict copy(ies): ${st.conflicts.slice(0, 3).join(", ")}`, "each machine writes only its own file; look for two machines using one device name");
73
+ }
74
+ }
52
75
  const state = fold(events);
53
76
  add("projects", state.projects.size ? "ok" : "info", state.projects.size ? [...state.projects.keys()].join(", ") : "none registered (claims still work)");
54
77
  // the desktop app
@@ -1,62 +1,49 @@
1
1
  import fs from "node:fs";
2
- import { eventsPath, homeDir, parseEventLines } from "./store.js";
2
+ import { endOf, eventFiles, eventsDir, isAfter, parseEventLines, readEvents } from "./store.js";
3
3
  /**
4
- * Call `onEvent` for every event appended after `afterSeq` (or after the current end when it is
5
- * undefined). Watches the store folder and also checks once a second. Returns a stop function.
4
+ * Call `onEvent` for every event appended after `after` (or after the current end when it is
5
+ * undefined), in any machine's log. Watches the log folder and also checks once a second, so a
6
+ * log a sync tool brings in from another machine shows up too. Returns a stop function.
6
7
  */
7
- export function followEvents(onEvent, afterSeq) {
8
- const file = eventsPath();
9
- let offset = 0;
10
- let lastSeq = afterSeq ?? Infinity;
11
- try {
12
- const text = fs.readFileSync(file, "utf8");
13
- offset = Buffer.byteLength(text);
14
- const events = parseEventLines(text);
15
- const end = events[events.length - 1]?.seq ?? 0;
16
- if (afterSeq === undefined)
17
- lastSeq = end;
18
- else {
19
- for (const ev of events)
20
- if (ev.seq > afterSeq)
21
- onEvent(ev);
22
- lastSeq = Math.max(afterSeq, end);
23
- }
24
- }
25
- catch {
26
- lastSeq = afterSeq ?? 0;
27
- }
8
+ export function followEvents(onEvent, after) {
9
+ const offsets = new Map();
10
+ let pos = after ?? endOf(readEvents());
28
11
  const pump = () => {
29
- let size = 0;
30
- try {
31
- size = fs.statSync(file).size;
32
- }
33
- catch {
34
- return;
35
- }
36
- if (size < offset)
37
- offset = 0; // file replaced
38
- if (size === offset)
39
- return;
40
- const fd = fs.openSync(file, "r");
41
- const buf = Buffer.alloc(size - offset);
42
- fs.readSync(fd, buf, 0, buf.length, offset);
43
- fs.closeSync(fd);
44
- const text = buf.toString("utf8");
45
- const cut = text.lastIndexOf("\n");
46
- if (cut < 0)
47
- return;
48
- offset += Buffer.byteLength(text.slice(0, cut + 1));
49
- for (const ev of parseEventLines(text.slice(0, cut + 1))) {
50
- if (ev.seq > lastSeq) {
12
+ for (const f of eventFiles()) {
13
+ let size = 0;
14
+ try {
15
+ size = fs.statSync(f.file).size;
16
+ }
17
+ catch {
18
+ continue;
19
+ }
20
+ let offset = offsets.get(f.file) ?? 0;
21
+ if (size < offset)
22
+ offset = 0; // file replaced
23
+ if (size === offset)
24
+ continue;
25
+ const fd = fs.openSync(f.file, "r");
26
+ const buf = Buffer.alloc(size - offset);
27
+ fs.readSync(fd, buf, 0, buf.length, offset);
28
+ fs.closeSync(fd);
29
+ const text = buf.toString("utf8");
30
+ const cut = text.lastIndexOf("\n");
31
+ if (cut < 0)
32
+ continue;
33
+ offsets.set(f.file, offset + Buffer.byteLength(text.slice(0, cut + 1)));
34
+ for (const ev of parseEventLines(text.slice(0, cut + 1), f.device)) {
35
+ if (!isAfter(ev, pos))
36
+ continue;
37
+ pos = { ...pos, [f.device]: ev.seq };
51
38
  onEvent(ev);
52
- lastSeq = ev.seq;
53
39
  }
54
40
  }
55
41
  };
42
+ pump();
56
43
  let watcher;
57
44
  try {
58
- fs.mkdirSync(homeDir(), { recursive: true });
59
- watcher = fs.watch(homeDir(), () => pump());
45
+ fs.mkdirSync(eventsDir(), { recursive: true });
46
+ watcher = fs.watch(eventsDir(), () => pump());
60
47
  }
61
48
  catch {
62
49
  watcher = undefined; // the timer alone still works
@@ -138,9 +138,13 @@ export function cardDetail(c) {
138
138
  if (c.from || c.session)
139
139
  out.push(`From: ${[c.from, c.session ? `session ${c.session.slice(0, 8)}` : ""].filter(Boolean).join(", ")}`);
140
140
  out.push(`Opened: ${new Date(c.openedAt).toLocaleString()}${c.since ? `, first came up ${new Date(c.since).toLocaleDateString()}` : ""}`);
141
+ if (c.dev)
142
+ out.push(`Machine: ${c.dev}`);
141
143
  for (const a of c.answers) {
142
144
  const choice = a.choice !== undefined ? ` chose ${a.choice}${c.options?.[a.choice - 1] ? ` (${c.options[a.choice - 1]})` : ""}` : "";
143
- out.push("", `${a.by}${choice}${a.comment ? `: ${a.comment}` : ""}`);
145
+ const where = a.dev ? ` on ${a.dev}` : "";
146
+ const late = a.late ? " (not used: given at the same time as the answer before it, which stands)" : "";
147
+ out.push("", `${a.by}${where}${choice}${a.comment ? `: ${a.comment}` : ""}${late}`);
144
148
  for (const f of a.attach ?? [])
145
149
  out.push(` attached ${f}`);
146
150
  }
@@ -348,4 +352,26 @@ export function runsTree(runs, v = PLAIN, now = Date.now(), indent = "") {
348
352
  }
349
353
  return out.join("\n");
350
354
  }
355
+ // ------------------------------------------------------------------ sharing
356
+ /** `parawatch sync status` as text. */
357
+ export function syncText(st, color = false, now = Date.now()) {
358
+ const v = { ...PLAIN, color };
359
+ if (!st)
360
+ return "This machine keeps its log to itself. To share it with another machine: parawatch sync setup <folder> --device <name>";
361
+ const lines = [
362
+ `${paint(v, st.device, "bold")} writes to ${st.dir}`,
363
+ `adapter ${st.adapter}${st.ok ? "" : paint(v, " (not reachable)", "yellow")}${st.lastSync ? ` · last in step ${ago(st.lastSync, now)} ago` : ""}${st.pending ? ` · ${st.pending} file(s) on the way` : ""}`,
364
+ ];
365
+ if (!st.peers.length)
366
+ lines.push("peers none yet");
367
+ for (const p of st.peers) {
368
+ const state = p.connected === true ? paint(v, "connected", "green") : p.connected === false ? paint(v, "not connected", "yellow") : "seen";
369
+ lines.push(`peer ${p.name} ${state}${p.lastSeen && p.connected !== true ? ` · last ${ago(p.lastSeen, now)} ago` : ""}${p.address ? ` · ${p.address}` : ""}${p.version ? ` · parawatch ${p.version}` : ""}`);
370
+ }
371
+ if (st.conflicts.length)
372
+ lines.push(paint(v, `conflicts ${st.conflicts.length}:`, "yellow"), ...st.conflicts.map((c) => ` ${c}`));
373
+ if (st.error)
374
+ lines.push(paint(v, st.error, "yellow"));
375
+ return lines.join("\n");
376
+ }
351
377
  //# sourceMappingURL=format.js.map
@@ -1,9 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { readInbox, inboxFrom } from "./ops.js";
4
+ import { readInbox, inboxFrom, readState } from "./ops.js";
5
5
  import { homeDir, readCursor, readEvents } from "./store.js";
6
- import { fold } from "./state.js";
7
6
  import { inboxLine } from "./format.js";
8
7
  export const BEGIN = "<!-- parawatch:begin -->";
9
8
  export const END = "<!-- parawatch:end -->";
@@ -97,7 +96,7 @@ export function instructionState(file) {
97
96
  */
98
97
  export function hookContext(input, project) {
99
98
  const events = readEvents();
100
- const state = fold(events);
99
+ const state = readState(events);
101
100
  let name = project;
102
101
  if (!name && input.session_id)
103
102
  name = state.claims.find((c) => input.session_id.startsWith(c.session))?.project;
@@ -108,7 +107,7 @@ export function hookContext(input, project) {
108
107
  }
109
108
  if (!name)
110
109
  return undefined;
111
- const items = inboxFrom(events, readCursor(name), { project: name });
110
+ const items = inboxFrom(events, readCursor(name), { project: name }, state);
112
111
  if (!items.length)
113
112
  return undefined;
114
113
  const cards = items.filter((i) => i.kind !== "pr").length;