@pmndrs/pine 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +180 -2
  3. package/bin/pine.js +27 -0
  4. package/docs/SKILL.md +189 -0
  5. package/docs/references/agents.md +368 -0
  6. package/docs/references/coding.md +126 -0
  7. package/docs/references/configuration.md +229 -0
  8. package/docs/references/events.md +263 -0
  9. package/docs/references/mcp.md +257 -0
  10. package/docs/references/operations.md +280 -0
  11. package/docs/references/overview.md +186 -0
  12. package/docs/references/security.md +489 -0
  13. package/docs/references/setup.md +181 -0
  14. package/docs/references/sources.md +768 -0
  15. package/docs/references/stack.md +105 -0
  16. package/docs/references/tui.md +348 -0
  17. package/examples/home/.env.example +5 -0
  18. package/examples/home/USER.md +4 -0
  19. package/examples/home/agents/main/agent.json +4 -0
  20. package/examples/home/agents/main/workspace/AGENTS.md +14 -0
  21. package/examples/home/agents/main/workspace/MEMORY.md +4 -0
  22. package/examples/home/agents/main/workspace/SOUL.md +4 -0
  23. package/examples/home/models.json +62 -0
  24. package/examples/home/pine.json +30 -0
  25. package/examples/home/skills/README.txt +7 -0
  26. package/examples/launchd/com.pine.daemon.plist +32 -0
  27. package/mcp/browser.d.ts +34 -0
  28. package/mcp/browser.js +228 -0
  29. package/mcp/commands.d.ts +20 -0
  30. package/mcp/commands.js +88 -0
  31. package/mcp/lib.d.ts +13 -0
  32. package/mcp/lib.js +50 -0
  33. package/mcp/web.d.ts +6 -0
  34. package/mcp/web.js +140 -0
  35. package/package.json +32 -4
  36. package/src/agent-admin.d.ts +24 -0
  37. package/src/agent-admin.js +55 -0
  38. package/src/agents.d.ts +343 -0
  39. package/src/agents.js +1587 -0
  40. package/src/cli.d.ts +2 -0
  41. package/src/cli.js +526 -0
  42. package/src/config.d.ts +351 -0
  43. package/src/config.js +514 -0
  44. package/src/dev/claude-model.d.ts +28 -0
  45. package/src/dev/claude-model.js +174 -0
  46. package/src/env-file.d.ts +4 -0
  47. package/src/env-file.js +40 -0
  48. package/src/http.d.ts +46 -0
  49. package/src/http.js +407 -0
  50. package/src/limiter.d.ts +32 -0
  51. package/src/limiter.js +84 -0
  52. package/src/login.d.ts +21 -0
  53. package/src/login.js +126 -0
  54. package/src/manual.d.ts +61 -0
  55. package/src/manual.js +425 -0
  56. package/src/mcp-config.d.ts +40 -0
  57. package/src/mcp-config.js +132 -0
  58. package/src/net.d.ts +36 -0
  59. package/src/net.js +122 -0
  60. package/src/pine.d.ts +32 -0
  61. package/src/pine.js +142 -0
  62. package/src/router.d.ts +56 -0
  63. package/src/router.js +164 -0
  64. package/src/sandbox-fs.d.ts +27 -0
  65. package/src/sandbox-fs.js +172 -0
  66. package/src/security.d.ts +121 -0
  67. package/src/security.js +363 -0
  68. package/src/skills.d.ts +67 -0
  69. package/src/skills.js +163 -0
  70. package/src/source-types.d.ts +61 -0
  71. package/src/source-types.js +1 -0
  72. package/src/sources/command.d.ts +4 -0
  73. package/src/sources/command.js +77 -0
  74. package/src/sources/folder.d.ts +6 -0
  75. package/src/sources/folder.js +116 -0
  76. package/src/sources/ftp.d.ts +4 -0
  77. package/src/sources/ftp.js +307 -0
  78. package/src/sources/http.d.ts +4 -0
  79. package/src/sources/http.js +201 -0
  80. package/src/sources/mqtt.d.ts +18 -0
  81. package/src/sources/mqtt.js +97 -0
  82. package/src/sources/telegram.d.ts +22 -0
  83. package/src/sources/telegram.js +269 -0
  84. package/src/sources/webhook.d.ts +4 -0
  85. package/src/sources/webhook.js +52 -0
  86. package/src/sources.d.ts +339 -0
  87. package/src/sources.js +955 -0
  88. package/src/store.d.ts +89 -0
  89. package/src/store.js +251 -0
  90. package/src/timers.d.ts +83 -0
  91. package/src/timers.js +318 -0
  92. package/src/tools.d.ts +6 -0
  93. package/src/tools.js +777 -0
  94. package/src/tui/app.d.ts +209 -0
  95. package/src/tui/app.js +2223 -0
  96. package/src/tui/client.d.ts +178 -0
  97. package/src/tui/client.js +116 -0
  98. package/src/tui/code-pane.d.ts +51 -0
  99. package/src/tui/code-pane.js +398 -0
  100. package/src/tui/highlight.d.ts +8 -0
  101. package/src/tui/highlight.js +86 -0
  102. package/src/tui/picture.d.ts +141 -0
  103. package/src/tui/picture.js +571 -0
  104. package/src/tui/run.d.ts +8 -0
  105. package/src/tui/run.js +97 -0
  106. package/src/tui/theme.d.ts +32 -0
  107. package/src/tui/theme.js +128 -0
  108. package/src/types.d.ts +124 -0
  109. package/src/types.js +1 -0
  110. package/src/update.d.ts +25 -0
  111. package/src/update.js +110 -0
  112. package/src/util.d.ts +32 -0
  113. package/src/util.js +116 -0
  114. package/src/watching.d.ts +20 -0
  115. package/src/watching.js +68 -0
  116. package/src/workspace-view.d.ts +38 -0
  117. package/src/workspace-view.js +160 -0
