privateer-agent 0.1.1 → 0.3.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 (106) hide show
  1. package/README.md +143 -398
  2. package/bin/privateer-tui +86 -0
  3. package/bin/privateer.mjs +17 -6
  4. package/bin/pv +28 -0
  5. package/package.json +26 -23
  6. package/src/auth/privateer.ts +128 -1
  7. package/src/boot.ts +43 -0
  8. package/src/bridge/engineAdapter.ts +182 -0
  9. package/src/cli/chat.ts +243 -0
  10. package/src/config/paths.ts +24 -44
  11. package/src/config/permissionMode.ts +5 -0
  12. package/src/crypto/outboxSeal.ts +61 -0
  13. package/src/daemon/index.ts +405 -0
  14. package/src/daemon/ipc.ts +127 -0
  15. package/src/engine/errors.ts +95 -45
  16. package/src/engine/router.ts +11 -165
  17. package/src/ext/permissionGate.ts +216 -0
  18. package/src/main.ts +32 -0
  19. package/src/permissions/classify.ts +172 -0
  20. package/src/permissions/gate.ts +13 -11
  21. package/src/permissions/mode.ts +9 -1
  22. package/src/permissions/{uiGate.ts → modeGate.ts} +22 -6
  23. package/src/providers/account.ts +170 -0
  24. package/src/providers/catalog.ts +73 -61
  25. package/src/providers/genModelsJson.ts +97 -0
  26. package/src/remote/relayClient.ts +91 -4
  27. package/src/remote/remoteBridge.ts +152 -0
  28. package/src/routines/cron.ts +109 -0
  29. package/src/routines/delivery.ts +184 -0
  30. package/src/routines/schema.ts +84 -0
  31. package/src/routines/store.ts +248 -0
  32. package/src/routines/toolSelect.ts +50 -0
  33. package/src/routines/trigger.ts +41 -0
  34. package/src/session.ts +84 -251
  35. package/src/tools/routine.ts +129 -0
  36. package/src/tools/saveAttachment.ts +39 -42
  37. package/src/tools/sendFile.ts +75 -0
  38. package/src/util/attachmentStore.ts +18 -35
  39. package/src/util/redact.ts +33 -3
  40. package/LICENSE +0 -21
  41. package/src/agents/loader.ts +0 -49
  42. package/src/commands/custom.ts +0 -75
  43. package/src/commands/registry.ts +0 -499
  44. package/src/components/AgentGroupView.tsx +0 -104
  45. package/src/components/App.tsx +0 -1420
  46. package/src/components/ApprovalPrompt.tsx +0 -38
  47. package/src/components/Banner.tsx +0 -76
  48. package/src/components/Markdown.tsx +0 -183
  49. package/src/components/ModeHint.tsx +0 -40
  50. package/src/components/ModelPicker.tsx +0 -269
  51. package/src/components/Onboarding.tsx +0 -203
  52. package/src/components/PlanConfirm.tsx +0 -37
  53. package/src/components/PrivateerLogin.tsx +0 -109
  54. package/src/components/PromptInput.tsx +0 -602
  55. package/src/components/RewindPicker.tsx +0 -69
  56. package/src/components/Root.tsx +0 -95
  57. package/src/components/SessionPicker.tsx +0 -64
  58. package/src/components/StatusBar.tsx +0 -131
  59. package/src/components/TodoPanel.tsx +0 -36
  60. package/src/components/ToolCallView.tsx +0 -109
  61. package/src/components/Transcript.tsx +0 -203
  62. package/src/components/figures.ts +0 -14
  63. package/src/components/promptModel.ts +0 -73
  64. package/src/components/spinnerVerbs.ts +0 -46
  65. package/src/components/theme.ts +0 -55
  66. package/src/components/types.ts +0 -34
  67. package/src/components/useTeeShield.ts +0 -104
  68. package/src/components/useTerminalWidth.ts +0 -24
  69. package/src/components/useZdrShield.ts +0 -126
  70. package/src/config/load.ts +0 -115
  71. package/src/config/schema.ts +0 -94
  72. package/src/context/outputStyles.ts +0 -42
  73. package/src/context/projectInfo.ts +0 -59
  74. package/src/context/systemPrompt.ts +0 -167
  75. package/src/engine/QueryEngine.ts +0 -399
  76. package/src/hooks/engine.ts +0 -155
  77. package/src/main.tsx +0 -167
  78. package/src/mcp/client.ts +0 -236
  79. package/src/mcp/oauth.ts +0 -245
  80. package/src/memory/auto.ts +0 -146
  81. package/src/memory/checkpoints.ts +0 -227
  82. package/src/memory/store.ts +0 -127
  83. package/src/providers/attestation.ts +0 -149
  84. package/src/providers/capabilities.ts +0 -104
  85. package/src/providers/models.ts +0 -183
  86. package/src/providers/registry.ts +0 -71
  87. package/src/providers/resolve.ts +0 -78
  88. package/src/tools/bash.ts +0 -98
  89. package/src/tools/context.ts +0 -114
  90. package/src/tools/edit.ts +0 -67
  91. package/src/tools/exec.ts +0 -60
  92. package/src/tools/glob.ts +0 -39
  93. package/src/tools/grep.ts +0 -86
  94. package/src/tools/index.ts +0 -69
  95. package/src/tools/memory.ts +0 -53
  96. package/src/tools/processRegistry.ts +0 -77
  97. package/src/tools/read.ts +0 -42
  98. package/src/tools/task.ts +0 -52
  99. package/src/tools/todo.ts +0 -36
  100. package/src/tools/todoStore.ts +0 -31
  101. package/src/tools/walk.ts +0 -44
  102. package/src/tools/web.ts +0 -145
  103. package/src/tools/write.ts +0 -40
  104. package/src/util/images.ts +0 -356
  105. package/src/util/limit.ts +0 -32
  106. package/src/version.ts +0 -13
