agent-lookout 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -28,11 +28,11 @@
28
28
  document.documentElement.setAttribute("data-theme", theme);
29
29
  })();
30
30
  </script>
31
- <script type="module" crossorigin src="/assets/index-B-myfVQS.js"></script>
31
+ <script type="module" crossorigin src="/assets/index-a7ubgAmE.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">
35
- <link rel="stylesheet" crossorigin href="/assets/index-Cceyz9yZ.css">
35
+ <link rel="stylesheet" crossorigin href="/assets/index-BsTJp5-3.css">
36
36
  </head>
37
37
  <body>
38
38
  <div id="root"></div>
package/docs/API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API
2
2
 
3
- Agent Lookout's server answers a small HTTP API under `/api/`. The dashboard reads it, and so do `agent-lookout status` and `agent-lookout mcp`. It is meant for this computer only: it listens on a loopback address, and it answers only requests made to a loopback name. Every route returns JSON, and every route but four only reads.
3
+ Agent Lookout's server answers a small HTTP API under `/api/`. The dashboard reads it, and so do `agent-lookout status` and `agent-lookout mcp`. It is meant for this computer only: it listens on a loopback address, and it answers only requests made to a loopback name. Every route returns JSON, and every route but six only reads.
4
4
 
5
5
  The answers hold your session names and folder paths. Check one before you share it.
6
6
 
@@ -31,30 +31,33 @@ curl -s http://127.0.0.1:4777/api/sessions
31
31
 
32
32
  Every answer has `Content-Type: application/json; charset=utf-8`, `Cache-Control: no-store`, `X-Content-Type-Options: nosniff` and `Cross-Origin-Resource-Policy: same-origin`. Every answer that is not a 200 has the body `{ "error": "<one sentence>" }`.
33
33
 
34
- | Status | When |
35
- | ------ | ------------------------------------------------------------------------------------------------------ |
36
- | 400 | The address, `since` or `windowMs` could not be read |
37
- | 403 | The `Host`, the `Origin` or `Sec-Fetch-Site` above |
38
- | 404 | There is no route at that path |
39
- | 405 | Any method but `GET`, with `Allow: GET`. The four routes that act take only `POST`, with `Allow: POST` |
40
- | 500 | Something went wrong that the server did not expect |
34
+ | Status | When |
35
+ | ------ | ----------------------------------------------------------------------------------------------------- |
36
+ | 400 | The address, `since` or `windowMs` could not be read |
37
+ | 403 | The `Host`, the `Origin` or `Sec-Fetch-Site` above |
38
+ | 404 | There is no route at that path |
39
+ | 405 | Any method but `GET`, with `Allow: GET`. The six routes that act take only `POST`, with `Allow: POST` |
40
+ | 500 | Something went wrong that the server did not expect |
41
41
 
42
42
  ## Routes
43
43
 
