privateer-agent 0.1.0

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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +474 -0
  3. package/bin/privateer.mjs +11 -0
  4. package/package.json +74 -0
  5. package/src/agents/loader.ts +49 -0
  6. package/src/auth/privateer.ts +393 -0
  7. package/src/commands/custom.ts +75 -0
  8. package/src/commands/registry.ts +499 -0
  9. package/src/components/AgentGroupView.tsx +104 -0
  10. package/src/components/App.tsx +1376 -0
  11. package/src/components/ApprovalPrompt.tsx +38 -0
  12. package/src/components/Banner.tsx +58 -0
  13. package/src/components/Markdown.tsx +183 -0
  14. package/src/components/ModeHint.tsx +40 -0
  15. package/src/components/ModelPicker.tsx +269 -0
  16. package/src/components/Onboarding.tsx +203 -0
  17. package/src/components/PlanConfirm.tsx +37 -0
  18. package/src/components/PrivateerLogin.tsx +109 -0
  19. package/src/components/PromptInput.tsx +602 -0
  20. package/src/components/RewindPicker.tsx +69 -0
  21. package/src/components/Root.tsx +95 -0
  22. package/src/components/SessionPicker.tsx +64 -0
  23. package/src/components/StatusBar.tsx +121 -0
  24. package/src/components/TodoPanel.tsx +36 -0
  25. package/src/components/ToolCallView.tsx +109 -0
  26. package/src/components/Transcript.tsx +203 -0
  27. package/src/components/figures.ts +13 -0
  28. package/src/components/promptModel.ts +73 -0
  29. package/src/components/spinnerVerbs.ts +46 -0
  30. package/src/components/theme.ts +55 -0
  31. package/src/components/types.ts +34 -0
  32. package/src/components/useTeeShield.ts +104 -0
  33. package/src/components/useTerminalWidth.ts +24 -0
  34. package/src/components/useZdrShield.ts +126 -0
  35. package/src/config/load.ts +115 -0
  36. package/src/config/paths.ts +61 -0
  37. package/src/config/schema.ts +94 -0
  38. package/src/context/outputStyles.ts +42 -0
  39. package/src/context/projectInfo.ts +59 -0
  40. package/src/context/systemPrompt.ts +167 -0
  41. package/src/engine/QueryEngine.ts +399 -0
  42. package/src/engine/errors.ts +197 -0
  43. package/src/engine/events.ts +74 -0
  44. package/src/engine/router.ts +165 -0
  45. package/src/hooks/engine.ts +155 -0
  46. package/src/main.tsx +167 -0
  47. package/src/mcp/client.ts +236 -0
  48. package/src/mcp/oauth.ts +245 -0
  49. package/src/memory/auto.ts +146 -0
  50. package/src/memory/checkpoints.ts +227 -0
  51. package/src/memory/store.ts +127 -0
  52. package/src/permissions/danger.ts +56 -0
  53. package/src/permissions/gate.ts +38 -0
  54. package/src/permissions/mode.ts +39 -0
  55. package/src/permissions/protected.ts +29 -0
  56. package/src/permissions/uiGate.ts +73 -0
  57. package/src/providers/attestation.ts +149 -0
  58. package/src/providers/capabilities.ts +104 -0
  59. package/src/providers/catalog.ts +66 -0
  60. package/src/providers/models.ts +183 -0
  61. package/src/providers/registry.ts +71 -0
  62. package/src/providers/resolve.ts +78 -0
  63. package/src/remote/relayClient.ts +283 -0
  64. package/src/session.ts +264 -0
  65. package/src/tools/bash.ts +98 -0
  66. package/src/tools/context.ts +114 -0
  67. package/src/tools/edit.ts +67 -0
  68. package/src/tools/exec.ts +60 -0
  69. package/src/tools/glob.ts +39 -0
  70. package/src/tools/grep.ts +86 -0
  71. package/src/tools/index.ts +69 -0
  72. package/src/tools/memory.ts +53 -0
  73. package/src/tools/processRegistry.ts +77 -0
  74. package/src/tools/read.ts +42 -0
  75. package/src/tools/saveAttachment.ts +53 -0
  76. package/src/tools/task.ts +52 -0
  77. package/src/tools/todo.ts +36 -0
  78. package/src/tools/todoStore.ts +31 -0
  79. package/src/tools/walk.ts +44 -0
  80. package/src/tools/web.ts +145 -0
  81. package/src/tools/write.ts +40 -0
  82. package/src/util/attachmentStore.ts +72 -0
  83. package/src/util/images.ts +343 -0
  84. package/src/util/limit.ts +32 -0
  85. package/src/util/redact.ts +44 -0
  86. package/src/version.ts +13 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,474 @@
