@bankr/cli 0.2.13 → 0.2.15

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.
@@ -0,0 +1,501 @@
1
+ import { readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { CLI_USER_AGENT, getApiUrl, requireApiKey } from "../lib/config.js";
4
+ import * as output from "../lib/output.js";
5
+ async function readStdin() {
6
+ if (process.stdin.isTTY) {
7
+ throw new Error("No content provided. Pipe content via stdin or pass --from <file>.");
8
+ }
9
+ const chunks = [];
10
+ for await (const chunk of process.stdin)
11
+ chunks.push(chunk);
12
+ return Buffer.concat(chunks).toString("utf-8");
13
+ }
14
+ function authHeaders() {
15
+ return {
16
+ "X-API-Key": requireApiKey(),
17
+ "Content-Type": "application/json",
18
+ "User-Agent": CLI_USER_AGENT,
19
+ };
20
+ }
21
+ // ── List files ──────────────────────────────────────────────────────────
22
+ export async function filesListCommand(opts) {
23
+ const spinner = output.spinner("Fetching files...");
24
+ try {
25
+ const params = new URLSearchParams();
26
+ if (opts.folder)
27
+ params.set("folder", opts.folder);
28
+ const res = await fetch(`${getApiUrl()}/user/files?${params.toString()}`, {
29
+ headers: authHeaders(),
30
+ });
31
+ if (!res.ok) {
32
+ const body = await res.json().catch(() => ({ error: res.statusText }));
33
+ throw new Error(body.error || res.statusText);
34
+ }
35
+ const { files } = (await res.json());
36
+ spinner.stop();
37
+ if (files.length === 0) {
38
+ output.dim("No files found.");
39
+ return;
40
+ }
41
+ // Show folders first, then files
42
+ const folders = files.filter((f) => f.isFolder);
43
+ const regularFiles = files.filter((f) => !f.isFolder);
44
+ for (const f of folders) {
45
+ console.log(` ${output.fmt.brand("\u{1F4C2}")} ${output.fmt.brandBold(f.name + "/")} ${output.fmt.dim(f.folder)}`);
46
+ }
47
+ for (const f of regularFiles) {
48
+ const size = formatSize(f.sizeBytes);
49
+ const date = new Date(f.createdAt).toLocaleDateString();
50
+ console.log(` ${output.fmt.dim(f._id.slice(0, 8))} ${f.name} ${output.fmt.dim(size)} ${output.fmt.dim(date)} ${output.fmt.dim(f.folder)}`);
51
+ }
52
+ output.blank();
53
+ output.dim(`${regularFiles.length} file(s), ${folders.length} folder(s)`);
54
+ }
55
+ catch (err) {
56
+ spinner.stop();
57
+ output.error(err instanceof Error ? err.message : String(err));
58
+ process.exit(1);
59
+ }
60
+ }
61
+ // ── Upload file ─────────────────────────────────────────────────────────
62
+ export async function filesUploadCommand(filePath, opts) {
63
+ const spinner = output.spinner(`Uploading ${filePath}...`);
64
+ try {
65
+ // Read the file
66
+ const stat = statSync(filePath);
67
+ if (stat.size > 10 * 1024 * 1024) {
68
+ throw new Error("File exceeds 10MB limit");
69
+ }
70
+ const buffer = readFileSync(filePath);
71
+ const name = basename(filePath);
72
+ // Build multipart form data
73
+ const boundary = `----BankrCLI${Date.now()}`;
74
+ const folder = opts.folder || "/";
75
+ const parts = [];
76
+ // Folder field
77
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="folder"\r\n\r\n${folder}\r\n`));
78
+ // File field
79
+ const mimeType = inferMimeType(name);
80
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${name}"\r\nContent-Type: ${mimeType}\r\n\r\n`));
81
+ parts.push(buffer);
82
+ parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
83
+ const body = Buffer.concat(parts);
84
+ const res = await fetch(`${getApiUrl()}/user/files/upload`, {
85
+ method: "POST",
86
+ headers: {
87
+ "X-API-Key": requireApiKey(),
88
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
89
+ "User-Agent": CLI_USER_AGENT,
90
+ },
91
+ body,
92
+ });
93
+ if (!res.ok) {
94
+ const errBody = await res.json().catch(() => ({ error: res.statusText }));
95
+ throw new Error(errBody.error || res.statusText);
96
+ }
97
+ const result = (await res.json());
98
+ spinner.stop();
99
+ output.success(`Uploaded ${result.file.name} (${formatSize(result.file.sizeBytes)})`);
100
+ output.dim(`File ID: ${result.file._id}`);
101
+ output.dim(`Download: ${result.downloadUrl}`);
102
+ }
103
+ catch (err) {
104
+ spinner.stop();
105
+ output.error(err instanceof Error ? err.message : String(err));
106
+ process.exit(1);
107
+ }
108
+ }
109
+ // ── Download file ───────────────────────────────────────────────────────
110
+ export async function filesDownloadCommand(fileId) {
111
+ const spinner = output.spinner("Getting download URL...");
112
+ try {
113
+ const res = await fetch(`${getApiUrl()}/user/files/${fileId}/download`, {
114
+ headers: authHeaders(),
115
+ });
116
+ if (!res.ok) {
117
+ const body = await res.json().catch(() => ({ error: res.statusText }));
118
+ throw new Error(body.error || res.statusText);
119
+ }
120
+ const { downloadUrl } = (await res.json());
121
+ spinner.stop();
122
+ output.success("Download URL (valid for 15 minutes):");
123
+ console.log(downloadUrl);
124
+ }
125
+ catch (err) {
126
+ spinner.stop();
127
+ output.error(err instanceof Error ? err.message : String(err));
128
+ process.exit(1);
129
+ }
130
+ }
131
+ // ── Create folder ───────────────────────────────────────────────────────
132
+ export async function filesMkdirCommand(name, opts) {
133
+ const spinner = output.spinner(`Creating folder ${name}...`);
134
+ try {
135
+ const res = await fetch(`${getApiUrl()}/user/files/folder`, {
136
+ method: "POST",
137
+ headers: authHeaders(),
138
+ body: JSON.stringify({
139
+ name,
140
+ parentFolder: opts.parent || "/",
141
+ }),
142
+ });
143
+ if (!res.ok) {
144
+ const body = await res.json().catch(() => ({ error: res.statusText }));
145
+ throw new Error(body.error || res.statusText);
146
+ }
147
+ spinner.stop();
148
+ output.success(`Created folder: ${name}`);
149
+ }
150
+ catch (err) {
151
+ spinner.stop();
152
+ output.error(err instanceof Error ? err.message : String(err));
153
+ process.exit(1);
154
+ }
155
+ }
156
+ // ── Delete file ─────────────────────────────────────────────────────────
157
+ export async function filesRmCommand(fileId) {
158
+ const spinner = output.spinner("Deleting file...");
159
+ try {
160
+ const res = await fetch(`${getApiUrl()}/user/files/${fileId}`, {
161
+ method: "DELETE",
162
+ headers: authHeaders(),
163
+ });
164
+ if (!res.ok) {
165
+ const body = await res.json().catch(() => ({ error: res.statusText }));
166
+ throw new Error(body.error || res.statusText);
167
+ }
168
+ spinner.stop();
169
+ output.success("File deleted.");
170
+ }
171
+ catch (err) {
172
+ spinner.stop();
173
+ output.error(err instanceof Error ? err.message : String(err));
174
+ process.exit(1);
175
+ }
176
+ }
177
+ // ── Storage usage ───────────────────────────────────────────────────────
178
+ export async function filesStorageCommand() {
179
+ const spinner = output.spinner("Fetching storage usage...");
180
+ try {
181
+ const res = await fetch(`${getApiUrl()}/user/files/storage`, {
182
+ headers: authHeaders(),
183
+ });
184
+ if (!res.ok) {
185
+ const body = await res.json().catch(() => ({ error: res.statusText }));
186
+ throw new Error(body.error || res.statusText);
187
+ }
188
+ const usage = (await res.json());
189
+ spinner.stop();
190
+ const filePct = usage.files.quotaBytes > 0
191
+ ? ((usage.files.usedBytes / usage.files.quotaBytes) * 100).toFixed(1)
192
+ : "0";
193
+ output.label("Storage", `${formatSize(usage.files.usedBytes)} / ${formatSize(usage.files.quotaBytes)} (${filePct}%)`);
194
+ output.label("Files", `${usage.files.fileCount}`);
195
+ if (usage.cliCache) {
196
+ const cachePct = usage.cliCache.quotaBytes > 0
197
+ ? ((usage.cliCache.usedBytes / usage.cliCache.quotaBytes) *
198
+ 100).toFixed(1)
199
+ : "0";
200
+ output.label("CLI cache", `${formatSize(usage.cliCache.usedBytes)} / ${formatSize(usage.cliCache.quotaBytes)} (${cachePct}%)`);
201
+ }
202
+ }
203
+ catch (err) {
204
+ spinner.stop();
205
+ output.error(err instanceof Error ? err.message : String(err));
206
+ process.exit(1);
207
+ }
208
+ }
209
+ // ── Read file contents ──────────────────────────────────────────────────
210
+ export async function filesCatCommand(fileId, opts) {
211
+ const spinner = output.spinner("Fetching file...");
212
+ try {
213
+ const urlRes = await fetch(`${getApiUrl()}/user/files/${fileId}/download`, {
214
+ headers: authHeaders(),
215
+ });
216
+ if (!urlRes.ok) {
217
+ const body = await urlRes
218
+ .json()
219
+ .catch(() => ({ error: urlRes.statusText }));
220
+ throw new Error(body.error || urlRes.statusText);
221
+ }
222
+ const { downloadUrl } = (await urlRes.json());
223
+ const contentRes = await fetch(downloadUrl);
224
+ if (!contentRes.ok) {
225
+ throw new Error(`Failed to download file: ${contentRes.statusText}`);
226
+ }
227
+ spinner.stop();
228
+ if (opts.output) {
229
+ const buffer = Buffer.from(await contentRes.arrayBuffer());
230
+ writeFileSync(opts.output, buffer);
231
+ output.success(`Saved ${buffer.length} bytes to ${opts.output}`);
232
+ }
233
+ else {
234
+ const text = await contentRes.text();
235
+ process.stdout.write(text);
236
+ if (!text.endsWith("\n"))
237
+ process.stdout.write("\n");
238
+ }
239
+ }
240
+ catch (err) {
241
+ spinner.stop();
242
+ output.error(err instanceof Error ? err.message : String(err));
243
+ process.exit(1);
244
+ }
245
+ }
246
+ // ── Edit file (find/replace or overwrite) ───────────────────────────────
247
+ export async function filesEditCommand(fileId, opts) {
248
+ const hasReplace = opts.find !== undefined || opts.replace !== undefined;
249
+ const hasOverwrite = opts.content !== undefined || opts.from !== undefined;
250
+ if (!hasReplace && !hasOverwrite) {
251
+ output.error("Specify either --find/--replace for find-and-replace, or --content/--from for full overwrite.");
252
+ process.exit(1);
253
+ }
254
+ if (hasReplace && hasOverwrite) {
255
+ output.error("Cannot combine --find/--replace with --content/--from.");
256
+ process.exit(1);
257
+ }
258
+ if (hasReplace && (opts.find === undefined || opts.replace === undefined)) {
259
+ output.error("Find-and-replace requires both --find and --replace.");
260
+ process.exit(1);
261
+ }
262
+ const spinner = output.spinner("Editing file...");
263
+ try {
264
+ let nextContent;
265
+ if (hasOverwrite) {
266
+ nextContent = opts.content ?? readFileSync(opts.from, "utf-8");
267
+ }
268
+ else {
269
+ // Fetch current content, apply find/replace locally
270
+ const urlRes = await fetch(`${getApiUrl()}/user/files/${fileId}/download`, { headers: authHeaders() });
271
+ if (!urlRes.ok) {
272
+ const body = await urlRes
273
+ .json()
274
+ .catch(() => ({ error: urlRes.statusText }));
275
+ throw new Error(body.error || urlRes.statusText);
276
+ }
277
+ const { downloadUrl } = (await urlRes.json());
278
+ const contentRes = await fetch(downloadUrl);
279
+ if (!contentRes.ok) {
280
+ throw new Error(`Failed to fetch file: ${contentRes.statusText}`);
281
+ }
282
+ const current = await contentRes.text();
283
+ const occurrences = current.split(opts.find).length - 1;
284
+ if (occurrences === 0) {
285
+ throw new Error(`--find text not found in file (must match exactly, including whitespace).`);
286
+ }
287
+ if (occurrences > 1 && !opts.all) {
288
+ throw new Error(`--find text appears ${occurrences} times. Pass --all to replace every occurrence, or provide a more specific --find string.`);
289
+ }
290
+ if (opts.all) {
291
+ nextContent = current.split(opts.find).join(opts.replace);
292
+ }
293
+ else {
294
+ const idx = current.indexOf(opts.find);
295
+ nextContent =
296
+ current.slice(0, idx) +
297
+ opts.replace +
298
+ current.slice(idx + opts.find.length);
299
+ }
300
+ }
301
+ const putRes = await fetch(`${getApiUrl()}/user/files/${fileId}/content`, {
302
+ method: "PUT",
303
+ headers: authHeaders(),
304
+ body: JSON.stringify({ content: nextContent }),
305
+ });
306
+ if (!putRes.ok) {
307
+ const body = await putRes
308
+ .json()
309
+ .catch(() => ({ error: putRes.statusText }));
310
+ throw new Error(body.error || putRes.statusText);
311
+ }
312
+ const { file } = (await putRes.json());
313
+ spinner.stop();
314
+ output.success(`Updated ${file.name} (${formatSize(file.sizeBytes)})`);
315
+ }
316
+ catch (err) {
317
+ spinner.stop();
318
+ output.error(err instanceof Error ? err.message : String(err));
319
+ process.exit(1);
320
+ }
321
+ }
322
+ // ── Write/replace file content from stdin or local file ─────────────────
323
+ export async function filesWriteCommand(fileId, opts) {
324
+ const spinner = output.spinner("Writing file content...");
325
+ try {
326
+ const content = opts.from
327
+ ? readFileSync(opts.from, "utf-8")
328
+ : await readStdin();
329
+ const res = await fetch(`${getApiUrl()}/user/files/${fileId}/content`, {
330
+ method: "PUT",
331
+ headers: authHeaders(),
332
+ body: JSON.stringify({ content }),
333
+ });
334
+ if (!res.ok) {
335
+ const body = await res.json().catch(() => ({ error: res.statusText }));
336
+ throw new Error(body.error || res.statusText);
337
+ }
338
+ const { file } = (await res.json());
339
+ spinner.stop();
340
+ output.success(`Wrote ${formatSize(file.sizeBytes)} to ${file.name}`);
341
+ }
342
+ catch (err) {
343
+ spinner.stop();
344
+ output.error(err instanceof Error ? err.message : String(err));
345
+ process.exit(1);
346
+ }
347
+ }
348
+ // ── Search files ────────────────────────────────────────────────────────
349
+ export async function filesSearchCommand(query, opts) {
350
+ const spinner = output.spinner(`Searching for "${query}"...`);
351
+ try {
352
+ const params = new URLSearchParams({ query });
353
+ if (opts.folder)
354
+ params.set("folder", opts.folder);
355
+ if (opts.mimeType)
356
+ params.set("mimeType", opts.mimeType);
357
+ if (opts.limit)
358
+ params.set("limit", opts.limit);
359
+ const res = await fetch(`${getApiUrl()}/user/files/search?${params.toString()}`, { headers: authHeaders() });
360
+ if (!res.ok) {
361
+ const body = await res.json().catch(() => ({ error: res.statusText }));
362
+ throw new Error(body.error || res.statusText);
363
+ }
364
+ const { files } = (await res.json());
365
+ spinner.stop();
366
+ if (files.length === 0) {
367
+ output.dim(`No files found matching "${query}".`);
368
+ return;
369
+ }
370
+ for (const f of files) {
371
+ const size = formatSize(f.sizeBytes);
372
+ const date = new Date(f.createdAt).toLocaleDateString();
373
+ const desc = f.metadata?.description
374
+ ? ` — ${f.metadata.description}`
375
+ : "";
376
+ console.log(` ${output.fmt.dim(f._id.slice(0, 8))} ${f.name} ${output.fmt.dim(size)} ${output.fmt.dim(date)} ${output.fmt.dim(f.folder)}${output.fmt.dim(desc)}`);
377
+ }
378
+ output.blank();
379
+ output.dim(`${files.length} result(s)`);
380
+ }
381
+ catch (err) {
382
+ spinner.stop();
383
+ output.error(err instanceof Error ? err.message : String(err));
384
+ process.exit(1);
385
+ }
386
+ }
387
+ // ── Move file to a different folder ─────────────────────────────────────
388
+ export async function filesMvCommand(fileId, folder) {
389
+ const spinner = output.spinner("Moving file...");
390
+ try {
391
+ const res = await fetch(`${getApiUrl()}/user/files/${fileId}/move`, {
392
+ method: "PATCH",
393
+ headers: authHeaders(),
394
+ body: JSON.stringify({ folder }),
395
+ });
396
+ if (!res.ok) {
397
+ const body = await res.json().catch(() => ({ error: res.statusText }));
398
+ throw new Error(body.error || res.statusText);
399
+ }
400
+ const { file } = (await res.json());
401
+ spinner.stop();
402
+ output.success(`Moved ${file.name} to ${file.folder}`);
403
+ }
404
+ catch (err) {
405
+ spinner.stop();
406
+ output.error(err instanceof Error ? err.message : String(err));
407
+ process.exit(1);
408
+ }
409
+ }
410
+ // ── Rename file ─────────────────────────────────────────────────────────
411
+ export async function filesRenameCommand(fileId, name) {
412
+ const spinner = output.spinner("Renaming file...");
413
+ try {
414
+ const res = await fetch(`${getApiUrl()}/user/files/${fileId}/rename`, {
415
+ method: "PATCH",
416
+ headers: authHeaders(),
417
+ body: JSON.stringify({ name }),
418
+ });
419
+ if (!res.ok) {
420
+ const body = await res.json().catch(() => ({ error: res.statusText }));
421
+ throw new Error(body.error || res.statusText);
422
+ }
423
+ const { file } = (await res.json());
424
+ spinner.stop();
425
+ output.success(`Renamed to ${file.name}`);
426
+ }
427
+ catch (err) {
428
+ spinner.stop();
429
+ output.error(err instanceof Error ? err.message : String(err));
430
+ process.exit(1);
431
+ }
432
+ }
433
+ // ── File info ───────────────────────────────────────────────────────────
434
+ export async function filesInfoCommand(fileId) {
435
+ const spinner = output.spinner("Fetching file metadata...");
436
+ try {
437
+ const res = await fetch(`${getApiUrl()}/user/files/${fileId}`, {
438
+ headers: authHeaders(),
439
+ });
440
+ if (!res.ok) {
441
+ const body = await res.json().catch(() => ({ error: res.statusText }));
442
+ throw new Error(body.error || res.statusText);
443
+ }
444
+ const { file } = (await res.json());
445
+ spinner.stop();
446
+ output.label("Name", file.name);
447
+ output.label("ID", file._id);
448
+ output.label("Folder", file.folder);
449
+ output.label("Type", file.isFolder ? "folder" : file.mimeType);
450
+ output.label("Size", formatSize(file.sizeBytes));
451
+ output.label("Created", new Date(file.createdAt).toLocaleString());
452
+ if (file.metadata?.description) {
453
+ output.label("Description", file.metadata.description);
454
+ }
455
+ }
456
+ catch (err) {
457
+ spinner.stop();
458
+ output.error(err instanceof Error ? err.message : String(err));
459
+ process.exit(1);
460
+ }
461
+ }
462
+ // ── Helpers ─────────────────────────────────────────────────────────────
463
+ function formatSize(bytes) {
464
+ if (bytes < 1024)
465
+ return `${bytes} B`;
466
+ if (bytes < 1024 * 1024)
467
+ return `${(bytes / 1024).toFixed(1)} KB`;
468
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
469
+ }
470
+ const EXTENSION_MIME_MAP = {
471
+ csv: "text/csv",
472
+ json: "application/json",
473
+ txt: "text/plain",
474
+ md: "text/markdown",
475
+ html: "text/html",
476
+ pdf: "application/pdf",
477
+ js: "text/javascript",
478
+ ts: "text/typescript",
479
+ py: "text/x-python",
480
+ sql: "text/x-sql",
481
+ yaml: "text/yaml",
482
+ yml: "text/yaml",
483
+ css: "text/css",
484
+ sol: "text/x-solidity",
485
+ rs: "text/x-rust",
486
+ go: "text/x-go",
487
+ rb: "text/x-ruby",
488
+ sh: "text/x-shellscript",
489
+ toml: "text/toml",
490
+ svg: "image/svg+xml",
491
+ png: "image/png",
492
+ jpg: "image/jpeg",
493
+ jpeg: "image/jpeg",
494
+ gif: "image/gif",
495
+ webp: "image/webp",
496
+ };
497
+ function inferMimeType(filename) {
498
+ const ext = filename.split(".").pop()?.toLowerCase();
499
+ return (ext && EXTENSION_MIME_MAP[ext]) || "application/octet-stream";
500
+ }
501
+ //# sourceMappingURL=files.js.map
@@ -848,6 +848,10 @@ export async function setupClaudeCodeCommand() {
848
848
  console.log();
849
849
  output.dim("All Claude Code requests will route through the Bankr gateway.");
850
850
  output.dim("Or launch directly: bankr llm claude");
851
+ console.log();
852
+ output.info("Model format:");
853
+ output.dim(" Claude Code accepts Anthropic-style IDs (dashed): claude-opus-4-7, claude-sonnet-4-6.");
854
+ output.dim(" `bankr llm claude` auto-translates the dotted form (claude-opus-4.7) for you.");
851
855
  }
