@alpacakit/channels 0.1.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +772 -0
- package/dist/binding-compiler.d.ts +29 -0
- package/dist/binding-compiler.d.ts.map +1 -0
- package/dist/binding-compiler.js +40 -0
- package/dist/cli-coercion.d.ts +5 -0
- package/dist/cli-coercion.d.ts.map +1 -0
- package/dist/cli-coercion.js +51 -0
- package/dist/cli-compiler.d.ts +46 -0
- package/dist/cli-compiler.d.ts.map +1 -0
- package/dist/cli-compiler.js +213 -0
- package/dist/cli-executor.d.ts +25 -0
- package/dist/cli-executor.d.ts.map +1 -0
- package/dist/cli-executor.js +44 -0
- package/dist/cli-help-projection.d.ts +13 -0
- package/dist/cli-help-projection.d.ts.map +1 -0
- package/dist/cli-help-projection.js +159 -0
- package/dist/cli-model.d.ts +240 -0
- package/dist/cli-model.d.ts.map +1 -0
- package/dist/cli-model.js +89 -0
- package/dist/cli-presentation.d.ts +23 -0
- package/dist/cli-presentation.d.ts.map +1 -0
- package/dist/cli-presentation.js +265 -0
- package/dist/cli-schema.d.ts +4 -0
- package/dist/cli-schema.d.ts.map +1 -0
- package/dist/cli-schema.js +7 -0
- package/dist/cli-tree.d.ts +4 -0
- package/dist/cli-tree.d.ts.map +1 -0
- package/dist/cli-tree.js +421 -0
- package/dist/cli-values.d.ts +24 -0
- package/dist/cli-values.d.ts.map +1 -0
- package/dist/cli-values.js +40 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +5 -0
- package/dist/codecs.d.ts +25 -0
- package/dist/codecs.d.ts.map +1 -0
- package/dist/codecs.js +74 -0
- package/dist/contract.d.ts +12 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +28 -0
- package/dist/factory.d.ts +16 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +188 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +3 -0
- package/dist/introspection.d.ts +40 -0
- package/dist/introspection.d.ts.map +1 -0
- package/dist/introspection.js +56 -0
- package/dist/mcp.d.ts +26 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +84 -0
- package/dist/model.d.ts +166 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +22 -0
- package/dist/runtime.d.ts +47 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +140 -0
- package/dist/schema.d.ts +14 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +70 -0
- package/dist/tree-internal.d.ts +14 -0
- package/dist/tree-internal.d.ts.map +1 -0
- package/dist/tree-internal.js +20 -0
- package/dist/values.d.ts +32 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +30 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
# @alpacakit/channels
|
|
2
|
+
|
|
3
|
+
Define an entry once, bind it to CLI and MCP, and use the same typed endpoint
|
|
4
|
+
for argument parsing, help, MCP schemas, input decoding, and projection.
|
|
5
|
+
|
|
6
|
+
HTTP routing is not currently provided. The supported entry-tree channels are
|
|
7
|
+
CLI and MCP.
|
|
8
|
+
|
|
9
|
+
## The mental model
|
|
10
|
+
|
|
11
|
+
You only need four concepts:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
Entry = what the command accepts and what it projects
|
|
15
|
+
Binding = how one channel supplies that input
|
|
16
|
+
Endpoint = the Entry and its Bindings bundled together
|
|
17
|
+
Tree = where the Endpoint lives in the command hierarchy
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A useful shorthand is:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
What? -> Entry
|
|
24
|
+
How? -> Binding
|
|
25
|
+
Where? -> Tree
|
|
26
|
+
Bundle them -> Endpoint
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Suppose an application exposes this CLI command:
|
|
30
|
+
|
|
31
|
+
```console
|
|
32
|
+
my-app hint "entry tree" --limit 5
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The same operation is exposed to MCP with this JSON input:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"query": "entry tree",
|
|
40
|
+
"limit": 5
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The definitions have this structure:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
┌────────────────────────────────────────┐
|
|
48
|
+
│ Entry: HintEntry │
|
|
49
|
+
│ │
|
|
50
|
+
│ query: required string │
|
|
51
|
+
│ limit: optional integer, default 20 │
|
|
52
|
+
│ │
|
|
53
|
+
│ project the values into a HintRequest │
|
|
54
|
+
└───────────────────┬────────────────────┘
|
|
55
|
+
│
|
|
56
|
+
│ attach channel input mappings
|
|
57
|
+
▼
|
|
58
|
+
┌────────────────────────────────────────┐
|
|
59
|
+
│ Endpoint: HintEndpoint │
|
|
60
|
+
│ │
|
|
61
|
+
│ ┌────────────────────────────────────┐ │
|
|
62
|
+
│ │ CLI Binding │ │
|
|
63
|
+
│ │ query -> positional argument │ │
|
|
64
|
+
│ │ limit -> --limit / -n │ │
|
|
65
|
+
│ └────────────────────────────────────┘ │
|
|
66
|
+
│ │
|
|
67
|
+
│ ┌────────────────────────────────────┐ │
|
|
68
|
+
│ │ MCP Binding │ │
|
|
69
|
+
│ │ query -> JSON property "query" │ │
|
|
70
|
+
│ │ limit -> JSON property "limit" │ │
|
|
71
|
+
│ └────────────────────────────────────┘ │
|
|
72
|
+
└───────────────────┬────────────────────┘
|
|
73
|
+
│
|
|
74
|
+
│ place the endpoint at an address
|
|
75
|
+
▼
|
|
76
|
+
┌────────────────────────────────────────┐
|
|
77
|
+
│ Tree │
|
|
78
|
+
│ │
|
|
79
|
+
│ my-app │
|
|
80
|
+
│ └── hint │
|
|
81
|
+
│ └── HintEndpoint │
|
|
82
|
+
└────────────────────────────────────────┘
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The important rule is that value types, validation, requiredness, defaults, and
|
|
86
|
+
projection are declared only on the Entry. A Binding only says how a channel
|
|
87
|
+
supplies a parameter. A Tree only says where an Endpoint lives.
|
|
88
|
+
|
|
89
|
+
The following sections build that example from top to bottom.
|
|
90
|
+
|
|
91
|
+
## 1. Define what the command does: Entry
|
|
92
|
+
|
|
93
|
+
An Entry answers four questions:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
What values does this operation accept?
|
|
97
|
+
↓
|
|
98
|
+
How are those values validated?
|
|
99
|
+
↓
|
|
100
|
+
Which defaults are applied?
|
|
101
|
+
↓
|
|
102
|
+
What application value is produced?
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import {
|
|
107
|
+
codecs,
|
|
108
|
+
defineEntry,
|
|
109
|
+
defineParameter,
|
|
110
|
+
optional,
|
|
111
|
+
required,
|
|
112
|
+
} from "@alpacakit/channels";
|
|
113
|
+
|
|
114
|
+
const HintEntry = defineEntry({
|
|
115
|
+
name: "hint",
|
|
116
|
+
|
|
117
|
+
parameters: [
|
|
118
|
+
defineParameter({
|
|
119
|
+
name: "query",
|
|
120
|
+
codec: codecs.string(),
|
|
121
|
+
requirement: required(),
|
|
122
|
+
description: "Text to search for",
|
|
123
|
+
}),
|
|
124
|
+
|
|
125
|
+
defineParameter({
|
|
126
|
+
name: "limit",
|
|
127
|
+
codec: codecs.positiveInteger({ max: 100 }),
|
|
128
|
+
requirement: optional({ default: 20 }),
|
|
129
|
+
description: "Maximum number of hints",
|
|
130
|
+
}),
|
|
131
|
+
],
|
|
132
|
+
|
|
133
|
+
project: ({ values }) => ({
|
|
134
|
+
query: values.query,
|
|
135
|
+
limit: values.limit,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
In plain English, this Entry means:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
hint
|
|
144
|
+
|
|
145
|
+
Input:
|
|
146
|
+
query
|
|
147
|
+
- string
|
|
148
|
+
- required
|
|
149
|
+
|
|
150
|
+
limit
|
|
151
|
+
- positive integer
|
|
152
|
+
- at most 100
|
|
153
|
+
- defaults to 20
|
|
154
|
+
|
|
155
|
+
Projection:
|
|
156
|
+
{
|
|
157
|
+
query: string,
|
|
158
|
+
limit: number
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
There is no CLI or MCP logic here. The Entry describes the operation itself.
|
|
163
|
+
|
|
164
|
+
Because `limit` has a default, `values.limit` is typed as `number`, not
|
|
165
|
+
`number | undefined`. Only optional parameters without a default remain
|
|
166
|
+
optional in `values`.
|
|
167
|
+
|
|
168
|
+
## 2. Define how each channel supplies input: Bindings
|
|
169
|
+
|
|
170
|
+
A CLI Binding maps the Entry parameters to argv syntax:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { bindCliEntry } from "@alpacakit/channels/cli";
|
|
174
|
+
|
|
175
|
+
const HintCliBinding = bindCliEntry({
|
|
176
|
+
parameters: [
|
|
177
|
+
{ kind: "positional", parameterName: "query" },
|
|
178
|
+
{ kind: "option", parameterName: "limit", flags: ["--limit", "-n"] },
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A binding row only needs `kind` and `parameterName`. The rest is derived from
|
|
184
|
+
the parameter name and only stated when you want to override it:
|
|
185
|
+
|
|
186
|
+
- `query` needs no `valueName` (help shows `QUERY`) and no `variadic` (defaults
|
|
187
|
+
to `false`).
|
|
188
|
+
- `limit` would default to the single flag `--limit`; here `flags` is given
|
|
189
|
+
only to add the `-n` alias.
|
|
190
|
+
|
|
191
|
+
The mapping is:
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
my-app hint "entry tree" --limit 5
|
|
195
|
+
└────┬────┘ └───┬───┘
|
|
196
|
+
│ │
|
|
197
|
+
▼ ▼
|
|
198
|
+
query limit
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Do not repeat `query`'s string codec or `limit`'s default in the Binding. Those
|
|
202
|
+
facts already belong to the Entry.
|
|
203
|
+
|
|
204
|
+
An MCP Binding selects the Entry parameters exposed as tool input:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { bindMcpEntry } from "@alpacakit/channels/mcp";
|
|
208
|
+
|
|
209
|
+
const HintMcpBinding = bindMcpEntry("query", "limit");
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The mapping is:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
{
|
|
216
|
+
"query": "entry tree",
|
|
217
|
+
"limit": 5
|
|
218
|
+
}
|
|
219
|
+
│ │
|
|
220
|
+
▼ ▼
|
|
221
|
+
query limit
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## 3. Bundle the Entry and Bindings: Endpoint
|
|
225
|
+
|
|
226
|
+
An Endpoint is the public execution identity for the operation:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { defineEntryEndpoint } from "@alpacakit/channels";
|
|
230
|
+
|
|
231
|
+
const HintEndpoint = defineEntryEndpoint(
|
|
232
|
+
HintEntry,
|
|
233
|
+
HintCliBinding,
|
|
234
|
+
HintMcpBinding,
|
|
235
|
+
);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Its structure is:
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
HintEndpoint
|
|
242
|
+
│
|
|
243
|
+
├── operation
|
|
244
|
+
│ └── HintEntry
|
|
245
|
+
│
|
|
246
|
+
├── CLI input mapping
|
|
247
|
+
│ ├── query is positional
|
|
248
|
+
│ └── limit is --limit / -n
|
|
249
|
+
│
|
|
250
|
+
└── MCP input mapping
|
|
251
|
+
├── expose query
|
|
252
|
+
└── expose limit
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Keep the object returned by `defineEntryEndpoint()`. CLI result narrowing and
|
|
256
|
+
all MCP runtime APIs use endpoint identity. Do not reconstruct an equivalent
|
|
257
|
+
Endpoint later.
|
|
258
|
+
|
|
259
|
+
## 4. Give the Endpoint an address: Tree
|
|
260
|
+
|
|
261
|
+
Place the Endpoint under the `hint` command:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import {
|
|
265
|
+
defineEntryNode,
|
|
266
|
+
defineEntryTree,
|
|
267
|
+
} from "@alpacakit/channels";
|
|
268
|
+
|
|
269
|
+
const CommandTree = defineEntryTree(
|
|
270
|
+
defineEntryNode({
|
|
271
|
+
name: "my-app",
|
|
272
|
+
description: "My application",
|
|
273
|
+
|
|
274
|
+
children: [
|
|
275
|
+
defineEntryNode({
|
|
276
|
+
name: "hint",
|
|
277
|
+
description: "Find implementation hints",
|
|
278
|
+
endpoints: [HintEndpoint],
|
|
279
|
+
}),
|
|
280
|
+
],
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The resulting hierarchy is:
|
|
286
|
+
|
|
287
|
+
```text
|
|
288
|
+
CommandTree
|
|
289
|
+
|
|
290
|
+
my-app <- root surface name
|
|
291
|
+
│
|
|
292
|
+
└── hint <- token that appears in argv
|
|
293
|
+
│
|
|
294
|
+
└── HintEndpoint <- operation executed here
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The root name is the program or surface name. It is not included in the argv
|
|
298
|
+
passed to the router.
|
|
299
|
+
|
|
300
|
+
Independent modules can contribute canonical nodes and merge them without an
|
|
301
|
+
intermediate route model:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
const children = mergeEntryTreeNodes([
|
|
305
|
+
...configCommandNodes,
|
|
306
|
+
...integrationCommandNodes,
|
|
307
|
+
]);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Nodes with the same name are merged recursively. Their descriptions and
|
|
311
|
+
channel segment overrides must agree; identical endpoint objects are
|
|
312
|
+
deduplicated by identity.
|
|
313
|
+
|
|
314
|
+
An endpoint placed directly on the root is the default command:
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
my-app
|
|
318
|
+
├── DefaultEndpoint <- runs for []
|
|
319
|
+
└── hint
|
|
320
|
+
└── HintEndpoint <- runs for ["hint", ...]
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## 5. Execute it from CLI
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import {
|
|
327
|
+
createCliRouter,
|
|
328
|
+
isCliResolvedEndpoint,
|
|
329
|
+
} from "@alpacakit/channels/cli";
|
|
330
|
+
|
|
331
|
+
const router = createCliRouter(CommandTree);
|
|
332
|
+
|
|
333
|
+
const result = router.resolve([
|
|
334
|
+
"hint",
|
|
335
|
+
"entry tree",
|
|
336
|
+
"--limit",
|
|
337
|
+
"5",
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
if (isCliResolvedEndpoint(result, HintEndpoint)) {
|
|
341
|
+
console.log(result.projection);
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The typed projection is:
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
{
|
|
349
|
+
query: "entry tree",
|
|
350
|
+
limit: 5,
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
At runtime the data flows through these stages:
|
|
355
|
+
|
|
356
|
+
```text
|
|
357
|
+
CLI argv
|
|
358
|
+
│
|
|
359
|
+
│ ["hint", "entry tree", "--limit", "5"]
|
|
360
|
+
▼
|
|
361
|
+
CLI Router
|
|
362
|
+
│
|
|
363
|
+
│ find HintEndpoint from the Tree
|
|
364
|
+
▼
|
|
365
|
+
CLI Binding
|
|
366
|
+
│
|
|
367
|
+
│ query = "entry tree"
|
|
368
|
+
│ limit = "5"
|
|
369
|
+
▼
|
|
370
|
+
Shared Decoder
|
|
371
|
+
│
|
|
372
|
+
│ validate query as a string
|
|
373
|
+
│ coerce and validate limit as an integer
|
|
374
|
+
▼
|
|
375
|
+
HintEntry.project()
|
|
376
|
+
│
|
|
377
|
+
▼
|
|
378
|
+
{
|
|
379
|
+
query: "entry tree",
|
|
380
|
+
limit: 5
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
A stored Tree may contain many differently typed entries, so its endpoints are
|
|
385
|
+
deliberately type-erased. `isCliResolvedEndpoint()` checks endpoint identity and
|
|
386
|
+
restores the exact projection type for the selected Endpoint.
|
|
387
|
+
|
|
388
|
+
The same router also exposes a channel-derived help surface:
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
const help = router.help;
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
For applications with many endpoints, an optional typed registry replaces a
|
|
395
|
+
manual identity-check chain:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
const executor = createCliExecutor(router, [
|
|
399
|
+
handleCliEndpoint(HintEndpoint, ({ projection }) => runHint(projection)),
|
|
400
|
+
]);
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
The registry rejects duplicate, missing, and out-of-router endpoints when it is
|
|
404
|
+
created. Handlers, output policy, and process exit behavior remain outside the
|
|
405
|
+
Entry definition.
|
|
406
|
+
|
|
407
|
+
## 6. Execute the same Endpoint from MCP
|
|
408
|
+
|
|
409
|
+
Compile the Endpoint once, then reuse the handle for both registration and
|
|
410
|
+
per-invocation decoding:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
import {
|
|
414
|
+
createMcpEndpoint,
|
|
415
|
+
isMcpResolved,
|
|
416
|
+
} from "@alpacakit/channels/mcp";
|
|
417
|
+
|
|
418
|
+
const HintMcpEndpoint = createMcpEndpoint(HintEndpoint);
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
`createMcpEndpoint()` validates the MCP binding once and returns a handle:
|
|
422
|
+
|
|
423
|
+
```text
|
|
424
|
+
HintMcpEndpoint
|
|
425
|
+
│
|
|
426
|
+
├── inputSchema <- raw Zod object shape for tool registration
|
|
427
|
+
│
|
|
428
|
+
└── parse(raw) <- decode + project one invocation
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`inputSchema` is a raw Zod object shape suitable for MCP tool registration. It
|
|
432
|
+
contains descriptions, validation, and requiredness derived from `HintEntry`.
|
|
433
|
+
|
|
434
|
+
Decode and project an MCP invocation through the same handle:
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
const result = HintMcpEndpoint.parse({
|
|
438
|
+
query: "entry tree",
|
|
439
|
+
limit: 5,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (isMcpResolved(result)) {
|
|
443
|
+
console.log(result.projection);
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Create the handle once (typically at startup) and reuse it for every request.
|
|
448
|
+
The binding is compiled a single time, so `parse()` does no per-invocation
|
|
449
|
+
recompilation, and an invalid binding throws `EntryTreeDefinitionError`
|
|
450
|
+
eagerly — exactly like `createCliRouter()`.
|
|
451
|
+
|
|
452
|
+
The MCP runtime path is:
|
|
453
|
+
|
|
454
|
+
```text
|
|
455
|
+
MCP JSON
|
|
456
|
+
│
|
|
457
|
+
│ { query: "entry tree", limit: 5 }
|
|
458
|
+
▼
|
|
459
|
+
HintEndpoint
|
|
460
|
+
│
|
|
461
|
+
▼
|
|
462
|
+
MCP Binding
|
|
463
|
+
│
|
|
464
|
+
│ select query and limit
|
|
465
|
+
▼
|
|
466
|
+
Shared Decoder
|
|
467
|
+
│
|
|
468
|
+
│ validate both values
|
|
469
|
+
▼
|
|
470
|
+
HintEntry.project()
|
|
471
|
+
│
|
|
472
|
+
▼
|
|
473
|
+
{
|
|
474
|
+
query: "entry tree",
|
|
475
|
+
limit: 5
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
CLI and MCP have different inputs, but they converge on the same decoder,
|
|
480
|
+
defaults, and projection:
|
|
481
|
+
|
|
482
|
+
```text
|
|
483
|
+
CLI argv ──> CLI Binding ──┐
|
|
484
|
+
│
|
|
485
|
+
▼
|
|
486
|
+
HintEndpoint
|
|
487
|
+
│
|
|
488
|
+
▼
|
|
489
|
+
Shared Decoder
|
|
490
|
+
│
|
|
491
|
+
▼
|
|
492
|
+
HintEntry.project()
|
|
493
|
+
│
|
|
494
|
+
▼
|
|
495
|
+
same projection
|
|
496
|
+
▲
|
|
497
|
+
│
|
|
498
|
+
MCP JSON ──> MCP Binding ──┘
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
If `limit` is omitted from either channel, both produce the Entry default:
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
{
|
|
505
|
+
query: "entry tree",
|
|
506
|
+
limit: 20,
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Where each decision belongs
|
|
511
|
+
|
|
512
|
+
When adding a command, ask these questions in order:
|
|
513
|
+
|
|
514
|
+
| Question | Definition |
|
|
515
|
+
| --- | --- |
|
|
516
|
+
| What values does the operation need? | Entry parameters |
|
|
517
|
+
| How are they validated and defaulted? | Entry codecs and requirements |
|
|
518
|
+
| What application value should be produced? | Entry `project()` |
|
|
519
|
+
| How does CLI supply each value? | CLI Binding |
|
|
520
|
+
| Which values does MCP expose? | MCP Binding |
|
|
521
|
+
| What object ties the operation and channels together? | Endpoint |
|
|
522
|
+
| Under which command path does it live? | Tree |
|
|
523
|
+
|
|
524
|
+
Do not put CLI flags, MCP property selection, or command-tree placement on the
|
|
525
|
+
Entry. Do not repeat codecs, requiredness, or defaults in Bindings.
|
|
526
|
+
|
|
527
|
+
## Suggested application file layout
|
|
528
|
+
|
|
529
|
+
For a larger application, separating these responsibilities keeps the model
|
|
530
|
+
easy to navigate:
|
|
531
|
+
|
|
532
|
+
```text
|
|
533
|
+
src/
|
|
534
|
+
├── hint-entry.ts
|
|
535
|
+
│ └── HintEntry
|
|
536
|
+
│ parameters, codecs, defaults, projection
|
|
537
|
+
│
|
|
538
|
+
├── hint-endpoint.ts
|
|
539
|
+
│ └── HintEndpoint
|
|
540
|
+
│ CLI and MCP Bindings
|
|
541
|
+
│
|
|
542
|
+
├── command-tree.ts
|
|
543
|
+
│ └── CommandTree
|
|
544
|
+
│ command hierarchy
|
|
545
|
+
│
|
|
546
|
+
├── cli.ts
|
|
547
|
+
│ └── createCliRouter(CommandTree)
|
|
548
|
+
│
|
|
549
|
+
└── mcp.ts
|
|
550
|
+
└── createMcpEndpoint(HintEndpoint)
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## Package entries
|
|
554
|
+
|
|
555
|
+
The neutral model and channel adapters are intentionally separate imports:
|
|
556
|
+
|
|
557
|
+
| Import | Purpose |
|
|
558
|
+
| --- | --- |
|
|
559
|
+
| `@alpacakit/channels` | Parameters, codecs, entries, output contracts, and entry-tree builders |
|
|
560
|
+
| `@alpacakit/channels/cli` | CLI bindings/coercion, router, typed executor, help traversal, and text renderers |
|
|
561
|
+
| `@alpacakit/channels/mcp` | MCP binding, the compiled endpoint handle, output schema projection, and the resolved-result guard |
|
|
562
|
+
|
|
563
|
+
`zod` is a peer dependency. In this workspace, depend on the package with
|
|
564
|
+
`workspace:*` and use the workspace Zod version.
|
|
565
|
+
|
|
566
|
+
## Parameter and codec reference
|
|
567
|
+
|
|
568
|
+
Built-in codecs are:
|
|
569
|
+
|
|
570
|
+
- `codecs.string()`
|
|
571
|
+
- `codecs.stringList()`
|
|
572
|
+
- `codecs.boolean()`
|
|
573
|
+
- `codecs.positiveInteger({ max })`
|
|
574
|
+
- `codecs.stringEnum(values)`
|
|
575
|
+
- `codecs.stringEnumList(values)`
|
|
576
|
+
|
|
577
|
+
Use `required()` for required input, `optional()` for an optional value without
|
|
578
|
+
a default, and `optional({ default })` for a materialized default.
|
|
579
|
+
|
|
580
|
+
Defaults belong to the Entry, not a channel. They are applied even when an
|
|
581
|
+
optional parameter is not exposed by the selected channel. A required parameter
|
|
582
|
+
must be reachable through every channel binding on that Endpoint.
|
|
583
|
+
|
|
584
|
+
`codecs.define({ schema, shape })` is available for advanced codecs. Its shape
|
|
585
|
+
is the CLI representation contract and must match the schema's value type.
|
|
586
|
+
Arrays are limited to supported scalar element shapes.
|
|
587
|
+
|
|
588
|
+
## CLI reference
|
|
589
|
+
|
|
590
|
+
Use `overrideCliSegment()` when the CLI spelling differs from the logical node
|
|
591
|
+
name:
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
import { overrideCliSegment } from "@alpacakit/channels/cli";
|
|
595
|
+
|
|
596
|
+
defineEntryNode({
|
|
597
|
+
name: "hint",
|
|
598
|
+
description: "Find implementation hints",
|
|
599
|
+
segments: [overrideCliSegment({ token: "h", aliases: ["hi"] })],
|
|
600
|
+
endpoints: [HintEndpoint],
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
CLI binding and routing rules:
|
|
605
|
+
|
|
606
|
+
- An option with no `flags` defaults to a single `--<kebab-case parameter
|
|
607
|
+
name>` long flag. Provide `flags` only to add aliases or a short flag.
|
|
608
|
+
- A value-taking option's `valueName` defaults to the upper snake-cased
|
|
609
|
+
parameter name. Boolean flags reject `valueName` because they take no value.
|
|
610
|
+
- A positional's `valueName` defaults to the upper snake-cased parameter name,
|
|
611
|
+
and `variadic` defaults to `false`. State either only to override it.
|
|
612
|
+
- Options use reachable `-x` or `--long` flags. Requiredness comes from the
|
|
613
|
+
Entry parameter, not the Binding.
|
|
614
|
+
- A defaulted boolean option is a flag by default. `negated: true` adds its
|
|
615
|
+
generated `--no-...` long flags.
|
|
616
|
+
- A boolean without a default accepts an explicit value unless `booleanMode`
|
|
617
|
+
selects another supported behavior.
|
|
618
|
+
- Array options may be repeated. `repeatMode: "comma_or_repeat"` also accepts
|
|
619
|
+
comma-separated values.
|
|
620
|
+
- A variadic positional requires an array codec and must be the final
|
|
621
|
+
positional.
|
|
622
|
+
- An executable node cannot have both positional arguments and child commands.
|
|
623
|
+
- `--` forces all remaining tokens to be parsed as positionals.
|
|
624
|
+
- A matching child command wins over an executable parent.
|
|
625
|
+
- `coercion: "json"` parses strict JSON before Entry validation.
|
|
626
|
+
`coercion: "json_or_string"` preserves malformed JSON as text. Final value
|
|
627
|
+
validation always belongs to the Entry codec.
|
|
628
|
+
|
|
629
|
+
`createCliRouter()` compiles and validates the complete CLI surface. Invalid
|
|
630
|
+
definitions throw `EntryTreeDefinitionError` at construction time.
|
|
631
|
+
|
|
632
|
+
The router's `help` property contains the compiled hierarchy, aliases,
|
|
633
|
+
render-ready option groups, positionals, requiredness, defaults, repeat modes,
|
|
634
|
+
and generated negative flags. Authored option-section names are resolved during
|
|
635
|
+
projection, so renderers consume `optionGroups` without performing a join.
|
|
636
|
+
`findCliHelpSurface()`, `listCliHelpSurfaces()`, `renderCliHelp()`, and
|
|
637
|
+
`formatCliRouterError()` provide standard lookup, traversal, one-surface text
|
|
638
|
+
rendering, and diagnostics without writing output or choosing an exit code.
|
|
639
|
+
`renderCliHelp()` implements `CliHelpRender`; its context carries the full
|
|
640
|
+
selected `programName`, root `executableName`, path, and description preference.
|
|
641
|
+
The renderer path uses canonical CLI segment tokens; a directly invoked alias
|
|
642
|
+
may still be preserved in `programName` for user-facing usage text.
|
|
643
|
+
`--help`, complete-reference framing, and `--version` remain application policy.
|
|
644
|
+
|
|
645
|
+
The router returns a discriminated union:
|
|
646
|
+
|
|
647
|
+
- `resolved`
|
|
648
|
+
- `unknown_command`
|
|
649
|
+
- `incomplete_command`
|
|
650
|
+
- `invalid_input`
|
|
651
|
+
- `internal_error`
|
|
652
|
+
|
|
653
|
+
`path` is preserved for diagnostics. Consumers may use the standard formatter
|
|
654
|
+
or supply their own wording. Process exit codes remain consumer policy.
|
|
655
|
+
|
|
656
|
+
## MCP reference
|
|
657
|
+
|
|
658
|
+
`createMcpEndpoint()` compiles the Endpoint once and returns the handle used for
|
|
659
|
+
both schema generation and runtime parsing. Do not keep a second runtime copy of
|
|
660
|
+
the MCP Binding.
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
declare const toolArguments: Record<string, unknown>;
|
|
664
|
+
|
|
665
|
+
const mcpEndpoint = createMcpEndpoint(HintEndpoint);
|
|
666
|
+
const inputShape = mcpEndpoint.inputSchema;
|
|
667
|
+
const result = mcpEndpoint.parse(toolArguments);
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
`inputSchema` is a raw Zod object shape. It includes only MCP-bound parameters
|
|
671
|
+
and projects their schemas, descriptions, and requiredness.
|
|
672
|
+
|
|
673
|
+
`parse()` applies Entry defaults to all parameters, validates bound input, and
|
|
674
|
+
runs the Entry projection. It returns:
|
|
675
|
+
|
|
676
|
+
- `resolved` with the typed projection
|
|
677
|
+
- `invalid_input` for missing input, codec failures, or an `EntryInputError`
|
|
678
|
+
deliberately thrown by `project()`
|
|
679
|
+
- `internal_error` with the original error for an unexpected `project()` throw
|
|
680
|
+
|
|
681
|
+
`isMcpResolved(result)` narrows a parse result to its `resolved` variant, the
|
|
682
|
+
MCP counterpart of `isCliResolvedEndpoint()`.
|
|
683
|
+
|
|
684
|
+
An invalid Endpoint binding throws `EntryTreeDefinitionError` from
|
|
685
|
+
`createMcpEndpoint()` itself, so definition errors surface at construction rather
|
|
686
|
+
than on the first invocation.
|
|
687
|
+
|
|
688
|
+
MCP tool naming and registration stay with the consumer. Hierarchical MCP tool
|
|
689
|
+
names are not currently derived from the Tree.
|
|
690
|
+
|
|
691
|
+
## Output contracts
|
|
692
|
+
|
|
693
|
+
An output contract is optional. Add one when the same successful result schema
|
|
694
|
+
must drive runtime verification and MCP structured output:
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
import {
|
|
698
|
+
createOutputContract,
|
|
699
|
+
defineEntry,
|
|
700
|
+
defineEntryEndpoint,
|
|
701
|
+
verifyEntryOutput,
|
|
702
|
+
} from "@alpacakit/channels";
|
|
703
|
+
import {
|
|
704
|
+
bindMcpEntry,
|
|
705
|
+
createMcpOutputSchema,
|
|
706
|
+
} from "@alpacakit/channels/mcp";
|
|
707
|
+
import { z } from "zod";
|
|
708
|
+
|
|
709
|
+
const HintOutputSchema = z
|
|
710
|
+
.object({
|
|
711
|
+
hints: z.array(z.string()).readonly(),
|
|
712
|
+
})
|
|
713
|
+
.strict()
|
|
714
|
+
.readonly();
|
|
715
|
+
|
|
716
|
+
const EntryWithOutput = defineEntry({
|
|
717
|
+
name: "hint",
|
|
718
|
+
parameters: [],
|
|
719
|
+
project: () => null,
|
|
720
|
+
output: createOutputContract(HintOutputSchema),
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const EndpointWithOutput = defineEntryEndpoint(
|
|
724
|
+
EntryWithOutput,
|
|
725
|
+
bindMcpEntry(),
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
const outputSchema = createMcpOutputSchema(EndpointWithOutput);
|
|
729
|
+
|
|
730
|
+
const verified = verifyEntryOutput(EntryWithOutput, {
|
|
731
|
+
hints: ["A validated result"],
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
void outputSchema;
|
|
735
|
+
void verified;
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
`createMcpOutputSchema()` requires an object output contract. Invalid output
|
|
739
|
+
passed to `verifyEntryOutput()` throws `EntryOutputContractError`.
|
|
740
|
+
|
|
741
|
+
## Error boundaries
|
|
742
|
+
|
|
743
|
+
- `EntryTreeDefinitionError` means the Tree or a channel Binding is invalid.
|
|
744
|
+
Treat it as a startup or programmer error.
|
|
745
|
+
- `EntryInputError` may be thrown inside `project()` for an expected semantic
|
|
746
|
+
input rejection. It becomes `invalid_input`. Pass
|
|
747
|
+
`{ parameterName: "name" }` to associate all issues with one Entry parameter;
|
|
748
|
+
an unknown parameter name is an `internal_error` programmer failure.
|
|
749
|
+
- Codec and required-input failures become `invalid_input`.
|
|
750
|
+
- Any other projection exception becomes `internal_error` and retains the
|
|
751
|
+
original error. It is never reported as caller input failure.
|
|
752
|
+
- `EntryOutputContractError` means a handler returned a value outside its
|
|
753
|
+
declared success contract.
|
|
754
|
+
|
|
755
|
+
## Scope
|
|
756
|
+
|
|
757
|
+
The package currently provides the neutral entry tree, CLI router, optional CLI
|
|
758
|
+
executor and text renderers, and MCP endpoint adapter. It does not provide an
|
|
759
|
+
HTTP router, process exit policy, automatic `--help`/`--version` handling, MCP
|
|
760
|
+
tool registration, authentication, or a network listener.
|
|
761
|
+
|
|
762
|
+
Every command surface uses `defineEntryEndpoint()` with `bindCliEntry()` and
|
|
763
|
+
`bindMcpEntry()`. There is no separate legacy option-descriptor API.
|
|
764
|
+
|
|
765
|
+
## Zod peer and source linking
|
|
766
|
+
|
|
767
|
+
Packed consumers are type-checked against the declared `zod: ^4.3.6` peer. A
|
|
768
|
+
packed channels build was verified with both Zod 4.3.6 and 4.4.3. Source-linking
|
|
769
|
+
the package is different: if channels resolves its development Zod 4.3.6 while
|
|
770
|
+
the consumer resolves 4.4.3, TypeScript can expand the two distinct Zod type
|
|
771
|
+
graphs until it reports excessive depth or exhausts memory. When linking source,
|
|
772
|
+
use exactly one shared Zod installation/version, or consume a packed tarball.
|