@acid-cat/spec-ts 0.1.0 → 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.
package/dist/index.d.ts CHANGED
@@ -58,6 +58,16 @@ export declare const CLI_EXIT_CODES: {
58
58
  readonly cancelled: 130;
59
59
  };
60
60
  export declare const cliExitCode: (outcome: Outcome<unknown, unknown>) => number;
61
+ /**
62
+ * The flags of a command, as `usage` writes them (spec/bindings/cli.md#commands): required flags first, each group in
63
+ * code point order of the names. `input` is the bundled input schema.
64
+ */
65
+ export declare function cliFlags(input: JsonObject): string[];
66
+ /**
67
+ * The `usage` of a capability in `list` (spec/bindings/cli.md#commands). `input` is the bundled input schema, or
68
+ * `undefined` when the capability has no input.
69
+ */
70
+ export declare function cliUsage(program: string, id: string, input: JsonObject | undefined): string;
61
71
  export declare const HTTP_STATUS: {
62
72
  readonly "unknown-capability": 404;
63
73
  readonly "invalid-input": 422;
package/dist/index.js CHANGED
@@ -1,174 +1,249 @@
1
- // src/index.ts
2
- var CALL_FAILURE_KINDS = ["cancelled", "unknown-capability", "unavailable", "invalid-input", "internal"];
3
- var ok = (value) => ({ ok: true, value });
4
- var fail = (tag, data) => ({ ok: false, error: { _tag: tag, ...data ?? {} } });
5
- var callFailure = (kind, message) => ({ kind, message });
6
- var isCallFailure = (outcome) => !("ok" in outcome);
1
+ //#region src/index.ts
2
+ /**
3
+ * The fixed details of the AcidCat specification, as TypeScript data and pure functions, for the TypeScript reference
4
+ * implementation. This package has no dependencies.
5
+ *
6
+ * - Outcomes and call failures: spec/invocation.md
7
+ * - Names, views and bundling: spec/catalog.md
8
+ * - Exit codes, status codes and MCP results: spec/bindings/
9
+ */
10
+ /** The kinds of call failure, in order of precedence: when more than one applies, the first one is used. */
11
+ const CALL_FAILURE_KINDS = [
12
+ "cancelled",
13
+ "unknown-capability",
14
+ "unavailable",
15
+ "invalid-input",
16
+ "internal"
17
+ ];
18
+ const ok = (value) => ({
19
+ ok: true,
20
+ value
21
+ });
22
+ /** A failure with its tag, as a result. */
23
+ const fail = (tag, data) => ({
24
+ ok: false,
25
+ error: {
26
+ _tag: tag,
27
+ ...data ?? {}
28
+ }
29
+ });
30
+ const callFailure = (kind, message) => ({
31
+ kind,
32
+ message
33
+ });
34
+ const isCallFailure = (outcome) => !("ok" in outcome);
35
+ /** The JavaScript error for a call failure (spec/bindings/typescript.md#handles). */
7
36
  var CallFailureError = class extends Error {
8
- kind;
9
- constructor(failure) {
10
- super(failure.message);
11
- this.kind = failure.kind;
12
- this.name = failure.kind === "cancelled" ? "AbortError" : failure.kind === "invalid-input" ? "TypeError" : "Error";
13
- }
37
+ kind;
38
+ constructor(failure) {
39
+ super(failure.message);
40
+ this.kind = failure.kind;
41
+ this.name = failure.kind === "cancelled" ? "AbortError" : failure.kind === "invalid-input" ? "TypeError" : "Error";
42
+ }
14
43
  };
15
- var AVAILABLE = { available: true };
16
- var unavailable = (reason) => ({ available: false, reason });
17
- var ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)+$/;
18
- var isId = (id) => id.length <= 128 && ID_PATTERN.test(id);
19
- var commandPath = (id) => id.split(".");
20
- var CLI_EXIT_CODES = {
21
- value: 0,
22
- failure: 1,
23
- "unknown-capability": 2,
24
- "invalid-input": 3,
25
- unavailable: 4,
26
- internal: 5,
27
- cancelled: 130
44
+ const AVAILABLE = { available: true };
45
+ const unavailable = (reason) => ({
46
+ available: false,
47
+ reason
48
+ });
49
+ const ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)+$/;
50
+ const isId = (id) => id.length <= 128 && ID_PATTERN.test(id);
51
+ /** The path of the CLI command for an id: `aircon.power.set` → `["aircon", "power", "set"]`. */
52
+ const commandPath = (id) => id.split(".");
53
+ const CLI_EXIT_CODES = {
54
+ value: 0,
55
+ failure: 1,
56
+ "unknown-capability": 2,
57
+ "invalid-input": 3,
58
+ unavailable: 4,
59
+ internal: 5,
60
+ cancelled: 130
28
61
  };