852
856
  /* ─────────────────────── Shared launcher helpers ────────────────────────── */
853
857
  function requireAuth() {
@@ -887,11 +891,38 @@ function fileContains(filePath, search) {
887
891
  }
888
892
  }
889
893
  /* ─────────────────────────── bankr llm claude ─────────────────────────────── */
894
+ /**
895
+ * Rewrite gateway-canonical Claude IDs (dotted, e.g. `claude-opus-4.7`) to
896
+ * the wire format Claude Code expects (dashed, e.g. `claude-opus-4-7`).
897
+ * Otherwise Claude Code silently falls back to its default model — the
898
+ * `--model` flag appears honored but the real request is a different
899
+ * model. Any suffix (e.g. `[1m]` context tier) is preserved.
900
+ */
901
+ function toAnthropicModelId(model) {
902
+ return model.replace(/^(claude-(?:opus|sonnet|haiku)-)(\d+)\.(\d+)(.*)$/, "$1$2-$3$4");
903
+ }
904
+ function translateClaudeModelArgs(args) {
905
+ const out = [...args];
906
+ for (let i = 0; i < out.length; i++) {
907
+ const arg = out[i];
908
+ const inlineMatch = /^(--model|-m)=(.+)$/.exec(arg);
909
+ if (inlineMatch) {
910
+ out[i] = `${inlineMatch[1]}=${toAnthropicModelId(inlineMatch[2])}`;
911
+ continue;
912
+ }
913
+ if ((arg === "--model" || arg === "-m") && i + 1 < out.length) {
914
+ out[i + 1] = toAnthropicModelId(out[i + 1]);
915
+ i++;
916
+ }
917
+ }
918
+ return out;
919
+ }
890
920
  export async function claudeCommand(args) {
891
921
  const llmKey = requireAuth();
892
922
  const llmUrl = getLlmUrl();
923
+ const translated = translateClaudeModelArgs(args);
893
924
  output.dim(`Launching Claude Code via ${llmUrl}`);
894
- return launchTool("claude", args, { ANTHROPIC_BASE_URL: llmUrl, ANTHROPIC_AUTH_TOKEN: llmKey }, "https://docs.anthropic.com/en/docs/claude-code");
925
+ return launchTool("claude", translated, { ANTHROPIC_BASE_URL: llmUrl, ANTHROPIC_AUTH_TOKEN: llmKey }, "https://docs.anthropic.com/en/docs/claude-code");
895
926
  }
