parawatch 0.3.10 → 0.3.11
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/dist/adapters/claudeCode.js +48 -0
- package/dist/bin/cli.js +5 -1
- package/dist/core/ops.js +14 -1
- package/dist/core/snapshot.js +12 -1
- package/dist/core/store.js +24 -8
- package/package.json +1 -1
|
@@ -508,4 +508,52 @@ function metaIsCurrent(file) {
|
|
|
508
508
|
return false;
|
|
509
509
|
}
|
|
510
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Which run of a Claude Code session is running a command right now: the session itself, or one of
|
|
513
|
+
* its subagents. A subagent's commands get the session's own `CLAUDE_CODE_SESSION_ID`, so only the
|
|
514
|
+
* logs tell them apart: the run whose log ends on a pending Bash call matching `command`, the
|
|
515
|
+
* newest one if several do. Returns the subagent's id, or undefined for the session itself (or
|
|
516
|
+
* when the logs do not say).
|
|
517
|
+
*/
|
|
518
|
+
export function callingSubagent(session, command, now = Date.now()) {
|
|
519
|
+
const root = claudeProjectsDir();
|
|
520
|
+
let dirs;
|
|
521
|
+
try {
|
|
522
|
+
dirs = fs.readdirSync(root);
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
const pendingAt = (file) => {
|
|
528
|
+
try {
|
|
529
|
+
if (now - fs.statSync(file).mtimeMs > 120_000)
|
|
530
|
+
return undefined;
|
|
531
|
+
const recs = readTailLines(file, 64 * 1024).filter(isObj);
|
|
532
|
+
const last = recs[recs.length - 1];
|
|
533
|
+
if (!last || last.type !== "assistant")
|
|
534
|
+
return undefined;
|
|
535
|
+
const call = blocks(last).find((b) => b.type === "tool_use" && b.name === "Bash" && command.test(str(b.input?.command) ?? ""));
|
|
536
|
+
return call ? (str(last.timestamp) ?? new Date(fs.statSync(file).mtimeMs).toISOString()) : undefined;
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
for (const d of dirs) {
|
|
543
|
+
const own = path.join(root, d, `${session}.jsonl`);
|
|
544
|
+
if (!fs.existsSync(own))
|
|
545
|
+
continue;
|
|
546
|
+
let best;
|
|
547
|
+
const mine = pendingAt(own);
|
|
548
|
+
if (mine)
|
|
549
|
+
best = { at: mine };
|
|
550
|
+
for (const f of listJsonl(path.join(root, d, session, "subagents"))) {
|
|
551
|
+
const at = pendingAt(f);
|
|
552
|
+
if (at && (!best || at > best.at))
|
|
553
|
+
best = { id: path.basename(f, ".jsonl").replace(/^agent-/, ""), at };
|
|
554
|
+
}
|
|
555
|
+
return best?.id;
|
|
556
|
+
}
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
511
559
|
//# sourceMappingURL=claudeCode.js.map
|
package/dist/bin/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import { UserError, addProject, answerCard, beatRun, claimSession, closeCard, dismissCard, commentCard, inboxScope, editCard, endRun, hideRun, unhideRun, 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 { recordPrompt } from "../core/prompts.js";
|
|
8
|
+
import { callingSubagent } from "../adapters/claudeCode.js";
|
|
8
9
|
import { digest, digestText, localDate } from "../core/digest.js";
|
|
9
10
|
import { everywhere, readEvents } from "../core/store.js";
|
|
10
11
|
import { followEvents } from "../core/follow.js";
|
|
@@ -331,9 +332,12 @@ program
|
|
|
331
332
|
.option("--kind <kind>", "'scheduled' for a session a schedule starts again and again")
|
|
332
333
|
.option("--done <summary>", "the work is finished; one line on how it ended")
|
|
333
334
|
.action((session, o) => {
|
|
334
|
-
|
|
335
|
+
let id = session ?? currentSession();
|
|
335
336
|
if (!id)
|
|
336
337
|
throw new UserError("no session id given and none found in the environment (CLAUDE_CODE_SESSION_ID, CODEX_THREAD_ID)");
|
|
338
|
+
// a subagent runs with its session's id; its claim is its own, not a new name for the session
|
|
339
|
+
if (!session && process.env.CLAUDE_CODE_SESSION_ID === id)
|
|
340
|
+
id = callingSubagent(id, /\bparawatch\b.*\bclaim\b/) ?? id;
|
|
337
341
|
if (o.kind !== undefined && o.kind !== "scheduled")
|
|
338
342
|
throw new UserError("--kind can only be 'scheduled'");
|
|
339
343
|
const c = claimSession({ session: id, project: o.project, task: o.task, role: o.role, title: o.title, kind: o.kind, done: o.done });
|
package/dist/core/ops.js
CHANGED
|
@@ -610,13 +610,26 @@ export async function waitInbox(consumer, filter, timeoutMs, pollMs = 1000) {
|
|
|
610
610
|
const dir = eventsDir();
|
|
611
611
|
fs.mkdirSync(dir, { recursive: true });
|
|
612
612
|
watcher = fs.watch(dir, () => wake?.());
|
|
613
|
+
// on Windows a watched folder a sync tool writes into can report EPERM; polling goes on
|
|
614
|
+
watcher.on("error", () => {
|
|
615
|
+
watcher?.close();
|
|
616
|
+
watcher = undefined;
|
|
617
|
+
});
|
|
613
618
|
}
|
|
614
619
|
catch {
|
|
615
620
|
watcher = undefined; // polling alone still works
|
|
616
621
|
}
|
|
617
622
|
try {
|
|
618
623
|
for (;;) {
|
|
619
|
-
|
|
624
|
+
let items = [];
|
|
625
|
+
try {
|
|
626
|
+
items = readInbox(consumer, filter, true);
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
// a file busy for a moment is read again on the next poll; a wait must not end without an answer
|
|
630
|
+
if (!["EPERM", "EBUSY", "EACCES"].includes(err.code ?? ""))
|
|
631
|
+
throw err;
|
|
632
|
+
}
|
|
620
633
|
if (items.length)
|
|
621
634
|
return items;
|
|
622
635
|
const left = deadline - Date.now();
|
package/dist/core/snapshot.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { prViews, keyOf, parsePrRef, readCache, githubConfig, githubFailing } from "./github.js";
|
|
2
4
|
import { endedSessions, linkPr, listeners, readState } from "./ops.js";
|
|
3
5
|
import { collectRuns } from "./runs.js";
|
|
@@ -16,7 +18,12 @@ export function snapshot(opts = {}) {
|
|
|
16
18
|
state = readState();
|
|
17
19
|
const all = [...state.cards.values()].filter((c) => !opts.project || c.project === opts.project);
|
|
18
20
|
const ended = endedSessions(state);
|
|
19
|
-
const cards = [...all.filter((c) => c.status !== "closed"), ...recentlyClosed(all, opts.closed ?? 0)]
|
|
21
|
+
const cards = [...all.filter((c) => c.status !== "closed"), ...recentlyClosed(all, opts.closed ?? 0)]
|
|
22
|
+
.map((c) => (c.status !== "closed" && c.session && ended(c.session) ? { ...c, ownerEnded: true } : c))
|
|
23
|
+
.map((c) => {
|
|
24
|
+
const missing = notHere([...(c.attach ?? []), ...(c.open ?? [])]);
|
|
25
|
+
return missing.length ? { ...c, missing } : c;
|
|
26
|
+
});
|
|
20
27
|
quietAnsweredAsks(runs, all);
|
|
21
28
|
const cache = readCache();
|
|
22
29
|
const enabled = githubConfig().enabled && (cache.hosts === undefined ? false : cache.hosts.length > 0);
|
|
@@ -63,6 +70,10 @@ function linkCreatedPrs(runs, state) {
|
|
|
63
70
|
}
|
|
64
71
|
return added;
|
|
65
72
|
}
|
|
73
|
+
/** Files named by absolute path (on either system) that this machine does not have. */
|
|
74
|
+
function notHere(targets) {
|
|
75
|
+
return targets.filter((t) => (path.isAbsolute(t) || /^[A-Za-z]:[\\/]/.test(t)) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(t) && !fs.existsSync(t));
|
|
76
|
+
}
|
|
66
77
|
/** A question asked in chat is no longer pending once the session put it on a card. */
|
|
67
78
|
function quietAnsweredAsks(runs, cards) {
|
|
68
79
|
for (const r of runs) {
|
package/dist/core/store.js
CHANGED
|
@@ -150,17 +150,33 @@ export function withLock(fn) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
// ------------------------------------------------------------------ reading
|
|
153
|
+
/** What each log held at its last good reading, for a moment when it cannot be opened. */
|
|
154
|
+
const lastRead = new Map();
|
|
155
|
+
/** Errors a sync tool causes for a moment while it replaces a file (Windows mostly). */
|
|
156
|
+
const BUSY = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
153
157
|
function readFileEvents(f) {
|
|
154
158
|
let text;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
for (let attempt = 0; text === undefined; attempt++) {
|
|
160
|
+
try {
|
|
161
|
+
text = fs.readFileSync(f.file, "utf8");
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const code = err.code ?? "";
|
|
165
|
+
if (code === "ENOENT")
|
|
166
|
+
return [];
|
|
167
|
+
if (!BUSY.has(code))
|
|
168
|
+
throw err;
|
|
169
|
+
// Syncthing and cloud drives write a new copy and rename it over the old one; on Windows the
|
|
170
|
+
// file cannot be opened during the swap. Try again shortly, then go on with the last reading.
|
|
171
|
+
if (attempt < 5)
|
|
172
|
+
sleepSync(50);
|
|
173
|
+
else
|
|
174
|
+
return lastRead.get(f.file) ?? [];
|
|
175
|
+
}
|
|
162
176
|
}
|
|
163
|
-
|
|
177
|
+
const events = parseEventLines(text, f.device);
|
|
178
|
+
lastRead.set(f.file, events);
|
|
179
|
+
return events;
|
|
164
180
|
}
|
|
165
181
|
/**
|
|
166
182
|
* Every event, from every machine, in time order. Each machine's own order is kept as written;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parawatch",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "A control tower for people running many AI coding agents in parallel: see what is running, what needs you, and wake the right agent when you answer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|