29
- var cliExitCode = (outcome) => isCallFailure(outcome) ? CLI_EXIT_CODES[outcome.kind] : outcome.ok ? CLI_EXIT_CODES.value : CLI_EXIT_CODES.failure;
30
- var HTTP_STATUS = {
31
- "unknown-capability": 404,
32
- "invalid-input": 422,
33
- unavailable: 503,
34
- cancelled: 409,
35
- internal: 500
62
+ const cliExitCode = (outcome) => isCallFailure(outcome) ? CLI_EXIT_CODES[outcome.kind] : outcome.ok ? CLI_EXIT_CODES.value : CLI_EXIT_CODES.failure;
63
+ /**
64
+ * The flags of a command, as `usage` writes them (spec/bindings/cli.md#commands): required flags first, each group in
65
+ * code point order of the names. `input` is the bundled input schema.
66
+ */
67
+ function cliFlags(input) {
68
+ const properties = input.properties ?? {};
69
+ const definitions = input.$defs ?? {};
70
+ const required = new Set(input.required ?? []);
71
+ const byName = (a, b) => a < b ? -1 : a > b ? 1 : 0;
72
+ const names = Object.keys(properties).sort(byName);
73
+ return [...names.filter((name) => required.has(name)), ...names.filter((name) => !required.has(name))].map((name) => {
74
+ const reference = properties[name].$ref;
75
+ const target = typeof reference === "string" ? REFERENCE.exec(reference)?.[1] : void 0;
76
+ const property = (target === void 0 ? void 0 : definitions[target]) ?? properties[name];
77
+ const flag = property.type === "boolean" ? `--${name}` : `--${name} <${flagValue(property)}>`;
78
+ return required.has(name) ? flag : `[${flag}]`;
79
+ });
80
+ }
81
+ function flagValue(property) {
82
+ if (Array.isArray(property.enum) && property.enum.every((value) => typeof value === "string")) return property.enum.join("|");
83
+ if (property.type === "integer" || property.type === "number" || property.type === "string") return property.type;
84
+ return "json";
85
+ }
86
+ /**
87
+ * The `usage` of a capability in `list` (spec/bindings/cli.md#commands). `input` is the bundled input schema, or
88
+ * `undefined` when the capability has no input.
89
+ */
90
+ function cliUsage(program, id, input) {
91
+ if (input !== void 0 && input.properties === void 0) return `${program} call ${id} '<json>'`;
92
+ return [
93
+ program,
94
+ ...commandPath(id),
95
+ ...input === void 0 ? [] : cliFlags(input)
96
+ ].join(" ");
97
+ }
98
+ const HTTP_STATUS = {
99
+ "unknown-capability": 404,
100
+ "invalid-input": 422,
101
+ unavailable: 503,
102
+ cancelled: 409,
103
+ internal: 500
36
104
  };
37
- var httpStatus = (outcome) => isCallFailure(outcome) ? HTTP_STATUS[outcome.kind] : 200;
105
+ const httpStatus = (outcome) => isCallFailure(outcome) ? HTTP_STATUS[outcome.kind] : 200;
106
+ /**
107
+ * The first rule that a request breaks, before its body is read: method, origin, media type, declared length.
108
+ * The body itself is checked as it arrives, with `readBounded`.
109
+ */
38
110
  function screenRequest(request, rules) {
39
- if (!rules.methods.includes(request.method)) return "method";
40
- const origin = request.headers.get("Origin");
41
- if (origin !== null && !rules.allowedOrigins.includes(origin)) return "origin";
42
- if (request.method === "GET" || request.method === "HEAD") return void 0;
43
- const mediaType = (request.headers.get("Content-Type") ?? "").split(";")[0].trim().toLowerCase();
44
- if (mediaType !== rules.mediaType) return "media-type";
45
- const declared = Number(request.headers.get("Content-Length") ?? "");
46
- if (Number.isFinite(declared) && declared > rules.maximumBytes) return "too-large";
47
- return void 0;
111
+ if (!rules.methods.includes(request.method)) return "method";
112
+ const origin = request.headers.get("Origin");
113
+ if (origin !== null && !rules.allowedOrigins.includes(origin)) return "origin";
114
+ if (request.method === "GET" || request.method === "HEAD") return void 0;
115
+ if ((request.headers.get("Content-Type") ?? "").split(";")[0].trim().toLowerCase() !== rules.mediaType) return "media-type";
116
+ const declared = Number(request.headers.get("Content-Length") ?? "");
117
+ if (Number.isFinite(declared) && declared > rules.maximumBytes) return "too-large";
48
118
  }
119
+ /** Read a request body as UTF-8 text. Count the bytes as they arrive, and stop at the limit. */
49
120
  async function readBounded(request, maximumBytes) {
50
- if (request.body === null) return { text: "" };
51
- const reader = request.body.getReader();
52
- const chunks = [];
53
- let length = 0;
54
- for (; ; ) {
55
- const { done, value } = await reader.read();
56
- if (done) break;
57
- length += value.byteLength;
58
- if (length > maximumBytes) {
59
- await reader.cancel().catch(() => void 0);
60
- return { rejection: "too-large" };
61
- }
62
- chunks.push(value);
63
- }
64
- const bytes = new Uint8Array(length);
65
- let offset = 0;
66
- for (const chunk of chunks) {
67
- bytes.set(chunk, offset);
68
- offset += chunk.byteLength;
69
- }
70
- try {
71
- return { text: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
72
- } catch {
73
- return { rejection: "not-utf8" };
74
- }
121
+ if (request.body === null) return { text: "" };
122
+ const reader = request.body.getReader();
123
+ const chunks = [];
124
+ let length = 0;
125
+ for (;;) {
126
+ const { done, value } = await reader.read();
127
+ if (done) break;
128
+ length += value.byteLength;
129
+ if (length > maximumBytes) {
130
+ await reader.cancel().catch(() => void 0);
131
+ return { rejection: "too-large" };
132
+ }
133
+ chunks.push(value);
134
+ }
135
+ const bytes = new Uint8Array(length);
136
+ let offset = 0;
137
+ for (const chunk of chunks) {
138
+ bytes.set(chunk, offset);
139
+ offset += chunk.byteLength;
140
+ }
141
+ try {
142
+ return { text: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
143
+ } catch {
144
+ return { rejection: "not-utf8" };
145
+ }
75
146
  }
76
- var MCP_PROTOCOL_VERSION = "2026-07-28";
77
- var MCP_UNKNOWN_TOOL = -32602;
78
- var MCP_REJECTIONS = {
79
- method: { status: 405 },
80
- origin: { status: 403 },
81
- "media-type": { status: 415 },
82
- "too-large": { status: 413, code: -32600 },
83
- "not-utf8": { status: 400, code: -32700 }
147
+ const MCP_PROTOCOL_VERSION = "2026-07-28";
148
+ const MCP_UNKNOWN_TOOL = -32602;
149
+ /** The HTTP status and JSON-RPC error code for each rejected MCP request. */
150
+ const MCP_REJECTIONS = {
151
+ method: { status: 405 },
152
+ origin: { status: 403 },
153
+ "media-type": { status: 415 },
154
+ "too-large": {
155
+ status: 413,
156
+ code: -32600
157
+ },
158
+ "not-utf8": {
159
+ status: 400,
160
+ code: -32700
161
+ }
84
162
  };
163
+ /** The MCP tool result for an outcome that is not `unknown-capability`. */
85
164
  function mcpToolResult(outcome, hasOutputSchema) {
86
- const text = (value) => [{ type: "text", text: JSON.stringify(value) }];
87
- if (isCallFailure(outcome)) return { content: text({ kind: outcome.kind, message: outcome.message }), isError: true };
88
- if (!outcome.ok) return { content: text({ ok: false, error: outcome.error }), isError: true };
89
- return hasOutputSchema ? { content: text(outcome.value), structuredContent: outcome.value } : { content: text(outcome.value) };
165
+ const text = (value) => [{
166
+ type: "text",
167
+ text: JSON.stringify(value)
168
+ }];
169
+ if (isCallFailure(outcome)) return {
170
+ content: text({
171
+ kind: outcome.kind,
172
+ message: outcome.message
173
+ }),
174
+ isError: true
175
+ };
176
+ if (!outcome.ok) return {
177
+ content: text({
178
+ ok: false,
179
+ error: outcome.error
180
+ }),
181
+ isError: true
182
+ };
183
+ return hasOutputSchema ? {
184
+ content: text(outcome.value),
185
+ structuredContent: outcome.value
186
+ } : { content: text(outcome.value) };
90
187
  }
91
- var REFERENCE = /^#\/\$defs\/([A-Za-z][A-Za-z0-9_]*)$/;
188
+ const REFERENCE = /^#\/\$defs\/([A-Za-z][A-Za-z0-9_]*)$/;
189
+ /** The schemas of a capability, each with the member that holds it. */
92
190
  function schemasOf(capability) {
93
- const members = [
94
- ["input", capability.input],
95
- ["output", capability.output],
96
- ["state", capability.state],
97
- ...Object.entries(capability.failures ?? {}).map(([tag, schema]) => [`failure ${tag}`, schema])
98
- ];
99
- return members.filter((entry) => entry[1] !== void 0);
191
+ return [
192
+ ["input", capability.input],
193
+ ["output", capability.output],
194
+ ["state", capability.state],
195
+ ...Object.entries(capability.failures ?? {}).map(([tag, schema]) => [`failure ${tag}`, schema])
196
+ ].filter((entry) => entry[1] !== void 0);
100
197
  }
198
+ /** Every object in a JSON value, at all levels. */
101
199
  function visitObjects(value, onObject) {
102
- if (Array.isArray(value)) {
103
- value.forEach((item) => visitObjects(item, onObject));
104
- } else if (value !== null && typeof value === "object") {
105
- onObject(value);
106
- Object.values(value).forEach((item) => visitObjects(item, onObject));
107
- }
200
+ if (Array.isArray(value)) value.forEach((item) => visitObjects(item, onObject));
201
+ else if (value !== null && typeof value === "object") {
202
+ onObject(value);
203
+ Object.values(value).forEach((item) => visitObjects(item, onObject));
204
+ }
108
205
  }
206
+ /** The types that a schema uses, directly or through other types. */
109
207
  function typesUsedBy(schema, types) {
110
- const used = /* @__PURE__ */ new Map();
111
- const collect = (node) => visitObjects(node, (inner) => {
112
- const name = typeof inner.$ref === "string" ? REFERENCE.exec(inner.$ref)?.[1] : void 0;
113
- const type = name === void 0 ? void 0 : types[name];
114
- if (name !== void 0 && type !== void 0 && !used.has(name)) {
115
- used.set(name, type);
116
- collect(type);
117
- }
118
- });
119
- collect(schema);
120
- return used;
208
+ const used = /* @__PURE__ */ new Map();
209
+ const collect = (node) => visitObjects(node, (inner) => {
210
+ const name = typeof inner.$ref === "string" ? REFERENCE.exec(inner.$ref)?.[1] : void 0;
211
+ const type = name === void 0 ? void 0 : types[name];
212
+ if (name !== void 0 && type !== void 0 && !used.has(name)) {
213
+ used.set(name, type);
214
+ collect(type);
215
+ }
216
+ });
217
+ collect(schema);
218
+ return used;
121
219
  }
220
+ /** Bundle a schema (spec/catalog.md#bundling). */
122
221
  function bundle(schema, types) {
123
- const only = Object.keys(schema).length === 1 ? REFERENCE.exec(String(schema.$ref))?.[1] : void 0;
124
- const base = only === void 0 ? schema : types[only] ?? schema;
125
- const used = typesUsedBy(base, types);
126
- return used.size === 0 ? base : { ...base, $defs: Object.fromEntries(used) };
222
+ const only = Object.keys(schema).length === 1 ? REFERENCE.exec(String(schema.$ref))?.[1] : void 0;
223
+ const base = only === void 0 ? schema : types[only] ?? schema;
224
+ const used = typesUsedBy(base, types);
225
+ return used.size === 0 ? base : {
226
+ ...base,
227
+ $defs: Object.fromEntries(used)
228
+ };
127
229
  }
230
+ /** The view of a target (spec/catalog.md#views). */
128
231
  function viewOf(catalog, target) {
129
- const types = catalog.$defs ?? {};
130
- const capabilities = Object.fromEntries(
131
- Object.entries(catalog.capabilities).filter(([, capability]) => (capability.audience ?? []).includes(target)).map(([id, capability]) => [id, { ...capability, audience: [target] }])
132
- );
133
- const used = /* @__PURE__ */ new Map();
134
- for (const capability of Object.values(capabilities)) {
135
- for (const [, schema] of schemasOf(capability)) for (const [name, type] of typesUsedBy(schema, types)) used.set(name, type);
136
- }
137
- const { $defs: _, ...rest } = catalog;
138
- const declared = catalog.targets[target];
139
- return {
140
- ...rest,
141
- targets: declared === void 0 ? {} : { [target]: declared },
142
- ...used.size > 0 ? { $defs: Object.fromEntries(used) } : {},
143
- capabilities
144
- };
232
+ const types = catalog.$defs ?? {};
233
+ const capabilities = Object.fromEntries(Object.entries(catalog.capabilities).filter(([, capability]) => (capability.audience ?? []).includes(target)).map(([id, capability]) => [id, {
234
+ ...capability,
235
+ audience: [target]
236
+ }]));
237
+ const used = /* @__PURE__ */ new Map();
238
+ for (const capability of Object.values(capabilities)) for (const [, schema] of schemasOf(capability)) for (const [name, type] of typesUsedBy(schema, types)) used.set(name, type);
239
+ const { $defs: _, ...rest } = catalog;
240
+ const declared = catalog.targets[target];
241
+ return {
242
+ ...rest,
243
+ targets: declared === void 0 ? {} : { [target]: declared },
244
+ ...used.size > 0 ? { $defs: Object.fromEntries(used) } : {},
245
+ capabilities
246
+ };
145
247
  }
146
- export {
147
- AVAILABLE,
148
- CALL_FAILURE_KINDS,
149
- CLI_EXIT_CODES,
150
- CallFailureError,
151
- HTTP_STATUS,
152
- ID_PATTERN,
153
- MCP_PROTOCOL_VERSION,
154
- MCP_REJECTIONS,
155
- MCP_UNKNOWN_TOOL,
156
- REFERENCE,
157
- bundle,
158
- callFailure,
159
- cliExitCode,
160
- commandPath,
161
- fail,
162
- httpStatus,
163
- isCallFailure,
164
- isId,
165
- mcpToolResult,
166
- ok,
167
- readBounded,
168
- schemasOf,
169
- screenRequest,
170
- typesUsedBy,
171
- unavailable,
172
- viewOf,
173
- visitObjects
174
- };
248
+ //#endregion
249
+ export { AVAILABLE, CALL_FAILURE_KINDS, CLI_EXIT_CODES, CallFailureError, HTTP_STATUS, ID_PATTERN, MCP_PROTOCOL_VERSION, MCP_REJECTIONS, MCP_UNKNOWN_TOOL, REFERENCE, bundle, callFailure, cliExitCode, cliFlags, cliUsage, commandPath, fail, httpStatus, isCallFailure, isId, mcpToolResult, ok, readBounded, schemasOf, screenRequest, typesUsedBy, unavailable, viewOf, visitObjects };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acid-cat/spec-ts",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "The fixed details of the AcidCat specification as TypeScript data and pure functions, with no dependencies. Part of the TypeScript reference implementation.",
5
5
  "keywords": [
6
6
  "acidcat",