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 +21 -0
- package/README.md +114 -146
- package/package.json +8 -3
- package/dist/contract.test.d.ts +0 -2
- package/dist/contract.test.d.ts.map +0 -1
- package/dist/contract.test.js +0 -35
- package/dist/contract.test.js.map +0 -1
- package/dist/service.test.d.ts +0 -2
- package/dist/service.test.d.ts.map +0 -1
- package/dist/service.test.js +0 -239
- package/dist/service.test.js.map +0 -1
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
|
-
#
|
|
1
|
+
# underwritten-mcp
|
|
2
2
|
|
|
3
|
-
`underwritten-mcp` is the
|
|
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
|
-
|
|
5
|
+
The package exists because Underwritten itself is a browser app. External MCP clients need a local companion that can:
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
12
|
+
## Install
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
- bind a localhost API
|
|
13
|
-
- coordinate with the live browser editor session
|
|
14
|
+
You can run the published package directly:
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
|
33
|
+
After the process starts:
|
|
39
34
|
|
|
40
|
-
1.
|
|
41
|
-
2. The
|
|
42
|
-
3. The
|
|
43
|
-
4. The active
|
|
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
|
-
##
|
|
40
|
+
## What It Does
|
|
46
41
|
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
vp install
|
|
51
|
-
```
|
|
48
|
+
This is the published bridge package. The website app is not the MCP server.
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
## Tool Surface
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
vp run website#dev
|
|
57
|
-
```
|
|
52
|
+
Workspace tools:
|
|
58
53
|
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
vp dlx tsx apps/mcp/src/cli.ts
|
|
63
|
-
```
|
|
64
|
+
Document tools:
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
- `get_current_document`
|
|
67
|
+
- `replace_current_document`
|
|
68
|
+
- `apply_markdown_edits`
|
|
66
69
|
|
|
67
|
-
|
|
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
|
-
|
|
72
|
+
## Bridge Behavior
|
|
73
73
|
|
|
74
|
-
|
|
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
|
-
|
|
76
|
+
Endpoints:
|
|
86
77
|
|
|
87
|
-
|
|
78
|
+
- `GET /discover`
|
|
79
|
+
- `POST /pair`
|
|
80
|
+
- `POST /session/sync`
|
|
81
|
+
- `POST /session/disconnect`
|
|
82
|
+
- `GET /status`
|
|
88
83
|
|
|
89
|
-
|
|
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
|
-
|
|
86
|
+
## Session Routing
|
|
96
87
|
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
If no live session is available, the bridge returns an explicit error instead of guessing.
|
|
104
96
|
|
|
105
|
-
|
|
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
|
-
|
|
99
|
+
Default package exports:
|
|
112
100
|
|
|
113
|
-
|
|
101
|
+
- `startUnderwrittenBridge`
|
|
102
|
+
- `resolveBridgePort`
|
|
103
|
+
- `UnderwrittenBridgeService`
|
|
104
|
+
- `UnderwrittenBridgeError`
|
|
114
105
|
|
|
115
|
-
|
|
106
|
+
Contract exports are available from `underwritten-mcp/contract`, including:
|
|
116
107
|
|
|
117
|
-
- `
|
|
118
|
-
- `
|
|
119
|
-
-
|
|
120
|
-
-
|
|
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
|
-
|
|
113
|
+
Example:
|
|
132
114
|
|
|
133
|
-
|
|
115
|
+
```ts
|
|
116
|
+
import { startUnderwrittenBridge } from "underwritten-mcp";
|
|
117
|
+
import { underwrittenBridgePortRange } from "underwritten-mcp/contract";
|
|
134
118
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
125
|
+
## Local Development In This Monorepo
|
|
148
126
|
|
|
149
|
-
|
|
150
|
-
- `replace_current_document`
|
|
151
|
-
- `apply_markdown_edits`
|
|
127
|
+
Install dependencies:
|
|
152
128
|
|
|
153
|
-
|
|
129
|
+
```bash
|
|
130
|
+
vp install
|
|
131
|
+
```
|
|
154
132
|
|
|
155
|
-
|
|
133
|
+
Run the website:
|
|
156
134
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
139
|
+
Run the bridge from source in another terminal:
|
|
164
140
|
|
|
165
|
-
|
|
141
|
+
```bash
|
|
142
|
+
vp dlx tsx apps/mcp/src/cli.ts
|
|
143
|
+
```
|
|
166
144
|
|
|
167
|
-
|
|
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
|
-
|
|
147
|
+
```bash
|
|
148
|
+
vp run mcp#build
|
|
149
|
+
```
|
|
174
150
|
|
|
175
|
-
|
|
151
|
+
If you want to run the built entrypoint directly:
|
|
176
152
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
157
|
+
## Validation
|
|
185
158
|
|
|
186
|
-
|
|
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
|
-
|
|
161
|
+
```bash
|
|
162
|
+
vp check
|
|
163
|
+
vp test
|
|
164
|
+
```
|
|
191
165
|
|
|
192
|
-
|
|
166
|
+
Run the package build:
|
|
193
167
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
- known session count
|
|
198
|
-
- copyable MCP config snippet
|
|
199
|
-
- reconnect action
|
|
168
|
+
```bash
|
|
169
|
+
vp run mcp#build
|
|
170
|
+
```
|
|
200
171
|
|
|
201
|
-
##
|
|
172
|
+
## Monorepo Context
|
|
202
173
|
|
|
203
|
-
-
|
|
204
|
-
- `
|
|
205
|
-
-
|
|
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
|
-
##
|
|
178
|
+
## License
|
|
208
179
|
|
|
209
|
-
|
|
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.
|
|
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
|
}
|
package/dist/contract.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"contract.test.d.ts","sourceRoot":"","sources":["../src/contract.test.ts"],"names":[],"mappings":""}
|
package/dist/contract.test.js
DELETED
|
@@ -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"}
|
package/dist/service.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../src/service.test.ts"],"names":[],"mappings":""}
|
package/dist/service.test.js
DELETED
|
@@ -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
|
package/dist/service.test.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"service.test.js","sourceRoot":"","sources":["../src/service.test.ts"],"names":[],"mappings":"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"}
|