896
927
  /* ────────────────────────── bankr llm opencode ───────────────────────────── */
897
928
  export async function opencodeCommand(args) {
@@ -444,8 +444,10 @@ function isValidIpOrCidr(value) {
444
444
  // ::ffff: mapped IPv4 addresses normalize to IPv4 at runtime,
445
445
  // so validate prefix against IPv4 max (32) not IPv6 max (128)
446
446
  const isV4Mapped = version === 6 && ip.toLowerCase().startsWith("::ffff:");
447
- const maxPrefix = isV4Mapped ? 32 : version === 4 ? 32 : 128;
448
- return prefix <= maxPrefix;
447
+ const isV4 = version === 4 || isV4Mapped;
448
+ const maxPrefix = isV4 ? 32 : 128;
449
+ const minPrefix = isV4 ? 8 : 16;
450
+ return prefix >= minPrefix && prefix <= maxPrefix;
449
451
  }
450
452
  function parseAndValidateIps(raw) {
451
453
  const ips = raw
@@ -0,0 +1,29 @@
1
+ /**
2
+ * CLI commands for user webhook hosting.
3
+ *
4
+ * bankr webhooks init — Scaffold webhooks/ + bankr.webhooks.json
5
+ * bankr webhooks add <name> — Add a new webhook handler
6
+ * bankr webhooks deploy [name] — Deploy all or a single webhook
7
+ * bankr webhooks list — List deployed webhooks
8
+ * bankr webhooks pause <name> — Pause a webhook
9
+ * bankr webhooks resume <name> — Resume a webhook
10
+ * bankr webhooks delete <name> — Delete a webhook
11
+ * bankr webhooks logs <name> — View recent invocations
12
+ * bankr webhooks env set KEY=VALUE — Set encrypted env var
13
+ * bankr webhooks env list — List env var names
14
+ * bankr webhooks env unset KEY — Remove env var
15
+ */
16
+ export declare function webhooksInitCommand(): Promise<void>;
17
+ export type WebhookProvider = "slack" | "github" | "stripe" | "generic";
18
+ export declare function webhooksAddCommand(name: string, options?: {
19
+ provider?: string;
20
+ }): Promise<void>;
21
+ export declare function webhooksDeployCommand(name?: string): Promise<void>;
22
+ export declare function webhooksListCommand(): Promise<void>;
23
+ export declare function webhooksPauseResumeCommand(name: string, action: "pause" | "resume"): Promise<void>;
24
+ export declare function webhooksDeleteCommand(name: string): Promise<void>;
25
+ export declare function webhooksLogsCommand(name: string): Promise<void>;
26
+ export declare function webhooksEnvSetCommand(keyValue: string): Promise<void>;
27
+ export declare function webhooksEnvListCommand(): Promise<void>;
28
+ export declare function webhooksEnvUnsetCommand(key: string): Promise<void>;
29
+ //# sourceMappingURL=webhooks.d.ts.map