@hasna/skills 0.4.0 → 0.5.1
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 +220 -5
- package/bin/index.js +7747 -5645
- package/bin/mcp.js +1493 -431
- package/bin/migrate.js +148 -40
- package/bin/server.js +53 -83
- package/bin/worker.js +41 -73
- package/dist/admin-contract.d.ts +37 -19
- package/dist/admin-contract.js +1 -1
- package/dist/cli/cli.test-utils.d.ts +10 -8
- package/dist/cli/commands/customer-profile.d.ts +2 -0
- package/dist/cli/commands/customer-verification.d.ts +5 -0
- package/dist/cli/commands/tool-primitives.d.ts +1 -1
- package/dist/cli/commands/workspace-member-mutations.d.ts +2 -0
- package/dist/cli/commands/workspace-members.d.ts +2 -0
- package/dist/cli/commands/workspace-selection.d.ts +11 -0
- package/dist/cli/env-assignment.d.ts +9 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +841 -167
- package/dist/lib/agent-sync.d.ts +13 -8
- package/dist/lib/api-url.d.ts +4 -3
- package/dist/lib/app-home.d.ts +0 -1
- package/dist/lib/client-types.d.ts +75 -0
- package/dist/lib/credential-state.d.ts +12 -0
- package/dist/lib/fleet-credentials.d.ts +41 -15
- package/dist/lib/home-adoption.d.ts +2 -0
- package/dist/lib/home-census.d.ts +3 -1
- package/dist/lib/local-opt-in.d.ts +24 -0
- package/dist/lib/portable-skills-files.d.ts +6 -2
- package/dist/lib/read-access.d.ts +83 -0
- package/dist/lib/remote-auth.d.ts +23 -3
- package/dist/lib/remote-client.d.ts +45 -5
- package/dist/lib/remote-profile.d.ts +26 -0
- package/dist/lib/remote-registry.d.ts +7 -3
- package/dist/lib/remote-workspace-selection.d.ts +58 -0
- package/dist/lib/remote-workspace.d.ts +76 -0
- package/dist/lib/skillinfo.d.ts +1 -1
- package/dist/lib/workspace-profile.d.ts +49 -0
- package/dist/mcp/helpers.d.ts +22 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/sdk/governance-store.d.ts +1 -0
- package/dist/sdk/index.d.ts +8 -2
- package/dist/sdk/index.js +1312 -297
- package/dist/sdk/outputs.d.ts +0 -11
- package/dist/sdk/runs.d.ts +1 -1
- package/dist/storage.js +6 -40
- package/docs/skill-standard.md +30 -2
- package/package.json +6 -4
- package/dist/lib/instance-credentials-race.fixture.d.ts +0 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RemoteCurrentWorkspace, RemoteCustomerProfile, RemoteCustomerRole } from "./remote-profile.js";
|
|
2
|
+
/** An observed identity and exact membership incarnation, never a slug or cached role. */
|
|
3
|
+
export type RemoteWorkspaceContext = Readonly<{
|
|
4
|
+
userId: string;
|
|
5
|
+
membershipId: string;
|
|
6
|
+
}>;
|
|
7
|
+
export type RemoteAccountWorkspace = {
|
|
8
|
+
membershipId: string;
|
|
9
|
+
organization: RemoteCurrentWorkspace;
|
|
10
|
+
role: RemoteCustomerRole;
|
|
11
|
+
current: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type RemoteAccountWorkspaces = {
|
|
14
|
+
workspaces: RemoteAccountWorkspace[];
|
|
15
|
+
};
|
|
16
|
+
export type RemoteWorkspaceIdentity = {
|
|
17
|
+
user: RemoteCustomerProfile & {
|
|
18
|
+
membershipId: string;
|
|
19
|
+
};
|
|
20
|
+
organization: RemoteCurrentWorkspace;
|
|
21
|
+
};
|
|
22
|
+
/** Contains a secret. SDK callers own its lifetime; never print or persist it implicitly. */
|
|
23
|
+
export type RemoteWorkspaceSession = RemoteWorkspaceIdentity & {
|
|
24
|
+
token: string;
|
|
25
|
+
};
|
|
26
|
+
export type RemoteAccountWorkspaceDiscovery = RemoteAccountWorkspaces & {
|
|
27
|
+
userId: string;
|
|
28
|
+
};
|
|
29
|
+
export declare const invalidWorkspaceResult = "The server returned an invalid workspace selection result.";
|
|
30
|
+
export declare class WorkspaceContextInputError extends Error {
|
|
31
|
+
constructor();
|
|
32
|
+
}
|
|
33
|
+
export declare class WorkspaceIdentityMismatchError extends Error {
|
|
34
|
+
constructor();
|
|
35
|
+
}
|
|
36
|
+
export declare function workspaceExpectedUserId(value: unknown): string;
|
|
37
|
+
/** Copy before the first await: caller mutations cannot retarget verification. */
|
|
38
|
+
export declare function workspaceContext(value: unknown): RemoteWorkspaceContext;
|
|
39
|
+
/** Project documented fields only; server extras cannot reach display metadata. */
|
|
40
|
+
export declare function parseAccountWorkspaces(value: unknown): RemoteAccountWorkspaces;
|
|
41
|
+
export declare function parseWorkspaceIdentity(value: unknown, expectedUserId: string): RemoteWorkspaceIdentity;
|
|
42
|
+
/** The service still verifies the token; response metadata never grants authority. */
|
|
43
|
+
export declare function parseWorkspaceSession(value: unknown, expected: RemoteWorkspaceContext): RemoteWorkspaceSession;
|
|
44
|
+
/** Fresh verification does not require newer workspace metadata from default-only servers. */
|
|
45
|
+
export declare function parseWorkspaceLogin(value: unknown, expectedUserId?: string): {
|
|
46
|
+
token: string;
|
|
47
|
+
userId: string;
|
|
48
|
+
};
|
|
49
|
+
export declare const workspaceSelectionFailures: {
|
|
50
|
+
readonly INVALID_WORKSPACE_SELECTION: readonly [400, "Provide only the exact membership ID from your workspace list."];
|
|
51
|
+
readonly SESSION_EXPIRED: readonly [401, "Sign in again before selecting a workspace."];
|
|
52
|
+
readonly ACCOUNT_UNAVAILABLE: readonly [403, "Account is unavailable."];
|
|
53
|
+
readonly INTERACTIVE_SESSION_REQUIRED: readonly [403, "Interactive sign-in is required to select a workspace."];
|
|
54
|
+
readonly WORKSPACE_UNAVAILABLE: readonly [404, "Workspace is unavailable. Refresh your workspace list."];
|
|
55
|
+
readonly WORKSPACE_BUSY: readonly [503, "Workspace is busy. Refresh before retrying."];
|
|
56
|
+
};
|
|
57
|
+
export type RemoteWorkspaceSelectionErrorCode = keyof typeof workspaceSelectionFailures;
|
|
58
|
+
export declare function workspaceSelectionFailure(value: unknown, status: number): RemoteWorkspaceSelectionErrorCode | null;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { RemoteCustomerRole } from "./remote-profile.js";
|
|
2
|
+
export type RemoteWorkspaceMember = {
|
|
3
|
+
membershipId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
email: string;
|
|
6
|
+
displayName: string | null;
|
|
7
|
+
role: RemoteCustomerRole;
|
|
8
|
+
/** Exact server timestamp, including microseconds; never rounded to Date. */
|
|
9
|
+
createdAt: string;
|
|
10
|
+
};
|
|
11
|
+
export type RemoteWorkspaceMembersPage = {
|
|
12
|
+
organizationId: string;
|
|
13
|
+
members: RemoteWorkspaceMember[];
|
|
14
|
+
nextCursor: string | null;
|
|
15
|
+
};
|
|
16
|
+
export type RemoteWorkspaceMembersOptions = {
|
|
17
|
+
limit?: number;
|
|
18
|
+
cursor?: string;
|
|
19
|
+
};
|
|
20
|
+
export type SetRemoteWorkspaceMemberRole = {
|
|
21
|
+
role: RemoteCustomerRole;
|
|
22
|
+
expectedRole: RemoteCustomerRole;
|
|
23
|
+
};
|
|
24
|
+
export type RemoveRemoteWorkspaceMember = {
|
|
25
|
+
expectedRole: RemoteCustomerRole;
|
|
26
|
+
};
|
|
27
|
+
export type RemoteWorkspaceMemberRoleResult = {
|
|
28
|
+
organizationId: string;
|
|
29
|
+
member: RemoteWorkspaceMember;
|
|
30
|
+
changed: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type RemoteWorkspaceMemberRemovalResult = {
|
|
33
|
+
organizationId: string;
|
|
34
|
+
membershipId: string;
|
|
35
|
+
removed: true;
|
|
36
|
+
alreadyRemoved: boolean;
|
|
37
|
+
};
|
|
38
|
+
/** Pagination is transport input, not a workspace or authority selector. */
|
|
39
|
+
export declare function workspaceMembersQuery(options?: RemoteWorkspaceMembersOptions): string;
|
|
40
|
+
export declare class WorkspaceMemberInputError extends Error {
|
|
41
|
+
constructor();
|
|
42
|
+
}
|
|
43
|
+
/** Snapshot transport input before verification awaits; authority stays on the server. */
|
|
44
|
+
export declare function workspaceMemberRoleInput(membershipId: string, input: SetRemoteWorkspaceMemberRole): {
|
|
45
|
+
membershipId: string;
|
|
46
|
+
body: {
|
|
47
|
+
role: RemoteCustomerRole;
|
|
48
|
+
expectedRole: RemoteCustomerRole;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export declare function workspaceMemberRemovalInput(membershipId: string, input: RemoveRemoteWorkspaceMember): {
|
|
52
|
+
membershipId: string;
|
|
53
|
+
body: {
|
|
54
|
+
expectedRole: RemoteCustomerRole;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export declare const invalidMemberResult = "The server returned an invalid workspace member result. Refresh the roster before another action.";
|
|
58
|
+
export declare function parseWorkspaceMemberRoleResult(value: unknown, membershipId: string, role: RemoteCustomerRole): RemoteWorkspaceMemberRoleResult;
|
|
59
|
+
export declare function parseWorkspaceMemberRemovalResult(value: unknown, membershipId: string): RemoteWorkspaceMemberRemovalResult;
|
|
60
|
+
/** Only known code/status pairs become public messages; arbitrary server text stays private. */
|
|
61
|
+
export declare const workspaceMemberFailures: {
|
|
62
|
+
readonly INVALID_REQUEST: readonly [400, "Provide the exact membership role parameters."];
|
|
63
|
+
readonly ACCOUNT_UNAVAILABLE: readonly [403, "Account is unavailable."];
|
|
64
|
+
readonly INTERACTIVE_SESSION_REQUIRED: readonly [403, "Fresh interactive sign-in is required."];
|
|
65
|
+
readonly WORKSPACE_ADMIN_REQUIRED: readonly [403, "A current workspace owner or admin is required."];
|
|
66
|
+
readonly MEMBERSHIP_ACTION_FORBIDDEN: readonly [403, "Your current workspace role cannot perform this membership action."];
|
|
67
|
+
readonly MEMBERSHIP_NOT_FOUND: readonly [404, "Membership was not found in the current workspace."];
|
|
68
|
+
readonly SELF_REMOVAL_UNAVAILABLE: readonly [409, "Leaving your own workspace is not available through member removal."];
|
|
69
|
+
readonly MEMBERSHIP_ROLE_CHANGED: readonly [409, "The member role changed. Refresh the roster before another action."];
|
|
70
|
+
readonly LAST_OWNER_REQUIRED: readonly [409, "The workspace must retain at least one active owner."];
|
|
71
|
+
readonly MEMBERSHIP_BUSY: readonly [503, "Membership is busy. Refresh the roster before another action."];
|
|
72
|
+
};
|
|
73
|
+
export type RemoteWorkspaceMemberErrorCode = keyof typeof workspaceMemberFailures;
|
|
74
|
+
export declare function workspaceMemberFailure(value: unknown, status: number): RemoteWorkspaceMemberErrorCode | null;
|
|
75
|
+
/** Project the documented roster only; auth metadata and other fields stay out. */
|
|
76
|
+
export declare function parseWorkspaceMembersPage(value: unknown): RemoteWorkspaceMembersPage;
|
package/dist/lib/skillinfo.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export declare function getSkillDependencyStatus(name: string): SkillDependencyS
|
|
|
45
45
|
*/
|
|
46
46
|
export declare function runSkill(name: string, args: string[], options?: {
|
|
47
47
|
installed?: boolean;
|
|
48
|
-
stdio?: "inherit" | "pipe";
|
|
48
|
+
stdio?: "inherit" | "pipe" | "stderr";
|
|
49
49
|
env?: Record<string, string>;
|
|
50
50
|
}): Promise<{
|
|
51
51
|
exitCode: number;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
type Env = Record<string, string | undefined>;
|
|
2
|
+
export declare class WorkspaceProfileError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** Capture actual key authority before OTP; cached identity never supplies authority. */
|
|
5
|
+
export declare function prepareProfileWorkspace(action: string, source?: Env): {
|
|
6
|
+
origin: string;
|
|
7
|
+
resolve(): Promise<{
|
|
8
|
+
origin: string;
|
|
9
|
+
context: undefined;
|
|
10
|
+
unchanged: () => void;
|
|
11
|
+
} | {
|
|
12
|
+
origin: string;
|
|
13
|
+
context: Readonly<{
|
|
14
|
+
userId: string;
|
|
15
|
+
membershipId: string;
|
|
16
|
+
}>;
|
|
17
|
+
unchanged: () => void;
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
20
|
+
export declare function captureProfileWorkspace(action: string, source?: Env): Promise<{
|
|
21
|
+
origin: string;
|
|
22
|
+
context: undefined;
|
|
23
|
+
unchanged: () => void;
|
|
24
|
+
} | {
|
|
25
|
+
origin: string;
|
|
26
|
+
context: Readonly<{
|
|
27
|
+
userId: string;
|
|
28
|
+
membershipId: string;
|
|
29
|
+
}>;
|
|
30
|
+
unchanged: () => void;
|
|
31
|
+
}>;
|
|
32
|
+
/** Own only the explicitly named profile; injected credentials may not hide enrollment. */
|
|
33
|
+
export declare function prepareWorkspaceEnrollment(membershipId: string, source?: Env): Promise<{
|
|
34
|
+
profile: string;
|
|
35
|
+
origin: string;
|
|
36
|
+
complete(email: string, code: string): Promise<{
|
|
37
|
+
status: string;
|
|
38
|
+
profile: string;
|
|
39
|
+
apiUrl: string;
|
|
40
|
+
userId: string;
|
|
41
|
+
email: string;
|
|
42
|
+
membershipId: string;
|
|
43
|
+
organization: string;
|
|
44
|
+
organizationId: string;
|
|
45
|
+
role: import("./remote-profile.js").RemoteCustomerRole;
|
|
46
|
+
keyCreated: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
}>;
|
|
49
|
+
export {};
|
package/dist/mcp/helpers.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/** The shape every tool handler returns; kept structural so registrars need no SDK type import. */
|
|
2
|
+
type McpToolResult = {
|
|
3
|
+
content: Array<{
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
}>;
|
|
7
|
+
isError?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Run a DATA tool behind the fleet ladder's refusal.
|
|
11
|
+
*
|
|
12
|
+
* The body is expected to go through `requireSkillsReadAccess()` /
|
|
13
|
+
* `getBrowseRegistry()` (lib/read-access.ts) before it reads the bundled
|
|
14
|
+
* catalog or the on-machine corpus. When the ladder refuses — no credential, no
|
|
15
|
+
* authority, no `HASNA_SKILLS_LOCAL=1` opt-in; an authority with no key; a
|
|
16
|
+
* deliberate selection that cannot be honoured — the refusal comes back as the
|
|
17
|
+
* same `AUTH_REQUIRED` result `get_run_status` already gives, never as the
|
|
18
|
+
* local answer. Any other failure propagates unchanged (the SDK reports it as
|
|
19
|
+
* a tool error with its message).
|
|
20
|
+
*/
|
|
21
|
+
export declare function readSurface(body: () => Promise<McpToolResult>): Promise<McpToolResult>;
|
|
1
22
|
export declare function stripNulls(obj: Record<string, unknown>): Record<string, unknown>;
|
|
2
23
|
export declare function cacheGet(key: string): unknown | undefined;
|
|
3
24
|
export declare function cacheSet(key: string, value: unknown): void;
|
|
@@ -24,3 +45,4 @@ export declare const TOOL_DESCRIPTIONS: Record<string, {
|
|
|
24
45
|
description: string;
|
|
25
46
|
params: string[];
|
|
26
47
|
}>;
|
|
48
|
+
export {};
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -8,6 +8,22 @@
|
|
|
8
8
|
* skills-mcp # Direct binary
|
|
9
9
|
* skills-mcp --http # Streamable HTTP on 127.0.0.1:8836
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* FAIL CLOSED AT STARTUP (owner ruling 2026-09-04, hasna/apps#1720; #1720
|
|
13
|
+
* validation, round 1): the MCP process resolves the fleet ladder BEFORE it
|
|
14
|
+
* connects any transport. With no credential, no authority and no
|
|
15
|
+
* `HASNA_SKILLS_LOCAL=1` opt-in it exits 1 with the ladder's one line on
|
|
16
|
+
* stderr — before `initialize` is answered, before an HTTP port is bound —
|
|
17
|
+
* mirroring the CLI, which exits 1 on the same machine. Serving the bundled
|
|
18
|
+
* catalog to an agent host that registered `skills-mcp` on a station without
|
|
19
|
+
* its credential is the false green the ruling removed. The explicit local
|
|
20
|
+
* opt-in starts the server and announces local mode once on stderr; a
|
|
21
|
+
* credential (or a vault pointer the tools complete per call) starts it hosted.
|
|
22
|
+
*
|
|
23
|
+
* Every data tool still runs the same gate per call (lib/read-access.ts), so a
|
|
24
|
+
* credential that disappears mid-process is refused by the next call too.
|
|
25
|
+
*/
|
|
26
|
+
export declare function assertSkillsMcpConfigured(env?: Record<string, string | undefined>): void;
|
|
11
27
|
/**
|
|
12
28
|
* Start the Skills MCP server on stdio (newline-delimited JSON-RPC).
|
|
13
29
|
*
|
|
@@ -51,6 +51,7 @@ export interface GovernanceStore {
|
|
|
51
51
|
estimatedCents: number;
|
|
52
52
|
}): Promise<CreditReservation>;
|
|
53
53
|
reservationsForRun(orgId: string, runId: string): Promise<CreditReservation[]>;
|
|
54
|
+
/** First terminal reconciliation wins; retries return that persisted state unchanged. */
|
|
54
55
|
reconcileReservation(reservationId: string, actualCents: number, status: "charged" | "released"): Promise<CreditReservation | null>;
|
|
55
56
|
/** Sum of cost_cents of runs created in the given calendar month (YYYY-MM), plus un-reconciled reservations. */
|
|
56
57
|
monthlySpendCents(orgId: string, monthPrefix: string): Promise<number>;
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* — the shared @hasna/contracts ladder — instead of reading an environment
|
|
16
16
|
* variable of its own. See lib/fleet-credentials.ts.
|
|
17
17
|
*/
|
|
18
|
-
export { MissingSkillsFleetError, SkillsFleetCredentialError, SKILLS_API_KEY_ENV, SKILLS_API_URL_ENV, SKILLS_APP, configuredSkillsApiUrl, noticeLocalSkillsMode, normalizeSkillsApiOrigin, requireSkillsApiOrigin, requireSkillsFleet, resolveSkillsApiOrigin, resolveSkillsFleet, skillsCredentialFilePath, skillsCredentialFiles, skillsCredentialOrReason, type HostedSkillsFleet, type LocalSkillsFleet, type SkillsFleet, type SkillsFleetErrorCode, type SkillsFleetOptions, } from "../lib/fleet-credentials.js";
|
|
18
|
+
export { MissingSkillsFleetError, SkillsFleetCredentialError, SKILLS_API_KEY_ENV, SKILLS_API_URL_ENV, SKILLS_APP, SKILLS_LOCAL_OPT_IN_ENV_KEYS, configuredSkillsApiUrl, isSkillsLocalOptIn, noticeLocalSkillsMode, normalizeSkillsApiOrigin, requireSkillsApiOrigin, requireSkillsFleet, resolveSkillsApiOrigin, resolveSkillsFleet, selectsSkillsLocalMode, skillsCredentialFilePath, skillsCredentialFiles, skillsCredentialOrReason, type HostedSkillsFleet, type LocalSkillsFleet, type SkillsFleet, type SkillsFleetErrorCode, type SkillsFleetOptions, } from "../lib/fleet-credentials.js";
|
|
19
19
|
export * from "./server.js";
|
|
20
20
|
export * from "./registry.js";
|
|
21
21
|
export * from "./runs.js";
|
|
@@ -30,7 +30,13 @@ export * from "./events.js";
|
|
|
30
30
|
export * from "./spend.js";
|
|
31
31
|
export * from "./offline.js";
|
|
32
32
|
export * from "./execution/index.js";
|
|
33
|
-
export { RemoteSkillsClient, createRemoteSkillsClient, RemoteRequestError, RemoteRouteUnsupportedError } from "../lib/remote-client.js";
|
|
33
|
+
export { RemoteSkillsClient, createRemoteSkillsClient, RemoteRequestError, RemoteRouteUnsupportedError, RemoteCapabilityUnavailableError, RemoteWorkspaceMemberError } from "../lib/remote-client.js";
|
|
34
34
|
export { RemoteCreditApprovalError, type RemoteRunApproval, type RemoteRunQuote, type RemoteCreditPack } from "../lib/remote-account.js";
|
|
35
35
|
export { type RemoteInputFile, type RemoteInputFileDescriptor } from "../lib/remote-files.js";
|
|
36
36
|
export { RemoteSkillsAuthClient, HostedApiError } from "../lib/remote-auth.js";
|
|
37
|
+
export type { RemoteWorkspaceMember, RemoteWorkspaceMembersPage, RemoteWorkspaceMembersOptions } from "../lib/remote-workspace.js";
|
|
38
|
+
export type { SetRemoteWorkspaceMemberRole, RemoveRemoteWorkspaceMember, RemoteWorkspaceMemberRoleResult, RemoteWorkspaceMemberRemovalResult, RemoteWorkspaceMemberErrorCode } from "../lib/remote-workspace.js";
|
|
39
|
+
export type { RemoteCustomerRole, RemoteCustomerProfile, RemoteCurrentWorkspace, UpdateRemoteProfile, UpdateRemoteWorkspace } from "../lib/remote-profile.js";
|
|
40
|
+
export type { RemoteWorkspaceContext, RemoteAccountWorkspace, RemoteAccountWorkspaces, RemoteWorkspaceIdentity, RemoteWorkspaceSession, RemoteAccountWorkspaceDiscovery, RemoteWorkspaceSelectionErrorCode } from "../lib/remote-workspace-selection.js";
|
|
41
|
+
export { WorkspaceContextInputError, WorkspaceIdentityMismatchError } from "../lib/remote-workspace-selection.js";
|
|
42
|
+
export { RemoteWorkspaceSelectionError } from "../lib/remote-client.js";
|