underwritten-mcp 0.0.0 → 0.0.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Jerue
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,28 +1,23 @@
1
- # Underwritten MCP Bridge
1
+ # underwritten-mcp
2
2
 
3
- `underwritten-mcp` is the local MCP companion for `underwritten.app`.
3
+ `underwritten-mcp` is the published MCP server package for [Underwritten](https://underwritten.app). It runs as a local process, speaks MCP over `stdio`, exposes a localhost HTTP bridge for the browser app, and routes tool calls into the active Underwritten editor session.
4
4
 
5
- It is the MCP server. The browser app is not.
5
+ The package exists because Underwritten itself is a browser app. External MCP clients need a local companion that can:
6
6
 
7
- ## Why this package exists
7
+ - start from a normal MCP command
8
+ - bind a localhost API the browser can reach
9
+ - pair with a live Underwritten tab
10
+ - operate on the real editor and workspace state already open in the app
8
11
 
9
- `underwritten.app` is a PWA and the editor state lives in the browser. External MCP clients need a local process that can:
12
+ ## Install
10
13
 
11
- - speak MCP over `stdio`
12
- - bind a localhost API
13
- - coordinate with the live browser editor session
14
+ You can run the published package directly:
14
15
 
15
- This package provides that bridge while the PWA remains the source of truth for document and workspace state.
16
-
17
- ## Monorepo layout
18
-
19
- - `apps/mcp`: publishable MCP server package and localhost bridge
20
- - `apps/mcp/src/contract.ts`: shared browser-safe protocol types and markdown edit helpers
21
- - `apps/website`: PWA integration, bridge discovery, session registration, polling, and settings UI
22
-
23
- ## User setup
16
+ ```bash
17
+ npx -y underwritten-mcp
18
+ ```
24
19
 
25
- The intended end-user MCP config is:
20
+ The intended end-user MCP configuration is:
26
21
 
27
22
  ```json
28
23
  {
@@ -35,178 +30,151 @@ The intended end-user MCP config is:
35
30
  }
36
31
  ```
37
32
 
38
- After that:
33
+ After the process starts:
39
34
 
40
- 1. The MCP client starts `underwritten-mcp`.
41
- 2. The bridge binds a localhost API on `127.0.0.1`.
42
- 3. The PWA discovers the bridge automatically and pairs without a manual token-copy step.
43
- 4. The active editor tab registers itself and starts polling for pending actions.
35
+ 1. `underwritten-mcp` binds a free localhost port on `127.0.0.1`.
36
+ 2. The Underwritten web app discovers the bridge with `GET /discover`.
37
+ 3. The browser pairs with `POST /pair`.
38
+ 4. The active tab syncs session state and executes queued actions.
44
39
 
45
- ## Running locally
40
+ ## What It Does
46
41
 
47
- Install dependencies first:
42
+ - Runs an MCP server over `stdio`
43
+ - Exposes a localhost bridge for the browser app
44
+ - Routes file and document operations to the most relevant live Underwritten tab
45
+ - Shares browser-safe contract types through `underwritten-mcp/contract`
46
+ - Keeps the browser app as the source of truth for workspace and editor state
48
47
 
49
- ```bash
50
- vp install
51
- ```
48
+ This is the published bridge package. The website app is not the MCP server.
52
49
 
53
- Run the website in one terminal:
50
+ ## Tool Surface
54
51
 
55
- ```bash
56
- vp run website#dev
57
- ```
52
+ Workspace tools:
58
53
 
59
- Run the MCP bridge from source in another terminal:
54
+ - `get_workspace_status`
55
+ - `list_files`
56
+ - `read_file`
57
+ - `open_file`
58
+ - `create_file`
59
+ - `create_folder`
60
+ - `move_path`
61
+ - `delete_path`
62
+ - `save_document`
60
63
 
61
- ```bash
62
- vp dlx tsx apps/mcp/src/cli.ts
63
- ```
64
+ Document tools:
64
65
 
65
- If you want to run the built output instead:
66
+ - `get_current_document`
67
+ - `replace_current_document`
68
+ - `apply_markdown_edits`
66
69
 
67
- ```bash
68
- vp exec tsc -p apps/mcp/tsconfig.build.json
69
- node apps/mcp/dist/cli.js
70
- ```
70
+ `apply_markdown_edits` works on raw markdown text with literal anchored matching. Targets use a literal `text` value and optional 1-based `occurrence`. Edits apply sequentially against the updated buffer, and ambiguous or missing matches fail explicitly.
71
71
 
72
- If you want to test the user-facing bootstrap shape locally, the equivalent MCP client entry is still:
72
+ ## Bridge Behavior
73
73
 
74
- ```json
75
- {
76
- "mcpServers": {
77
- "underwritten": {
78
- "command": "npx",
79
- "args": ["-y", "underwritten-mcp"]
80
- }
81
- }
82
- }
83
- ```
74
+ The bridge only listens on `127.0.0.1` and uses a reserved port range of `45261-45271` by default. The browser app pairs first, receives a bearer token, and then uses that token for sync and status requests.
84
75
 
85
- ## Validation
76
+ Endpoints:
86
77
 
87
- Repo-level validation:
78
+ - `GET /discover`
79
+ - `POST /pair`
80
+ - `POST /session/sync`
81
+ - `POST /session/disconnect`
82
+ - `GET /status`
88
83
 
89
- ```bash
90
- vp check
91
- vp test
92
- vp run test:e2e
93
- ```
84
+ The browser polls every `1000ms`, and sessions expire after `15000ms` without a fresh heartbeat.
94
85
 
95
- MCP package build only:
86
+ ## Session Routing
96
87
 
97
- ```bash
98
- vp exec tsc -p apps/mcp/tsconfig.build.json
99
- ```
88
+ Tool calls target one live browser session at a time. Routing is deterministic:
100
89
 
101
- ## Discovery and pairing
90
+ 1. Keep only live, non-disconnected sessions.
91
+ 2. Prefer the most recent `lastFocusAt`.
92
+ 3. Break ties with the most recent `lastHeartbeatAt`.
93
+ 4. Break any remaining tie lexically by `sessionId`.
102
94
 
103
- By default the PWA scans a reserved localhost port range and probes `GET /discover`.
95
+ If no live session is available, the bridge returns an explicit error instead of guessing.
104
96
 
105
- - Reserved range: `45261-45271`
106
- - Bind address: `127.0.0.1`
107
- - Pairing: automatic `POST /pair` from an approved Underwritten origin
108
- - Session sync: `POST /session/sync`
109
- - Status: `GET /status`
97
+ ## Package Exports
110
98
 
111
- For test isolation and local debugging, the browser also honors a `localStorage` override at `underwritten.mcp.bridgePorts` with an explicit JSON array of ports to probe.
99
+ Default package exports:
112
100
 
113
- ## Browser session model
101
+ - `startUnderwrittenBridge`
102
+ - `resolveBridgePort`
103
+ - `UnderwrittenBridgeService`
104
+ - `UnderwrittenBridgeError`
114
105
 
115
- Each tab registers as a distinct session with:
106
+ Contract exports are available from `underwritten-mcp/contract`, including:
116
107
 
117
- - `sessionId`
118
- - `activeFilePath`
119
- - `title`
120
- - `markdown`
121
- - `dirty`
122
- - `storageMode`
123
- - `nativeFolderSelected`
124
- - `revision`
125
- - `lastHeartbeatAt`
126
- - `lastFocusAt`
127
- - `visibilityState`
128
- - `pageUrl`
129
- - `windowLabel`
108
+ - `underwrittenBridgePortRange`
109
+ - `underwrittenBridgeApiVersion`
110
+ - markdown edit types and helpers
111
+ - session, status, and action types shared with the browser app
130
112
 
131
- The browser polls the bridge, receives queued actions, applies them through the existing `EditorPage` editor/workspace code paths, and posts the result back in the next sync.
113
+ Example:
132
114
 
133
- ## Tool surface
115
+ ```ts
116
+ import { startUnderwrittenBridge } from "underwritten-mcp";
117
+ import { underwrittenBridgePortRange } from "underwritten-mcp/contract";
134
118
 
135
- Workspace tools:
136
-
137
- - `get_workspace_status`
138
- - `list_files`
139
- - `read_file`
140
- - `open_file`
141
- - `create_file`
142
- - `create_folder`
143
- - `move_path`
144
- - `delete_path`
145
- - `save_document`
119
+ const bridge = await startUnderwrittenBridge({
120
+ connectStdio: true,
121
+ portRange: underwrittenBridgePortRange,
122
+ });
123
+ ```
146
124
 
147
- Markdown tools:
125
+ ## Local Development In This Monorepo
148
126
 
149
- - `get_current_document`
150
- - `replace_current_document`
151
- - `apply_markdown_edits`
127
+ Install dependencies:
152
128
 
153
- ### Markdown edit semantics
129
+ ```bash
130
+ vp install
131
+ ```
154
132
 
155
- `apply_markdown_edits` operates on raw markdown text, not editor-internal node ids.
133
+ Run the website:
156
134
 
157
- - Target matching is literal text matching.
158
- - `occurrence` is 1-based when provided.
159
- - If `occurrence` is omitted, the target text must match exactly once.
160
- - Ambiguous or missing targets fail with a clear error.
161
- - Edits apply sequentially to the updated markdown buffer.
135
+ ```bash
136
+ vp run website#dev
137
+ ```
162
138
 
163
- ## Deterministic routing
139
+ Run the bridge from source in another terminal:
164
140
 
165
- Current-document and workspace tools route to one live session:
141
+ ```bash
142
+ vp dlx tsx apps/mcp/src/cli.ts
143
+ ```
166
144
 
167
- 1. Keep only live sessions. A live session has a fresh heartbeat within `15s` and has not disconnected.
168
- 2. Sort by most recent `lastFocusAt`.
169
- 3. Break ties with most recent `lastHeartbeatAt`.
170
- 4. Break remaining ties lexically by `sessionId`.
171
- 5. If no live session remains, return an explicit error instead of guessing.
145
+ Build the package:
172
146
 
173
- This keeps tool routing deterministic across multiple open tabs while leaving room for future explicit `sessionId` targeting.
147
+ ```bash
148
+ vp run mcp#build
149
+ ```
174
150
 
175
- ## Security model
151
+ If you want to run the built entrypoint directly:
176
152
 
177
- - The HTTP bridge binds only to `127.0.0.1`.
178
- - Requests are limited to Underwritten origins plus local dev origins (`localhost` and `127.0.0.1`).
179
- - The browser must pair first and then present a bridge-issued bearer token.
180
- - The bridge does not expose shell execution.
181
- - Workspace operations are restricted to Underwritten’s real browser workspace model.
182
- - Native folder access still depends on the browser-granted File System Access handle already chosen in the app.
153
+ ```bash
154
+ node apps/mcp/dist/cli.js
155
+ ```
183
156
 
184
- ### Threat model limits
157
+ ## Validation
185
158
 
186
- - Pairing is automatic for approved origins, so trust is anchored in the origin allowlist and localhost-only binding.
187
- - A malicious script running on an approved Underwritten origin would inherit the same bridge access as the app.
188
- - Multiple simultaneous MCP bridge processes are supported, but the browser only probes the configured port list/range and pairs with bridges it can discover.
159
+ Run repo checks:
189
160
 
190
- ## Settings UI
161
+ ```bash
162
+ vp check
163
+ vp test
164
+ ```
191
165
 
192
- The existing settings dialog now includes an `MCP Bridge` section with:
166
+ Run the package build:
193
167
 
194
- - reachability / connection status
195
- - current session id
196
- - detected localhost port
197
- - known session count
198
- - copyable MCP config snippet
199
- - reconnect action
168
+ ```bash
169
+ vp run mcp#build
170
+ ```
200
171
 
201
- ## Current limitations
172
+ ## Monorepo Context
202
173
 
203
- - The public tool surface does not yet support explicit `sessionId` targeting.
204
- - `delete_path.force` only bypasses unsaved-current-document protection; it is not a separate recursive-delete flag because the browser file APIs already define directory deletion behavior.
205
- - The settings panel reports bridge/session status from the local browser’s perspective; it is not a multi-client admin console.
174
+ - `apps/mcp`: publishable MCP bridge package
175
+ - `apps/mcp/src/contract.ts`: shared protocol types and markdown edit helpers
176
+ - `apps/website`: Underwritten web app that discovers, pairs with, and drives the bridge
206
177
 
207
- ## Future improvements
178
+ ## License
208
179
 
209
- - explicit `sessionId` tool targeting
210
- - richer bridge diagnostics and per-session inspection
211
- - optional Streamable HTTP exposure in addition to `stdio`
212
- - stronger persisted pairing state if the product needs cross-restart trust continuity
180
+ MIT
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "underwritten-mcp",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "private": false,
5
+ "license": "MIT",
6
+ "repository": {
7
+ "url": "https://github.com/rjerue/underwritten.app"
8
+ },
5
9
  "bin": {
6
10
  "underwritten-mcp": "./dist/cli.js"
7
11
  },
8
12
  "files": [
9
- "dist"
13
+ "dist",
14
+ "LICENSE"
10
15
  ],
11
16
  "type": "module",
12
17
  "exports": {
@@ -31,6 +36,6 @@
31
36
  "typescript": "^5"
32
37
  },
33
38
  "scripts": {
34
- "build": "tsc -p tsconfig.build.json"
39
+ "build": "node -e \"require('node:fs').rmSync('dist', { force: true, recursive: true })\" && tsc -p tsconfig.build.json"
35
40
  }
36
41
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=contract.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"contract.test.d.ts","sourceRoot":"","sources":["../src/contract.test.ts"],"names":[],"mappings":""}
@@ -1,35 +0,0 @@
1
- import { describe, expect, test } from "vite-plus/test";
2
- import { applyMarkdownTextEdits, buildMarkdownOutline } from "./contract.js";
3
- describe("contract helpers", () => {
4
- test("applies sequential markdown edits", () => {
5
- const markdown = "# Title\n\nAlpha\nBeta\n";
6
- expect(applyMarkdownTextEdits(markdown, [
7
- {
8
- newText: "Intro\n",
9
- target: { text: "Alpha" },
10
- type: "insert_before",
11
- },
12
- {
13
- newText: "Gamma",
14
- target: { text: "Beta" },
15
- type: "replace",
16
- },
17
- ])).toBe("# Title\n\nIntro\nAlpha\nGamma\n");
18
- });
19
- test("requires occurrence when the target text is ambiguous", () => {
20
- expect(() => applyMarkdownTextEdits("repeat\nrepeat\n", [
21
- {
22
- target: { text: "repeat" },
23
- type: "delete",
24
- },
25
- ])).toThrow("ambiguous");
26
- });
27
- test("builds a markdown outline with heading depth and line numbers", () => {
28
- expect(buildMarkdownOutline("# Title\n\n## First\nBody\n### Nested\n")).toEqual([
29
- { depth: 1, line: 1, text: "Title" },
30
- { depth: 2, line: 3, text: "First" },
31
- { depth: 3, line: 5, text: "Nested" },
32
- ]);
33
- });
34
- });
35
- //# sourceMappingURL=contract.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"contract.test.js","sourceRoot":"","sources":["../src/contract.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE7E,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,IAAI,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC7C,MAAM,QAAQ,GAAG,0BAA0B,CAAC;QAE5C,MAAM,CACJ,sBAAsB,CAAC,QAAQ,EAAE;YAC/B;gBACE,OAAO,EAAE,SAAS;gBAClB,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;gBACzB,IAAI,EAAE,eAAe;aACtB;YACD;gBACE,OAAO,EAAE,OAAO;gBAChB,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;gBACxB,IAAI,EAAE,SAAS;aAChB;SACF,CAAC,CACH,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uDAAuD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,GAAG,EAAE,CACV,sBAAsB,CAAC,kBAAkB,EAAE;YACzC;gBACE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,IAAI,EAAE,QAAQ;aACf;SACF,CAAC,CACH,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACzE,MAAM,CAAC,oBAAoB,CAAC,yCAAyC,CAAC,CAAC,CAAC,OAAO,CAAC;YAC9E,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE;YACpC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE;YACpC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE;SACtC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=service.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../src/service.test.ts"],"names":[],"mappings":""}
@@ -1,239 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test";
2
- import { underwrittenBridgePortRange } from "./contract.js";
3
- import { startUnderwrittenBridge } from "./index.js";
4
- const allowedOrigin = "http://127.0.0.1:4173";
5
- function createSessionState(sessionId, overrides) {
6
- return {
7
- activeFilePath: "notes/test.md",
8
- appCapabilities: {
9
- supportsDirectoryAccess: true,
10
- },
11
- dirty: false,
12
- lastFocusAt: Date.now(),
13
- lastHeartbeatAt: Date.now(),
14
- markdown: "# Title\n",
15
- nativeFolderSelected: false,
16
- pageUrl: `${allowedOrigin}/`,
17
- revision: "rev-1",
18
- sessionId,
19
- storageMode: "origin-private",
20
- title: "Title",
21
- visibilityState: "visible",
22
- windowLabel: null,
23
- ...overrides,
24
- };
25
- }
26
- async function pairBridge(port, sessionId) {
27
- const response = await fetch(`http://127.0.0.1:${port}/pair`, {
28
- body: JSON.stringify({
29
- pageUrl: `${allowedOrigin}/`,
30
- sessionId,
31
- }),
32
- headers: {
33
- "Content-Type": "application/json",
34
- Origin: allowedOrigin,
35
- },
36
- method: "POST",
37
- });
38
- const payload = (await response.json());
39
- return payload.browserToken;
40
- }
41
- async function syncBridge(port, token, session, completedActions) {
42
- const response = await fetch(`http://127.0.0.1:${port}/session/sync`, {
43
- body: JSON.stringify({
44
- completedActions,
45
- session,
46
- }),
47
- headers: {
48
- Authorization: `Bearer ${token}`,
49
- "Content-Type": "application/json",
50
- Origin: allowedOrigin,
51
- },
52
- method: "POST",
53
- });
54
- return response;
55
- }
56
- describe("underwritten bridge service", () => {
57
- let startedBridge;
58
- beforeEach(async () => {
59
- startedBridge = await startUnderwrittenBridge({
60
- connectStdio: false,
61
- portRange: underwrittenBridgePortRange,
62
- });
63
- });
64
- afterEach(async () => {
65
- await startedBridge.close();
66
- });
67
- test("starts the localhost bridge and exposes discovery metadata", async () => {
68
- const response = await fetch(`http://127.0.0.1:${startedBridge.port}/discover`, {
69
- headers: {
70
- Origin: allowedOrigin,
71
- },
72
- });
73
- expect(response.ok).toBe(true);
74
- await expect(response.json()).resolves.toMatchObject({
75
- appName: "underwritten",
76
- bridgeId: startedBridge.service.bridgeId,
77
- port: startedBridge.port,
78
- });
79
- });
80
- test("rejects sync requests that are missing a valid pairing token", async () => {
81
- const response = await syncBridge(startedBridge.port, "bad-token", createSessionState("unauthorized-session"));
82
- expect(response.status).toBe(401);
83
- await expect(response.json()).resolves.toMatchObject({
84
- error: {
85
- code: "PAIRING_REQUIRED",
86
- },
87
- });
88
- });
89
- test("registers a session, routes tools to the most recently focused live session, and reports status", async () => {
90
- const leftToken = await pairBridge(startedBridge.port, "left-session");
91
- const rightToken = await pairBridge(startedBridge.port, "right-session");
92
- const now = Date.now();
93
- await syncBridge(startedBridge.port, leftToken, createSessionState("left-session", {
94
- lastFocusAt: now - 10,
95
- lastHeartbeatAt: now - 10,
96
- title: "Left",
97
- }));
98
- await syncBridge(startedBridge.port, rightToken, createSessionState("right-session", {
99
- lastFocusAt: now,
100
- lastHeartbeatAt: now,
101
- title: "Right",
102
- }));
103
- const toolPromise = startedBridge.service.callTool("get_current_document", {});
104
- const leftPoll = await syncBridge(startedBridge.port, leftToken, createSessionState("left-session", {
105
- lastFocusAt: now - 10,
106
- lastHeartbeatAt: Date.now(),
107
- title: "Left",
108
- }));
109
- const leftPayload = (await leftPoll.json());
110
- expect(leftPayload.pendingActions).toHaveLength(0);
111
- const rightPoll = await syncBridge(startedBridge.port, rightToken, createSessionState("right-session", {
112
- lastFocusAt: now,
113
- lastHeartbeatAt: Date.now(),
114
- markdown: "# Right\n",
115
- title: "Right",
116
- }));
117
- const rightPayload = (await rightPoll.json());
118
- expect(rightPayload.pendingActions).toHaveLength(1);
119
- expect(rightPayload.pendingActions[0]).toMatchObject({
120
- type: "get_current_document",
121
- });
122
- await syncBridge(startedBridge.port, rightToken, createSessionState("right-session", {
123
- lastFocusAt: now,
124
- lastHeartbeatAt: Date.now(),
125
- markdown: "# Right\n",
126
- title: "Right",
127
- }), [
128
- {
129
- actionId: rightPayload.pendingActions[0].id,
130
- ok: true,
131
- result: {
132
- markdown: "# Right\n",
133
- title: "Right",
134
- },
135
- },
136
- ]);
137
- await expect(toolPromise).resolves.toEqual({
138
- markdown: "# Right\n",
139
- title: "Right",
140
- });
141
- const statusResponse = await fetch(`http://127.0.0.1:${startedBridge.port}/status`, {
142
- headers: {
143
- Authorization: `Bearer ${rightToken}`,
144
- Origin: allowedOrigin,
145
- },
146
- });
147
- expect(statusResponse.ok).toBe(true);
148
- await expect(statusResponse.json()).resolves.toMatchObject({
149
- activeSessionId: "right-session",
150
- resolvedBy: "lastFocusAt,lastHeartbeatAt,sessionId",
151
- });
152
- });
153
- test("fails safely when no live session can be resolved", async () => {
154
- const token = await pairBridge(startedBridge.port, "stale-session");
155
- await syncBridge(startedBridge.port, token, createSessionState("stale-session", {
156
- lastFocusAt: Date.now() - 60_000,
157
- lastHeartbeatAt: Date.now() - 60_000,
158
- }));
159
- await expect(startedBridge.service.callTool("get_workspace_status", {})).rejects.toThrow(/No live underwritten\.app session/i);
160
- });
161
- test("dispatches each required tool action shape", async () => {
162
- const token = await pairBridge(startedBridge.port, "tool-session");
163
- const session = createSessionState("tool-session");
164
- await syncBridge(startedBridge.port, token, session);
165
- const cases = [
166
- { args: {}, name: "get_workspace_status", type: "get_workspace_status" },
167
- {
168
- args: { includeDirectories: true, path: "notes", recursive: true },
169
- name: "list_files",
170
- type: "list_files",
171
- },
172
- { args: { path: "notes/test.md" }, name: "read_file", type: "read_file" },
173
- {
174
- args: { discardUnsavedChanges: true, path: "notes/test.md" },
175
- name: "open_file",
176
- type: "open_file",
177
- },
178
- {
179
- args: { content: "# New", openAfterCreate: true, path: "notes/new-file" },
180
- name: "create_file",
181
- type: "create_file",
182
- },
183
- { args: { path: "notes/folder" }, name: "create_folder", type: "create_folder" },
184
- {
185
- args: { destinationPath: "notes/renamed.md", sourcePath: "notes/test.md" },
186
- name: "move_path",
187
- type: "move_path",
188
- },
189
- { args: { force: true, path: "notes/test.md" }, name: "delete_path", type: "delete_path" },
190
- { args: { path: "notes/test.md" }, name: "save_document", type: "save_document" },
191
- {
192
- args: { includeOutline: true },
193
- name: "get_current_document",
194
- type: "get_current_document",
195
- },
196
- {
197
- args: { markdown: "# Replaced" },
198
- name: "replace_current_document",
199
- type: "replace_current_document",
200
- },
201
- {
202
- args: {
203
- edits: [
204
- {
205
- newText: "Updated",
206
- target: { text: "Title" },
207
- type: "replace",
208
- },
209
- ],
210
- },
211
- name: "apply_markdown_edits",
212
- type: "apply_markdown_edits",
213
- },
214
- ];
215
- for (const testCase of cases) {
216
- const toolPromise = startedBridge.service.callTool(testCase.name, testCase.args);
217
- const pollResponse = await syncBridge(startedBridge.port, token, {
218
- ...session,
219
- lastHeartbeatAt: Date.now(),
220
- });
221
- const pollPayload = (await pollResponse.json());
222
- expect(pollPayload.pendingActions[0]).toMatchObject({
223
- type: testCase.type,
224
- });
225
- await syncBridge(startedBridge.port, token, {
226
- ...session,
227
- lastHeartbeatAt: Date.now(),
228
- }, [
229
- {
230
- actionId: pollPayload.pendingActions[0].id,
231
- ok: true,
232
- result: { ok: true, type: testCase.type },
233
- },
234
- ]);
235
- await expect(toolPromise).resolves.toEqual({ ok: true, type: testCase.type });
236
- }
237
- });
238
- });
239
- //# sourceMappingURL=service.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"service.test.js","sourceRoot":"","sources":["../src/service.test.ts"],"names":[],"mappings":"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"}