agent-lookout 0.2.6 → 0.2.7

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.
@@ -167,7 +167,7 @@ async function runCommand(options) {
167
167
  return EXIT.notKnown;
168
168
  }
169
169
  if (parsed.kind === "start") {
170
- const { startCommand } = await import("./startCommand-PR5JPWCT.js");
170
+ const { startCommand } = await import("./startCommand-3LYUYPG4.js");
171
171
  const url = await startCommand({
172
172
  ...parsed.options,
173
173
  distDir: options.distDir ?? fileURLToPath(new URL("../../dist/", import.meta.url)),
@@ -254,10 +254,30 @@ function createQuietHold() {
254
254
  const last = lastWaitOf(session.id);
255
255
  if (last?.begunAt === begunAt) {
256
256
  last.endedAt = null;
257
+ last.reminderOnly = false;
257
258
  return;
258
259
  }
259
260
  if (last && last.endedAt === null) last.endedAt = begunAt;
260
- waits.push({ session: withoutWaitingText(session), begunAt, endedAt: null });
261
+ waits.push({
262
+ session: withoutWaitingText(session),
263
+ begunAt,
264
+ endedAt: null,
265
+ reminderOnly: false
266
+ });
267
+ },
268
+ holdReminder(session, begunAt) {
269
+ const last = lastWaitOf(session.id);
270
+ if (last?.begunAt === begunAt) {
271
+ last.endedAt = null;
272
+ return;
273
+ }
274
+ if (last && last.endedAt === null) last.endedAt = begunAt;
275
+ waits.push({
276
+ session: withoutWaitingText(session),
277
+ begunAt,
278
+ endedAt: null,
279
+ reminderOnly: true
280
+ });
261
281
  },
262
282
  holdOver(event, session, at) {
263
283
  over.push({ event, session: withoutWaitingText(session), at });
@@ -279,6 +299,7 @@ function createQuietHold() {
279
299
  }
280
300
  const open2 = [];
281
301
  for (const [id, wait] of stillOpen) {
302
+ if (wait.reminderOnly) continue;
282
303
  open2.push({ session: listed.get(id), begunAt: wait.begunAt });
283
304
  }
284
305
  const answered = /* @__PURE__ */ new Map();
@@ -336,6 +357,7 @@ var DAY_MS = 24 * HOUR_MS;
336
357
  var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
337
358
  var TIME_RULE_NAMES = ["longWait", "idle", "quietHours"];
338
359
  var LONG_WAIT_MINUTES = { min: 1, max: 1440, default: 10 };
360
+ var REPEAT_MINUTES = { min: 5, max: 1440, default: 30 };
339
361
  var IDLE_HOURS = { min: 1, max: 720, default: 48 };
340
362
  var DEFAULT_TIME_RULES = {
341
363
  longWait: { on: false, minutes: LONG_WAIT_MINUTES.default },
@@ -365,12 +387,22 @@ function holds(value, keys, exact2) {
365
387
  function wholeNumberIn(value, range) {
366
388
  return Number.isSafeInteger(value) && value >= range.min && value <= range.max;
367
389
  }
368
- function readLongWait(value, exact2) {
390
+ function readRepeat(value, exact2) {
369
391
  if (!isRecord(value) || !holds(value, ["on", "minutes"], exact2)) return null;
370
392
  const { on, minutes } = value;
371
- if (typeof on !== "boolean" || !wholeNumberIn(minutes, LONG_WAIT_MINUTES)) return null;
393
+ if (typeof on !== "boolean" || !wholeNumberIn(minutes, REPEAT_MINUTES)) return null;
372
394
  return { on, minutes };
373
395
  }
396
+ function readLongWait(value, exact2) {
397
+ if (!isRecord(value)) return null;
398
+ const keys = value.repeat === void 0 ? ["on", "minutes"] : ["on", "minutes", "repeat"];
399
+ if (!holds(value, keys, exact2)) return null;
400
+ const { on, minutes } = value;
401
+ if (typeof on !== "boolean" || !wholeNumberIn(minutes, LONG_WAIT_MINUTES)) return null;
402
+ if (value.repeat === void 0) return { on, minutes };
403
+ const repeat = readRepeat(value.repeat, exact2);
404
+ return repeat === null ? null : { on, minutes, repeat };
405
+ }
374
406
  function readIdle(value, exact2) {
375
407
  if (!isRecord(value) || !holds(value, ["on", "hours"], exact2)) return null;
376
408
  const { on, hours } = value;
@@ -402,7 +434,7 @@ var READERS = {
402
434
  quietHours: readQuietHours
403
435
  };
404
436
  var SHAPES = {
405
- longWait: `longWait must be {"on", "minutes"}, with minutes a whole number from ${LONG_WAIT_MINUTES.min} to ${LONG_WAIT_MINUTES.max}.`,
437
+ longWait: `longWait must be {"on", "minutes"}, with minutes a whole number from ${LONG_WAIT_MINUTES.min} to ${LONG_WAIT_MINUTES.max}, and may have "repeat", {"on", "minutes"} with minutes a whole number from ${REPEAT_MINUTES.min} to ${REPEAT_MINUTES.max}.`,
406
438
  idle: `idle must be {"on", "hours"}, with hours a whole number from ${IDLE_HOURS.min} to ${IDLE_HOURS.max}.`,
407
439
  quietHours: 'quietHours must be {"on", "from", "to", "days", "leaveOutAnswered"}, with from and to two different times written as 22:00 is, and days any of mon, tue, wed, thu, fri, sat and sun, each once.'
408
440
  };
@@ -467,6 +499,18 @@ function quietOf(snapshot) {
467
499
  }
468
500
 
469
501
  // src/core/time-rules/reminders.ts
502
+ var MINUTE_MS = 6e4;
503
+ function reminderSchedule(rule) {
504
+ return {
505
+ thresholdMs: rule.minutes * MINUTE_MS,
506
+ everyMs: rule.repeat?.on ? rule.repeat.minutes * MINUTE_MS : null
507
+ };
508
+ }
509
+ function reminderNumber(waitedMs, schedule) {
510
+ if (waitedMs < schedule.thresholdMs) return -1;
511
+ if (schedule.everyMs === null) return 0;
512
+ return Math.floor((waitedMs - schedule.thresholdMs) / schedule.everyMs);
513
+ }
470
514
  function readable(snapshot) {
471
515
  return new Set(
472
516
  snapshot.sources.filter((source) => source.state === "ok" || source.state === "not-set-up").map((source) => source.id)
@@ -485,29 +529,33 @@ function createReminderWatch() {
485
529
  if (known && (since === null || since === known.begunAt)) continue;
486
530
  watched.set(session.id, {
487
531
  begunAt: since ?? at,
488
- firstSeenAfterMs: since === null ? 0 : at - since,
489
- reminded: /* @__PURE__ */ new Set()
532
+ seenAfterMs: since === null ? 0 : at - since,
533
+ toldAfterMs: null
490
534
  });
491
535
  }
492
536
  },
493
- due(snapshot, at, thresholdMs, eligible = () => true) {
537
+ due(snapshot, at, schedule, eligible = () => true) {
494
538
  const found = [];
495
539
  for (const session of snapshot.sessions) {
496
540
  if (!needsYou(session)) continue;
497
541
  const wait = watched.get(session.id);
498
- if (!wait || wait.reminded.has(thresholdMs) || !eligible(session.id)) continue;
542
+ if (!wait || !eligible(session.id)) continue;
499
543
  const waitedMs = at - wait.begunAt;
500
- if (waitedMs < thresholdMs || wait.firstSeenAfterMs >= thresholdMs) continue;
501
- found.push({ session, begunAt: wait.begunAt, waitedMs });
544
+ const repeat = reminderNumber(waitedMs, schedule);
545
+ if (repeat < 0 || repeat <= reminderNumber(wait.seenAfterMs, schedule)) continue;
546
+ const told = wait.toldAfterMs;
547
+ if (told !== null && repeat <= reminderNumber(told, schedule)) continue;
548
+ const { thresholdMs, everyMs } = schedule;
549
+ if (told !== null && everyMs !== null && repeat > 0) {
550
+ if (thresholdMs + repeat * everyMs - told < everyMs / 2) continue;
551
+ }
552
+ found.push({ session, begunAt: wait.begunAt, waitedMs, repeat });
502
553
  }
503
554
  return found;
504
555
  },
505
- reminded(sessionId, thresholdMs) {
506
- watched.get(sessionId)?.reminded.add(thresholdMs);
507
- },
508
- toldLate(sessionId, at, thresholdMs) {
556
+ told(sessionId, at) {
509
557
  const wait = watched.get(sessionId);
510
- if (wait && at - wait.begunAt >= thresholdMs) wait.reminded.add(thresholdMs);
558
+ if (wait) wait.toldAfterMs = Math.max(wait.toldAfterMs ?? 0, at - wait.begunAt);
511
559
  },
512
560
  clear() {
513
561
  watched.clear();
@@ -609,7 +657,7 @@ function createOutboundChannel(options) {
609
657
  sentAt.push(at);
610
658
  send2(write(at));
611
659
  }
612
- const thresholdMs = rules.longWait.minutes * 6e4;
660
+ const schedule = reminderSchedule(rules.longWait);
613
661
  for (const [id, begunAt] of open2) {
614
662
  const session = snapshot.sessions.find(
615
663
  (candidate) => candidate.id === id && needsYou(candidate)
@@ -635,13 +683,17 @@ function createOutboundChannel(options) {
635
683
  now: at
636
684
  })
637
685
  );
638
- if (rules.longWait.on) reminders.toldLate(id, at, thresholdMs);
686
+ if (rules.longWait.on) reminders.told(id, at);
639
687
  }
640
- if (!quiet && rules.longWait.on && wanted.has("needs-you")) {
641
- for (const due of reminders.due(snapshot, at, thresholdMs, (id) => !open2.has(id))) {
688
+ if (rules.longWait.on && wanted.has("needs-you")) {
689
+ for (const due of reminders.due(snapshot, at, schedule, (id) => !open2.has(id))) {
690
+ if (quiet) {
691
+ hold.holdReminder(due.session, due.begunAt);
692
+ continue;
693
+ }
642
694
  if (limitLiftsAt(sentAt, at) !== null) break;
643
695
  sentAt.push(at);
644
- reminders.reminded(due.session.id, thresholdMs);
696
+ reminders.told(due.session.id, at);
645
697
  send2(
646
698
  reminderMessage({
647
699
  session: withoutWaitingText(due.session),
@@ -649,7 +701,9 @@ function createOutboundChannel(options) {
649
701
  asking: asking ? waitingText(due.session.waitingText) ?? null : null,
650
702
  begunAt: due.begunAt,
651
703
  now: at,
652
- thresholdMs
704
+ thresholdMs: schedule.thresholdMs,
705
+ everyMs: schedule.everyMs,
706
+ repeat: due.repeat
653
707
  })
654
708
  );
655
709
  }
@@ -669,9 +723,9 @@ function createOutboundChannel(options) {
669
723
  }
670
724
 
671
725
  // src/core/time-rules/timeRulesWords.ts
672
- var MINUTE_MS = 6e4;
726
+ var MINUTE_MS2 = 6e4;
673
727
  function waitedInWords(ms) {
674
- if (ms < MINUTE_MS) return "under a minute";
728
+ if (ms < MINUTE_MS2) return "under a minute";
675
729
  return partsInWords(durationParts(ms).filter((part) => part.unit !== "s"));
676
730
  }
677
731
  var WAITED_FOR = {
@@ -873,9 +927,10 @@ function waitEmail(facts) {
873
927
  }
874
928
  function reminderEmail(facts) {
875
929
  const { session, begunAt, now } = facts;
930
+ const again = facts.everyMs === null ? "" : `, and again every ${waitedInWords(facts.everyMs)} while it goes on`;
876
931
  return emailOf(
877
932
  reminderSentence(session, now - begunAt),
878
- `It has waited since ${clockAt(begunAt, now)}. Agent Lookout reminds you once a wait lasts ${waitedInWords(facts.thresholdMs)}, as Time rules in Settings says.`,
933
+ `It has waited since ${clockAt(begunAt, now)}. Agent Lookout reminds you once a wait lasts ${waitedInWords(facts.thresholdMs)}${again}, as Time rules in Settings says.`,
879
934
  session,
880
935
  facts.agent,
881
936
  facts.asking
@@ -1634,7 +1689,9 @@ function reminderPost(facts) {
1634
1689
  post.session.app,
1635
1690
  post.session.agent
1636
1691
  ]),
1637
- reminder: true
1692
+ reminder: true,
1693
+ // Left out on the first, so its post is the post it always was.
1694
+ ...facts.repeat > 0 && { repeat: facts.repeat }
1638
1695
  };
1639
1696
  }
1640
1697
  function summaryPost(facts) {
@@ -9217,7 +9274,7 @@ function createServerNotifications(options) {
9217
9274
  const at = now();
9218
9275
  const rules = rulesOf(snapshot);
9219
9276
  const quietNow = quietOf(snapshot);
9220
- const thresholdMs = rules.longWait.minutes * 6e4;
9277
+ const schedule = reminderSchedule(rules.longWait);
9221
9278
  const result = sessionChanges(memory, snapshot);
9222
9279
  memory = result.memory;
9223
9280
  for (const id of result.stopped) {
@@ -9243,7 +9300,7 @@ function createServerNotifications(options) {
9243
9300
  change: { event: "needs-you", session },
9244
9301
  since: at
9245
9302
  });
9246
- if (rules.longWait.on) reminders.toldLate(session.id, at, thresholdMs);
9303
+ if (rules.longWait.on) reminders.told(session.id, at);
9247
9304
  }
9248
9305
  }
9249
9306
  if (quietNow) quiet.begin(at);
@@ -9253,9 +9310,13 @@ function createServerNotifications(options) {
9253
9310
  else
9254
9311
  held.set(heldKey(change.event, change.session.id), { kind: "change", change, since: at });
9255
9312
  }
9256
- if (!quietNow && rules.longWait.on && wants("needs-you")) {
9257
- for (const reminder of reminders.due(snapshot, at, thresholdMs)) {
9258
- reminders.reminded(reminder.session.id, thresholdMs);
9313
+ if (rules.longWait.on && wants("needs-you")) {
9314
+ for (const reminder of reminders.due(snapshot, at, schedule)) {
9315
+ if (quietNow) {
9316
+ quiet.holdReminder(reminder.session, reminder.begunAt);
9317
+ continue;
9318
+ }
9319
+ reminders.told(reminder.session.id, at);
9259
9320
  held.set(reminderKey(reminder.session.id), { kind: "reminder", reminder, since: at });
9260
9321
  }
9261
9322
  }
package/dist/index.html CHANGED
@@ -28,7 +28,7 @@
28
28
  document.documentElement.setAttribute("data-theme", theme);
29
29
  })();
30
30
  </script>
31
- <script type="module" crossorigin src="/assets/index-QSfCI-L_.js"></script>
31
+ <script type="module" crossorigin src="/assets/index-DLPmOXDe.js"></script>
32
32
  <link rel="modulepreload" crossorigin href="/assets/react-C2DiJaR6.js">
33
33
  <link rel="modulepreload" crossorigin href="/assets/motion-BAhdRuCw.js">
34
34
  <link rel="modulepreload" crossorigin href="/assets/radix-BH0JH8UU.js">
package/docs/API.md CHANGED
@@ -335,7 +335,7 @@ The settings Agent Lookout keeps itself, in a file on this computer, so they hol
335
335
  ```json
336
336
  {
337
337
  "timeRules": {
338
- "longWait": { "on": true, "minutes": 10 },
338
+ "longWait": { "on": true, "minutes": 10, "repeat": { "on": true, "minutes": 30 } },
339
339
  "idle": { "on": true, "hours": 48 },
340
340
  "quietHours": {
341
341
  "on": true,
@@ -370,6 +370,7 @@ The settings Agent Lookout keeps itself, in a file on this computer, so they hol
370
370
  | Field | Holds |
371
371
  | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
372
372
  | `timeRules.longWait` | The long wait reminder: `on`, and `minutes`, a whole number from 1 to 1,440, how long a wait lasts before it is reminded of. 10 unless set |
373
+ | `longWait.repeat` | Whether the reminder goes again while the wait goes on: `on`, and `minutes`, a whole number from 5 to 1,440, how long from one reminder to the next. Left out until it is first set, which is the same as off, so a file from before it reads as it did. The Time rules card sets 30 when it is first turned on |
373
374
  | `timeRules.idle` | The idle rule: `on`, and `hours`, a whole number from 1 to 720, how long a session is idle before it is stale. 48 unless set. While it is off a session is stale after 24 hours |
374
375
  | `timeRules.quietHours` | Quiet hours: `on`, `from` and `to` on the 24-hour clock as `HH:MM`, two different times, which run past midnight when `to` is the earlier, and `days`, the days they begin on, any of `mon` to `sun`. 22:00 to 08:00 every day unless set |
375
376
  | `quietHours.leaveOutAnswered` | Whether the summary sent when quiet hours end leaves out the waits that were answered while they held |
@@ -391,7 +392,7 @@ A route that acts. It puts the time rules in force and saves them in the setting
391
392
  - is marked `same-origin` in `Sec-Fetch-Site`, when that header is sent, or gets 403.
392
393
  - carries `X-Agent-Lookout-Action: time-rules`, or gets 403.
393
394
  - has `Content-Type: application/json`, or gets 415.
394
- - has a body of 2,048 bytes or less, or gets 413, that is exactly the three rules, `longWait`, `idle` and `quietHours`, each whole, with exactly the fields `GET /api/settings` gives and every value in range, or gets 400.
395
+ - has a body of 2,048 bytes or less, or gets 413, that is exactly the three rules, `longWait`, `idle` and `quietHours`, each whole, with exactly the fields `GET /api/settings` gives and every value in range, or gets 400. `longWait.repeat` may be left out, which is no repeat, and when it is there it is `{ "on", "minutes" }` whole.
395
396
 
396
397
  The rules are in force once they are saved, and not before: the file is written whole, with mode 600, in a folder made with mode 700, and never through a link. Nothing in the request names a file.
397
398
 
package/docs/GUIDE.md CHANGED
@@ -2,13 +2,7 @@
2
2
 
3
3
  ## Requirements
4
4
 
5
- - macOS, Linux or Windows. Agent Lookout is developed and tested on macOS. On Linux and on Windows it has been checked in CI by starting it with `npm start`, and not yet by a person on a Linux or Windows desktop. [On Linux](#on-linux) and [On Windows](#on-windows) say what differs there.
6
- - Node.js 22.12 or newer. The [Mac app](#desktop-app) needs no Node.js.
7
- - Claude Code, Codex or both. Neither needs any setup. Any other agent can appear too, by writing a status file: see [Your own agents](#your-own-agents).
8
- - For Claude Code, a version that has the `claude agents` command. `claude agents --help` should print `Usage: claude agents`. Without that command Agent Lookout still reads the session files, but cannot list background jobs that have finished or failed.
9
- - For Codex, version 0.155 or later, so that Agent Lookout can tell a session that has ended from one that is idle.
10
-
11
- The [README](../README.md#install) has the steps to install and start it.
5
+ Node.js 22.12 or newer, or the Mac app, which needs none, and Claude Code, Codex or both. [INSTALL.md](INSTALL.md) has the versions each needs, how to check them, and the steps to install and start Agent Lookout.
12
6
 
13
7
  ## The screen
14
8
 
@@ -320,15 +314,17 @@ The browser gives its permission to the address, not to Agent Lookout. Another p
320
314
 
321
315
  The Time rules card has three rules, each with an Off and On switch, and all three are off until you turn them on. Agent Lookout keeps them in `~/.agent-lookout/settings.json`, so they hold with no dashboard tab open and after a restart. A change takes effect at once. The card shows what Agent Lookout saved: if a change could not be saved, a note says why and the switch stays as it was. A field is read when you leave it or press Enter, and Escape puts back what it held.
322
316
 
323
- Remind me of a long wait sends a reminder once a session has waited the minutes you set: 10 unless you change them, from 1 to 1,440. It goes once for each wait, the way the wait's first notice went: as a notification while Needs you is switched on, as an email or a webhook post while `needs-you` is among the events those send. It says how long the session has waited. The notification reads "Has waited 10 minutes for permission" under the session's name, takes the place of the wait's own, and is cleared when the session moves on. The email's subject and the post's line say "checkout-flow has waited 10 minutes for permission", and the post has `reminder: true`. A wait that had already gone on longer when the page opened, or when Agent Lookout started, is not reminded of, and nor is one whose first email or post went only once it had waited that long, since that one says how long already. Set more minutes while a session waits, and it is reminded of again once it has waited those.
317
+ Remind me of a long wait sends a reminder once a session has waited the minutes you set: 10 unless you change them, from 1 to 1,440. It goes once for each wait, the way the wait's first notice went: as a notification while Needs you is switched on, as an email or a webhook post while `needs-you` is among the events those send. Remind again, below, sends it again while the wait goes on. It says how long the session has waited. The notification reads "Has waited 10 minutes for permission" under the session's name, takes the place of the wait's own, and is cleared when the session moves on. The email's subject and the post's line say "checkout-flow has waited 10 minutes for permission", and the post has `reminder: true`. A wait that had already gone on longer when the page opened, or when Agent Lookout started, is not reminded of at those minutes, and nor is one whose first email or post went only once it had waited that long, since that one says how long already. Set more minutes while a session waits, and it is reminded of again once it has waited those.
318
+
319
+ Under it, Remind again while it waits sends the reminder again while the session still waits: every 30 minutes unless you change them, from 5 to 1,440. It is off until you turn it on. With a reminder after 10 minutes and again every 30, a session that keeps waiting is reminded of at 10 minutes, 40 minutes, 1 hour 10 minutes and so on, each time by the same channels as the first, saying how long it has waited by then, as in "checkout-flow has waited 40 minutes for permission". On the page each notification takes the place of the last. The ones Agent Lookout shows itself, with no dashboard tab open, cannot be taken down, so each stays in Notification Centre until you clear it: with Remind again set to 5 minutes, that can be 12 an hour for one wait. The email's subject is worded as the first reminder's, and the post has `reminder: true` and `repeat`, which counts the intervals since the first reminder's time: 1 at 40 minutes, 2 at 1 hour 10 minutes. After a sleep it can skip numbers. No reminder comes once the wait is answered, from the session itself, with Allow or Deny or by a permission rule, or once the session ends. The times go by how long the wait has lasted, so each channel, and a dashboard tab opened later, reminds at the same moments. A wait has one reminder at a time: after the computer has slept, or when quiet hours end, one comes, saying how long the session has waited by then, and none for the moments missed, and a moment that falls less than half the interval after it is skipped. A wait that had already passed its minutes when Agent Lookout started has its next reminder at the next of its moments, not at once.
324
320
 
325
321
  Mark idle sessions stale sets how long a session is idle before it is stale, in hours or days, from an hour to 30 days: 2 days unless you change them. A stale session is counted under Stale rather than Idle, its idle line in the Timeline turns to dots where it became stale, and a Claude Code session among them whose process still runs is listed under [Left running](#sessions-left-running), where it can be ended. With the rule off, a session is stale after a day.
326
322
 
327
- Quiet hours hold back every notification, email and post between two times on your computer's clock: 22:00 to 08:00 unless you change them. When the end comes before the start, they run past midnight, and the days you tick are the days they begin on: with Monday to Friday ticked, Friday night is quiet until Saturday morning, and Saturday night is not. With no day ticked they never begin. When they end, a session still waiting is notified as usual, and everything else held comes as one summary on each channel, such as "While quiet: checkout-flow waited 25 minutes, docs-site waited 1 hour 5 minutes over 2 waits and billing-webhooks finished": one notification headed While quiet, which stays until you clear it, one email and one webhook post. A session that waited and then finished, failed or ended is one item, as in "checkout-flow waited 25 minutes then ended". Each summary holds what its channel would have sent, so a wait answered before the email's delay was up is in no email. Turn on Leave answered waits out of the summary and it names only the sessions that finished, failed or ended. With nothing to say, there is no summary. Turning quiet hours off while they hold ends them at once. While they hold, a line under the times says so: "Quiet now, until 08:00. Notifications, emails and posts are held." In the Mac app, the notice of a new version waits for them to end too.
323
+ Quiet hours hold back every notification, email and post between two times on your computer's clock: 22:00 to 08:00 unless you change them. When the end comes before the start, they run past midnight, and the days you tick are the days they begin on: with Monday to Friday ticked, Friday night is quiet until Saturday morning, and Saturday night is not. With no day ticked they never begin. When they end, a session still waiting is notified as usual, or reminded of if it was told of before they began and its reminder came due in them, and everything else held comes as one summary on each channel, such as "While quiet: checkout-flow waited 25 minutes, docs-site waited 1 hour 5 minutes over 2 waits and billing-webhooks finished": one notification headed While quiet, which stays until you clear it, one email and one webhook post. A session that waited and then finished, failed or ended is one item, as in "checkout-flow waited 25 minutes then ended". A wait whose reminders came due in quiet hours and that was answered before they ended is one item too, however many came due. Each summary holds what its channel would have sent, so a wait answered before the email's delay was up is in no email. Turn on Leave answered waits out of the summary and it names only the sessions that finished, failed or ended. With nothing to say, there is no summary. Turning quiet hours off while they hold ends them at once. While they hold, a line under the times says so: "Quiet now, until 08:00. Notifications, emails and posts are held." In the Mac app, the notice of a new version waits for them to end too.
328
324
 
329
325
  The rules apply on top of the switches under Notifications, and of the settings for [email](#email) and the [webhook](#webhook), which still choose what goes where. Quiet hours go by the clock and time zone of the computer Agent Lookout runs on, and a dashboard tab follows what Agent Lookout says, so a tab in another time zone holds and sums up at the same moments. A dashboard tab opened during quiet hours sums up what happened after it opened. With no tab open, Agent Lookout shows its own summary on a Mac, and it covers all of the quiet hours. The rules set here cover the sessions of [another machine](#another-machine-over-ssh) too, as they cover this computer's.
330
326
 
331
- What quiet hours hold is kept in memory only. If Agent Lookout is stopped or restarted while they hold, what it held so far is forgotten: no summary comes of those hours, and a session that began waiting before the restart and still waits is not notified when they end. With the long wait reminder on, it is still reminded of, if it had not yet waited the reminder's minutes at the restart.
327
+ What quiet hours hold is kept in memory only. If Agent Lookout is stopped or restarted while they hold, what it held so far is forgotten: no summary comes of those hours, and a session that began waiting before the restart and still waits is not notified when they end. With the long wait reminder on, it is still reminded of, if it had not yet waited the reminder's minutes at the restart, or with Remind again on, at its next moment.
332
328
 
333
329
  `AGENT_LOOKOUT_SETTINGS_FILE` keeps the rules in another file. Agent Lookout reads the file as it starts, and again just before each change, which it makes to what the file holds then. It writes it whole when you change a rule, with mode 600, in a folder it makes with mode 700, and never through a link. If it cannot read the file, the rules it holds are off, the card says why in a note, and the terminal you started Agent Lookout in prints the same line. A file that is not JSON is written again by your first change. One it could not read at all, such as one larger than 64 KB, is never written over: a change is not saved until you mend or remove the file. Two copies running at once, such as the Mac app and `npx agent-lookout`, share the file. Each looks at it at every poll and reads it again when the other has saved a change, so a change made in one is in force in the other by its next poll, usually within 2 seconds. A change to the permission rules is made to the list the file holds just then, so it keeps the other copy's. The Time rules card saves all three rules at once, so a time rule changed in one copy before the other copy's change has reached it puts that change back. After changing the time rules in one copy, wait until the other copy's card shows the change before changing them there. [PRIVACY.md](../PRIVACY.md#the-settings-file) says what the file holds.
334
330
 
@@ -931,7 +927,7 @@ In a clone without `npm link`, give the path to yours:
931
927
  claude mcp add agent-lookout -- /path/to/agent-lookout/bin/agent-lookout.mjs mcp
932
928
  ```
933
929
 
934
- Claude Code adds it for the folder you run that in. Put `--scope user` after `add` to have it in every folder. Any other app that takes MCP servers starts it with the same command: the program `npx` with the arguments `-y`, `agent-lookout` and `mcp`, or `agent-lookout` with the one argument `mcp`. Many take it as JSON in this shape:
930
+ Claude Code adds it for the folder you run that in. Put `--scope user` after `add` to have it in every folder. `claude mcp list` then lists `agent-lookout`. Any other app that takes MCP servers starts it with the same command: the program `npx` with the arguments `-y`, `agent-lookout` and `mcp`, or `agent-lookout` with the one argument `mcp`. Many take it as JSON in this shape:
935
931
 
936
932
  ```json
937
933
  {
@@ -1314,60 +1310,17 @@ npm run dev -- --port 5180
1314
1310
 
1315
1311
  ## Run the built version
1316
1312
 
1317
- In a clone, `npm run dev` runs Agent Lookout with its development tools. To run it from built files instead:
1318
-
1319
- ```sh
1320
- npm run build
1321
- npm start
1322
- ```
1323
-
1324
- `npm start` prints `Agent Lookout is running at http://127.0.0.1:4777`. Open that address. To check it from a terminal:
1325
-
1326
- ```sh
1327
- curl -s http://127.0.0.1:4777/api/health
1328
- ```
1329
-
1330
- It prints `{"ok":true,"version":"0.2.1"}`, or a later version number. If you run `npm start` before `npm run build`, it stops and tells you to build first. After you pull new code, run `npm run build` again.
1313
+ In a clone, `npm run build` and then `npm start` run Agent Lookout from built files, at `http://127.0.0.1:4777`. [From the repository](INSTALL.md#from-the-repository) has the steps.
1331
1314
 
1332
1315
  ## Start it with one command
1333
1316
 
1334
- `agent-lookout` on its own starts Agent Lookout from the built files, as `npm start` does: the dashboard and its API at `http://127.0.0.1:4777`, on this computer only. It prints `Agent Lookout is running at http://127.0.0.1:4777` and keeps running until you press Ctrl+C. `agent-lookout start` does the same.
1335
-
1336
- | Option | What it does |
1337
- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1338
- | `--port <number>` | Listens on this port in place of 4777, such as `--port 4778`. `AGENT_LOOKOUT_PORT` does the same, and `--port` wins when both are given |
1339
- | `--open` | Opens the address in your default browser once it is listening, with `open` on macOS, `xdg-open` on Linux or `rundll32.exe` on Windows. If that cannot be done, it prints one line with the address and keeps running |
1340
- | `--help` | Prints the options of `agent-lookout`, `agent-lookout status` and `agent-lookout mcp` |
1341
- | `--version` | Prints the version of Agent Lookout, such as `0.2.1`, and nothing else |
1342
-
1343
- Every other setting under [Settings you can change](#settings-you-can-change) works with it as with `npm start`. It ends with 0 when you press Ctrl+C, and with 1 when it cannot start, such as when the port is in use. Under `npx agent-lookout`, the shell reports the interrupt instead, as exit code 130.
1344
-
1345
- Agent Lookout is on npm, so `npx agent-lookout` starts it with nothing to clone or install first, and `npx agent-lookout --open` opens it in the browser as well. npm downloads the package the first time, under 2 MB with what it needs and about 4 MB once installed, and keeps it in its cache. The package holds the built dashboard and the collector as plain JavaScript, so it needs Node.js and nothing else. Each time, npx asks npm's registry whether a newer version is out, as it does for any package it runs. To start it without that, install it once with `npm install -g agent-lookout` and run `agent-lookout`.
1346
-
1347
- In a clone, run `npm run build` and `npm link` once in the `agent-lookout` folder, then `agent-lookout` from any folder.
1317
+ `npx agent-lookout`, or `agent-lookout` once it is installed, starts Agent Lookout at `http://127.0.0.1:4777`, on this computer only. [With npx](INSTALL.md#with-npx) has its options, and how to run it in the background, stop it and update it.
1348
1318
 
1349
1319
  ## Desktop app
1350
1320
 
1351
1321
  Agent Lookout can also run as a Mac app: the same dashboard in a window of its own, with the collector running inside the app. The app opens no port, so nothing else on this computer can reach it. It keeps its rules in the same [settings file](#time-rules) as `npx agent-lookout`, though, and takes up a change to that file at its next poll, so a program running as you can change them by writing the file. It needs no Node.js.
1352
1322
 
1353
- To install it, download the disk image for your Mac from the [latest release](https://github.com/Olanetsoft/agent-lookout/releases/latest) on GitHub: `Agent-Lookout-<version>-mac-arm64.dmg` for Apple silicon, or `Agent-Lookout-<version>-mac-x64.dmg` for Intel. About This Mac, in the Apple menu, shows which you have: a chip such as Apple M1 is Apple silicon, and a processor named Intel is Intel. Open the disk image and drag Agent Lookout to Applications. Keep it there: the app [updates itself](#updates) only from a folder it can change.
1354
-
1355
- The app is not yet signed with an Apple Developer ID, so the first time a downloaded copy is opened, macOS does not open it. Its message is titled “Agent Lookout” Not Opened and says that Apple could not verify “Agent Lookout” is free of malware. To open it anyway:
1356
-
1357
- 1. Press Done. Do not press Move to Bin, or Move to Trash, which deletes the app.
1358
- 2. Open System Settings › Privacy & Security.
1359
- 3. Under Security, next to the line that says Agent Lookout was blocked, press Open Anyway, and confirm with your password or Touch ID. The button is there for about an hour after macOS stopped the app. If it has gone, open the app again, press Done, and go back to Privacy & Security.
1360
-
1361
- macOS remembers the choice.
1362
-
1363
- To build the app yourself instead, from a clone, on a Mac:
1364
-
1365
- ```sh
1366
- npm install
1367
- npm run dist:mac
1368
- ```
1369
-
1370
- It takes a minute or two, and the first time it downloads Electron for each kind of Mac. It puts a disk image and a zip for each in `release/`, with the same names as on the release. Open the disk image for your Mac and drag Agent Lookout to Applications. A copy you built yourself on this Mac opens without the steps above.
1323
+ To install it, download it from the [latest release](https://github.com/Olanetsoft/agent-lookout/releases/latest) and drag it to Applications, or build it yourself: [Mac app](INSTALL.md#mac-app) has the steps, and what to do the first time macOS will not open it. Keep it in Applications: the app [updates itself](#updates) only from a folder it can change.
1371
1324
 
1372
1325
  It behaves as a Mac app does:
1373
1326
 
@@ -1526,7 +1479,7 @@ The program serving the page has stopped. The page keeps the last thing it saw,
1526
1479
 
1527
1480
  ## How it finds sessions
1528
1481
 
1529
- For Claude Code, Agent Lookout reads the small file Claude Code keeps for each running session in `~/.claude/sessions/`, every 2 seconds. That starts no program and uses no network. When it starts, and every 30 seconds after that, it also runs `claude agents --json --all`, the command Claude Code [documents](https://code.claude.com/docs/en/agent-view) for listing its sessions. That answer decides which sessions exist, and it adds background jobs that have finished or failed. If the command cannot be found or fails, Agent Lookout uses the files alone. While a session is waiting for you, Agent Lookout also reads the end of its transcript, in `~/.claude/projects/`, to say what it is asking, and forgets it when the wait ends. Claude Code does not document its transcripts, so a new version can change them, and then the reason shows alone.
1482
+ For Claude Code, Agent Lookout reads the small file Claude Code keeps for each running session in `~/.claude/sessions/`, every 2 seconds, whether the session runs in a terminal, in VS Code or in the desktop app. That starts no program and uses no network. When it starts, and every 30 seconds after that, it also runs `claude agents --json --all`, the command Claude Code [documents](https://code.claude.com/docs/en/agent-view) for listing its sessions. That answer decides which sessions exist, and it adds background jobs that have finished or failed. If the command cannot be found or fails, Agent Lookout uses the files alone. While a session is waiting for you, Agent Lookout also reads the end of its transcript, in `~/.claude/projects/`, to say what it is asking, and forgets it when the wait ends. Claude Code does not document its transcripts, so a new version can change them, and then the reason shows alone.
1530
1483
 
1531
1484
  For Codex it runs nothing. Every 2 seconds it reads what Codex has added to the session files under `~/.codex/sessions/` and finds the last line that says a turn started or ended: a session with a turn under way is working, and one whose last turn ended is idle. A session that no Codex program has open is finished. Those files hold your conversations with Codex. Agent Lookout keeps only when each turn started and ended and a few details, such as the session's folder, and when each file was last changed, which says how long a working session has been [quiet](#quiet-for). Codex documents none of these files, so a new Codex version can change them.
1532
1485
 
@@ -0,0 +1,175 @@
1
+ # Install
2
+
3
+ Agent Lookout runs with npx, as a Mac app, or from a clone of the repository. The Claude Code plugin adds Allow and Deny to any of them. [The README](../README.md) says what it does.
4
+
5
+ ## Requirements
6
+
7
+ - macOS, Linux or Windows. Agent Lookout is developed and used on macOS. On Linux and Windows, CI runs the tests and starts it, and no one has used it on those desktops yet. [On Linux](GUIDE.md#on-linux) and [On Windows](GUIDE.md#on-windows) say what differs there.
8
+ - Node.js 22.12 or newer. The [Mac app](#mac-app) needs no Node.js.
9
+ - Claude Code, Codex or both. Neither needs any setup. Any other agent can appear too, by writing a [status file](GUIDE.md#your-own-agents).
10
+ - For Claude Code, a version that has the `claude agents` command. Without it, Agent Lookout still reads the session files, but cannot list background jobs that have finished or failed.
11
+ - For Codex, version 0.155 or later, so that Agent Lookout can tell a session that has ended from one that is idle.
12
+
13
+ Check what you have:
14
+
15
+ ```sh
16
+ node --version
17
+ claude --version
18
+ claude agents --help
19
+ codex --version
20
+ ```
21
+
22
+ `node --version` prints `v22.12.0` or later. `claude --version` prints a version number followed by `(Claude Code)`, and `claude agents --help` prints a line that starts with `Usage: claude agents`. `codex --version` prints `codex-cli` followed by a version number. A command for a program you do not use prints `command not found`: carry on. Agent Lookout never runs `codex`, so the Codex desktop app needs no CLI.
23
+
24
+ ## With npx
25
+
26
+ ### Start it
27
+
28
+ ```sh
29
+ npx agent-lookout
30
+ ```
31
+
32
+ The first time, npm names the package and asks `Ok to proceed? (y)`. Press Enter. npm downloads under 2 MB, about 4 MB once installed, and keeps it in its cache. The package holds the built dashboard and the collector as plain JavaScript, so it needs Node.js and nothing else. Then Agent Lookout prints:
33
+
34
+ ```text
35
+ Agent Lookout is running at http://127.0.0.1:4777
36
+ It listens on this machine only. Press Ctrl+C to stop.
37
+ ```
38
+
39
+ Open <http://127.0.0.1:4777>. Your sessions appear within a few seconds. If none do, see [No sessions appear](GUIDE.md#no-sessions-appear). It keeps running until you press Ctrl+C, so leave that terminal open. If you used `npm run dev` before, turn notifications on again here: the browser keeps them for each address.
40
+
41
+ | Option | What it does |
42
+ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
+ | `--port <number>` | Listens on this port in place of 4777, such as `--port 4778`. `AGENT_LOOKOUT_PORT` does the same, and `--port` wins when both are given |
44
+ | `--open` | Opens the address in your default browser once it is listening, with `open` on macOS, `xdg-open` on Linux or `rundll32.exe` on Windows. If that cannot be done, it prints one line with the address and keeps running |
45
+ | `--help` | Prints the options of `agent-lookout`, `agent-lookout status` and `agent-lookout mcp` |
46
+ | `--version` | Prints the version of Agent Lookout and nothing else |
47
+
48
+ `agent-lookout start` does the same as `agent-lookout`. Every setting under [Settings you can change](GUIDE.md#settings-you-can-change) works with it. It ends with 0 when you press Ctrl+C, and with 1 when it cannot start, such as when the port is in use. Under `npx agent-lookout`, the shell reports the interrupt instead, as exit code 130.
49
+
50
+ ### Run it in the background
51
+
52
+ ```sh
53
+ nohup npx --yes agent-lookout > agent-lookout.log 2>&1 &
54
+ ```
55
+
56
+ `--yes` stops npm asking `Ok to proceed?`, since nothing would answer. What Agent Lookout prints goes to `agent-lookout.log`.
57
+
58
+ ### Another port
59
+
60
+ If it prints `Port 4777 is already in use`, another program has that port. Choose another:
61
+
62
+ ```sh
63
+ npx agent-lookout --port 4778
64
+ ```
65
+
66
+ Use the new port in the address you open and in the check below. `agent-lookout status` and `agent-lookout mcp` then need `--url http://127.0.0.1:4778`, or `AGENT_LOOKOUT_URL` set to that address, as [In the terminal](GUIDE.md#in-the-terminal) says. [The port is in use](GUIDE.md#the-port-is-in-use) shows how to find which program holds a port.
67
+
68
+ ### Check that it answers
69
+
70
+ If `npx agent-lookout` holds your terminal, use a second one:
71
+
72
+ ```sh
73
+ curl -s http://127.0.0.1:4777/api/health
74
+ ```
75
+
76
+ It prints `{"ok":true,"version":"…"}`, with the version you are running. If a step prints something else, [When something goes wrong](GUIDE.md#when-something-goes-wrong) says what to do for the common problems.
77
+
78
+ ### Stop it
79
+
80
+ Press Ctrl+C in its terminal. If it runs in the background, find its process ID and end it. On macOS, or on Linux with `lsof` installed, `lsof -nP -iTCP:4777 -sTCP:LISTEN` shows the process ID under `PID`, and `kill` followed by that number stops it. On Windows, `netstat -ano | findstr :4777` shows the process ID in its last column, and `taskkill /F /PID` followed by that number stops it. Put your port in place of 4777 if you chose another.
81
+
82
+ ### Update it
83
+
84
+ ```sh
85
+ npx agent-lookout@latest
86
+ ```
87
+
88
+ This also makes sure npx does not reuse an older copy it keeps. Each time npx starts it, npx asks npm's registry whether a newer version is out, as it does for any package it runs.
89
+
90
+ ### Install it once
91
+
92
+ To start it without npx asking the registry each time, install it once and run it by name:
93
+
94
+ ```sh
95
+ npm install -g agent-lookout
96
+ agent-lookout
97
+ ```
98
+
99
+ `agent-lookout status` and `agent-lookout mcp` then start without npm asking its registry each time, which suits a tmux status line or a shell prompt that runs them every few seconds. Run `npm install -g agent-lookout` again to update it, and `npm uninstall -g agent-lookout` to take it away.
100
+
101
+ ## Mac app
102
+
103
+ Download the disk image for your Mac from the [latest release](https://github.com/Olanetsoft/agent-lookout/releases/latest): `Agent-Lookout-<version>-mac-arm64.dmg` for Apple silicon, or `Agent-Lookout-<version>-mac-x64.dmg` for Intel. About This Mac, in the Apple menu, shows which you have: a chip such as Apple M1 is Apple silicon, and a processor named Intel is Intel. Open the disk image and drag Agent Lookout to Applications. Keep it there: the app updates itself only from a folder it can change. It needs no Node.js.
104
+
105
+ The app is not yet signed with an Apple Developer ID, so the first time a downloaded copy is opened, macOS does not open it. Its message is titled “Agent Lookout” Not Opened and says that Apple could not verify “Agent Lookout” is free of malware. To open it anyway:
106
+
107
+ 1. Press Done. Do not press Move to Bin, or Move to Trash, which deletes the app.
108
+ 2. Open System Settings › Privacy & Security.
109
+ 3. Under Security, next to the line that says Agent Lookout was blocked, press Open Anyway, and confirm with your password or Touch ID. The button is there for about an hour after macOS stopped the app. If it has gone, open the app again, press Done, and go back to Privacy & Security.
110
+
111
+ macOS remembers the choice. [Desktop app](GUIDE.md#desktop-app) says how the app behaves, [Menu bar](GUIDE.md#menu-bar) what its icon in the menu bar lists, and [Updates](GUIDE.md#updates) how it checks for a newer version and installs it.
112
+
113
+ ### Build it yourself
114
+
115
+ From a clone, on a Mac:
116
+
117
+ ```sh
118
+ npm install
119
+ npm run dist:mac
120
+ ```
121
+
122
+ It takes a minute or two, and the first time it downloads Electron for each kind of Mac. It puts a disk image and a zip for each in `release/`, with the same names as on the release. Open the disk image for your Mac and drag Agent Lookout to Applications. A copy you built yourself on this Mac opens without the steps above.
123
+
124
+ ### Mac app and npx together
125
+
126
+ The app opens no port, so `agent-lookout status`, `agent-lookout mcp` and a [tmux status line](GUIDE.md#in-a-tmux-status-line) cannot reach it. For those, run `npx agent-lookout` as well. The two keep their history in the same folder, so the Events log and the charts carry over, and their time rules and permission rules in the same settings file. Turn notifications on in only one of the two, or you get each one twice.
127
+
128
+ ## Claude Code plugin
129
+
130
+ With Agent Lookout running, the plugin lets you answer a Claude Code permission prompt from the page, with Allow or Deny. In Claude Code, run:
131
+
132
+ ```text
133
+ /plugin marketplace add Olanetsoft/agent-lookout
134
+ /plugin install agent-lookout@agent-lookout
135
+ ```
136
+
137
+ From a shell, `claude plugin marketplace add Olanetsoft/agent-lookout` and `claude plugin install agent-lookout@agent-lookout` do the same. To remove it, run `/plugin uninstall agent-lookout@agent-lookout`. The hook needs `curl`, which macOS has, and most Linux systems do. On Windows the plugin does nothing, and Claude Code asks as usual. [plugins/agent-lookout/README.md](../plugins/agent-lookout/README.md) says what the hook does, and [Answer a permission prompt](GUIDE.md#answer-a-permission-prompt) what the page shows.
138
+
139
+ ## From the repository
140
+
141
+ To run it from a clone, as you would to work on its code:
142
+
143
+ ```sh
144
+ git clone https://github.com/Olanetsoft/agent-lookout.git
145
+ cd agent-lookout
146
+ npm install
147
+ npm run build
148
+ npm start
149
+ ```
150
+
151
+ `npm start` prints the same two lines as `npx agent-lookout`, at the same address, and takes another port as `AGENT_LOOKOUT_PORT=4778 npm start`. Run before `npm run build`, it stops and tells you to build first. After a `git pull`, run `npm install` and `npm run build` again.
152
+
153
+ `npm link`, run once in the folder after `npm run build`, puts `agent-lookout` on your `PATH`, pointing at the clone, so it starts from any folder. It serves the same built page, so a pull needs the same `npm install` and `npm run build`.
154
+
155
+ `npm run dev` starts it at <http://localhost:5173> with the development tools used to work on its code, as [CONTRIBUTING.md](../CONTRIBUTING.md) describes.
156
+
157
+ ## With your coding agent
158
+
159
+ A coding agent such as Claude Code or Codex can start it for you. Give it this prompt:
160
+
161
+ ```text
162
+ Start Agent Lookout on this computer. First check that node --version
163
+ prints v22.12.0 or later. Then run npx --yes agent-lookout in the
164
+ background and leave it running.
165
+ It should print "Agent Lookout is running at http://127.0.0.1:4777".
166
+ If it prints anything else, or says the port is in use, stop and tell
167
+ me what it printed.
168
+
169
+ Do not use sudo, and do not set up email, a webhook, pull requests
170
+ or other machines. You are done when
171
+ curl -s http://127.0.0.1:4777/api/health prints {"ok":true and a
172
+ version number. Tell me what it printed.
173
+ ```
174
+
175
+ An agent's background command can end when its session does. If the page then says Agent Lookout has stopped updating, run `npx agent-lookout` yourself.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-lookout",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Shows the AI agent sessions on your computer in one browser page, and which need you.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -32,6 +32,7 @@
32
32
  "dist/",
33
33
  "docs/API.md",
34
34
  "docs/GUIDE.md",
35
+ "docs/INSTALL.md",
35
36
  "CHANGELOG.md",
36
37
  "DISCLAIMER.md",
37
38
  "LICENSE",