@@ -1,356 +0,0 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { isAbsolute, join, extname, basename } from "node:path";
3
-
4
- // The model-input modalities Privateer can attach and route on. `text` files are not
5
- // a modality here — they're inlined as plain text (see resolveAttachments), never
6
- // routed — so this union only covers the binary kinds that need a capable model.
7
- export type Modality = "image" | "document" | "audio" | "video";
8
-
9
- // Extension → { mediaType, modality }. Drives both detection and the media type we
10
- // hand the provider. Inferred from the extension alone (no magic-byte sniffing).
11
- const MEDIA_TYPES: Record<string, { mediaType: string; modality: Modality }> = {
12
- ".png": { mediaType: "image/png", modality: "image" },
13
- ".jpg": { mediaType: "image/jpeg", modality: "image" },
14
- ".jpeg": { mediaType: "image/jpeg", modality: "image" },
15
- ".gif": { mediaType: "image/gif", modality: "image" },
16
- ".webp": { mediaType: "image/webp", modality: "image" },
17
- ".pdf": { mediaType: "application/pdf", modality: "document" },
18
- ".mp3": { mediaType: "audio/mpeg", modality: "audio" },
19
- ".wav": { mediaType: "audio/wav", modality: "audio" },
20
- ".m4a": { mediaType: "audio/mp4", modality: "audio" },
21
- ".ogg": { mediaType: "audio/ogg", modality: "audio" },
22
- ".flac": { mediaType: "audio/flac", modality: "audio" },
23
- ".mp4": { mediaType: "video/mp4", modality: "video" },
24
- ".mov": { mediaType: "video/quicktime", modality: "video" },
25
- ".webm": { mediaType: "video/webm", modality: "video" },
26
- ".mkv": { mediaType: "video/x-matroska", modality: "video" },
27
- };
28
-
29
- // Classify a media type into one of our binary modalities, or null when it's a
30
- // text-like file that should be inlined as plain text rather than attached. Used by
31
- // the relay path (App.tsx) to decide what to do with a file received from the app:
32
- // a null result means "decode and inline the text"; otherwise it's a binary
33
- // attachment the model reads directly.
34
- export function mediaModality(mediaType: string): Modality | null {
35
- if (mediaType.startsWith("image/")) return "image";
36
- if (mediaType === "application/pdf") return "document";
37
- if (mediaType.startsWith("audio/")) return "audio";
38
- if (mediaType.startsWith("video/")) return "video";
39
- return null;
40
- }
41
-
42
- // Magic-byte checks per media type, used to reject placeholder/corrupt files at capture
43
- // time. The motivating case: macOS delivers a drag from a screenshot thumbnail as a
44
- // *file promise*, so the terminal's …/T/drop-XXXXXX/ file can be a 4-byte stub holding
45
- // only the start of the PNG signature (0x89 'P' 'N' 'G') — never the real bytes. Reading
46
- // that into an attachment yields a broken "[Image #n]" the model can't use, so we drop
47
- // it here and leave the raw path in the buffer (a visible signal the capture failed).
48
- const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
49
- const MAGIC: Record<string, (b: Buffer) => boolean> = {
50
- "image/png": (b) => b.length >= 8 && b.subarray(0, 8).equals(PNG_SIG),
51
- "image/jpeg": (b) => b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff,
52
- "image/gif": (b) => b.length >= 6 && /^GIF8[79]a$/.test(b.toString("latin1", 0, 6)),
53
- "image/webp": (b) =>
54
- b.length >= 12 && b.toString("latin1", 0, 4) === "RIFF" && b.toString("latin1", 8, 12) === "WEBP",
55
- "application/pdf": (b) => b.length >= 5 && b.toString("latin1", 0, 5) === "%PDF-",
56
- };
57
-
58
- // True when `buf` plausibly holds a real file of `mediaType`. For types with a known
59
- // signature we check it; for the rest (audio/video containers vary too much to sniff
60
- // cheaply) we only reject an empty buffer. The aim is to catch promise stubs and
61
- // zero-byte drops, not to fully validate the format.
62
- export function validateBytes(buf: Buffer, mediaType: string): boolean {
63
- const check = MAGIC[mediaType];
64
- return check ? check(buf) : buf.length > 0;
65
- }
66
-
67
- export interface ImageDims {
68
- w: number;
69
- h: number;
70
- }
71
-
72
- // Pull pixel dimensions straight from an image's header, or null when the format
73
- // isn't one we parse (or the header is too short). Cheap header reads only — no
74
- // decode. This drives the provenance shown at drop time so a wrong-but-complete
75
- // capture (e.g. a stale 1412×496 banner where you meant a full-height screenshot)
76
- // is visible before the prompt is sent. Validation already rejected truncated stubs.
77
- export function readImageSize(buf: Buffer, mediaType: string): ImageDims | null {
78
- try {
79
- if (mediaType === "image/png") {
80
- // 8-byte sig + 4-byte length + "IHDR" → width@16, height@20, big-endian.
81
- if (buf.length < 24) return null;
82
- return { w: buf.readUInt32BE(16), h: buf.readUInt32BE(20) };
83
- }
84
- if (mediaType === "image/gif") {
85
- if (buf.length < 10) return null;
86
- return { w: buf.readUInt16LE(6), h: buf.readUInt16LE(8) };
87
- }
88
- if (mediaType === "image/jpeg") {
89
- // Walk segments to the first Start-Of-Frame marker, which carries the size.
90
- let off = 2; // skip SOI (0xFFD8)
91
- while (off + 9 < buf.length) {
92
- if (buf[off] !== 0xff) {
93
- off++;
94
- continue;
95
- }
96
- const marker = buf[off + 1];
97
- // SOF0–SOF15 hold the frame size; skip DHT(C4)/JPG(C8)/DAC(CC) which share the range.
98
- if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
99
- return { w: buf.readUInt16BE(off + 7), h: buf.readUInt16BE(off + 5) };
100
- }
101
- const segLen = buf.readUInt16BE(off + 2);
102
- if (segLen < 2) return null; // malformed → give up
103
- off += 2 + segLen;
104
- }
105
- return null;
106
- }
107
- } catch {
108
- return null; // short/corrupt header → no dimensions, not fatal
109
- }
110
- return null;
111
- }
112
-
113
- // Text/code/data files: read-as-text (inlined into the prompt), never attached as a
114
- // binary or routed. Anything not here and not in MEDIA_TYPES is left as literal text.
115
- const TEXT_EXTS = new Set([
116
- ".txt", ".md", ".markdown", ".csv", ".tsv", ".json", ".jsonl", ".yaml", ".yml", ".toml",
117
- ".ini", ".env", ".xml", ".html", ".css", ".js", ".jsx", ".ts", ".tsx", ".py", ".rb",
118
- ".go", ".rs", ".java", ".c", ".h", ".cpp", ".hpp", ".cs", ".php", ".sh", ".bash", ".zsh",
119
- ".sql", ".log", ".diff", ".patch", ".lua", ".swift", ".kt", ".scala", ".r", ".pl",
120
- ]);
121
-
122
- // Upper bound on an inlined text file (bytes). Larger files are left as a path token
123
- // for the agent's read tool rather than dumping the whole thing into the prompt.
124
- const DEFAULT_INLINE_MAX_BYTES = 65_536;
125
-
126
- export interface Attachment {
127
- data: string; // base64-encoded file content
128
- mediaType: string;
129
- modality: Modality;
130
- path: string; // the token as written, for display
131
- n?: number; // session reference number, when resolved as a "[Kind #n]" chip
132
- bytes?: number; // decoded size, for the drop-time provenance line
133
- dims?: ImageDims | null; // pixel dimensions for images we can parse, else null
134
- }
135
-
136
- // Back-compat alias: callers that predate multimodal still import AttachedImage.
137
- export type AttachedImage = Attachment;
138
-
139
- // The chip a resolved attachment collapses to in the prompt/transcript. The kind
140
- // label is derived from the modality so the user (and the model) can tell them apart.
141
- const CHIP_LABEL: Record<Modality, string> = {
142
- image: "Image",
143
- document: "PDF",
144
- audio: "Audio",
145
- video: "Video",
146
- };
147
- export function chipFor(att: Pick<Attachment, "modality" | "n">): string {
148
- return `[${CHIP_LABEL[att.modality]} #${att.n}]`;
149
- }
150
-
151
- // Compact, human size: "217 KB", "3.4 MB", "812 B". For provenance lines, not exactness.
152
- function formatBytes(n: number): string {
153
- if (n < 1024) return `${n} B`;
154
- if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`;
155
- return `${(n / (1024 * 1024)).toFixed(1)} MB`;
156
- }
157
-
158
- // A one-line "what did we actually capture" description for a staged attachment:
159
- // "screenshot.png · 1412×496 · 217 KB". Filename + dimensions + size are exactly the
160
- // signals that expose a wrong-file drop before the prompt is sent.
161
- export function describeAttachment(att: Attachment): string {
162
- const parts = [basename(att.path)];
163
- if (att.dims) parts.push(`${att.dims.w}×${att.dims.h}`);
164
- if (att.bytes != null) parts.push(formatBytes(att.bytes));
165
- return parts.join(" · ");
166
- }
167
-
168
- // A path-like token and the slice of the original text it occupies, so callers can
169
- // substitute it in place (e.g. with a chip) without re-matching the quoted/escaped
170
- // raw form.
171
- interface Span {
172
- value: string; // unescaped/unquoted token text
173
- start: number; // inclusive index into the original string
174
- end: number; // exclusive index into the original string
175
- }
176
-
177
- // Split text into path-like spans, honoring the shell-style quoting people reach
178
- // for when a path contains spaces: '"a b.png"', "'a b.png'", and backslash escapes
179
- // ("a\ b.png"). Without this, a pasted path like
180
- // /Users/me/Screenshot\ 2026.png
181
- // would shatter on whitespace and never match a real file.
182
- function tokenizeSpans(text: string): Span[] {
183
- const spans: Span[] = [];
184
- let cur = "";
185
- let quote: '"' | "'" | null = null;
186
- let started = false; // distinguishes an empty quoted token from no token
187
- let tokenStart = 0;
188
- const flush = (end: number) => {
189
- if (started) spans.push({ value: cur, start: tokenStart, end });
190
- cur = "";
191
- started = false;
192
- };
193
- const begin = (i: number) => {
194
- if (!started) {
195
- tokenStart = i;
196
- started = true;
197
- }
198
- };
199
- for (let i = 0; i < text.length; i++) {
200
- const ch = text[i];
201
- if (quote) {
202
- if (ch === quote) quote = null;
203
- else cur += ch;
204
- } else if (ch === '"' || ch === "'") {
205
- begin(i);
206
- quote = ch;
207
- } else if (ch === "\\" && i + 1 < text.length) {
208
- begin(i);
209
- cur += text[++i]; // escaped char joins the token literally
210
- } else if (/\s/.test(ch)) {
211
- flush(i);
212
- } else {
213
- begin(i);
214
- cur += ch;
215
- }
216
- }
217
- flush(text.length);
218
- return spans;
219
- }
220
-
221
- function resolvePath(token: string, cwd: string): string {
222
- return isAbsolute(token) ? token : join(cwd, token);
223
- }
224
-
225
- // Read the binary-modality file behind a path-like token (image/document/audio/video),
226
- // or null when the token isn't such a file or can't be read. Capability of the target
227
- // model is the router's concern, not this function's.
228
- function readAttachment(
229
- token: string,
230
- cwd: string,
231
- ): { data: string; mediaType: string; modality: Modality; abs: string; bytes: number; dims: ImageDims | null } | null {
232
- const meta = MEDIA_TYPES[extname(token).toLowerCase()];
233
- if (!meta) return null;
234
- const abs = resolvePath(token, cwd);
235
- if (!existsSync(abs)) return null;
236
- try {
237
- const buf = readFileSync(abs);
238
- if (!validateBytes(buf, meta.mediaType)) return null; // promise stub / corrupt → skip
239
- const dims = meta.modality === "image" ? readImageSize(buf, meta.mediaType) : null;
240
- return { data: buf.toString("base64"), ...meta, abs, bytes: buf.length, dims };
241
- } catch {
242
- return null; // unreadable → skip
243
- }
244
- }
245
-
246
- // Find binary-modality file paths referenced in a prompt (optionally as @mentions) and
247
- // read them as base64 so they can be attached to the model message. Handles quoted and
248
- // backslash-escaped paths with spaces. Tokens that don't resolve to a readable
249
- // image/document/audio/video file are ignored.
250
- export function extractAttachments(text: string, cwd: string): Attachment[] {
251
- const out: Attachment[] = [];
252
- const seen = new Set<string>();
253
- for (const span of tokenizeSpans(text)) {
254
- const token = span.value.replace(/^@/, "");
255
- const att = readAttachment(token, cwd);
256
- if (!att || seen.has(att.abs)) continue;
257
- out.push({
258
- data: att.data,
259
- mediaType: att.mediaType,
260
- modality: att.modality,
261
- path: token,
262
- bytes: att.bytes,
263
- dims: att.dims,
264
- });
265
- seen.add(att.abs);
266
- }
267
- return out;
268
- }
269
-
270
- // Back-compat: the old image-only entry point.
271
- export const extractImages = extractAttachments;
272
-
273
- // Read a text/code/data file for inlining, or null when it isn't a text file, doesn't
274
- // exist, or exceeds the size cap (left for the agent's read tool instead).
275
- function readTextFile(token: string, cwd: string, maxBytes: number): string | null {
276
- if (!TEXT_EXTS.has(extname(token).toLowerCase())) return null;
277
- const abs = resolvePath(token, cwd);
278
- if (!existsSync(abs)) return null;
279
- try {
280
- if (statSync(abs).size > maxBytes) return null; // too big → leave the path alone
281
- return readFileSync(abs, "utf8");
282
- } catch {
283
- return null;
284
- }
285
- }
286
-
287
- export interface ResolvedAttachments {
288
- text: string; // prompt with binary paths rewritten to chips and text paths to [file: …]
289
- attachments: Attachment[]; // image/document/audio/video, each carrying its [Kind #n]
290
- inlinedText: string; // concatenated contents of any read-as-text files
291
- }
292
-
293
- // Rewrite a prompt's referenced files in place: binary-modality paths become stable
294
- // "[Kind #n]" chips (and their base64 is collected as attachments), and recognized
295
- // text/code files are inlined — their path replaced with "[file: name]" and their
296
- // contents appended to `inlinedText`. Chip numbers are assigned from `startSeq` and
297
- // shared across the session; the same file referenced twice reuses its number. Edits
298
- // are applied right-to-left so indices stay valid.
299
- export function resolveAttachments(
300
- text: string,
301
- cwd: string,
302
- startSeq: number,
303
- inlineMaxBytes: number = DEFAULT_INLINE_MAX_BYTES,
304
- ): ResolvedAttachments {
305
- const spans = tokenizeSpans(text);
306
- const byAbs = new Map<string, Attachment>(); // abs path → attachment (dedupe)
307
- const attachments: Attachment[] = [];
308
- const inlinedParts: string[] = [];
309
- const inlinedSeen = new Set<string>();
310
- const edits: { start: number; end: number; replacement: string }[] = [];
311
- let seq = startSeq;
312
-
313
- for (const span of spans) {
314
- const token = span.value.replace(/^@/, "");
315
- const abs = resolvePath(token, cwd);
316
-
317
- const existing = byAbs.get(abs);
318
- if (existing) {
319
- edits.push({ start: span.start, end: span.end, replacement: chipFor(existing) });
320
- continue;
321
- }
322
-
323
- const att = readAttachment(token, cwd);
324
- if (att) {
325
- const resolved: Attachment = {
326
- data: att.data,
327
- mediaType: att.mediaType,
328
- modality: att.modality,
329
- path: token,
330
- n: ++seq,
331
- bytes: att.bytes,
332
- dims: att.dims,
333
- };
334
- byAbs.set(abs, resolved);
335
- attachments.push(resolved);
336
- edits.push({ start: span.start, end: span.end, replacement: chipFor(resolved) });
337
- continue;
338
- }
339
-
340
- const body = readTextFile(token, cwd, inlineMaxBytes);
341
- if (body !== null) {
342
- const name = basename(token);
343
- if (!inlinedSeen.has(abs)) {
344
- inlinedSeen.add(abs);
345
- inlinedParts.push(`--- ${name} ---\n${body}`);
346
- }
347
- edits.push({ start: span.start, end: span.end, replacement: `[file: ${name}]` });
348
- }
349
- }
350
-
351
- let out = text;
352
- for (const e of edits.sort((a, b) => b.start - a.start)) {
353
- out = out.slice(0, e.start) + e.replacement + out.slice(e.end);
354
- }
355
- return { text: out, attachments, inlinedText: inlinedParts.join("\n\n") };
356
- }
package/src/util/limit.ts DELETED
@@ -1,32 +0,0 @@
1
- // A minimal async concurrency limiter. `run(task)` resolves the task's value but
2
- // never lets more than `max` tasks run at once; the rest queue FIFO. Used to bound
3
- // how many `task` sub-agents execute in parallel when the model fans them out.
4
- export type Limiter = <T>(task: () => Promise<T>) => Promise<T>;
5
-
6
- export function createLimiter(max: number): Limiter {
7
- let active = 0;
8
- const queue: (() => void)[] = [];
9
-
10
- function release(): void {
11
- const next = queue.shift();
12
- if (next) next(); // hand the slot straight to the next waiter (active unchanged)
13
- else active--; // no waiter → free the slot
14
- }
15
-
16
- async function acquire(): Promise<void> {
17
- if (active < max) {
18
- active++;
19
- return;
20
- }
21
- await new Promise<void>((resolve) => queue.push(resolve)); // slot handed to us
22
- }
23
-
24
- return async function run<T>(task: () => Promise<T>): Promise<T> {
25
- await acquire();
26
- try {
27
- return await task();
28
- } finally {
29
- release();
30
- }
31
- };
32
- }
package/src/version.ts DELETED
@@ -1,13 +0,0 @@
1
- // Single source of truth for the app's name/version, read from package.json.
2
- import { readFileSync } from "node:fs";
3
- import { fileURLToPath } from "node:url";
4
- import { dirname, resolve } from "node:path";
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const pkg = JSON.parse(
8
- readFileSync(resolve(__dirname, "../package.json"), "utf8"),
9
- ) as { name: string; version: string; description: string };
10
-
11
- export const NAME = "privateer";
12
- export const VERSION = pkg.version;
13
- export const DESCRIPTION = pkg.description;