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/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://localhost: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.
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://localhost:3479/health
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://localhost: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.
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) — 28 MCP tools (25 active, 3 deprecated stubs) + channel API endpoints
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-024
205
- - [Lessons Learned](docs/lessons-learned.md) — 48 implementation lessons
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://localhost:3479/mcp"
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://localhost:3479" }
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://localhost:3479` | Channel shim server URL |
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) |
@@ -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://localhost:${DEFAULT_MCP_PORT}`;
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}/api/channel-error`,
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: "CHANNEL_CONNECT_FAILED",
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, "/api/channel-error", CHANNEL_ERROR_REPORT_TIMEOUT_MS)
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}/api/events`, { headers, signal: connectCtrl.signal });
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}/api/channel-awareness`,
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(err, "/api/channel-awareness clear", CHANNEL_AWARENESS_FETCH_TIMEOUT_MS)
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}/api/channel-awareness`,
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
- "/api/channel-awareness update",
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(`${tandemUrl}/api/mode`, {}, CHANNEL_MODE_FETCH_TIMEOUT_MS);
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, "/api/mode", CHANNEL_MODE_FETCH_TIMEOUT_MS)
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}/api/channel-reply`,
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
- "/api/channel-reply",
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}/api/channel-permission`,
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, "/api/channel-permission", CHANNEL_PERMISSION_FETCH_TIMEOUT_MS)
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://localhost:3479`
18621
+ `[Channel] Invalid TANDEM_URL: "${url}" \u2014 expected format: http://127.0.0.1:3479`
18581
18622
  );
18582
18623
  return false;
18583
18624
  }