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.
- package/LICENSE +21 -0
- package/README.md +474 -0
- package/bin/privateer.mjs +11 -0
- package/package.json +74 -0
- package/src/agents/loader.ts +49 -0
- package/src/auth/privateer.ts +393 -0
- package/src/commands/custom.ts +75 -0
- package/src/commands/registry.ts +499 -0
- package/src/components/AgentGroupView.tsx +104 -0
- package/src/components/App.tsx +1376 -0
- package/src/components/ApprovalPrompt.tsx +38 -0
- package/src/components/Banner.tsx +58 -0
- package/src/components/Markdown.tsx +183 -0
- package/src/components/ModeHint.tsx +40 -0
- package/src/components/ModelPicker.tsx +269 -0
- package/src/components/Onboarding.tsx +203 -0
- package/src/components/PlanConfirm.tsx +37 -0
- package/src/components/PrivateerLogin.tsx +109 -0
- package/src/components/PromptInput.tsx +602 -0
- package/src/components/RewindPicker.tsx +69 -0
- package/src/components/Root.tsx +95 -0
- package/src/components/SessionPicker.tsx +64 -0
- package/src/components/StatusBar.tsx +121 -0
- package/src/components/TodoPanel.tsx +36 -0
- package/src/components/ToolCallView.tsx +109 -0
- package/src/components/Transcript.tsx +203 -0
- package/src/components/figures.ts +13 -0
- package/src/components/promptModel.ts +73 -0
- package/src/components/spinnerVerbs.ts +46 -0
- package/src/components/theme.ts +55 -0
- package/src/components/types.ts +34 -0
- package/src/components/useTeeShield.ts +104 -0
- package/src/components/useTerminalWidth.ts +24 -0
- package/src/components/useZdrShield.ts +126 -0
- package/src/config/load.ts +115 -0
- package/src/config/paths.ts +61 -0
- package/src/config/schema.ts +94 -0
- package/src/context/outputStyles.ts +42 -0
- package/src/context/projectInfo.ts +59 -0
- package/src/context/systemPrompt.ts +167 -0
- package/src/engine/QueryEngine.ts +399 -0
- package/src/engine/errors.ts +197 -0
- package/src/engine/events.ts +74 -0
- package/src/engine/router.ts +165 -0
- package/src/hooks/engine.ts +155 -0
- package/src/main.tsx +167 -0
- package/src/mcp/client.ts +236 -0
- package/src/mcp/oauth.ts +245 -0
- package/src/memory/auto.ts +146 -0
- package/src/memory/checkpoints.ts +227 -0
- package/src/memory/store.ts +127 -0
- package/src/permissions/danger.ts +56 -0
- package/src/permissions/gate.ts +38 -0
- package/src/permissions/mode.ts +39 -0
- package/src/permissions/protected.ts +29 -0
- package/src/permissions/uiGate.ts +73 -0
- package/src/providers/attestation.ts +149 -0
- package/src/providers/capabilities.ts +104 -0
- package/src/providers/catalog.ts +66 -0
- package/src/providers/models.ts +183 -0
- package/src/providers/registry.ts +71 -0
- package/src/providers/resolve.ts +78 -0
- package/src/remote/relayClient.ts +283 -0
- package/src/session.ts +264 -0
- package/src/tools/bash.ts +98 -0
- package/src/tools/context.ts +114 -0
- package/src/tools/edit.ts +67 -0
- package/src/tools/exec.ts +60 -0
- package/src/tools/glob.ts +39 -0
- package/src/tools/grep.ts +86 -0
- package/src/tools/index.ts +69 -0
- package/src/tools/memory.ts +53 -0
- package/src/tools/processRegistry.ts +77 -0
- package/src/tools/read.ts +42 -0
- package/src/tools/saveAttachment.ts +53 -0
- package/src/tools/task.ts +52 -0
- package/src/tools/todo.ts +36 -0
- package/src/tools/todoStore.ts +31 -0
- package/src/tools/walk.ts +44 -0
- package/src/tools/web.ts +145 -0
- package/src/tools/write.ts +40 -0
- package/src/util/attachmentStore.ts +72 -0
- package/src/util/images.ts +343 -0
- package/src/util/limit.ts +32 -0
- package/src/util/redact.ts +44 -0
- 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
|
+
}
|