44
- | Route | Answers |
45
- | ---------------------------------- | --------------------------------------------------------------------------------------- |
46
- | `GET /api/health` | `{ ok: true, version }` |
47
- | `GET /api/sessions` | The latest snapshot: `{ generatedAt, sources, sessions }` |
48
- | `GET /api/events?since=<epoch ms>` | `{ events }`, newest first, at most 200 |
49
- | `GET /api/history?windowMs=<ms>` | `{ points, startedAt, since, kept, restarts }`, one point for each poll, oldest first |
50
- | `GET /api/email` | `{ on, to, events, afterMs, asking, problem, last, limitedUntil }` |
51
- | `GET /api/webhook` | `{ on, host, events, afterMs, asking, problem, last, limitedUntil }` |
52
- | `GET /api/pull-requests` | `{ on, problem, gh, last }`: whether each branch's pull request is shown |
53
- | `GET /api/waits` | `{ at, today, sevenDays, since, where }`: how long sessions waited on you |
54
- | `POST /api/jump` | `{ ok: true, kind, place }`, with `app` for a terminal tab. One of four routes that act |
55
- | `POST /api/history/clear` | `{ ok: true, clearedAt }`. A route that acts |
56
- | `POST /api/sessions/stop` | `{ ok: true }` once the session's process has ended. A route that acts |
57
- | `POST /api/sessions/clean-up` | `{ results }`, what became of each session left running. A route that acts |
44
+ | Route | Answers |
45
+ | ---------------------------------- | -------------------------------------------------------------------------------------- |
46
+ | `GET /api/health` | `{ ok: true, version }` |
47
+ | `GET /api/sessions` | The latest snapshot: `{ generatedAt, sources, sessions, answering, timeRules, quiet }` |
48
+ | `GET /api/events?since=<epoch ms>` | `{ events }`, newest first, at most 200 |
49
+ | `GET /api/history?windowMs=<ms>` | `{ points, startedAt, since, kept, restarts }`, one point for each poll, oldest first |
50
+ | `GET /api/email` | `{ on, to, events, afterMs, asking, problem, last, limitedUntil }` |
51
+ | `GET /api/webhook` | `{ on, host, events, afterMs, asking, problem, last, limitedUntil }` |
52
+ | `GET /api/pull-requests` | `{ on, problem, gh, last }`: whether each branch's pull request is shown |
53
+ | `GET /api/waits` | `{ at, today, sevenDays, since, where }`: how long sessions waited on you |
54
+ | `GET /api/settings` | `{ timeRules, file, problem }`: the time rules, and the file they are kept in |
55
+ | `POST /api/jump` | `{ ok: true, kind, place }`, with `app` for a terminal tab. One of six routes that act |
56
+ | `POST /api/history/clear` | `{ ok: true, clearedAt }`. A route that acts |
57
+ | `POST /api/sessions/stop` | `{ ok: true }` once the session's process has ended. A route that acts |
58
+ | `POST /api/sessions/clean-up` | `{ results }`, what became of each session left running. A route that acts |
59
+ | `POST /api/permission/answer` | `{ ok: true, decision }` once a permission prompt was answered. A route that acts |
60
+ | `POST /api/settings/time-rules` | `{ ok: true, timeRules }` once the time rules are saved. A route that acts |
58
61
 
59
62
  Times are milliseconds since 1970, and lengths of time are milliseconds.
60
63
 
@@ -98,50 +101,70 @@ The snapshot the collector made at its last poll, two seconds apart at most:
98
101
  "links": {},
99
102
  "stale": false
100
103
  }
101
- ]
104
+ ],
105
+ "timeRules": {
106
+ "longWait": { "on": true, "minutes": 10 },
107
+ "idle": { "on": false, "hours": 48 },
108
+ "quietHours": {
109
+ "on": true,
110
+ "from": "22:00",
111
+ "to": "08:00",
112
+ "days": ["mon", "tue", "wed", "thu", "fri"],
113
+ "leaveOutAnswered": false
114
+ }
115
+ },
116
+ "quiet": false
102
117
  }
