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