agent-lookout 0.2.1 → 0.2.4

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/PRIVACY.md +113 -25
  3. package/README.md +21 -15
  4. package/SECURITY.md +20 -8
  5. package/bin/agent-lookout.mjs +49 -24
  6. package/dist/THIRD-PARTY-LICENSES.md +195 -0
  7. package/dist/assets/HistoryPanel-CIjshvhu.js +1 -0
  8. package/dist/assets/SessionPanel-eBOdTLD9.js +1 -0
  9. package/dist/assets/index-B-myfVQS.js +2 -0
  10. package/dist/assets/index-Cceyz9yZ.css +1 -0
  11. package/dist/assets/radix-BH0JH8UU.js +41 -0
  12. package/dist/cli/agent-lookout.js +4 -4
  13. package/dist/cli/{chunk-PQ3B5TN6.js → chunk-63XPRBHW.js} +88 -2
  14. package/dist/cli/{chunk-XJM5RZNV.js → chunk-KNOLJ3IC.js} +1 -1
  15. package/dist/cli/{chunk-UYGESSPG.js → chunk-XVXSGVEW.js} +6 -2
  16. package/dist/cli/mcpServer-HGH6PY4W.js +23664 -0
  17. package/dist/cli/{startCommand-ZV2NQ6YG.js → startCommand-KNXMVM2U.js} +3417 -676
  18. package/dist/index.html +3 -3
  19. package/docs/API.md +301 -79
  20. package/docs/GUIDE.md +215 -67
  21. package/package.json +6 -6
  22. package/dist/assets/HistoryPanel-Llcz5XPi.js +0 -1
  23. package/dist/assets/SessionPanel-B4g_6Unu.js +0 -1
  24. package/dist/assets/atkinson-hyperlegible-mono-latin-400-normal-Cmjm2dBj.woff +0 -0
  25. package/dist/assets/atkinson-hyperlegible-mono-latin-500-normal-BV5ipwqM.woff +0 -0
  26. package/dist/assets/atkinson-hyperlegible-mono-latin-600-normal-CuPF9o0F.woff +0 -0
  27. package/dist/assets/atkinson-hyperlegible-mono-latin-ext-400-normal-ITMuFrSK.woff +0 -0
  28. package/dist/assets/atkinson-hyperlegible-mono-latin-ext-500-normal-BiHEOgiK.woff +0 -0
  29. package/dist/assets/atkinson-hyperlegible-mono-latin-ext-600-normal-ZiH-ObRk.woff +0 -0
  30. package/dist/assets/atkinson-hyperlegible-next-latin-400-normal-DPp7N0cD.woff +0 -0
  31. package/dist/assets/atkinson-hyperlegible-next-latin-500-normal-B0CYqHl3.woff +0 -0
  32. package/dist/assets/atkinson-hyperlegible-next-latin-600-normal-BhgCag5w.woff +0 -0
  33. package/dist/assets/atkinson-hyperlegible-next-latin-ext-400-normal-qxMiU51n.woff +0 -0
  34. package/dist/assets/atkinson-hyperlegible-next-latin-ext-500-normal-oZS3KxrF.woff +0 -0
  35. package/dist/assets/atkinson-hyperlegible-next-latin-ext-600-normal-BBAQIogt.woff +0 -0
  36. package/dist/assets/index-DRcy1hT5.js +0 -2
  37. package/dist/assets/index-Dz4q_OYZ.css +0 -1
  38. package/dist/assets/radix-27o0jeIu.js +0 -41
  39. package/dist/cli/mcpServer-CGLIGL4X.js +0 -246
package/SECURITY.md CHANGED
@@ -12,15 +12,15 @@ The project has one maintainer, so replies are best effort. Fixes land on `main`
12
12
 
13
13
  ## What the app handles
14
14
 
15
- Agent Lookout reads metadata about the Claude Code and Codex sessions on your machine: names, working directories, statuses and, for Claude Code, process IDs. For each session's folder it reads which git branch is checked out, and which repository the folder belongs to, from the repository's `.git` entry, `HEAD` and a worktree's `commondir` alone, and runs no git command. On a Mac it also lists every process's ID, parent, terminal and program name with `ps` when a new Claude Code session appears, to find the Terminal or iTerm2 tab it runs in, and keeps only the app and the session's terminal. It serves that to the dashboard over HTTP on a loopback address. Of Claude Code's transcripts it reads only the end of a waiting session's own, opened without following a link, and keeps one line of it, what the session is asking, until the wait ends. That line goes to the dashboard and to notifications on this machine, never to an email, a webhook post or `agent-lookout mcp`. It does open Codex's session files, which hold whole conversations, and keeps only when turns started and ended and a few fields. It writes to no agent's files. It also reads the folder of status files, `~/.agent-lookout/sessions` or the one `AGENT_LOOKOUT_STATUS_DIR` names, where any program can write a small JSON file to show one of its sessions, and it writes nothing there. [PRIVACY.md](PRIVACY.md) lists everything it reads, runs and keeps.
15
+ Agent Lookout reads metadata about the Claude Code and Codex sessions on your machine: names, working directories, statuses and, for Claude Code, process IDs. For each session's folder it reads which git branch is checked out, and which repository the folder belongs to, from the repository's `.git` entry, `HEAD` and a worktree's `commondir` alone, and runs no git command. On a Mac it also lists every process's ID, parent, terminal and program name with `ps` when a new Claude Code session appears, to find the Terminal or iTerm2 tab it runs in, and keeps only the app and the session's terminal. It serves that to the dashboard over HTTP on a loopback address. Of Claude Code's transcripts it reads only the end of a waiting session's own, opened without following a link, and keeps one line of it, what the session is asking, until the wait ends. That line goes to the dashboard and to notifications on this machine, never to an email, a webhook post or `agent-lookout mcp`. It does open Codex's session files, which hold whole conversations, and keeps only when turns started and ended and a few fields. It writes to no agent's files. It ends a Claude Code session's process only when the person presses Stop and confirms, by the route described below, and never on its own. It also reads the folder of status files, `~/.agent-lookout/sessions` or the one `AGENT_LOOKOUT_STATUS_DIR` names, where any program can write a small JSON file to show one of its sessions, and it writes nothing there. It keeps the Events log and the counts behind its charts in its own folder, `~/.agent-lookout/history`, which it makes with mode 700, in files of mode 600 that it neither reads nor writes through a link. They hold session IDs and names, statuses and times, and never what a waiting session is asking. `AGENT_LOOKOUT_HISTORY=off` keeps them in memory only. [PRIVACY.md](PRIVACY.md) lists everything it reads, runs and keeps.
16
16
 