1
+ <p align="center">
2
+ <img src="brand/privateer_logo.png" alt="Privateer" width="140" />
3
+ </p>
4
+
5
+ <h1 align="center">⚓ Privateer</h1>
6
+
7
+ <p align="center">
8
+ <strong>A provider-agnostic terminal coding agent — bring your own model.</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://github.com/privateer-agent/privateer-agent/actions/workflows/ci.yml">
13
+ <img src="https://github.com/privateer-agent/privateer-agent/actions/workflows/ci.yml/badge.svg" alt="CI" />
14
+ </a>
15
+ <img src="https://img.shields.io/badge/node-%E2%89%A520-brightgreen" alt="Node >= 20" />
16
+ <img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" />
17
+ <img src="https://img.shields.io/badge/providers-OpenRouter%20·%20Anthropic%20·%20OpenAI%20·%20Ollama%20·%20NEAR%20AI-5b8def" alt="Providers" />
18
+ <img src="https://img.shields.io/badge/built%20on-Vercel%20AI%20SDK-black" alt="Vercel AI SDK" />
19
+ </p>
20
+
21
+ Switch between **OpenRouter**, **Anthropic**, **OpenAI**, local **Ollama**, and **NEAR AI**
22
+ (private, attestable inference) with one command. Built on the Vercel AI SDK, so tool-calling
23
+ and streaming work identically across every provider — no model lock-in, no separate code paths.
24
+
25
+ <p align="center">
26
+ <img src="docs/screenshot.png" alt="Privateer running in the terminal" width="820" />
27
+ </p>
28
+
29
+ ## Why Privateer?
30
+
31
+ - **No lock-in.** Point it at a frontier model today and a local Ollama model tomorrow —
32
+ `/model` swaps mid-session. Your config, commands, and agents come along for the ride.
33
+ - **The agent UX you already know.** Plan mode, checkpoint/rewind, a modal prompt, slash
34
+ commands, sub-agents, and project memory — but vendor-neutral.
35
+ - **Genuinely extensible.** MCP servers, lifecycle hooks, custom commands, output styles,
36
+ and sub-agents are all just files under `.privateer/`. No plugins to compile.
37
+ - **Zero binary deps.** The file/search/shell tools are pure Node — nothing to install
38
+ beyond `node`.
39
+
40
+ ## Highlights
41
+
42
+ - A modal prompt with `/` command and `@` file autocomplete, `!` shell passthrough,
43
+ `#` memory append, input history, optional **vim** mode, and **ctrl-r** history search
44
+ - Layered `settings.json` (user → project → local → managed), **custom slash commands**
45
+ and **output styles** as markdown files
46
+ - **Plan mode** (read-only → present a plan → approve), **checkpoint/rewind** of
47
+ conversation and files
48
+ - Extensible: **MCP servers** (local stdio + remote HTTP/SSE, with interactive OAuth),
49
+ lifecycle **hooks**, and **custom sub-agents**
50
+ - Background shells, bounded parallel sub-agents, thinking display, structured compaction,
51
+ and image attachment for vision-capable models
52
+ - **Zero-Data-Retention surfacing** for OpenRouter: a status-bar shield colors the selected
53
+ model's retention posture, and `/zdr` pins routing to zero-retention endpoints
54
+ - **Private, verifiable inference** via NEAR AI: every model runs in a TEE, a `⛉ TEE` status
55
+ shield reflects the live attestation, and `/verify` fetches the attestation report (validate
56
+ the raw quote chains with the NEAR Cloud Verifier for full cryptographic proof)
57
+
58
+ ## Quickstart
59
+
60
+ ```bash
61
+ npx privateer-agent # zero-install, runs the latest
62
+ ```
63
+
64
+ Or install the `privateer` command on your PATH:
65
+
66
+ ```bash
67
+ npm install -g privateer-agent # then just run: privateer
68
+ # or, the one-liner installer (checks Node, then installs):
69
+ curl -fsSL https://privateer.pro/install.sh | sh
70
+ ```
71
+
72
+ ```bash
73
+ export OPENROUTER_API_KEY=sk-or-... # one provider is enough — or run /login
74
+ privateer # launches the interactive TUI
75
+ ```
76
+
77
+ First run walks you through picking a provider and default model. From there, just type.
78
+
79
+ ## Contents
80
+
81
+ - [Requirements](#requirements) · [Install](#install) · [Configure a provider](#configure-a-provider) · [Model routing](#model-routing) · [Data retention (ZDR)](#data-retention-zdr) · [Private inference (NEAR AI)](#private-inference-near-ai) · [Usage](#usage)
82
+ - [The prompt](#the-prompt) · [Slash commands](#slash-commands) · [Tools](#tools)
83
+ - [Customize & extend](#customize--extend) · [Permission modes](#permission-modes) · [Project context](#project-context)
84
+ - [Develop](#develop) · [Caveats](#caveats) · [Docs](#docs) · [License](#license)
85
+
86
+ ## Requirements
87
+
88
+ - Node.js ≥ 20
89
+ - An API key for at least one provider (or a local Ollama install)
90
+
91
+ ## Install
92
+
93
+ ```bash
94
+ npm install -g privateer-agent # installs the `privateer` command
95
+ # or run it without installing:
96
+ npx privateer-agent
97
+ # or the one-liner installer (verifies Node >= 20, then installs globally):
98
+ curl -fsSL https://privateer.pro/install.sh | sh
99
+ ```
100
+
101
+ **From source** (for hacking on Privateer):
102
+
103
+ ```bash
104
+ git clone https://github.com/privateer-agent/privateer-agent.git
105
+ cd privateer-agent
106
+ npm install # install dependencies
107
+ npm start # launches the interactive TUI (or: node bin/privateer.mjs)
108
+ npm link # optional: put your local `privateer` on PATH
109
+ ```
110
+
111
+ ## Configure a provider
112
+
113
+ Privateer reads credentials from environment variables or a config file.
114
+
115
+ **Env vars (quickest):**
116
+
117
+ ```bash
118
+ export OPENROUTER_API_KEY=sk-or-... # gateway to ~everything
119
+ export ANTHROPIC_API_KEY=sk-ant-...
120
+ export OPENAI_API_KEY=sk-...
121
+ export OLLAMA_BASE_URL=http://localhost:11434/api # optional; defaults to this
122
+ export NEAR_AI_API_KEY=... # private TEE inference (cloud.near.ai)
123
+ ```
124
+
125
+ **Config file** — `~/.privateer/config.json` (global) and/or `./.privateer/config.json` (per project):
126
+
127
+ ```json
128
+ {
129
+ "defaultModel": "openrouter:anthropic/claude-opus-4.8",
130
+ "permissionMode": "default",
131
+ "providers": {
132
+ "openrouter": { "apiKey": "sk-or-..." },
133
+ "anthropic": { "apiKey": "sk-ant-..." }
134
+ }
135
+ }
136
+ ```
137
+
138
+ Override the config location with `PRIVATEER_HOME`.
139
+
140
+ ## Model routing
141
+
142
+ `defaultModel` handles most turns, but it's often the wrong tool for a particular one —
143
+ it may not accept the file you dropped in, or you'd rather spend a cheaper model on a
144
+ trivial question. The optional **`router`** block lets Privateer switch models per turn
145
+ based on the turn's **data type** and shape:
146
+
147
+ ```json
148
+ {
149
+ "defaultModel": "openrouter:minimax/minimax-m3",
150
+ "router": {
151
+ "vision": "openrouter:google/gemini-2.5-flash",
152
+ "document": "openrouter:anthropic/claude-opus-4.8",
153
+ "audio": "openrouter:google/gemini-2.5-flash",
154
+ "video": "openrouter:google/gemini-2.5-flash",
155
+ "long": "openrouter:anthropic/claude-opus-4.8",
156
+ "fast": "openrouter:openai/gpt-4o-mini",
157
+ "longThreshold": 60000,
158
+ "fastMaxChars": 280,
159
+ "inlineTextMaxBytes": 65536,
160
+ "auto": true
161
+ }
162
+ }
163
+ ```
164
+
165
+ Reference a file in the prompt — drag-drop, paste a path, or `@`-mention — and Privateer
166
+ classifies it by **modality** and routes accordingly:
167
+
168
+ | Route | Chosen when the turn (or conversation) includes… |
169
+ |---|---|
170
+ | **vision** | an image (`.png .jpg .jpeg .gif .webp`) |
171
+ | **document** | a PDF (`.pdf`) |
172
+ | **audio** | audio (`.mp3 .wav .m4a .ogg .flac`) |
173
+ | **video** | video (`.mp4 .mov .webm .mkv`) |
174
+ | **long** | the estimated context exceeds `longThreshold` tokens (default: half of `contextBudget`) |
175
+ | **fast** | the prompt is ≤ `fastMaxChars` characters (and needs no attachment) |
176
+ | **default** | everything else (`defaultModel`) |
177
+
178
+ Each attached file collapses to a chip — `[Image #1]`, `[PDF #2]`, `[Audio #3]`,
179
+ `[Video #4]` — while the file itself rides along to the model. **Code/CSV/markdown and
180
+ other text files aren't routed**: they're read and inlined into the prompt (up to
181
+ `inlineTextMaxBytes`; larger ones are left as a path for the agent's read tool).
182
+
183
+ **Capability requirements outrank `long`/`fast`.** A turn that needs a modality is
184
+ routed to a model that can actually accept it — and routing is **sticky**: once a file
185
+ is in the conversation, later turns stay on a capable model so the attachment is never
186
+ replayed to one that can't read it. A turn that needs **several** modalities at once
187
+ (say an image *and* a PDF) is routed to a model whose support covers all of them. When
188
+ a turn is routed, the transcript shows a line like `↪ routed to gemini-2.5-flash ·
189
+ image input`.
190
+
191
+ **Hybrid auto-detect** (`"auto": true`, the default): if you reference, say, a PDF but
192
+ haven't set `router.document`, and your `defaultModel` can't read PDFs, Privateer
193
+ auto-selects a capable model from a configured provider. Set the route explicitly to
194
+ control exactly which model is used, or `"auto": false` to disable it (you'll get a
195
+ one-line warning when nothing can handle the modality).
196
+
197
+ ## Data retention (ZDR)
198
+
199
+ When you route through **OpenRouter**, where your prompts end up depends on which upstream
200
+ endpoint serves the request — some retain data, some don't. Privateer surfaces that for the
201
+ **selected model** so you can see the posture before you send, and optionally enforce it.
202
+
203
+ **The status-bar shield.** A `⛉ ZDR` badge sits in the status line, colored against the
204
+ model you have selected:
205
+
206
+ | Badge | Meaning |
207
+ |---|---|
208
+ | 🟢 `⛉ ZDR` | The model has a zero-retention endpoint **and** enforcement is on — the request is pinned to it, so prompts can't be retained. |
209
+ | 🟡 `⛉ ZDR` | A zero-retention endpoint exists, but enforcement is off — a request *may* still land on an endpoint that retains prompts. |
210
+ | 🔴 `⛉ ZDR` | No zero-retention endpoint for this model, or it's blocked by your account's privacy settings — under enforcement the request is rejected outright. |
211
+ | `⛉ ZDR?` (dim) | Posture unknown — no OpenRouter key yet, still loading, or the lookup failed. |
212
+
213
+ The badge only appears for OpenRouter models; other providers show nothing. The posture is
214
+ derived from two authenticated OpenRouter endpoints — `/endpoints/zdr` (models with at least
215
+ one zero-retention endpoint) and `/models/user` (models your account's privacy settings
216
+ actually permit) — fetched once per account and re-evaluated synchronously as you switch
217
+ models. The same colors annotate every row in the `/model` picker, with a legend explaining
218
+ them.
219
+
220
+ **Enforcement.** Run **`/zdr`** to toggle enforcement (persisted as
221
+ `providers.openrouter.enforceZdr`). With it on, Privateer pins routing to zero-retention
222
+ endpoints (`provider.zdr` on every request), so yellow models go green — and any model
223
+ *without* a zero-retention endpoint is rejected rather than silently retained. Toggle it off
224
+ to let OpenRouter route freely. Enforcement applies to OpenRouter only; add an OpenRouter key
225
+ with `/login` first.
226
+
227
+ ## Private inference (NEAR AI)
228
+
229
+ **NEAR AI Cloud** runs every model inside a **Trusted Execution Environment** — an Intel TDX
230
+ confidential VM paired with an NVIDIA confidential-computing GPU. Your prompts are encrypted
231
+ all the way into the enclave (TLS terminates *inside* the TEE, not at a load balancer), so
232
+ the model's inputs, weights, and outputs are invisible to the infrastructure provider, the
233
+ model provider, and NEAR itself. And it's not "trust us": each request can produce a
234
+ **cryptographic attestation** attesting that the inference happened on genuine TEE hardware,
235
+ signed by a key that never leaves the enclave and bound to a nonce you supply. (Privateer's
236
+ `/verify` does a pragmatic check of that report; full validation of the quote chains is done
237
+ with the NEAR Cloud Verifier — see `/verify` below.)
238
+
239
+ It's a drop-in OpenAI-compatible provider — pick a `nearai:*` model with `/model` (e.g.
240
+ `nearai:zai-org/GLM-5.1-FP8`) and everything else works as usual.
241
+
242
+ **The status-bar shield.** A `⛉ TEE` badge appears whenever a NEAR AI model is selected,
243
+ colored by the live attestation for that model:
244
+
245
+ | Badge | Meaning |
246
+ |---|---|
247
+ | 🟢 `⛉ TEE` | A fresh attestation came back bound to our nonce, with a TEE signing key and NVIDIA + Intel hardware evidence — confidential **and** verifiable. |
248
+ | 🟡 `⛉ TEE` | A report returned but couldn't be fully confirmed here (missing signing key, hardware marker, or nonce echo). |
249
+ | 🔴 `⛉ TEE` | No attestation material returned. |
250
+ | `⛉ TEE?` (dim) | Unknown — no NEAR AI key yet, still loading, or the lookup failed. |
251
+
252
+ **`/verify`.** Run it on a NEAR AI model to fetch the attestation on demand and print the
253
+ verdict, detected hardware, the enclave's signing address, and the nonce. Privateer does a
254
+ pragmatic freshness + presence check suited to a terminal; for full validation of the raw
255
+ NVIDIA/Intel quote chains, take the printed report to the
256
+ [NEAR AI Cloud Verifier](https://github.com/nearai/cloud-verifier).
257
+
258
+ ## Privateer account (billed inference) — what it sees
259
+
260
+ Instead of bringing your own provider key, run **`/login`** to sign into a Privateer account
261
+ (an app-brokered device flow — you approve a short code in the Privateer app/web, so wallet
262
+ and email accounts work identically and no password or wallet key ever touches the terminal).
263
+ Inference then runs on Privateer's server and is billed to your subscription.
264
+
265
+ What that means for your data, precisely:
266
+
267
+ - **The server proxies your prompts; it does not store them.** Like the Privateer apps, the
268
+ account path sends your prompt to the server, which forwards it to the model provider and
269
+ streams the reply back. The only thing written server-side is **billing metadata** — model
270
+ id, token counts, cost — never prompt or response text, and nothing is logged in plaintext.
271
+ - **Same privacy guarantee as the apps: ZDR / TEE, not in-transit E2EE.** The server has to
272
+ read your prompt to run inference (true of every product, every path). The guarantee isn't
273
+ "nobody sees it" — it's "nobody *retains* it": OpenRouter routes are pinned to zero-retention
274
+ endpoints, and the account default is a **NEAR TEE** model where even the provider can't read
275
+ the prompt. Run **`/verify`** to check the live attestation — a pragmatic freshness + presence
276
+ check (signing key + hardware markers + your nonce echoed); for full cryptographic validation
277
+ of the raw quote chains, take the printed report to the NEAR AI Cloud Verifier (see above).
278
+ - **Your local transcript is plaintext on your machine.** Privateer's end-to-end encryption
279
+ protects data **at rest in Privateer's storage** — it does not (and cannot) encrypt the
280
+ conversation files this CLI keeps on your own disk under `~/.privateer/`. Treat them like any
281
+ local shell history.
282
+ - **Your session token is stored unencrypted on disk** at `~/.privateer/credentials.json`,
283
+ protected only by file permissions (`0600` — readable just by your user). It's a scoped
284
+ session — not your password or wallet/encryption keys — and it's long-lived: anyone who can
285
+ read that file (root, a backup, a compromised account) has a usable billed session until it's
286
+ revoked. It rotates on refresh with server-side reuse detection; revoke it any time from the
287
+ app (**Settings → Linked terminals**, which now lists individual terminals) or with **`/logout`**.
288
+ - **`/remote-access` streams this terminal's activity to your phone.** When you turn it on, the
289
+ app can drive this terminal: prompts come down, and the agent's replies **and tool input/output**
290
+ go up through the Privateer server relay so you can watch and approve actions remotely. Output is
291
+ size-truncated and run through a best-effort secret redactor before it leaves, but that's a safety
292
+ net, not a guarantee — terminal output can contain whatever a command prints. The relay is
293
+ live-only (nothing is archived) and carries no keys; the terminal label sent is a non-PII random
294
+ tag (no username/host/path). It's **off** until you run `/remote-access on`.
295
+
296
+ > **Only approve a code you generated yourself.** The login code authorizes *this* terminal to
297
+ > spend on your account. If someone sends you a code and asks you to approve it ("paste this to
298
+ > activate…"), **don't** — approving it hands *them* a billed session on *your* account. A code
299
+ > you didn't just create in your own terminal is an attack, not a convenience.
300
+
301
+ ## Usage
302
+
303
+ ```bash
304
+ privateer # interactive TUI with the default model
305
+ privateer -m openrouter:anthropic/claude-opus-4.8
306
+ privateer -c # resume the last session in this dir
307
+ privateer -p "summarize src/" # headless one-shot, prints to stdout
308
+ ```
309
+
310
+ Run `/model` (no argument) to browse the models each configured provider actually
311
+ offers — the list is fetched live using the API key you entered, then filtered as you
312
+ type. Onboarding ends on the same picker so you choose your default model up front.
313
+
314
+ You can also pass a model string directly as `provider:model`:
315
+
316
+ | Example | |
317
+ |---|---|
318
+ | `openrouter:anthropic/claude-opus-4.8` | any model on OpenRouter |
319
+ | `anthropic:claude-opus-4-8` | direct Anthropic |
320
+ | `openai:gpt-5.5` | direct OpenAI |
321
+ | `ollama:qwen3-coder` | local model |
322
+
323
+ ## The prompt
324
+
325
+ The input is modal — the first character chooses what happens:
326
+
327
+ | Prefix | Mode |
328
+ |---|---|
329
+ | _(text)_ | a normal prompt to the model |
330
+ | `/` | a slash command — opens an autocomplete menu |
331
+ | `@` | a file mention — fuzzy-completes paths from the cwd |
332
+ | `!` | run a shell command locally and show its output (no model turn) |
333
+ | `#` | append the rest of the line to `PRIVATEER.md` |
334
+
335
+ Also: **↑/↓** history, **ctrl-r** reverse history search, emacs line editing
336
+ (`ctrl-a/e/u/w`), `ctrl-l` to clear the screen, and **`\`+Enter** for a newline. Messages
337
+ typed while the agent is busy are queued and run in order. `/vim` toggles modal (vim)
338
+ editing. Reference an image file to attach it for vision-capable models — by `@`-mention
339
+ (`@screenshot.png`), or by pasting a path anywhere in the prompt. Absolute paths and paths
340
+ with spaces work too, quoted (`"/Users/me/My Shot.png"`) or backslash-escaped
341
+ (`/Users/me/My\ Shot.png`); a leading `/path/...` is treated as a file, not a command. Each
342
+ referenced image collapses to a short `[Image #1]` chip in the transcript (numbered across
343
+ the session) while the picture itself rides along to the model.
344
+
345
+ While the agent is working, press **Esc** to interrupt the turn (partial output is kept);
346
+ **Ctrl-C** quits.
347
+
348
+ ## Slash commands
349
+
350
+ Built-ins (plus any custom commands you add):
351
+
352
+ | Command | |
353
+ |---|---|
354
+ | `/help` `/doctor` `/config` | help, diagnostics, resolved settings layers |
355
+ | `/model [spec]` `/provider` `/login` | choose a model, list providers, re-run onboarding |
356
+ | `/permissions [mode]` `/cost` `/context` | permission mode, token usage, context window |
357
+ | `/init` `/memory` | write/show `PRIVATEER.md` |
358
+ | `/agents` `/mcp [logout]` `/hooks` | inspect sub-agents; MCP status / clear OAuth; hooks |
359
+ | `/output-style [name]` `/vim` `/verbose` | persona, modal editing, full tool output |
360
+ | `/zdr` | toggle OpenRouter zero-data-retention enforcement (see [Data retention](#data-retention-zdr)) |
361
+ | `/verify` | fetch the NEAR AI TEE attestation for the current model (see [Private inference](#private-inference-near-ai)) |
362
+ | `/rewind` `/compact` `/clear` `/export` | restore a checkpoint, compact, clear, save transcript |
363
+ | `/exit` | quit |
364
+
365
+ - `/model` — open a picker of each provider's live models (or `/model provider:id` to set one directly).
366
+ - `/init` — the agent explores the repo and writes a `PRIVATEER.md` for you
367
+ (`/init --stub` just drops an empty template, no model call).
368
+ - `/rewind` — pick an earlier checkpoint and restore the conversation, the files, or both.
369
+ - `/compact` — summarize older history to reclaim context (also happens automatically).
370
+
371
+ ## Tools
372
+
373
+ `read` · `write` · `edit` · `glob` · `grep` · `bash` · `bash_output` · `kill_shell` ·
374
+ `todo` · `task` · `web_fetch` · `web_search` — plus any tools exposed by connected MCP servers.
375
+
376
+ The file/search/shell tools are pure-Node (no external binaries required). Mutating tools
377
+ (write/edit/bash) and network tools (web_fetch/web_search, MCP) go through the permission gate.
378
+ `todo` maintains the live task list; `task` delegates an investigation to a sub-agent that
379
+ returns a summary. `bash` can run detached with `run_in_background`; `bash_output` polls a
380
+ background shell's new output and `kill_shell` stops it.
381
+
382
+ ## Customize & extend
383
+
384
+ Everything below is optional and lives under `.privateer/` (project) or `~/.privateer/`
385
+ (user); project files win. Settings merge across `config.json` → `settings.json` →
386
+ `settings.local.json` (run `/config` to see the resolved chain).
387
+
388
+ - **Custom commands** — `.privateer/commands/<name>.md`. The body is a prompt template
389
+ (`$ARGUMENTS`, `$1`…`$9`); optional frontmatter sets `description`/`argument-hint`. They
390
+ appear in `/help` and `/` autocomplete; subfolders namespace as `dir:name`.
391
+ - **Output styles** — `.privateer/output-styles/<name>.md` swap the agent's persona.
392
+ Switch with `/output-style <name>` (or `default`).
393
+ - **Sub-agents** — `.privateer/agents/<name>.md` with frontmatter (`description`, `tools`,
394
+ `model`). Invoke via the `task` tool's `subagent_type`; `/agents` lists them.
395
+ - **Hooks** — a `hooks` section in `settings.json` runs shell commands on `PreToolUse`,
396
+ `PostToolUse`, `UserPromptSubmit`, and `Stop`. A hook blocks by exiting `2` or printing
397
+ `{"decision":"block"}`; `UserPromptSubmit` can inject `additionalContext`. `/hooks` lists them.
398
+ - **MCP servers** — declare them in `.privateer/mcp.json` (`{ "mcpServers": { … } }`).
399
+ Both **local stdio** servers (`{ "command", "args", "env" }`) and **remote HTTP** servers
400
+ (`{ "url", "headers?", "transport?" }`) are supported; remote defaults to Streamable HTTP
401
+ with a fallback to legacy SSE. Their tools are namespaced `server__tool` and gated like the
402
+ rest. Remote servers authenticate by a static `headers` bearer token, or — when none is set —
403
+ via **interactive OAuth** (PKCE + dynamic client registration): on a `401` Privateer opens your
404
+ browser, catches the redirect on a loopback port, and caches the tokens (owner-only) under
405
+ `~/.privateer/mcp-auth/`. `/mcp` shows each server's connection and auth state;
406
+ `/mcp logout [server]` clears saved OAuth.
407
+
408
+ ```json
409
+ {
410
+ "mcpServers": {
411
+ "fs": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."] },
412
+ "github": { "url": "https://api.githubcopilot.com/mcp/" },
413
+ "internal": { "url": "https://mcp.example.com/mcp", "headers": { "Authorization": "Bearer $TOKEN" } }
414
+ }
415
+ }
416
+ ```
417
+ - **Status line** — set `statusLine` to a shell command; it receives session JSON on stdin
418
+ and its stdout becomes the status line.
419
+
420
+ ## Permission modes
421
+
422
+ | Mode | Behavior |
423
+ |---|---|
424
+ | `default` | prompt before edits and shell commands |
425
+ | `acceptEdits` | auto-approve file edits; still prompt for other shell commands (the default) |
426
+ | `bypass` | no prompts (also `--dangerously-skip-permissions` or `--no-quarter`) |
427
+ | `plan` | read-only; the agent presents a plan, then you approve to leave plan mode |
428
+
429
+ At an approval prompt: **y** allow once · **a** always · **n** deny. In plan mode, after the
430
+ agent presents its plan: **a** approve and exit plan mode · **k** keep planning.
431
+
432
+ ## Project context
433
+
434
+ Create a `PRIVATEER.md` in your repo (via `/init`) to give the agent standing
435
+ context — conventions, architecture notes, anything it should always know.
436
+
437
+ ## Develop
438
+
439
+ ```bash
440
+ npm run typecheck
441
+ npm test
442
+ ```
443
+
444
+ ## Caveats
445
+
446
+ Privateer's agent core is built provider-agnostic from the ground up; a few areas are
447
+ deliberately simplified for now:
448
+
449
+ - **Prompt caching and extended thinking are Anthropic-only.** Ephemeral cache breakpoints
450
+ and the `thinkingBudget` setting apply to direct Anthropic models and OpenRouter routes to
451
+ `anthropic/*`. Other providers ignore them (a harmless no-op).
452
+ - **Checkpoints are in-memory.** `/rewind` is a within-session undo; snapshots live in memory
453
+ and aren't persisted across restarts.
454
+ - **Remote MCP OAuth uses a fixed loopback port** (`7777` by default; override with
455
+ `PRIVATEER_OAUTH_PORT`) so the redirect URI stays stable across runs. Set `PRIVATEER_NO_BROWSER=1`
456
+ in headless environments to skip the auto-launch and use the printed URL.
457
+ - **Image attachment assumes vision support.** Referenced images are sent as content parts;
458
+ non-vision models will return an error.
459
+ - **Compaction estimates context size** (~4 chars/token). The summary itself is
460
+ schema-guided (goals / decisions / files / open threads), with a plain-text fallback, and
461
+ the most recent messages are always kept verbatim.
462
+ - **`web_search` scrapes DuckDuckGo's keyless HTML endpoint.** It needs no API key but is
463
+ best-effort and can break if their markup changes. `web_fetch` is robust for known URLs.
464
+ - **Protected files** (`.env`, `.npmrc`, shell rc files, …) always prompt before edit — except
465
+ in `bypass` mode, which by definition skips all prompts.
466
+
467
+ ## Docs
468
+
469
+ - [Architecture](docs/ARCHITECTURE.md) — how the provider layer, agent loop, tools, and permissions fit together
470
+ - [Brand assets](brand/README.md) — the logo and icon set
471
+
472
+ ## License
473
+
474
+ [MIT](LICENSE) © Patrick
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // Privateer launcher. Registers tsx's ESM loader so the TypeScript/JSX
3
+ // entrypoint runs with no build step. (Phase 6 can add a compiled binary path.)
4
+ import { fileURLToPath } from "node:url";
5
+ import { dirname, resolve } from "node:path";
6
+ import { register } from "tsx/esm/api";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+
10
+ register();
11
+ await import(resolve(__dirname, "../src/main.tsx"));
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "privateer-agent",
3
+ "version": "0.1.0",
4
+ "description": "Privateer — a provider-agnostic terminal coding agent. Bring your own model (OpenRouter, Anthropic, OpenAI, Ollama, NEAR AI).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Patrick (zahnno)",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/privateer-agent/privateer-agent.git"
11
+ },
12
+ "keywords": [
13
+ "ai",
14
+ "coding-agent",
15
+ "cli",
16
+ "tui",
17
+ "terminal",
18
+ "openrouter",
19
+ "anthropic",
20
+ "openai",
21
+ "ollama",
22
+ "nearai",
23
+ "tee",
24
+ "confidential-computing",
25
+ "ai-sdk",
26
+ "ink"
27
+ ],
28
+ "bin": {
29
+ "privateer": "./bin/privateer.mjs"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "files": [
35
+ "bin",
36
+ "src",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "start": "tsx src/main.tsx",
42
+ "dev": "tsx watch src/main.tsx",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "for f in tests/*.test.ts; do node --import tsx --test \"$f\" || exit 1; done"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "dependencies": {
50
+ "@ai-sdk/anthropic": "^2.0.0",
51
+ "@ai-sdk/openai": "^2.0.0",
52
+ "@modelcontextprotocol/sdk": "^1.29.0",
53
+ "@openrouter/ai-sdk-provider": "^1.0.0",
54
+ "@types/picomatch": "^4.0.3",
55
+ "ai": "^5.0.0",
56
+ "commander": "^12.0.0",
57
+ "ink": "^5.0.0",
58
+ "ink-spinner": "^5.0.0",
59
+ "ink-text-input": "^6.0.0",
60
+ "ollama-ai-provider-v2": "^1.2.0",
61
+ "picomatch": "^4.0.4",
62
+ "react": "^18.3.1",
63
+ "tsx": "^4.16.0",
64
+ "ws": "^8.21.0",
65
+ "zod": "^4.0.16"
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "^20.14.0",
69
+ "@types/react": "^18.3.3",
70
+ "@types/ws": "^8.18.1",
71
+ "ink-testing-library": "^4.0.0",
72
+ "typescript": "^5.5.0"
73
+ }
74
+ }
@@ -0,0 +1,49 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { globalPaths, projectPaths } from "../config/paths.ts";
4
+ import { walkFiles } from "../tools/walk.ts";
5
+ import { parseFrontmatter } from "../commands/custom.ts";
6
+
7
+ // A user-defined sub-agent loaded from .privateer/agents/<name>.md. The body is the
8
+ // agent's instructions (system prompt); frontmatter narrows its tools and model.
9
+ export interface AgentDefinition {
10
+ name: string;
11
+ description: string;
12
+ tools?: string[]; // tool names the agent may use (default: read/glob/grep)
13
+ model?: string; // optional "provider:model" override
14
+ prompt: string; // the agent's instructions
15
+ scope: "project" | "user";
16
+ }
17
+
18
+ function loadFromDir(dir: string, scope: "project" | "user"): AgentDefinition[] {
19
+ if (!existsSync(dir)) return [];
20
+ const out: AgentDefinition[] = [];
21
+ for (const rel of walkFiles(dir)) {
22
+ if (!rel.endsWith(".md")) continue;
23
+ const { meta, body } = parseFrontmatter(readFileSync(join(dir, rel), "utf8"));
24
+ out.push({
25
+ name: meta.name || rel.replace(/\.md$/, "").split("/").join(":"),
26
+ description: meta.description ?? `custom ${scope} sub-agent`,
27
+ tools: meta.tools
28
+ ?.split(",")
29
+ .map((s) => s.trim())
30
+ .filter(Boolean),
31
+ model: meta.model,
32
+ prompt: body.trim(),
33
+ scope,
34
+ });
35
+ }
36
+ return out;
37
+ }
38
+
39
+ // User (~/.privateer) then project (./.privateer) agents; project overrides user.
40
+ export function loadAgents(cwd: string = process.cwd()): AgentDefinition[] {
41
+ const byName = new Map<string, AgentDefinition>();
42
+ for (const a of loadFromDir(globalPaths().agents, "user")) byName.set(a.name, a);
43
+ for (const a of loadFromDir(projectPaths(cwd).agents, "project")) byName.set(a.name, a);
44
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
45
+ }
46
+
47
+ export function findAgent(name: string, cwd: string = process.cwd()): AgentDefinition | undefined {
48
+ return loadAgents(cwd).find((a) => a.name === name);
49
+ }