tandem-editor 0.11.2 → 0.12.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/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +135 -65
- package/README.md +9 -9
- package/dist/channel/index.js +59 -18
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +102 -31
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/{CoworkSettings-DK3jjdwK.js → CoworkSettings-C0cS9R7L.js} +1 -1
- package/dist/client/assets/event-CNdo2oXa.js +1 -0
- package/dist/client/assets/index-Dn5JwXA3.css +1 -0
- package/dist/client/assets/index-n-vFW5By.js +299 -0
- package/dist/client/assets/webview-Bhf-n_os.js +1 -0
- package/dist/client/assets/window-DePn7tLG.js +1 -0
- package/dist/client/index.html +4 -2
- package/dist/monitor/index.js +23 -16
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +388 -331
- package/dist/server/index.js.map +1 -1
- package/package.json +7 -3
- package/dist/client/assets/index-CfT503n4.js +0 -297
- package/dist/client/assets/index-DeJe09pn.css +0 -1
- package/dist/client/assets/webview-Ben21ZLJ.js +0 -1
- package/dist/client/assets/window-BxBvHL5k.js +0 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ claude plugin marketplace add bloknayrb/tandem
|
|
|
41
41
|
claude plugin install tandem@tandem-editor
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
**Tandem must be running on the host before the plugin can do anything.** The plugin spawns two stdio MCP processes (`tandem mcp-stdio` and `tandem channel`) that proxy to `http://
|
|
44
|
+
**Tandem must be running on the host before the plugin can do anything.** The plugin spawns two stdio MCP processes (`tandem mcp-stdio` and `tandem channel`) that proxy to `http://127.0.0.1:3479`. If the server isn't up they fail fast and log "Tandem server not reachable at …". Start the Tauri desktop app or run `tandem start` on the host first, then open Claude.
|
|
45
45
|
|
|
46
46
|
### Legacy stdio channel shim
|
|
47
47
|
|
|
@@ -116,7 +116,7 @@ npm run doctor # checks Node.js, MCP config, server health, ports
|
|
|
116
116
|
Or check the raw health endpoint:
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
|
-
curl http://
|
|
119
|
+
curl http://127.0.0.1:3479/health
|
|
120
120
|
# → {"status":"ok","version":"0.8.0","transport":"http","hasSession":false}
|
|
121
121
|
```
|
|
122
122
|
|
|
@@ -132,7 +132,7 @@ npm install
|
|
|
132
132
|
npm run dev:standalone # starts backend (:3478/:3479), editor client (:5173), and monitor
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
Open <http://
|
|
135
|
+
Open <http://127.0.0.1:5173> — you'll see `sample/welcome.md` loaded automatically on first run. The `.mcp.json` in the repo configures Claude Code automatically when run from this directory.
|
|
136
136
|
|
|
137
137
|
</details>
|
|
138
138
|
|
|
@@ -197,12 +197,12 @@ See the full [Roadmap](docs/roadmap.md) and [Known Limitations](docs/roadmap.md#
|
|
|
197
197
|
## Documentation
|
|
198
198
|
|
|
199
199
|
- **[User Guide](docs/user-guide.md)** — How to use Tandem: editor UI, annotations, chat, review mode, keyboard shortcuts
|
|
200
|
-
- [MCP Tool Reference](docs/mcp-tools.md) —
|
|
200
|
+
- [MCP Tool Reference](docs/mcp-tools.md) — 29 MCP tools (26 active, 3 deprecated stubs) + channel API endpoints
|
|
201
201
|
- [Architecture](docs/architecture.md) — System design, data flows, coordinate systems, channel push
|
|
202
202
|
- [Workflows](docs/workflows.md) — Claude Code usage patterns: text iteration, cross-referencing, multi-model
|
|
203
203
|
- [Roadmap](docs/roadmap.md) — Phase 2+ roadmap, known issues, future extensions
|
|
204
|
-
- [Design Decisions](docs/decisions.md) — ADR-001 through ADR-
|
|
205
|
-
- [Lessons Learned](docs/lessons-learned.md) —
|
|
204
|
+
- [Design Decisions](docs/decisions.md) — ADR-001 through ADR-030
|
|
205
|
+
- [Lessons Learned](docs/lessons-learned.md) — 73 implementation lessons
|
|
206
206
|
|
|
207
207
|
## CLI Commands
|
|
208
208
|
|
|
@@ -231,12 +231,12 @@ Tandem registers two MCP connections: **HTTP** for document tools (28 tools incl
|
|
|
231
231
|
"mcpServers": {
|
|
232
232
|
"tandem": {
|
|
233
233
|
"type": "http",
|
|
234
|
-
"url": "http://
|
|
234
|
+
"url": "http://127.0.0.1:3479/mcp"
|
|
235
235
|
},
|
|
236
236
|
"tandem-channel": {
|
|
237
237
|
"command": "npx",
|
|
238
238
|
"args": ["tsx", "src/channel/index.ts"],
|
|
239
|
-
"env": { "TANDEM_URL": "http://
|
|
239
|
+
"env": { "TANDEM_URL": "http://127.0.0.1:3479" }
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
}
|
|
@@ -252,7 +252,7 @@ All optional — defaults work out of the box.
|
|
|
252
252
|
| ---------------------------------- | ----------------------- | --------------------------------------------------------------- |
|
|
253
253
|
| `TANDEM_PORT` | `3478` | Hocuspocus WebSocket port |
|
|
254
254
|
| `TANDEM_MCP_PORT` | `3479` | MCP HTTP + REST API port |
|
|
255
|
-
| `TANDEM_URL` | `http://
|
|
255
|
+
| `TANDEM_URL` | `http://127.0.0.1:3479` | Channel shim server URL |
|
|
256
256
|
| `TANDEM_TRANSPORT` | `http` | Transport mode (`http` or `stdio`) |
|
|
257
257
|
| `TANDEM_NO_SAMPLE` | unset | Set to `1` to skip auto-opening `sample/welcome.md` |
|
|
258
258
|
| `TANDEM_CLAUDE_CMD` | `claude` | Claude Code executable name (for `tandem setup` auto-detection) |
|
package/dist/channel/index.js
CHANGED
|
@@ -17993,13 +17993,19 @@ var StdioServerTransport = class {
|
|
|
17993
17993
|
}
|
|
17994
17994
|
};
|
|
17995
17995
|
|
|
17996
|
+
// src/shared/api-paths.ts
|
|
17997
|
+
var API_EVENTS = "/api/events";
|
|
17998
|
+
var API_CHANNEL_AWARENESS = "/api/channel-awareness";
|
|
17999
|
+
var API_CHANNEL_ERROR = "/api/channel-error";
|
|
18000
|
+
var API_CHANNEL_REPLY = "/api/channel-reply";
|
|
18001
|
+
var API_CHANNEL_PERMISSION = "/api/channel-permission";
|
|
18002
|
+
var API_MODE = "/api/mode";
|
|
18003
|
+
|
|
17996
18004
|
// src/shared/constants.ts
|
|
17997
18005
|
var DEFAULT_MCP_PORT = 3479;
|
|
17998
18006
|
var TANDEM_REPO_URL = "https://github.com/bloknayrb/tandem";
|
|
17999
18007
|
var TANDEM_ISSUES_NEW_URL = `${TANDEM_REPO_URL}/issues/new`;
|
|
18000
18008
|
var MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
18001
|
-
var MAX_WS_PAYLOAD = 10 * 1024 * 1024;
|
|
18002
|
-
var IDLE_TIMEOUT = 30 * 60 * 1e3;
|
|
18003
18009
|
var SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
|
|
18004
18010
|
var CHANNEL_MAX_RETRIES = 5;
|
|
18005
18011
|
var CHANNEL_RETRY_DELAY_MS = 2e3;
|
|
@@ -18030,7 +18036,7 @@ function resolveTandemUrlCandidate(override) {
|
|
|
18030
18036
|
for (const url of candidates) {
|
|
18031
18037
|
if (url !== void 0 && url.trim() !== "") return url.trim();
|
|
18032
18038
|
}
|
|
18033
|
-
return `http://
|
|
18039
|
+
return `http://127.0.0.1:${DEFAULT_MCP_PORT}`;
|
|
18034
18040
|
}
|
|
18035
18041
|
function resolveAuthTokenCandidate(override) {
|
|
18036
18042
|
const candidates = [
|
|
@@ -18186,6 +18192,33 @@ function formatEventMeta(event) {
|
|
|
18186
18192
|
return meta;
|
|
18187
18193
|
}
|
|
18188
18194
|
|
|
18195
|
+
// src/shared/types.ts
|
|
18196
|
+
var AnnotationTypeSchema = external_exports.enum(["highlight", "note", "comment"]);
|
|
18197
|
+
var AnnotationStatusSchema = external_exports.enum(["pending", "accepted", "dismissed"]);
|
|
18198
|
+
var HighlightColorSchema = external_exports.enum(["yellow", "green", "blue", "pink"]);
|
|
18199
|
+
var SeveritySchema = external_exports.enum(["info", "warning", "error", "success"]);
|
|
18200
|
+
var TandemModeSchema = external_exports.enum(["solo", "tandem"]);
|
|
18201
|
+
var AuthorSchema = external_exports.enum(["user", "claude", "import"]);
|
|
18202
|
+
var ReplyAuthorSchema = external_exports.enum(["user", "claude"]);
|
|
18203
|
+
var AnnotationActionSchema = external_exports.enum(["accept", "dismiss"]);
|
|
18204
|
+
var ExportFormatSchema = external_exports.enum(["markdown", "json"]);
|
|
18205
|
+
var DocumentFormatSchema = external_exports.enum(["md", "txt", "html", "docx"]);
|
|
18206
|
+
var ToolErrorCodeSchema = external_exports.enum([
|
|
18207
|
+
"RANGE_GONE",
|
|
18208
|
+
"RANGE_MOVED",
|
|
18209
|
+
"FILE_LOCKED",
|
|
18210
|
+
"FILE_NOT_FOUND",
|
|
18211
|
+
"NO_DOCUMENT",
|
|
18212
|
+
"INVALID_RANGE",
|
|
18213
|
+
"INVALID_ARGUMENT",
|
|
18214
|
+
"NOT_FOUND",
|
|
18215
|
+
"ANNOTATION_RESOLVED",
|
|
18216
|
+
"FORMAT_ERROR",
|
|
18217
|
+
"PERMISSION_DENIED"
|
|
18218
|
+
]);
|
|
18219
|
+
var ChannelErrorCodeSchema = external_exports.enum(["CHANNEL_CONNECT_FAILED", "MONITOR_CONNECT_FAILED"]);
|
|
18220
|
+
var CHANNEL_CONNECT_FAILED = "CHANNEL_CONNECT_FAILED";
|
|
18221
|
+
|
|
18189
18222
|
// src/channel/event-bridge.ts
|
|
18190
18223
|
var AWARENESS_DEBOUNCE_MS = 500;
|
|
18191
18224
|
var MODE_CACHE_TTL_MS = 2e3;
|
|
@@ -18208,12 +18241,12 @@ async function startEventBridge(mcp, tandemUrl) {
|
|
|
18208
18241
|
console.error("[Channel] SSE connection exhausted, reporting error and exiting");
|
|
18209
18242
|
try {
|
|
18210
18243
|
await fetchWithTimeout(
|
|
18211
|
-
`${tandemUrl}
|
|
18244
|
+
`${tandemUrl}${API_CHANNEL_ERROR}`,
|
|
18212
18245
|
{
|
|
18213
18246
|
method: "POST",
|
|
18214
18247
|
headers: { "Content-Type": "application/json" },
|
|
18215
18248
|
body: JSON.stringify({
|
|
18216
|
-
error:
|
|
18249
|
+
error: CHANNEL_CONNECT_FAILED,
|
|
18217
18250
|
message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`
|
|
18218
18251
|
})
|
|
18219
18252
|
},
|
|
@@ -18222,7 +18255,7 @@ async function startEventBridge(mcp, tandemUrl) {
|
|
|
18222
18255
|
} catch (reportErr) {
|
|
18223
18256
|
console.error(
|
|
18224
18257
|
"[Channel] Could not report failure to server:",
|
|
18225
|
-
describeFetchError(reportErr,
|
|
18258
|
+
describeFetchError(reportErr, API_CHANNEL_ERROR, CHANNEL_ERROR_REPORT_TIMEOUT_MS)
|
|
18226
18259
|
);
|
|
18227
18260
|
}
|
|
18228
18261
|
process.exit(1);
|
|
@@ -18241,7 +18274,7 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18241
18274
|
);
|
|
18242
18275
|
let res;
|
|
18243
18276
|
try {
|
|
18244
|
-
res = await authFetch(`${tandemUrl}
|
|
18277
|
+
res = await authFetch(`${tandemUrl}${API_EVENTS}`, { headers, signal: connectCtrl.signal });
|
|
18245
18278
|
} finally {
|
|
18246
18279
|
clearTimeout(connectTimer);
|
|
18247
18280
|
}
|
|
@@ -18265,7 +18298,7 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18265
18298
|
const AWARENESS_CLEAR_MS = 3e3;
|
|
18266
18299
|
function clearAwareness(documentId) {
|
|
18267
18300
|
fetchWithTimeout(
|
|
18268
|
-
`${tandemUrl}
|
|
18301
|
+
`${tandemUrl}${API_CHANNEL_AWARENESS}`,
|
|
18269
18302
|
{
|
|
18270
18303
|
method: "POST",
|
|
18271
18304
|
headers: { "Content-Type": "application/json" },
|
|
@@ -18279,7 +18312,11 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18279
18312
|
).catch((err) => {
|
|
18280
18313
|
console.error(
|
|
18281
18314
|
"[Channel] clearAwareness failed (non-fatal):",
|
|
18282
|
-
describeFetchError(
|
|
18315
|
+
describeFetchError(
|
|
18316
|
+
err,
|
|
18317
|
+
`${API_CHANNEL_AWARENESS} clear`,
|
|
18318
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18319
|
+
)
|
|
18283
18320
|
);
|
|
18284
18321
|
});
|
|
18285
18322
|
}
|
|
@@ -18288,7 +18325,7 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18288
18325
|
const event = pendingAwareness;
|
|
18289
18326
|
pendingAwareness = null;
|
|
18290
18327
|
fetchWithTimeout(
|
|
18291
|
-
`${tandemUrl}
|
|
18328
|
+
`${tandemUrl}${API_CHANNEL_AWARENESS}`,
|
|
18292
18329
|
{
|
|
18293
18330
|
method: "POST",
|
|
18294
18331
|
headers: { "Content-Type": "application/json" },
|
|
@@ -18304,7 +18341,7 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18304
18341
|
"[Channel] Awareness update failed:",
|
|
18305
18342
|
describeFetchError(
|
|
18306
18343
|
err,
|
|
18307
|
-
|
|
18344
|
+
`${API_CHANNEL_AWARENESS} update`,
|
|
18308
18345
|
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18309
18346
|
)
|
|
18310
18347
|
);
|
|
@@ -18400,7 +18437,11 @@ async function getCachedMode(tandemUrl) {
|
|
|
18400
18437
|
const now = Date.now();
|
|
18401
18438
|
if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;
|
|
18402
18439
|
try {
|
|
18403
|
-
const res = await fetchWithTimeout(
|
|
18440
|
+
const res = await fetchWithTimeout(
|
|
18441
|
+
`${tandemUrl}${API_MODE}`,
|
|
18442
|
+
{},
|
|
18443
|
+
CHANNEL_MODE_FETCH_TIMEOUT_MS
|
|
18444
|
+
);
|
|
18404
18445
|
if (res.ok) {
|
|
18405
18446
|
const { mode } = await res.json();
|
|
18406
18447
|
cachedMode = mode;
|
|
@@ -18411,7 +18452,7 @@ async function getCachedMode(tandemUrl) {
|
|
|
18411
18452
|
} catch (err) {
|
|
18412
18453
|
console.error(
|
|
18413
18454
|
"[Channel] Mode check failed, delivering event (fail-open):",
|
|
18414
|
-
describeFetchError(err,
|
|
18455
|
+
describeFetchError(err, API_MODE, CHANNEL_MODE_FETCH_TIMEOUT_MS)
|
|
18415
18456
|
);
|
|
18416
18457
|
cachedModeAt = now;
|
|
18417
18458
|
}
|
|
@@ -18473,7 +18514,7 @@ async function runChannel(opts = {}) {
|
|
|
18473
18514
|
const args = req.params.arguments;
|
|
18474
18515
|
try {
|
|
18475
18516
|
const res = await fetchWithTimeout(
|
|
18476
|
-
`${tandemUrl}
|
|
18517
|
+
`${tandemUrl}${API_CHANNEL_REPLY}`,
|
|
18477
18518
|
{
|
|
18478
18519
|
method: "POST",
|
|
18479
18520
|
headers: { "Content-Type": "application/json" },
|
|
@@ -18507,7 +18548,7 @@ async function runChannel(opts = {}) {
|
|
|
18507
18548
|
type: "text",
|
|
18508
18549
|
text: `Failed to send reply: ${describeFetchError(
|
|
18509
18550
|
err,
|
|
18510
|
-
|
|
18551
|
+
API_CHANNEL_REPLY,
|
|
18511
18552
|
CHANNEL_REPLY_FETCH_TIMEOUT_MS
|
|
18512
18553
|
)}`
|
|
18513
18554
|
}
|
|
@@ -18530,7 +18571,7 @@ async function runChannel(opts = {}) {
|
|
|
18530
18571
|
mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
|
|
18531
18572
|
try {
|
|
18532
18573
|
const res = await fetchWithTimeout(
|
|
18533
|
-
`${tandemUrl}
|
|
18574
|
+
`${tandemUrl}${API_CHANNEL_PERMISSION}`,
|
|
18534
18575
|
{
|
|
18535
18576
|
method: "POST",
|
|
18536
18577
|
headers: { "Content-Type": "application/json" },
|
|
@@ -18551,7 +18592,7 @@ async function runChannel(opts = {}) {
|
|
|
18551
18592
|
} catch (err) {
|
|
18552
18593
|
console.error(
|
|
18553
18594
|
"[Channel] Failed to forward permission request:",
|
|
18554
|
-
describeFetchError(err,
|
|
18595
|
+
describeFetchError(err, API_CHANNEL_PERMISSION, CHANNEL_PERMISSION_FETCH_TIMEOUT_MS)
|
|
18555
18596
|
);
|
|
18556
18597
|
}
|
|
18557
18598
|
});
|
|
@@ -18577,7 +18618,7 @@ async function checkServerReachable(url, timeoutMs = 2e3) {
|
|
|
18577
18618
|
parsed = new URL(url);
|
|
18578
18619
|
} catch {
|
|
18579
18620
|
console.error(
|
|
18580
|
-
`[Channel] Invalid TANDEM_URL: "${url}" \u2014 expected format: http://
|
|
18621
|
+
`[Channel] Invalid TANDEM_URL: "${url}" \u2014 expected format: http://127.0.0.1:3479`
|
|
18581
18622
|
);
|
|
18582
18623
|
return false;
|
|
18583
18624
|
}
|