103
118
  ```
104
119
 
120
+ `timeRules` are the [time rules](GUIDE.md#time-rules) in force when the snapshot was made, as `GET /api/settings` gives them. Every session's `stale` follows the idle rule, worked out again at each poll whatever its source said, and the dashboard's notifications follow the reminder by them, at the snapshot's `generatedAt`. `quiet` says whether the snapshot was made in quiet hours, by the clock and time zone of the computer Agent Lookout runs on, and the dashboard holds its notifications by it rather than by its own clock. A session on another machine is stale by this computer's idle rule, and its waits are reminded of and held back here by this computer's rules: the `timeRules` and `quiet` in that machine's own snapshot are not read.
121
+
105
122
  Sessions come in this order: those that need you first, longest wait first, then working, idle, finished, failed and unknown, each with the most recent change first.
106
123
 
107
- | Session field | Holds |
108
- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
109
- | `id` | Stable across polls: the source, a colon, and the agent's own id for the session |
110
- | `source` | `claude-code`, `codex` or `status-files` |
111
- | `agent` | The agent's own name, for a session from a status file. Left out for the others, whose agent is the source's `label` |
112
- | `surface` | `terminal`, `vscode`, `desktop`, `cloud`, `browser` or `unknown` |
113
- | `name` | What the dashboard calls it |
114
- | `cwd` | The folder it works in, or `null` |
115
- | `project` | The last part of `cwd`, or `null` |
116
- | `git` | `{ branch }`, or `{ commit }` with seven characters when no branch is checked out, each with `repository`. Left out when the folder is in no repository that could be read |
117
- | `git.repository` | `{ id, name }`: the repository the folder belongs to, the same for its main folder and every worktree of it. `name` is the main folder's name. `id` is 16 hexadecimal characters, a hash of the path of the repository's own git folder, which its worktrees share, for telling repositories apart. Left out for a repository with no name, at the root, or a worktree whose `commondir` cannot be read |
118
- | `git.pullRequest` | With `AGENT_LOOKOUT_PULL_REQUESTS=on` only, for a branch whose pull request on github.com your own `gh` found: `{ number, title, state, checks, url }`. Described below. Left out with the setting off, for a commit, for the default branch, for a branch named with digits alone, such as `51`, for a repository with no remote on github.com, and for a branch with no pull request |
119
- | `status` | `needs-you`, `working`, `idle`, `finished`, `failed` or `unknown` |
120
- | `waitingReason` | For `needs-you` only: `permission`, `question` or `other` |
121
- | `waitingDetail` | The agent's own words for the wait, when it gives any |
122
- | `waitingText` | For `needs-you` Claude Code sessions only: what the session is asking, in a line or two, read from the last message of its transcript, such as `Run: npm test`, `Edit: src/app.ts` or the question it put. Plain text of at most 200 characters. Left out when the transcript does not say, or with `AGENT_LOOKOUT_WAITING_TEXT=off`. No event in `/api/events`, and no history point, holds it. An email or webhook post holds it only with `AGENT_LOOKOUT_EMAIL_ASKING=on` or `AGENT_LOOKOUT_WEBHOOK_ASKING=on` |
123
- | `startedAt` | When it started, or `null` |
124
- | `statusSince` | When its status last changed, or `null`. It does not move while a wait goes on |
125
- | `lastWriteAt` | When its agent last wrote the file it is read from, for Codex and status files |
126
- | `pid`, `alive` | Its process and whether that still runs, when known |
127
- | `links.open` | A `vscode://` address that opens it, for a session in VS Code |
128
- | `jump` | `{ kind: "tmux", place }` or `{ kind: "terminal", app, place }` when `POST /api/jump` can take you to it |
129
- | `stop` | `{ how: "signal" }` or `{ how: "background" }` when `POST /api/sessions/stop` can stop it: a Claude Code session in a terminal or VS Code, or a background job. It holds nothing else |
130
- | `stale` | `true` once it has been idle for 24 hours |
131
-
132
- | Source field | Holds |
133
- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
134
- | `id`, `label` | `claude-code` and `Claude Code`, `codex` and `Codex`, or `status-files` and `Status files` |
135
- | `state` | `ok`, `searching`, `unavailable`, `not-set-up` or `error` |
136
- | `detail` | One plain sentence: how it is being read, or what went wrong |
137
- | `advice` | One plain sentence saying what you can do about a problem, when the collector knows |
138
- | `watching` | `[{ label, value }]`: what it reads and runs, and how often |
139
- | `basis` | Which way the sessions were read, for a source that has more than one |
140
- | `capabilities` | What its agent can report, and whether its sessions can be stopped: for each of eight things, `{ level: "yes" }`, or `{ level: "no" }` or `"partly"` with a `reason` |
141
- | `checkedAt` | When it was last read |
124
+ | Session field | Holds |
125
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
126
+ | `id` | Stable across polls: the source, a colon, and the agent's own id for the session. A session on another machine has that machine's own id for it after its source: `remote:devbox:claude-code:…` |
127
+ | `source` | `claude-code`, `codex` or `status-files`, or for a session on another machine `remote:` and the machine's name, such as `remote:devbox` |
128
+ | `agent` | The agent's own name, for a session from a status file, and for a session on another machine, such as `Claude Code`. Left out for the others, whose agent is the source's `label` |
129
+ | `machine` | The other machine it runs on, as `AGENT_LOOKOUT_REMOTES` names it: letters, digits and dashes, such as `devbox`. Left out for a session on this computer. Such a session has no `pid`, `links.open`, `jump`, `stop`, `ask` or `git.pullRequest`, since those act on this computer or are read from its folders |
130
+ | `surface` | `terminal`, `vscode`, `desktop`, `cloud`, `browser` or `unknown` |
131
+ | `name` | What the dashboard calls it |
132
+ | `cwd` | The folder it works in, or `null` |
133
+ | `project` | The last part of `cwd`, or `null` |
134
+ | `git` | `{ branch }`, or `{ commit }` with seven characters when no branch is checked out, each with `repository`. Left out when the folder is in no repository that could be read |
135
+ | `git.repository` | `{ id, name }`: the repository the folder belongs to, the same for its main folder and every worktree of it. `name` is the main folder's name. `id` is 16 hexadecimal characters, a hash of the path of the repository's own git folder, which its worktrees share, for telling repositories apart. Left out for a repository with no name, at the root, or a worktree whose `commondir` cannot be read |
136
+ | `git.pullRequest` | With `AGENT_LOOKOUT_PULL_REQUESTS=on` only, for a branch whose pull request on github.com your own `gh` found: `{ number, title, state, checks, url }`. Described below. Left out with the setting off, for a commit, for the default branch, for a branch named with digits alone, such as `51`, for a repository with no remote on github.com, for a branch with no pull request, and for a session on another machine |
137
+ | `status` | `needs-you`, `working`, `idle`, `finished`, `failed` or `unknown` |
138
+ | `waitingReason` | For `needs-you` only: `permission`, `question` or `other` |
139
+ | `waitingDetail` | The agent's own words for the wait, when it gives any |
140
+ | `waitingText` | For `needs-you` Claude Code sessions only, and sessions on another machine whose Agent Lookout sent it: what the session is asking, in a line or two, read from the last message of its transcript, such as `Run: npm test`, `Edit: src/app.ts` or the question it put. Plain text of at most 200 characters. Left out when the transcript does not say, or with `AGENT_LOOKOUT_WAITING_TEXT=off`. No event in `/api/events`, and no history point, holds it. An email or webhook post holds it only with `AGENT_LOOKOUT_EMAIL_ASKING=on` or `AGENT_LOOKOUT_WEBHOOK_ASKING=on` |
141
+ | `startedAt` | When it started, or `null` |
142
+ | `statusSince` | When its status last changed, or `null`. It does not move while a wait goes on |
143
+ | `lastWriteAt` | When its agent last wrote the file it is read from, for Codex and status files |
144
+ | `pid`, `alive` | Its process and whether that still runs, when known |
145
+ | `links.open` | A `vscode://` address that opens it, for a session in VS Code |
146
+ | `jump` | `{ kind: "tmux", place }` or `{ kind: "terminal", app, place }` when `POST /api/jump` can take you to it |
147
+ | `stop` | `{ how: "signal" }` or `{ how: "background" }` when `POST /api/sessions/stop` can stop it: a Claude Code session in a terminal or VS Code, or a background job. It holds nothing else |
148
+ | `ask` | For `needs-you` Claude Code sessions with the Agent Lookout plugin only, while Agent Lookout holds the session's permission request: `{ requestId, tool, command, description, inputs, allow, denyOnly, subagent, until }`. `command` is the whole command for `Bash`, every line, and `inputs` is `[{ name, value }]`, each input as text, for any other tool or the rest of a command's. `allow` says whether `POST /api/permission/answer` takes `allow`, which it does only when the whole of what is allowed is here: never for `Edit`, `Write`, `MultiEdit` or `NotebookEdit`, and not for `ExitPlanMode` or `AskUserQuestion`, past 4,000 characters or 40 lines, with a character that cannot be shown as it is, with more than two blank lines in a row, or for a command holding right-to-left letters, with `denyOnly` saying why: `edit`, `too-long`, `hidden-characters`, `not-yes-or-no`, `right-to-left` or `blank-lines`. `until` is when the request is let go. Kept in memory only while it is held: no event, history point, email, webhook post or answer of `agent-lookout mcp` holds it |
149
+ | `stale` | `true` once it has been idle for 24 hours, or for as long as the idle rule in `timeRules` says while it is on. For a session on another machine too, by this computer's rule, whatever that machine said |
150
+
151
+ | Source field | Holds |
152
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
153
+ | `id`, `label` | `claude-code` and `Claude Code`, `codex` and `Codex`, or `status-files` and `Status files`, or for another machine `remote:devbox` and `devbox` |
154
+ | `state` | `ok`, `searching`, `unavailable`, `not-set-up` or `error` |
155
+ | `detail` | One plain sentence: how it is being read, or what went wrong |
156
+ | `advice` | One plain sentence saying what you can do about a problem, when the collector knows |
157
+ | `watching` | `[{ label, value }]`: what it reads and runs, and how often |
158
+ | `basis` | Which way the sessions were read, for a source that has more than one |
159
+ | `capabilities` | What its agent can report, and whether its sessions can be stopped and answered: for each of nine things, `{ level: "yes" }`, or `{ level: "no" }` or `"partly"` with a `reason` |
160
+ | `checkedAt` | When it was last read |
161
+ | `machine` | For another machine: its name, which its sessions carry as their `machine` |
162
+ | `agents` | For another machine: `[{ label, capabilities }]`, what each agent found there can report, as last read from it, with `jump`, `stop` and `answer` always `no` |
142
163
 
