@tikoci/centrs 0.1.1 → 0.1.2

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/src/cli.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { apiCommand, runApiCli } from "./cli/api.ts";
3
4
  import { btestCommand, runBtestCli } from "./cli/btest.ts";
4
5
  import { devicesCommand, runDevicesCli } from "./cli/devices.ts";
5
6
  import { discoverCommand, runDiscoverCli } from "./cli/discover.ts";
@@ -14,6 +15,7 @@ import { describeCentrs, plannedProtocols, plannedSurfaces } from "./index.ts";
14
15
  const commandSummaries: ReadonlyArray<{ name: string; summary: string }> = [
15
16
  retrieveCommand,
16
17
  executeCommand,
18
+ apiCommand,
17
19
  transferCommand,
18
20
  terminalCommand,
19
21
  devicesCommand,
@@ -54,6 +56,9 @@ export async function runCli(
54
56
  if (command === "execute") {
55
57
  return await runExecuteCli(rest);
56
58
  }
59
+ if (command === "api") {
60
+ return await runApiCli(rest);
61
+ }
57
62
  if (command === "transfer") {
58
63
  return await runTransferCli(rest);
59
64
  }
@@ -80,6 +85,23 @@ export async function runCli(
80
85
  if (command === "mcp") {
81
86
  return await runMcpCli(rest);
82
87
  }
88
+ // `stream` (and the `tail` shorthand) folded into `api --stream`.
89
+ if (command === "stream" || command === "tail") {
90
+ console.error(
91
+ formatCentrsErrorText(
92
+ asCentrsError(
93
+ new Error(`\`centrs ${command}\` has been folded into \`api\`.`),
94
+ {
95
+ code: "input/invalid-command",
96
+ summary: `\`centrs ${command}\` is no longer a command — open-ended follow is now \`api … --stream\`.`,
97
+ remediation:
98
+ "Use `centrs api <router> <endpoint> --stream` (native-api only), e.g. `centrs api edge1 ip/address --stream`.",
99
+ },
100
+ ),
101
+ ),
102
+ );
103
+ return 1;
104
+ }
83
105
 
84
106
  console.error(
85
107
  formatCentrsErrorText(
@@ -218,6 +218,11 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
218
218
  summary:
219
219
  "The RouterOS native API returned a !trap with no more specific mapping.",
220
220
  },
221
+ {
222
+ code: "routeros/btest-connection-count-single-stream",
223
+ summary:
224
+ "Fewer TCP data connections were opened than --connection-count requested (e.g. authenticated multi-connection is not yet supported), so throughput will not scale to the full count.",
225
+ },
221
226
  {
222
227
  code: "routeros/btest-protocol",
223
228
  summary: "A bandwidth-test (btest) packet could not be decoded.",
@@ -227,6 +232,11 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
227
232
  summary:
228
233
  "The btest server refused a client because max-sessions is reached.",
229
234
  },
235
+ {
236
+ code: "routeros/btest-udp-tx-size-ignored",
237
+ summary:
238
+ "For UDP --direction both, the btest wire protocol carries a single tx-size; --remote-udp-tx-size is ignored and --local-udp-tx-size is used for both directions.",
239
+ },
230
240
  {
231
241
  code: "routeros/command-failed",
232
242
  summary: "RouterOS reported a command failure.",
@@ -415,6 +425,10 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
415
425
  code: "usage/invalid-concurrency",
416
426
  summary: "The --concurrency value must be an integer >= 1.",
417
427
  },
428
+ {
429
+ code: "usage/invalid-method",
430
+ summary: "The -X/--method value is not a supported HTTP method.",
431
+ },
418
432
  {
419
433
  code: "usage/missing-group",
420
434
  summary: "The operation requires a non-empty --group value.",
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Transport-agnostic fan-out engine.
3
+ *
4
+ * `retrieve` group fan-out (`src/retrieve-fanout.ts`) grew the reusable
5
+ * machinery: a bounded worker pool, transport-aware concurrency defaults, a
6
+ * retry allowlist with jittered backoff, and the ok/failed summary. `api` — and
7
+ * any later fan-out command — needs the identical behavior, so it lives here as
8
+ * one vocabulary. The command-specific orchestration (CDB group expansion,
9
+ * per-target resolve, envelope shaping, text rendering) stays in each command's
10
+ * fan-out module; this core only knows about generic items, concurrency, retry,
11
+ * and counting.
12
+ */
13
+
14
+ import { CentrsError } from "../errors.ts";
15
+ import type { RouterOsProtocol } from "../protocols/index.ts";
16
+ import type { FanoutSummary } from "./envelope.ts";
17
+
18
+ /** Transport-aware default worker-pool size (REST drops parallel POSTs above ~8). */
19
+ export const FANOUT_CONCURRENCY_DEFAULTS: Record<
20
+ "rest-api" | "native-api",
21
+ number
22
+ > = {
23
+ "rest-api": 8,
24
+ "native-api": 4,
25
+ };
26
+
27
+ /** Extra attempts after the first try (jittered backoff between them). */
28
+ export const FANOUT_MAX_RETRIES = 2;
29
+
30
+ /** Base backoff in milliseconds; doubled per attempt with up to +base jitter. */
31
+ export const FANOUT_BACKOFF_BASE_MS = 200;
32
+
33
+ /** RouterOS-style codes that are safe to retry under fan-out. */
34
+ export const FANOUT_RETRYABLE_CODES = [
35
+ "transport/network",
36
+ "transport/connection-closed",
37
+ ] as const;
38
+
39
+ /** Resolve and validate the worker-pool size. */
40
+ export function resolveFanoutConcurrency(
41
+ requested: number | undefined,
42
+ via: RouterOsProtocol,
43
+ ): number {
44
+ if (requested !== undefined) {
45
+ if (!Number.isInteger(requested) || requested < 1) {
46
+ throw new CentrsError({
47
+ code: "usage/invalid-concurrency",
48
+ summary: `--concurrency must be an integer >= 1. Received: ${requested}`,
49
+ remediation: "Pass a positive integer, e.g. `--concurrency 4`.",
50
+ context: { concurrency: requested },
51
+ });
52
+ }
53
+ return requested;
54
+ }
55
+ if (via === "native-api") {
56
+ return FANOUT_CONCURRENCY_DEFAULTS["native-api"];
57
+ }
58
+ return FANOUT_CONCURRENCY_DEFAULTS["rest-api"];
59
+ }
60
+
61
+ /**
62
+ * Classify a per-target failure as retryable. Only the locked allowlist retries:
63
+ * generic transport/network failures and dropped/closed connections (including
64
+ * REST 5xx mapped to `transport/connection-closed`). Router-side (`routeros/*`),
65
+ * validation, auth, cdb, target, input, usage, timeout, TLS, connection-refused,
66
+ * and DNS failures are deterministic and never retried.
67
+ */
68
+ export function isRetryableFanoutError(error: unknown): boolean {
69
+ if (!(error instanceof CentrsError)) {
70
+ return false;
71
+ }
72
+ return (FANOUT_RETRYABLE_CODES as readonly string[]).includes(error.code);
73
+ }
74
+
75
+ /** Count inner `ok`/`failed` across a completed target set. */
76
+ export function summarizeFanout(
77
+ targets: readonly { ok: boolean }[],
78
+ ): FanoutSummary {
79
+ let ok = 0;
80
+ let failed = 0;
81
+ for (const target of targets) {
82
+ if (target.ok) {
83
+ ok += 1;
84
+ } else {
85
+ failed += 1;
86
+ }
87
+ }
88
+ return { total: targets.length, ok, failed };
89
+ }
90
+
91
+ /**
92
+ * Ordered bounded worker pool. Runs `worker` over `items` with at most
93
+ * `concurrency` in flight, returning results in INPUT order regardless of
94
+ * completion order.
95
+ */
96
+ export async function runBoundedPool<I, O>(
97
+ items: readonly I[],
98
+ concurrency: number,
99
+ worker: (item: I, index: number) => Promise<O>,
100
+ ): Promise<O[]> {
101
+ const results = new Array<O>(items.length);
102
+ let next = 0;
103
+ const width = Math.max(1, Math.min(concurrency, items.length));
104
+
105
+ async function drain(): Promise<void> {
106
+ for (;;) {
107
+ const index = next;
108
+ next += 1;
109
+ if (index >= items.length) {
110
+ return;
111
+ }
112
+ results[index] = await worker(items[index] as I, index);
113
+ }
114
+ }
115
+
116
+ await Promise.all(Array.from({ length: width }, () => drain()));
117
+ return results;
118
+ }
119
+
120
+ /**
121
+ * Run `run`, retrying only {@link isRetryableFanoutError} failures up to
122
+ * `maxRetries` times with jittered backoff. A non-retryable error, or the final
123
+ * exhausted attempt, is converted to a value via `recover` (so a fan-out target
124
+ * yields an inner error envelope instead of throwing out of the batch).
125
+ */
126
+ export async function runWithRetry<T>(
127
+ run: () => Promise<T>,
128
+ recover: (error: unknown) => T,
129
+ options: {
130
+ sleep?: (ms: number) => Promise<void>;
131
+ maxRetries?: number;
132
+ } = {},
133
+ ): Promise<T> {
134
+ const sleep = options.sleep ?? defaultFanoutSleep;
135
+ const maxRetries = options.maxRetries ?? FANOUT_MAX_RETRIES;
136
+ let attempt = 0;
137
+ for (;;) {
138
+ try {
139
+ return await run();
140
+ } catch (error) {
141
+ if (attempt < maxRetries && isRetryableFanoutError(error)) {
142
+ attempt += 1;
143
+ await sleep(fanoutBackoffMs(attempt));
144
+ continue;
145
+ }
146
+ return recover(error);
147
+ }
148
+ }
149
+ }
150
+
151
+ /** Backoff for attempt N: base·2^(N-1) plus up to +base jitter. */
152
+ export function fanoutBackoffMs(attempt: number): number {
153
+ const base = FANOUT_BACKOFF_BASE_MS * 2 ** (attempt - 1);
154
+ return base + Math.floor(Math.random() * FANOUT_BACKOFF_BASE_MS);
155
+ }
156
+
157
+ /** Real timer sleep; tests inject a no-op. */
158
+ export function defaultFanoutSleep(ms: number): Promise<void> {
159
+ return new Promise((resolve) => setTimeout(resolve, ms));
160
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Shared `/console/inspect` client primitives.
3
+ *
4
+ * `retrieve` and `execute` each grew their own private copy of these helpers
5
+ * (path tokenizing, the comma-path join, node-type predicates, completion-name
6
+ * extraction). This module is the single home so a third consumer — `api`, and a
7
+ * future `explain`/`check` — reuses one grounded implementation instead of
8
+ * forking a fourth. It deliberately holds only the transport-agnostic
9
+ * *primitives*; each command composes its own discovery strategy on top (their
10
+ * strategies differ: `retrieve` probes print/get support, `execute` unions
11
+ * child-args with completions), so the strategy stays in the command.
12
+ *
13
+ * Grounding: the request modes mirror `tikoci/lsp-routeros-ts`
14
+ * (`server/src/routeros.ts` `InspectRequest`). The behavioral facts (the array-
15
+ * typed `path`, completion shapes) are confirmed on CHR 7.23.1 in
16
+ * `commands/api/AGENTS.md` — read that before changing wire behavior here.
17
+ */
18
+
19
+ import { CentrsError } from "../errors.ts";
20
+
21
+ /**
22
+ * The transport seam this module needs. A {@link ProtocolAdapter} satisfies it
23
+ * structurally; depending on the narrow shape keeps `core/` decoupled from the
24
+ * protocol layer (no import cycle).
25
+ */
26
+ export interface InspectBackend {
27
+ inspect(request: "child" | "completion", path: string): Promise<unknown[]>;
28
+ }
29
+
30
+ /**
31
+ * The `/console/inspect` request modes (lsp's `InspectRequest`). Only `child`
32
+ * and `completion` are wired today (the two the {@link InspectBackend} seam
33
+ * exposes); `highlight` (token-level error detection) and `syntax` (parse tree),
34
+ * plus the `input`/`.query`/`.proplist` request fields, are the documented
35
+ * Phase-2 extension point for value-level validation — widen the adapter seam
36
+ * when adding them.
37
+ */
38
+ export type InspectRequestKind =
39
+ | "child"
40
+ | "completion"
41
+ | "highlight"
42
+ | "syntax";
43
+
44
+ /** A row from `request=child`: a menu/argument node under a path. */
45
+ export interface InspectChildItem {
46
+ type?: string;
47
+ name?: string;
48
+ "node-type"?: string;
49
+ }
50
+
51
+ /** A row from `request=completion`: a candidate token for a path/argument. */
52
+ export interface InspectCompletionItem {
53
+ type?: string;
54
+ name?: string;
55
+ completion?: string;
56
+ value?: string;
57
+ text?: string;
58
+ }
59
+
60
+ /**
61
+ * Split a slash RouterOS path into menu tokens, dropping the leading slash and
62
+ * empties: `"/ip/address"` → `["ip", "address"]`.
63
+ */
64
+ export function pathTokens(path: string): string[] {
65
+ return path.split("/").filter(Boolean);
66
+ }
67
+
68
+ /**
69
+ * Join menu tokens into the COMMA form `/console/inspect` expects for its `path`
70
+ * argument: `["ip", "address"]` → `"ip,address"`.
71
+ *
72
+ * The inspect `path` argument is internally a RouterOS *array*: a comma string
73
+ * is `:toarray`-split into the menu-walk tokens, whereas a `/`-prefixed command
74
+ * string is not split and matches no menu (confirmed on CHR 7.23.1 — slash forms
75
+ * return nothing; see `commands/api/AGENTS.md`). So callers must pass tokens
76
+ * joined by comma, never a slash command.
77
+ */
78
+ export function inspectPath(tokens: readonly string[]): string {
79
+ return tokens.join(",");
80
+ }
81
+
82
+ /** True for an argument (`arg`) node from `request=child`. */
83
+ export function isArgumentNode(child: InspectChildItem): boolean {
84
+ return child.type === "arg" || child["node-type"] === "arg";
85
+ }
86
+
87
+ /** True for a command (`cmd`) node with the given name (e.g. `print`, `get`). */
88
+ export function isCommandNode(child: InspectChildItem, name: string): boolean {
89
+ return (
90
+ child.name === name &&
91
+ (child.type === "cmd" || child["node-type"] === "cmd")
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Flatten completion rows into attribute names. Reads every name-like field a
97
+ * RouterOS build might populate (`completion`/`name`/`value`/`text`), strips
98
+ * everything from the first `=` onward (the `name=value` completion form), and
99
+ * drops blanks. Returns the names in row order **without** de-duplication or
100
+ * sorting — callers that need a stable set wrap with `[...new Set(names)].sort()`
101
+ * (both current callers do).
102
+ */
103
+ export function extractCompletionNames(
104
+ rows: readonly InspectCompletionItem[],
105
+ ): string[] {
106
+ return rows
107
+ .flatMap((row) => [row.completion, row.name, row.value, row.text])
108
+ .filter((value): value is string => typeof value === "string")
109
+ .map((value) => nameBeforeEquals(value).trim())
110
+ .filter((value) => value.length > 0);
111
+ }
112
+
113
+ /**
114
+ * The token before the first `=` in a RouterOS completion row. Plain string
115
+ * slicing (no regex) — `value.replace(/=.*$/, "")` triggers a CodeQL
116
+ * polynomial-ReDoS flag on this exported function's caller-supplied input.
117
+ */
118
+ function nameBeforeEquals(value: string): string {
119
+ const eq = value.indexOf("=");
120
+ return eq === -1 ? value : value.slice(0, eq);
121
+ }
122
+
123
+ /** `request=child` for a token path. */
124
+ export async function inspectChildren(
125
+ backend: InspectBackend,
126
+ tokens: readonly string[],
127
+ ): Promise<InspectChildItem[]> {
128
+ return (await backend.inspect(
129
+ "child",
130
+ inspectPath(tokens),
131
+ )) as InspectChildItem[];
132
+ }
133
+
134
+ /** `request=completion` for a token path. */
135
+ export async function inspectCompletions(
136
+ backend: InspectBackend,
137
+ tokens: readonly string[],
138
+ ): Promise<InspectCompletionItem[]> {
139
+ return (await backend.inspect(
140
+ "completion",
141
+ inspectPath(tokens),
142
+ )) as InspectCompletionItem[];
143
+ }
144
+
145
+ /**
146
+ * {@link inspectChildren} that swallows a "path not found" trap to an empty
147
+ * list, so the caller can classify the absence itself (REST returns an empty
148
+ * child list; native traps). Only the two grounded not-found codes are
149
+ * swallowed — `routeros/unknown-path` (the classified form) and the legacy
150
+ * `routeros/api-trap` catch-all; every other error propagates. Use this for the
151
+ * existence probe, not for attribute/value discovery (where a trap is a real
152
+ * failure).
153
+ */
154
+ export async function inspectChildrenOrEmpty(
155
+ backend: InspectBackend,
156
+ tokens: readonly string[],
157
+ ): Promise<InspectChildItem[]> {
158
+ try {
159
+ return await inspectChildren(backend, tokens);
160
+ } catch (error) {
161
+ if (
162
+ error instanceof CentrsError &&
163
+ (error.code === "routeros/api-trap" ||
164
+ error.code === "routeros/unknown-path")
165
+ ) {
166
+ return [];
167
+ }
168
+ throw error;
169
+ }
170
+ }
package/src/execute.ts CHANGED
@@ -9,6 +9,13 @@ import type {
9
9
  Warning,
10
10
  } from "./core/envelope.ts";
11
11
  import { buildTip } from "./core/envelope.ts";
12
+ import {
13
+ extractCompletionNames,
14
+ inspectChildren,
15
+ inspectCompletions,
16
+ isArgumentNode,
17
+ pathTokens,
18
+ } from "./core/inspect.ts";
12
19
  import { mapRouterOsError } from "./core/routeros-errors.ts";
13
20
  import { CentrsError, serializeCentrsError } from "./errors.ts";
14
21
  import {
@@ -132,20 +139,6 @@ export interface ResolvedExecuteRequest {
132
139
  warnings: readonly Warning[];
133
140
  }
134
141
 
135
- interface InspectChildItem {
136
- type?: string;
137
- name?: string;
138
- "node-type"?: string;
139
- }
140
-
141
- interface InspectCompletionItem {
142
- type?: string;
143
- name?: string;
144
- completion?: string;
145
- value?: string;
146
- text?: string;
147
- }
148
-
149
142
  export async function execute(
150
143
  request: ExecuteRequest,
151
144
  env: Record<string, string | undefined> = Bun.env,
@@ -735,24 +728,18 @@ async function inspectExecuteAttributes(
735
728
  resolved: ResolvedExecuteRequest,
736
729
  backend: ProtocolAdapter,
737
730
  ): Promise<string[]> {
738
- const commandPath = inspectPath([
731
+ const commandTokens = [
739
732
  ...pathTokens(resolved.canonical.path),
740
733
  resolved.canonical.verb,
741
- ]);
742
- const children = (await backend.inspect(
743
- "child",
744
- commandPath,
745
- )) as InspectChildItem[];
734
+ ];
735
+ const children = await inspectChildren(backend, commandTokens);
746
736
  const childAttributes = children
747
737
  .filter(isArgumentNode)
748
738
  .map((child) => child.name)
749
739
  .filter(
750
740
  (name): name is string => typeof name === "string" && name.length > 0,
751
741
  );
752
- const completionRows = (await backend.inspect(
753
- "completion",
754
- commandPath,
755
- )) as InspectCompletionItem[];
742
+ const completionRows = await inspectCompletions(backend, commandTokens);
756
743
  return [
757
744
  ...new Set([...childAttributes, ...extractCompletionNames(completionRows)]),
758
745
  ].sort();
@@ -819,7 +806,9 @@ async function assertWriteConfirmed(
819
806
  });
820
807
  }
821
808
 
822
- async function promptForWriteConfirmation(prompt: string): Promise<boolean> {
809
+ export async function promptForWriteConfirmation(
810
+ prompt: string,
811
+ ): Promise<boolean> {
823
812
  process.stderr.write(prompt);
824
813
  for await (const chunk of Bun.stdin.stream()) {
825
814
  const answer = new TextDecoder().decode(chunk).trim().toLowerCase();
@@ -1299,28 +1288,6 @@ function syntaxCause(error: unknown): unknown {
1299
1288
  return error;
1300
1289
  }
1301
1290
 
1302
- function pathTokens(path: string): string[] {
1303
- return path.split("/").filter(Boolean);
1304
- }
1305
-
1306
- function inspectPath(tokens: readonly string[]): string {
1307
- return tokens.join(",");
1308
- }
1309
-
1310
- function isArgumentNode(child: InspectChildItem): boolean {
1311
- return child.type === "arg" || child["node-type"] === "arg";
1312
- }
1313
-
1314
- function extractCompletionNames(
1315
- rows: readonly InspectCompletionItem[],
1316
- ): string[] {
1317
- return rows
1318
- .flatMap((row) => [row.completion, row.name, row.value, row.text])
1319
- .filter((value): value is string => typeof value === "string")
1320
- .map((value) => value.replace(/=.*$/, "").trim())
1321
- .filter((value) => value.length > 0);
1322
- }
1323
-
1324
1291
  function parseOutputFormat(value: string): ExecuteOutputFormat {
1325
1292
  if (executeOutputFormats.includes(value as ExecuteOutputFormat)) {
1326
1293
  return value as ExecuteOutputFormat;
package/src/index.ts CHANGED
@@ -1,5 +1,38 @@
1
1
  export const centrsVersion = "0.1.0";
2
2
 
3
+ export {
4
+ type ApiEnvelope,
5
+ type ApiErrorEnvelope,
6
+ type ApiMethod,
7
+ type ApiOperationMeta,
8
+ type ApiOutputFormat,
9
+ type ApiRequest,
10
+ type ApiRequestSummary,
11
+ type ApiStreamMeta,
12
+ type ApiStreamStopReason,
13
+ type ApiStreamSummary,
14
+ type ApiSuccessEnvelope,
15
+ api,
16
+ apiEnvelope,
17
+ apiListen,
18
+ apiMethods,
19
+ apiOutputFormats,
20
+ buildApiBody,
21
+ buildApiErrorEnvelope,
22
+ buildApiErrorEnvelopeFromResolved,
23
+ buildApiQuery,
24
+ buildProtocolApiRequest,
25
+ isApiMutating,
26
+ mapMethodToVerb,
27
+ type NormalizedApiEndpoint,
28
+ normalizeApiEndpoint,
29
+ type ResolvedApiRequest,
30
+ renderApiEnvelope,
31
+ renderApiStreamLine,
32
+ resolveApiRequest,
33
+ runResolvedApi,
34
+ validateApiRequestShape,
35
+ } from "./api.ts";
3
36
  export {
4
37
  BTEST_CLIENT_CSV_HEADER,
5
38
  BTEST_SERVER_CSV_HEADER,
@@ -41,6 +74,31 @@ export {
41
74
  type SettingSourceKind,
42
75
  type Warning,
43
76
  } from "./core/envelope.ts";
77
+ export {
78
+ FANOUT_BACKOFF_BASE_MS,
79
+ FANOUT_CONCURRENCY_DEFAULTS,
80
+ FANOUT_MAX_RETRIES,
81
+ FANOUT_RETRYABLE_CODES,
82
+ isRetryableFanoutError,
83
+ resolveFanoutConcurrency,
84
+ runBoundedPool,
85
+ runWithRetry,
86
+ summarizeFanout,
87
+ } from "./core/fanout.ts";
88
+ export {
89
+ extractCompletionNames,
90
+ type InspectBackend,
91
+ type InspectChildItem,
92
+ type InspectCompletionItem,
93
+ type InspectRequestKind,
94
+ inspectChildren,
95
+ inspectChildrenOrEmpty,
96
+ inspectCompletions,
97
+ inspectPath,
98
+ isArgumentNode,
99
+ isCommandNode,
100
+ pathTokens,
101
+ } from "./core/inspect.ts";
44
102
  export {
45
103
  encodeMndpPacket,
46
104
  MNDP_BROADCAST_ADDRESS,
@@ -185,11 +243,14 @@ export {
185
243
  validateExecuteRequestShape,
186
244
  } from "./execute.ts";
187
245
  export {
246
+ type ApiVerb,
188
247
  createProtocolAdapter,
189
248
  getProtocolPlan,
190
249
  type ProtocolAdapter,
191
250
  type ProtocolAdapterCapabilities,
192
251
  type ProtocolAdapterConfig,
252
+ type ProtocolApiRequest,
253
+ type ProtocolApiResult,
193
254
  type ProtocolCapability,
194
255
  type ProtocolExecuteRequest,
195
256
  type ProtocolExecuteResult,
@@ -262,21 +323,13 @@ export {
262
323
  } from "./retrieve.ts";
263
324
  export {
264
325
  buildRetrieveFanoutErrorEnvelope,
265
- isRetryableFanoutError,
266
- RETRIEVE_FANOUT_BACKOFF_BASE_MS,
267
- RETRIEVE_FANOUT_CONCURRENCY_DEFAULTS,
268
- RETRIEVE_FANOUT_MAX_RETRIES,
269
- RETRIEVE_FANOUT_RETRYABLE_CODES,
270
326
  type RetrieveFanoutData,
271
327
  type RetrieveFanoutEnvelope,
272
328
  type RetrieveFanoutErrorEnvelope,
273
329
  type RetrieveFanoutOperationMeta,
274
330
  type RetrieveGroupInternals,
275
331
  renderRetrieveFanoutEnvelope,
276
- resolveFanoutConcurrency,
277
332
  retrieveGroup,
278
- runBoundedPool,
279
- summarizeFanout,
280
333
  } from "./retrieve-fanout.ts";
281
334
  export {
282
335
  buildSshTerminalArgv,