@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.
- package/LICENSE +21 -0
- package/README.md +180 -2
- package/bin/pine.js +27 -0
- package/docs/SKILL.md +189 -0
- package/docs/references/agents.md +368 -0
- package/docs/references/coding.md +126 -0
- package/docs/references/configuration.md +229 -0
- package/docs/references/events.md +263 -0
- package/docs/references/mcp.md +257 -0
- package/docs/references/operations.md +280 -0
- package/docs/references/overview.md +186 -0
- package/docs/references/security.md +489 -0
- package/docs/references/setup.md +181 -0
- package/docs/references/sources.md +768 -0
- package/docs/references/stack.md +105 -0
- package/docs/references/tui.md +348 -0
- package/examples/home/.env.example +5 -0
- package/examples/home/USER.md +4 -0
- package/examples/home/agents/main/agent.json +4 -0
- package/examples/home/agents/main/workspace/AGENTS.md +14 -0
- package/examples/home/agents/main/workspace/MEMORY.md +4 -0
- package/examples/home/agents/main/workspace/SOUL.md +4 -0
- package/examples/home/models.json +62 -0
- package/examples/home/pine.json +30 -0
- package/examples/home/skills/README.txt +7 -0
- package/examples/launchd/com.pine.daemon.plist +32 -0
- package/mcp/browser.d.ts +34 -0
- package/mcp/browser.js +228 -0
- package/mcp/commands.d.ts +20 -0
- package/mcp/commands.js +88 -0
- package/mcp/lib.d.ts +13 -0
- package/mcp/lib.js +50 -0
- package/mcp/web.d.ts +6 -0
- package/mcp/web.js +140 -0
- package/package.json +32 -4
- package/src/agent-admin.d.ts +24 -0
- package/src/agent-admin.js +55 -0
- package/src/agents.d.ts +343 -0
- package/src/agents.js +1587 -0
- package/src/cli.d.ts +2 -0
- package/src/cli.js +526 -0
- package/src/config.d.ts +351 -0
- package/src/config.js +514 -0
- package/src/dev/claude-model.d.ts +28 -0
- package/src/dev/claude-model.js +174 -0
- package/src/env-file.d.ts +4 -0
- package/src/env-file.js +40 -0
- package/src/http.d.ts +46 -0
- package/src/http.js +407 -0
- package/src/limiter.d.ts +32 -0
- package/src/limiter.js +84 -0
- package/src/login.d.ts +21 -0
- package/src/login.js +126 -0
- package/src/manual.d.ts +61 -0
- package/src/manual.js +425 -0
- package/src/mcp-config.d.ts +40 -0
- package/src/mcp-config.js +132 -0
- package/src/net.d.ts +36 -0
- package/src/net.js +122 -0
- package/src/pine.d.ts +32 -0
- package/src/pine.js +142 -0
- package/src/router.d.ts +56 -0
- package/src/router.js +164 -0
- package/src/sandbox-fs.d.ts +27 -0
- package/src/sandbox-fs.js +172 -0
- package/src/security.d.ts +121 -0
- package/src/security.js +363 -0
- package/src/skills.d.ts +67 -0
- package/src/skills.js +163 -0
- package/src/source-types.d.ts +61 -0
- package/src/source-types.js +1 -0
- package/src/sources/command.d.ts +4 -0
- package/src/sources/command.js +77 -0
- package/src/sources/folder.d.ts +6 -0
- package/src/sources/folder.js +116 -0
- package/src/sources/ftp.d.ts +4 -0
- package/src/sources/ftp.js +307 -0
- package/src/sources/http.d.ts +4 -0
- package/src/sources/http.js +201 -0
- package/src/sources/mqtt.d.ts +18 -0
- package/src/sources/mqtt.js +97 -0
- package/src/sources/telegram.d.ts +22 -0
- package/src/sources/telegram.js +269 -0
- package/src/sources/webhook.d.ts +4 -0
- package/src/sources/webhook.js +52 -0
- package/src/sources.d.ts +339 -0
- package/src/sources.js +955 -0
- package/src/store.d.ts +89 -0
- package/src/store.js +251 -0
- package/src/timers.d.ts +83 -0
- package/src/timers.js +318 -0
- package/src/tools.d.ts +6 -0
- package/src/tools.js +777 -0
- package/src/tui/app.d.ts +209 -0
- package/src/tui/app.js +2223 -0
- package/src/tui/client.d.ts +178 -0
- package/src/tui/client.js +116 -0
- package/src/tui/code-pane.d.ts +51 -0
- package/src/tui/code-pane.js +398 -0
- package/src/tui/highlight.d.ts +8 -0
- package/src/tui/highlight.js +86 -0
- package/src/tui/picture.d.ts +141 -0
- package/src/tui/picture.js +571 -0
- package/src/tui/run.d.ts +8 -0
- package/src/tui/run.js +97 -0
- package/src/tui/theme.d.ts +32 -0
- package/src/tui/theme.js +128 -0
- package/src/types.d.ts +124 -0
- package/src/types.js +1 -0
- package/src/update.d.ts +25 -0
- package/src/update.js +110 -0
- package/src/util.d.ts +32 -0
- package/src/util.js +116 -0
- package/src/watching.d.ts +20 -0
- package/src/watching.js +68 -0
- package/src/workspace-view.d.ts +38 -0
- package/src/workspace-view.js +160 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# MCP servers
|
|
2
|
+
|
|
3
|
+
pi has MCP built in; pine decides which agent gets which server. Tools appear to the model as
|
|
4
|
+
`mcp__<server>__<tool>`. Characters such as `-` in a server's name become `_` there: the server
|
|
5
|
+
`home-link` offers `mcp__home_link__status`. Use the tool-name form in `approve`
|
|
6
|
+
patterns; the server's own `taints` and `approve` settings apply either way.
|
|
7
|
+
|
|
8
|
+
## Global and per agent
|
|
9
|
+
|
|
10
|
+
Global servers go in `pine.json`:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
"mcp": {
|
|
14
|
+
"web": { "pine": "web", "agents": ["scout"] },
|
|
15
|
+
"calendar": { "command": "npx", "args": ["-y", "some-calendar-mcp"], "env": { "TOKEN": "${CALENDAR_TOKEN}" } },
|
|
16
|
+
"docs": { "url": "https://mcp.example.com/mcp", "headers": { "Authorization": "Bearer ${DOCS_TOKEN}" }, "agents": ["main", "gonzo"] }
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Servers of one agent go in its `agent.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{ "mcp": { "library": { "command": "library-mcp", "args": ["--stdio"] } } }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Who gets what:
|
|
27
|
+
|
|
28
|
+
| Setting | Effect |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `agents` on a global server | Which agents get it. Default: all (`["*"]`). |
|
|
31
|
+
| `globalMcp: true` in `agent.json` (default) | The agent gets every global server that includes it. |
|
|
32
|
+
| `globalMcp: false` | No global servers. |
|
|
33
|
+
| `globalMcp: ["web"]` | Exactly these global servers, whether or not their `agents` list names this agent. |
|
|
34
|
+
| A server in `agent.json` with a global server's name | Replaces the global one for this agent (an entry with `"enabled": false` does not: the global server stays). |
|
|
35
|
+
| `"enabled": false` | Switches a server off without deleting it. |
|
|
36
|
+
|
|
37
|
+
`pine check` prints the resulting list per agent.
|
|
38
|
+
|
|
39
|
+
## Adding a server
|
|
40
|
+
|
|
41
|
+
Three ways, all ending in the same entry in `pine.json` (for everyone) or an `agent.json`:
|
|
42
|
+
|
|
43
|
+
**Ask an agent.** "Connect this MCP server: https://mcp.example.com/mcp". The agent calls
|
|
44
|
+
`add_mcp_server`; you get an approval dialog with the name, the URL and who gets it. Said just
|
|
45
|
+
like that, it is for the conversation you are in: only that conversation has the server's
|
|
46
|
+
tools, and the server is removed with the conversation (in an agent's main conversation it is
|
|
47
|
+
the agent's, in all its conversations). Say "for you as an agent" to keep it for the agent from
|
|
48
|
+
any conversation, and ask the **main agent** for "all agents": only it can add or remove a
|
|
49
|
+
server for everyone. `remove_mcp_server` takes a server out again. See [setup.md](setup.md).
|
|
50
|
+
If you allow it, the server is written to the configuration and its tools are there from your
|
|
51
|
+
next message, without a restart. This works for remote servers (an https URL) only: a local
|
|
52
|
+
server is a program that runs with your rights, and that stays something you add yourself. The
|
|
53
|
+
question comes in every mode, bypass included, because it changes pine itself.
|
|
54
|
+
|
|
55
|
+
Your yes makes it a server of yours: using it does not mark the conversation as having read
|
|
56
|
+
outside content. It is written with `"approve": "tainted"`, which only matters after the
|
|
57
|
+
conversation has read something from outside (a web page, say): from then on the server's tools
|
|
58
|
+
that act ask each time, until you answer "Always here" in that dialog, which settles it for the
|
|
59
|
+
conversation. Its
|
|
60
|
+
read-only tools never ask. If the server itself returns other people's text (mail, tickets, a
|
|
61
|
+
web search), add `"taints": true` to its entry; if you want no questions at all, remove
|
|
62
|
+
`approve`.
|
|
63
|
+
|
|
64
|
+
**From a shell.**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pine mcp add docs https://mcp.example.com/mcp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pine mcp add library --agent gonzo -- /usr/local/bin/library-mcp --stdio
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Options: `--agent <id>` (only that agent), `--taints`, `--approve always|tainted`,
|
|
75
|
+
`--timeout <seconds>`, `--description "..."`, `--exposure direct|codemode|deferred|hidden`.
|
|
76
|
+
`pine mcp list --offline` lists without connecting. A remote server needs an https URL (http only for
|
|
77
|
+
`localhost`). If a daemon is running the change applies at once; otherwise at the next start.
|
|
78
|
+
`pine mcp remove <name>` takes it out again (`--agent <id>` for one in an `agent.json`), and
|
|
79
|
+
`pine mcp list` shows every server, who gets it, and whether it connects:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
notes needs sign-in all: main
|
|
83
|
+
https://gateway.example/d/abc/mcp [outside content] [approval: tainted]
|
|
84
|
+
web connected all: main
|
|
85
|
+
bundled: web [outside content]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**By hand.** Edit the file as described below, run `pine check`, restart.
|
|
89
|
+
|
|
90
|
+
## Signing in (OAuth)
|
|
91
|
+
|
|
92
|
+
A remote server that answers "unauthorized" and supports OAuth needs you to sign in once:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pine mcp login notes
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
or `/mcp login notes` in the terminal UI. Your browser opens the server's sign-in page; when
|
|
99
|
+
you approve, pine stores the tokens in `data/pi/mcp-auth.json` (closed to agents) and refreshes
|
|
100
|
+
them on its own afterwards. Open conversations get the server's tools from their next message.
|
|
101
|
+
`pine mcp logout <name>` deletes the tokens.
|
|
102
|
+
|
|
103
|
+
The sign-in is pi's: it follows the server's OAuth metadata and registers itself as a client.
|
|
104
|
+
For servers that need a pre-registered client or a particular client name, add an `oauth`
|
|
105
|
+
block to the entry (`clientId`, `clientSecret`, `callbackPort`, `clientName`,
|
|
106
|
+
`clientRegistration`); pi's `docs/mcp.md` describes each. A server that takes a fixed token
|
|
107
|
+
instead needs no sign-in: put `"headers": { "Authorization": "Bearer ${TOKEN}" }` in the entry
|
|
108
|
+
and the token in `.env`.
|
|
109
|
+
|
|
110
|
+
## Server entries
|
|
111
|
+
|
|
112
|
+
An entry has the shape of an `mcpServers` entry in pi's `mcp.json`:
|
|
113
|
+
|
|
114
|
+
| Key | Meaning |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `command`, `args`, `env`, `cwd` | A local server started as a child process (stdio). |
|
|
117
|
+
| `url`, `headers` | A remote server (streamable HTTP). |
|
|
118
|
+
| `exposure` | How tools reach the model. pine's default is `direct`: every tool is declared. `codemode` offers one scripting tool instead, `deferred` makes tools searchable, `hidden` hides them. Use `deferred` or `codemode` for servers with dozens of tools. |
|
|
119
|
+
| `toolExposure` | Per-tool override, for example `{ "delete_*": "hidden" }`. |
|
|
120
|
+
| `description` | One sentence for the model about what the server offers. |
|
|
121
|
+
| `url` (remote servers) | Also decides where the `download` tool fetches from without asking: addresses on the same scheme, host and port. A server that hands out download links for its files on its own host works with it out of the box. |
|
|
122
|
+
| `timeout` | Seconds one call to this server may take before the agent is told it timed out. Default 60. Raise it for a server with slow tools, e.g. `"timeout": 180`. |
|
|
123
|
+
|
|
124
|
+
and the keys pine adds (besides `agents` and `enabled` from the table at the top):
|
|
125
|
+
|
|
126
|
+
| Key | Meaning |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `pine` | Use a server that ships with pine (`"web"`, `"browser"` or `"commands"`), configured through `options`. |
|
|
129
|
+
| `session` | In an `agent.json` only, written by `add_mcp_server`: the one conversation the server was added for. Other conversations of the agent do not get it, and it is removed with that conversation. |
|
|
130
|
+
| `taints` | `true` when the server returns content written by others (web pages, mail, tickets, chat). A session that calls it becomes tainted. Default `false`, `true` for the bundled `web` and `browser`. |
|
|
131
|
+
| `approve` | `"always"`: every call needs your approval, each time (except in bypass mode, where nothing asks). `"tainted"`: once the session has seen outside content, the server's tools that act need approval (its read-only tools do not), and you can answer "always in this conversation"; use it for servers that change things or send things out. Default `"never"`. |
|
|
132
|
+
|
|
133
|
+
Each session starts its own copy of a local server and stops it when the session closes. A
|
|
134
|
+
local server is a normal process of yours: it is not sandboxed, and what it can reach, agents
|
|
135
|
+
can reach through it. Give it the narrowest credentials that do the job.
|
|
136
|
+
|
|
137
|
+
pi's `mcp.json` files are not read. The daemon gives pi a folder of its own (`data/pi`) and
|
|
138
|
+
agent sessions load no MCP configuration file, so servers you set up for your own interactive
|
|
139
|
+
pi do not appear in agents.
|
|
140
|
+
|
|
141
|
+
## Bundled: web
|
|
142
|
+
|
|
143
|
+
Search and page reading without a browser.
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
"web": { "pine": "web", "options": { "braveApiKey": "${BRAVE_API_KEY:-}" }, "agents": ["scout"] }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Tool | What it does |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `mcp__web__search(query, count?)` | Web search. Uses the Brave Search API when `braveApiKey` is set, otherwise DuckDuckGo's HTML page (fine for light use; it may rate-limit). |
|
|
152
|
+
| `mcp__web__fetch(url, max_chars?, start?)` | Fetches a page and returns its text. `start` continues a long page. |
|
|
153
|
+
|
|
154
|
+
| Option | Default | Meaning |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `braveApiKey` | none | Brave Search API key. |
|
|
157
|
+
| `allowHosts` | all | Only these hosts (and their subdomains) can be fetched. Without `braveApiKey` the search is such a fetch too: add `duckduckgo.com`. |
|
|
158
|
+
| `denyHosts` | none | These cannot. |
|
|
159
|
+
| `allowPrivate` | `false` | Allow addresses on your own network. Leave it off. |
|
|
160
|
+
| `maxBytes` | 2 MB | Cut-off for one response. |
|
|
161
|
+
| `maxUrlLength` | 2000 | Longer URLs are refused, which limits how much can be smuggled out in one. |
|
|
162
|
+
| `timeoutMs` | 20000 | Per request. |
|
|
163
|
+
|
|
164
|
+
It only speaks http and https, follows redirects itself and checks every hop, and refuses
|
|
165
|
+
loopback, private, link-local and similar addresses at the moment the connection is made. A page
|
|
166
|
+
therefore cannot send an agent to your router, your NAS or pine's own control port. Results are
|
|
167
|
+
wrapped in markers that say they are outside content, and the server is marked `taints`.
|
|
168
|
+
|
|
169
|
+
Once you have agents with private files, move `web` and `browser` to a research agent that has
|
|
170
|
+
nothing private in its workspace and let the others delegate to it.
|
|
171
|
+
[security.md](security.md) explains why.
|
|
172
|
+
|
|
173
|
+
## Bundled: commands
|
|
174
|
+
|
|
175
|
+
Turns fixed commands into typed tools, so an agent that should "check the printer" does not
|
|
176
|
+
need a shell for it.
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
"tools": { "pine": "commands", "agents": ["house"], "options": { "commands": {
|
|
180
|
+
"printer_status": {
|
|
181
|
+
"description": "Current state of the Bambu printer as JSON",
|
|
182
|
+
"run": ["/opt/homebrew/bin/mosquitto_sub", "-h", "192.168.1.50", "-t", "device/{{serial}}/report", "-C", "1", "-W", "5"],
|
|
183
|
+
"params": { "serial": { "description": "Printer serial", "pattern": "[0-9A-Z]{15}" } },
|
|
184
|
+
"timeoutSec": 10
|
|
185
|
+
},
|
|
186
|
+
"disk_usage": { "description": "Free space on the data volume", "run": ["/bin/df", "-h", "/Volumes/data"] }
|
|
187
|
+
} } }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Key | Meaning |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `description` | What the model reads. |
|
|
193
|
+
| `run` | Program and arguments. `{{name}}` inserts a parameter. |
|
|
194
|
+
| `params.<name>` | `type` (`string`, `number`, `boolean`), `description`, `enum`, `pattern` (must match the whole value), `default`, `required` (default true). |
|
|
195
|
+
| `timeoutSec` | Default 30. |
|
|
196
|
+
| `cwd`, `env` | Working folder (default: where the conversation's tools start: the agent's workspace, or the folder opened for the conversation) and extra environment. |
|
|
197
|
+
| `sandbox` | `true` runs the command inside the agent's sandbox (the variant in which instruction files are read-only). It has no effect for an agent with `"sandbox": false` or on a platform without `sandbox-exec`. Default `false`: these are your commands, and most need the network. |
|
|
198
|
+
| `maxOutput` | Characters of output returned. Default 30000. |
|
|
199
|
+
|
|
200
|
+
Arguments are passed as a list, never through a shell: a parameter value of `x; rm -rf ~` is one
|
|
201
|
+
harmless argument. A value that starts with `-` is refused unless the parameter is a number or
|
|
202
|
+
has a `pattern` or `enum`, so the model cannot turn a value into an option. Constrain parameters
|
|
203
|
+
with `pattern` or `enum` wherever you can.
|
|
204
|
+
|
|
205
|
+
Mark a commands server `"approve": "tainted"` if any of its commands changes something.
|
|
206
|
+
|
|
207
|
+
## Bundled: browser
|
|
208
|
+
|
|
209
|
+
A real browser, for pages that need JavaScript, clicks, forms or a login. It is Microsoft's
|
|
210
|
+
Playwright MCP server with a guard around it.
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
"browser": { "pine": "browser", "options": { "headed": false } }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`pine init` downloads the browser itself; `pine browser-install` fetches it again after an update.
|
|
217
|
+
|
|
218
|
+
The tools appear as `mcp__browser__browser_navigate`, `..._snapshot`, `..._click`, `..._type`,
|
|
219
|
+
`..._fill_form`, `..._press_key`, `..._select_option`, `..._hover`, `..._wait_for`, `..._find`,
|
|
220
|
+
`..._tabs`, `..._navigate_back`, `..._handle_dialog`, `..._close`. A page is read as a
|
|
221
|
+
snapshot: a text outline in which every element has a reference the click and type tools take.
|
|
222
|
+
|
|
223
|
+
| Option | Default | Meaning |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| `headed` | `false` | Show the browser window, so you can watch. |
|
|
226
|
+
| `profile` | none | A folder for a persistent profile: cookies and logins survive. Without it every session starts clean. |
|
|
227
|
+
| `tools` | `"core"` | `"core"`: the fourteen tools above. `"all"`: everything Playwright offers that is not blocked. Or a list of names. |
|
|
228
|
+
| `block` | `[]` | Further tool names to withhold. |
|
|
229
|
+
| `allowHosts`, `denyHosts` | none | Limit where the browser may go. Subdomains are included. |
|
|
230
|
+
| `allowPrivate` | `false` | Allow addresses on your own network. Leave it off. |
|
|
231
|
+
| `browser` | `chromium` | `chrome` or `msedge` use an installed browser instead of Playwright's Chromium. |
|
|
232
|
+
| `executablePath`, `viewport`, `caps`, `extraArgs` | | Passed on to Playwright MCP. |
|
|
233
|
+
|
|
234
|
+
What the guard does:
|
|
235
|
+
|
|
236
|
+
- **No local network.** Everything the browser requests goes through a proxy inside the guard,
|
|
237
|
+
which refuses loopback, private and link-local addresses, judged on the address it actually
|
|
238
|
+
connects to. A page cannot reach your router, your NAS, the model server or pine's control
|
|
239
|
+
port, whether by link, redirect, script or DNS trick. `file:` URLs are blocked by Playwright.
|
|
240
|
+
- **No code on your machine.** `browser_run_code_unsafe` (runs code in the server process),
|
|
241
|
+
`browser_file_upload` and `browser_drop` (hand local files to a page) and `browser_install`
|
|
242
|
+
are removed from the tool list and refused if called, whatever `tools` says.
|
|
243
|
+
- **One step at a time.** Calls a model issues in parallel are run one after another; a browser
|
|
244
|
+
has one page in front.
|
|
245
|
+
- **Snapshots inline.** Playwright writes snapshots to files; the guard puts their content into
|
|
246
|
+
the tool result (cut at 24,000 characters; `browser_find` locates things on longer pages).
|
|
247
|
+
|
|
248
|
+
A browser is the widest door for outside content: sessions that use it are tainted, and with a
|
|
249
|
+
`profile` it is signed in as you wherever you signed in. Treat a persistent profile like a
|
|
250
|
+
password; do not give it to an agent that also handles untrusted events.
|
|
251
|
+
|
|
252
|
+
## Telegram, MQTT and other outputs
|
|
253
|
+
|
|
254
|
+
Sending a Telegram message or an MQTT command is not an MCP server. Both are event sources
|
|
255
|
+
that can also send, and an agent uses its `send` tool for them: the daemon already holds the
|
|
256
|
+
connection, and the source decides who may be messaged and which topics may be written. See
|
|
257
|
+
"Sending through a source" in [sources.md](sources.md).
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Running pine
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- macOS (the sandbox uses `sandbox-exec`). pine starts elsewhere, but withholds the shell tool.
|
|
6
|
+
- Node 24 or newer. From a checkout Node runs the TypeScript sources directly; the npm package
|
|
7
|
+
holds them compiled.
|
|
8
|
+
- A model that supports tool calls: a hosted provider pi knows (sign in with `pine login`), or
|
|
9
|
+
your own model server with an OpenAI-compatible chat completions endpoint.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @pmndrs/pine
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That puts the `pine` command on your PATH. The package holds pine compiled to JavaScript,
|
|
18
|
+
because Node does not run TypeScript from inside `node_modules`.
|
|
19
|
+
|
|
20
|
+
To work on pine itself, use a checkout instead. `bin/pine` runs the sources as they are and
|
|
21
|
+
installs the dependencies the first time; `npm link` puts that copy on your PATH:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/pmndrs/pine ~/dev/pine
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
~/dev/pine/bin/pine init
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## First start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pine init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
sets everything up in one step:
|
|
38
|
+
|
|
39
|
+
- From a fresh checkout, `bin/pine` first installs pine's dependencies (`npm install`), once.
|
|
40
|
+
- It copies the example home to `~/.pine`: one agent, `main`, with web search and the browser, a
|
|
41
|
+
`models.json`, an empty `.env`. Nothing is overwritten if you run it again.
|
|
42
|
+
- It downloads the Chromium build the bundled browser tools drive (about 600 MB, into
|
|
43
|
+
Playwright's cache). A second run finds it there and takes a moment. If the download fails,
|
|
44
|
+
everything else is set up and `pine init` can simply be run again.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pine
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
opens the terminal UI ([tui.md](tui.md)) and, since no daemon is running yet, starts one inside
|
|
51
|
+
it. That is the quickest way to try things. For a daemon that keeps running without a terminal:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pine check
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pine start
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
and from another terminal `pine` (attaches to it) or
|
|
62
|
+
`pine send main "Say hello and list your tools."`.
|
|
63
|
+
|
|
64
|
+
### Choosing a model
|
|
65
|
+
|
|
66
|
+
The example home uses `dev/claude-haiku`: an OpenAI-compatible endpoint whose answers come from
|
|
67
|
+
`claude -p`, so it needs Claude Code installed and signed in. The UI starts it on demand; for
|
|
68
|
+
`pine start`, run it yourself:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pine dev-model
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Every model call starts a `claude` process: expect about five seconds per call and usage on
|
|
75
|
+
your Claude plan. It is for trying things out, not for a daemon that runs all day.
|
|
76
|
+
|
|
77
|
+
For a hosted provider, sign in once and pick a model:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pine login opencode-go
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pine login anthropic
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pine login openai-codex
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The first asks for an API key (typed without echo); the other two open a browser sign-in for a
|
|
92
|
+
Claude or ChatGPT subscription. Credentials go to `~/.pine/auth.json` (mode 600). Then set
|
|
93
|
+
`defaults.model` in `pine.json`, for example `"opencode-go/deepseek-v4.1-flash"`, or switch in
|
|
94
|
+
the UI with `/model`. `pine login <unknown>` lists every provider pi knows.
|
|
95
|
+
|
|
96
|
+
### With llama.cpp
|
|
97
|
+
|
|
98
|
+
Start the server with tool-call support and a context that matches `models.json`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
llama-server -m /path/to/model.gguf --jinja -c 65536 -np 2 --port 8080
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`-np` (parallel slots) should equal `limits.maxConcurrentRuns`. The context is shared between
|
|
105
|
+
slots, so each run gets `c / np` tokens; put that number into `contextWindow`.
|
|
106
|
+
|
|
107
|
+
## Updating
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pine update
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
brings pine itself up to date and fetches what the new version needs. What it does depends on
|
|
114
|
+
how pine got onto the machine, which `pine version` tells you:
|
|
115
|
+
|
|
116
|
+
| Installed | `pine update` |
|
|
117
|
+
|---|---|
|
|
118
|
+
| with `npm install -g` | Asks npm for the latest version. If there is one, installs it (`npm install -g @pmndrs/pine@latest`). |
|
|
119
|
+
| as a checkout (`git clone`) | Fetches from the repository the checkout follows and moves it forward, then runs `npm install`. Only ever a fast-forward: if commits or changes of your own are in the way, nothing is changed and it says so. |
|
|
120
|
+
| as a dependency inside a project | Tells you the command to run in that project; that project decides its version. |
|
|
121
|
+
|
|
122
|
+
Afterwards it makes sure the browser the web tools drive matches the new version, which is a
|
|
123
|
+
download only when that version changed.
|
|
124
|
+
|
|
125
|
+
`pine update --check` changes nothing and says whether there is something newer.
|
|
126
|
+
|
|
127
|
+
A pine that is running keeps the old version until it is restarted: quit the terminal UI and
|
|
128
|
+
open it again, or restart the daemon (if it runs at login as a LaunchAgent:
|
|
129
|
+
`launchctl kickstart -k gui/$(id -u)/com.pine.daemon`).
|
|
130
|
+
Your home folder (`~/.pine`) is not touched by an update. When a new version adds something to
|
|
131
|
+
the example home, `pine init` copies the files you do not have yet and overwrites nothing.
|
|
132
|
+
|
|
133
|
+
## Commands
|
|
134
|
+
|
|
135
|
+
| Command | What it does |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `pine`, `pine tui` | The terminal UI; starts a daemon inside if none is running |
|
|
138
|
+
| `pine .`, `pine <folder>` | The terminal UI with one more tab: a conversation of the main agent that works in that folder (the one from last time, if there is one). A first word that is a command stays a command. See [coding.md](coding.md) |
|
|
139
|
+
| `pine login <provider>`, `pine logout <provider>` | Sign in to a model provider. `--api-key` or `--oauth` force the kind of sign-in |
|
|
140
|
+
| `pine init [dir]` | Set pine up: a home directory from the example and the browser its web tools drive. Safe to run again |
|
|
141
|
+
| `pine update [--check]` | Bring pine up to date. `--check` only says whether there is something newer. See "Updating" |
|
|
142
|
+
| `pine version` | Which version this is and how it is installed |
|
|
143
|
+
| `pine browser-install` | Fetch the browser again. `pine init` and `pine update` do this already |
|
|
144
|
+
| `pine check` | Validate the configuration; show each agent's workspace, model, tools, MCP servers; list security notes |
|
|
145
|
+
| `pine start` | Run the daemon in the foreground. Ctrl-C or SIGTERM stops it cleanly |
|
|
146
|
+
| `pine dev-model [--port 7464] [--model haiku]` | The `claude -p` model server |
|
|
147
|
+
| `pine status` | Running tasks, live sessions (and whether they are tainted), model slots |
|
|
148
|
+
| `pine agents` | Configured agents |
|
|
149
|
+
| `pine send <agent> <text...>` | Give an agent a task as yourself and print the answer. `--session <key>`, `--no-wait` |
|
|
150
|
+
| `pine emit <topic> [json]` | Publish an event as yourself |
|
|
151
|
+
| `pine events` | The event log. `--topic <pattern>`, `--limit <n>`, `-f` to follow |
|
|
152
|
+
| `pine tasks` | Recent tasks. `--agent <id>`, `--status pending\|running\|done\|failed\|cancelled` |
|
|
153
|
+
| `pine task <id>` | One task in full |
|
|
154
|
+
| `pine cancel <id>` | Stop a task |
|
|
155
|
+
| `pine state [pattern]` | Shared state |
|
|
156
|
+
| `pine sources` | Lists the event sources: running or idle, who is subscribed, last event; for one that polls (`http`, `command`), when it last looked or since when that fails |
|
|
157
|
+
| `pine subscribe <agent> <source> [--session name] [--note text] [--throttle 5m] [--when json] [--separate]`, `pine unsubscribe <agent> <source> [--session name]` | Wake an agent on a source's events (with `--session`: only that conversation), or stop that |
|
|
158
|
+
| `pine subscribe --global <source> [--note text] ...`, `pine unsubscribe --global <source>` | The same for every agent that can use the source |
|
|
159
|
+
| `pine approvals`, `pine approve <id> [--always]`, `pine deny <id>` | Answer approval requests; `--always` stops the question for the rest of that conversation |
|
|
160
|
+
| `pine reset <agent> [session]` | Start a conversation over; also clears its taint |
|
|
161
|
+
| `pine mode <agent> [manual\|auto\|bypass]` | Show or change an agent's mode until the daemon restarts |
|
|
162
|
+
| `pine mcp list` | MCP servers, who gets them, whether they connect |
|
|
163
|
+
| `pine skills` | Lists every skill: whom it is for (all agents, an agent, one conversation) and where it lies |
|
|
164
|
+
| `pine schedules` | Lists what is scheduled: for whom, when, and the task |
|
|
165
|
+
| `pine manual <words>`, `pine manual --read <id>` | Search this manual the way the agents do, or read one part of it in full |
|
|
166
|
+
| `pine mcp add <name> <url>`, `pine mcp add <name> -- <command...>` | Add a remote or a local server. `--agent <id>` for one agent |
|
|
167
|
+
| `pine mcp remove <name> [--agent <id>]` | Remove a server from `pine.json`, or from that agent's `agent.json` |
|
|
168
|
+
| `pine mcp login <name>`, `pine mcp logout <name>` | Sign in to a remote server through the browser, or forget its tokens |
|
|
169
|
+
|
|
170
|
+
All take `--home <dir>`; the default is `$PINE_HOME` or `~/.pine`. The commands from `status`
|
|
171
|
+
down to `mode` talk to the running daemon over its control API on `127.0.0.1:7463`, using the
|
|
172
|
+
token in `data/http-token`. The `pine mcp` commands work on the files, and `add` and `remove`
|
|
173
|
+
also tell a running daemon, so the change applies at once. `pine help` prints the list.
|
|
174
|
+
|
|
175
|
+
## Running at login
|
|
176
|
+
|
|
177
|
+
[examples/launchd/com.pine.daemon.plist](../../examples/launchd/com.pine.daemon.plist) is a
|
|
178
|
+
LaunchAgent that starts pine at login and restarts it when it exits. Follow the three steps at
|
|
179
|
+
its top. Logs then go to `~/.pine/data/pine.log`.
|
|
180
|
+
|
|
181
|
+
A LaunchAgent runs as you, in your login session. To separate the agents from your files more
|
|
182
|
+
strongly, create a dedicated macOS user, keep pine's home and the agents' workspaces there, and
|
|
183
|
+
install the plist for that user.
|
|
184
|
+
|
|
185
|
+
## The control API
|
|
186
|
+
|
|
187
|
+
For scripts and other programs. Every request needs `Authorization: Bearer <token>`; POST bodies
|
|
188
|
+
are JSON with `Content-Type: application/json`.
|
|
189
|
+
|
|
190
|
+
| Request | Purpose |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `GET /health` | Liveness (no token) |
|
|
193
|
+
| `GET /status`, `/agents`, `/approvals` | As the CLI commands |
|
|
194
|
+
| `GET /models` | Models that have credentials, as `provider/id` |
|
|
195
|
+
| `GET /mcp` | Configured MCP servers |
|
|
196
|
+
| `POST /mcp` `{ name, server, agent? }` or `{ name, remove: true, agent? }` | Add or remove a server while the daemon runs |
|
|
197
|
+
| `POST /mcp/refresh` | Close idle sessions so they start again with the current servers |
|
|
198
|
+
| `GET /agents/<id>/sessions` | The agent's conversations: name, number of messages, last use, whether pinned or open in a tab, and when one will be removed by itself |
|
|
199
|
+
| `POST /agents/<id>/pin` `{ session, pinned? }` | Pin a conversation so that it is kept, or with `pinned: false` take the pin off |
|
|
200
|
+
| `POST /agents/<id>/rename` `{ session, to }` | Give a conversation another name; its history, pin, subscriptions and what was set up for it follow |
|
|
201
|
+
| `POST /agents/<id>/remove` `{ session }` | Remove a conversation for good, with its history and what was set up for it alone. Not the main one, and not while it is working |
|
|
202
|
+
| `GET /ui/tabs`, `POST /ui/tabs` `{ tabs, active, code? }` | The tabs the terminal UI has open (`tabs` is a list of `{ agent, session }`) and whether its code pane was open, kept so that it opens the same way next time |
|
|
203
|
+
| `GET /agents/<id>/folder?session=`, `GET /agents/<id>/folders`, `POST /agents/<id>/folder` `{ session, folder }` | The folder a conversation works in; every conversation of the agent that has one; open a folder for a conversation (`folder: null` closes it). Refused for pine's home, pine's program and other agents' workspaces, and while the conversation is working |
|
|
204
|
+
| `GET /agents/<id>/view/changes?session=`, `/view/diff?session=&path=`, `/view/tree?session=&path=`, `/view/file?session=&path=` | What the code pane shows of a conversation's folder (or of the agent's workspace): what git says changed, one file's diff, what is in a folder, a file's text. Read-only, and nothing outside that folder |
|
|
205
|
+
| `POST /agents/<id>/model` `{ model }` | Switch an agent's model and save it in its `agent.json` |
|
|
206
|
+
| `POST /agents/<id>/thinking` `{ level }` | Set an agent's reasoning level and save it in its `agent.json` |
|
|
207
|
+
| `POST /agents/<id>/mode` `{ mode, session?, anyway? }` | Switch an agent's mode until the daemon restarts. `anyway: true` with a `session` keeps bypass in force in that conversation although it has read outside content |
|
|
208
|
+
| `GET /tasks?agent=&session=&status=&limit=`, `GET /tasks/<id>` | Tasks |
|
|
209
|
+
| `GET /tasks/<id>/transcript` | What a task did, step by step: the text it was given, what it wrote, its tool calls with the start of each result. With `?full=1`: with the whole result of each call, up to 200 000 characters |
|
|
210
|
+
| `GET /events?topic=&after=&limit=` | Event log; `after=<id>` pages forward |
|
|
211
|
+
| `GET /events/stream?topic=` | Live events as server-sent events |
|
|
212
|
+
| `GET /state?key=` | Shared state |
|
|
213
|
+
| `POST /events` `{ topic, payload }` | Publish an event (as owner; `"trust": "untrusted"` to lower it) |
|
|
214
|
+
| `POST /agents/<id>/prompt` `{ prompt, session?, wait?, attachments? }` | Give a task. Waits for the result unless `wait` is false. `attachments` is a list of `{ name, data }` with base64 data; the files are saved to `inbox/` in the agent's workspace |
|
|
215
|
+
| `POST /agents/<id>/reset` `{ session? }` | Reset a session. What it was subscribed to on its own ends too |
|
|
216
|
+
| `GET /media/tool/<id>` | A picture a tool returned. The ids are in the `images` field of `agent/<id>/tool` events and of transcript steps |
|
|
217
|
+
| `GET /tasks/<id>/images/<n>` | The n-th picture that came with a task (an attachment, a Telegram photo) |
|
|
218
|
+
| `GET /agents/<id>/image?path=&session=` | A picture or video file in that agent's workspace or, with `session`, in the folder opened for that conversation. Anything that is not an image or video file in there is "not found" |
|
|
219
|
+
| `POST /images/fetch` `{ url }` | Fetch a picture or video from the web for display: https and public addresses only; pictures up to 20 MB, videos up to 100 MB |
|
|
220
|
+
| `GET /sources`, `GET /agents/<id>/sources` | Event sources and their state; the ones that agent can subscribe to |
|
|
221
|
+
| `POST /agents/<id>/subscribe` `{ source, session?, note?, when?, throttle?, debounce?, separate? }`, `POST /agents/<id>/unsubscribe` `{ source, session? }` | Change an agent's subscriptions (written to its `agent.json`); `session` makes it one conversation's |
|
|
222
|
+
| `POST /subscribe` `{ source, note?, ... }`, `POST /unsubscribe` `{ source }` | The subscription of all agents (written to `pine.json`) |
|
|
223
|
+
| `POST /agents/<id>/close` `{ session }` | A conversation's tab was closed in a UI: ends what it was subscribed to on its own. The conversation itself is kept |
|
|
224
|
+
| `POST /tasks/<id>/cancel` | Cancel |
|
|
225
|
+
| `POST /approvals/<id>` `{ allow, always?, values? }` | Answer an approval. A request that lists `inputs` (a token, a password) is only allowed together with `values: { <name>: "..." }`; the terminal UI has a field for them |
|
|
226
|
+
| `POST /state` `{ key, value }` | Write state; `null` deletes |
|
|
227
|
+
| `POST /hook/<name>` | Webhook: a hook from `http.hooks` ([events.md](events.md)) or a running `webhook` source ([sources.md](sources.md)). No token; each has its own secret, unless a hook is declared `"open": true` |
|
|
228
|
+
|
|
229
|
+
## Day to day
|
|
230
|
+
|
|
231
|
+
- **See what is happening:** `pine events -f` in a terminal. Add `--topic "agent/#"` for agent
|
|
232
|
+
activity only; tool calls appear live on `agent/<id>/tool`.
|
|
233
|
+
- **Why did it answer that?** `pine task <id>` shows the prompt as the agent received it, the
|
|
234
|
+
tools it called and the result. The full transcript is the newest `.jsonl` under
|
|
235
|
+
`~/.pine/agents/<id>/sessions/<session>/`.
|
|
236
|
+
- **Change instructions:** tell the agent ("from now on ..."); it writes that into `AGENTS.md` in
|
|
237
|
+
its workspace. Or edit the file yourself. Either applies to the next message. What all agents
|
|
238
|
+
should know about you goes into `USER.md` in pine's home, which the main agent keeps.
|
|
239
|
+
- **Change configuration:** edit `pine.json` or `agent.json`, run `pine check`, restart the
|
|
240
|
+
daemon. Queued tasks survive the restart. MCP servers, event sources, subscriptions and new
|
|
241
|
+
agents need neither an edit nor a restart: ask an agent ([setup.md](setup.md)), or use
|
|
242
|
+
`pine mcp add` and `pine subscribe`.
|
|
243
|
+
- **An agent is stuck:** `pine status`, then `pine cancel <id>`.
|
|
244
|
+
- **An agent keeps asking for approval:** its session is tainted. `/reset` or `pine reset`.
|
|
245
|
+
- **Back up:** the home directory. `data/pine.db` holds events and tasks, `agents/*/sessions`
|
|
246
|
+
the conversations. Workspaces are wherever you put them.
|
|
247
|
+
|
|
248
|
+
## Problems
|
|
249
|
+
|
|
250
|
+
| Symptom | Likely cause |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `pine.json not found` | Wrong home. Pass `--home` or set `PINE_HOME`. |
|
|
253
|
+
| `model "x/y" is not defined in models.json` | `defaults.model` or an agent's `model` does not match `provider/id`. `/model` in the UI lists what is available. |
|
|
254
|
+
| A task fails with 401 or "API key" | The provider's credential is missing or expired. `pine login <provider>` again. |
|
|
255
|
+
| The browser tools fail with "Executable doesn't exist" | Run `pine init` again (or `pine browser-install`): the browser is missing or pine was updated to a newer one. |
|
|
256
|
+
| The UI says "reconnecting…" | The daemon it was attached to stopped. Start it again; the UI picks it up. |
|
|
257
|
+
| `no daemon answers on http://127.0.0.1:7463` | The daemon is not running, or `http.port` differs from what the CLI reads in this home. |
|
|
258
|
+
| A task fails with a connection error | The model server is down or `baseUrl` is wrong. `curl <baseUrl>/models` should answer. |
|
|
259
|
+
| A task fails after minutes with a timeout | The model stopped streaming (`limits.modelStreamIdleMs`) or took too long to start (`limits.modelHeadersTimeoutMs`); raise them for a slow model. |
|
|
260
|
+
| The agent says a file is "not permitted" | It is outside the workspace. In `auto` mode it can ask you with `request_access`; to open a folder for good, use `sandbox.read` / `sandbox.write`. |
|
|
261
|
+
| `git` complains about identity | The agent's shell has its own empty home. Run `git config --global user.name ...` once through the agent, or set it per repository. |
|
|
262
|
+
| A command needs the network | `sandbox.network: true` for that agent, or better a `commands` tool for that one command. |
|
|
263
|
+
| Telegram bot does not react | `pine sources`: is the `telegram` source running, and is a conversation subscribed? A new bot waits to be paired: write to it, and type the code it sends you into the question in the terminal UI (`pine sources` says whom it listens to). Otherwise another program polls the same bot token. |
|
|
264
|
+
| An event arrives but no task starts | See "When nothing happens" in [events.md](events.md) for routes and in [sources.md](sources.md) for subscriptions. |
|
|
265
|
+
| The first answer after a pause is slow | The session was closed after `idleMs` and is being resumed; MCP servers start again. |
|
|
266
|
+
|
|
267
|
+
## Coming from OpenClaw
|
|
268
|
+
|
|
269
|
+
| OpenClaw | pine |
|
|
270
|
+
|---|---|
|
|
271
|
+
| Agents in `openclaw.json` with `workspace-*` folders | One folder per agent under `agents/`; `workspace` points at the repository |
|
|
272
|
+
| Telegram `bindings` | A `telegram` source the agent sets up when you ask it to, and is subscribed to |
|
|
273
|
+
| `SOUL.md`, `AGENTS.md`, `MEMORY.md` | The same three names in the agent's workspace, read into its prompt and written by the agent |
|
|
274
|
+
| `USER.md` | One `USER.md` in pine's home for all agents, kept by the main agent |
|
|
275
|
+
| `IDENTITY.md`, `TOOLS.md`, daily memory files | Not read by pine. Fold what matters into `SOUL.md`, `AGENTS.md` or `MEMORY.md`; other notes are ordinary files in the workspace |
|
|
276
|
+
| Cron jobs | A schedule: ask the agent ("every morning at seven ..."), or write it under `schedules` with a `task` |
|
|
277
|
+
| Heartbeat | Usually not needed: an event source wakes the agent when something happens, and looking again and again (`http`, `command`) costs no model turn. For a real one, a schedule with `every` whose task ends in "reply NO_REPLY if nothing needs attention" |
|
|
278
|
+
| `/hooks/*` mappings | A `webhook` source the agent subscribes to, or `http.hooks` plus a route |
|
|
279
|
+
| Managed extensions that watch something and message Telegram themselves | An event source the agent subscribes to (a folder, an FTP drop, MQTT), and the `send` tool for what it reports to you on Telegram. The agent then knows what happened |
|
|
280
|
+
| Skills | Skills, same format |
|