143
164
  The [guide](GUIDE.md#what-each-agent-can-report) has the table `capabilities` holds.
144
165
 
166
+ Another machine that `AGENT_LOOKOUT_REMOTES` names is a source of its own, after this computer's three. Its `state` is `ok` while its sessions are read, `searching` while ssh signs in the first time, for 10 seconds at most, and `unavailable`, with the reason in `detail` and what to do in `advice`, for anything else: still connecting after those 10 seconds, ssh not found, the machine turned ssh away, the connection dropped, no Agent Lookout answering there, or an answer that cannot be read. A machine that is switched off is no fault of this computer, so it is not `error`. Its `watching` gives the ssh target, the command, the routes asked for, how often, the version there and the state of each source there, the first 20. When `AGENT_LOOKOUT_REMOTES` is set but cannot be read, one source stands for the machines instead: `remote:`, with no name after it, labelled `Other machines`, `not-set-up`, with what is wrong in `detail` and how to correct it in `advice`, and no sessions. Its sessions are listed with this computer's, in the same order, and counted in the same way. [Another machine over SSH](GUIDE.md#another-machine-over-ssh) has the rest.
167
+
145
168
  With `AGENT_LOOKOUT_PULL_REQUESTS=on`, a session on a branch that has a pull request carries it in `git`:
146
169
 
147
170
  ```json
@@ -170,10 +193,11 @@ With `AGENT_LOOKOUT_PULL_REQUESTS=on`, a session on a branch that has a pull req
170
193
  | `url` | Its page, `https://github.com/<owner>/<repository>/pull/<number>`, which the collector builds from the remote and the number rather than take from GitHub |
171
194
 
172
195
  It is what `gh` last said, asked at most once every 2 minutes for each repository and branch, so it can be up to about 2 minutes old. [Pull requests](GUIDE.md#pull-requests) in the guide says when it is asked for.
196
+ `answering` says whether permission prompts can be answered from the dashboard: `{ state, plugin, holdMs, problem }`. `state` is `on` while Agent Lookout listens for the plugin's requests, `off` with `AGENT_LOOKOUT_ANSWER=off`, and `unavailable` when it could not listen, with `problem` saying why in one sentence. `plugin` is `seen` once a request has arrived since the last permission prompt it missed, `missed` when a Claude Code session waited for permission and no request arrived, as for a session without the plugin, and `unknown` before either. `holdMs` is how long a request is held.
173
197
 
174
198
  ### `GET /api/events?since=<epoch ms>`
175
199
 
176
- `{ "events": [...] }`: what changed, newest first, at most 200, and with `since` only those after it. Each event is `{ id, at, sessionId, sessionName, kind, from, to, severity }`, where `kind` is `appeared`, `status-changed`, `ended` or `stopped`, `from` and `to` are statuses, and `severity` is `advisory`, `warning` for a change to `needs-you` or `critical` for a change to `failed`. A `stopped` event says that Agent Lookout stopped the session, when you pressed Stop or ended the sessions left running: it has `from`, the status the session had, no `to`, and `by: "agent-lookout"`. Its leaving the list is an `ended` event of its own. A reader should pass over a `kind` it does not know: later versions may add others. The collector keeps the last 1,000 in memory, and with history kept on disk it reads them back when it starts again.
200
+ `{ "events": [...] }`: what changed, newest first, at most 200, and with `since` only those after it. Each event is `{ id, at, sessionId, sessionName, kind, from, to, severity }`, where `kind` is `appeared`, `status-changed`, `ended`, `stopped` or `answered`, `from` and `to` are statuses, and `severity` is `advisory`, `warning` for a change to `needs-you` or `critical` for a change to `failed`. A `stopped` event says that Agent Lookout stopped the session, when you pressed Stop or ended the sessions left running: it has `from`, the status the session had, no `to`, and `by: "agent-lookout"`. Its leaving the list is an `ended` event of its own. An `answered` event says that Agent Lookout answered a permission prompt of the session's, when you pressed Allow or Deny: it has `from`, `by: "agent-lookout"` and `decision`, `allow` or `deny`, and nothing of what was asked. A reader should pass over a `kind` it does not know: later versions may add others. The collector keeps the last 1,000 in memory, and with history kept on disk it reads them back when it starts again.
177
201
 
178
202
  ### `GET /api/history?windowMs=<ms>`
179
203
 
@@ -302,9 +326,61 @@ Whether Agent Lookout shows each session's [pull request](GUIDE.md#pull-requests
302
326
 
303
327
  It never holds a token: `gh` signs in with its own login, which Agent Lookout never reads.
304
328
 
329
+ ### `GET /api/settings`
330
+
331
+ The settings Agent Lookout keeps itself, in a file on this computer, so they hold with no dashboard open: the [time rules](GUIDE.md#time-rules). It only reads.
332
+
333
+ ```json
334
+ {
335
+ "timeRules": {
336
+ "longWait": { "on": true, "minutes": 10 },
337
+ "idle": { "on": true, "hours": 48 },
338
+ "quietHours": {
339
+ "on": true,
340
+ "from": "22:00",
341
+ "to": "08:00",
342
+ "days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
343
+ "leaveOutAnswered": false
344
+ }
345
+ },
346
+ "file": "~/.agent-lookout/settings.json",
347
+ "problem": null
348
+ }
349
+ ```
350
+
351
+ | Field | Holds |
352
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
353
+ | `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 |
354
+ | `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 |
355
+ | `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 |
356
+ | `quietHours.leaveOutAnswered` | Whether the summary sent when quiet hours end leaves out the waits that were answered while they held |
357
+ | `file` | The file they are kept in, `~/.agent-lookout/settings.json` or the one `AGENT_LOOKOUT_SETTINGS_FILE` names, with your home folder written `~` |
358
+ | `problem` | While the file could not be read when Agent Lookout started, or the last change could not be saved: one sentence that says why and names the file. `null` otherwise |
359
+
360
+ Every rule is off until you turn it on. A rule the file holds in a form that cannot be read is off, and `problem` names it.
361
+
362
+ ### `POST /api/settings/time-rules`
363
+
364
+ A route that acts. It puts the time rules in force and saves them in the settings file, as the Time rules card in Settings does. It makes the same checks as `POST /api/jump`, with its own action, in `src/collector/settings/timeRulesRoute.ts`, and answers only a request that:
365
+
366
+ - is a `POST`. Any other method gets 405, with `Allow: POST`.
367
+ - has an `Origin` that names this computer, or gets 403.
368
+ - is marked `same-origin` in `Sec-Fetch-Site`, when that header is sent, or gets 403.
369
+ - carries `X-Agent-Lookout-Action: time-rules`, or gets 403.
370
+ - has `Content-Type: application/json`, or gets 415.
371
+ - 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.
372
+
373
+ 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.
374
+
375
+ | Status | Body |
376
+ | ------ | ----------------------------------------------------------------------------------------------- |
377
+ | 200 | `{ "ok": true, "timeRules": { ... } }`: the rules now in force |
378
+ | 400 | `reason: "invalid"`, with an `error` sentence that says what the body must be |
379
+ | 500 | `reason: "not-saved"`: the file could not be written, as when it is a link, and nothing changed |
380
+
305
381
  ### `POST /api/jump`
306
382
 
307
- One of the four routes that act. It selects the tmux pane a session runs in, or brings its tab of Terminal or iTerm2 to the front, as the Jump button does. Its checks, on top of the ones every request passes, are `jumpRefusalFor` in `src/collector/jumpRoute.ts`, which makes those `actionRefusalFor` in `src/collector/handler.ts` makes for every route that acts. It answers only a request that:
383
+ One of the six routes that act. It selects the tmux pane a session runs in, or brings its tab of Terminal or iTerm2 to the front, as the Jump button does. Its checks, on top of the ones every request passes, are `jumpRefusalFor` in `src/collector/jumpRoute.ts`, which makes those `actionRefusalFor` in `src/collector/handler.ts` makes for every route that acts. It answers only a request that:
308
384
 
309
385
  - is a `POST`. Any other method gets 405, with `Allow: POST`.
310
386
  - has an `Origin` that names this computer. A request with no `Origin` gets 403 here, though a read may go without one.
@@ -389,7 +465,7 @@ A route that acts. It ends the sessions left running that you chose, as End does
389
465
  }
390
466
  ```
391
467
 
392
- with 1 to 20 sessions, none named twice, each with the `statusSince` the page showed for it. Anything else gets 400. For each session in turn the server makes every check of `POST /api/sessions/stop`, and then reads in the session's registry file, and in its own latest list of sessions, that it is still idle, since that same `statusSince`, and that this was 24 hours or more ago. A session that has done anything since is left running. Then it stops each one that passed, as the stop route does, and waits up to 10 seconds for them all.
468
+ with 1 to 20 sessions, none named twice, each with the `statusSince` the page showed for it. Anything else gets 400. For each session in turn the server makes every check of `POST /api/sessions/stop`, and then reads in the session's registry file, and in its own latest list of sessions, that it is still idle, since that same `statusSince`, and that this was 24 hours or more ago, or as long ago as the idle rule says while it is on. A session that has done anything since is left running. Then it stops each one that passed, as the stop route does, and waits up to 10 seconds for them all.
393
469
 
394
470
  ```json
395
471
  {
@@ -402,6 +478,32 @@ with 1 to 20 sessions, none named twice, each with the `statusSince` the page sh
402
478
 
403
479
  The answer is 200 with an `outcome` for each session asked for, in order: `ended`, `became-active`, `not-stale`, `gone`, `unsupported`, `cannot-confirm`, `not-allowed`, `still-running` or `failed`, which mean what the stop route's reasons do. One `stopped` event is added for each session ended. A clean-up while a stop or another clean-up is under way, or less than a second after one began, gets 429 with `reason: "too-soon"`.
404
480
 
481
+ ### `POST /api/permission/answer`
482
+
483
+ A route that acts. It answers one permission prompt of a Claude Code session, as Allow and Deny do in Needs you and in the session's details. It makes the same checks as `POST /api/jump`, with `X-Agent-Lookout-Action: answer`, in `src/collector/answers/answerRoute.ts`, and takes a body of 1,024 bytes or less that is exactly:
484
+
485
+ ```json
486
+ {
487
+ "sessionId": "claude-code:00000000-0000-4000-8000-000000000001",
488
+ "requestId": "0123456789abcdef0123456789abcdef",
489
+ "decision": "allow"
490
+ }
491
+ ```
492
+
493
+ with `decision` `allow` or `deny`, and `requestId` the one the session's `ask` gave. Anything else gets 400. The server answers only a request it holds for that session under that ID, `allow` only when `ask.allow` is `true`, and only after it has read the session's registry file again and found it still says the session is waiting. It then hands the plugin's hook one of two fixed answers, allow, or deny with a sentence that says the person denied it from Agent Lookout, and never a rewritten input or a permission rule. An answer adds an `answered` event, and the server reads the sessions again.
494
+
495
+ | Status | Body |
496
+ | ------ | -------------------------------------------------------------------------------------------------------- |
497
+ | 200 | `{ "ok": true, "decision": "allow" }`: the answer was handed to the session |
498
+ | 404 | `reason: "no-ask"`: no such request is held, or the session is not listed |
499
+ | 409 | `reason: "not-allowable"`: `allow` for a request that offers only Deny |
500
+ | 409 | `reason: "gone"`: the session was answered there, is no longer waiting, or the request was held too long |
501
+ | 429 | `reason: "too-soon"`: that request is being answered |
502
+
503
+ Each of these has an `error` sentence beside its `reason`. With `AGENT_LOOKOUT_ANSWER=off` there is no such route, and a POST to it gets 405.
504
+
505
+ The plugin's hook reaches Agent Lookout another way, not through this API: a Unix socket, `~/.agent-lookout/answer.sock` unless `AGENT_LOOKOUT_ANSWER_SOCKET` names another, of mode 600 in a folder of mode 700. It takes `POST /hooks/permission-request` with `X-Agent-Lookout-Hook: permission-request` and the hook's input as a JSON body of 1 MiB or less, holds it, and answers it with the decision JSON when you press Allow or Deny, or with an empty 200 when the request is let go. [Answer a permission prompt](GUIDE.md#answer-a-permission-prompt) says when that is.
506
+
405
507
  ## In the Mac app only
406
508
 
407
509
  The Mac app serves the dashboard and this API from its own address, `agent-lookout://app/`, with no port, and answers six more routes there, for its updates and its menu bar item. `npm start`, `npx agent-lookout` and `npm run dev` do not have them: there, each answers 404.
@@ -429,6 +531,8 @@ The API has no token or password. The `Host` and `Origin` rules keep websites, a
429
531
 
430
532
  A token would not keep out programs running as you, which could read it as easily as the API. It could keep out other accounts on a shared computer, stopping your sessions included, the known limit above, and is left for a later version. The MCP server changes none of this: it opens no port of its own, speaks to the app that started it over stdin and stdout, and reads the same loopback API the dashboard and `agent-lookout status` read without one. A token is also for an API that can be reached some other way, such as from another computer, and Agent Lookout has none.
431
533
 
534
+ Another computer reads it only through SSH: with `AGENT_LOOKOUT_REMOTES` set there, Agent Lookout on that computer runs `ssh` to this one and forwards a port of its own loopback to this one's `127.0.0.1:4777`. Its requests then arrive on this computer's loopback, from the ssh server, which only someone who can sign in to this computer can open. It asks for `GET /api/health` and `GET /api/sessions`, with no `Origin` and no notifications header, and nothing else. [Another machine over SSH](GUIDE.md#another-machine-over-ssh) describes it.
535
+
432
536
  ## The MCP server
433
537
 
434
538
  `agent-lookout mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) 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. For each tool call it reads `GET /api/sessions` once, exactly as `agent-lookout status` does: from a loopback address only, with no `Origin` and no notifications header. It has three tools, and none of them acts: