@acid-cat/core-ts 0.3.0 → 0.4.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/README.md +36 -1
- package/dist/app.d.ts +23 -14
- package/dist/app.js +22 -4
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +22 -3
- package/dist/http.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/session.d.ts +25 -7
- package/dist/session.js +49 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -35,9 +35,10 @@ export const aircon = defineApp<{ unit: Unit }>({
|
|
|
35
35
|
|
|
36
36
|
- `run` gets the input, an `AbortSignal` and the context. It returns `ok(value)` or `fail(tag, data)`. If it throws, the call fails as `internal`.
|
|
37
37
|
- A failure schema describes the whole failure object, with its `_tag`.
|
|
38
|
-
- `availability` is an optional function that returns an observable availability.
|
|
38
|
+
- `availability` is an optional function that returns an observable availability. A session reads it with `getSnapshot` when a call comes in. It subscribes to it only while `onChange` or the handle has listeners.
|
|
39
39
|
- `state` is an optional `{ schema, source }`: the schema of the [state](../../spec/invocation.md#state), and a function that returns an observable. The handle gives it as `snapshot`.
|
|
40
40
|
- `capabilitiesFor<Context>()` sets the context type one time. The other types come from the schemas.
|
|
41
|
+
- `defineCapability` and `capabilitiesFor` keep the literal type of `id`, so that an application can type its ids. `run`, `availability` and `state` all get the same `Context`.
|
|
41
42
|
|
|
42
43
|
## Write the catalog
|
|
43
44
|
|
|
@@ -60,6 +61,20 @@ Standard Schema does not convert to JSON Schema. Give the converter of your sche
|
|
|
60
61
|
| `cliMain(app, options)`, `runCli(…)` | [CLI](../../spec/bindings/cli.md) | The command line of the process, or an exit code |
|
|
61
62
|
| `installWebMcp(session)` | [WebMCP](../../spec/bindings/webmcp.md) | Tools in the page, with a status |
|
|
62
63
|
|
|
64
|
+
When the signal of a request aborts, `mcpHandler` and `httpHandler` cancel the call of the request.
|
|
65
|
+
|
|
66
|
+
### Typed handles
|
|
67
|
+
|
|
68
|
+
`getHandle(session.handles, capability)` gives the handle of a capability with the types of its schemas. For a capability outside the view, the handle is always unavailable, and each call rejects as `unknown-capability`.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { createSession, getHandle } from "@acid-cat/core-ts";
|
|
72
|
+
|
|
73
|
+
const session = createSession(aircon, { target: "app", context: { unit }, toJsonSchema });
|
|
74
|
+
const power = getHandle(session.handles, powerSet); // HandleOf<typeof powerSet>
|
|
75
|
+
const result = await power.execute({ on: true }); // Result<null, { _tag: "Offline"; since?: string }>
|
|
76
|
+
```
|
|
77
|
+
|
|
63
78
|
### Context for each request
|
|
64
79
|
|
|
65
80
|
`mcpHandler` and `httpHandler` take `context` or `createContext`. With `context`, all requests share one context. With `createContext(request)`, the handler makes a new context for each request that passes the screening. Use it when a request must not use objects that an earlier request made, for example on Cloudflare Workers, or when a stateful service must not be shared between callers. When the context has `Symbol.asyncDispose` or `Symbol.dispose`, the handler calls it after the response:
|
|
@@ -84,6 +99,26 @@ const handler = httpHandler(aircon, {
|
|
|
84
99
|
});
|
|
85
100
|
```
|
|
86
101
|
|
|
102
|
+
### Commands of the application
|
|
103
|
+
|
|
104
|
+
The option `commands` of `cliMain` and `runCli` adds commands of the application beside the commands of the capabilities, `call` and `list`. A command has a `name`, a `description` for the help, and `run(args, io)`. `run` gets the arguments after the name of the command, and `io` with `stdout`, `stderr`, `signal` and the `context`. It returns the exit code. The name must not be `call`, `list`, the name of another command of the application, or the first word of the command of a capability. If it is, `runCli` throws. The option `description` gives the text at the top of the help. The default is the description of the application, then its title, then the name of the program.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { cliMain } from "@acid-cat/core-ts";
|
|
108
|
+
import type { CliCommand } from "@acid-cat/core-ts";
|
|
109
|
+
|
|
110
|
+
const report: CliCommand<{ unit: Unit }> = {
|
|
111
|
+
name: "report",
|
|
112
|
+
description: "Write the temperature of the room.",
|
|
113
|
+
run: (_args, { context, stdout }) => {
|
|
114
|
+
stdout(`The room is at ${context.unit.state.roomCelsius} °C.`);
|
|
115
|
+
return 0;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
cliMain(aircon, { name: "aircon", version: "0.1.0", context: { unit }, toJsonSchema, description: "Control the air conditioner.", commands: [report] });
|
|
120
|
+
```
|
|
121
|
+
|
|
87
122
|
## Examples and tests
|
|
88
123
|
|
|
89
124
|
- [`examples/aircon.ts`](examples/aircon.ts) is the air conditioner of [`examples/aircon.pkl`](../../examples/aircon.pkl), with a simulated unit. Its catalog agrees with the Pkl catalog.
|
package/dist/app.d.ts
CHANGED
|
@@ -7,8 +7,12 @@ export interface Observable<T> {
|
|
|
7
7
|
unsubscribe(): void;
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
type
|
|
10
|
+
/** The input that `run` receives: the output of the input schema, or an empty object. */
|
|
11
|
+
export type InputOf<S> = S extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<S> : Record<string, never>;
|
|
12
|
+
/** The value of a successful call: the output of the output schema, or `null`. */
|
|
13
|
+
export type Output<S> = S extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<S> : null;
|
|
14
|
+
/** The failures, each with its `_tag`. */
|
|
15
|
+
export type FailureOf<F> = {
|
|
12
16
|
[Tag in keyof F]: {
|
|
13
17
|
readonly _tag: Tag;
|
|
14
18
|
} & (F[Tag] extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<F[Tag]> : object);
|
|
@@ -17,13 +21,16 @@ export interface Capability<Context = unknown, Input extends StandardSchemaV1 |
|
|
|
17
21
|
readonly [tag: string]: StandardSchemaV1;
|
|
18
22
|
} = {
|
|
19
23
|
readonly [tag: string]: StandardSchemaV1;
|
|
20
|
-
}> {
|
|
21
|
-
readonly id:
|
|
24
|
+
}, StateSchema extends StandardSchemaV1 = StandardSchemaV1, Id extends string = string> {
|
|
25
|
+
readonly id: Id;
|
|
22
26
|
readonly description: string;
|
|
23
27
|
/** The targets that can use the capability. With no audience, no target can use it. */
|
|
24
28
|
readonly audience: readonly string[];
|
|
25
29
|
readonly readOnly?: boolean | undefined;
|
|
26
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* The input: an object schema, or a union of object schemas. The catalog writes a union with `"type": "object"`. With
|
|
32
|
+
* no input schema, the input is an empty object.
|
|
33
|
+
*/
|
|
27
34
|
readonly input?: Input;
|
|
28
35
|
/** The value of a successful call. With no output schema, the value is `null`. */
|
|
29
36
|
readonly output?: OutputSchema;
|
|
@@ -31,21 +38,21 @@ export interface Capability<Context = unknown, Input extends StandardSchemaV1 |
|
|
|
31
38
|
readonly failures?: Failures;
|
|
32
39
|
/** A value that clients can observe (spec/invocation.md#state): its schema, and its source. */
|
|
33
40
|
readonly state?: {
|
|
34
|
-
readonly schema:
|
|
35
|
-
readonly source: (context: Context) => Observable<
|
|
41
|
+
readonly schema: StateSchema;
|
|
42
|
+
readonly source: (context: Context) => Observable<StandardSchemaV1.InferOutput<StateSchema>>;
|
|
36
43
|
} | undefined;
|
|
37
44
|
/** The availability of the capability. With none, the capability is always available. */
|
|
38
45
|
readonly availability?: ((context: Context) => Observable<Availability>) | undefined;
|
|
39
|
-
readonly run: (input:
|
|
46
|
+
readonly run: (input: InputOf<Input>, call: {
|
|
40
47
|
readonly signal: AbortSignal;
|
|
41
48
|
readonly context: Context;
|
|
42
49
|
}) => Promise<Result<Output<OutputSchema>, FailureOf<Failures>>> | Result<Output<OutputSchema>, FailureOf<Failures>>;
|
|
43
50
|
}
|
|
44
|
-
export type AnyCapability<Context = any> = Capability<Context, any, any, any>;
|
|
45
|
-
/** Declare a capability. `Context` is the type of the services that `run` receives. */
|
|
51
|
+
export type AnyCapability<Context = any> = Capability<Context, any, any, any, any>;
|
|
52
|
+
/** Declare a capability. `Context` is the type of the services that `run` receives. The id keeps its literal type. */
|
|
46
53
|
export declare const defineCapability: <Context = unknown, Input extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined, Failures extends {
|
|
47
54
|
readonly [tag: string]: StandardSchemaV1;
|
|
48
|
-
} = {}>(capability: Capability<Context, Input, OutputSchema, Failures>) => Capability<Context, Input, OutputSchema, Failures>;
|
|
55
|
+
} = {}, StateSchema extends StandardSchemaV1 = never, const Id extends string = string>(capability: Capability<Context, Input, OutputSchema, Failures, StateSchema, Id>) => Capability<Context, Input, OutputSchema, Failures, StateSchema, Id>;
|
|
49
56
|
/**
|
|
50
57
|
* Declare capabilities for one context type. The other types come from the schemas:
|
|
51
58
|
*
|
|
@@ -56,7 +63,7 @@ export declare const defineCapability: <Context = unknown, Input extends Standar
|
|
|
56
63
|
*/
|
|
57
64
|
export declare const capabilitiesFor: <Context>() => <Input extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined, Failures extends {
|
|
58
65
|
readonly [tag: string]: StandardSchemaV1;
|
|
59
|
-
} = {}>(capability: Capability<Context, Input, OutputSchema, Failures>) => Capability<Context, Input, OutputSchema, Failures>;
|
|
66
|
+
} = {}, StateSchema extends StandardSchemaV1 = never, const Id extends string = string>(capability: Capability<Context, Input, OutputSchema, Failures, StateSchema, Id>) => Capability<Context, Input, OutputSchema, Failures, StateSchema, Id>;
|
|
60
67
|
export interface Target {
|
|
61
68
|
readonly bindings: readonly string[];
|
|
62
69
|
readonly description?: string | undefined;
|
|
@@ -74,7 +81,10 @@ export interface App<Context = unknown> {
|
|
|
74
81
|
export declare const defineApp: <Context = unknown>(app: App<Context>) => App<Context>;
|
|
75
82
|
/** Converts a schema to JSON Schema 2020-12, for example `(schema) => z.toJSONSchema(schema)` with Zod. */
|
|
76
83
|
export type ToJsonSchema = (schema: StandardSchemaV1) => JsonObject;
|
|
77
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* The catalog of an application, as a JSON document. The catalog is written one time for each application and
|
|
86
|
+
* `toJsonSchema`. Later calls with the same two give the same document.
|
|
87
|
+
*/
|
|
78
88
|
export declare function catalogOf(app: App<any>, options: {
|
|
79
89
|
readonly toJsonSchema: ToJsonSchema;
|
|
80
90
|
}): CatalogDocument;
|
|
@@ -82,4 +92,3 @@ export declare function catalogOf(app: App<any>, options: {
|
|
|
82
92
|
export declare const renderCatalog: (app: App<any>, options: {
|
|
83
93
|
readonly toJsonSchema: ToJsonSchema;
|
|
84
94
|
}) => string;
|
|
85
|
-
export {};
|
package/dist/app.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { canonicalText } from "@acid-cat/catalog";
|
|
2
|
+
import { objectInput } from "@acid-cat/spec-ts";
|
|
2
3
|
//#region src/app.ts
|
|
3
4
|
/**
|
|
4
5
|
* Capabilities and applications in plain TypeScript. An application writes its own catalog.
|
|
5
6
|
* See spec/catalog.md.
|
|
6
7
|
*/
|
|
7
|
-
/** Declare a capability. `Context` is the type of the services that `run` receives. */
|
|
8
|
+
/** Declare a capability. `Context` is the type of the services that `run` receives. The id keeps its literal type. */
|
|
8
9
|
const defineCapability = (capability) => capability;
|
|
9
10
|
/**
|
|
10
11
|
* Declare capabilities for one context type. The other types come from the schemas:
|
|
@@ -17,11 +18,28 @@ const defineCapability = (capability) => capability;
|
|
|
17
18
|
const capabilitiesFor = () => (capability) => capability;
|
|
18
19
|
/** Declare an application. */
|
|
19
20
|
const defineApp = (app) => app;
|
|
20
|
-
|
|
21
|
+
const catalogs = /* @__PURE__ */ new WeakMap();
|
|
22
|
+
/**
|
|
23
|
+
* The catalog of an application, as a JSON document. The catalog is written one time for each application and
|
|
24
|
+
* `toJsonSchema`. Later calls with the same two give the same document.
|
|
25
|
+
*/
|
|
21
26
|
function catalogOf(app, options) {
|
|
27
|
+
let byConverter = catalogs.get(app);
|
|
28
|
+
if (byConverter === void 0) {
|
|
29
|
+
byConverter = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
catalogs.set(app, byConverter);
|
|
31
|
+
}
|
|
32
|
+
let written = byConverter.get(options.toJsonSchema);
|
|
33
|
+
if (written === void 0) {
|
|
34
|
+
written = write(app, options.toJsonSchema);
|
|
35
|
+
byConverter.set(options.toJsonSchema, written);
|
|
36
|
+
}
|
|
37
|
+
return written;
|
|
38
|
+
}
|
|
39
|
+
function write(app, toJsonSchema) {
|
|
22
40
|
const types = {};
|
|
23
41
|
const toJson = (schema, where) => {
|
|
24
|
-
const { $schema: _, $defs, ...json } =
|
|
42
|
+
const { $schema: _, $defs, ...json } = toJsonSchema(schema);
|
|
25
43
|
for (const [name, definition] of Object.entries($defs ?? {})) {
|
|
26
44
|
const existing = types[name];
|
|
27
45
|
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(definition)) throw new Error(`${where}: two different schemas have the name ${name}. Give each one its own name.`);
|
|
@@ -36,7 +54,7 @@ function catalogOf(app, options) {
|
|
|
36
54
|
capabilities[capability.id] = {
|
|
37
55
|
description: capability.description,
|
|
38
56
|
audience: capability.audience,
|
|
39
|
-
...capability.input === void 0 ? {} : { input: toJson(capability.input, capability.id) },
|
|
57
|
+
...capability.input === void 0 ? {} : { input: objectInput(toJson(capability.input, capability.id), types) },
|
|
40
58
|
...capability.output === void 0 ? {} : { output: toJson(capability.output, capability.id) },
|
|
41
59
|
...Object.keys(failures).length === 0 ? {} : { failures },
|
|
42
60
|
...capability.readOnly === true ? { readOnly: true } : {},
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,12 +6,32 @@ export interface CliOptions<Context> {
|
|
|
6
6
|
readonly toJsonSchema: ToJsonSchema;
|
|
7
7
|
/** The target that the command line serves. The default is `"cli"`. */
|
|
8
8
|
readonly target?: string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* The text at the top of the help. The default is the description of the application, then its title, then `name`.
|
|
11
|
+
*/
|
|
12
|
+
readonly description?: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The commands of the application, beside the commands of the capabilities, `call` and `list`. The name of each one
|
|
15
|
+
* must not be `call`, `list`, the name of another command of the application, or the first word of the command of a
|
|
16
|
+
* capability (spec/bindings/cli.md#commands).
|
|
17
|
+
*/
|
|
18
|
+
readonly commands?: readonly CliCommand<Context>[] | undefined;
|
|
9
19
|
}
|
|
10
20
|
export interface CliIo {
|
|
11
21
|
readonly stdout: (text: string) => void;
|
|
12
22
|
readonly stderr: (text: string) => void;
|
|
13
23
|
readonly signal?: AbortSignal | undefined;
|
|
14
24
|
}
|
|
25
|
+
/** A command that the application adds to the command line. */
|
|
26
|
+
export interface CliCommand<Context> {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
/** The text that the help gives for the command. */
|
|
29
|
+
readonly description: string;
|
|
30
|
+
/** Run the command with the arguments that come after its name. The result is the exit code. */
|
|
31
|
+
run(args: readonly string[], io: CliIo & {
|
|
32
|
+
readonly context: Context;
|
|
33
|
+
}): number | Promise<number>;
|
|
34
|
+
}
|
|
15
35
|
/** Run the command line as the main program of the process. An interrupt (`SIGINT`) cancels the call. */
|
|
16
36
|
export declare function main<Context>(app: App<Context>, options: CliOptions<Context>): void;
|
|
17
37
|
/** Run the command line with arguments. The result is the exit code (spec/bindings/cli.md#outcomes). */
|
package/dist/cli.js
CHANGED
|
@@ -36,6 +36,16 @@ async function runCli(app, options, args, io) {
|
|
|
36
36
|
path: commandPath(id)
|
|
37
37
|
}));
|
|
38
38
|
for (const { path } of commands) if (path[0] === "call" || path[0] === "list") throw new Error(`The command ${path.join(" ")} has the same name as a command of the command line.`);
|
|
39
|
+
const added = options.commands ?? [];
|
|
40
|
+
const taken = /* @__PURE__ */ new Set([
|
|
41
|
+
"call",
|
|
42
|
+
"list",
|
|
43
|
+
...commands.map(({ path }) => path[0])
|
|
44
|
+
]);
|
|
45
|
+
for (const command of added) {
|
|
46
|
+
if (taken.has(command.name)) throw new Error(`The command ${command.name} of the application has the same name as another command.`);
|
|
47
|
+
taken.add(command.name);
|
|
48
|
+
}
|
|
39
49
|
const print = (outcome) => {
|
|
40
50
|
if (isCallFailure(outcome)) io.stderr(JSON.stringify({
|
|
41
51
|
kind: outcome.kind,
|
|
@@ -47,7 +57,8 @@ async function runCli(app, options, args, io) {
|
|
|
47
57
|
try {
|
|
48
58
|
const [first, ...rest] = args;
|
|
49
59
|
if (first === void 0 || first === "--help" || first === "-h") {
|
|
50
|
-
|
|
60
|
+
const description = options.description ?? app.description ?? app.title ?? options.name;
|
|
61
|
+
io.stdout(help(options.name, description, commands.map(({ path }) => path.join(" ")), added));
|
|
51
62
|
return CLI_EXIT_CODES.value;
|
|
52
63
|
}
|
|
53
64
|
if (first === "--version" || first === "-v") {
|
|
@@ -80,6 +91,11 @@ async function runCli(app, options, args, io) {
|
|
|
80
91
|
}
|
|
81
92
|
return print(await session.invoke(id, input, { signal: io.signal }));
|
|
82
93
|
}
|
|
94
|
+
const command = added.find(({ name }) => name === first);
|
|
95
|
+
if (command !== void 0) return await command.run(rest, {
|
|
96
|
+
...io,
|
|
97
|
+
context: options.context
|
|
98
|
+
});
|
|
83
99
|
const match = commands.filter(({ path }) => path.every((word, index) => args[index] === word)).sort((a, b) => b.path.length - a.path.length)[0];
|
|
84
100
|
if (match === void 0) throw new UsageError(`There is no command ${args.filter((arg) => !arg.startsWith("-")).join(" ")}.`, CLI_EXIT_CODES["unknown-capability"]);
|
|
85
101
|
const capability = session.view.capabilities[match.id];
|
|
@@ -142,14 +158,17 @@ function resolve(property, definitions) {
|
|
|
142
158
|
const name = typeof property.$ref === "string" ? REFERENCE.exec(property.$ref)?.[1] : void 0;
|
|
143
159
|
return name === void 0 ? property : definitions[name] ?? property;
|
|
144
160
|
}
|
|
145
|
-
function help(name, paths) {
|
|
161
|
+
function help(name, description, paths, added) {
|
|
146
162
|
return [
|
|
163
|
+
description,
|
|
164
|
+
"",
|
|
147
165
|
`Usage: ${name} <command> [flags]`,
|
|
148
166
|
"",
|
|
149
167
|
"Commands:",
|
|
150
168
|
...paths.map((path) => ` ${path}`),
|
|
151
169
|
" call <id> [input]",
|
|
152
|
-
" list"
|
|
170
|
+
" list",
|
|
171
|
+
...added.map((command) => ` ${command.name} ${command.description}`)
|
|
153
172
|
].join("\n");
|
|
154
173
|
}
|
|
155
174
|
function usage(name, path, description, schema) {
|
package/dist/http.js
CHANGED
|
@@ -16,6 +16,8 @@ function httpHandler(app, options) {
|
|
|
16
16
|
maximumBytes
|
|
17
17
|
});
|
|
18
18
|
if (screened !== void 0) return rejection(screened);
|
|
19
|
+
const body = await readBounded(request, maximumBytes);
|
|
20
|
+
if ("rejection" in body) return rejection(body.rejection);
|
|
19
21
|
const path = new URL(request.url).pathname.slice((options.base ?? "").length);
|
|
20
22
|
return withContext(options, request, async (context) => {
|
|
21
23
|
const session = createSession(app, {
|
|
@@ -31,8 +33,6 @@ function httpHandler(app, options) {
|
|
|
31
33
|
if (request.method === "GET" && path === "/catalog" && options.serveCatalog !== false) return json(200, session.view);
|
|
32
34
|
const id = request.method === "POST" && path.startsWith("/capabilities/") ? path.slice(14) : void 0;
|
|
33
35
|
if (id === void 0) return json(404, callFailure("unknown-capability", "There is no such route."));
|
|
34
|
-
const body = await readBounded(request, maximumBytes);
|
|
35
|
-
if ("rejection" in body) return rejection(body.rejection);
|
|
36
36
|
let input;
|
|
37
37
|
try {
|
|
38
38
|
input = body.text === "" ? {} : JSON.parse(body.text);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export { capabilitiesFor, catalogOf, defineApp, defineCapability, renderCatalog } from "./app.js";
|
|
2
2
|
export type { AnyCapability, App, Capability, Observable, Target, ToJsonSchema } from "./app.js";
|
|
3
|
-
export { createSession } from "./session.js";
|
|
4
|
-
export type { Handle, Session, SessionOptions } from "./session.js";
|
|
3
|
+
export { createSession, getHandle } from "./session.js";
|
|
4
|
+
export type { Handle, HandleOf, Session, SessionOptions } from "./session.js";
|
|
5
5
|
export { mcpHandler } from "./mcp.js";
|
|
6
6
|
export type { McpOptions, McpSettings } from "./mcp.js";
|
|
7
7
|
export { httpHandler } from "./http.js";
|
|
8
8
|
export type { HttpOptions, HttpSettings } from "./http.js";
|
|
9
9
|
export type { ContextOption } from "./context.js";
|
|
10
10
|
export { main as cliMain, runCli } from "./cli.js";
|
|
11
|
-
export type { CliIo, CliOptions } from "./cli.js";
|
|
11
|
+
export type { CliCommand, CliIo, CliOptions } from "./cli.js";
|
|
12
12
|
export { findHost, installWebMcp } from "./webmcp.js";
|
|
13
13
|
export type { Installation, ModelContext, ModelContextTool, Phase, Status } from "./webmcp.js";
|
|
14
14
|
export type { StandardSchemaV1 } from "./standard-schema.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { capabilitiesFor, catalogOf, defineApp, defineCapability, renderCatalog } from "./app.js";
|
|
2
|
-
import { createSession } from "./session.js";
|
|
2
|
+
import { createSession, getHandle } from "./session.js";
|
|
3
3
|
import { mcpHandler } from "./mcp.js";
|
|
4
4
|
import { httpHandler } from "./http.js";
|
|
5
5
|
import { main, runCli } from "./cli.js";
|
|
6
6
|
import { findHost, installWebMcp } from "./webmcp.js";
|
|
7
7
|
import { AVAILABLE, CallFailureError, fail, ok, unavailable } from "@acid-cat/spec-ts";
|
|
8
|
-
export { AVAILABLE, CallFailureError, capabilitiesFor, catalogOf, main as cliMain, createSession, defineApp, defineCapability, fail, findHost, httpHandler, installWebMcp, mcpHandler, ok, renderCatalog, runCli, unavailable };
|
|
8
|
+
export { AVAILABLE, CallFailureError, capabilitiesFor, catalogOf, main as cliMain, createSession, defineApp, defineCapability, fail, findHost, getHandle, httpHandler, installWebMcp, mcpHandler, ok, renderCatalog, runCli, unavailable };
|
package/dist/session.d.ts
CHANGED
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
import type { Availability, CatalogDocument, Outcome, Result } from "@acid-cat/spec-ts";
|
|
2
|
-
import type { App, Observable, ToJsonSchema } from "./app.js";
|
|
3
|
-
|
|
2
|
+
import type { AnyCapability, App, Capability, FailureOf, InputOf, Observable, Output, ToJsonSchema } from "./app.js";
|
|
3
|
+
import type { StandardSchemaV1 } from "./standard-schema.js";
|
|
4
|
+
/**
|
|
5
|
+
* The handle of a capability (spec/bindings/typescript.md#handles). `Input`, `Value`, `Failure` and `State` are the
|
|
6
|
+
* types of the schemas of the capability. `getHandle` gives a handle with these types.
|
|
7
|
+
*/
|
|
8
|
+
export interface Handle<Input = unknown, Value = unknown, Failure = unknown, State = unknown> {
|
|
4
9
|
readonly id: string;
|
|
5
10
|
readonly description: string;
|
|
6
11
|
readonly audience: readonly string[];
|
|
7
12
|
readonly availability: Observable<Availability>;
|
|
8
13
|
/** The state of the capability, if it has one. Each value is a copy. */
|
|
9
|
-
readonly snapshot?: Observable<
|
|
14
|
+
readonly snapshot?: Observable<State>;
|
|
10
15
|
/** A call with an input that is already valid. A call failure rejects with a `CallFailureError`. */
|
|
11
|
-
execute(input:
|
|
16
|
+
execute(input: Input, options?: {
|
|
12
17
|
readonly signal?: AbortSignal | undefined;
|
|
13
|
-
}): Promise<Result<
|
|
18
|
+
}): Promise<Result<Value, Failure>>;
|
|
14
19
|
/** A call with an input that is not trusted. A call failure rejects with a `CallFailureError`. */
|
|
15
20
|
invokeExternal(input: unknown, options?: {
|
|
16
21
|
readonly signal?: AbortSignal | undefined;
|
|
17
22
|
}): Promise<Result<unknown, unknown>>;
|
|
18
23
|
}
|
|
24
|
+
/** The handle of a capability, with the types of its schemas. */
|
|
25
|
+
export type HandleOf<C> = C extends Capability<any, infer Input, infer OutputSchema, infer Failures, infer StateSchema> ? Handle<InputOf<Input>, Output<OutputSchema>, FailureOf<Failures>, StandardSchemaV1.InferOutput<StateSchema>> : never;
|
|
26
|
+
/**
|
|
27
|
+
* The handle of a capability in `handles`, with the types of the capability. For a capability outside the view, the
|
|
28
|
+
* handle is always unavailable, and each call rejects as `unknown-capability`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getHandle<C extends AnyCapability>(handles: ReadonlyMap<string, Handle>, capability: C): HandleOf<C>;
|
|
19
31
|
export interface Session {
|
|
20
32
|
readonly target: string;
|
|
21
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The view of the target. Bindings take names, descriptions and schemas from it. The session writes the view when it
|
|
35
|
+
* is first read.
|
|
36
|
+
*/
|
|
22
37
|
readonly view: CatalogDocument;
|
|
23
38
|
/** The handles of the capabilities in the view, by id. */
|
|
24
39
|
readonly handles: ReadonlyMap<string, Handle>;
|
|
@@ -26,7 +41,10 @@ export interface Session {
|
|
|
26
41
|
invoke(id: string, input: unknown, options?: {
|
|
27
42
|
readonly signal?: AbortSignal | undefined;
|
|
28
43
|
}): Promise<Outcome<unknown, unknown>>;
|
|
29
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* An event each time the availability of a capability changes. The session subscribes to the availabilities when the
|
|
46
|
+
* first listener subscribes, and unsubscribes after the last listener unsubscribes.
|
|
47
|
+
*/
|
|
30
48
|
onChange(listener: () => void): {
|
|
31
49
|
unsubscribe(): void;
|
|
32
50
|
};
|
package/dist/session.js
CHANGED
|
@@ -4,23 +4,48 @@ import { AVAILABLE, CallFailureError, callFailure, isCallFailure, viewOf } from
|
|
|
4
4
|
/**
|
|
5
5
|
* Sessions and handles (spec/invocation.md, spec/bindings/typescript.md).
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* The handle of a capability in `handles`, with the types of the capability. For a capability outside the view, the
|
|
9
|
+
* handle is always unavailable, and each call rejects as `unknown-capability`.
|
|
10
|
+
*/
|
|
11
|
+
function getHandle(handles, capability) {
|
|
12
|
+
return handles.get(capability.id) ?? outsideView(capability);
|
|
13
|
+
}
|
|
14
|
+
function outsideView(capability) {
|
|
15
|
+
const failure = callFailure("unknown-capability", `There is no capability ${capability.id}.`);
|
|
16
|
+
const unavailable = {
|
|
17
|
+
available: false,
|
|
18
|
+
reason: failure.message
|
|
19
|
+
};
|
|
20
|
+
const reject = () => Promise.reject(new CallFailureError(failure));
|
|
21
|
+
return {
|
|
22
|
+
id: capability.id,
|
|
23
|
+
description: capability.description,
|
|
24
|
+
audience: [],
|
|
25
|
+
availability: {
|
|
26
|
+
getSnapshot: () => unavailable,
|
|
27
|
+
subscribe: () => ({ unsubscribe() {} })
|
|
28
|
+
},
|
|
29
|
+
execute: reject,
|
|
30
|
+
invokeExternal: reject
|
|
31
|
+
};
|
|
32
|
+
}
|
|
7
33
|
const always = {
|
|
8
34
|
getSnapshot: () => AVAILABLE,
|
|
9
35
|
subscribe: () => ({ unsubscribe() {} })
|
|
10
36
|
};
|
|
11
37
|
/** Open a session for a target. */
|
|
12
38
|
function createSession(app, options) {
|
|
13
|
-
const view = viewOf(catalogOf(app, { toJsonSchema: options.toJsonSchema }), options.target);
|
|
14
39
|
const lifetime = new AbortController();
|
|
15
40
|
const running = /* @__PURE__ */ new Set();
|
|
16
41
|
const listeners = /* @__PURE__ */ new Set();
|
|
17
|
-
|
|
42
|
+
let subscriptions = [];
|
|
18
43
|
const onDefect = options.onDefect ?? ((id, cause) => console.error(`A capability failed with a defect: ${id}`, cause));
|
|
19
44
|
const served = /* @__PURE__ */ new Map();
|
|
20
45
|
for (const capability of app.capabilities) {
|
|
21
|
-
if (!
|
|
46
|
+
if (!capability.audience.includes(options.target)) continue;
|
|
47
|
+
if (served.has(capability.id)) throw new Error(`The capability ${capability.id} is declared two times.`);
|
|
22
48
|
const availability = capability.availability?.(options.context) ?? always;
|
|
23
|
-
subscriptions.push(availability.subscribe(() => listeners.forEach((listener) => listener())));
|
|
24
49
|
served.set(capability.id, {
|
|
25
50
|
capability,
|
|
26
51
|
availability
|
|
@@ -77,26 +102,41 @@ function createSession(app, options) {
|
|
|
77
102
|
handles.set(id, {
|
|
78
103
|
id,
|
|
79
104
|
description: capability.description,
|
|
80
|
-
audience:
|
|
105
|
+
audience: [options.target],
|
|
81
106
|
availability,
|
|
82
107
|
...state === void 0 ? {} : { snapshot: detached(state) },
|
|
83
108
|
execute: (input, callOptions) => reject(call(id, input, true, callOptions?.signal)),
|
|
84
109
|
invokeExternal: (input, callOptions) => reject(call(id, input, false, callOptions?.signal))
|
|
85
110
|
});
|
|
86
111
|
}
|
|
112
|
+
const unsubscribeAll = () => {
|
|
113
|
+
subscriptions.forEach((subscription) => subscription.unsubscribe());
|
|
114
|
+
subscriptions = [];
|
|
115
|
+
};
|
|
116
|
+
let view;
|
|
87
117
|
return {
|
|
88
118
|
target: options.target,
|
|
89
|
-
view
|
|
119
|
+
get view() {
|
|
120
|
+
return view ??= viewOf(catalogOf(app, { toJsonSchema: options.toJsonSchema }), options.target);
|
|
121
|
+
},
|
|
90
122
|
handles,
|
|
91
123
|
signal: lifetime.signal,
|
|
92
124
|
invoke: (id, input, callOptions) => call(id, input, false, callOptions?.signal),
|
|
93
125
|
onChange(listener) {
|
|
126
|
+
if (lifetime.signal.aborted) return { unsubscribe() {} };
|
|
127
|
+
if (listeners.size === 0) {
|
|
128
|
+
const announce = () => listeners.forEach((each) => each());
|
|
129
|
+
subscriptions = [...served.values()].map(({ availability }) => availability.subscribe(announce));
|
|
130
|
+
}
|
|
94
131
|
listeners.add(listener);
|
|
95
|
-
return { unsubscribe
|
|
132
|
+
return { unsubscribe() {
|
|
133
|
+
if (listeners.delete(listener) && listeners.size === 0) unsubscribeAll();
|
|
134
|
+
} };
|
|
96
135
|
},
|
|
97
136
|
async dispose() {
|
|
98
137
|
lifetime.abort();
|
|
99
|
-
|
|
138
|
+
listeners.clear();
|
|
139
|
+
unsubscribeAll();
|
|
100
140
|
await Promise.allSettled([...running]);
|
|
101
141
|
}
|
|
102
142
|
};
|
|
@@ -136,4 +176,4 @@ async function checkResult(capability, result) {
|
|
|
136
176
|
};
|
|
137
177
|
}
|
|
138
178
|
//#endregion
|
|
139
|
-
export { createSession };
|
|
179
|
+
export { createSession, getHandle };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acid-cat/core-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Serve AcidCat capabilities in plain TypeScript, with no Effect dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"acidcat",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@acid-cat/catalog": "0.
|
|
36
|
-
"@acid-cat/spec-ts": "0.
|
|
35
|
+
"@acid-cat/catalog": "0.4.0",
|
|
36
|
+
"@acid-cat/spec-ts": "0.4.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@acid-cat/cli": "0.
|
|
39
|
+
"@acid-cat/cli": "0.4.0",
|
|
40
40
|
"@types/node": "26.6.5",
|
|
41
41
|
"esbuild": "0.28.2",
|
|
42
42
|
"playwright": "1.64.0",
|