@asterism-app/sdk 0.0.0-stage → 1.0.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/LICENSE +9 -0
- package/README.md +20 -2
- package/docs/data-and-lifecycle.md +79 -0
- package/docs/getting-started.md +37 -0
- package/docs/manual.md +38 -0
- package/docs/minimal-plugin.md +35 -0
- package/docs/project-structure.md +33 -0
- package/docs/resources-and-tools.md +43 -0
- package/docs/session-entries.md +31 -0
- package/docs/views-and-settings.md +43 -0
- package/examples/author.ts +158 -0
- package/examples/consume-usage.ts +29 -0
- package/examples/content-author.ts +29 -0
- package/examples/content-contract.ts +10 -0
- package/examples/content-renderer.ts +26 -0
- package/examples/counter/docs/usage.md +9 -0
- package/examples/counter/package.json +27 -0
- package/examples/counter/src/contracts.ts +34 -0
- package/examples/counter/src/panel.module.css +2 -0
- package/examples/counter/src/renderer.tsx +80 -0
- package/examples/counter/src/runtime.ts +49 -0
- package/examples/counter/src/storage.ts +4 -0
- package/examples/counter/src/styles.d.ts +4 -0
- package/examples/counter/tsconfig.json +16 -0
- package/examples/counter-contract.ts +18 -0
- package/examples/data-codecs.ts +18 -0
- package/examples/events.ts +37 -0
- package/examples/i18n.ts +18 -0
- package/examples/model-completion.ts +15 -0
- package/examples/model-input.ts +16 -0
- package/examples/plugin-calls.ts +22 -0
- package/examples/provider-usage-headers.ts +20 -0
- package/examples/provider-usage.ts +34 -0
- package/examples/renderer.ts +47 -0
- package/examples/session-resources.ts +29 -0
- package/examples/settings-contract.ts +8 -0
- package/examples/settings-renderer.ts +30 -0
- package/examples/settings-runtime.ts +11 -0
- package/examples/storage.ts +26 -0
- package/examples/tool-renderer.ts +47 -0
- package/examples/web-provider.ts +27 -0
- package/package.json +39 -4
- package/src/calls.ts +28 -0
- package/src/card.ts +7 -0
- package/src/channels.ts +20 -0
- package/src/commands.ts +28 -0
- package/src/config.ts +25 -0
- package/src/content-runtime.ts +11 -0
- package/src/content.ts +6 -0
- package/src/contracts.ts +39 -0
- package/src/data-encoding.ts +14 -0
- package/src/error-details.ts +78 -0
- package/src/events.ts +91 -0
- package/src/i18n.ts +58 -0
- package/src/json.ts +4 -0
- package/src/mcp.ts +80 -0
- package/src/model-catalog.ts +18 -0
- package/src/model-cost-estimate.ts +49 -0
- package/src/model-cost.ts +44 -0
- package/src/model-input.ts +19 -0
- package/src/model-media.ts +11 -0
- package/src/model-provider.ts +44 -0
- package/src/model-records.ts +56 -0
- package/src/model-selection.ts +34 -0
- package/src/models.ts +134 -0
- package/src/module-contract.ts +44 -0
- package/src/objects.ts +89 -0
- package/src/plugin.ts +58 -0
- package/src/presentation.ts +21 -0
- package/src/provider-usage.ts +47 -0
- package/src/records.ts +60 -0
- package/src/renderer-calls.ts +15 -0
- package/src/renderer-card.ts +29 -0
- package/src/renderer-content.ts +9 -0
- package/src/renderer-creation.ts +83 -0
- package/src/renderer-i18n.ts +49 -0
- package/src/renderer-island.ts +66 -0
- package/src/renderer-records.ts +49 -0
- package/src/renderer-services.ts +89 -0
- package/src/renderer-tools.ts +103 -0
- package/src/renderer-view-state.ts +33 -0
- package/src/renderer.ts +174 -0
- package/src/runtime-agent-catalog.ts +27 -0
- package/src/runtime-application.ts +7 -0
- package/src/runtime-calls.ts +22 -0
- package/src/runtime-channels.ts +24 -0
- package/src/runtime-contracts.ts +207 -0
- package/src/runtime-mcp.ts +24 -0
- package/src/runtime-model-providers.ts +16 -0
- package/src/runtime-resources.ts +117 -0
- package/src/runtime-sessions.ts +36 -0
- package/src/runtime-shell.ts +18 -0
- package/src/runtime-terminal.ts +24 -0
- package/src/runtime-tools.ts +31 -0
- package/src/runtime-usage.ts +54 -0
- package/src/runtime-web.ts +36 -0
- package/src/runtime.ts +53 -0
- package/src/session-title.ts +8 -0
- package/src/shell.ts +76 -0
- package/src/terminal.ts +36 -0
- package/src/tool-result.ts +45 -0
- package/src/tool-validation.ts +14 -0
- package/src/usage-state.ts +20 -0
- package/src/web.ts +75 -0
- package/src/workspace.ts +43 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Tool view author example: the three states are declared in parallel, and the same component can be reused across phases. */
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineData, type Json } from "@asterism-app/sdk/contracts";
|
|
5
|
+
import { toolRenderer, type ToolRenderProps } from "@asterism-app/sdk/renderer";
|
|
6
|
+
|
|
7
|
+
const Result = defineData("example.writer/write", z.object({ written: z.number() }));
|
|
8
|
+
const targetSchema = z.object({ path: z.string() });
|
|
9
|
+
const argsSchema = z.object({ content: z.string() });
|
|
10
|
+
type Props = ToolRenderProps<z.infer<typeof Result.schema>, z.infer<typeof targetSchema>, z.infer<typeof argsSchema>>;
|
|
11
|
+
|
|
12
|
+
function inputContent(args: Json): string {
|
|
13
|
+
return args !== null && typeof args === "object" && !Array.isArray(args) && typeof args.content === "string" ? args.content : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Header(props: Props) {
|
|
17
|
+
let path: string | undefined;
|
|
18
|
+
if (props.phase === "input") {
|
|
19
|
+
const target = props.target;
|
|
20
|
+
if (target !== null && typeof target === "object" && !Array.isArray(target) && typeof target.path === "string") path = target.path;
|
|
21
|
+
} else path = props.target.path;
|
|
22
|
+
return createElement("span", null, path ?? "Write");
|
|
23
|
+
}
|
|
24
|
+
function Body(props: Props) {
|
|
25
|
+
if (props.phase === "input") {
|
|
26
|
+
return createElement("pre", null, inputContent(props.args).slice(-2_000));
|
|
27
|
+
}
|
|
28
|
+
return createElement("pre", null, props.result?.data?.written ?? props.args.content);
|
|
29
|
+
}
|
|
30
|
+
const view = { headerRenderer: Header, bodyRenderer: Body };
|
|
31
|
+
export const writerView = toolRenderer({ ...Result, targetSchema, tool: { argsSchema } }, {
|
|
32
|
+
label: "Write",
|
|
33
|
+
input: { ...view, streaming: ({ args }) => inputContent(args).length > 0 },
|
|
34
|
+
execution: view, result: view,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Plugins render progressive output themselves; the shell manages automatic expansion and collapse based on declarations and user settings. */
|
|
38
|
+
const Report = defineData("example.writer/report", z.object({ text: z.string() }));
|
|
39
|
+
function ReportBody({ phase, result }: ToolRenderProps<z.infer<typeof Report.schema>>) {
|
|
40
|
+
const text = result?.data?.text ?? "";
|
|
41
|
+
return createElement("pre", null, phase === "executing" ? text.slice(-2_000) : text);
|
|
42
|
+
}
|
|
43
|
+
export const reportView = toolRenderer(Report, {
|
|
44
|
+
label: "Report",
|
|
45
|
+
execution: { bodyRenderer: ReportBody, streaming: ({ result }) => !!result?.data?.text },
|
|
46
|
+
result: { bodyRenderer: ReportBody },
|
|
47
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { pluginNamespace, present, webResponseSchema } from "@asterism-app/sdk/contracts";
|
|
2
|
+
import { definePlugin, WebError, type WebProviderDefinition } from "@asterism-app/sdk/runtime";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const ns = pluginNamespace("example.web");
|
|
6
|
+
const Metadata = ns.data("metadata", z.object({ sourceId: z.string() }));
|
|
7
|
+
const responseSchema = z.object({ id: z.string(), title: z.string(), text: z.string() });
|
|
8
|
+
const provider: WebProviderDefinition = {
|
|
9
|
+
id: "example.web/pages", label: "Example pages", search: false, fetch: true,
|
|
10
|
+
urlPatterns: ["https://docs.example.test/*", "https://help.example.test/*"],
|
|
11
|
+
defaultBaseUrl: "https://api.example.test", credential: { kind: "api_key", keyUrl: "https://example.test/api-keys" },
|
|
12
|
+
create({ baseUrl, credential }) {
|
|
13
|
+
return { async fetch(request, { signal }) {
|
|
14
|
+
const response = await fetch(new URL("/read", baseUrl), {
|
|
15
|
+
method: "POST", signal, headers: { "Content-Type": "application/json", Authorization: `Bearer ${credential}` },
|
|
16
|
+
body: JSON.stringify({ url: request.url }),
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) throw new WebError("request_failed", `Page service returned HTTP ${response.status}.`);
|
|
19
|
+
const page = responseSchema.parse(await response.json());
|
|
20
|
+
return webResponseSchema.parse({ documents: [{ url: request.url, title: page.title,
|
|
21
|
+
content: [{ kind: "body", format: "markdown", text: page.text }], details: [present(Metadata, { sourceId: page.id })] }],
|
|
22
|
+
hints: request.query ? [{ code: "unsupported_parameter", parameter: "query", message: "This provider returned the page without query-focused excerpts." }] : [],
|
|
23
|
+
});
|
|
24
|
+
} };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
export default definePlugin({ namespace: ns, setup: () => ({ webProviders: [provider] }) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asterism-app/sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Public contracts and host capabilities for Asterism plugins",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Chien Zhang",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/d3ara1n/asterism.git",
|
|
10
|
+
"directory": "packages/sdk"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public",
|
|
14
|
+
"tag": "next",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
"./package.json": "./package.json",
|
|
20
|
+
"./contracts": "./src/contracts.ts",
|
|
21
|
+
"./runtime": "./src/runtime.ts",
|
|
22
|
+
"./renderer": "./src/renderer.ts"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"zod": "^4.6.5",
|
|
26
|
+
"react": "^19.3.0",
|
|
27
|
+
"react-intl": "^12.1.4"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"react-intl": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src",
|
|
36
|
+
"!src/**/*.test.ts",
|
|
37
|
+
"docs",
|
|
38
|
+
"examples",
|
|
39
|
+
"README.md"
|
|
40
|
+
]
|
|
41
|
+
}
|
package/src/calls.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { Json } from "./json.ts";
|
|
3
|
+
import { contractReferenceSchema } from "./presentation.ts";
|
|
4
|
+
import type { ResourceRef } from "./workspace.ts";
|
|
5
|
+
import type { ContentReader } from "./content-runtime.ts";
|
|
6
|
+
|
|
7
|
+
export type CallScope = "plugin" | "object";
|
|
8
|
+
/** Scope is declared by the contract; callers cannot downgrade an object-level operation by omitting the object. */
|
|
9
|
+
export interface CallContract<Input extends z.ZodType<Json> = z.ZodType<Json>, Output extends z.ZodType<Json> = z.ZodType<Json>, S extends CallScope = CallScope> {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly scope: S;
|
|
12
|
+
readonly input: Input;
|
|
13
|
+
readonly output: Output;
|
|
14
|
+
}
|
|
15
|
+
export function defineCall<Input extends z.ZodType<Json>, Output extends z.ZodType<Json>, S extends CallScope>(id: string, definition: {
|
|
16
|
+
scope: S; input: Input; output: Output;
|
|
17
|
+
}): CallContract<Input, Output, S> {
|
|
18
|
+
if (definition.scope !== "plugin" && definition.scope !== "object") throw new Error("A call must declare plugin or object scope.");
|
|
19
|
+
return Object.freeze({ ...contractReferenceSchema.parse({ id }), ...definition });
|
|
20
|
+
}
|
|
21
|
+
export type CallOptions<S extends CallScope = CallScope, Content = ContentReader> = { signal?: AbortSignal; content?: Content } &
|
|
22
|
+
(S extends "object" ? { object: ResourceRef } : { object?: never });
|
|
23
|
+
export interface PluginCalls<Content = ContentReader> {
|
|
24
|
+
/** Submit JSON matching the input schema's input; receive the output schema's parsed result. */
|
|
25
|
+
invoke<Input extends z.ZodType<Json>, Output extends z.ZodType<Json>, S extends CallScope>(
|
|
26
|
+
contract: CallContract<Input, Output, S>, input: NoInfer<z.input<Input>> & Json, options: CallOptions<NoInfer<S>, Content>,
|
|
27
|
+
): Promise<z.output<Output>>;
|
|
28
|
+
}
|
package/src/card.ts
ADDED
package/src/channels.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { contractReferenceSchema } from "./presentation.ts";
|
|
2
|
+
|
|
3
|
+
/** Channel payloads: JSON values and bytes supported by structured cloning, with arbitrary nesting. The host does not validate each frame. */
|
|
4
|
+
export type ChannelValue = null | boolean | number | string | Uint8Array | readonly ChannelValue[] | { readonly [key: string]: ChannelValue };
|
|
5
|
+
/** Channel end reasons: closed means either end closed it explicitly; removed means the object left the current set; unavailable means the contract or object is unavailable; disconnected means the connection was lost. */
|
|
6
|
+
export type ChannelEnd = { reason: "closed" | "removed" | "unavailable" | "disconnected"; message?: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Object-level duplex channel contract: opened by the frontend and routed by the backend to the object's owning plugin according to the contract.
|
|
10
|
+
* Up carries frontend-to-backend messages, and Down carries backend-to-frontend messages; both ends deliver in order without per-frame acknowledgments.
|
|
11
|
+
*/
|
|
12
|
+
export interface ChannelContract<Up extends ChannelValue = ChannelValue, Down extends ChannelValue = ChannelValue> {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly kind: "channel";
|
|
15
|
+
/** For type inference only. */
|
|
16
|
+
readonly __types?: { up: Up; down: Down };
|
|
17
|
+
}
|
|
18
|
+
export function defineChannel<Up extends ChannelValue, Down extends ChannelValue>(id: string): ChannelContract<Up, Down> {
|
|
19
|
+
return Object.freeze({ ...contractReferenceSchema.parse({ id }), kind: "channel" as const });
|
|
20
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import { jsonSchema, type Json } from "./json.ts";
|
|
3
|
+
import type { ResourceContract } from "./module-contract.ts";
|
|
4
|
+
import { contractReferenceSchema, type ContractReference } from "./presentation.ts";
|
|
5
|
+
|
|
6
|
+
/** I is the invocation input and O is the schema parsing result; both are inferred automatically from the declared schema. */
|
|
7
|
+
export interface CreationContract<I = Json, S extends Json = Json, O extends Json = Json> extends ContractReference {
|
|
8
|
+
readonly kind: "creation";
|
|
9
|
+
readonly resource: ResourceContract<S>;
|
|
10
|
+
readonly schema: z.ZodType<O, I>;
|
|
11
|
+
}
|
|
12
|
+
export interface ActionContract<I = Json, O extends Json = Json> extends ContractReference {
|
|
13
|
+
readonly kind: "action";
|
|
14
|
+
readonly resource: ResourceContract;
|
|
15
|
+
readonly schema: z.ZodType<O, I>;
|
|
16
|
+
}
|
|
17
|
+
export function defineCreation<S extends Json, I, O extends Json>(resource: ResourceContract<S>, definition: ContractReference & { schema: z.ZodType<O, I> }): CreationContract<I, S, O> {
|
|
18
|
+
return { ...contractReferenceSchema.parse({ id: definition.id }), kind: "creation", resource, schema: definition.schema };
|
|
19
|
+
}
|
|
20
|
+
export function defineAction<I, O extends Json>(resource: ResourceContract, definition: ContractReference & { schema: z.ZodType<O, I> }): ActionContract<I, O> {
|
|
21
|
+
return { ...contractReferenceSchema.parse({ id: definition.id }), kind: "action", resource, schema: definition.schema };
|
|
22
|
+
}
|
|
23
|
+
/** Command envelopes carry only stable identity and raw JSON input; the host parses them using assembled declarations. */
|
|
24
|
+
export const resourceCallSchema = contractReferenceSchema.extend({ input: jsonSchema });
|
|
25
|
+
export type ResourceCall = z.infer<typeof resourceCallSchema>;
|
|
26
|
+
export function resourceCall<I, O extends Json>(contract: CreationContract<I, Json, O> | ActionContract<I, O>, input: NoInfer<I> & Json): ResourceCall {
|
|
27
|
+
return { id: contract.id, input };
|
|
28
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { Json } from "./json.ts";
|
|
3
|
+
import { encodeJson } from "./data-encoding.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Plugin configuration contract: settings agreed between users and plugins, with one configuration per plugin shared by frontend and backend.
|
|
7
|
+
* Identity is the plugin ID; the host handles storage, validation, and change notifications, while the plugin provides the page. Preferences such as the last selection are not configuration.
|
|
8
|
+
*/
|
|
9
|
+
export interface ConfigContract<T extends Json = Json> {
|
|
10
|
+
readonly plugin: string;
|
|
11
|
+
/** Decodes stored and transported configuration; updates and defaults use T. */
|
|
12
|
+
readonly schema: z.ZodType<T>;
|
|
13
|
+
readonly defaults: T;
|
|
14
|
+
}
|
|
15
|
+
export function defineConfig<T extends Json>(plugin: string, schema: z.ZodType<T>, defaults: NoInfer<T>): ConfigContract<T> {
|
|
16
|
+
const parsed = schema.safeParse(encodeJson(schema, structuredClone(defaults), `${plugin}.configuration defaults`));
|
|
17
|
+
if (!parsed.success) throw new Error(`Configuration defaults of ${plugin} do not match the schema.`);
|
|
18
|
+
return Object.freeze({ plugin, schema, defaults: structuredClone(parsed.data) });
|
|
19
|
+
}
|
|
20
|
+
/** Decode saved values according to the contract; fall back to defaults when invalid or missing, leaving status display to the caller. */
|
|
21
|
+
export function readConfig<T extends Json>(contract: ConfigContract<T>, raw: unknown): { value: T; status: "saved" | "defaults" | "invalid" } {
|
|
22
|
+
if (raw === undefined) return { value: structuredClone(contract.defaults), status: "defaults" };
|
|
23
|
+
const parsed = contract.schema.safeParse(raw);
|
|
24
|
+
return parsed.success ? { value: parsed.data, status: "saved" } : { value: structuredClone(contract.defaults), status: "invalid" };
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ContentRef, ContentRange } from "./content.ts";
|
|
2
|
+
|
|
3
|
+
/** Individual reads have a size limit; large content uses bounded byte chunks through open. */
|
|
4
|
+
export interface ContentReader {
|
|
5
|
+
read(ref: ContentRef, range?: ContentRange): Promise<Uint8Array>;
|
|
6
|
+
open(ref: ContentRef, range?: ContentRange): AsyncIterable<Uint8Array>;
|
|
7
|
+
}
|
|
8
|
+
/** Temporary workspace managed by the host; stage neither modifies the session nor establishes persistent references. */
|
|
9
|
+
export interface ContentScope extends ContentReader {
|
|
10
|
+
stage(source: Uint8Array | AsyncIterable<Uint8Array>): Promise<ContentRef>;
|
|
11
|
+
}
|
package/src/content.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** A JSON reference value reserved by the SDK; identity contains no path and grants no right to read or retain content. */
|
|
4
|
+
export const contentRefSchema = z.strictObject({ "$asterism.content": z.string().uuid() });
|
|
5
|
+
export type ContentRef = z.infer<typeof contentRefSchema>;
|
|
6
|
+
export interface ContentRange { offset?: number; length?: number }
|
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** SDK pure contract entry point: does not depend on Node, Electron, React, or host source code. */
|
|
2
|
+
export * from "./json.ts";
|
|
3
|
+
export * from "./error-details.ts";
|
|
4
|
+
export * from "./presentation.ts";
|
|
5
|
+
export * from "./module-contract.ts";
|
|
6
|
+
export type { CardAppearance } from "./card.ts";
|
|
7
|
+
export * from "./plugin.ts";
|
|
8
|
+
export * from "./records.ts";
|
|
9
|
+
export * from "./events.ts";
|
|
10
|
+
export * from "./calls.ts";
|
|
11
|
+
export * from "./channels.ts";
|
|
12
|
+
export * from "./tool-result.ts";
|
|
13
|
+
export * from "./workspace.ts";
|
|
14
|
+
export type * from "./models.ts";
|
|
15
|
+
export { thinkingLevels } from "./models.ts";
|
|
16
|
+
export * from "./model-records.ts";
|
|
17
|
+
export { validationMessage, jsonArgumentHint } from "./tool-validation.ts";
|
|
18
|
+
export * from "./commands.ts";
|
|
19
|
+
export * from "./i18n.ts";
|
|
20
|
+
export * from "./config.ts";
|
|
21
|
+
export * from "./content.ts";
|
|
22
|
+
export * from "./model-input.ts";
|
|
23
|
+
|
|
24
|
+
export * from "./model-selection.ts";
|
|
25
|
+
export * from "./model-catalog.ts";
|
|
26
|
+
export * from "./model-cost.ts";
|
|
27
|
+
export * from "./model-cost-estimate.ts";
|
|
28
|
+
export * from "./model-media.ts";
|
|
29
|
+
export * from "./model-provider.ts";
|
|
30
|
+
export * from "./provider-usage.ts";
|
|
31
|
+
export * from "./web.ts";
|
|
32
|
+
|
|
33
|
+
export * from "./session-title.ts";
|
|
34
|
+
|
|
35
|
+
export * from "./usage-state.ts";
|
|
36
|
+
|
|
37
|
+
export * from "./shell.ts";
|
|
38
|
+
export * from "./terminal.ts";
|
|
39
|
+
export * from "./mcp.ts";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { jsonSchema, type Json } from "./json.ts";
|
|
3
|
+
import { validationMessage } from "./tool-validation.ts";
|
|
4
|
+
|
|
5
|
+
/** Data producers supply decoded values; storage and transport retain the JSON encoding. */
|
|
6
|
+
export function encodeJson<T extends Json>(schema: z.ZodType<T>, value: T, subject: string): Json {
|
|
7
|
+
try { return jsonSchema.parse(z.encode(schema, value)); }
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (!(error instanceof z.ZodError) && !(error instanceof z.core.$ZodEncodeError)) throw error;
|
|
10
|
+
const detail = error instanceof z.ZodError ? validationMessage(error, subject)
|
|
11
|
+
: `${subject}: ${error.message}`;
|
|
12
|
+
throw new Error(`Cannot encode data: ${detail}`, { cause: error });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** Transferable raw error data; response bodies and additional data remain text to avoid being scanned as business references. */
|
|
4
|
+
export type ErrorDetails = {
|
|
5
|
+
name?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
code?: string | number;
|
|
8
|
+
statusCode?: number;
|
|
9
|
+
/** The actual HTTP status received; statusCode preserves the error object's original value, which may be inferred by the SDK. */
|
|
10
|
+
httpStatus?: number;
|
|
11
|
+
responseBody?: string;
|
|
12
|
+
/** The streaming response fragment actually received by the transport when the SDK fails before delivering the first model block. */
|
|
13
|
+
rawResponse?: string;
|
|
14
|
+
rawEvent?: string;
|
|
15
|
+
responseHeaders?: Record<string, string>;
|
|
16
|
+
data?: string;
|
|
17
|
+
stack?: string;
|
|
18
|
+
causes?: ErrorDetails[];
|
|
19
|
+
};
|
|
20
|
+
export const errorDetailsSchema: z.ZodType<ErrorDetails> = z.strictObject({
|
|
21
|
+
name: z.string().optional(), message: z.string(), code: z.union([z.string(), z.number()]).optional(),
|
|
22
|
+
statusCode: z.number().optional(), httpStatus: z.number().optional(), responseBody: z.string().optional(),
|
|
23
|
+
rawResponse: z.string().optional(), rawEvent: z.string().optional(),
|
|
24
|
+
responseHeaders: z.record(z.string(), z.string()).optional(), data: z.string().optional(),
|
|
25
|
+
stack: z.string().optional(), causes: z.lazy(() => errorDetailsSchema.array()).optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** Extract the error itself and response data; request bodies, request headers, and credentials are not error details. */
|
|
29
|
+
export function describeError(error: unknown): ErrorDetails {
|
|
30
|
+
const seen = new Set<object>();
|
|
31
|
+
const encode = (value: unknown): string => {
|
|
32
|
+
if (typeof value === "string") return value;
|
|
33
|
+
const ancestors: object[] = [];
|
|
34
|
+
return JSON.stringify(value, function (_key, item: unknown) {
|
|
35
|
+
if (typeof item === "bigint") return String(item);
|
|
36
|
+
if (item && typeof item === "object") {
|
|
37
|
+
while (ancestors.length && ancestors.at(-1) !== this) ancestors.pop();
|
|
38
|
+
if (ancestors.includes(item)) return "[Circular]";
|
|
39
|
+
ancestors.push(item);
|
|
40
|
+
}
|
|
41
|
+
return item;
|
|
42
|
+
}) ?? String(value);
|
|
43
|
+
};
|
|
44
|
+
const visit = (value: unknown): ErrorDetails => {
|
|
45
|
+
if (!value || typeof value !== "object") return { message: typeof value === "string" ? value : encode(value) };
|
|
46
|
+
if (seen.has(value)) return { message: "[Circular error cause]" };
|
|
47
|
+
seen.add(value);
|
|
48
|
+
const record = value as Record<string, unknown>;
|
|
49
|
+
const attached = errorDetailsSchema.safeParse(record.details);
|
|
50
|
+
if (attached.success) return attached.data;
|
|
51
|
+
const { requestBodyValues: _values, requestBody: _body, requestHeaders: _headers, responseHeaders: _responseHeaders, ...raw } = record;
|
|
52
|
+
const details: ErrorDetails = {
|
|
53
|
+
message: typeof record.message === "string" ? record.message : encode(raw),
|
|
54
|
+
};
|
|
55
|
+
if (typeof record.name === "string") details.name = record.name;
|
|
56
|
+
if (typeof record.code === "string" || typeof record.code === "number") details.code = record.code;
|
|
57
|
+
if (typeof record.stack === "string") details.stack = record.stack;
|
|
58
|
+
if (typeof record.statusCode === "number") details.statusCode = record.statusCode;
|
|
59
|
+
if (typeof record.httpStatus === "number") details.httpStatus = record.httpStatus;
|
|
60
|
+
if (typeof record.responseBody === "string") details.responseBody = record.responseBody;
|
|
61
|
+
if (typeof record.rawResponse === "string") details.rawResponse = record.rawResponse;
|
|
62
|
+
if (typeof record.rawEvent === "string") details.rawEvent = record.rawEvent;
|
|
63
|
+
if (record.responseHeaders && typeof record.responseHeaders === "object") {
|
|
64
|
+
details.responseHeaders = Object.fromEntries(Object.entries(record.responseHeaders)
|
|
65
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === "string"
|
|
66
|
+
&& !/^(authorization|proxy-authorization|cookie|set-cookie|x-api-key)$/iu.test(entry[0])));
|
|
67
|
+
}
|
|
68
|
+
if (record.data !== undefined) details.data = encode(record.data);
|
|
69
|
+
else if (!(value instanceof Error)) details.data = encode(raw);
|
|
70
|
+
const causes = [
|
|
71
|
+
...(record.cause !== undefined ? [record.cause] : []),
|
|
72
|
+
...(Array.isArray(record.errors) ? record.errors : []),
|
|
73
|
+
];
|
|
74
|
+
if (causes.length) details.causes = causes.map(visit);
|
|
75
|
+
return details;
|
|
76
|
+
};
|
|
77
|
+
return visit(error);
|
|
78
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Json } from "./json.ts";
|
|
3
|
+
import type { Source } from "./workspace.ts";
|
|
4
|
+
import { uiTextSchema } from "./i18n.ts";
|
|
5
|
+
|
|
6
|
+
export type EventSourceKind = "plugin" | "resource" | "any" | "host";
|
|
7
|
+
export interface EventEnvelope<D extends Json = Json> {
|
|
8
|
+
readonly contract: { readonly id: string };
|
|
9
|
+
readonly emittedAt: string;
|
|
10
|
+
readonly source: Source;
|
|
11
|
+
readonly data: D;
|
|
12
|
+
}
|
|
13
|
+
export type EventSemantics = { readonly kind: "occurrence" } | { readonly kind: "snapshot"; readonly key: readonly string[] };
|
|
14
|
+
export interface EventContract<I = unknown, D extends Json = Json> {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly schema: z.ZodType<D, I>;
|
|
17
|
+
readonly source: EventSourceKind;
|
|
18
|
+
readonly semantics: EventSemantics;
|
|
19
|
+
}
|
|
20
|
+
export interface EventDefinition<I, D extends Json> {
|
|
21
|
+
schema: z.ZodType<D, I>;
|
|
22
|
+
source: EventSourceKind;
|
|
23
|
+
semantics: { kind: "occurrence" } | { kind: "snapshot"; key: readonly Extract<keyof D, string>[] };
|
|
24
|
+
}
|
|
25
|
+
export function defineEvent<I, D extends Json>(id: string, definition: EventDefinition<I, D>): EventContract<I, D> {
|
|
26
|
+
const semantics = definition.semantics.kind === "snapshot"
|
|
27
|
+
? Object.freeze({ kind: "snapshot" as const, key: Object.freeze([...definition.semantics.key]) })
|
|
28
|
+
: Object.freeze({ kind: "occurrence" as const });
|
|
29
|
+
return Object.freeze({ id, schema: definition.schema, source: definition.source, semantics });
|
|
30
|
+
}
|
|
31
|
+
/** Source conditions filter only the host envelope; they neither interpret domain payloads nor grant resource operation permissions. */
|
|
32
|
+
export interface EventFilter { plugin?: string; sessionId?: string; resourceId?: string; resourceKind?: string }
|
|
33
|
+
export interface EventSubscribeOptions {
|
|
34
|
+
delivery?: "all" | "latest";
|
|
35
|
+
source?: EventFilter;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
export interface SubscriptionEnd {
|
|
39
|
+
readonly reason: "closed" | "aborted" | "unavailable" | "invalid_contract" | "consumer_failed" | "disconnected";
|
|
40
|
+
readonly message?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface EventSubscription {
|
|
43
|
+
readonly ready: Promise<void>;
|
|
44
|
+
readonly closed: Promise<SubscriptionEnd>;
|
|
45
|
+
close(): void;
|
|
46
|
+
}
|
|
47
|
+
export type EventHandler<D extends Json> = (event: EventEnvelope<D>, context: { signal: AbortSignal }) => void | Promise<void>;
|
|
48
|
+
export interface EventSubscriber {
|
|
49
|
+
subscribe<I, D extends Json>(contract: EventContract<I, D>, options: EventSubscribeOptions, handler: EventHandler<NoInfer<D>>): EventSubscription;
|
|
50
|
+
}
|
|
51
|
+
export interface EventPublisher {
|
|
52
|
+
/** Deliver immediately after synchronous validation, without waiting for consumption or rolling back with the transaction; publish after a successful commit when effects must be confirmed. */
|
|
53
|
+
publish<I, D extends Json>(contract: EventContract<I, D>, data: NoInfer<I> & Json): void;
|
|
54
|
+
}
|
|
55
|
+
export interface PluginEvents extends EventSubscriber, EventPublisher {}
|
|
56
|
+
|
|
57
|
+
/** Public event published by the application itself after a session title actually changes, with no plugin or resource source. */
|
|
58
|
+
export const SessionTitleChanged = defineEvent("asterism/session-title", {
|
|
59
|
+
schema: z.strictObject({ sessionId: z.string(), title: z.string() }),
|
|
60
|
+
source: "host",
|
|
61
|
+
semantics: { kind: "snapshot", key: ["sessionId"] },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** Transient changes to the current resource set; used to release UI state across sessions. */
|
|
65
|
+
export const ResourcePresenceChanged = defineEvent("asterism/resource-presence", {
|
|
66
|
+
schema: z.strictObject({
|
|
67
|
+
sessionId: z.string(),
|
|
68
|
+
appeared: z.array(z.string()),
|
|
69
|
+
disappeared: z.array(z.string()),
|
|
70
|
+
}),
|
|
71
|
+
source: "host",
|
|
72
|
+
semantics: { kind: "occurrence" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/** Each publication fully replaces resource status; running remains visible, while other states are consumed when the session is viewed. */
|
|
76
|
+
export const ResourceStatusChanged = defineEvent("asterism/resource-status", {
|
|
77
|
+
schema: z.discriminatedUnion("status", [
|
|
78
|
+
z.strictObject({
|
|
79
|
+
title: uiTextSchema,
|
|
80
|
+
status: z.literal("running"),
|
|
81
|
+
progress: z.strictObject({ value: z.number().min(0).max(1) }).optional(),
|
|
82
|
+
}),
|
|
83
|
+
z.strictObject({
|
|
84
|
+
title: uiTextSchema,
|
|
85
|
+
status: z.enum(["failed", "awaiting", "completed"]),
|
|
86
|
+
}),
|
|
87
|
+
]),
|
|
88
|
+
source: "resource",
|
|
89
|
+
semantics: { kind: "snapshot", key: [] },
|
|
90
|
+
});
|
|
91
|
+
export type ResourceStatus = z.infer<typeof ResourceStatusChanged.schema>;
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export interface SourceMessage {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly defaultMessage: string;
|
|
6
|
+
readonly description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type MessageValues = Readonly<Record<string, string | number | boolean>>;
|
|
10
|
+
|
|
11
|
+
export interface UiMessage extends SourceMessage {
|
|
12
|
+
readonly kind: "message";
|
|
13
|
+
readonly owner: string;
|
|
14
|
+
readonly sourceLocale: string;
|
|
15
|
+
readonly values?: MessageValues;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type UiText = string | UiMessage;
|
|
19
|
+
|
|
20
|
+
export const localeSchema = z.string().refine(value => {
|
|
21
|
+
try { Intl.getCanonicalLocales(value); return true; }
|
|
22
|
+
catch { return false; }
|
|
23
|
+
}, { message: "Invalid language tag.", abort: true })
|
|
24
|
+
.overwrite(value => Intl.getCanonicalLocales(value)[0]!);
|
|
25
|
+
|
|
26
|
+
export const messageValuesSchema = z.record(z.string(), z.union([z.string(), z.number().finite(), z.boolean()]));
|
|
27
|
+
export const uiMessageSchema = z.strictObject({
|
|
28
|
+
kind: z.literal("message"),
|
|
29
|
+
owner: z.string().min(1),
|
|
30
|
+
sourceLocale: localeSchema,
|
|
31
|
+
id: z.string().min(1),
|
|
32
|
+
defaultMessage: z.string().refine(value => !!value.trim(), "A default message is required."),
|
|
33
|
+
description: z.string().optional(),
|
|
34
|
+
values: messageValuesSchema.optional(),
|
|
35
|
+
});
|
|
36
|
+
export const uiTextSchema = z.union([z.string(), uiMessageSchema]);
|
|
37
|
+
|
|
38
|
+
/** Standard static descriptor shape for extraction by the official FormatJS tooling. */
|
|
39
|
+
export function defineMessages<const T extends Record<string, SourceMessage>>(messages: T): T {
|
|
40
|
+
return messages;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function bindMessages<T extends Record<string, SourceMessage>>(
|
|
44
|
+
owner: string,
|
|
45
|
+
sourceLocale: string,
|
|
46
|
+
definitions: T,
|
|
47
|
+
): { readonly [K in keyof T]: UiMessage } {
|
|
48
|
+
const locale = localeSchema.parse(sourceLocale);
|
|
49
|
+
const entries = Object.entries(definitions).map(([key, value]) => [
|
|
50
|
+
key,
|
|
51
|
+
Object.freeze(uiMessageSchema.parse({ ...value, kind: "message", owner, sourceLocale: locale })),
|
|
52
|
+
]);
|
|
53
|
+
return Object.fromEntries(entries) as { readonly [K in keyof T]: UiMessage };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function withValues(message: UiMessage, values: MessageValues): UiMessage {
|
|
57
|
+
return uiMessageSchema.parse({ ...message, values: { ...message.values, ...values } });
|
|
58
|
+
}
|
package/src/json.ts
ADDED
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCall } from "./calls.ts";
|
|
3
|
+
import { defineEvent } from "./events.ts";
|
|
4
|
+
import { jsonSchema } from "./json.ts";
|
|
5
|
+
|
|
6
|
+
export const SERVER_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/u;
|
|
7
|
+
export const serverIdSchema = z.string().min(1).max(64).regex(SERVER_ID_PATTERN);
|
|
8
|
+
export const mcpServerSchema = z.strictObject({
|
|
9
|
+
id: serverIdSchema, name: z.string().trim().min(1), enabled: z.boolean(),
|
|
10
|
+
mode: z.enum(["eager", "lazy"]), idleSeconds: z.number().int().min(1).max(86_400),
|
|
11
|
+
transport: z.enum(["stdio", "http"]), command: z.string(), args: z.array(z.string()), cwd: z.string(),
|
|
12
|
+
url: z.string(), auth: z.enum(["none", "oauth"]),
|
|
13
|
+
}).superRefine((value, ctx) => {
|
|
14
|
+
if (value.transport === "stdio" && !value.command.trim()) ctx.addIssue({ code: "custom", path: ["command"], message: "An executable is required." });
|
|
15
|
+
if (value.transport === "http") {
|
|
16
|
+
try { const url = new URL(value.url); if (!["http:", "https:"].includes(url.protocol)) throw new Error(); }
|
|
17
|
+
catch { ctx.addIssue({ code: "custom", path: ["url"], message: "An HTTP or HTTPS MCP URL is required." }); }
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
export type McpServerConfig = z.infer<typeof mcpServerSchema>;
|
|
21
|
+
export const mcpSettingsSchema = z.object({ servers: z.array(mcpServerSchema).refine(values =>
|
|
22
|
+
new Set(values.map(value => value.id)).size === values.length, "Server IDs are unique.") });
|
|
23
|
+
export const defaultMcpSettings = { servers: [] };
|
|
24
|
+
export const credentialsSchema = z.record(z.string(), z.string());
|
|
25
|
+
export const connectionPhases = ["disconnected", "connecting", "awaiting_auth", "authorizing", "connected", "error"] as const;
|
|
26
|
+
export type ConnectionPhase = typeof connectionPhases[number];
|
|
27
|
+
export const mcpServerStateSchema = z.object({
|
|
28
|
+
id: z.string(), name: z.string(), enabled: z.boolean(), mode: z.enum(["eager", "lazy"]),
|
|
29
|
+
connection: z.enum(connectionPhases),
|
|
30
|
+
indexed: z.boolean(), toolCount: z.number(), error: z.string().nullable(), authorizationUrl: z.string().nullable(),
|
|
31
|
+
authorizationExpiresAt: z.number().nullable(),
|
|
32
|
+
});
|
|
33
|
+
export type McpServerState = z.infer<typeof mcpServerStateSchema>;
|
|
34
|
+
export const cachedMcpToolSchema = z.object({
|
|
35
|
+
name: z.string(), title: z.string().optional(), description: z.string().optional(),
|
|
36
|
+
inputSchema: z.record(z.string(), jsonSchema), outputSchema: z.record(z.string(), jsonSchema).optional(),
|
|
37
|
+
annotations: z.record(z.string(), jsonSchema).optional(),
|
|
38
|
+
});
|
|
39
|
+
export type CachedMcpTool = z.infer<typeof cachedMcpToolSchema>;
|
|
40
|
+
export const mcpDirectorySchema = z.object({ tools: z.array(cachedMcpToolSchema), instructions: z.string().optional() });
|
|
41
|
+
export type McpDirectory = z.infer<typeof mcpDirectorySchema>;
|
|
42
|
+
export interface McpCatalogServer { config: McpServerConfig; state: McpServerState; directory?: McpDirectory }
|
|
43
|
+
|
|
44
|
+
export const McpChanged = defineEvent("core.mcp.changed", { schema: z.object({}), source: "host", semantics: { kind: "snapshot", key: [] } });
|
|
45
|
+
export const ReadMcp = defineCall("core.mcp.status", { scope: "plugin", input: z.object({}), output: z.array(mcpServerStateSchema) });
|
|
46
|
+
/** keep retains saved environment or headers; a new server with keep starts without any. */
|
|
47
|
+
export const mcpCredentialUpdateSchema = z.discriminatedUnion("action", [
|
|
48
|
+
z.strictObject({ action: z.literal("keep") }), z.strictObject({ action: z.literal("replace"), values: credentialsSchema }),
|
|
49
|
+
]);
|
|
50
|
+
export type McpCredentialUpdate = z.infer<typeof mcpCredentialUpdateSchema>;
|
|
51
|
+
export const SaveMcpServer = defineCall("core.mcp.save-server", { scope: "plugin",
|
|
52
|
+
input: z.object({ server: mcpServerSchema, create: z.boolean(), credentials: mcpCredentialUpdateSchema }), output: z.null() });
|
|
53
|
+
export const ReadMcpCredentials = defineCall("core.mcp.read-credentials", { scope: "plugin", input: z.object({ serverId: z.string() }), output: credentialsSchema });
|
|
54
|
+
export const mcpToolSummarySchema = z.object({ name: z.string(), title: z.string().optional(), description: z.string().optional() });
|
|
55
|
+
export type McpToolSummary = z.infer<typeof mcpToolSummarySchema>;
|
|
56
|
+
export const ReadMcpTools = defineCall("core.mcp.read-tools", { scope: "plugin", input: z.object({ serverId: z.string() }),
|
|
57
|
+
output: z.object({ tools: z.array(mcpToolSummarySchema), instructions: z.string().optional() }).nullable() });
|
|
58
|
+
export const SetMcpServerEnabled = defineCall("core.mcp.set-enabled", { scope: "plugin", input: z.object({ serverId: z.string(), enabled: z.boolean() }), output: z.null() });
|
|
59
|
+
export const RemoveMcpServer = defineCall("core.mcp.remove-server", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.null() });
|
|
60
|
+
export const ConnectMcpServer = defineCall("core.mcp.connect-server", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.null() });
|
|
61
|
+
export const AuthorizeMcpServer = defineCall("core.mcp.authorize-server", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.object({ url: z.string() }) });
|
|
62
|
+
export const DisconnectMcpServer = defineCall("core.mcp.disconnect-server", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.null() });
|
|
63
|
+
export const ResetMcpAuth = defineCall("core.mcp.reset-auth", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.null() });
|
|
64
|
+
|
|
65
|
+
export type McpErrorCode = "invalid_tool_id" | "server_not_found" | "server_disabled" | "tool_not_found" | "invalid_tool_arguments"
|
|
66
|
+
| "service_unavailable" | "connection_failed" | "authorization_expired" | "request_failed" | "tool_error";
|
|
67
|
+
export class McpError extends Error {
|
|
68
|
+
readonly code: McpErrorCode;
|
|
69
|
+
constructor(code: McpErrorCode, message: string) { super(message); this.code = code; }
|
|
70
|
+
}
|
|
71
|
+
export class McpInterrupted extends Error {
|
|
72
|
+
readonly outcome: "cancelled" | "unknown";
|
|
73
|
+
constructor(outcome: "cancelled" | "unknown", message: string) { super(message); this.outcome = outcome; }
|
|
74
|
+
}
|
|
75
|
+
export function mcpToolIdentity(serverId: string, name: string): string { return `${serverId}/${name}`; }
|
|
76
|
+
export function parseMcpToolIdentity(value: string): [string, string] {
|
|
77
|
+
const split = value.indexOf("/"), serverId = value.slice(0, split), name = value.slice(split + 1);
|
|
78
|
+
if (split < 1 || !name || !SERVER_ID_PATTERN.test(serverId)) throw new McpError("invalid_tool_id", "Tool identity uses the form server_id/tool_name.");
|
|
79
|
+
return [serverId, name];
|
|
80
|
+
}
|