duet-mcp 0.6.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/LICENSE +21 -0
- package/README.md +367 -0
- package/doc/README.jp.md +347 -0
- package/lib/blob.d.ts +16 -0
- package/lib/blob.js +57 -0
- package/lib/boot.d.ts +2 -0
- package/lib/boot.js +134 -0
- package/lib/client-store.d.ts +28 -0
- package/lib/client-store.js +147 -0
- package/lib/client.d.ts +25 -0
- package/lib/client.js +58 -0
- package/lib/diff.d.ts +27 -0
- package/lib/diff.js +103 -0
- package/lib/doc.d.ts +30 -0
- package/lib/doc.js +221 -0
- package/lib/edit.d.ts +25 -0
- package/lib/edit.js +63 -0
- package/lib/http.d.ts +9 -0
- package/lib/http.js +151 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +2 -0
- package/lib/mcp.d.ts +5 -0
- package/lib/mcp.js +109 -0
- package/lib/op.d.ts +10 -0
- package/lib/op.js +19 -0
- package/lib/paths.d.ts +4 -0
- package/lib/paths.js +9 -0
- package/lib/protocol.d.ts +36 -0
- package/lib/protocol.js +10 -0
- package/lib/server.d.ts +1 -0
- package/lib/server.js +1 -0
- package/lib/shot.d.ts +8 -0
- package/lib/shot.js +85 -0
- package/lib/transport.d.ts +7 -0
- package/lib/transport.js +22 -0
- package/lib/types.d.ts +66 -0
- package/lib/types.js +1 -0
- package/lib/wire.d.ts +14 -0
- package/lib/wire.js +42 -0
- package/package.json +97 -0
- package/template/app.ts +17 -0
- package/template/doc.ts +18 -0
- package/template/main.ts +8 -0
- package/template/ops.ts +42 -0
- package/template/start.ts +4 -0
- package/template/ui/canvas.tsx +91 -0
- package/template/ui/card-editing.tsx +48 -0
- package/template/ui/edit-actions.tsx +30 -0
- package/template/ui/index.html +15 -0
- package/template/ui/main.tsx +53 -0
- package/template/ui/style.css +15 -0
- package/template/ui/tsconfig.json +15 -0
- package/template/ui/vite.config.ts +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Taniguchi Ryoga (SabaCan0141)
|
|
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
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# duet-mcp
|
|
2
|
+
|
|
3
|
+
English | [日本語](doc/README.jp.md)
|
|
4
|
+
|
|
5
|
+
**Build apps where humans edit through a GUI and LLMs edit the same JSON document through MCP.**
|
|
6
|
+
|
|
7
|
+
Define each operation once and expose it through both HTTP and MCP. One daemon owns the document,
|
|
8
|
+
and callers can wait for changes since their last observation. Designed for discrete operations on
|
|
9
|
+
small to medium documents: game boards, task boards, diagrams, and slide outlines.
|
|
10
|
+
|
|
11
|
+
## Define an operation once
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { opFactory } from "duet-mcp";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
type Doc = { text: string };
|
|
18
|
+
const op = opFactory<Doc>();
|
|
19
|
+
|
|
20
|
+
op({
|
|
21
|
+
name: "set_text",
|
|
22
|
+
description: "Replace the text.",
|
|
23
|
+
input: { text: z.string() },
|
|
24
|
+
handler: ({ doc, reject }, { text }) => {
|
|
25
|
+
if (text.length > 100) return reject("Use no more than 100 characters.");
|
|
26
|
+
doc.text = text;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This defines the MCP tool `set_text({ text, baseRevision })` and the HTTP endpoint
|
|
32
|
+
`POST /api/op/set_text`. The GUI calls `snap.run("set_text", { text })` from a subscribed snapshot.
|
|
33
|
+
|
|
34
|
+
**An operation applies to the document revision the caller observed when forming its intent.**
|
|
35
|
+
If the document has changed, duet returns `conflict` and the latest document without running the
|
|
36
|
+
handler. Even unrelated changes conflict. Read the current document and reconsider the operation;
|
|
37
|
+
do not automatically resend the same arguments with a newer revision.
|
|
38
|
+
|
|
39
|
+
## Use the npm package
|
|
40
|
+
|
|
41
|
+
Requires Node.js 22 or later and ESM. The React adapter targets React 18.3; input schemas use Zod 3.
|
|
42
|
+
CI covers Node.js 22 / 24 on Ubuntu and macOS. Windows startup and build instructions are untested.
|
|
43
|
+
|
|
44
|
+
The package name is `duet-mcp`. Registry installation becomes available after the first release.
|
|
45
|
+
Before publication, use an absolute path to a tarball produced by `npm pack` in place of
|
|
46
|
+
`duet-mcp` in the installation command.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
mkdir my-duet-app
|
|
50
|
+
cd my-duet-app
|
|
51
|
+
npm init -y
|
|
52
|
+
npm pkg set type=module
|
|
53
|
+
npm install duet-mcp react@^18.3.1 react-dom@^18.3.1 zod@^3.23.8
|
|
54
|
+
npm install -D typescript@^5.7.2 vite@^6.0.5 @vitejs/plugin-react@^4.3.4 tailwindcss@^4.3.3 @tailwindcss/vite@^4.3.3 @types/node@^22.10.2 @types/react@^18.3.17 @types/react-dom@^18.3.5
|
|
55
|
+
npx playwright install chromium
|
|
56
|
+
cp -R node_modules/duet-mcp/template ./template
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
On Linux, use `npx playwright install --with-deps chromium` if system libraries are also needed.
|
|
60
|
+
Browser downloads are explicit, not an installation hook. Run the command again after updating Playwright.
|
|
61
|
+
|
|
62
|
+
Create `tsconfig.json` at the project root:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"compilerOptions": {
|
|
67
|
+
"target": "ES2022",
|
|
68
|
+
"module": "NodeNext",
|
|
69
|
+
"moduleResolution": "NodeNext",
|
|
70
|
+
"rootDir": ".",
|
|
71
|
+
"outDir": "dist",
|
|
72
|
+
"strict": true,
|
|
73
|
+
"skipLibCheck": true
|
|
74
|
+
},
|
|
75
|
+
"include": ["template/*.ts"]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx tsc
|
|
81
|
+
npx vite build --config template/ui/vite.config.ts
|
|
82
|
+
node dist/template/main.js
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For GUI development, run `npx vite --config template/ui/vite.config.ts` in another terminal.
|
|
86
|
+
Register `node` and the absolute path to `<project>/dist/template/main.js` with your MCP client.
|
|
87
|
+
When renaming the app, update its directory name, `app.id`, `webDist`, build inputs, and startup path together.
|
|
88
|
+
|
|
89
|
+
The package exposes four entry points. Direct imports from `lib/` are not public API.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { defineApp, opFactory, type AppDef, type Op } from "duet-mcp";
|
|
93
|
+
import { runApp } from "duet-mcp/server";
|
|
94
|
+
import { useDoc, useEdit, EditSession, refreshDoc } from "duet-mcp/react";
|
|
95
|
+
import { portFor, baseUrlFor } from "duet-mcp/wire";
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`rootDir` is an absolute path to the application root. Documents and blobs are stored under its
|
|
99
|
+
`data/` directory. `webDist` is either relative to `rootDir` or an absolute path.
|
|
100
|
+
Without `rootDir`, duet uses the process working directory. MCP clients may launch from any directory,
|
|
101
|
+
so explicitly derive the root from `import.meta.url`, as the template does.
|
|
102
|
+
Keeping the same `rootDir` and `app.id` preserves the storage location across package updates.
|
|
103
|
+
When migrating from a copied repository, point `rootDir` at the previous project root.
|
|
104
|
+
|
|
105
|
+
Update the library with `npm install duet-mcp@<version>`. Your app owns the copied template files.
|
|
106
|
+
|
|
107
|
+
## Run from the repository
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm ci
|
|
111
|
+
npx playwright install chromium
|
|
112
|
+
npm run build
|
|
113
|
+
npm start
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The GUI URL is printed to stderr. `template/` is a starter app built with React and Tailwind CSS.
|
|
117
|
+
It includes text fields, notes, checkboxes, radio buttons, a select, an opacity slider, and a real
|
|
118
|
+
canvas with a draggable box and four corner resize handles.
|
|
119
|
+
|
|
120
|
+
The form commits its settings together with **Apply**. Canvas gestures commit on pointer release;
|
|
121
|
+
position and size are also editable through number inputs. Escape or pointer cancellation aborts
|
|
122
|
+
an active drag. The canvas uses a 720×480 coordinate system with a minimum box size of 64×64.
|
|
123
|
+
The operation handler also validates its bounds.
|
|
124
|
+
|
|
125
|
+
MCP tools `set_settings`, `set_box`, and `set_text` edit the same document. Conflicts preserve the
|
|
126
|
+
draft and show the current values for explicit review. Drawing and gestures live in `ui/canvas.tsx`;
|
|
127
|
+
commit controls and conflict review live in `ui/edit-actions.tsx`. Edit `ui/style.css` and component
|
|
128
|
+
Tailwind classes to customize the appearance. Text-only snapshots from the earlier template display
|
|
129
|
+
default values for the additional fields.
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"mcpServers": {
|
|
134
|
+
"duet": {
|
|
135
|
+
"command": "node",
|
|
136
|
+
"args": ["<repo>/dist/template/main.js"]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Replace `<repo>` with an absolute path and adapt the configuration format to your MCP client.
|
|
143
|
+
After connecting, call `gui_url` for the GUI URL and `await_change` for the current document.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
DUET_APP=myapp npm start
|
|
147
|
+
DUET_APP=myapp npm run dev:web
|
|
148
|
+
# Capture the development GUI:
|
|
149
|
+
DUET_SHOT_ORIGIN=http://127.0.0.1:5173 DUET_APP=myapp npm start
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Documents, operations, and observations
|
|
153
|
+
|
|
154
|
+
| Name | Contract |
|
|
155
|
+
|---|---|
|
|
156
|
+
| doc | Application-defined JSON, changed through operations |
|
|
157
|
+
| op | A name, description, Zod input schema, and synchronous handler defined once |
|
|
158
|
+
| ctx | `{ doc, actor, reject }`; doc is a writable copy |
|
|
159
|
+
| revision | An opaque string; pass it back without parsing or arithmetic |
|
|
160
|
+
| snapshot | An observation containing `{ doc, revision, actor, activity }` together |
|
|
161
|
+
| await_change | Read the current state or wait for commits since an observed revision |
|
|
162
|
+
|
|
163
|
+
Calling `reject` discards the working copy. An operation that leaves doc unchanged does not advance
|
|
164
|
+
the revision. **Query operations with stale revisions also conflict.** The handler is not executed
|
|
165
|
+
first to determine whether it is read-only.
|
|
166
|
+
|
|
167
|
+
Handlers must be short, synchronous functions returning `Json | void`. Promises are rejected by
|
|
168
|
+
both the types and runtime. External APIs, file writes, timers, and deferred draft mutations are
|
|
169
|
+
outside the handler contract: cloning a document cannot roll back external side effects.
|
|
170
|
+
When applying a result computed elsewhere, use the revision of the snapshot that computation used.
|
|
171
|
+
|
|
172
|
+
Documents and results must be JSON. Properties containing `undefined`, NaN, Map, Date, and cycles
|
|
173
|
+
are rejected. Use `delete` to remove properties and `null` for empty values. An `undefined` return
|
|
174
|
+
value means there is no result. Assigning `doc = next` only reassigns a local variable; modify the
|
|
175
|
+
copy's properties instead.
|
|
176
|
+
|
|
177
|
+
Invalid input and application rejections return explanations. Unexpected handler or infrastructure
|
|
178
|
+
exceptions and persistence failures propagate as transport errors. The MCP SDK validates input
|
|
179
|
+
schemas in addition to HTTP-side validation.
|
|
180
|
+
|
|
181
|
+
## GUI operations and drafts
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
const snap = useDoc<Doc>(); // null before the first response
|
|
185
|
+
if (!snap) return <p>Connecting…</p>;
|
|
186
|
+
|
|
187
|
+
// run uses this snapshot's revision.
|
|
188
|
+
const result = await snap.run("move_card", { cardId, beforeCardId });
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Multiple `useDoc` calls share one subscription. Documents and revisions are applied together;
|
|
192
|
+
late responses cannot rewind the state. A saved reference to an older `snap.run` still sends its
|
|
193
|
+
original revision.
|
|
194
|
+
|
|
195
|
+
Use `useEdit` for edits that span time, such as typing or dragging:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
const snap = useDoc<Doc>();
|
|
199
|
+
const edit = useEdit<string>();
|
|
200
|
+
|
|
201
|
+
// When editing first begins, after a snapshot is available:
|
|
202
|
+
edit.begin(snap, snap.doc.text);
|
|
203
|
+
edit.setValue(nextText);
|
|
204
|
+
|
|
205
|
+
// Commit against the revision captured by begin.
|
|
206
|
+
const result = await edit.run("set_text", { text: edit.value });
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- `active`, `value`, `pending`, `result`, and `error` describe the edit state.
|
|
210
|
+
- `begin` rejects an already active edit. Subscription updates do not change the draft or its base revision.
|
|
211
|
+
- Duplicate `run` calls while pending share one Promise. Disable inputs while `pending`.
|
|
212
|
+
- Success ends the edit. Conflicts, rejections, and network failures preserve the draft.
|
|
213
|
+
- `cancel()` discards the draft.
|
|
214
|
+
- `restart(latestSnapshot, revisedValue)` explicitly replaces the base and reviewed draft.
|
|
215
|
+
- `refreshDoc()` requests a refresh and resolves once the subscription confirms the current state.
|
|
216
|
+
|
|
217
|
+
The template displays the draft and current values with cancel and review controls. A network
|
|
218
|
+
failure can leave the outcome unknown; refresh the document before deciding whether to apply again.
|
|
219
|
+
Drafts stored only inside a list row can disappear when another participant moves or deletes that row.
|
|
220
|
+
|
|
221
|
+
The [movable list example](template/ui/card-editing.tsx) keeps an `EditSession` per card ID in a Map
|
|
222
|
+
outside the columns. Rows subscribe with `useSyncExternalStore`, so drafts survive remounting after
|
|
223
|
+
a move between columns. This example is intended for card-based apps and is not rendered in the
|
|
224
|
+
studio template. `EditSession` is the state management class used by `useEdit`.
|
|
225
|
+
|
|
226
|
+
## Responses and waiting
|
|
227
|
+
|
|
228
|
+
Operations return one of three normal responses. Each includes the document and revision:
|
|
229
|
+
|
|
230
|
+
```jsonc
|
|
231
|
+
{ "revision": "epoch-a:12", "actor": "llm", "doc": { "text": "hello" },
|
|
232
|
+
"activity": {}, "ok": true }
|
|
233
|
+
{ "revision": "epoch-a:12", "actor": "llm", "doc": { "text": "hello" },
|
|
234
|
+
"activity": {}, "rejected": "Use no more than 100 characters." }
|
|
235
|
+
{ "revision": "epoch-a:14", "actor": "llm", "doc": { "text": "new" },
|
|
236
|
+
"activity": {}, "conflict": true, "changes": [], "truncated": true }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The revision format above is illustrative, not a public contract. A daemon restart changes the
|
|
240
|
+
revision even if the document is unchanged. Old operations conflict; waits using old revisions
|
|
241
|
+
immediately return the latest document with `truncated: true`.
|
|
242
|
+
|
|
243
|
+
- `await_change()` immediately reads the current document and revision.
|
|
244
|
+
- `await_change({ sinceRevision })` waits for commits after that revision, or returns immediately if they already exist.
|
|
245
|
+
- `until: ["move_card"]` filters the operation names to wait for and describe. The returned document is still the full current state.
|
|
246
|
+
- `timeoutMs` defaults to 25 seconds, with a minimum of 1 second and maximum of 120 seconds.
|
|
247
|
+
- A timeout may still include changes outside the filter. Adopt the returned document and revision together.
|
|
248
|
+
|
|
249
|
+
`changes` describes operations, actors, counts, and touched JSON Pointers. Consecutive commits by the
|
|
250
|
+
same participant using the same operation are grouped. These descriptions do not determine whether
|
|
251
|
+
a write is allowed and are not instructions to retry. Descriptions exceeding 100 entries or 32 KiB,
|
|
252
|
+
and unavailable history, return an empty array with `truncated: true`. Memory retains 1,000 commits
|
|
253
|
+
from the current daemon. The document itself has a separate size cost; refer to large assets by blob ID.
|
|
254
|
+
|
|
255
|
+
`activity` reports milliseconds since each participant's last activity. `useDoc` batches reports
|
|
256
|
+
from pointerdown and keydown events and updates the elapsed time shown by the GUI. `touch()` reports
|
|
257
|
+
activity explicitly. This is advisory information: it does not indicate edit completion or guarantee
|
|
258
|
+
priority or fairness. Activity does not advance the revision or wake waiters.
|
|
259
|
+
|
|
260
|
+
## Application structure
|
|
261
|
+
|
|
262
|
+
Copy `template/` to `<app>/`, keeping the directory name consistent with `app.id`.
|
|
263
|
+
The repository's build and typecheck scripts cover all apps. The npm setup example only builds
|
|
264
|
+
`template/`; update its configuration when adding apps.
|
|
265
|
+
|
|
266
|
+
```text
|
|
267
|
+
<app>/
|
|
268
|
+
doc.ts JSON types, initial values, and shared pure functions
|
|
269
|
+
ops.ts Operation definitions
|
|
270
|
+
app.ts defineApp({ id, version, rootDir?, initialDoc, ops, webDist, shot? })
|
|
271
|
+
start.ts runApp(app)
|
|
272
|
+
main.ts Protect stdout, then start the app
|
|
273
|
+
ui/ index.html / main.tsx / vite.config.ts / tsconfig.json
|
|
274
|
+
rules.ts Optional domain logic
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Use `.js` extensions for relative imports loaded by the server. UI-only files use bundler resolution.
|
|
278
|
+
Keep server dependencies out of files shared with the UI so they are not pulled into the browser.
|
|
279
|
+
Derived values do not need to be stored in doc: for example, share a pure board-to-FEN function
|
|
280
|
+
between the UI and a query operation.
|
|
281
|
+
|
|
282
|
+
## Participants and turns
|
|
283
|
+
|
|
284
|
+
Set `env: { "DUET_ACTOR": "gpt" }` in the MCP configuration to change the participant name.
|
|
285
|
+
Defaults are `llm` for MCP and `human` for the browser. Subagents sharing an MCP connection also
|
|
286
|
+
share an actor. Authentication and multi-user account management are not provided.
|
|
287
|
+
|
|
288
|
+
Store seats and turns in doc, with a shared validation function for the UI and handlers.
|
|
289
|
+
Apps with seats should provide operations such as `sit` for additional participants. Rejections
|
|
290
|
+
should explain enough to reconsider the request, such as whose seat it is and who is calling.
|
|
291
|
+
|
|
292
|
+
## Screenshots and blobs
|
|
293
|
+
|
|
294
|
+
`render_screenshot({ path? })` renders the same GUI in a separate headless Chromium session.
|
|
295
|
+
Human drafts, hover state, selection, and scroll positions are not shared. The page is opened on
|
|
296
|
+
first use and reused afterward.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
shot: { selector: "#board", viewport: { w: 1024, h: 768 } }
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`useDoc` updates `data-duet-revision` after the DOM commit. Capture waits for the requested revision
|
|
303
|
+
or a later revision from the same daemon; the image is not guaranteed to represent the exact snapshot
|
|
304
|
+
at request time. The attribute alone does not guarantee that asynchronous images or app-specific
|
|
305
|
+
rendering have finished. Set `DUET_SHOT_ORIGIN` to the development server when needed; otherwise,
|
|
306
|
+
capture uses the built GUI served from `webDist`.
|
|
307
|
+
|
|
308
|
+
`uploadBlob(file)` stores an asset. Put its returned ID in doc through an operation. The GUI uses
|
|
309
|
+
`blobUrl(id)` and the LLM uses `read_blob({ id })`. Immutable blobs are read directly by each MCP process.
|
|
310
|
+
|
|
311
|
+
The built-in tool names `gui_url`, `await_change`, `render_screenshot`, and `read_blob` are reserved.
|
|
312
|
+
|
|
313
|
+
## Persistence and reconnection
|
|
314
|
+
|
|
315
|
+
```text
|
|
316
|
+
data/<id>.json Document, app version, and internal commit sequence
|
|
317
|
+
data/<id>.log Auxiliary diagnostic log; not a complete audit or recovery log
|
|
318
|
+
data/<id>-blobs/ Immutable assets
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
A snapshot commits when its temporary file is successfully renamed. A subsequent log write failure
|
|
322
|
+
does not undo the operation. Logs do not determine conflicts or restore change history after restart.
|
|
323
|
+
Full durability against power loss is not guaranteed. Unreadable snapshots are moved aside; app version
|
|
324
|
+
mismatches produce a uniquely named backup and a warning. Document schema validation and migrations
|
|
325
|
+
remain application responsibilities.
|
|
326
|
+
|
|
327
|
+
Ports are derived from `app.id` in the range 8000–8999. Override collisions with `DUET_PORT`, using
|
|
328
|
+
the same value for the daemon and Vite. Delegation checks the destination app ID and rejects other
|
|
329
|
+
apps; version mismatches generate warnings. The process owning the port owns the state, and other
|
|
330
|
+
MCP processes delegate through HTTP.
|
|
331
|
+
|
|
332
|
+
If the owner exits, a surviving MCP process attempts to reconnect or become the owner on its next
|
|
333
|
+
call. The GUI alone cannot start a daemon; keep the app process running to continue using the GUI.
|
|
334
|
+
**Operation POSTs are never automatically resent.** If a response is lost after persistence, refresh
|
|
335
|
+
the document and decide what to do. Exactly-once execution, locking, and human priority are not guaranteed.
|
|
336
|
+
|
|
337
|
+
## Compatibility and limits
|
|
338
|
+
|
|
339
|
+
The old API using numeric revisions, standalone `runOp`, or async handlers is incompatible.
|
|
340
|
+
Update the daemon, MCP process, and GUI together. Old snapshot integer revisions can be imported
|
|
341
|
+
as internal commit sequence numbers.
|
|
342
|
+
|
|
343
|
+
Whole-document revision checks mean unrelated changes can conflict. The library is not suitable for
|
|
344
|
+
character-by-character simultaneous editing or guaranteed LLM progress while humans keep committing
|
|
345
|
+
changes. Each operation clones, saves, and sends the full document. Undo/redo, CRDT/OT, and
|
|
346
|
+
transactions for external side effects are not provided.
|
|
347
|
+
|
|
348
|
+
## Development and validation
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
npm run typecheck
|
|
352
|
+
npm test
|
|
353
|
+
npm run test:package
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Tests use temporary directories rather than modifying saved app documents. Browser tests require
|
|
357
|
+
localhost and Chromium. `test:package` installs a real tarball into a temporary project and checks
|
|
358
|
+
types, the GUI, stdio MCP, persistence, blobs, screenshots, and restart behavior. It also downloads
|
|
359
|
+
dependencies and Chromium and removes the temporary project when finished.
|
|
360
|
+
|
|
361
|
+
GitHub Actions runs installation, typechecking, builds, the test suite, and the installed-package test
|
|
362
|
+
on pushes and pull requests, using Ubuntu / macOS and Node.js 22 / 24. Use `npm ci` to install the
|
|
363
|
+
versions recorded in the shared `package-lock.json`.
|
|
364
|
+
|
|
365
|
+
## License
|
|
366
|
+
|
|
367
|
+
[MIT License](LICENSE) — Copyright (c) 2026 Taniguchi Ryoga (SabaCan0141).
|