@@ -0,0 +1,229 @@
1
+ # Configuration
2
+
3
+ Everything pine needs is in its home directory: `~/.pine` by default, or `$PINE_HOME`, or
4
+ `--home <dir>` on the command line.
5
+
6
+ ```
7
+ ~/.pine/
8
+ pine.json global settings, routes, schedules, global MCP servers and event sources
9
+ models.json optional: your own model servers, in pi's format
10
+ .env secrets, referenced from the JSON files as ${NAME}
11
+ auth.json provider credentials, written by `pine login`
12
+ USER.md what all agents know about you; in every agent's prompt, kept by the main agent
13
+ agents/
14
+ main/
15
+ agent.json this agent's settings
16
+ workspace/ its working folder, unless agent.json points elsewhere
17
+ AGENTS.md its purpose and how it works ┐ read into its prompt,
18
+ SOUL.md who it is │ written by the agent itself
19
+ MEMORY.md what should outlive a talk ┘ (agents.md)
20
+ AGENTS.md optional: rules of yours the agent cannot change
21
+ skills/ skills only this agent has
22
+ conversations/ what was set up for one conversation alone (its skills); made when needed
23
+ extensions/ pi extensions only this agent loads
24
+ sessions/ conversation history (written by pine)
25
+ skills/ skills for all agents; no entry needed (the main agent adds them with add_skill)
26
+ sources/ source types the main agent registered (pine's own copies)
27
+ sandbox/ per-agent private home and generated sandbox profiles (written by pine)
28
+ data/ event log, task queue, media, control token, files of event sources (written by pine)
29
+ ```
30
+
31
+ `pine check` validates names, references, paths, models and the entries of sources and
32
+ schedules, prints what each agent ends up with, and lists anything that
33
+ weakens the defences. Problems are reported together, each with the file it is in.
34
+
35
+ Paths in `pine.json` are relative to the home directory; paths in `agent.json` are relative to
36
+ that agent's folder. `~` works in both.
37
+
38
+ ## Secrets
39
+
40
+ Write `${NAME}` or `${NAME:-fallback}` anywhere in a string in `pine.json` or `agent.json`.
41
+ Values come from `<home>/.env` first, then from the environment of the daemon. A missing
42
+ variable without a fallback is a configuration error.
43
+
44
+ `.env` is read by pine only. Its values are not exported to the daemon's environment, so
45
+ agent shells and MCP servers do not inherit them; they are also removed from events, messages
46
+ and log lines if they ever appear there (values of 8 characters or more). Keep the file at
47
+ mode 600 (`pine init` sets that).
48
+
49
+ pine writes to `.env` itself in one case: when an agent sets up an event source that has a
50
+ password or secret, the value is stored there as `PINE_SOURCE_<NAME>_<AGENT>_<OPTION>` (without
51
+ the agent for a source of all agents), and the configuration holds only the `${...}` reference.
52
+ Removing the source removes the entry. See [sources.md](sources.md).
53
+
54
+ ## pine.json
55
+
56
+ ```json
57
+ {
58
+ "main": "main",
59
+ "logLevel": "info",
60
+ "defaults": { "model": "local/deepseek-v4.1-flash", "thinking": "off" },
61
+ "limits": { "maxConcurrentRuns": 2 },
62
+ "security": { "approvalTimeout": "5m" },
63
+ "http": { "host": "127.0.0.1", "port": 7463, "hooks": {} },
64
+ "mcp": {},
65
+ "sources": {},
66
+ "sourceTypes": {},
67
+ "subscribe": [],
68
+ "extensions": [],
69
+ "skills": [],
70
+ "routes": [],
71
+ "schedules": []
72
+ }
73
+ ```
74
+
75
+ | Key | Default | Meaning |
76
+ |---|---|---|
77
+ | `main` | none | Id of the main agent. It may delegate to every other agent unless its `agent.json` says otherwise, and it is the only one that can create agents and set things up for all agents ([setup.md](setup.md)). Without this key, the agent called `main` (or else the first one) has those setup rights, but nobody delegates by default. |
78
+ | `logLevel` | `info` | `debug`, `info`, `warn`, `error`, `silent`. Logs go to stderr; a daemon started inside the terminal UI logs to `data/pine.log`. |
79
+ | `dataDir` | `data` | Where the database, media and control token live. |
80
+ | `defaults.model` | none | `provider/model-id` from `models.json`: the model of every agent whose `agent.json` names none. There is no built-in fallback: an agent with neither is a configuration error, which `pine check` and the start of pine report. |
81
+ | `defaults.thinking` | `off` | Reasoning level for models that can reason: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. |
82
+ | `defaults.mode` | `auto` | `manual`, `auto` or `bypass`: how much agents do without asking. See [security.md](security.md). |
83
+ | `defaults.bashTimeoutSec` | `300` | Longest a shell command may run. The model cannot raise it. |
84
+ | `defaults.taskTimeoutSec` | `1800` | Longest a task may run before it is aborted. |
85
+ | `defaults.idleMs` | `15m` | A live session without work is closed after this (and resumed from disk later). |
86
+ | `defaults.settings` | `{}` | pi settings applied to every agent, for example `{ "compaction": { "reserveTokens": 8000 } }`. |
87
+ | `limits.maxConcurrentRuns` | `2` | Model slots: how many runs talk to the model server at once. Match it to the server's parallel slots. |
88
+ | `limits.maxDelegationDepth` | `3` | How deep a chain of delegations may go. |
89
+ | `limits.maxDownloadMb` | `200` | Largest file the `download` tool fetches. |
90
+ | `limits.maxEventHops` | `8` | Events further than this from an outside trigger are not routed. |
91
+ | `limits.maxQueuedPerAgent` | `50` | New tasks are refused when an agent has this many waiting. |
92
+ | `limits.maxEventChars` | `20000` | Longest text of an untrusted event in a prompt (at least 1000). Your own messages and delegated tasks are cut at ten times this. |
93
+ | `limits.modelHeadersTimeoutMs` | `10m` | How long the model server may take to start answering. |
94
+ | `limits.modelStreamIdleMs` | `5m` | How long a started answer may go silent before the request is dropped. |
95
+ | `limits.eventRetentionDays` | `30` | Older events are deleted, and so are the copies kept of conversations that were reset. |
96
+ | `limits.conversationDays` | `7` | A conversation in which nothing was said for this long is removed with its history, unless it is an agent's main conversation, pinned, open in a tab, subscribed to an event source, or has a schedule of its own. `0`: never. |
97
+ | `security.approvalTimeout` | `5m` | An unanswered approval request counts as "no" after this. |
98
+ | `security.taint` | `session` | How long a session stays tainted: until reset (`session`), until the task ends (`run`), or never (`off`). See [security.md](security.md). |
99
+ | `security.taintTools` | `[]` | Tool name globs whose results count as outside content, besides MCP servers marked `taints`. |
100
+ | `security.bypassWhenTainted` | `false` | Keep bypass mode in force in a session that has read outside content. Off, such a session asks again. |
101
+ | `http` | on, loopback, port 7463 | Control API and webhooks. `false` turns it off (the CLI's client commands then do not work). |
102
+ | `http.host`, `http.port` | `127.0.0.1`, `7463` | Where the control API listens. Anything but loopback is reported by `pine check`. |
103
+ | `http.token` | generated | Bearer token of the control API. When unset, pine generates one into `data/http-token`. |
104
+ | `http.hooks.<name>` | none | A webhook at `POST /hook/<name>`: `{ "secret": "...", "topic": "hook/<name>" }`, or `"open": true` instead of a secret to accept anyone who can reach the port. See [events.md](events.md). For a webhook an agent subscribes to, use a `webhook` source instead. |
105
+ | `mcp` | `{}` | Global MCP servers. See [mcp.md](mcp.md). |
106
+ | `sources` | `{}` | Event sources every agent can subscribe to. See [sources.md](sources.md). |
107
+ | `sourceTypes` | `{}` | Source types of your own: a name and the module that implements it, e.g. `{ "doorbell": "./sources/doorbell.ts" }`. The module says which options it takes, and agents can then set up sources of that type by tool call. The main agent adds entries here with `add_source_type`, pointing at the copy pine keeps in `sources/` of its home. |
108
+ | `subscribe` | `[]` | Subscriptions for all agents: every agent that can use the source is woken in its main conversation. |
109
+ | `extensions` | `[]` | Paths of pi extensions loaded into every agent's sessions. |
110
+ | `skills` | `[]` | Further folders of skills for every agent, for skills that lie outside pine's home. Not needed for `skills/` in pine's home: what is there is for every agent without an entry, and so is what the main agent adds with `add_skill`. Agents cannot replace or remove a skill that is named here. |
111
+ | `routes` | `[]` | See [events.md](events.md). |
112
+ | `schedules` | `[]` | What happens at a time. One with a `task` wakes every agent with it; one without only publishes an event. The main agent adds entries with `add_schedule`. See [events.md](events.md). |
113
+
114
+ Two things that older setups had are configuration errors now, each reported with what to
115
+ write instead: a `plugins` list that is not empty, and an event source of `"type": "timer"`
116
+ (that is a schedule).
117
+
118
+ Durations are a number of milliseconds or a string such as `"90s"`, `"15m"`, `"2h"`, `"1d"`.
119
+
120
+ ## agent.json
121
+
122
+ Every key is optional; `{}` is a valid agent.
123
+
124
+ ```json
125
+ {
126
+ "description": "Contract law; knows the case files",
127
+ "workspace": "~/dev/gonzo",
128
+ "model": "local/deepseek-v4.1-flash",
129
+ "tools": ["read", "bash", "edit", "write", "grep", "find", "ls"],
130
+ "mcp": { "library": { "command": "library-mcp" } },
131
+ "globalMcp": ["web"],
132
+ "delegates": [],
133
+ "subagents": { "maxParallel": 3 },
134
+ "sandbox": { "network": false, "read": ["~/Documents/law-library"] }
135
+ }
136
+ ```
137
+
138
+ ### Identity and model
139
+
140
+ | Key | Default | Meaning |
141
+ |---|---|---|
142
+ | `description` | empty | One line other agents see when they choose whom to delegate to. Write it for them. |
143
+ | `workspace` | `agents/<id>/workspace` | The agent's working folder. `AGENTS.md`, `SOUL.md` and `MEMORY.md` at its top are the agent's instructions and are loaded into its prompt. Must exist when you set it. |
144
+ | `context` | `[]` | Further instruction files to load into the system prompt. |
145
+ | `model`, `thinking` | from `defaults` | Per-agent model and thinking level. |
146
+ | `settings` | from `defaults.settings` | pi settings for this agent. `agents/<id>/settings.json` is honoured as well. |
147
+ | `taskTimeoutSec`, `idleMs` | from `defaults` | Per-agent overrides. |
148
+
149
+ ### Tools and extensions
150
+
151
+ | Key | Default | Meaning |
152
+ |---|---|---|
153
+ | `tools` | `read`, `bash`, `edit`, `write` | Which of pi's built-in tools the agent gets: any of `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`. |
154
+ | `mcp` | `{}` | MCP servers only this agent has. An entry with the name of a global server replaces it for this agent. An entry with `"session": "<conversation>"` was added for that conversation alone and is removed with it ([setup.md](setup.md)). |
155
+ | `globalMcp` | `true` | `true`: every global server that includes this agent. `false`: none. A list of names: exactly those. |
156
+ | `sources` | `{}` | Event sources only this agent can subscribe to. An entry with `"session": "<conversation>"` was set up for that conversation alone and is removed with it. See [sources.md](sources.md). |
157
+ | `subscribe` | `[]` | What this agent is subscribed to: a list of names, an object with `note`, `when`, `throttle`, `debounce` per source, or a list of entries with `source` and, for one conversation only, `session`. The agent's `subscribe` tool and `pine subscribe` write here. |
158
+ | `schedules` | `[]` | What this agent does at set times: `{ "name", "cron" or "every" or "at", "task" }`, with `"session": "<conversation>"` for one conversation. The agent adds entries with `add_schedule`. See [events.md](events.md). |
159
+ | `extensions`, `skills` | `[]` | Further pi extension files and skill folders for this agent. Its own `skills/` folder needs no entry. |
160
+ | `bashTimeoutSec` | from `defaults` | Longest shell command. |
161
+ | `bashDeny` | `[]` | Regular expressions; a shell command matching one is refused. A convenience, not a boundary: the sandbox is the boundary. |
162
+
163
+ ### Working with other agents
164
+
165
+ | Key | Default | Meaning |
166
+ |---|---|---|
167
+ | `delegates` | `["*"]` for the agent that `main` in `pine.json` names, else `[]` | Agents this one may hand tasks to. |
168
+ | `subagents` | on, 4 in parallel | `false`, or `{ "maxParallel": 4, "tools": [...], "mcp": true }`. `tools` narrows what sub-agents get. |
169
+ | `publish` | `[]` | Topic patterns the `publish_event` tool may publish on. Empty: the agent has no such tool. |
170
+ | `state.read` | `["#"]` | State keys the agent may read. |
171
+ | `state.write` | `["agent/<id>/#"]` | State keys the agent may write. |
172
+ | `state.inject` | `[]` | State keys shown to the agent in every turn. |
173
+
174
+ ### Containment
175
+
176
+ | Key | Default | Meaning |
177
+ |---|---|---|
178
+ | `mode` | from `defaults` | `manual`: every action waits for approval. `auto`: works inside the sandbox and asks for the rest. `bypass`: never asks, and reaches your whole account. |
179
+ | `sandbox` | on | `false` runs shell and file tools with the daemon's full rights. Do not, unless the machine itself is disposable. |
180
+ | `sandbox.network` | `false` | Shell commands may use the network. |
181
+ | `sandbox.read`, `sandbox.write` | `[]` | Further folders the agent may read / write, besides its workspace. |
182
+ | `sandbox.sharedTmp` | `true` | Allow `/tmp` and the user's temp folder. |
183
+ | `sandbox.env` | `{}` | Extra environment variables for the agent's shell. Do not put secrets here; the agent can read them. |
184
+ | `trustWorkspace` | `false` | `true` lets pi load `.pi/extensions`, `.pi/settings.json` and skills from the workspace. That is code execution inside the daemon for whoever can write there, the agent included. |
185
+ | `approve` | `[]` | Tool calls that always need your approval: `"tool"` or `"tool:argument-glob"`, for example `"bash:*git push*"`. |
186
+ | `approveWhenTainted` | `["publish_event", "send"]` | Tool calls that need approval once the session has seen outside content. |
187
+ | `untrusted` | read-only profile | What a run started by an untrusted event may use: `{ "tools": ["read","grep","find","ls"], "mcp": [], "delegates": [], "publish": [], "stateWrite": false, "subagents": false }`. `false` refuses such runs. |
188
+
189
+ ## models.json
190
+
191
+ This is pi's own file; pine passes it through. It is only needed for model servers of your own:
192
+ pi already knows the hosted providers (OpenCode, Anthropic, OpenAI and many more), and
193
+ `pine login <provider>` supplies their credentials. `pine init` writes two providers: `dev`
194
+ (Claude through `claude -p`, served by `pine dev-model`; the example's `defaults.model` is
195
+ `dev/claude-haiku`) and `local` for llama.cpp. The `local` entry:
196
+
197
+ ```json
198
+ {
199
+ "providers": {
200
+ "local": {
201
+ "name": "llama-server",
202
+ "baseUrl": "http://127.0.0.1:8080/v1",
203
+ "api": "openai-completions",
204
+ "apiKey": "none",
205
+ "models": [
206
+ {
207
+ "id": "deepseek-v4.1-flash",
208
+ "contextWindow": 65536,
209
+ "maxTokens": 8192,
210
+ "input": ["text"],
211
+ "reasoning": true,
212
+ "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
213
+ "compat": { "supportsStore": false, "supportsDeveloperRole": false, "maxTokensField": "max_tokens", "thinkingFormat": "deepseek" }
214
+ }
215
+ ]
216
+ }
217
+ }
218
+ }
219
+ ```
220
+
221
+ - Refer to a model as `provider/id`, here `local/deepseek-v4.1-flash`.
222
+ - `contextWindow` must match what the server was started with (`-c`), or compaction triggers at
223
+ the wrong time.
224
+ - Add `"image"` to `input` only for a model that accepts images; agents then receive the
225
+ pictures that Telegram and watched folders deliver.
226
+ - The `compat` flags stop pi from sending fields a plain llama-server may reject. They have not
227
+ been verified against a real llama-server yet; see [stack.md](stack.md).
228
+ - pi's documentation for this file is in
229
+ `node_modules/@earendil-works/pi-coding-agent/docs/models-configuration.md`.
@@ -0,0 +1,263 @@
1
+ # Events, routes and schedules
2
+
3
+ > To let an agent react to a folder, a device or a web request, you usually do not need this
4
+ > page: ask it, or configure a [source](sources.md) and have the agent subscribe. To have it do
5
+ > something at a time, ask it too: see "Schedules" below. Routes are for rules of your own:
6
+ > setting state, chaining events, single model calls.
7
+
8
+ ## Topics
9
+
10
+ A topic is a slash-separated name. Patterns use `+` for exactly one level and `#` for the rest:
11
+
12
+ | Pattern | Matches |
13
+ |---|---|
14
+ | `cam/front/motion` | only that topic |
15
+ | `cam/+/motion` | `cam/front/motion`, `cam/back/motion` |
16
+ | `source/#` | everything below `source`, and `source` itself |
17
+ | `#` | everything |
18
+
19
+ Where events come from:
20
+
21
+ | Source | Topics | Trust |
22
+ |---|---|---|
23
+ | Webhook `POST /hook/<name>` | `hook/<name>` (or the hook's `topic`) | untrusted |
24
+ | Event sources ([sources.md](sources.md)) | `source/<name>/<event>`, or `source/<name>@<agent>/<event>` for an agent's own | untrusted; owner for a source whose entry says `"trust": "owner"` (`add_source` writes that for `telegram`, whose account you pair) |
25
+ | `pine emit`, `POST /events` | whatever you name | owner |
26
+ | Schedules | `timer/<name>`, or `timer/<name>@<agent>` for an agent's own | owner |
27
+ | Agents (`publish_event`) | topics in the agent's `publish` list | owner, or untrusted if the run was restricted |
28
+ | pine | `agent/...`, `state/...`, `approval/...`, `pine/...` | untrusted |
29
+
30
+ Watch them:
31
+
32
+ ```bash
33
+ pine events -f
34
+ ```
35
+
36
+ ```bash
37
+ pine events --topic "source/#" --limit 50
38
+ ```
39
+
40
+ ## Routes
41
+
42
+ A route is an entry in `routes` in `pine.json`.
43
+
44
+ ```json
45
+ {
46
+ "name": "front-door",
47
+ "on": "source/house/message",
48
+ "when": { "payload.topic": "house/door/front", "payload.value.open": true, "state.house/mode": "away" },
49
+ "throttleMs": "5m",
50
+ "agent": "house",
51
+ "prompt": "The front door was opened at {{time}} while the house is in away mode.",
52
+ "session": "new",
53
+ "reply": { "topic": "out/source/telegram" }
54
+ }
55
+ ```
56
+
57
+ Here `house` is an `mqtt` source and `telegram` a `telegram` source, both in `pine.json`
58
+ ([sources.md](sources.md)). For a source an agent set up for itself the topic carries the
59
+ agent's name: `out/source/telegram@main`.
60
+ A route does not subscribe to anything, so a source it relies on needs `"start": "always"`
61
+ unless a conversation is subscribed to it anyway.
62
+
63
+ | Key | Meaning |
64
+ |---|---|
65
+ | `name` | For logs and errors. Default `route<n>`. |
66
+ | `on` | A topic pattern or a list of them. |
67
+ | `when` | A filter; every entry must hold. See below. |
68
+ | `debounceMs` | Wait until no matching event has arrived for this long, then handle the last one. For bursts (a camera uploading five images). |
69
+ | `throttleMs` | Handle one event, then ignore matching ones for this long. For repeating alarms. |
70
+ | `key` | Template for what counts as "the same" for debounce and throttle. Default `{{topic}}`. |
71
+ | `set` | Write shared state: `{ "house/mode": "{{payload.value}}" }`. `null`, or a value that is exactly one `{{path}}` that finds nothing, deletes the key. |
72
+ | `publish` | Publish another event: `{ "topic": "...", "payload": {...} }`. Without `payload` the original payload is passed on; after a `call` it is `{ "text": <the answer>, "of": <the original payload> }`. |
73
+ | `call` | One model call: `{ "prompt": "...", "system": "...", "model": "provider/id" }`. Its answer is `{{result}}`. |
74
+ | `agent` | Give an agent a task. |
75
+ | `prompt` | The task text. Default: the payload's `text`, or the payload as JSON. |
76
+ | `session` | Session key template. Default `main`. `new` for one-off work. |
77
+ | `whenBusy` | `queue` (default) or `steer`. |
78
+ | `priority` | Higher runs first when model slots are scarce. Default 10 for owner events, 0 otherwise. |
79
+ | `reply` | Where the agent's answer goes: `{ "topic": "...", "payload": {...} }`. Default: the event's own reply target. `false`: nowhere. |
80
+ | `trust` | Override the event's trust. `"owner"` gives events on this route the agent's full tool set: only for sources nobody else can write to. `pine check` lists such routes. |
81
+
82
+ A route needs at least one of `set`, `publish`, `call`, `agent`. They run in the order `set`,
83
+ `call`, `publish`, `agent`, so one route can classify with `call` and pass the result on with
84
+ `publish`. `call` and `agent` cannot be in the same route: publish the result and let a second
85
+ route start the agent, as in the example below.
86
+
87
+ ### Templates
88
+
89
+ `{{path}}` is replaced in `prompt`, `session`, `key`, `set`, `publish`, `reply` and `call`.
90
+
91
+ | Path | Value |
92
+ |---|---|
93
+ | `payload`, `payload.a.b` | The event's payload or a field of it |
94
+ | `topic`, `source`, `id`, `ts`, `time` | Event fields; `time` is local, `YYYY-MM-DD HH:MM:SS` |
95
+ | `state.<key>` | Shared state, for example `{{state.house/mode}}` |
96
+ | `result` | The answer of the route's `call` |
97
+
98
+ A value that is exactly one `{{path}}` keeps its type (a number stays a number); inside longer
99
+ text, objects become JSON and missing values become empty.
100
+
101
+ ### Filters
102
+
103
+ `when` maps a path to a value (equality) or to conditions:
104
+
105
+ ```json
106
+ {
107
+ "payload.value.temperature": { "gt": 30 },
108
+ "payload.value.room": { "in": ["kitchen", "boiler"] },
109
+ "payload.text": { "regex": "^alarm", "flags": "i" },
110
+ "state.house/mode": { "ne": "home" },
111
+ "payload.value.battery": { "exists": true }
112
+ }
113
+ ```
114
+
115
+ Conditions: `eq`, `ne`, `in`, `gt`, `gte`, `lt`, `lte`, `exists`, `contains`, `regex` (with
116
+ optional `flags`).
117
+
118
+ ### The cheapest handler that is enough
119
+
120
+ A full agent turn costs seconds of model time. Most events do not need one.
121
+
122
+ ```json
123
+ [
124
+ { "on": "source/house/message", "when": { "payload.topic": "house/mode" }, "set": { "house/mode": "{{payload.value}}" } },
125
+
126
+ { "on": "source/cam/added",
127
+ "debounceMs": "4s",
128
+ "call": { "system": "You look at one camera image. Answer in one line: PERSON, VEHICLE, ANIMAL or NOTHING, then a short description.",
129
+ "prompt": "Camera file {{payload.file}}" },
130
+ "publish": { "topic": "cam/classified", "payload": { "file": "{{payload.file}}", "verdict": "{{result}}" } } },
131
+
132
+ { "on": "cam/classified",
133
+ "when": { "payload.verdict": { "regex": "^(PERSON|VEHICLE)" }, "state.house/mode": "away" },
134
+ "agent": "house", "session": "new",
135
+ "prompt": "Camera saw: {{payload.verdict}} ({{payload.file}}). The house is in away mode.",
136
+ "reply": { "topic": "out/source/telegram" } }
137
+ ]
138
+ ```
139
+
140
+ The first route never calls the model. The second makes one call per burst of images (a `call`
141
+ receives the event's images if the model accepts them). Only a person or a vehicle while nobody
142
+ is home reaches an agent.
143
+
144
+ Text from untrusted events is prefixed for the agent with a line saying where it came from and
145
+ that it is data, and is cut at `limits.maxEventChars`. That line is a courtesy; the protection
146
+ is that the run has fewer tools (see [security.md](security.md)).
147
+
148
+ ## Schedules
149
+
150
+ A schedule is something that happens because of the time: again and again, or once. It is the
151
+ fourth thing an agent can be given by asking, next to event sources, MCP servers and skills,
152
+ and it has the same three scopes ([setup.md](setup.md)).
153
+
154
+ ### Ask the agent
155
+
156
+ > Every weekday at seven, check my mail and tell me on Telegram what matters.
157
+
158
+ > Remind me at five to call the dentist.
159
+
160
+ > In twenty minutes, look whether the build is through.
161
+
162
+ The agent calls its `add_schedule` tool with a name, the time and the task in its own words:
163
+ `cron` for set times ("every weekday at six in the evening" is `"0 18 * * 1-5"`), `every` for an
164
+ interval, `at` or `in` for a reminder that happens once. When it is time, it is woken with that
165
+ task, also when you are not there, and does it. What it starts
166
+ counts as your wish: the agent has its usual tools, and nothing is marked as outside content.
167
+
168
+ | Said | Who is woken | You are asked |
169
+ |---|---|---|
170
+ | in a side conversation | That conversation; the schedule goes when the conversation is removed | No |
171
+ | in the agent's main conversation, or "as an agent" | The agent's main conversation | Only from a side conversation |
172
+ | "all agents" | Every agent, each in its main conversation. Only the main agent can do this | Yes |
173
+
174
+ In a conversation that has read outside content you are asked in every case, and the dialog
175
+ shows the task: what a web page says must not become something that is done every hour.
176
+
177
+ | Tool | |
178
+ |---|---|
179
+ | `schedules` | What is scheduled for this agent, in which conversation, and when each is next due. |
180
+ | `add_schedule` `name` `task` + one of `cron`, `every`, `at`, `in` [`scope`] | Schedules it. `cron`: set times; `every`: an interval, at least a minute; `at`: once, at a local time (`2026-12-24 17:00`); `in`: once, after this long (`20m`). |
181
+ | `remove_schedule` `name` | Ends one. This conversation's own goes without a question. |
182
+
183
+ A schedule that happens once removes itself when it has fired. A one-off, or a `cron` schedule
184
+ with a task, that was due while pine was not running is done once when pine starts again, and
185
+ the agent is told that it comes late. An `every` schedule simply starts counting again. `pine
186
+ schedules` lists everything that is scheduled.
187
+
188
+ A name is unique among all schedules; a task has at most 4000 characters; an interval an agent
189
+ sets is at least a minute; a time in the past is refused. In manual mode you are asked for
190
+ every `add_schedule` and `remove_schedule`.
191
+
192
+ ### In the files
193
+
194
+ ```json
195
+ "schedules": [
196
+ { "name": "brief", "cron": "30 7 * * 1-5", "task": "Prepare the morning brief and send it to me on Telegram." },
197
+ { "name": "dentist", "at": "2026-11-12T17:00", "task": "Remind me to call the dentist." },
198
+ { "name": "printer-check", "every": "10m" }
199
+ ]
200
+ ```
201
+
202
+ In an `agent.json` this is that agent's schedule (add `"session": "<conversation>"` for one
203
+ conversation, otherwise its main one). In `pine.json` a schedule with a task wakes every
204
+ agent; one without a task only publishes an event.
205
+
206
+ | Key | Meaning |
207
+ |---|---|
208
+ | `name` | Required. Starts with a lowercase letter, then lowercase letters, digits, `-` and `_`; 40 characters at most. The event is published on `timer/<name>` (`timer/<name>@<agent>` for an agent's own) unless `topic` says otherwise. |
209
+ | `cron` | Five fields, local time: minute, hour, day of month, month, day of week (0 or 7 is Sunday). Lists, ranges and steps work: `*/15 8-18 * * 1-5`. |
210
+ | `every` | A fixed interval instead. |
211
+ | `at` | Once, at this local time. The entry is removed when it has fired. If pine was not running at that time, it fires when pine starts again, and the agent is told that it comes late. |
212
+ | `task` | What the agent is to do. Required in an `agent.json`. |
213
+ | `session` | In an `agent.json`: the conversation that is woken. Default: the main one. |
214
+ | `payload` | Fields added to the event's payload (`name` and `scheduled` are always there; `late` when it comes late, `task` when it has one). |
215
+ | `catchUp` | For `cron`: fire once at startup if a time passed while the daemon was down, marked as late. Default: `true` with a task, `false` without. (`at` always catches up; `every` never does.) |
216
+
217
+ ### Schedules and routes
218
+
219
+ Every schedule also publishes an event, trusted as yours, so a route can act on it. For a
220
+ schedule without a task that is all it does:
221
+
222
+ ```json
223
+ { "on": "timer/printer-check", "call": { "prompt": "..." } }
224
+ ```
225
+
226
+ A route is the way to do something other than an agent's turn at a time: set state, publish,
227
+ or one model call without tools. For "wake the agent and let it do this", give the schedule a
228
+ task instead; no route is needed.
229
+
230
+ ## Webhooks
231
+
232
+ ```json
233
+ "http": { "hooks": { "doorbell": { "secret": "${DOORBELL_SECRET}" } } }
234
+ ```
235
+
236
+ `POST http://127.0.0.1:7463/hook/doorbell` then publishes `hook/doorbell` with the request body
237
+ as payload (parsed JSON, or `{ "text": ... }`). The caller proves itself with either
238
+
239
+ - `Authorization: Bearer <secret>`, or
240
+ - an HMAC-SHA256 of the body with the secret, hex-encoded, in `X-Hub-Signature-256: sha256=<hex>`
241
+ (the GitHub convention) or `X-Signature`.
242
+
243
+ A hook needs a secret; `"open": true` instead of a secret accepts anyone who can reach the port
244
+ and is reported by `pine check`. Unknown hook names get 404. Each hook accepts at most 120
245
+ requests a minute and bodies up to 1 MB. Webhook events are untrusted even when the signature
246
+ is right: the signature says who sent it, not that its content is safe to obey.
247
+
248
+ To receive webhooks from outside your network, put a tunnel or reverse proxy in front and
249
+ forward only `/hook/`.
250
+
251
+ A hook here only publishes an event; a route decides what happens. For a webhook that wakes an
252
+ agent directly, use a source of type `webhook` ([sources.md](sources.md)): it answers under
253
+ `/hook/` too, while an agent is subscribed. A hook in `http.hooks` with the same name comes
254
+ first.
255
+
256
+ ## When nothing happens
257
+
258
+ 1. `pine events --topic "<pattern>"`: did the event arrive, and on which topic?
259
+ 2. `pine tasks`: was a task created? `pine task <id>` shows its prompt, result and error.
260
+ 3. `pine events --topic "pine/#"`: `pine/route-failed` carries the reason a route could not act,
261
+ for example an agent that refuses untrusted events or a full queue.
262
+ 4. Daemon log (stderr): a line `dropped ... after N hops` means a loop was cut.
263
+ 5. A filter that never matches is usually a wrong path: look at the real payload in step 1.