@nudge-ui/mcp 0.1.2 → 0.2.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 +10 -6
- package/dist/bridge.d.ts +6 -6
- package/dist/bridge.js +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.mjs +11 -1
- package/dist/internal/agent-protocol.d.ts +232 -0
- package/dist/internal/agent-protocol.js +237 -0
- package/dist/protocol.d.ts +5 -5
- package/dist/protocol.js +4 -4
- package/dist/server.d.ts +2 -2
- package/dist/server.js +13 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# `@nudge-ui/mcp`
|
|
2
2
|
|
|
3
|
+
> **Early alpha:** This integration is still under heavy testing. Expect
|
|
4
|
+
> breaking changes, incomplete host compatibility, and other rough edges. Do
|
|
5
|
+
> not rely on it for production workflows yet.
|
|
6
|
+
|
|
3
7
|
`@nudge-ui/mcp` connects a local Nudge UI inspector to an MCP-capable coding
|
|
4
8
|
agent. It combines a standard MCP stdio server with a project-scoped loopback
|
|
5
9
|
browser bridge. The browser does not speak MCP directly.
|
|
@@ -7,7 +11,7 @@ browser bridge. The browser does not speak MCP directly.
|
|
|
7
11
|
## Install
|
|
8
12
|
|
|
9
13
|
```sh
|
|
10
|
-
pnpm add -D @nudge-ui/mcp
|
|
14
|
+
pnpm add -D @nudge-ui/mcp@0.2.0
|
|
11
15
|
```
|
|
12
16
|
|
|
13
17
|
### Configure automatically
|
|
@@ -16,7 +20,7 @@ From the application project root, use [`add-mcp`](https://github.com/neon-solut
|
|
|
16
20
|
|
|
17
21
|
```sh
|
|
18
22
|
npx add-mcp \
|
|
19
|
-
'npx -y @nudge-ui/mcp@
|
|
23
|
+
'npx -y @nudge-ui/mcp@0.2.0 --project-id my-app --origin http://localhost:5173 --workspace-root .' \
|
|
20
24
|
--name nudge_ui
|
|
21
25
|
```
|
|
22
26
|
|
|
@@ -31,10 +35,10 @@ For standard Vite and Astro projects, `my-app` normally matches the project
|
|
|
31
35
|
directory name. Next.js and standalone HTML projects use host-specific IDs, so
|
|
32
36
|
retain the explicit ID from their integration configuration.
|
|
33
37
|
|
|
34
|
-
The generated command
|
|
35
|
-
`npx
|
|
36
|
-
|
|
37
|
-
so it refreshes its MCP tool catalog.
|
|
38
|
+
The generated command pins the published `@nudge-ui/mcp@0.2.0` package through
|
|
39
|
+
`npx` for reproducible tool versions. Update the version deliberately when
|
|
40
|
+
upgrading the MCP integration. After configuration, restart or reload the
|
|
41
|
+
agent host so it refreshes its MCP tool catalog.
|
|
38
42
|
|
|
39
43
|
The package has no install-time project mutation. The manual configuration
|
|
40
44
|
below remains useful when an agent host is not supported by `add-mcp` or when
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
|
-
import type { AgentPromptRequest, AgentRequestOutcome, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult, PairingResponse } from "
|
|
2
|
+
import type { AgentPromptRequest, AgentRequestOutcome, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult, PairingResponse } from "./internal/agent-protocol.js";
|
|
3
3
|
/** A clock port keeps command timeout tests deterministic. */
|
|
4
4
|
export interface BridgeClock {
|
|
5
5
|
now(): number;
|
|
@@ -61,15 +61,15 @@ export interface BrowserBridge {
|
|
|
61
61
|
/** Revokes a browser pairing; no data is written to disk. */
|
|
62
62
|
disconnectBrowser(token: string): void;
|
|
63
63
|
}
|
|
64
|
-
export type CanvasCommandInput = Omit<import("
|
|
64
|
+
export type CanvasCommandInput = Omit<import("./internal/agent-protocol.js").ReadCanvasStateCommand, "commandId"> & {
|
|
65
65
|
readonly commandId?: string;
|
|
66
|
-
} | Omit<import("
|
|
66
|
+
} | Omit<import("./internal/agent-protocol.js").PresentRoutesCommand, "commandId"> & {
|
|
67
67
|
readonly commandId?: string;
|
|
68
|
-
} | Omit<import("
|
|
68
|
+
} | Omit<import("./internal/agent-protocol.js").FocusCanvasGroupCommand, "commandId"> & {
|
|
69
69
|
readonly commandId?: string;
|
|
70
|
-
} | Omit<import("
|
|
70
|
+
} | Omit<import("./internal/agent-protocol.js").FitCanvasCommand, "commandId"> & {
|
|
71
71
|
readonly commandId?: string;
|
|
72
|
-
} | Omit<import("
|
|
72
|
+
} | Omit<import("./internal/agent-protocol.js").RemoveCanvasGroupCommand, "commandId"> & {
|
|
73
73
|
readonly commandId?: string;
|
|
74
74
|
};
|
|
75
75
|
/**
|
package/dist/bridge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer, } from "node:http";
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
|
-
import { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, canonicalOrigin, isAgentStatusUpdate, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isSameOriginRoute, validateRoutes, } from "
|
|
3
|
+
import { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, canonicalOrigin, isAgentStatusUpdate, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isSameOriginRoute, validateRoutes, } from "./internal/agent-protocol.js";
|
|
4
4
|
import { BRIDGE_ENDPOINTS } from "./protocol.js";
|
|
5
5
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
6
6
|
const MAX_BODY_BYTES = 256 * 1024;
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
|
-
import { defaultBridgePort } from "
|
|
4
|
+
import { defaultBridgePort } from "./internal/agent-protocol.js";
|
|
5
5
|
import { createAgentCompanion } from "./server.js";
|
|
6
6
|
const DEFAULT_PORT = 0;
|
|
7
7
|
const DEFAULT_COMMAND_TIMEOUT_MS = 5_000;
|
|
@@ -62,6 +62,7 @@ export function parseCliArguments(argv = process.argv.slice(2), environment = pr
|
|
|
62
62
|
const root = resolve(environment.NUDGE_UI_WORKSPACE_ROOT ?? environment.INIT_CWD ?? process.cwd());
|
|
63
63
|
let projectId = environment.NUDGE_UI_PROJECT_ID ?? projectIdFor(root);
|
|
64
64
|
let origin = environment.NUDGE_UI_ORIGIN;
|
|
65
|
+
// SAFETY: NUDGE_UI_BRIDGE_HOST is trusted operator config for a loopback-bound dev bridge; CliOptions["host"] is its declared address union.
|
|
65
66
|
let host = (environment.NUDGE_UI_BRIDGE_HOST ?? "127.0.0.1");
|
|
66
67
|
let port = environment.NUDGE_UI_BRIDGE_PORT === undefined
|
|
67
68
|
? DEFAULT_PORT
|
package/dist/cli.mjs
CHANGED
|
@@ -21578,6 +21578,9 @@ var StdioServerTransport = class {
|
|
|
21578
21578
|
}
|
|
21579
21579
|
};
|
|
21580
21580
|
|
|
21581
|
+
// src/server.ts
|
|
21582
|
+
import { createRequire } from "node:module";
|
|
21583
|
+
|
|
21581
21584
|
// src/bridge.ts
|
|
21582
21585
|
import {
|
|
21583
21586
|
createServer
|
|
@@ -22285,7 +22288,14 @@ function createLoopbackBridge(options) {
|
|
|
22285
22288
|
|
|
22286
22289
|
// src/server.ts
|
|
22287
22290
|
var MCP_SERVER_NAME = "nudge-ui";
|
|
22288
|
-
var MCP_SERVER_VERSION =
|
|
22291
|
+
var MCP_SERVER_VERSION = readPackageVersion();
|
|
22292
|
+
function readPackageVersion() {
|
|
22293
|
+
const metadata = createRequire(import.meta.url)("../package.json");
|
|
22294
|
+
if (typeof metadata !== "object" || metadata === null || !("version" in metadata) || typeof metadata.version !== "string") {
|
|
22295
|
+
throw new TypeError("@nudge-ui/mcp package metadata has no version.");
|
|
22296
|
+
}
|
|
22297
|
+
return metadata.version;
|
|
22298
|
+
}
|
|
22289
22299
|
var MCP_SERVER_INSTRUCTIONS = [
|
|
22290
22300
|
"Nudge UI is a local browser companion for one project workspace.",
|
|
22291
22301
|
"Browser pairing requires the app origin to be configured explicitly with --origin or NUDGE_UI_ORIGIN; an unconfigured bridge accepts no HTTP pairing.",
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public, host-neutral contracts shared by the Nudge browser inspector and
|
|
3
|
+
* its local MCP companion.
|
|
4
|
+
*
|
|
5
|
+
* This module deliberately has no Node, browser, MCP SDK, or framework
|
|
6
|
+
* imports. Hosts can use the same validation and message shapes without
|
|
7
|
+
* pulling the companion into a browser bundle.
|
|
8
|
+
*/
|
|
9
|
+
export declare const AGENT_PROTOCOL_VERSION: 1;
|
|
10
|
+
export declare const AGENT_PROTOCOL_LIMITS: {
|
|
11
|
+
readonly projectId: 256;
|
|
12
|
+
readonly workspaceRoot: 4096;
|
|
13
|
+
readonly origin: 2048;
|
|
14
|
+
readonly sessionToken: 256;
|
|
15
|
+
readonly requestId: 256;
|
|
16
|
+
readonly commandId: 256;
|
|
17
|
+
readonly prompt: 32000;
|
|
18
|
+
readonly groupId: 256;
|
|
19
|
+
readonly label: 160;
|
|
20
|
+
readonly routeUrl: 2048;
|
|
21
|
+
readonly routeTitle: 512;
|
|
22
|
+
readonly routeCount: 64;
|
|
23
|
+
readonly canvasCardCount: 128;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Returns the default project-specific loopback port used by the CLI.
|
|
27
|
+
*
|
|
28
|
+
* The value is deterministic so a browser can probe the companion without a
|
|
29
|
+
* port file, while the range stays high enough to avoid common dev servers.
|
|
30
|
+
* Callers may still override it with an explicit port when a collision occurs.
|
|
31
|
+
*/
|
|
32
|
+
export declare function defaultBridgePort(projectId: string): number;
|
|
33
|
+
export type AgentProtocolVersion = typeof AGENT_PROTOCOL_VERSION;
|
|
34
|
+
/** A project identity is the pairing boundary for one companion process. */
|
|
35
|
+
export interface AgentProjectIdentity {
|
|
36
|
+
readonly projectId: string;
|
|
37
|
+
/** Omitted when a trusted host establishes the browser origin out of band. */
|
|
38
|
+
readonly origin?: string;
|
|
39
|
+
readonly workspaceRoot?: string;
|
|
40
|
+
}
|
|
41
|
+
/** A same-origin route that an agent asks the browser to add to Canvas. */
|
|
42
|
+
export interface AgentRoute {
|
|
43
|
+
readonly url: string;
|
|
44
|
+
readonly title?: string;
|
|
45
|
+
/** A short user-facing name. When absent, the browser derives one. */
|
|
46
|
+
readonly label?: string;
|
|
47
|
+
}
|
|
48
|
+
/** The owner marker lets the browser remove only groups created by the agent. */
|
|
49
|
+
export type CanvasGroupOwner = "agent" | "user";
|
|
50
|
+
/** A browser-reported comparison group. */
|
|
51
|
+
export interface CanvasGroup {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly label: string;
|
|
54
|
+
readonly owner: CanvasGroupOwner;
|
|
55
|
+
readonly routes: readonly AgentRoute[];
|
|
56
|
+
}
|
|
57
|
+
/** A compact, serialisable Canvas snapshot returned by the browser. */
|
|
58
|
+
export interface CanvasState {
|
|
59
|
+
readonly mode: "inspect" | "canvas";
|
|
60
|
+
readonly groups: readonly CanvasGroup[];
|
|
61
|
+
readonly focusedGroupId?: string | null;
|
|
62
|
+
readonly focusedRouteUrl?: string | null;
|
|
63
|
+
}
|
|
64
|
+
export declare const CANVAS_COMMAND_TYPES: readonly ["read-state", "present-routes", "focus-group", "fit-all", "remove-group"];
|
|
65
|
+
export type CanvasCommandType = (typeof CANVAS_COMMAND_TYPES)[number];
|
|
66
|
+
export interface ReadCanvasStateCommand {
|
|
67
|
+
readonly type: "read-state";
|
|
68
|
+
readonly commandId: string;
|
|
69
|
+
}
|
|
70
|
+
export interface PresentRoutesCommand {
|
|
71
|
+
readonly type: "present-routes";
|
|
72
|
+
readonly commandId: string;
|
|
73
|
+
readonly groupId: string;
|
|
74
|
+
readonly label: string;
|
|
75
|
+
readonly routes: readonly AgentRoute[];
|
|
76
|
+
}
|
|
77
|
+
export interface FocusCanvasGroupCommand {
|
|
78
|
+
readonly type: "focus-group";
|
|
79
|
+
readonly commandId: string;
|
|
80
|
+
readonly groupId: string;
|
|
81
|
+
}
|
|
82
|
+
export interface FitCanvasCommand {
|
|
83
|
+
readonly type: "fit-all";
|
|
84
|
+
readonly commandId: string;
|
|
85
|
+
}
|
|
86
|
+
export interface RemoveCanvasGroupCommand {
|
|
87
|
+
readonly type: "remove-group";
|
|
88
|
+
readonly commandId: string;
|
|
89
|
+
readonly groupId: string;
|
|
90
|
+
}
|
|
91
|
+
/** Commands sent from the companion to the paired browser over SSE. */
|
|
92
|
+
export type CanvasCommand = ReadCanvasStateCommand | PresentRoutesCommand | FocusCanvasGroupCommand | FitCanvasCommand | RemoveCanvasGroupCommand;
|
|
93
|
+
export interface CanvasCommandError {
|
|
94
|
+
readonly code: string;
|
|
95
|
+
readonly message: string;
|
|
96
|
+
}
|
|
97
|
+
export interface CanvasCommandResult {
|
|
98
|
+
readonly commandId: string;
|
|
99
|
+
readonly ok: boolean;
|
|
100
|
+
readonly state?: CanvasState;
|
|
101
|
+
readonly group?: CanvasGroup;
|
|
102
|
+
readonly error?: CanvasCommandError;
|
|
103
|
+
}
|
|
104
|
+
/** A prompt is immutable after it is accepted by the browser bridge. */
|
|
105
|
+
export interface AgentPromptRequest {
|
|
106
|
+
readonly requestId: string;
|
|
107
|
+
readonly projectId: string;
|
|
108
|
+
readonly prompt: string;
|
|
109
|
+
/** The browser's canonical change revision captured at dispatch time. */
|
|
110
|
+
readonly changeRevision?: number;
|
|
111
|
+
}
|
|
112
|
+
export type AgentRequestStatus = "working" | "completed" | "failed" | "interrupted";
|
|
113
|
+
export interface AgentRequestOutcome {
|
|
114
|
+
readonly requestId: string;
|
|
115
|
+
readonly status: AgentRequestStatus;
|
|
116
|
+
readonly summary?: string;
|
|
117
|
+
readonly error?: string;
|
|
118
|
+
}
|
|
119
|
+
export type AgentConnectionState = "offline" | "listening" | "paired" | "working";
|
|
120
|
+
export interface AgentStatusSnapshot {
|
|
121
|
+
readonly protocolVersion: AgentProtocolVersion;
|
|
122
|
+
readonly projectId: string;
|
|
123
|
+
readonly connection: AgentConnectionState;
|
|
124
|
+
readonly listenerActive: boolean;
|
|
125
|
+
readonly paired: boolean;
|
|
126
|
+
/** The last paired page, retained only for this process lifetime. */
|
|
127
|
+
readonly pageUrl?: string | null;
|
|
128
|
+
readonly request: (AgentPromptRequest & {
|
|
129
|
+
readonly status: AgentRequestStatus;
|
|
130
|
+
readonly summary?: string;
|
|
131
|
+
readonly error?: string;
|
|
132
|
+
}) | null;
|
|
133
|
+
}
|
|
134
|
+
export type BridgeEventType = "status" | "canvas-command" | "connected" | "disconnected";
|
|
135
|
+
export interface BridgeStatusEvent {
|
|
136
|
+
readonly type: "status";
|
|
137
|
+
readonly status: AgentStatusSnapshot;
|
|
138
|
+
}
|
|
139
|
+
export interface BridgeConnectedEvent {
|
|
140
|
+
readonly type: "connected";
|
|
141
|
+
readonly status: AgentStatusSnapshot;
|
|
142
|
+
}
|
|
143
|
+
export interface BridgeDisconnectedEvent {
|
|
144
|
+
readonly type: "disconnected";
|
|
145
|
+
readonly reason?: string;
|
|
146
|
+
}
|
|
147
|
+
export interface BridgeCanvasCommandEvent {
|
|
148
|
+
readonly type: "canvas-command";
|
|
149
|
+
readonly command: CanvasCommand;
|
|
150
|
+
}
|
|
151
|
+
/** Payload delivered to a browser SSE connection. */
|
|
152
|
+
export type BridgeEvent = BridgeStatusEvent | BridgeConnectedEvent | BridgeDisconnectedEvent | BridgeCanvasCommandEvent;
|
|
153
|
+
export interface BridgeEnvelope {
|
|
154
|
+
readonly protocolVersion: AgentProtocolVersion;
|
|
155
|
+
readonly projectId: string;
|
|
156
|
+
readonly sessionToken: string;
|
|
157
|
+
readonly event: BridgeEvent;
|
|
158
|
+
}
|
|
159
|
+
export interface CanvasCommandAcknowledgement {
|
|
160
|
+
readonly commandId: string;
|
|
161
|
+
readonly ok: boolean;
|
|
162
|
+
readonly state?: CanvasState;
|
|
163
|
+
readonly group?: CanvasGroup;
|
|
164
|
+
readonly error?: CanvasCommandError;
|
|
165
|
+
}
|
|
166
|
+
export interface PairingRequest {
|
|
167
|
+
readonly projectId: string;
|
|
168
|
+
readonly origin: string;
|
|
169
|
+
/** The active page is retained only while this process is alive. */
|
|
170
|
+
readonly pageUrl?: string;
|
|
171
|
+
}
|
|
172
|
+
export interface PairingResponse {
|
|
173
|
+
readonly protocolVersion: AgentProtocolVersion;
|
|
174
|
+
readonly projectId: string;
|
|
175
|
+
readonly origin: string;
|
|
176
|
+
readonly sessionToken: string;
|
|
177
|
+
readonly pageUrl?: string;
|
|
178
|
+
readonly status: AgentStatusSnapshot;
|
|
179
|
+
}
|
|
180
|
+
export interface PromptDispatchRequest {
|
|
181
|
+
readonly projectId: string;
|
|
182
|
+
readonly sessionToken: string;
|
|
183
|
+
readonly prompt: string;
|
|
184
|
+
readonly changeRevision?: number;
|
|
185
|
+
}
|
|
186
|
+
export interface PromptDispatchResponse {
|
|
187
|
+
readonly request: AgentPromptRequest;
|
|
188
|
+
readonly status: AgentRequestStatus;
|
|
189
|
+
}
|
|
190
|
+
export interface StatusEventRequest {
|
|
191
|
+
readonly projectId: string;
|
|
192
|
+
readonly sessionToken: string;
|
|
193
|
+
}
|
|
194
|
+
export interface AgentStatusUpdate {
|
|
195
|
+
readonly requestId: string;
|
|
196
|
+
readonly status: AgentRequestStatus;
|
|
197
|
+
readonly summary?: string;
|
|
198
|
+
readonly error?: string;
|
|
199
|
+
}
|
|
200
|
+
export interface ProtocolErrorShape {
|
|
201
|
+
readonly code: string;
|
|
202
|
+
readonly message: string;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Returns a canonical origin or null. Credentials, paths, and fragments are
|
|
206
|
+
* rejected so the browser cannot broaden the pairing boundary accidentally.
|
|
207
|
+
*/
|
|
208
|
+
export declare function canonicalOrigin(value: unknown): string | null;
|
|
209
|
+
/** Returns true only for exact, canonical HTTP(S) origins. */
|
|
210
|
+
export declare function isCanonicalOrigin(value: unknown): value is string;
|
|
211
|
+
/**
|
|
212
|
+
* Checks an origin against an explicit allow-list. An empty allow-list does
|
|
213
|
+
* not mean wildcard access; callers must choose an origin before pairing.
|
|
214
|
+
*/
|
|
215
|
+
export declare function isAllowedOrigin(value: unknown, allowedOrigins: readonly string[]): value is string;
|
|
216
|
+
/**
|
|
217
|
+
* Validates an agent route and ensures it belongs to the paired origin.
|
|
218
|
+
* Fragments are retained because they are useful initial Canvas locations.
|
|
219
|
+
*/
|
|
220
|
+
export declare function isSameOriginRoute(value: unknown, origin: string): value is AgentRoute;
|
|
221
|
+
/** Validates and normalises route values for a presentation command. */
|
|
222
|
+
export declare function validateRoutes(value: unknown, origin: string): AgentRoute[];
|
|
223
|
+
/** Validates the fields shared by every companion message. */
|
|
224
|
+
export declare function isProtocolVersion(value: unknown): value is AgentProtocolVersion;
|
|
225
|
+
export declare function isProjectIdentity(value: unknown): value is AgentProjectIdentity;
|
|
226
|
+
export declare function isAgentPromptRequest(value: unknown): value is AgentPromptRequest;
|
|
227
|
+
export declare function isAgentStatusUpdate(value: unknown): value is AgentStatusUpdate;
|
|
228
|
+
export declare function isCanvasState(value: unknown): value is CanvasState;
|
|
229
|
+
export declare function isCanvasGroup(value: unknown): value is CanvasGroup;
|
|
230
|
+
export declare function isCanvasCommand(value: unknown): value is CanvasCommand;
|
|
231
|
+
/** Creates a stable protocol error shape without exposing thrown values. */
|
|
232
|
+
export declare function protocolError(code: string, message: string): ProtocolErrorShape;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public, host-neutral contracts shared by the Nudge browser inspector and
|
|
3
|
+
* its local MCP companion.
|
|
4
|
+
*
|
|
5
|
+
* This module deliberately has no Node, browser, MCP SDK, or framework
|
|
6
|
+
* imports. Hosts can use the same validation and message shapes without
|
|
7
|
+
* pulling the companion into a browser bundle.
|
|
8
|
+
*/
|
|
9
|
+
export const AGENT_PROTOCOL_VERSION = 1;
|
|
10
|
+
export const AGENT_PROTOCOL_LIMITS = {
|
|
11
|
+
projectId: 256,
|
|
12
|
+
workspaceRoot: 4096,
|
|
13
|
+
origin: 2048,
|
|
14
|
+
sessionToken: 256,
|
|
15
|
+
requestId: 256,
|
|
16
|
+
commandId: 256,
|
|
17
|
+
prompt: 32_000,
|
|
18
|
+
groupId: 256,
|
|
19
|
+
label: 160,
|
|
20
|
+
routeUrl: 2048,
|
|
21
|
+
routeTitle: 512,
|
|
22
|
+
routeCount: 64,
|
|
23
|
+
canvasCardCount: 128,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Returns the default project-specific loopback port used by the CLI.
|
|
27
|
+
*
|
|
28
|
+
* The value is deterministic so a browser can probe the companion without a
|
|
29
|
+
* port file, while the range stays high enough to avoid common dev servers.
|
|
30
|
+
* Callers may still override it with an explicit port when a collision occurs.
|
|
31
|
+
*/
|
|
32
|
+
export function defaultBridgePort(projectId) {
|
|
33
|
+
if (!nonEmptyString(projectId, AGENT_PROTOCOL_LIMITS.projectId)) {
|
|
34
|
+
throw new TypeError("projectId must be a bounded non-empty string");
|
|
35
|
+
}
|
|
36
|
+
let hash = 2_166_136_261;
|
|
37
|
+
for (let index = 0; index < projectId.length; index += 1) {
|
|
38
|
+
hash ^= projectId.charCodeAt(index);
|
|
39
|
+
hash = Math.imul(hash, 16_777_619);
|
|
40
|
+
}
|
|
41
|
+
return 30_000 + ((hash >>> 0) % 20_000);
|
|
42
|
+
}
|
|
43
|
+
export const CANVAS_COMMAND_TYPES = [
|
|
44
|
+
"read-state",
|
|
45
|
+
"present-routes",
|
|
46
|
+
"focus-group",
|
|
47
|
+
"fit-all",
|
|
48
|
+
"remove-group",
|
|
49
|
+
];
|
|
50
|
+
const ORIGIN_PROTOCOLS = new Set(["http:", "https:"]);
|
|
51
|
+
function isRecord(value) {
|
|
52
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
+
}
|
|
54
|
+
function hasOnlyKeys(value, keys) {
|
|
55
|
+
const allowed = new Set(keys);
|
|
56
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
57
|
+
}
|
|
58
|
+
function nonEmptyString(value, maximum) {
|
|
59
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximum;
|
|
60
|
+
}
|
|
61
|
+
function optionalString(value, maximum) {
|
|
62
|
+
return value === undefined || (typeof value === "string" && value.length <= maximum);
|
|
63
|
+
}
|
|
64
|
+
function optionalRevision(value) {
|
|
65
|
+
return value === undefined || (typeof value === "number" && Number.isSafeInteger(value) && value >= 0);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns a canonical origin or null. Credentials, paths, and fragments are
|
|
69
|
+
* rejected so the browser cannot broaden the pairing boundary accidentally.
|
|
70
|
+
*/
|
|
71
|
+
export function canonicalOrigin(value) {
|
|
72
|
+
if (!nonEmptyString(value, AGENT_PROTOCOL_LIMITS.origin))
|
|
73
|
+
return null;
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = new URL(value);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!ORIGIN_PROTOCOLS.has(parsed.protocol) || parsed.username || parsed.password)
|
|
82
|
+
return null;
|
|
83
|
+
if (parsed.pathname !== "/" || parsed.search || parsed.hash)
|
|
84
|
+
return null;
|
|
85
|
+
return parsed.origin;
|
|
86
|
+
}
|
|
87
|
+
/** Returns true only for exact, canonical HTTP(S) origins. */
|
|
88
|
+
export function isCanonicalOrigin(value) {
|
|
89
|
+
return canonicalOrigin(value) === value;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Checks an origin against an explicit allow-list. An empty allow-list does
|
|
93
|
+
* not mean wildcard access; callers must choose an origin before pairing.
|
|
94
|
+
*/
|
|
95
|
+
export function isAllowedOrigin(value, allowedOrigins) {
|
|
96
|
+
const origin = canonicalOrigin(value);
|
|
97
|
+
if (!origin || allowedOrigins.length === 0)
|
|
98
|
+
return false;
|
|
99
|
+
return allowedOrigins.some((candidate) => canonicalOrigin(candidate) === origin);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Validates an agent route and ensures it belongs to the paired origin.
|
|
103
|
+
* Fragments are retained because they are useful initial Canvas locations.
|
|
104
|
+
*/
|
|
105
|
+
export function isSameOriginRoute(value, origin) {
|
|
106
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["url", "title", "label"]))
|
|
107
|
+
return false;
|
|
108
|
+
if (!nonEmptyString(value.url, AGENT_PROTOCOL_LIMITS.routeUrl))
|
|
109
|
+
return false;
|
|
110
|
+
if (!optionalString(value.title, AGENT_PROTOCOL_LIMITS.routeTitle))
|
|
111
|
+
return false;
|
|
112
|
+
if (!optionalString(value.label, AGENT_PROTOCOL_LIMITS.label))
|
|
113
|
+
return false;
|
|
114
|
+
const expectedOrigin = canonicalOrigin(origin);
|
|
115
|
+
if (!expectedOrigin)
|
|
116
|
+
return false;
|
|
117
|
+
let route;
|
|
118
|
+
try {
|
|
119
|
+
route = new URL(value.url, expectedOrigin);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (route.origin !== expectedOrigin || !ORIGIN_PROTOCOLS.has(route.protocol))
|
|
125
|
+
return false;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
/** Validates and normalises route values for a presentation command. */
|
|
129
|
+
export function validateRoutes(value, origin) {
|
|
130
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > AGENT_PROTOCOL_LIMITS.routeCount) {
|
|
131
|
+
throw new TypeError(`routes must contain between 1 and ${AGENT_PROTOCOL_LIMITS.routeCount} items`);
|
|
132
|
+
}
|
|
133
|
+
const expectedOrigin = canonicalOrigin(origin);
|
|
134
|
+
if (!expectedOrigin)
|
|
135
|
+
throw new TypeError("origin must be a canonical HTTP(S) origin");
|
|
136
|
+
return value.map((route, index) => {
|
|
137
|
+
if (!isSameOriginRoute(route, expectedOrigin)) {
|
|
138
|
+
throw new TypeError(`routes[${index}] must be a same-origin route`);
|
|
139
|
+
}
|
|
140
|
+
const input = route;
|
|
141
|
+
const parsed = new URL(input.url, expectedOrigin);
|
|
142
|
+
return {
|
|
143
|
+
url: parsed.href,
|
|
144
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
145
|
+
...(input.label === undefined ? {} : { label: input.label }),
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Validates the fields shared by every companion message. */
|
|
150
|
+
export function isProtocolVersion(value) {
|
|
151
|
+
return value === AGENT_PROTOCOL_VERSION;
|
|
152
|
+
}
|
|
153
|
+
export function isProjectIdentity(value) {
|
|
154
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["projectId", "origin", "workspaceRoot"]))
|
|
155
|
+
return false;
|
|
156
|
+
return nonEmptyString(value.projectId, AGENT_PROTOCOL_LIMITS.projectId)
|
|
157
|
+
&& (value.origin === undefined || isCanonicalOrigin(value.origin))
|
|
158
|
+
&& optionalString(value.workspaceRoot, AGENT_PROTOCOL_LIMITS.workspaceRoot);
|
|
159
|
+
}
|
|
160
|
+
export function isAgentPromptRequest(value) {
|
|
161
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["requestId", "projectId", "prompt", "changeRevision"]))
|
|
162
|
+
return false;
|
|
163
|
+
return nonEmptyString(value.requestId, AGENT_PROTOCOL_LIMITS.requestId)
|
|
164
|
+
&& nonEmptyString(value.projectId, AGENT_PROTOCOL_LIMITS.projectId)
|
|
165
|
+
&& nonEmptyString(value.prompt, AGENT_PROTOCOL_LIMITS.prompt)
|
|
166
|
+
&& optionalRevision(value.changeRevision);
|
|
167
|
+
}
|
|
168
|
+
export function isAgentStatusUpdate(value) {
|
|
169
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["requestId", "status", "summary", "error"]))
|
|
170
|
+
return false;
|
|
171
|
+
return nonEmptyString(value.requestId, AGENT_PROTOCOL_LIMITS.requestId)
|
|
172
|
+
&& (value.status === "working" || value.status === "completed"
|
|
173
|
+
|| value.status === "failed" || value.status === "interrupted")
|
|
174
|
+
&& optionalString(value.summary, AGENT_PROTOCOL_LIMITS.prompt)
|
|
175
|
+
&& optionalString(value.error, AGENT_PROTOCOL_LIMITS.prompt);
|
|
176
|
+
}
|
|
177
|
+
export function isCanvasState(value) {
|
|
178
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["mode", "groups", "focusedGroupId", "focusedRouteUrl"]))
|
|
179
|
+
return false;
|
|
180
|
+
if (value.mode !== "inspect" && value.mode !== "canvas")
|
|
181
|
+
return false;
|
|
182
|
+
if (!Array.isArray(value.groups) || value.groups.length > AGENT_PROTOCOL_LIMITS.canvasCardCount)
|
|
183
|
+
return false;
|
|
184
|
+
if (!(value.focusedGroupId === undefined || value.focusedGroupId === null
|
|
185
|
+
|| nonEmptyString(value.focusedGroupId, AGENT_PROTOCOL_LIMITS.groupId)))
|
|
186
|
+
return false;
|
|
187
|
+
if (!(value.focusedRouteUrl === undefined || value.focusedRouteUrl === null
|
|
188
|
+
|| nonEmptyString(value.focusedRouteUrl, AGENT_PROTOCOL_LIMITS.routeUrl)))
|
|
189
|
+
return false;
|
|
190
|
+
return value.groups.every(isCanvasGroup);
|
|
191
|
+
}
|
|
192
|
+
export function isCanvasGroup(value) {
|
|
193
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["id", "label", "owner", "routes"]))
|
|
194
|
+
return false;
|
|
195
|
+
return nonEmptyString(value.id, AGENT_PROTOCOL_LIMITS.groupId)
|
|
196
|
+
&& nonEmptyString(value.label, AGENT_PROTOCOL_LIMITS.label)
|
|
197
|
+
&& (value.owner === "agent" || value.owner === "user")
|
|
198
|
+
&& Array.isArray(value.routes)
|
|
199
|
+
&& value.routes.length <= AGENT_PROTOCOL_LIMITS.routeCount
|
|
200
|
+
&& value.routes.every((route) => isRecord(route)
|
|
201
|
+
&& nonEmptyString(route.url, AGENT_PROTOCOL_LIMITS.routeUrl)
|
|
202
|
+
&& optionalString(route.title, AGENT_PROTOCOL_LIMITS.routeTitle)
|
|
203
|
+
&& optionalString(route.label, AGENT_PROTOCOL_LIMITS.label));
|
|
204
|
+
}
|
|
205
|
+
export function isCanvasCommand(value) {
|
|
206
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["type", "commandId", "groupId", "label", "routes"]))
|
|
207
|
+
return false;
|
|
208
|
+
if (!nonEmptyString(value.commandId, AGENT_PROTOCOL_LIMITS.commandId))
|
|
209
|
+
return false;
|
|
210
|
+
switch (value.type) {
|
|
211
|
+
case "read-state":
|
|
212
|
+
case "fit-all":
|
|
213
|
+
return true;
|
|
214
|
+
case "focus-group":
|
|
215
|
+
case "remove-group":
|
|
216
|
+
return nonEmptyString(value.groupId, AGENT_PROTOCOL_LIMITS.groupId);
|
|
217
|
+
case "present-routes":
|
|
218
|
+
return nonEmptyString(value.groupId, AGENT_PROTOCOL_LIMITS.groupId)
|
|
219
|
+
&& nonEmptyString(value.label, AGENT_PROTOCOL_LIMITS.label)
|
|
220
|
+
&& Array.isArray(value.routes)
|
|
221
|
+
&& value.routes.length > 0
|
|
222
|
+
&& value.routes.length <= AGENT_PROTOCOL_LIMITS.routeCount
|
|
223
|
+
&& value.routes.every((route) => isRecord(route)
|
|
224
|
+
&& nonEmptyString(route.url, AGENT_PROTOCOL_LIMITS.routeUrl)
|
|
225
|
+
&& optionalString(route.title, AGENT_PROTOCOL_LIMITS.routeTitle)
|
|
226
|
+
&& optionalString(route.label, AGENT_PROTOCOL_LIMITS.label));
|
|
227
|
+
default:
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Creates a stable protocol error shape without exposing thrown values. */
|
|
232
|
+
export function protocolError(code, message) {
|
|
233
|
+
if (!nonEmptyString(code, 128) || !nonEmptyString(message, AGENT_PROTOCOL_LIMITS.prompt)) {
|
|
234
|
+
throw new TypeError("Protocol errors require bounded code and message strings");
|
|
235
|
+
}
|
|
236
|
+
return { code, message };
|
|
237
|
+
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Companion-facing aliases and transport envelopes.
|
|
3
3
|
*
|
|
4
|
-
* The domain contracts live in
|
|
5
|
-
* transport names discoverable for Node consumers while
|
|
6
|
-
* protocol vocabulary for browser and agent adapters.
|
|
4
|
+
* The domain contracts live in the private shared protocol module. This public
|
|
5
|
+
* subpath keeps transport names discoverable for Node consumers while
|
|
6
|
+
* preserving one protocol vocabulary for browser and agent adapters.
|
|
7
7
|
*/
|
|
8
|
-
export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, defaultBridgePort, isAgentPromptRequest, isAgentStatusUpdate, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateRoutes, } from "
|
|
9
|
-
export type { AgentConnectionState, AgentProjectIdentity, AgentPromptRequest, AgentRequestOutcome, AgentRequestStatus, AgentRoute, AgentStatusSnapshot, AgentStatusUpdate, BridgeCanvasCommandEvent, BridgeConnectedEvent, BridgeDisconnectedEvent, BridgeEnvelope, BridgeEvent, BridgeEventType, BridgeStatusEvent, CanvasCommand, CanvasCommandAcknowledgement, CanvasCommandError, CanvasCommandResult, CanvasGroup, CanvasGroupOwner, CanvasState, FitCanvasCommand, FocusCanvasGroupCommand, PairingRequest, PairingResponse, PresentRoutesCommand, PromptDispatchRequest, PromptDispatchResponse, ReadCanvasStateCommand, RemoveCanvasGroupCommand, StatusEventRequest, } from "
|
|
8
|
+
export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, defaultBridgePort, isAgentPromptRequest, isAgentStatusUpdate, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateRoutes, } from "./internal/agent-protocol.js";
|
|
9
|
+
export type { AgentConnectionState, AgentProjectIdentity, AgentPromptRequest, AgentRequestOutcome, AgentRequestStatus, AgentRoute, AgentStatusSnapshot, AgentStatusUpdate, BridgeCanvasCommandEvent, BridgeConnectedEvent, BridgeDisconnectedEvent, BridgeEnvelope, BridgeEvent, BridgeEventType, BridgeStatusEvent, CanvasCommand, CanvasCommandAcknowledgement, CanvasCommandError, CanvasCommandResult, CanvasGroup, CanvasGroupOwner, CanvasState, FitCanvasCommand, FocusCanvasGroupCommand, PairingRequest, PairingResponse, PresentRoutesCommand, PromptDispatchRequest, PromptDispatchResponse, ReadCanvasStateCommand, RemoveCanvasGroupCommand, StatusEventRequest, } from "./internal/agent-protocol.js";
|
|
10
10
|
/** Stable endpoint names used by the loopback browser bridge. */
|
|
11
11
|
export declare const BRIDGE_ENDPOINTS: {
|
|
12
12
|
readonly health: "/health";
|
package/dist/protocol.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Companion-facing aliases and transport envelopes.
|
|
3
3
|
*
|
|
4
|
-
* The domain contracts live in
|
|
5
|
-
* transport names discoverable for Node consumers while
|
|
6
|
-
* protocol vocabulary for browser and agent adapters.
|
|
4
|
+
* The domain contracts live in the private shared protocol module. This public
|
|
5
|
+
* subpath keeps transport names discoverable for Node consumers while
|
|
6
|
+
* preserving one protocol vocabulary for browser and agent adapters.
|
|
7
7
|
*/
|
|
8
|
-
export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, defaultBridgePort, isAgentPromptRequest, isAgentStatusUpdate, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateRoutes, } from "
|
|
8
|
+
export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, defaultBridgePort, isAgentPromptRequest, isAgentStatusUpdate, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateRoutes, } from "./internal/agent-protocol.js";
|
|
9
9
|
/** Stable endpoint names used by the loopback browser bridge. */
|
|
10
10
|
export const BRIDGE_ENDPOINTS = {
|
|
11
11
|
health: "/health",
|
package/dist/server.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
-
import type { AgentProjectIdentity } from "
|
|
3
|
+
import type { AgentProjectIdentity } from "./internal/agent-protocol.js";
|
|
4
4
|
import { type BrowserBridge, type BrowserBridgeOptions, type BridgeAddress } from "./bridge.js";
|
|
5
5
|
export declare const MCP_SERVER_NAME = "nudge-ui";
|
|
6
|
-
export declare const MCP_SERVER_VERSION
|
|
6
|
+
export declare const MCP_SERVER_VERSION: string;
|
|
7
7
|
/** Instructions are sent through MCP initialization for every host. */
|
|
8
8
|
export declare const MCP_SERVER_INSTRUCTIONS: string;
|
|
9
9
|
export interface AgentCompanionOptions extends BrowserBridgeOptions {
|
package/dist/server.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { validateRoutes } from "
|
|
5
|
+
import { validateRoutes } from "./internal/agent-protocol.js";
|
|
5
6
|
import { createLoopbackBridge, } from "./bridge.js";
|
|
6
7
|
export const MCP_SERVER_NAME = "nudge-ui";
|
|
7
|
-
export const MCP_SERVER_VERSION =
|
|
8
|
+
export const MCP_SERVER_VERSION = readPackageVersion();
|
|
9
|
+
function readPackageVersion() {
|
|
10
|
+
const metadata = createRequire(import.meta.url)("../package.json");
|
|
11
|
+
if (typeof metadata !== "object"
|
|
12
|
+
|| metadata === null
|
|
13
|
+
|| !("version" in metadata)
|
|
14
|
+
|| typeof metadata.version !== "string") {
|
|
15
|
+
throw new TypeError("@nudge-ui/mcp package metadata has no version.");
|
|
16
|
+
}
|
|
17
|
+
return metadata.version;
|
|
18
|
+
}
|
|
8
19
|
/** Instructions are sent through MCP initialization for every host. */
|
|
9
20
|
export const MCP_SERVER_INSTRUCTIONS = [
|
|
10
21
|
"Nudge UI is a local browser companion for one project workspace.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nudge-ui/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Optional local MCP companion for pairing Nudge UI with coding agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
|
-
"zod": "^3.25.76"
|
|
50
|
-
"@nudge-ui/agent-protocol": "0.1.2"
|
|
49
|
+
"zod": "^3.25.76"
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
53
52
|
"@types/node": "^22.20.1",
|
|
54
53
|
"esbuild": "^0.21.5",
|
|
55
54
|
"typescript": "^5.9.3",
|
|
56
|
-
"vitest": "^2.1.9"
|
|
55
|
+
"vitest": "^2.1.9",
|
|
56
|
+
"@nudge-ui/agent-protocol": "0.2.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=20"
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"access": "public"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
|
-
"build": "node ../../scripts/build-package.mjs && node scripts/build.mjs",
|
|
65
|
+
"build": "node ../../scripts/build-package.mjs && node ../../scripts/vendor-agent-protocol.mjs && node scripts/build.mjs",
|
|
66
66
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
67
67
|
"test:unit": "vitest run",
|
|
68
68
|
"test:unit:watch": "vitest"
|