17
17
  With notifications turned on in Settings, the dashboard page hands a session's name and a fixed text, the reason it waits or Finished, Failed or Ended, to the browser's Notifications API, which passes them to the operating system. When no dashboard page is open, the local server shows the notification itself on macOS: it runs `/usr/bin/osascript` with a fixed script, and passes the name and the text to that script as arguments. Each dashboard page tells the server whether its notifications are on, in a header on its requests, and the server shows its own only while the last page to say so said they are on, or `AGENT_LOOKOUT_NOTIFICATIONS=on` was set and no page has said anything. Nothing is sent over the network for any of this. [PRIVACY.md](PRIVACY.md#notifications) has the details.
18
18
 
19
- Email notifications are off unless they are set up in the environment Agent Lookout starts with. Then, for a session that has waited the delay set, and, if `AGENT_LOOKOUT_EMAIL_EVENTS` asks for them, for one that finishes, fails or ends, the collector sends one short plain-text email, through the mail server named in `AGENT_LOOKOUT_SMTP_URL`, to the one address in `AGENT_LOOKOUT_EMAIL_TO`. It holds the session's name, what happened, how long it waited or when it ended, the name of its project folder, its app and its agent. The password for the mail server is in `AGENT_LOOKOUT_SMTP_URL`, so it lives in the environment of the Agent Lookout process and nowhere else: it is never written to a file, sent to the dashboard or printed. The connection to the mail server is encrypted with TLS, with its certificate checked, except to a mail server on this machine. `GET /api/email` tells the dashboard only whether email is on, the address with most of it hidden, the events, the delay and how the last email went. [PRIVACY.md](PRIVACY.md#email) has the details.
19
+ Email notifications are off unless they are set up in the environment Agent Lookout starts with. Then, for a session that has waited the delay set, and, if `AGENT_LOOKOUT_EMAIL_EVENTS` asks for them, for one that finishes, fails or ends, the collector sends one short plain-text email, through the mail server named in `AGENT_LOOKOUT_SMTP_URL`, to the one address in `AGENT_LOOKOUT_EMAIL_TO`. It holds the session's name, what happened, how long it waited or when it ended, the name of its project folder, its app and its agent, and, for a wait with `AGENT_LOOKOUT_EMAIL_ASKING=on`, what the session is asking, which can hold a command, a web address or a file's full path. The password for the mail server is in `AGENT_LOOKOUT_SMTP_URL`, so it lives in the environment of the Agent Lookout process and nowhere else: it is never written to a file, sent to the dashboard or printed. The connection to the mail server is encrypted with TLS, with its certificate checked, except to a mail server on this machine. `GET /api/email` tells the dashboard only whether email is on, the address with most of it hidden, the events, the delay, whether a wait's email says what the session is asking, and how the last email went. [PRIVACY.md](PRIVACY.md#email) has the details.
20
20
 
21
- Webhook posts are off unless `AGENT_LOOKOUT_WEBHOOK_URL` is set in the environment Agent Lookout starts with. Then, for each event chosen, the collector sends one short JSON post to that one address, over HTTPS with the certificate checked, or over plain HTTP only to `127.0.0.1` or `localhost`. It holds the session's name, what happened, when, the name of its project folder, its app and its agent. It follows no redirect and tries nothing twice. The address is itself a secret: whoever has a Slack incoming webhook's address can post to its channel. So it lives in the environment of the Agent Lookout process and nowhere else: it is never written to a file, sent to the dashboard or printed. `GET /api/webhook` tells the dashboard only whether the webhook is on, the host, the events, the delay and how the last post went. [PRIVACY.md](PRIVACY.md#webhook) has the details.
21
+ Webhook posts are off unless `AGENT_LOOKOUT_WEBHOOK_URL` is set in the environment Agent Lookout starts with. Then, for each event chosen, the collector sends one short JSON post to that one address, over HTTPS with the certificate checked, or over plain HTTP only to `127.0.0.1` or `localhost`. It holds the session's name, what happened, when, the name of its project folder, its app and its agent, and, for a wait with `AGENT_LOOKOUT_WEBHOOK_ASKING=on`, what the session is asking. It follows no redirect and tries nothing twice. The address is itself a secret: whoever has a Slack incoming webhook's address can post to its channel. So it lives in the environment of the Agent Lookout process and nowhere else: it is never written to a file, sent to the dashboard or printed. `GET /api/webhook` tells the dashboard only whether the webhook is on, the host, the events, the delay, whether a wait's post says what the session is asking, and how the last post went. [PRIVACY.md](PRIVACY.md#webhook) has the details.
22
22
 
23
- One route of the local API changes something: `POST /api/jump`, which the Jump button of a session in tmux, Terminal or iTerm2 sends. Every other route only reads. For a session in tmux it can change three things, all inside tmux: which pane is selected in its window, which window is selected in its tmux session, and which tmux session each attached terminal is showing. For a session in a tab of Terminal or iTerm2, on macOS, it can bring that tab forward: select it in its window, bring the window back from the Dock if it was minimised and in front of the app's others, and bring the app to the front. It sends no keys, runs nothing inside a pane or a tab, starts no tmux server and starts no app.
23
+ Four routes of the local API change something: `POST /api/jump`, which the Jump button of a session in tmux, Terminal or iTerm2 sends, `POST /api/history/clear`, which the Clear history button in Settings sends, and `POST /api/sessions/stop` and `POST /api/sessions/clean-up`, which Stop session and the ending of the sessions left running send. Every other route only reads. The two that stop sessions are described after the jump route, below. `POST /api/history/clear` deletes the history files in Agent Lookout's own folder and empties the Events log and the history in memory. Its body is exactly `{}`, nothing in it names a file, and it makes the same checks as the jump route, below, with `X-Agent-Lookout-Action: clear-history`. The rest of this part is about `POST /api/jump`. For a session in tmux it can change three things, all inside tmux: which pane is selected in its window, which window is selected in its tmux session, and which tmux session each attached terminal is showing. For a session in a tab of Terminal or iTerm2, on macOS, it can bring that tab forward: select it in its window, bring the window back from the Dock if it was minimised and in front of the app's others, and bring the app to the front. It sends no keys, runs nothing inside a pane or a tab, starts no tmux server and starts no app.
24
24
 
25
25
  A request to it names a session by its ID and nothing else. The server looks that session up in its own list and takes the pane it found for that session's process when it last asked tmux. It checks that the pane's ID is a `%` followed by digits, and gives it to a fixed list of tmux commands: `select-window`, `select-pane`, `display-message -p`, `list-clients` and `switch-client`. Nothing from the request is passed to tmux, and tmux is started directly, never through a shell. So the route can never be made to run another program, another tmux command or a command with other arguments. The most a request can do is choose which of the panes already found for a listed session is selected.
26
26
 
@@ -36,11 +36,20 @@ On top of the checks every route makes, the route answers only a request that:
36
36
 
37
37
  It makes one jump a second, and one at a time. `AGENT_LOOKOUT_TMUX=off` stops Agent Lookout running tmux, and the route then finds no pane for any session. `AGENT_LOOKOUT_TERMINAL_JUMP=off` stops it looking for tabs, and the route then brings no tab forward. [PRIVACY.md](PRIVACY.md#tmux) lists each command, and [Terminal and iTerm2](PRIVACY.md#terminal-and-iterm2) the script.
38
38
 
39
+ `POST /api/sessions/stop` ends one Claude Code session's process, and `POST /api/sessions/clean-up` ends up to 20 sessions left running, idle for a day or more. Each passes the checks above, with `X-Agent-Lookout-Action: stop` or `clean-up`, and a body of 1,024 bytes or less that is exactly `{"sessionId": "..."}`, or of 8 KiB or less that is exactly `{"sessions": [{"sessionId": "...", "statusSince": ...}]}`. A request names sessions, and for a clean-up the moment each one's idle began, and nothing else. The server looks each session up in its own list and acts on what the Claude Code adapter found for it: its process ID, the start time its registry file recorded and, for a background job, the job's ID, none of which is sent to the page or read from a request. Before it acts, it makes every check again at that moment, with nothing remembered:
40
+
41
+ - It reads the session's registry file again, an ordinary file it opens without following a link. The file must still name that session and that process, be of kind `interactive` or `bg`, the kind it had, and record the same start time. A file that names no kind, or another, is refused.
42
+ - It runs `ps` for that process again, and acts only when `ps` gives the same start time the file recorded, a second apart at most, since `ps` gives it in whole seconds. A start time `ps` does not give, or that cannot be compared, is refused as not confirmed. So a pid that has been handed to another program is not signalled unless that program started within a second of the session, and the few milliseconds before the signal, below, are a known gap.
43
+ - It refuses process 1, its own process, the process it was started from and every process above that, which it finds by following its own process up through its parents in `ps -A -o pid=,ppid=` at that moment, a process of another user's, and a session in Claude Code's desktop app.
44
+ - For a clean-up, the file, and the server's own latest list of sessions, which can come from `claude agents`, must also still say the session is idle, since the moment the page sent, and that moment must be 24 hours or more ago. A session that has done anything since is left running.
45
+
46
+ Then it sends SIGTERM to that one process, never SIGKILL, and waits up to 10 seconds for it to end, or, for a background job, runs the `claude` binary it already runs for `claude agents`, directly, never through a shell, with stdin closed and a 10 second timeout, as `claude stop` and the job's ID, which must be letters, digits, dashes and underscores, starting with a letter or a digit. One stop or clean-up is made at a time, a second apart. The time between the `ps` check and the signal is a few milliseconds, and for a pid to be given to another program in that time, the process would have to end and the system would have to give out every other pid first. macOS has no way to close that gap entirely. `AGENT_LOOKOUT_STOP=off` removes both routes. [Stopping a session](PRIVACY.md#stopping-a-session) has the rest.
47
+
39
48
  The Mac app answers four more routes, under `/api/app/` on its own address, which no port reaches and which `npm start` and the npm package do not have: `GET /api/app/update`, and three POSTs that check GitHub for a newer version, install the one that is ready, and turn the daily check on or off. Each POST passes the checks above, with an `X-Agent-Lookout-Action` of its own, and a body of 256 bytes or less that is exactly what it needs. The app's only requests off the machine are the update check's and its download's, over HTTPS, with the certificate checked, to GitHub and the two hosts GitHub redirects a release's downloads to, and to no other address, a redirect's included. A download must have the size and SHA-512 the release's `latest-mac.yml` gives, and the bundle in it must be this app, by its bundle identifier, at the version offered, with no link where the bundle, its `Contents` or its `Info.plist` should be. Only Install and Restart starts the helper that replaces the app: `/bin/sh` with a fixed script, and the app's process ID and three paths as its arguments, two of them in the app's own temporary folder. Nothing from the network or from a request reaches its arguments or any command. The app is not signed with a Developer ID yet, so a download is vouched for by the release it comes from and the hash in that release, and is as safe as the project's GitHub releases are.
40
49
 
41
- `agent-lookout mcp` is a Model Context Protocol server for an agent that keeps track of your other agents. The agent's app starts it and speaks to it over stdin and stdout, so it opens no port. For each tool call it reads `GET /api/sessions` from the local server, at a loopback address only, with no `Origin` and no notifications header, exactly as `agent-lookout status` does, and answers the app with what it read. Its three tools only read: none of them reaches `POST /api/jump`, changes a notification or changes anything else. A session's name, agent, folder and branch are text that other programs wrote, and the client is a model that takes instructions from text. So each tool's description and each answer say that this text is data, never instructions, the one-sentence summary puts each name in quotation marks, and escape sequences and control characters are taken out of every text, as `agent-lookout status` does. So are characters that show nothing, such as zero-width spaces and Unicode's tag characters, in which words can be hidden from a person but not from a model.
50
+ `agent-lookout mcp` is a Model Context Protocol server for an agent that keeps track of your other agents. The agent's app starts it and speaks to it over stdin and stdout, so it opens no port. For each tool call it reads `GET /api/sessions` from the local server, at a loopback address only, with no `Origin` and no notifications header, exactly as `agent-lookout status` does, and answers the app with what it read. Its three tools only read: none of them reaches `POST /api/jump`, `POST /api/history/clear`, `POST /api/sessions/stop` or `POST /api/sessions/clean-up`, changes a notification or changes anything else. A session's name, agent, folder and branch are text that other programs wrote, and the client is a model that takes instructions from text. So each tool's description and each answer say that this text is data, never instructions, the one-sentence summary puts each name in quotation marks, and escape sequences and control characters are taken out of every text, as `agent-lookout status` does. So are characters that show nothing, such as zero-width spaces and Unicode's tag characters, in which words can be hidden from a person but not from a model.
42
51
 
43
- The local API has no token, and the MCP server adds none, though one was suggested for it. The server opens no port of its own, and the API it reads is the same loopback API that the dashboard and `agent-lookout status` read without one. Websites and other computers are already kept away from that API by the `Host` and `Origin` checks and by its listening on loopback alone. Against websites and other computers a token would add nothing to those checks, and programs running as you could read a token as easily as the API. It could keep out other accounts on a shared computer, the known limit listed below, and is left for later. An API that can be reached some other way, such as from another computer, would need one too, and Agent Lookout has none. [docs/API.md](docs/API.md) describes every route and its checks.
52
+ The local API has no token, and the MCP server adds none, though one was suggested for it. The server opens no port of its own, and the API it reads is the same loopback API that the dashboard and `agent-lookout status` read without one. Websites and other computers are already kept away from that API by the `Host` and `Origin` checks and by its listening on loopback alone. Against websites and other computers a token would add nothing to those checks, and programs running as you could read a token as easily as the API. It could keep out other accounts on a shared computer, the known limit listed below, which includes another account asking the server to stop your Claude Code sessions, and is left for later. An API that can be reached some other way, such as from another computer, would need one too, and Agent Lookout has none. [docs/API.md](docs/API.md) describes every route and its checks.
44
53
 
45
54
  Session names and folder paths can be sensitive. The main risk is that something other than your own browser reads them.
46
55
 
@@ -59,6 +68,8 @@ Session names and folder paths can be sensitive. The main risk is that something
59
68
  - A post that holds more than PRIVACY.md lists, or that goes to any address but the one in `AGENT_LOOKOUT_WEBHOOK_URL`, including by following a redirect.
60
69
  - A session's name, folder or anything else from a session that changes the structure of a post, adds a field or a header to it, or, in Slack or a service that reads Slack's format, mentions someone, pings a channel or makes a link that shows other words than its address.
61
70
  - A post sent over plain HTTP to an address that is not on this machine, or to a server whose certificate was not checked.
71
+ - What a waiting session is asking reaching an email while `AGENT_LOOKOUT_EMAIL_ASKING` is not `on`, a post while `AGENT_LOOKOUT_WEBHOOK_ASKING` is not `on`, an email's headers, or the email or post for anything but a wait.
72
+ - What a waiting session is asking being written to a file, an event or the history, or reaching `GET /api/email` or `GET /api/webhook`.
62
73
  - Any write to `~/.claude` or to another tool's files, and any read of the `.key` files in `~/.claude/sessions/`.
63
74
  - Any write under the Codex folder (`~/.codex`, or the folder `CODEX_HOME` or `AGENT_LOOKOUT_CODEX_HOME` names), any open of a file in its `thread-writer-locks/`, and any read of a Codex file that PRIVACY.md does not list.
64
75
  - Text from a Codex session file, such as a prompt, a reply or a command's output, reaching the API or the dashboard.
@@ -73,6 +84,7 @@ Session names and folder paths can be sensitive. The main risk is that something
73
84
  - In the Mac app, a request, a release or anything from the network that makes it run anything but `/usr/bin/ditto`, `/usr/bin/plutil` and its fixed update helper, passes them arguments it did not make itself, installs a download whose size or SHA-512 is not the one the release gives or whose bundle is not Agent Lookout at the version offered, installs anything before Install and Restart is pressed, or makes a request to any host but GitHub's three for a release.
74
85
  - A website, or a page served from anywhere but this machine, making the app select a tmux pane or bring a tab of Terminal or iTerm2 forward.
75
86
  - A request to `POST /api/jump` that makes tmux do anything but select a pane found for a listed session, its window and its tmux session: running a command, sending keys, or reaching any other pane.
87
+ - A request to `POST /api/sessions/stop` or `POST /api/sessions/clean-up` that signals any process but a listed Claude Code session's own, one whose registry file and start time were not confirmed at that moment, or one in the desktop app, that sends any signal but SIGTERM, that runs `claude` with any arguments but `stop` and a job's ID found for a listed session, or that ends a session that was not idle since the moment the page sent. Also a website, or a page served from anywhere but this machine, making the app stop a session.
76
88
  - A session's name, a tmux session's name, or anything else from a session or from tmux, that `tmux` reads as a command, a target or an option.
77
89
  - A request to `POST /api/jump` that makes `osascript` run any script but the fixed one for Terminal or iTerm2, or with any argument but a terminal found for a listed session, or that makes Terminal or iTerm2 do anything but bring a tab forward: running a command, sending keys or text, or reading what a tab shows.
78
90
  - A session name, or anything else from a session, that `osascript` reads as AppleScript or as one of its options instead of showing it as text.
@@ -80,14 +92,14 @@ Session names and folder paths can be sensitive. The main risk is that something
80
92
  - A notification, or the browser's question about allowing them, appearing when you have not turned notifications on, in Settings or with `AGENT_LOOKOUT_NOTIFICATIONS=on`.
81
93
  - A notification that holds anything but the session's name and the reason, or Finished, Failed or Ended, or that makes the browser fetch anything.
82
94
  - A page at another address reading or steering the dashboard's notifications, or turning the server's own on or off.
83
- - A tool of `agent-lookout mcp` that acts: that reaches `POST /api/jump`, sends the notifications header, or changes anything in Agent Lookout, in another tool or on this computer.
95
+ - A tool of `agent-lookout mcp` that acts: that reaches `POST /api/jump` or a route that stops a session, sends the notifications header, or changes anything in Agent Lookout, in another tool or on this computer.
84
96
  - `agent-lookout mcp` reading anything but `GET /api/sessions` from Agent Lookout at a loopback address, asking an address that is not loopback, opening a port, or making any other connection.
85
97
  - Text from a session reaching the app that started `agent-lookout mcp` without being marked as data: a tool description or an answer that does not say it is untrusted text, a name outside its quotation marks in the summary, or a name, agent, folder, branch or source detail with an escape sequence, control character or invisible character such as a Unicode tag character left in it.
86
98
  - Anything `agent-lookout mcp` writes to stdout that is not the protocol's own message, or an answer that holds more than PRIVACY.md lists, such as a session's whole folder path.
87
99
 
88
100
  ## Out of scope
89
101
 
90
- - Other programs on the same machine reading the local API, sending it the header that turns the server's notifications on or off, or asking it to select a tmux pane or bring a tab forward. The API has no authentication, so any local process can request it, and on a shared computer that includes other user accounts. This is a known limit of this version.
102
+ - Other programs on the same machine reading the local API, sending it the header that turns the server's notifications on or off, asking it to select a tmux pane or bring a tab forward, or asking it to stop a Claude Code session, with every check it makes for the Stop button. The API has no authentication, so any local process can request it, and on a shared computer that includes other user accounts. This is a known limit of this version. A program running as you could end your processes itself, but on a shared computer another account can also ask this server to stop your sessions, which it could not do itself. On such a computer, use the Mac app, which opens no port, so only its own window can ask, or set `AGENT_LOOKOUT_STOP=off`, which takes stopping away.
91
103
  - A program on this machine that names itself after Terminal or iTerm2 in the list of processes, and so gives a session under it a Jump button. Pressing it asks the real app for a tab on that session's terminal, which it does not have.
92
104
  - A notification the server showed staying in Notification Centre after its session has moved on. The server cannot take one down, and PRIVACY.md says so.
93
105
  - The dev server started with Vite's `--host` flag, which makes it listen on the network. `npm start` refuses to listen on anything but loopback.
@@ -11,35 +11,60 @@
11
11
  // does may end with it. start ends with 1 when it cannot start, as its help
12
12
  // says. Whatever goes wrong here, such as tsx missing while `npm ci` runs, is
13
13
  // one line on stderr and exit code 2.
14
-
15
- import { existsSync } from "node:fs";
16
- import { fileURLToPath } from "node:url";
14
+ //
15
+ // It checks the version of Node.js first, before it loads anything, so an
16
+ // older Node says in one line what it needs rather than failing on something
17
+ // it lacks. npm only warns about `engines`, so this check is the one that
18
+ // stops it. So the file has no import at the top, because imports load before
19
+ // any line runs, and nothing an older Node cannot read, such as await outside
20
+ // a function or `?.`, because an older Node reads the whole file before it
21
+ // runs any of it. The rest is loaded with import() once the version is known
22
+ // to be enough.
17
23
 
18
24
  // A reader that went away, such as `| head` or a prompt, is no reason to change the answer.
19
25
  process.stdout.on("error", () => {});
20
26
  process.stderr.on("error", () => {});
21
27
 
22
- const source = new URL("../src/cli/agentLookout.ts", import.meta.url);
23
- const bundle = new URL("../dist/cli/agent-lookout.js", import.meta.url);
28
+ // The oldest version it runs on, as `engines` in package.json says.
29
+ const NEEDED = { major: 22, minor: 12 };
30
+
31
+ const have = String(process.versions.node);
32
+ const [major, minor] = have.split(".").map(Number);
33
+ if (major < NEEDED.major || (major === NEEDED.major && minor < NEEDED.minor)) {
34
+ process.stderr.write(
35
+ `Agent Lookout needs Node.js ${NEEDED.major}.${NEEDED.minor} or newer, and this is Node.js ${have}.\n`,
36
+ );
37
+ // For status, 1 would say a session needs you, so it ends with 2, as when it cannot run at all.
38
+ process.exitCode = process.argv[2] === "status" ? 2 : 1;
39
+ } else {
40
+ run();
41
+ }
42
+
43
+ async function run() {
44
+ try {
45
+ const { existsSync } = await import("node:fs");
46
+ const { fileURLToPath } = await import("node:url");
47
+ const source = new URL("../src/cli/agentLookout.ts", import.meta.url);
48
+ const bundle = new URL("../dist/cli/agent-lookout.js", import.meta.url);
24
49
 
25
- try {
26
- let command;
27
- if (existsSync(source)) {
28
- const { register } = await import("tsx/esm/api");
29
- register();
30
- command = await import(source.href);
31
- } else {
32
- command = await import(bundle.href);
50
+ let command;
51
+ if (existsSync(source)) {
52
+ const { register } = await import("tsx/esm/api");
53
+ register();
54
+ command = await import(source.href);
55
+ } else {
56
+ command = await import(bundle.href);
57
+ }
58
+ process.exitCode = await command.runCommand({
59
+ argv: process.argv.slice(2),
60
+ env: process.env,
61
+ stdout: process.stdout,
62
+ stderr: process.stderr,
63
+ distDir: fileURLToPath(new URL("../dist/", import.meta.url)),
64
+ });
65
+ } catch (error) {
66
+ const why = error instanceof Error ? error.message.split("\n")[0] : String(error);
67
+ process.stderr.write(`agent-lookout could not run: ${why}\n`);
68
+ process.exitCode = 2;
33
69
  }
34
- process.exitCode = await command.runCommand({
35
- argv: process.argv.slice(2),
36
- env: process.env,
37
- stdout: process.stdout,
38
- stderr: process.stderr,
39
- distDir: fileURLToPath(new URL("../dist/", import.meta.url)),
40
- });
41
- } catch (error) {
42
- const why = error instanceof Error ? error.message.split("\n")[0] : String(error);
43
- process.stderr.write(`agent-lookout could not run: ${why}\n`);
44
- process.exitCode = 2;
45
70
  }
@@ -2375,3 +2375,198 @@ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
2375
2375
  DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
2376
2376
  FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
2377
2377
  OTHER DEALINGS IN THE FONT SOFTWARE.
2378
+
2379
+ ## @modelcontextprotocol/sdk - 1.32.1 (MIT)
2380
+
2381
+ MIT License
2382
+
2383
+ Copyright (c) 2024 Anthropic, PBC
2384
+
2385
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2386
+ of this software and associated documentation files (the "Software"), to deal
2387
+ in the Software without restriction, including without limitation the rights
2388
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2389
+ copies of the Software, and to permit persons to whom the Software is
2390
+ furnished to do so, subject to the following conditions:
2391
+
2392
+ The above copyright notice and this permission notice shall be included in all
2393
+ copies or substantial portions of the Software.
2394
+
2395
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2396
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2397
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2398
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2399
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2400
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2401
+ SOFTWARE.
2402
+
2403
+ ## ajv - 8.20.0 (MIT)
2404
+
2405
+ The MIT License (MIT)
2406
+
2407
+ Copyright (c) 2015-2021 Evgeny Poberezkin
2408
+
2409
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2410
+ of this software and associated documentation files (the "Software"), to deal
2411
+ in the Software without restriction, including without limitation the rights
2412
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2413
+ copies of the Software, and to permit persons to whom the Software is
2414
+ furnished to do so, subject to the following conditions:
2415
+
2416
+ The above copyright notice and this permission notice shall be included in all
2417
+ copies or substantial portions of the Software.
2418
+
2419
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2420
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2421
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2422
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2423
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2424
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2425
+ SOFTWARE.
2426
+
2427
+ ## ajv-formats - 3.0.1 (MIT)
2428
+
2429
+ MIT License
2430
+
2431
+ Copyright (c) 2020 Evgeny Poberezkin
2432
+
2433
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2434
+ of this software and associated documentation files (the "Software"), to deal
2435
+ in the Software without restriction, including without limitation the rights
2436
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2437
+ copies of the Software, and to permit persons to whom the Software is
2438
+ furnished to do so, subject to the following conditions:
2439
+
2440
+ The above copyright notice and this permission notice shall be included in all
2441
+ copies or substantial portions of the Software.
2442
+
2443
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2444
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2445
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2446
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2447
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2448
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2449
+ SOFTWARE.
2450
+
2451
+ ## fast-deep-equal - 3.1.3 (MIT)
2452
+
2453
+ MIT License
2454
+
2455
+ Copyright (c) 2017 Evgeny Poberezkin
2456
+
2457
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2458
+ of this software and associated documentation files (the "Software"), to deal
2459
+ in the Software without restriction, including without limitation the rights
2460
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2461
+ copies of the Software, and to permit persons to whom the Software is
2462
+ furnished to do so, subject to the following conditions:
2463
+
2464
+ The above copyright notice and this permission notice shall be included in all
2465
+ copies or substantial portions of the Software.
2466
+
2467
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2468
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2469
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2470
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2471
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2472
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2473
+ SOFTWARE.
2474
+
2475
+ ## fast-uri - 3.1.8 (BSD-3-Clause)
2476
+
2477
+ Copyright (c) 2011-2021, Gary Court until https://github.com/garycourt/uri-js/commit/a1acf730b4bba3f1097c9f52e7d9d3aba8cdcaae
2478
+ Copyright (c) 2021-present The Fastify team <https://github.com/fastify/fastify#team>
2479
+ All rights reserved.
2480
+
2481
+ Redistribution and use in source and binary forms, with or without
2482
+ modification, are permitted provided that the following conditions are met:
2483
+ * Redistributions of source code must retain the above copyright
2484
+ notice, this list of conditions and the following disclaimer.
2485
+ * Redistributions in binary form must reproduce the above copyright
2486
+ notice, this list of conditions and the following disclaimer in the
2487
+ documentation and/or other materials provided with the distribution.
2488
+ * The names of any contributors may not be used to endorse or promote
2489
+ products derived from this software without specific prior written
2490
+ permission.
2491
+
2492
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
2493
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
2494
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
2495
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY
2496
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
2497
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
2498
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
2499
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2500
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
2501
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2502
+
2503
+ * * *
2504
+
2505
+ The complete list of contributors can be found at:
2506
+ - https://github.com/garycourt/uri-js/graphs/contributors
2507
+
2508
+ ## json-schema-traverse - 1.0.0 (MIT)
2509
+
2510
+ MIT License
2511
+
2512
+ Copyright (c) 2017 Evgeny Poberezkin
2513
+
2514
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2515
+ of this software and associated documentation files (the "Software"), to deal
2516
+ in the Software without restriction, including without limitation the rights
2517
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2518
+ copies of the Software, and to permit persons to whom the Software is
2519
+ furnished to do so, subject to the following conditions:
2520
+
2521
+ The above copyright notice and this permission notice shall be included in all
2522
+ copies or substantial portions of the Software.
2523
+
2524
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2525
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2526
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2527
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2528
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2529
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2530
+ SOFTWARE.
2531
+
2532
+ ## zod - 4.6.5 (MIT)
2533
+
2534
+ MIT License
2535
+
2536
+ Copyright (c) 2025 Colin McDonnell
2537
+
2538
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2539
+ of this software and associated documentation files (the "Software"), to deal
2540
+ in the Software without restriction, including without limitation the rights
2541
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2542
+ copies of the Software, and to permit persons to whom the Software is
2543
+ furnished to do so, subject to the following conditions:
2544
+
2545
+ The above copyright notice and this permission notice shall be included in all
2546
+ copies or substantial portions of the Software.
2547
+
2548
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2549
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2550
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2551
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2552
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2553
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2554
+ SOFTWARE.
2555
+
2556
+ ## zod-to-json-schema - 3.25.2 (ISC)
2557
+
2558
+ ISC License
2559
+
2560
+ Copyright (c) 2020, Stefan Terdell
2561
+
2562
+ Permission to use, copy, modify, and/or distribute this software for any
2563
+ purpose with or without fee is hereby granted, provided that the above
2564
+ copyright notice and this permission notice appear in all copies.
2565
+
2566
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
2567
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
2568
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
2569
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
2570
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
2571
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
2572
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1 @@
1
+ import{r as y,j as t}from"./react-C2DiJaR6.js";import{b as G,t as K,r as q,f as U,a as _,c as X,d as V,h as $,m as J,H as Q,e as Z,D as ee,g as M,s as te,i as se,j as ne,S as oe,L as ae,C as re,k as ie}from"./index-B-myfVQS.js";import"./motion-BAhdRuCw.js";import"./radix-BH0JH8UU.js";const le={"needs-you":"text-status-needs-you-edge",working:"text-status-working",idle:"text-status-idle"},L={top:12,right:12,bottom:24,left:30},T=220,de=76,ce=104,I=1/60;function ue({samples:s,start:n,end:a,tone:g,label:w,describe:l,className:m}){const p=y.useRef(null),[c,k]=y.useState(0),[u,i]=y.useState(null);y.useEffect(()=>{const e=p.current;if(!e)return;const o=new ResizeObserver(([j])=>{j&&k(Math.floor(j.contentRect.width))});return o.observe(e),()=>o.disconnect()},[]);const r=L.left,h=Math.max(r+1,c-L.right),x=L.top,d=T-L.bottom,N=a-n;let O=0;for(const e of s)e.at>=n&&e.at<=a&&e.value>O&&(O=e.value);const A=V(O),b=G(s,{start:n,end:a},{left:r,right:h,top:x,bottom:d,ceiling:A.ceiling}),W=e=>r+(e-n)/N*(h-r),S=e=>d-e/A.ceiling*(d-x),B=K(n,a,Math.max(1,Math.floor((h-r)/de))),R=u??1,H=n+R*N,E=q(b.runs,H),C=E?l(E.value):"Not measured",v=W(H),z=(e,o)=>{const j=o.left+r,f=h-r;i(Math.min(1,Math.max(0,(e-j)/f)))},Y=e=>{const j={ArrowLeft:f=>f-I,ArrowRight:f=>f+I,PageDown:f=>f-I*10,PageUp:f=>f+I*10,Home:()=>0,End:()=>1}[e.key];j&&(e.preventDefault(),i(Math.min(1,Math.max(0,j(u??1)))))};return t.jsxs("div",{ref:p,"data-slot":"history-chart",role:"slider",tabIndex:0,"aria-label":`${w}. Arrow keys read the count at each moment.`,"aria-orientation":"horizontal","aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Math.round(R*100),"aria-valuetext":`${_(H)}: ${C}`,onPointerMove:e=>z(e.clientX,e.currentTarget.getBoundingClientRect()),onPointerLeave:()=>i(null),onFocus:e=>{e.currentTarget.matches(":focus-visible")&&i(o=>o??1)},onBlur:()=>i(null),onKeyDown:Y,className:X("relative w-full cursor-crosshair rounded-row",le[g],m),style:{height:T},children:[c>0&&b.unmeasured.map((e,o)=>t.jsx("div",{"aria-hidden":!0,"data-part":"unmeasured",className:"unmeasured-hatch absolute",style:{left:e.x,top:x,width:e.width,height:d-x}},o)),c>0&&t.jsxs("svg",{"aria-hidden":!0,width:c,height:T,className:"relative block",children:[A.ticks.map(e=>t.jsxs("g",{children:[t.jsx("line",{"data-part":"grid",x1:r,x2:h,y1:S(e),y2:S(e),stroke:e===0?"var(--rule-strong)":"var(--hairline)",strokeWidth:1,shapeRendering:"crispEdges"}),t.jsx("text",{x:r-8,y:S(e),dy:"0.32em",textAnchor:"end",className:"fill-ink-muted font-mono text-micro",children:e})]},e)),b.unmeasured.map((e,o)=>e.width>=ce&&t.jsx("text",{"data-part":"unmeasured-label",x:e.x+8,y:x+14,className:"fill-ink-muted font-mono text-micro",stroke:"var(--glass-float)",strokeWidth:4,strokeLinejoin:"round",paintOrder:"stroke",children:"Not measured"},o)),b.areas.map((e,o)=>t.jsx("path",{d:e,fill:"currentColor",stroke:"none",opacity:.1},o)),b.lines.map((e,o)=>t.jsx("path",{"data-part":"line",d:e,fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"},o)),B.map(e=>{const o=W(e);return o<r+16||o>h-16?null:t.jsx("text",{"data-part":"time-tick",x:o,y:T-6,textAnchor:"middle",className:"fill-ink-muted font-mono text-micro",children:U(e)},e)}),u!==null&&t.jsxs("g",{"data-part":"crosshair",children:[t.jsx("line",{x1:v,x2:v,y1:x,y2:d,stroke:"var(--ink-muted)",strokeWidth:1,shapeRendering:"crispEdges"}),E&&t.jsx("circle",{cx:v,cy:S(Math.min(E.value,A.ceiling)),r:4.5,fill:"currentColor",stroke:"var(--glass-float)",strokeWidth:2})]})]}),u!==null&&c>0&&t.jsxs("div",{"aria-hidden":!0,"data-part":"reading",className:"pointer-events-none absolute z-10 rounded-row bg-glass-float px-2.5 py-1.5 font-mono text-fact whitespace-nowrap inset-ring inset-ring-rule",style:R>.62?{top:x,right:c-v+10}:{top:x,left:v+10},children:[t.jsx("span",{className:"block text-ink-muted",children:_(H)}),t.jsx("span",{className:"block text-ink",children:C})]})]})}const he=6e4;function me(s,n){const[a,g]=y.useState(null),[w,l]=y.useState(null),m=s>Q,p=n?.startedAt??null,c=n?$(n).at:null;if(y.useEffect(()=>{if(!m)return;let u=!0;const i=()=>{Z(s).then(h=>{u&&(g({windowMs:s,history:h}),l(null))},()=>{u&&l(s)})};i();const r=setInterval(i,he);return()=>{u=!1,clearInterval(r)}},[m,s,p,c]),!m)return{history:n,status:n?"ready":"loading"};const k=a?.windowMs===s&&(p===null||a.history.startedAt===p)&&(c===null||$(a.history).at===c)?a.history:null;return k?{history:J(k,n),status:"ready"}:{history:null,status:w===s?"failed":"loading"}}const P={needsYou:{title:"Needs you",about:"How many sessions were waiting on you, over time.",tone:"needs-you",chart:"Sessions that need you",describe:s=>`${s} ${s===1?"needs":"need"} you`,explainer:"A session needs you when it has stopped and cannot go on without an answer: a permission to grant, a question, or a plan to approve. The line is how many were waiting at each moment."},working:{title:"Working",about:"How many sessions were in the middle of a task, over time.",tone:"working",chart:"Sessions working",describe:s=>`${s} working`,explainer:"A session is working while it is in the middle of a task. The line is how many were busy at each moment."},idle:{title:"Idle",about:"How many sessions were ready for a new prompt, over time.",tone:"idle",chart:"Idle sessions",describe:s=>`${s} idle`,explainer:"A session is idle when it has finished what it was asked and is ready for a new prompt. One left idle a day or more is stale, and is counted under Stale instead. The line is how many were idle at each moment."}},D="–";function F({label:s,value:n,caption:a}){return t.jsxs("div",{"data-slot":"figure",className:"min-w-0",children:[t.jsx("p",{className:"text-body font-medium text-ink-secondary",children:s}),t.jsx("p",{className:"mt-2 text-stat font-medium tabular-nums",children:n}),t.jsx("p",{className:"mt-2 text-caption text-ink-muted",children:a})]})}function xe({metric:s,state:n,now:a}){const[g,w]=y.useState("15m"),l=M.find(d=>d.id===g)??M[0],m=P[s],{history:p,status:c}=me(l.ms,n.history),k=a-l.ms,u=p?te(p.points,s):[];let i=null;for(const d of u)d.at<k||d.at>a||(i===null||d.value>i)&&(i=d.value);const r=u[u.length-1],h=r&&a-r.at<=se?r.value:null,x=m.tone==="needs-you"&&ne(n.snapshot?.sessions)===0?"idle":m.tone;return t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-5 flex flex-wrap items-end justify-between gap-x-6 gap-y-4",children:[t.jsxs("div",{className:"flex gap-6",children:[t.jsx(F,{label:"Now",value:h===null?D:String(h),caption:h===null?"Not being measured":"At the last check"}),t.jsx("div",{className:"border-l border-rule pl-6",children:t.jsx(F,{label:"Highest",value:i===null?D:String(i),caption:i===null?"Nothing measured":`In ${l.words}`})})]}),t.jsx(oe,{label:"Time window",value:g,onValueChange:w,options:M.map(({id:d,label:N})=>({value:d,label:N}))})]}),t.jsx("div",{className:"mt-5 h-55",children:c==="ready"?t.jsx(ue,{samples:u,start:k,end:a,tone:x,label:`${m.chart} over ${l.words}`,describe:m.describe}):c==="loading"?t.jsx(ae,{label:`Reading ${l.words}`,className:"h-full"}):t.jsx(re,{tone:"error",title:`${ie(l.words)} could not be read`,children:t.jsx("p",{children:"Agent Lookout did not answer when asked for it. The page asks again every minute. The 15 minute window is still there."})})}),t.jsxs("div",{"data-slot":"explainer",className:"mt-5 rounded-inner bg-fill-quiet px-4 py-3 inset-ring inset-ring-hairline",children:[t.jsx("p",{className:"text-row font-semibold text-ink",children:"What this counts"}),t.jsx("p",{className:"mt-0.5 text-body text-ink-secondary",children:m.explainer}),t.jsxs("p",{className:"mt-1.5 text-body text-ink-secondary",children:["Hatched time was not measured: Agent Lookout was not running, or could not read its source, or the history was cleared then. It is never drawn as zero."," ",n.history?.kept?.where==="memory"?"History covers the last six hours and starts again when Agent Lookout restarts.":"History covers the last six hours, and is kept when Agent Lookout restarts."]})]})]})}function we({metric:s,open:n,onOpenChange:a,state:g,now:w}){const l=P[s];return t.jsx(ee,{open:n,onOpenChange:a,title:l.title,description:l.about,size:"wide",children:t.jsx(xe,{metric:s,state:g,now:w})})}export{we as HistoryPanel};
@@ -0,0 +1 @@
1
+ import{j as e,r as c}from"./react-C2DiJaR6.js";import{F as j,T,p as O,l as q,w as L,u as M,J as P,c as R,n as C,D as W,o as D,q as B,v as _,M as $,x as I,y as J,z as K,A as z,B as G,E as F,G as U,O as V,f as N,I as X,K as Y,N as H,P as Q,Q as Z,R as ee,U as te,V as se,W as v,X as E,Y as ne,L as ae,Z as le}from"./index-B-myfVQS.js";import"./motion-BAhdRuCw.js";import"./radix-BH0JH8UU.js";function re({pullRequest:t}){return e.jsxs(j,{label:"Pull request",children:[e.jsxs(T,{"data-part":"pull-request",href:t.url,target:"_blank",rel:"noreferrer noopener",lines:2,tooltip:`#${t.number} ${t.title}`,className:"wrap-anywhere underline decoration-rule-strong underline-offset-2 transition-colors duration-120 hover:decoration-ink",children:[e.jsxs("span",{className:"tabular-nums",children:["#",t.number]})," ",t.title]}),e.jsx("span",{"data-part":"pull-request-state",className:"block text-caption font-normal text-ink-secondary tabular-nums",children:O(t)})]})}function ie(t,a={}){const{open:s=!0,onStopped:n,request:l=q}=a,[i,r]=c.useState({kind:"idle"}),[o,d]=c.useState(s);s!==o&&(d(s),!s&&!(i.kind==="confirming"&&i.busy)&&r({kind:"idle"}));const u=c.useRef(s);c.useEffect(()=>{u.current=s},[s]);const p=c.useId(),[m]=c.useState(()=>({stop:`${p}-stop`,cancel:`${p}-cancel`,outcome:`${p}-outcome`})),f=c.useRef(null),h=c.useRef(!1),x=c.useRef(!0);c.useEffect(()=>(x.current=!0,()=>{x.current=!1}),[]),c.useEffect(()=>{const b=f.current;f.current=null,b!==null&&document.getElementById(m[b])?.focus()},[i,m]);const g=c.useCallback(()=>{h.current||(f.current="cancel",r({kind:"confirming",busy:!1}))},[]),y=c.useCallback(()=>{h.current||(f.current="stop",r({kind:"idle"}))},[]),k=c.useCallback(()=>{h.current||(h.current=!0,r({kind:"confirming",busy:!0}),l(t).then(b=>{if(h.current=!1,!!x.current){if(b==="stopped"&&n?.(),!u.current){r({kind:"idle"});return}f.current="outcome",r({kind:"answered",outcome:b})}}))},[t,l,n]);return{step:i,ask:g,cancel:y,confirm:k,ids:m}}const A=[];function oe(t,a){const s=CSS.escape(t),n=[...document.querySelectorAll(`:is([data-slot="session-row"], [data-slot="board-card"], [data-slot="hero-session"])[data-session="${s}"] a[data-part="name"]`)];return a!==null&&n.includes(a)?a:n.find(l=>!l.closest('[data-slot="hero-session"]'))??n[0]??null}function ce({session:t,gone:a,asOf:s,now:n}){const l=t.status==="finished"||t.status==="failed",i=Y(t),r=i?"stale":a&&t.status==="needs-you"?"answered":t.status,o=t.statusSince,d=o!==null?Math.max(0,s-o):null,u=H(t,s),p=t.status==="needs-you",m=p?Q(t):null,f=p&&!a&&t.waitingText?.trim()||null,h=o!==null?`${l?"at":i?"idle since":"since"} ${F(o,n)}`:"time not reported",x=[p?Z(t):null,h,u&&ee(u)].filter(g=>typeof g=="string");return e.jsxs(j,{label:"Status",note:f?e.jsxs(e.Fragment,{children:[e.jsx(T,{"data-part":"asking",lines:2,className:"wrap-anywhere",children:f}),m&&e.jsx("span",{className:"block",children:m})]}):m??void 0,children:[e.jsxs("span",{className:"inline-flex items-center gap-2",children:[e.jsx(te,{kind:r}),e.jsx("span",{"data-part":"status",children:i?"Stale":se[t.status]}),d!==null&&e.jsxs("span",{"data-part":"duration",className:"tabular-nums",children:[e.jsx("span",{className:"sr-only",children:l?` ${v(d)} ago`:` for ${v(d)}`}),e.jsxs("span",{"aria-hidden":!0,children:[E(d),l&&" ago"]})]})]}),e.jsx("span",{"data-part":"since",className:"block text-caption font-normal text-ink-secondary tabular-nums",children:x.join(", ")})]})}function ue({session:t,state:a,now:s,asOf:n}){const l=c.useMemo(()=>ne({sessions:a.snapshot?.sessions??A,events:a.events,history:a.history,now:s,asOf:n,eventsFull:a.events.length>=$}),[a.snapshot,a.events,a.history,s,n]);if(!l)return e.jsxs(j,{label:"Waits",children:["Not known",e.jsx("span",{className:"block text-caption font-normal text-ink-secondary",children:"The history could not be read"})]});const i=l.sessions.find(d=>d.id===t.id),r=l.period.today?"today":`since ${N(l.period.from)}`;let o="None";if(i){const d=`${i.startKnown?"":"at least "}${E(i.ms)}`;o=i.times===1?`Once, ${d}`:`${i.times} times, ${d} in all`}return e.jsxs(j,{label:"Waits",children:[e.jsx("span",{"data-part":"waits",className:"tabular-nums",children:o}),e.jsx("span",{className:"block text-caption font-normal text-ink-secondary",children:r})]})}function S({title:t,aside:a,children:s,...n}){return e.jsxs("section",{...n,"aria-label":t,className:"mt-6",children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-4 border-b border-hairline pb-2",children:[e.jsx("h3",{className:"text-body font-semibold text-ink",children:t}),a&&e.jsx("p",{className:"text-caption text-ink-muted",children:a})]}),s]})}function de({session:t,gone:a,state:s,now:n,asOf:l}){const i=s.snapshot?.sources??[],r=s.snapshot?.sessions??A,o=_(t.surface),d=s.events.length>=$,u=c.useMemo(()=>I(s.events.filter(h=>h.sessionId===t.id),r),[s.events,r,t.id]),p=J(s.events,s.history,d),m=c.useMemo(()=>s.history?K({sessions:r,events:s.events,history:s.history,now:n,asOf:l,eventsFull:d}):null,[r,s.events,s.history,n,l,d]),f=m?.rows.find(h=>h.id===t.id);return e.jsxs(e.Fragment,{children:[a&&e.jsx("p",{"data-part":"gone",className:"mt-4 text-body text-ink-secondary",children:"This session has left the list. Here is what was last known of it."}),e.jsxs(z,{className:"mt-3",children:[e.jsx(ce,{session:t,gone:a,asOf:l,now:n}),e.jsx(j,{label:"Agent",children:G(t,i)}),o!==null&&e.jsx(j,{label:"App",children:o}),e.jsx(j,{label:"Folder",mono:t.cwd!==null,children:t.cwd!==null?e.jsx("span",{"data-part":"folder",className:"wrap-anywhere select-text",children:t.cwd.split("/").map((h,x,g)=>e.jsxs(c.Fragment,{children:[h,x<g.length-1&&e.jsxs(e.Fragment,{children:["/",e.jsx("wbr",{})]})]},x))}):"Not known"}),t.git?.repository!==void 0&&e.jsx(j,{label:"Repository",children:e.jsx("span",{"data-part":"repository",className:"wrap-anywhere",children:t.git.repository.name})}),t.git?.branch!==void 0?e.jsx(j,{label:"Branch",children:e.jsx("span",{"data-part":"branch",className:"wrap-anywhere",children:t.git.branch})}):t.git?.commit!==void 0&&e.jsx(j,{label:"Commit",mono:!0,children:t.git.commit}),t.git?.pullRequest!==void 0&&e.jsx(re,{pullRequest:t.git.pullRequest}),e.jsx(j,{label:"Started",children:e.jsx("span",{className:"tabular-nums",children:t.startedAt!==null?F(t.startedAt,n):"Not reported"})}),t.pid!==void 0&&e.jsx(j,{label:"Process",mono:!0,note:t.alive===!1?"The process has ended.":!a&&t.source==="claude-code"&&t.surface==="desktop"?U:void 0,children:t.pid}),e.jsx(ue,{session:t,state:s,now:n,asOf:l})]}),e.jsx(S,{"data-part":"events",title:"Events",aside:p?`since ${N(p.at)}`:void 0,children:u.length>0?e.jsx(V,{entries:u,now:n,className:"pt-0.5"}):e.jsxs("p",{className:"py-2.5 text-body text-ink-secondary",children:["Nothing has happened to this session"," ",p?`since ${N(p.at)}`:"yet","."]})}),e.jsx(S,{"data-part":"timeline",title:"Timeline",aside:"last hour",children:m&&f?e.jsx(X,{timeline:m,rows:[f],named:!1,label:"Its last hour",className:"pt-3"}):e.jsx("p",{className:"py-2.5 text-body text-ink-secondary",children:m?"It did not run in the last hour.":"The last hour could not be read. The page asks again every two seconds."})})]})}function pe({sessionId:t,open:a,onClose:s,state:n,now:l,onStopped:i}){const r=n.snapshot?.sessions.find(w=>w.id===t)??null,[o,d]=c.useState(null);r!==null&&r!==o?.session?d({session:r,at:n.lastOkAt??l}):r===null&&o?.session.waitingText!==void 0&&d({...o,session:L(o.session)});const u=r??o?.session??null,p=r===null&&o!==null,m=M(u??{id:t,source:"status-files",surface:"unknown",links:{}}),f=p&&o?o.at:n.phase==="stalled"&&n.lastOkAt!==null?n.lastOkAt:l,h=m.outcome!==null||m.asking!==null,x=ie(t,{open:a,onStopped:i}),g=x.step.kind==="answered"||x.step.kind==="confirming"&&(x.step.busy||!p);let y,k,b="wide";return u?(y=u.name,k=e.jsxs(e.Fragment,{children:[!p&&e.jsx("div",{className:R("flex flex-wrap items-center gap-x-2 gap-y-1",h&&"mt-3"),children:e.jsx(P,{session:u,jump:m})}),g&&e.jsx(C,{session:u,stop:x,className:"mt-3"}),e.jsx(de,{session:u,gone:p,state:n,now:l,asOf:f})]})):n.snapshot===null?(y="Session",k=e.jsx(ae,{label:"Reading sessions",className:"justify-start px-0 pt-5 pb-1"})):(y="No such session",b="sm",k=e.jsxs("div",{"data-part":"missing",children:[e.jsx("p",{className:"mt-2 text-body text-ink-secondary",children:"Agent Lookout is not watching a session at this address. It may have ended before this page was opened."}),e.jsx(le,{className:"mt-4",onClick:s,children:"Go to the Overview"})]})),e.jsx(W,{open:a,onOpenChange:w=>{w||s()},title:y,size:b,focusTitle:!0,returnFocus:w=>oe(t,w)??document.querySelector("main"),actions:u&&!p&&e.jsxs(e.Fragment,{children:[e.jsx(D,{session:u,jump:m,variant:u.status==="needs-you"?"needs-you":"quiet"}),e.jsx(B,{session:u,stop:x})]}),children:k})}function je({sessionId:t,onClose:a,state:s,now:n,onStopped:l}){const[i,r]=c.useState(t);return t!==null&&t!==i&&r(t),i===null?null:e.jsx(pe,{sessionId:i,open:t!==null,onClose:a,state:s,now:n,onStopped:l},i)}export{je as SessionPanel};