tandem-editor 0.2.12 → 0.3.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/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ All notable changes to Tandem will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2026-04-07
9
+
10
+ ### Wave 4: Notification & Interruption Redesign
11
+
12
+ - **Solo/Tandem mode** replaces All/Urgent/Paused interruption controls (#207, #226)
13
+ - **Dwell-time selection events** — selections fire after 1s hold (#188)
14
+ - **Configurable layout** — tabbed or three-panel, with settings popover (#206)
15
+ - **Click-to-navigate** — click annotated text to jump to annotation card
16
+ - **Tab badges** — notification counts on inactive panel tabs
17
+ - **Skill-directed response routing** — Claude responds in chat panel, not terminal
18
+ - Review banner replaced with per-annotation toasts (#208, landed earlier)
19
+ - `Y_MAP_MODE` constant, Zod validation for mode reads, error logging in channel event bridge
20
+ - 894 tests passing
21
+
22
+ ## [Unreleased]
23
+
24
+ ### Added
25
+
26
+ - Link (Ctrl+K), Horizontal Rule, and Code Block buttons in the formatting toolbar (#204)
27
+ - Suggestion cards show a visual diff — original text in red strikethrough → replacement in green (#195)
28
+ - Undo countdown progress bar — a shrinking indicator shows the 10-second undo window (#196)
29
+ - Review mode shortcut hints (Y / N / ↑↓ / Z) shown below the Review button (#200)
30
+ - Chat anchor previews expand on hover to show full text (#198)
31
+ - `disabledTitle` prop on toolbar buttons — annotation buttons show "Select text first" when no text is selected (#197)
32
+ - Explicit ✕ close button on the highlight color picker (#203)
33
+
34
+ ### Fixed
35
+
36
+ - Toolbar wraps to a second row on narrow windows instead of overflowing; inline inputs shrink responsively (#192)
37
+ - Edit button on annotation cards now shows a visible "✎ Edit" label instead of icon-only (#201)
38
+
8
39
  ## [0.2.12] - 2026-04-06
9
40
 
10
41
  ### Added
package/README.md CHANGED
@@ -80,7 +80,7 @@ Open http://localhost:5173 — you'll see `sample/welcome.md` loaded automatical
80
80
 
81
81
  ![Side panel showing annotation cards with filtering, bulk actions, and text previews](docs/screenshots/03-side-panel.png)
82
82
 
83
- Claude adds highlights, comments, suggestions, and flags directly in the document. The side panel lists all annotations with filtering by type, author, and status. Accept, dismiss, or edit each one individually — or use bulk actions to process them in batches.
83
+ Claude adds highlights, comments, suggestions, and flags directly in the document. Suggestion cards show a visual diff — original text in red strikethrough, replacement in green. The side panel lists all annotations with filtering by type, author, and status. Accept, dismiss, or edit each one individually — or use bulk actions to process them in batches.
84
84
 
85
85
  ### Chat
86
86
 
@@ -92,7 +92,7 @@ Send freeform messages to Claude alongside annotation review. Select text before
92
92
 
93
93
  ![Review mode with dimmed editor and active annotation highlighted](docs/screenshots/05-review-mode.png)
94
94
 
95
- Press **Ctrl+Shift+R** to enter keyboard review mode. Navigate with **Tab**, accept with **Y**, dismiss with **N**, examine with **E**. A 10-second undo window lets you reverse accidental accepts. The side panel tracks your position.
95
+ Press **Ctrl+Shift+R** to enter keyboard review mode. Navigate with **Tab**, accept with **Y**, dismiss with **N**, examine with **E**. A 10-second undo window with a visual countdown lets you reverse accidental accepts. Shortcut hints appear below the Review button.
96
96
 
97
97
  ### More
98
98
 
@@ -128,7 +128,7 @@ Press **Ctrl+Shift+R** to enter keyboard review mode. Navigate with **Tab**, acc
128
128
 
129
129
  ## MCP Configuration
130
130
 
131
- Tandem uses two MCP connections: **HTTP** for document tools (28 tools including annotation editing), and a **channel shim** for real-time push notifications.
131
+ Tandem uses two MCP connections: **HTTP** for document tools (30 tools including annotation editing), and a **channel shim** for real-time push notifications.
132
132
 
133
133
  **Global install** (`tandem setup`): Automatically writes both entries to `~/.claude/mcp_settings.json` (Claude Code) and/or `claude_desktop_config.json` (Claude Desktop) with absolute paths. No manual configuration needed.
134
134
 
@@ -57,7 +57,7 @@ function formatEventContent(event) {
57
57
  case "selection:changed": {
58
58
  const { from, to, selectedText } = event.payload;
59
59
  if (!selectedText) return `User cleared selection${doc}`;
60
- return `User selected text (${from}-${to}): "${selectedText}"${doc}`;
60
+ return `User is pointing at text (${from}-${to}): "${selectedText}"${doc} \u2014 respond via tandem_reply`;
61
61
  }
62
62
  case "document:opened": {
63
63
  const { fileName, format } = event.payload;
@@ -91,13 +91,25 @@ function formatEventMeta(event) {
91
91
  case "chat:message":
92
92
  meta.message_id = event.payload.messageId;
93
93
  break;
94
+ case "selection:changed":
95
+ meta.respond_via = "tandem_reply";
96
+ break;
97
+ case "document:opened":
98
+ case "document:closed":
99
+ case "document:switched":
100
+ break;
101
+ default: {
102
+ const _exhaustive = event;
103
+ break;
104
+ }
94
105
  }
95
106
  return meta;
96
107
  }
97
108
 
98
109
  // src/channel/event-bridge.ts
99
110
  var AWARENESS_DEBOUNCE_MS = 500;
100
- var SELECTION_DEBOUNCE_MS = 1500;
111
+ var SELECTION_DEBOUNCE_MS = 300;
112
+ var MODE_CACHE_TTL_MS = 2e3;
101
113
  async function startEventBridge(mcp2, tandemUrl) {
102
114
  let retries = 0;
103
115
  let lastEventId;
@@ -239,6 +251,14 @@ async function connectAndStream(mcp2, tandemUrl, lastEventId, onEventId) {
239
251
  console.error("[Channel] Received invalid SSE event, skipping");
240
252
  continue;
241
253
  }
254
+ if (event.type !== "chat:message") {
255
+ const mode = await getCachedMode(tandemUrl);
256
+ if (mode === "solo") {
257
+ console.error(`[Channel] Solo mode: suppressed ${event.type} event`);
258
+ if (eventId) onEventId(eventId);
259
+ continue;
260
+ }
261
+ }
242
262
  if (event.type === "selection:changed") {
243
263
  if (eventId) onEventId(eventId);
244
264
  if (isSelectionCleared(event)) continue;
@@ -264,6 +284,29 @@ async function connectAndStream(mcp2, tandemUrl, lastEventId, onEventId) {
264
284
  }
265
285
  }
266
286
  }
287
+ var cachedMode = "tandem";
288
+ var cachedModeAt = 0;
289
+ async function getCachedMode(tandemUrl) {
290
+ const now = Date.now();
291
+ if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;
292
+ try {
293
+ const res = await fetch(`${tandemUrl}/api/mode`);
294
+ if (res.ok) {
295
+ const { mode } = await res.json();
296
+ cachedMode = mode;
297
+ } else {
298
+ console.error(`[Channel] Mode check returned ${res.status}, using cached: "${cachedMode}"`);
299
+ }
300
+ cachedModeAt = now;
301
+ } catch (err) {
302
+ console.error(
303
+ "[Channel] Mode check failed, delivering event (fail-open):",
304
+ err instanceof Error ? err.message : err
305
+ );
306
+ cachedModeAt = now;
307
+ }
308
+ return cachedMode;
309
+ }
267
310
 
268
311
  // src/channel/index.ts
269
312
  console.log = console.error;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/channel/index.ts","../../src/shared/constants.ts","../../src/server/events/types.ts","../../src/channel/event-bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Tandem Channel Shim — Claude Code spawns this as a subprocess.\n *\n * Bridges Tandem's SSE event stream → Claude Code channel notifications,\n * and exposes a `tandem_reply` tool for Claude to respond to chat messages.\n *\n * Uses the low-level MCP `Server` class (not `McpServer`) as required by\n * the Channels API spec.\n */\n\nimport { createConnection } from \"node:net\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\nimport { DEFAULT_MCP_PORT } from \"../shared/constants.js\";\nimport { startEventBridge } from \"./event-bridge.js\";\n\n// stdout is the MCP wire — redirect console.log to stderr\nconsole.log = console.error;\nconsole.warn = console.error;\nconsole.info = console.error;\n\nconst TANDEM_URL = process.env.TANDEM_URL || \"http://localhost:3479\";\n\n// --- Pre-flight: verify Tandem server is reachable before MCP handshake ---\n\nasync function checkServerReachable(url: string, timeoutMs = 2000): Promise<boolean> {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n console.error(\n `[Channel] Invalid TANDEM_URL: \"${url}\" — expected format: http://localhost:3479`,\n );\n return false;\n }\n const port = parseInt(parsed.port || String(DEFAULT_MCP_PORT), 10);\n return new Promise((resolve) => {\n const socket = createConnection({ port, host: parsed.hostname }, () => {\n socket.destroy();\n resolve(true);\n });\n socket.setTimeout(timeoutMs);\n socket.on(\"timeout\", () => {\n socket.destroy();\n resolve(false);\n });\n socket.on(\"error\", (err) => {\n console.error(`[Channel] Server probe failed: ${err.message}`);\n socket.destroy();\n resolve(false);\n });\n });\n}\n\n// --- MCP Server setup ---\n\nconst mcp = new Server(\n { name: \"tandem-channel\", version: \"0.1.0\" },\n {\n capabilities: {\n experimental: {\n \"claude/channel\": {},\n \"claude/channel/permission\": {},\n },\n tools: {},\n },\n instructions: [\n 'Events from Tandem arrive as <channel source=\"tandem-channel\" event_type=\"...\" document_id=\"...\">.',\n \"These are real-time push notifications of user actions in the collaborative document editor.\",\n \"Event types: annotation:created, annotation:accepted, annotation:dismissed,\",\n \"chat:message, selection:changed, document:opened, document:closed, document:switched.\",\n \"Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_highlight, etc.) to act on them.\",\n \"Reply to chat messages using tandem_reply. Pass document_id from the tag attributes.\",\n \"Do not reply to non-chat events — just act on them using tools.\",\n \"If you haven't received channel notifications recently, call tandem_checkInbox as a fallback.\",\n ].join(\" \"),\n },\n);\n\n// --- Tool: tandem_reply (forwarded to Tandem HTTP server) ---\n\nmcp.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: [\n {\n name: \"tandem_reply\",\n description: \"Reply to a chat message in Tandem\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n text: { type: \"string\", description: \"The reply message\" },\n documentId: {\n type: \"string\",\n description: \"Document ID from the channel event (optional)\",\n },\n replyTo: {\n type: \"string\",\n description: \"Message ID being replied to (optional)\",\n },\n },\n required: [\"text\"],\n },\n },\n ],\n}));\n\nmcp.setRequestHandler(CallToolRequestSchema, async (req) => {\n if (req.params.name === \"tandem_reply\") {\n const args = req.params.arguments as Record<string, unknown>;\n try {\n const res = await fetch(`${TANDEM_URL}/api/channel-reply`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(args),\n });\n let data: unknown;\n try {\n data = await res.json();\n } catch {\n data = { message: \"Non-JSON response\" };\n }\n if (!res.ok) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Reply failed (${res.status}): ${JSON.stringify(data)}`,\n },\n ],\n isError: true,\n };\n }\n return { content: [{ type: \"text\" as const, text: JSON.stringify(data) }] };\n } catch (err) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Failed to send reply: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n };\n }\n }\n throw new Error(`Unknown tool: ${req.params.name}`);\n});\n\n// --- Permission relay: forward Claude Code's tool approval prompts to Tandem browser ---\n\nconst PermissionRequestSchema = z.object({\n method: z.literal(\"notifications/claude/channel/permission_request\"),\n params: z.object({\n request_id: z.string(),\n tool_name: z.string(),\n description: z.string(),\n input_preview: z.string(),\n }),\n});\n\nmcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {\n try {\n const res = await fetch(`${TANDEM_URL}/api/channel-permission`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n requestId: params.request_id,\n toolName: params.tool_name,\n description: params.description,\n inputPreview: params.input_preview,\n }),\n });\n if (!res.ok) {\n console.error(\n `[Channel] Permission relay got HTTP ${res.status} — browser may not see prompt`,\n );\n }\n } catch (err) {\n console.error(\"[Channel] Failed to forward permission request:\", err);\n }\n});\n\n// --- Connect and start ---\n\nasync function main() {\n console.error(`[Channel] Tandem channel shim starting (server: ${TANDEM_URL})`);\n\n const reachable = await checkServerReachable(TANDEM_URL);\n if (!reachable) {\n console.error(`[Channel] Cannot reach Tandem server at ${TANDEM_URL}`);\n console.error(\"[Channel] Start it with: npm run dev:standalone\");\n // Continue anyway — the event bridge will retry, and the server may start later\n }\n\n // Connect to Claude Code over stdio\n const transport = new StdioServerTransport();\n await mcp.connect(transport);\n console.error(\"[Channel] Connected to Claude Code via stdio\");\n\n // Start the SSE event bridge (runs until disconnected or max retries)\n startEventBridge(mcp, TANDEM_URL).catch((err) => {\n console.error(\"[Channel] Event bridge failed unexpectedly:\", err);\n process.exit(1);\n });\n}\n\nmain().catch((err) => {\n console.error(\"[Channel] Fatal error:\", err);\n process.exit(1);\n});\n","export const DEFAULT_WS_PORT = 3478;\nexport const DEFAULT_MCP_PORT = 3479;\n\n/** File extensions the server accepts for opening. */\nexport const SUPPORTED_EXTENSIONS = new Set([\".md\", \".txt\", \".html\", \".htm\", \".docx\"]);\nexport const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\nexport const MAX_WS_PAYLOAD = 10 * 1024 * 1024; // 10MB\nexport const MAX_WS_CONNECTIONS = 4;\nexport const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes\nexport const SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days\nexport const TYPING_DEBOUNCE = 3000; // 3 seconds\nexport const DISCONNECT_DEBOUNCE_MS = 3000; // 3 seconds before showing \"server not reachable\"\nexport const PROLONGED_DISCONNECT_MS = 30_000; // 30 seconds before showing App-level disconnect banner\nexport const OVERLAY_STALE_DEBOUNCE = 200; // 200ms\nexport const REVIEW_BANNER_THRESHOLD = 5;\n\nexport const HIGHLIGHT_COLORS: Record<string, string> = {\n yellow: \"rgba(255, 235, 59, 0.3)\",\n red: \"rgba(244, 67, 54, 0.3)\",\n green: \"rgba(76, 175, 80, 0.3)\",\n blue: \"rgba(33, 150, 243, 0.3)\",\n purple: \"rgba(156, 39, 176, 0.3)\",\n};\n\nexport const INTERRUPTION_MODE_DEFAULT = \"all\" as const;\nexport const INTERRUPTION_MODE_KEY = \"tandem:interruptionMode\";\n\n// Large file thresholds\nexport const CHARS_PER_PAGE = 3_000;\nexport const LARGE_FILE_PAGE_THRESHOLD = 50;\nexport const VERY_LARGE_FILE_PAGE_THRESHOLD = 100;\n\nexport const CLAUDE_PRESENCE_COLOR = \"#6366f1\";\nexport const CLAUDE_FOCUS_OPACITY = 0.1;\n\nexport const CTRL_ROOM = \"__tandem_ctrl__\";\n\n/** Y.Map key constants — centralized to prevent silent bugs from string typos. */\nexport const Y_MAP_ANNOTATIONS = \"annotations\";\nexport const Y_MAP_AWARENESS = \"awareness\";\nexport const Y_MAP_USER_AWARENESS = \"userAwareness\";\nexport const Y_MAP_CHAT = \"chat\";\nexport const Y_MAP_DOCUMENT_META = \"documentMeta\";\nexport const Y_MAP_SAVED_AT_VERSION = \"savedAtVersion\";\n\nexport const SERVER_INFO_DIR = \".tandem\";\nexport const SERVER_INFO_FILE = \".tandem/.server-info\";\n\nexport const RECENT_FILES_KEY = \"tandem:recentFiles\";\nexport const RECENT_FILES_CAP = 20;\n\nexport const USER_NAME_KEY = \"tandem:userName\";\nexport const USER_NAME_DEFAULT = \"You\";\n\n// Toast notifications\nexport const TOAST_DISMISS_MS = { error: 8000, warning: 6000, info: 4000 } as const;\nexport const MAX_VISIBLE_TOASTS = 5;\nexport const NOTIFICATION_BUFFER_SIZE = 50;\n\n// Onboarding tutorial\nexport const TUTORIAL_COMPLETED_KEY = \"tandem:tutorialCompleted\";\nexport const TUTORIAL_ANNOTATION_PREFIX = \"tutorial-\";\n\n// Editor layout\nexport const EDITOR_WIDTH_MODE_KEY = \"tandem:editorWidthMode\";\n\n// Channel / event queue\nexport const CHANNEL_EVENT_BUFFER_SIZE = 200;\nexport const CHANNEL_EVENT_BUFFER_AGE_MS = 60_000; // 60 seconds\nexport const CHANNEL_SSE_KEEPALIVE_MS = 15_000; // 15 seconds\nexport const CHANNEL_MAX_RETRIES = 5;\nexport const CHANNEL_RETRY_DELAY_MS = 2_000;\n","/**\n * Event types for the Tandem → Claude Code channel.\n *\n * These events flow from browser-originated Y.Map changes through an SSE\n * endpoint to the channel shim, which pushes them into Claude Code as\n * `notifications/claude/channel` messages.\n */\n\n// --- Per-event payload interfaces ---\n\nexport interface AnnotationCreatedPayload {\n annotationId: string;\n annotationType: string;\n content: string;\n textSnippet: string;\n}\n\nexport interface AnnotationAcceptedPayload {\n annotationId: string;\n textSnippet: string;\n}\n\nexport interface AnnotationDismissedPayload {\n annotationId: string;\n textSnippet: string;\n}\n\nexport interface ChatMessagePayload {\n messageId: string;\n text: string;\n replyTo: string | null;\n anchor: { from: number; to: number; textSnapshot: string } | null;\n}\n\nexport interface SelectionChangedPayload {\n from: number;\n to: number;\n selectedText: string;\n}\n\nexport interface DocumentOpenedPayload {\n fileName: string;\n format: string;\n}\n\nexport interface DocumentClosedPayload {\n fileName: string;\n}\n\nexport interface DocumentSwitchedPayload {\n fileName: string;\n}\n\n// --- Discriminated union ---\n\ninterface TandemEventBase {\n /** Timestamp-based unique ID for SSE `Last-Event-ID` reconnection. Format: `evt_<timestamp>_<rand>`. Roughly ordered but not strictly monotonic. */\n id: string;\n timestamp: number;\n /** Which document this event relates to (absent for global events). */\n documentId?: string;\n}\n\nexport type TandemEvent =\n | (TandemEventBase & { type: \"annotation:created\"; payload: AnnotationCreatedPayload })\n | (TandemEventBase & { type: \"annotation:accepted\"; payload: AnnotationAcceptedPayload })\n | (TandemEventBase & { type: \"annotation:dismissed\"; payload: AnnotationDismissedPayload })\n | (TandemEventBase & { type: \"chat:message\"; payload: ChatMessagePayload })\n | (TandemEventBase & { type: \"selection:changed\"; payload: SelectionChangedPayload })\n | (TandemEventBase & { type: \"document:opened\"; payload: DocumentOpenedPayload })\n | (TandemEventBase & { type: \"document:closed\"; payload: DocumentClosedPayload })\n | (TandemEventBase & { type: \"document:switched\"; payload: DocumentSwitchedPayload });\n\n/** Union of all event type discriminants. */\nexport type TandemEventType = TandemEvent[\"type\"];\n\n// Re-export from shared utils (single ID generation pattern)\nexport { generateEventId } from \"../../shared/utils.js\";\n\n// --- Parse guard for SSE consumers ---\n\nconst VALID_EVENT_TYPES = new Set<TandemEventType>([\n \"annotation:created\",\n \"annotation:accepted\",\n \"annotation:dismissed\",\n \"chat:message\",\n \"selection:changed\",\n \"document:opened\",\n \"document:closed\",\n \"document:switched\",\n]);\n\n/**\n * Validate a JSON-parsed value as a TandemEvent.\n * Used by the event-bridge to safely consume SSE data.\n */\nexport function parseTandemEvent(raw: unknown): TandemEvent | null {\n if (\n typeof raw !== \"object\" ||\n raw === null ||\n !(\"id\" in raw) ||\n typeof (raw as Record<string, unknown>).id !== \"string\" ||\n !(\"type\" in raw) ||\n !VALID_EVENT_TYPES.has((raw as Record<string, unknown>).type as TandemEventType) ||\n !(\"timestamp\" in raw) ||\n typeof (raw as Record<string, unknown>).timestamp !== \"number\" ||\n !(\"payload\" in raw) ||\n typeof (raw as Record<string, unknown>).payload !== \"object\"\n ) {\n return null;\n }\n return raw as TandemEvent;\n}\n\n/**\n * Convert a TandemEvent into a human-readable string for the channel `content` field.\n * Claude sees this text inside `<channel source=\"tandem-channel\">` tags.\n */\nexport function formatEventContent(event: TandemEvent): string {\n const doc = event.documentId ? ` [doc: ${event.documentId}]` : \"\";\n\n switch (event.type) {\n case \"annotation:created\": {\n const { annotationType, content, textSnippet } = event.payload;\n const snippet = textSnippet ? ` on \"${textSnippet}\"` : \"\";\n return `User created ${annotationType}${snippet}: ${content || \"(no content)\"}${doc}`;\n }\n case \"annotation:accepted\": {\n const { annotationId, textSnippet } = event.payload;\n return `User accepted annotation ${annotationId}${textSnippet ? ` (\"${textSnippet}\")` : \"\"}${doc}`;\n }\n case \"annotation:dismissed\": {\n const { annotationId, textSnippet } = event.payload;\n return `User dismissed annotation ${annotationId}${textSnippet ? ` (\"${textSnippet}\")` : \"\"}${doc}`;\n }\n case \"chat:message\": {\n const { text, replyTo } = event.payload;\n const reply = replyTo ? ` (replying to ${replyTo})` : \"\";\n return `User says${reply}: ${text}${doc}`;\n }\n case \"selection:changed\": {\n const { from, to, selectedText } = event.payload;\n if (!selectedText) return `User cleared selection${doc}`;\n return `User selected text (${from}-${to}): \"${selectedText}\"${doc}`;\n }\n case \"document:opened\": {\n const { fileName, format } = event.payload;\n return `User opened document: ${fileName} (${format})${doc}`;\n }\n case \"document:closed\": {\n const { fileName } = event.payload;\n return `User closed document: ${fileName}${doc}`;\n }\n case \"document:switched\": {\n const { fileName } = event.payload;\n return `User switched to document: ${fileName}${doc}`;\n }\n default: {\n const _exhaustive: never = event;\n return `Unknown event${doc}`;\n }\n }\n}\n\n/**\n * Build the `meta` record for a channel notification.\n * Keys use underscores only (Channels API silently drops hyphenated keys).\n */\nexport function formatEventMeta(event: TandemEvent): Record<string, string> {\n const meta: Record<string, string> = {\n event_type: event.type,\n };\n if (event.documentId) meta.document_id = event.documentId;\n\n switch (event.type) {\n case \"annotation:created\":\n case \"annotation:accepted\":\n case \"annotation:dismissed\":\n meta.annotation_id = event.payload.annotationId;\n break;\n case \"chat:message\":\n meta.message_id = event.payload.messageId;\n break;\n }\n\n return meta;\n}\n","/**\n * SSE event bridge: connects to Tandem server's /api/events endpoint\n * and pushes received events to Claude Code as channel notifications.\n */\n\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { TandemEvent } from \"../server/events/types.js\";\nimport { formatEventContent, formatEventMeta, parseTandemEvent } from \"../server/events/types.js\";\nimport { CHANNEL_MAX_RETRIES, CHANNEL_RETRY_DELAY_MS } from \"../shared/constants.js\";\n\nconst AWARENESS_DEBOUNCE_MS = 500;\nconst SELECTION_DEBOUNCE_MS = 1500;\n\nexport async function startEventBridge(mcp: Server, tandemUrl: string): Promise<void> {\n let retries = 0;\n let lastEventId: string | undefined;\n\n while (retries < CHANNEL_MAX_RETRIES) {\n try {\n await connectAndStream(mcp, tandemUrl, lastEventId, (id) => {\n lastEventId = id;\n retries = 0;\n });\n } catch (err) {\n retries++;\n console.error(\n `[Channel] SSE connection failed (${retries}/${CHANNEL_MAX_RETRIES}):`,\n err instanceof Error ? err.message : err,\n );\n\n if (retries >= CHANNEL_MAX_RETRIES) {\n console.error(\"[Channel] SSE connection exhausted, reporting error and exiting\");\n try {\n await fetch(`${tandemUrl}/api/channel-error`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n error: \"CHANNEL_CONNECT_FAILED\",\n message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`,\n }),\n });\n } catch (reportErr) {\n console.error(\n \"[Channel] Could not report failure to server:\",\n reportErr instanceof Error ? reportErr.message : reportErr,\n );\n }\n process.exit(1);\n }\n\n await new Promise((r) => setTimeout(r, CHANNEL_RETRY_DELAY_MS));\n }\n }\n}\n\nasync function connectAndStream(\n mcp: Server,\n tandemUrl: string,\n lastEventId: string | undefined,\n onEventId: (id: string) => void,\n): Promise<void> {\n const headers: Record<string, string> = { Accept: \"text/event-stream\" };\n if (lastEventId) headers[\"Last-Event-ID\"] = lastEventId;\n\n const res = await fetch(`${tandemUrl}/api/events`, { headers });\n if (!res.ok) throw new Error(`SSE endpoint returned ${res.status}`);\n if (!res.body) throw new Error(\"SSE endpoint returned no body\");\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n // Debounced awareness: only send the latest status after a quiet period\n let awarenessTimer: ReturnType<typeof setTimeout> | null = null;\n let clearAwarenessTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingAwareness: TandemEvent | null = null;\n const AWARENESS_CLEAR_MS = 3000; // Reset active state after 3s of no new events\n\n function clearAwareness(documentId?: string) {\n fetch(`${tandemUrl}/api/channel-awareness`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n documentId: documentId ?? null,\n status: \"idle\",\n active: false,\n }),\n }).catch(() => {});\n }\n\n function flushAwareness() {\n if (!pendingAwareness) return;\n const event = pendingAwareness;\n pendingAwareness = null;\n fetch(`${tandemUrl}/api/channel-awareness`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n documentId: event.documentId,\n status: `processing: ${event.type}`,\n active: true,\n }),\n }).catch((err) => {\n console.error(\"[Channel] Awareness update failed:\", err instanceof Error ? err.message : err);\n });\n\n // Auto-clear after timeout so the indicator doesn't stick\n if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);\n clearAwarenessTimer = setTimeout(() => clearAwareness(event.documentId), AWARENESS_CLEAR_MS);\n }\n\n function scheduleAwareness(event: TandemEvent) {\n pendingAwareness = event;\n if (awarenessTimer) clearTimeout(awarenessTimer);\n awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);\n }\n\n // Debounced selection: coalesce rapid selection changes, skip cleared selections\n let selectionTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingSelection: { event: TandemEvent; eventId?: string } | null = null;\n let transportBroken = false;\n\n async function flushSelection() {\n if (!pendingSelection) return;\n const { event, eventId } = pendingSelection;\n pendingSelection = null;\n if (eventId) onEventId(eventId);\n try {\n await mcp.notification({\n method: \"notifications/claude/channel\",\n params: {\n content: formatEventContent(event),\n meta: formatEventMeta(event),\n },\n });\n } catch (err) {\n console.error(\"[Channel] MCP notification failed (transport broken?):\", err);\n transportBroken = true;\n return;\n }\n scheduleAwareness(event);\n }\n\n function isSelectionCleared(event: TandemEvent): boolean {\n const p = event.payload as { from?: number; to?: number; selectedText?: string } | undefined;\n return !p || (p.from === p.to && !p.selectedText);\n }\n\n while (true) {\n if (transportBroken) throw new Error(\"MCP transport broken (detected in debounced flush)\");\n const { done, value } = await reader.read();\n if (done) throw new Error(\"SSE stream ended\");\n\n buffer += decoder.decode(value, { stream: true });\n\n let boundary: number;\n while ((boundary = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const frame = buffer.slice(0, boundary);\n buffer = buffer.slice(boundary + 2);\n\n if (frame.startsWith(\":\")) continue;\n\n let eventId: string | undefined;\n let data: string | undefined;\n\n for (const line of frame.split(\"\\n\")) {\n if (line.startsWith(\"id: \")) eventId = line.slice(4);\n else if (line.startsWith(\"data: \")) data = line.slice(6);\n }\n\n if (!data) continue;\n\n let event: TandemEvent | null;\n try {\n event = parseTandemEvent(JSON.parse(data));\n } catch {\n console.error(\"[Channel] Malformed SSE event data (skipping):\", data.slice(0, 200));\n continue;\n }\n if (!event) {\n console.error(\"[Channel] Received invalid SSE event, skipping\");\n continue;\n }\n\n // Selection events: drop cleared selections, debounce the rest\n if (event.type === \"selection:changed\") {\n if (eventId) onEventId(eventId);\n if (isSelectionCleared(event)) continue; // silently drop\n pendingSelection = { event, eventId };\n if (selectionTimer) clearTimeout(selectionTimer);\n selectionTimer = setTimeout(flushSelection, SELECTION_DEBOUNCE_MS);\n continue;\n }\n\n if (eventId) onEventId(eventId);\n\n try {\n await mcp.notification({\n method: \"notifications/claude/channel\",\n params: {\n content: formatEventContent(event),\n meta: formatEventMeta(event),\n },\n });\n } catch (err) {\n console.error(\"[Channel] MCP notification failed (transport broken?):\", err);\n throw err;\n }\n\n scheduleAwareness(event);\n }\n }\n}\n"],"mappings":"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","names":["mcp"]}
1
+ {"version":3,"sources":["../../src/channel/index.ts","../../src/shared/constants.ts","../../src/server/events/types.ts","../../src/channel/event-bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Tandem Channel Shim — Claude Code spawns this as a subprocess.\n *\n * Bridges Tandem's SSE event stream → Claude Code channel notifications,\n * and exposes a `tandem_reply` tool for Claude to respond to chat messages.\n *\n * Uses the low-level MCP `Server` class (not `McpServer`) as required by\n * the Channels API spec.\n */\n\nimport { createConnection } from \"node:net\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\nimport { DEFAULT_MCP_PORT } from \"../shared/constants.js\";\nimport { startEventBridge } from \"./event-bridge.js\";\n\n// stdout is the MCP wire — redirect console.log to stderr\nconsole.log = console.error;\nconsole.warn = console.error;\nconsole.info = console.error;\n\nconst TANDEM_URL = process.env.TANDEM_URL || \"http://localhost:3479\";\n\n// --- Pre-flight: verify Tandem server is reachable before MCP handshake ---\n\nasync function checkServerReachable(url: string, timeoutMs = 2000): Promise<boolean> {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n console.error(\n `[Channel] Invalid TANDEM_URL: \"${url}\" — expected format: http://localhost:3479`,\n );\n return false;\n }\n const port = parseInt(parsed.port || String(DEFAULT_MCP_PORT), 10);\n return new Promise((resolve) => {\n const socket = createConnection({ port, host: parsed.hostname }, () => {\n socket.destroy();\n resolve(true);\n });\n socket.setTimeout(timeoutMs);\n socket.on(\"timeout\", () => {\n socket.destroy();\n resolve(false);\n });\n socket.on(\"error\", (err) => {\n console.error(`[Channel] Server probe failed: ${err.message}`);\n socket.destroy();\n resolve(false);\n });\n });\n}\n\n// --- MCP Server setup ---\n\nconst mcp = new Server(\n { name: \"tandem-channel\", version: \"0.1.0\" },\n {\n capabilities: {\n experimental: {\n \"claude/channel\": {},\n \"claude/channel/permission\": {},\n },\n tools: {},\n },\n instructions: [\n 'Events from Tandem arrive as <channel source=\"tandem-channel\" event_type=\"...\" document_id=\"...\">.',\n \"These are real-time push notifications of user actions in the collaborative document editor.\",\n \"Event types: annotation:created, annotation:accepted, annotation:dismissed,\",\n \"chat:message, selection:changed, document:opened, document:closed, document:switched.\",\n \"Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_highlight, etc.) to act on them.\",\n \"Reply to chat messages using tandem_reply. Pass document_id from the tag attributes.\",\n \"Do not reply to non-chat events — just act on them using tools.\",\n \"If you haven't received channel notifications recently, call tandem_checkInbox as a fallback.\",\n ].join(\" \"),\n },\n);\n\n// --- Tool: tandem_reply (forwarded to Tandem HTTP server) ---\n\nmcp.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: [\n {\n name: \"tandem_reply\",\n description: \"Reply to a chat message in Tandem\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n text: { type: \"string\", description: \"The reply message\" },\n documentId: {\n type: \"string\",\n description: \"Document ID from the channel event (optional)\",\n },\n replyTo: {\n type: \"string\",\n description: \"Message ID being replied to (optional)\",\n },\n },\n required: [\"text\"],\n },\n },\n ],\n}));\n\nmcp.setRequestHandler(CallToolRequestSchema, async (req) => {\n if (req.params.name === \"tandem_reply\") {\n const args = req.params.arguments as Record<string, unknown>;\n try {\n const res = await fetch(`${TANDEM_URL}/api/channel-reply`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(args),\n });\n let data: unknown;\n try {\n data = await res.json();\n } catch {\n data = { message: \"Non-JSON response\" };\n }\n if (!res.ok) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Reply failed (${res.status}): ${JSON.stringify(data)}`,\n },\n ],\n isError: true,\n };\n }\n return { content: [{ type: \"text\" as const, text: JSON.stringify(data) }] };\n } catch (err) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Failed to send reply: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n };\n }\n }\n throw new Error(`Unknown tool: ${req.params.name}`);\n});\n\n// --- Permission relay: forward Claude Code's tool approval prompts to Tandem browser ---\n\nconst PermissionRequestSchema = z.object({\n method: z.literal(\"notifications/claude/channel/permission_request\"),\n params: z.object({\n request_id: z.string(),\n tool_name: z.string(),\n description: z.string(),\n input_preview: z.string(),\n }),\n});\n\nmcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {\n try {\n const res = await fetch(`${TANDEM_URL}/api/channel-permission`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n requestId: params.request_id,\n toolName: params.tool_name,\n description: params.description,\n inputPreview: params.input_preview,\n }),\n });\n if (!res.ok) {\n console.error(\n `[Channel] Permission relay got HTTP ${res.status} — browser may not see prompt`,\n );\n }\n } catch (err) {\n console.error(\"[Channel] Failed to forward permission request:\", err);\n }\n});\n\n// --- Connect and start ---\n\nasync function main() {\n console.error(`[Channel] Tandem channel shim starting (server: ${TANDEM_URL})`);\n\n const reachable = await checkServerReachable(TANDEM_URL);\n if (!reachable) {\n console.error(`[Channel] Cannot reach Tandem server at ${TANDEM_URL}`);\n console.error(\"[Channel] Start it with: npm run dev:standalone\");\n // Continue anyway — the event bridge will retry, and the server may start later\n }\n\n // Connect to Claude Code over stdio\n const transport = new StdioServerTransport();\n await mcp.connect(transport);\n console.error(\"[Channel] Connected to Claude Code via stdio\");\n\n // Start the SSE event bridge (runs until disconnected or max retries)\n startEventBridge(mcp, TANDEM_URL).catch((err) => {\n console.error(\"[Channel] Event bridge failed unexpectedly:\", err);\n process.exit(1);\n });\n}\n\nmain().catch((err) => {\n console.error(\"[Channel] Fatal error:\", err);\n process.exit(1);\n});\n","export const DEFAULT_WS_PORT = 3478;\nexport const DEFAULT_MCP_PORT = 3479;\n\n/** File extensions the server accepts for opening. */\nexport const SUPPORTED_EXTENSIONS = new Set([\".md\", \".txt\", \".html\", \".htm\", \".docx\"]);\nexport const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\nexport const MAX_WS_PAYLOAD = 10 * 1024 * 1024; // 10MB\nexport const MAX_WS_CONNECTIONS = 4;\nexport const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes\nexport const SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days\nexport const TYPING_DEBOUNCE = 3000; // 3 seconds\nexport const DISCONNECT_DEBOUNCE_MS = 3000; // 3 seconds before showing \"server not reachable\"\nexport const PROLONGED_DISCONNECT_MS = 30_000; // 30 seconds before showing App-level disconnect banner\nexport const OVERLAY_STALE_DEBOUNCE = 200; // 200ms\n\nexport const HIGHLIGHT_COLORS: Record<string, string> = {\n yellow: \"rgba(255, 235, 59, 0.3)\",\n red: \"rgba(244, 67, 54, 0.3)\",\n green: \"rgba(76, 175, 80, 0.3)\",\n blue: \"rgba(33, 150, 243, 0.3)\",\n purple: \"rgba(156, 39, 176, 0.3)\",\n};\n\nexport const TANDEM_MODE_DEFAULT = \"tandem\" as const;\nexport const TANDEM_MODE_KEY = \"tandem:mode\";\nexport const TANDEM_SETTINGS_KEY = \"tandem:settings\";\nexport const SELECTION_DWELL_DEFAULT_MS = 1000;\nexport const SELECTION_DWELL_MIN_MS = 500;\nexport const SELECTION_DWELL_MAX_MS = 3000;\n\n// Large file thresholds\nexport const CHARS_PER_PAGE = 3_000;\nexport const LARGE_FILE_PAGE_THRESHOLD = 50;\nexport const VERY_LARGE_FILE_PAGE_THRESHOLD = 100;\n\nexport const CLAUDE_PRESENCE_COLOR = \"#6366f1\";\nexport const CLAUDE_FOCUS_OPACITY = 0.1;\n\nexport const CTRL_ROOM = \"__tandem_ctrl__\";\n\n/** Y.Map key constants — centralized to prevent silent bugs from string typos. */\nexport const Y_MAP_ANNOTATIONS = \"annotations\";\nexport const Y_MAP_AWARENESS = \"awareness\";\nexport const Y_MAP_USER_AWARENESS = \"userAwareness\";\nexport const Y_MAP_MODE = \"mode\";\nexport const Y_MAP_CHAT = \"chat\";\nexport const Y_MAP_DOCUMENT_META = \"documentMeta\";\nexport const Y_MAP_SAVED_AT_VERSION = \"savedAtVersion\";\n\nexport const SERVER_INFO_DIR = \".tandem\";\nexport const SERVER_INFO_FILE = \".tandem/.server-info\";\n\nexport const RECENT_FILES_KEY = \"tandem:recentFiles\";\nexport const RECENT_FILES_CAP = 20;\n\nexport const USER_NAME_KEY = \"tandem:userName\";\nexport const USER_NAME_DEFAULT = \"You\";\n\n// Toast notifications\nexport const TOAST_DISMISS_MS = { error: 8000, warning: 6000, info: 4000 } as const;\nexport const MAX_VISIBLE_TOASTS = 5;\nexport const NOTIFICATION_BUFFER_SIZE = 50;\n\n// Onboarding tutorial\nexport const TUTORIAL_COMPLETED_KEY = \"tandem:tutorialCompleted\";\nexport const TUTORIAL_ANNOTATION_PREFIX = \"tutorial-\";\n\n// Editor layout\nexport const EDITOR_WIDTH_MODE_KEY = \"tandem:editorWidthMode\";\n\n// Channel / event queue\nexport const CHANNEL_EVENT_BUFFER_SIZE = 200;\nexport const CHANNEL_EVENT_BUFFER_AGE_MS = 60_000; // 60 seconds\nexport const CHANNEL_SSE_KEEPALIVE_MS = 15_000; // 15 seconds\nexport const CHANNEL_MAX_RETRIES = 5;\nexport const CHANNEL_RETRY_DELAY_MS = 2_000;\n","/**\n * Event types for the Tandem → Claude Code channel.\n *\n * These events flow from browser-originated Y.Map changes through an SSE\n * endpoint to the channel shim, which pushes them into Claude Code as\n * `notifications/claude/channel` messages.\n */\n\n// --- Per-event payload interfaces ---\n\nexport interface AnnotationCreatedPayload {\n annotationId: string;\n annotationType: string;\n content: string;\n textSnippet: string;\n}\n\nexport interface AnnotationAcceptedPayload {\n annotationId: string;\n textSnippet: string;\n}\n\nexport interface AnnotationDismissedPayload {\n annotationId: string;\n textSnippet: string;\n}\n\nexport interface ChatMessagePayload {\n messageId: string;\n text: string;\n replyTo: string | null;\n anchor: { from: number; to: number; textSnapshot: string } | null;\n}\n\nexport interface SelectionChangedPayload {\n from: number;\n to: number;\n selectedText: string;\n}\n\nexport interface DocumentOpenedPayload {\n fileName: string;\n format: string;\n}\n\nexport interface DocumentClosedPayload {\n fileName: string;\n}\n\nexport interface DocumentSwitchedPayload {\n fileName: string;\n}\n\n// --- Discriminated union ---\n\ninterface TandemEventBase {\n /** Timestamp-based unique ID for SSE `Last-Event-ID` reconnection. Format: `evt_<timestamp>_<rand>`. Roughly ordered but not strictly monotonic. */\n id: string;\n timestamp: number;\n /** Which document this event relates to (absent for global events). */\n documentId?: string;\n}\n\nexport type TandemEvent =\n | (TandemEventBase & { type: \"annotation:created\"; payload: AnnotationCreatedPayload })\n | (TandemEventBase & { type: \"annotation:accepted\"; payload: AnnotationAcceptedPayload })\n | (TandemEventBase & { type: \"annotation:dismissed\"; payload: AnnotationDismissedPayload })\n | (TandemEventBase & { type: \"chat:message\"; payload: ChatMessagePayload })\n | (TandemEventBase & { type: \"selection:changed\"; payload: SelectionChangedPayload })\n | (TandemEventBase & { type: \"document:opened\"; payload: DocumentOpenedPayload })\n | (TandemEventBase & { type: \"document:closed\"; payload: DocumentClosedPayload })\n | (TandemEventBase & { type: \"document:switched\"; payload: DocumentSwitchedPayload });\n\n/** Union of all event type discriminants. */\nexport type TandemEventType = TandemEvent[\"type\"];\n\n// Re-export from shared utils (single ID generation pattern)\nexport { generateEventId } from \"../../shared/utils.js\";\n\n// --- Parse guard for SSE consumers ---\n\nconst VALID_EVENT_TYPES = new Set<TandemEventType>([\n \"annotation:created\",\n \"annotation:accepted\",\n \"annotation:dismissed\",\n \"chat:message\",\n \"selection:changed\",\n \"document:opened\",\n \"document:closed\",\n \"document:switched\",\n]);\n\n/**\n * Validate a JSON-parsed value as a TandemEvent.\n * Used by the event-bridge to safely consume SSE data.\n */\nexport function parseTandemEvent(raw: unknown): TandemEvent | null {\n if (\n typeof raw !== \"object\" ||\n raw === null ||\n !(\"id\" in raw) ||\n typeof (raw as Record<string, unknown>).id !== \"string\" ||\n !(\"type\" in raw) ||\n !VALID_EVENT_TYPES.has((raw as Record<string, unknown>).type as TandemEventType) ||\n !(\"timestamp\" in raw) ||\n typeof (raw as Record<string, unknown>).timestamp !== \"number\" ||\n !(\"payload\" in raw) ||\n typeof (raw as Record<string, unknown>).payload !== \"object\"\n ) {\n return null;\n }\n return raw as TandemEvent;\n}\n\n/**\n * Convert a TandemEvent into a human-readable string for the channel `content` field.\n * Claude sees this text inside `<channel source=\"tandem-channel\">` tags.\n */\nexport function formatEventContent(event: TandemEvent): string {\n const doc = event.documentId ? ` [doc: ${event.documentId}]` : \"\";\n\n switch (event.type) {\n case \"annotation:created\": {\n const { annotationType, content, textSnippet } = event.payload;\n const snippet = textSnippet ? ` on \"${textSnippet}\"` : \"\";\n return `User created ${annotationType}${snippet}: ${content || \"(no content)\"}${doc}`;\n }\n case \"annotation:accepted\": {\n const { annotationId, textSnippet } = event.payload;\n return `User accepted annotation ${annotationId}${textSnippet ? ` (\"${textSnippet}\")` : \"\"}${doc}`;\n }\n case \"annotation:dismissed\": {\n const { annotationId, textSnippet } = event.payload;\n return `User dismissed annotation ${annotationId}${textSnippet ? ` (\"${textSnippet}\")` : \"\"}${doc}`;\n }\n case \"chat:message\": {\n const { text, replyTo } = event.payload;\n const reply = replyTo ? ` (replying to ${replyTo})` : \"\";\n return `User says${reply}: ${text}${doc}`;\n }\n case \"selection:changed\": {\n const { from, to, selectedText } = event.payload;\n if (!selectedText) return `User cleared selection${doc}`;\n return `User is pointing at text (${from}-${to}): \"${selectedText}\"${doc} — respond via tandem_reply`;\n }\n case \"document:opened\": {\n const { fileName, format } = event.payload;\n return `User opened document: ${fileName} (${format})${doc}`;\n }\n case \"document:closed\": {\n const { fileName } = event.payload;\n return `User closed document: ${fileName}${doc}`;\n }\n case \"document:switched\": {\n const { fileName } = event.payload;\n return `User switched to document: ${fileName}${doc}`;\n }\n default: {\n const _exhaustive: never = event;\n return `Unknown event${doc}`;\n }\n }\n}\n\n/**\n * Build the `meta` record for a channel notification.\n * Keys use underscores only (Channels API silently drops hyphenated keys).\n */\nexport function formatEventMeta(event: TandemEvent): Record<string, string> {\n const meta: Record<string, string> = {\n event_type: event.type,\n };\n if (event.documentId) meta.document_id = event.documentId;\n\n switch (event.type) {\n case \"annotation:created\":\n case \"annotation:accepted\":\n case \"annotation:dismissed\":\n meta.annotation_id = event.payload.annotationId;\n break;\n case \"chat:message\":\n meta.message_id = event.payload.messageId;\n break;\n case \"selection:changed\":\n meta.respond_via = \"tandem_reply\";\n break;\n case \"document:opened\":\n case \"document:closed\":\n case \"document:switched\":\n break;\n default: {\n const _exhaustive: never = event;\n break;\n }\n }\n\n return meta;\n}\n","/**\n * SSE event bridge: connects to Tandem server's /api/events endpoint\n * and pushes received events to Claude Code as channel notifications.\n */\n\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { TandemEvent } from \"../server/events/types.js\";\nimport { formatEventContent, formatEventMeta, parseTandemEvent } from \"../server/events/types.js\";\nimport { CHANNEL_MAX_RETRIES, CHANNEL_RETRY_DELAY_MS } from \"../shared/constants.js\";\n\nconst AWARENESS_DEBOUNCE_MS = 500;\nconst SELECTION_DEBOUNCE_MS = 300;\nconst MODE_CACHE_TTL_MS = 2000;\n\nexport async function startEventBridge(mcp: Server, tandemUrl: string): Promise<void> {\n let retries = 0;\n let lastEventId: string | undefined;\n\n while (retries < CHANNEL_MAX_RETRIES) {\n try {\n await connectAndStream(mcp, tandemUrl, lastEventId, (id) => {\n lastEventId = id;\n retries = 0;\n });\n } catch (err) {\n retries++;\n console.error(\n `[Channel] SSE connection failed (${retries}/${CHANNEL_MAX_RETRIES}):`,\n err instanceof Error ? err.message : err,\n );\n\n if (retries >= CHANNEL_MAX_RETRIES) {\n console.error(\"[Channel] SSE connection exhausted, reporting error and exiting\");\n try {\n await fetch(`${tandemUrl}/api/channel-error`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n error: \"CHANNEL_CONNECT_FAILED\",\n message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`,\n }),\n });\n } catch (reportErr) {\n console.error(\n \"[Channel] Could not report failure to server:\",\n reportErr instanceof Error ? reportErr.message : reportErr,\n );\n }\n process.exit(1);\n }\n\n await new Promise((r) => setTimeout(r, CHANNEL_RETRY_DELAY_MS));\n }\n }\n}\n\nasync function connectAndStream(\n mcp: Server,\n tandemUrl: string,\n lastEventId: string | undefined,\n onEventId: (id: string) => void,\n): Promise<void> {\n const headers: Record<string, string> = { Accept: \"text/event-stream\" };\n if (lastEventId) headers[\"Last-Event-ID\"] = lastEventId;\n\n const res = await fetch(`${tandemUrl}/api/events`, { headers });\n if (!res.ok) throw new Error(`SSE endpoint returned ${res.status}`);\n if (!res.body) throw new Error(\"SSE endpoint returned no body\");\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n // Debounced awareness: only send the latest status after a quiet period\n let awarenessTimer: ReturnType<typeof setTimeout> | null = null;\n let clearAwarenessTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingAwareness: TandemEvent | null = null;\n const AWARENESS_CLEAR_MS = 3000; // Reset active state after 3s of no new events\n\n function clearAwareness(documentId?: string) {\n fetch(`${tandemUrl}/api/channel-awareness`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n documentId: documentId ?? null,\n status: \"idle\",\n active: false,\n }),\n }).catch(() => {});\n }\n\n function flushAwareness() {\n if (!pendingAwareness) return;\n const event = pendingAwareness;\n pendingAwareness = null;\n fetch(`${tandemUrl}/api/channel-awareness`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n documentId: event.documentId,\n status: `processing: ${event.type}`,\n active: true,\n }),\n }).catch((err) => {\n console.error(\"[Channel] Awareness update failed:\", err instanceof Error ? err.message : err);\n });\n\n // Auto-clear after timeout so the indicator doesn't stick\n if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);\n clearAwarenessTimer = setTimeout(() => clearAwareness(event.documentId), AWARENESS_CLEAR_MS);\n }\n\n function scheduleAwareness(event: TandemEvent) {\n pendingAwareness = event;\n if (awarenessTimer) clearTimeout(awarenessTimer);\n awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);\n }\n\n // Debounced selection: coalesce rapid selection changes, skip cleared selections\n let selectionTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingSelection: { event: TandemEvent; eventId?: string } | null = null;\n let transportBroken = false;\n\n async function flushSelection() {\n if (!pendingSelection) return;\n const { event, eventId } = pendingSelection;\n pendingSelection = null;\n if (eventId) onEventId(eventId);\n try {\n await mcp.notification({\n method: \"notifications/claude/channel\",\n params: {\n content: formatEventContent(event),\n meta: formatEventMeta(event),\n },\n });\n } catch (err) {\n console.error(\"[Channel] MCP notification failed (transport broken?):\", err);\n transportBroken = true;\n return;\n }\n scheduleAwareness(event);\n }\n\n function isSelectionCleared(event: TandemEvent): boolean {\n const p = event.payload as { from?: number; to?: number; selectedText?: string } | undefined;\n return !p || (p.from === p.to && !p.selectedText);\n }\n\n while (true) {\n if (transportBroken) throw new Error(\"MCP transport broken (detected in debounced flush)\");\n const { done, value } = await reader.read();\n if (done) throw new Error(\"SSE stream ended\");\n\n buffer += decoder.decode(value, { stream: true });\n\n let boundary: number;\n while ((boundary = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const frame = buffer.slice(0, boundary);\n buffer = buffer.slice(boundary + 2);\n\n if (frame.startsWith(\":\")) continue;\n\n let eventId: string | undefined;\n let data: string | undefined;\n\n for (const line of frame.split(\"\\n\")) {\n if (line.startsWith(\"id: \")) eventId = line.slice(4);\n else if (line.startsWith(\"data: \")) data = line.slice(6);\n }\n\n if (!data) continue;\n\n let event: TandemEvent | null;\n try {\n event = parseTandemEvent(JSON.parse(data));\n } catch {\n console.error(\"[Channel] Malformed SSE event data (skipping):\", data.slice(0, 200));\n continue;\n }\n if (!event) {\n console.error(\"[Channel] Received invalid SSE event, skipping\");\n continue;\n }\n\n // Solo mode suppression: drop non-chat events when mode is \"solo\"\n if (event.type !== \"chat:message\") {\n const mode = await getCachedMode(tandemUrl);\n if (mode === \"solo\") {\n console.error(`[Channel] Solo mode: suppressed ${event.type} event`);\n if (eventId) onEventId(eventId);\n continue;\n }\n }\n\n // Selection events: drop cleared selections, debounce the rest\n if (event.type === \"selection:changed\") {\n if (eventId) onEventId(eventId);\n if (isSelectionCleared(event)) continue; // silently drop\n pendingSelection = { event, eventId };\n if (selectionTimer) clearTimeout(selectionTimer);\n selectionTimer = setTimeout(flushSelection, SELECTION_DEBOUNCE_MS);\n continue;\n }\n\n if (eventId) onEventId(eventId);\n\n try {\n await mcp.notification({\n method: \"notifications/claude/channel\",\n params: {\n content: formatEventContent(event),\n meta: formatEventMeta(event),\n },\n });\n } catch (err) {\n console.error(\"[Channel] MCP notification failed (transport broken?):\", err);\n throw err;\n }\n\n scheduleAwareness(event);\n }\n }\n}\n\n// Cached mode lookup — avoids an HTTP fetch per event\nlet cachedMode: string = \"tandem\";\nlet cachedModeAt = 0;\n\nasync function getCachedMode(tandemUrl: string): Promise<string> {\n const now = Date.now();\n if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;\n try {\n const res = await fetch(`${tandemUrl}/api/mode`);\n if (res.ok) {\n const { mode } = (await res.json()) as { mode: string };\n cachedMode = mode;\n } else {\n console.error(`[Channel] Mode check returned ${res.status}, using cached: \"${cachedMode}\"`);\n }\n cachedModeAt = now;\n } catch (err) {\n console.error(\n \"[Channel] Mode check failed, delivering event (fail-open):\",\n err instanceof Error ? err.message : err,\n );\n cachedModeAt = now;\n }\n return cachedMode;\n}\n"],"mappings":"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","names":["mcp"]}
package/dist/cli/index.js CHANGED
@@ -70,19 +70,18 @@ Choose the right type for each finding:
70
70
  - **\`tandem_highlight\`** \u2014 Visual marker with a short note. Colors: green (verified/good), red (problem), yellow (needs attention). Use when the finding is self-evident from the color and a brief note.
71
71
  - **\`tandem_comment\`** \u2014 Observation requiring explanation. Use when you need more than one sentence to convey reasoning.
72
72
  - **\`tandem_suggest\`** \u2014 Specific text replacement. **Prefer over comment when you can provide replacement text** \u2014 the user gets one-click accept/reject. Cannot create new paragraphs.
73
- - **\`tandem_flag\`** \u2014 Blocking issue the user must address before the document ships. Factual errors, compliance risks, missing required content. Always visible in urgent-only interruption mode.
73
+ - **\`tandem_flag\`** \u2014 Factual errors, compliance risks, missing required content. Signals a blocking issue the user must address before the document ships.
74
74
 
75
- **Priority:** Set \`priority: 'urgent'\` on any annotation type when the finding is critical and the user may be in urgent-only mode.
75
+ **Priority:** Set \`priority: 'urgent'\` on any annotation type when the finding is critical.
76
76
 
77
77
  **User-created types:** \`question\` and \`overlay\` annotations are created by users, not Claude. When you see a \`question\` in \`tandem_checkInbox\` or \`tandem_getAnnotations\`, respond with a \`tandem_comment\` on the same range or \`tandem_reply\` for conversational answers.
78
78
 
79
- ## Interruption Modes
79
+ ## Collaboration Mode
80
80
 
81
- Check \`interruptionMode\` from \`tandem_status\` or \`tandem_checkInbox\` and adapt:
81
+ Check \`mode\` from \`tandem_status\` or \`tandem_checkInbox\` and adapt:
82
82
 
83
- - **All** (default) \u2014 Annotate freely.
84
- - **Urgent-only** (\`"urgent-only"\`) \u2014 Only create \`tandem_flag\` and annotations with \`priority: 'urgent'\`. Continue reading and preparing findings, but hold non-urgent annotations until the mode changes.
85
- - **Paused** \u2014 Hold all new annotations. Keep working (read, outline, prepare) but don't push findings until the mode changes.
83
+ - **Tandem** (\`"tandem"\`, default) \u2014 Full collaboration. Annotate freely and react to selections and document changes.
84
+ - **Solo** (\`"solo"\`) \u2014 The user wants to write undisturbed. Only respond when the user sends a chat message. Do not proactively annotate or react to document activity.
86
85
 
87
86
  ## Collaboration Etiquette
88
87
 
@@ -337,7 +336,7 @@ var init_start = __esm({
337
336
 
338
337
  // src/cli/index.ts
339
338
  import updateNotifier from "update-notifier";
340
- var version = true ? "0.2.12" : "0.0.0-dev";
339
+ var version = true ? "0.3.0" : "0.0.0-dev";
341
340
  updateNotifier({ pkg: { name: "tandem-editor", version } }).notify();
342
341
  var args = process.argv.slice(2);
343
342
  if (args.includes("--help") || args.includes("-h")) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shared/constants.ts","../../src/cli/skill-content.ts","../../src/cli/setup.ts","../../src/cli/start.ts","../../src/cli/index.ts"],"sourcesContent":["export const DEFAULT_WS_PORT = 3478;\nexport const DEFAULT_MCP_PORT = 3479;\n\n/** File extensions the server accepts for opening. */\nexport const SUPPORTED_EXTENSIONS = new Set([\".md\", \".txt\", \".html\", \".htm\", \".docx\"]);\nexport const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\nexport const MAX_WS_PAYLOAD = 10 * 1024 * 1024; // 10MB\nexport const MAX_WS_CONNECTIONS = 4;\nexport const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes\nexport const SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days\nexport const TYPING_DEBOUNCE = 3000; // 3 seconds\nexport const DISCONNECT_DEBOUNCE_MS = 3000; // 3 seconds before showing \"server not reachable\"\nexport const PROLONGED_DISCONNECT_MS = 30_000; // 30 seconds before showing App-level disconnect banner\nexport const OVERLAY_STALE_DEBOUNCE = 200; // 200ms\nexport const REVIEW_BANNER_THRESHOLD = 5;\n\nexport const HIGHLIGHT_COLORS: Record<string, string> = {\n yellow: \"rgba(255, 235, 59, 0.3)\",\n red: \"rgba(244, 67, 54, 0.3)\",\n green: \"rgba(76, 175, 80, 0.3)\",\n blue: \"rgba(33, 150, 243, 0.3)\",\n purple: \"rgba(156, 39, 176, 0.3)\",\n};\n\nexport const INTERRUPTION_MODE_DEFAULT = \"all\" as const;\nexport const INTERRUPTION_MODE_KEY = \"tandem:interruptionMode\";\n\n// Large file thresholds\nexport const CHARS_PER_PAGE = 3_000;\nexport const LARGE_FILE_PAGE_THRESHOLD = 50;\nexport const VERY_LARGE_FILE_PAGE_THRESHOLD = 100;\n\nexport const CLAUDE_PRESENCE_COLOR = \"#6366f1\";\nexport const CLAUDE_FOCUS_OPACITY = 0.1;\n\nexport const CTRL_ROOM = \"__tandem_ctrl__\";\n\n/** Y.Map key constants — centralized to prevent silent bugs from string typos. */\nexport const Y_MAP_ANNOTATIONS = \"annotations\";\nexport const Y_MAP_AWARENESS = \"awareness\";\nexport const Y_MAP_USER_AWARENESS = \"userAwareness\";\nexport const Y_MAP_CHAT = \"chat\";\nexport const Y_MAP_DOCUMENT_META = \"documentMeta\";\nexport const Y_MAP_SAVED_AT_VERSION = \"savedAtVersion\";\n\nexport const SERVER_INFO_DIR = \".tandem\";\nexport const SERVER_INFO_FILE = \".tandem/.server-info\";\n\nexport const RECENT_FILES_KEY = \"tandem:recentFiles\";\nexport const RECENT_FILES_CAP = 20;\n\nexport const USER_NAME_KEY = \"tandem:userName\";\nexport const USER_NAME_DEFAULT = \"You\";\n\n// Toast notifications\nexport const TOAST_DISMISS_MS = { error: 8000, warning: 6000, info: 4000 } as const;\nexport const MAX_VISIBLE_TOASTS = 5;\nexport const NOTIFICATION_BUFFER_SIZE = 50;\n\n// Onboarding tutorial\nexport const TUTORIAL_COMPLETED_KEY = \"tandem:tutorialCompleted\";\nexport const TUTORIAL_ANNOTATION_PREFIX = \"tutorial-\";\n\n// Editor layout\nexport const EDITOR_WIDTH_MODE_KEY = \"tandem:editorWidthMode\";\n\n// Channel / event queue\nexport const CHANNEL_EVENT_BUFFER_SIZE = 200;\nexport const CHANNEL_EVENT_BUFFER_AGE_MS = 60_000; // 60 seconds\nexport const CHANNEL_SSE_KEEPALIVE_MS = 15_000; // 15 seconds\nexport const CHANNEL_MAX_RETRIES = 5;\nexport const CHANNEL_RETRY_DELAY_MS = 2_000;\n","/**\n * SKILL.md content installed to ~/.claude/skills/tandem/ by `tandem setup`.\n * Claude Code auto-discovers this and uses it when tandem_* tools are present.\n */\nexport const SKILL_CONTENT = `---\nname: tandem\ndescription: >\n Use when tandem_* MCP tools are available, the user asks about Tandem\n document editing, or collaborative document review. Provides workflow\n guidance, annotation strategy, and tool usage patterns for the Tandem\n collaborative editor.\n---\n\n# Tandem — Collaborative Document Editor\n\nTandem lets you annotate and edit documents alongside the user in real time. The user sees your changes in a browser editor; you interact via 28 tandem_* MCP tools.\n\n## Hard Rules\n\nThese prevent the most common failures. Follow them always.\n\n1. **Resolve before mutating.** Call \\`tandem_resolveRange\\` (or \\`tandem_search\\`) to get offsets before calling \\`tandem_edit\\`, \\`tandem_highlight\\`, \\`tandem_comment\\`, \\`tandem_suggest\\`, or \\`tandem_flag\\`. Never compute offsets by counting characters in previously-read text — they go stale when the user edits.\n2. **Pass \\`textSnapshot\\`.** Include the matched text as \\`textSnapshot\\` on mutations and annotations. If the text moved, the server returns \\`RANGE_MOVED\\` with relocated coordinates instead of corrupting the document.\n3. **Use \\`tandem_getTextContent\\`, not \\`tandem_getContent\\`.** \\`getContent\\` returns ProseMirror JSON and burns tokens. Use \\`getTextContent({ section: \"Section Name\" })\\` for targeted reads. The \\`section\\` parameter is case-insensitive.\n4. **\\`tandem_edit\\` cannot create paragraphs.** Newlines become literal characters. For multi-paragraph changes, use multiple \\`tandem_edit\\` calls or \\`tandem_suggest\\`.\n5. **\\`.docx\\` files are read-only.** Use annotations instead of \\`tandem_edit\\`. Offer \\`tandem_convertToMarkdown\\` if the user wants an editable copy.\n\n## Workflow\n\nStandard review sequence:\n\n1. \\`tandem_status\\` — check for already-open documents (sessions restore automatically)\n2. \\`tandem_getOutline\\` — understand document structure\n3. \\`tandem_setStatus(\"Reviewing [section]...\", { focusParagraph: N })\\` — show progress (use \\`index\\` from outline)\n4. \\`tandem_getTextContent({ section: \"...\" })\\` — read one section at a time\n5. Annotate findings (see annotation guide below)\n6. \\`tandem_checkInbox\\` — check for user messages and actions\n7. Repeat steps 3-6 for each section\n8. \\`tandem_save\\` — persist edits to disk when done\n\n## Annotation Guide\n\nChoose the right type for each finding:\n\n- **\\`tandem_highlight\\`** — Visual marker with a short note. Colors: green (verified/good), red (problem), yellow (needs attention). Use when the finding is self-evident from the color and a brief note.\n- **\\`tandem_comment\\`** — Observation requiring explanation. Use when you need more than one sentence to convey reasoning.\n- **\\`tandem_suggest\\`** — Specific text replacement. **Prefer over comment when you can provide replacement text** — the user gets one-click accept/reject. Cannot create new paragraphs.\n- **\\`tandem_flag\\`** — Blocking issue the user must address before the document ships. Factual errors, compliance risks, missing required content. Always visible in urgent-only interruption mode.\n\n**Priority:** Set \\`priority: 'urgent'\\` on any annotation type when the finding is critical and the user may be in urgent-only mode.\n\n**User-created types:** \\`question\\` and \\`overlay\\` annotations are created by users, not Claude. When you see a \\`question\\` in \\`tandem_checkInbox\\` or \\`tandem_getAnnotations\\`, respond with a \\`tandem_comment\\` on the same range or \\`tandem_reply\\` for conversational answers.\n\n## Interruption Modes\n\nCheck \\`interruptionMode\\` from \\`tandem_status\\` or \\`tandem_checkInbox\\` and adapt:\n\n- **All** (default) — Annotate freely.\n- **Urgent-only** (\\`\"urgent-only\"\\`) — Only create \\`tandem_flag\\` and annotations with \\`priority: 'urgent'\\`. Continue reading and preparing findings, but hold non-urgent annotations until the mode changes.\n- **Paused** — Hold all new annotations. Keep working (read, outline, prepare) but don't push findings until the mode changes.\n\n## Collaboration Etiquette\n\n- Check \\`tandem_getActivity()\\` before annotating near the user's cursor. If \\`isTyping\\` is true, wait for typing to stop before annotating that area.\n- Use \\`tandem_setStatus\\` to show what you're working on — the user sees it in the browser status bar.\n- **Call \\`tandem_checkInbox\\` every 2-3 tool calls**, not just at the end of a task. The real-time channel is often not connected; polling is the reliable path.\n- Reply to chat messages with \\`tandem_reply\\`, not annotations.\n\n## .docx Review Workflow\n\n1. \\`tandem_open\\` — opens in read-only mode (\\`readOnly: true\\`)\n2. \\`tandem_getAnnotations({ author: \"import\" })\\` — check for imported Word comments; read and act on them\n3. Annotate with findings (highlight, comment, suggest, flag)\n4. \\`tandem_exportAnnotations\\` — generate a review summary the user can share\n5. If the user wants editable text, offer \\`tandem_convertToMarkdown\\`\n\n## Error Recovery\n\n- **\\`RANGE_MOVED\\`** — Text shifted since you read it. The response includes \\`resolvedFrom\\`/\\`resolvedTo\\` — use those coordinates for your next call.\n- **\\`RANGE_GONE\\`** — The text was deleted. Re-read the section with \\`tandem_getTextContent\\` and re-assess.\n- **\\`INVALID_RANGE\\`** — You hit heading markup (e.g., \\`## \\`). Target text content only, not the heading prefix.\n- **\\`FORMAT_ERROR\\`** — Attempted \\`tandem_edit\\` on a read-only \\`.docx\\`. Use annotations instead.\n\n## Session Handoff\n\nWhen starting a new Claude session with Tandem already running:\n\n1. \\`tandem_status()\\` — check \\`openDocuments\\` array for restored sessions\n2. \\`tandem_listDocuments()\\` — see all open docs with details\n3. \\`tandem_getOutline()\\` — orient on the active document\n4. \\`tandem_getAnnotations()\\` — see what was already reviewed\n5. Continue where the previous session left off\n\n## Multi-Document\n\nWhen multiple documents are open, always pass \\`documentId\\` explicitly — omitting it targets the active document, which may have changed since your last call. Use \\`tandem_listDocuments\\` to see what's available. Cross-reference by reading both docs via \\`tandem_getTextContent({ documentId: \"...\" })\\` and annotating the relevant one.\n`;\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { writeFile, rename, copyFile, unlink, mkdir } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join, dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { randomUUID } from \"node:crypto\";\nimport { DEFAULT_MCP_PORT } from \"../shared/constants.js\";\nimport { SKILL_CONTENT } from \"./skill-content.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Absolute path to dist/channel/index.js (sibling of dist/cli/)\nconst CHANNEL_DIST = resolve(__dirname, \"../channel/index.js\");\n\nconst MCP_URL = `http://localhost:${DEFAULT_MCP_PORT}`;\n\nexport interface McpEntry {\n type?: \"http\";\n url?: string;\n command?: string;\n args?: string[];\n env?: Record<string, string>;\n}\n\nexport interface McpEntries {\n tandem: McpEntry;\n \"tandem-channel\": McpEntry;\n}\n\nexport function buildMcpEntries(channelPath: string): McpEntries {\n return {\n tandem: {\n type: \"http\",\n url: `${MCP_URL}/mcp`,\n },\n \"tandem-channel\": {\n command: \"node\",\n args: [channelPath],\n env: { TANDEM_URL: MCP_URL },\n },\n };\n}\n\nexport interface DetectedTarget {\n label: string;\n configPath: string;\n}\n\ninterface DetectOptions {\n homeOverride?: string;\n force?: boolean;\n}\n\nexport function detectTargets(opts: DetectOptions = {}): DetectedTarget[] {\n const home = opts.homeOverride ?? homedir();\n const targets: DetectedTarget[] = [];\n\n // Claude Code — cross-platform.\n // MCP servers are configured in ~/.claude.json under the \"mcpServers\" key.\n // Detect if the file exists OR if ~/.claude directory exists (Claude Code is installed).\n // With --force, always include regardless.\n const claudeCodeConfig = join(home, \".claude.json\");\n const claudeCodeDir = join(home, \".claude\");\n if (opts.force || existsSync(claudeCodeConfig) || existsSync(claudeCodeDir)) {\n targets.push({ label: \"Claude Code\", configPath: claudeCodeConfig });\n }\n\n // Claude Desktop — platform-specific.\n // Only detect if the config file already exists (user has launched Desktop at least once).\n // With --force, always include.\n let desktopConfig: string | null = null;\n if (process.platform === \"win32\") {\n const appdata = process.env.APPDATA ?? join(home, \"AppData\", \"Roaming\");\n desktopConfig = join(appdata, \"Claude\", \"claude_desktop_config.json\");\n } else if (process.platform === \"darwin\") {\n desktopConfig = join(\n home,\n \"Library\",\n \"Application Support\",\n \"Claude\",\n \"claude_desktop_config.json\",\n );\n } else {\n desktopConfig = join(home, \".config\", \"claude\", \"claude_desktop_config.json\");\n }\n\n if (desktopConfig && (opts.force || existsSync(desktopConfig))) {\n targets.push({ label: \"Claude Desktop\", configPath: desktopConfig });\n }\n\n return targets;\n}\n\n/**\n * Atomic write: write to a temp file in the SAME directory as the destination,\n * then rename. Using the same directory avoids EXDEV errors on Windows when\n * %TEMP% and %APPDATA% are on different drives.\n */\nasync function atomicWrite(content: string, dest: string): Promise<void> {\n const tmp = join(dirname(dest), `.tandem-setup-${randomUUID()}.tmp`);\n await writeFile(tmp, content, \"utf-8\");\n try {\n await rename(tmp, dest);\n } catch (err) {\n // EXDEV: cross-device link — fall back to copy + delete\n if ((err as NodeJS.ErrnoException).code === \"EXDEV\") {\n await copyFile(tmp, dest);\n await unlink(tmp).catch((cleanupErr: Error) => {\n console.error(` Warning: could not remove temp file ${tmp}: ${cleanupErr.message}`);\n });\n } else {\n await unlink(tmp).catch((cleanupErr: Error) => {\n console.error(` Warning: could not remove temp file ${tmp}: ${cleanupErr.message}`);\n });\n throw err;\n }\n }\n}\n\nexport async function applyConfig(configPath: string, entries: McpEntries): Promise<void> {\n // Read existing config or start fresh — no existsSync guard needed.\n // ENOENT and malformed JSON start fresh; other errors (permissions, disk) propagate.\n let existing: { mcpServers?: Record<string, McpEntry> } = {};\n try {\n existing = JSON.parse(readFileSync(configPath, \"utf-8\"));\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n // File doesn't exist yet — start fresh\n } else if (err instanceof SyntaxError) {\n console.error(\n ` Warning: ${configPath} contains malformed JSON — replacing with fresh config`,\n );\n } else {\n throw err; // Permission errors, disk errors, etc. should not be silently swallowed\n }\n }\n\n const updated = {\n ...existing,\n mcpServers: {\n ...(existing.mcpServers ?? {}),\n ...entries,\n },\n };\n\n await mkdir(dirname(configPath), { recursive: true });\n await atomicWrite(JSON.stringify(updated, null, 2) + \"\\n\", configPath);\n}\n\n/**\n * Install the Tandem skill to ~/.claude/skills/tandem/SKILL.md.\n * Claude Code auto-discovers skills in this directory and uses the description\n * field to trigger them when tandem_* tools are present.\n */\nexport async function installSkill(opts: { homeOverride?: string } = {}): Promise<void> {\n const home = opts.homeOverride ?? homedir();\n const skillPath = join(home, \".claude\", \"skills\", \"tandem\", \"SKILL.md\");\n await mkdir(dirname(skillPath), { recursive: true });\n await atomicWrite(SKILL_CONTENT, skillPath);\n}\n\n/** Run the setup command. Writes MCP config to all detected Claude installs. */\nexport async function runSetup(opts: { force?: boolean } = {}): Promise<void> {\n console.error(\"\\nTandem Setup\\n\");\n console.error(\"Detecting Claude installations...\");\n\n const targets = detectTargets({ force: opts.force });\n\n if (targets.length === 0) {\n console.error(\n \" No Claude installations detected.\\n\" +\n \" If Claude Code is installed, ensure ~/.claude exists.\\n\" +\n \" You can force configuration to default paths with: tandem setup --force\",\n );\n return;\n }\n\n for (const t of targets) {\n console.error(` Found: ${t.label} (${t.configPath})`);\n }\n\n console.error(\"\\nWriting MCP configuration...\");\n const entries = buildMcpEntries(CHANNEL_DIST);\n\n let failures = 0;\n for (const t of targets) {\n try {\n await applyConfig(t.configPath, entries);\n console.error(` \\x1b[32m✓\\x1b[0m ${t.label}`);\n } catch (err) {\n failures++;\n console.error(\n ` \\x1b[31m✗\\x1b[0m ${t.label}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n if (failures === targets.length) {\n console.error(\"\\nSetup failed — could not write any configuration. Check file permissions.\");\n process.exit(1);\n } else if (failures > 0) {\n console.error(\n `\\nSetup partially complete (${failures} target(s) failed). Start Tandem with: tandem`,\n );\n } else {\n console.error(\"\\nSetup complete! Start Tandem with: tandem\");\n console.error(\"Then in Claude, your tandem_* tools will be available.\");\n }\n\n // Install Claude Code skill (best-effort — doesn't block MCP setup)\n console.error(\"\\nInstalling Claude Code skill...\");\n try {\n await installSkill();\n console.error(\" \\x1b[32m✓\\x1b[0m ~/.claude/skills/tandem/SKILL.md\");\n } catch (err) {\n console.error(\n ` \\x1b[33m⚠\\x1b[0m Could not install skill: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Channel activation instructions (shown on all successful setups)\n if (failures < targets.length) {\n console.error(\n \"\\n\\x1b[1mReal-time push notifications (optional):\\x1b[0m\\n\" +\n \" To receive chat messages and events instantly (instead of polling),\\n\" +\n \" start Claude Code with the channel flag:\\n\\n\" +\n \" claude --dangerously-load-development-channels server:tandem-channel\\n\\n\" +\n \" Without this flag, Claude still works but relies on tandem_checkInbox polling.\\n\",\n );\n }\n}\n","import { spawn } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst SERVER_DIST = resolve(__dirname, \"../server/index.js\");\n\nexport function runStart(): void {\n if (!existsSync(SERVER_DIST)) {\n console.error(`[Tandem] Server not found at ${SERVER_DIST}`);\n console.error(\"[Tandem] The installation may be corrupted. Try: npm install -g tandem-editor\");\n process.exit(1);\n }\n\n console.error(\"[Tandem] Starting server...\");\n\n const proc = spawn(\"node\", [SERVER_DIST], {\n stdio: \"inherit\",\n env: { ...process.env, TANDEM_OPEN_BROWSER: \"1\" },\n });\n\n proc.on(\"error\", (err) => {\n console.error(`[Tandem] Failed to start server: ${err.message}`);\n process.exit(1);\n });\n\n proc.on(\"exit\", (code) => {\n process.exit(code ?? 0);\n });\n\n // Forward signals — proc.kill() with no argument uses SIGTERM on Unix\n // and TerminateProcess on Windows (correct cross-platform behavior).\n // On Windows SIGTERM is not emitted by the OS, but SIGINT (Ctrl+C) works.\n // Both are listed for Unix compatibility.\n for (const sig of [\"SIGINT\", \"SIGTERM\"] as const) {\n process.once(sig, () => proc.kill());\n }\n}\n","/**\n * Tandem CLI — entry point for the `tandem` global command.\n * Shebang is added by tsup banner at build time.\n *\n * Usage:\n * tandem Start the Tandem server and open the browser\n * tandem setup Register Tandem MCP tools with Claude Code / Claude Desktop\n * tandem setup --force Register even if no Claude install is auto-detected\n * tandem --help Show this help\n * tandem --version Show version\n */\n\nimport updateNotifier from \"update-notifier\";\n\n// Injected at build time by tsup define; declared here for TypeScript\ndeclare const __TANDEM_VERSION__: string;\nconst version = typeof __TANDEM_VERSION__ !== \"undefined\" ? __TANDEM_VERSION__ : \"0.0.0-dev\";\n\n// Check for updates in background (non-blocking, throttled to once/day)\nupdateNotifier({ pkg: { name: \"tandem-editor\", version } }).notify();\n\nconst args = process.argv.slice(2);\n\nif (args.includes(\"--help\") || args.includes(\"-h\")) {\n console.log(`tandem v${version}\n\nUsage:\n tandem Start Tandem server and open the browser\n tandem setup Register MCP tools with Claude Code / Claude Desktop\n tandem setup --force Register to default paths regardless of detection\n tandem --version\n tandem --help\n`);\n process.exit(0);\n}\n\nif (args.includes(\"--version\") || args.includes(\"-v\")) {\n console.log(version);\n process.exit(0);\n}\n\ntry {\n if (args[0] === \"setup\") {\n const { runSetup } = await import(\"./setup.js\");\n await runSetup({ force: args.includes(\"--force\") });\n } else if (!args[0] || args[0] === \"start\") {\n const { runStart } = await import(\"./start.js\");\n runStart();\n } else {\n console.error(`Unknown command: ${args[0]}`);\n console.error(\"Run 'tandem --help' for usage.\");\n process.exit(1);\n }\n} catch (err) {\n console.error(`\\n[Tandem] Fatal error: ${err instanceof Error ? err.message : String(err)}`);\n console.error(\"If this persists, try reinstalling: npm install -g tandem-editor\\n\");\n process.exit(1);\n}\n"],"mappings":"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","names":["existsSync","dirname","resolve","fileURLToPath","__dirname","runSetup","runStart"]}
1
+ {"version":3,"sources":["../../src/shared/constants.ts","../../src/cli/skill-content.ts","../../src/cli/setup.ts","../../src/cli/start.ts","../../src/cli/index.ts"],"sourcesContent":["export const DEFAULT_WS_PORT = 3478;\nexport const DEFAULT_MCP_PORT = 3479;\n\n/** File extensions the server accepts for opening. */\nexport const SUPPORTED_EXTENSIONS = new Set([\".md\", \".txt\", \".html\", \".htm\", \".docx\"]);\nexport const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\nexport const MAX_WS_PAYLOAD = 10 * 1024 * 1024; // 10MB\nexport const MAX_WS_CONNECTIONS = 4;\nexport const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes\nexport const SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days\nexport const TYPING_DEBOUNCE = 3000; // 3 seconds\nexport const DISCONNECT_DEBOUNCE_MS = 3000; // 3 seconds before showing \"server not reachable\"\nexport const PROLONGED_DISCONNECT_MS = 30_000; // 30 seconds before showing App-level disconnect banner\nexport const OVERLAY_STALE_DEBOUNCE = 200; // 200ms\n\nexport const HIGHLIGHT_COLORS: Record<string, string> = {\n yellow: \"rgba(255, 235, 59, 0.3)\",\n red: \"rgba(244, 67, 54, 0.3)\",\n green: \"rgba(76, 175, 80, 0.3)\",\n blue: \"rgba(33, 150, 243, 0.3)\",\n purple: \"rgba(156, 39, 176, 0.3)\",\n};\n\nexport const TANDEM_MODE_DEFAULT = \"tandem\" as const;\nexport const TANDEM_MODE_KEY = \"tandem:mode\";\nexport const TANDEM_SETTINGS_KEY = \"tandem:settings\";\nexport const SELECTION_DWELL_DEFAULT_MS = 1000;\nexport const SELECTION_DWELL_MIN_MS = 500;\nexport const SELECTION_DWELL_MAX_MS = 3000;\n\n// Large file thresholds\nexport const CHARS_PER_PAGE = 3_000;\nexport const LARGE_FILE_PAGE_THRESHOLD = 50;\nexport const VERY_LARGE_FILE_PAGE_THRESHOLD = 100;\n\nexport const CLAUDE_PRESENCE_COLOR = \"#6366f1\";\nexport const CLAUDE_FOCUS_OPACITY = 0.1;\n\nexport const CTRL_ROOM = \"__tandem_ctrl__\";\n\n/** Y.Map key constants — centralized to prevent silent bugs from string typos. */\nexport const Y_MAP_ANNOTATIONS = \"annotations\";\nexport const Y_MAP_AWARENESS = \"awareness\";\nexport const Y_MAP_USER_AWARENESS = \"userAwareness\";\nexport const Y_MAP_MODE = \"mode\";\nexport const Y_MAP_CHAT = \"chat\";\nexport const Y_MAP_DOCUMENT_META = \"documentMeta\";\nexport const Y_MAP_SAVED_AT_VERSION = \"savedAtVersion\";\n\nexport const SERVER_INFO_DIR = \".tandem\";\nexport const SERVER_INFO_FILE = \".tandem/.server-info\";\n\nexport const RECENT_FILES_KEY = \"tandem:recentFiles\";\nexport const RECENT_FILES_CAP = 20;\n\nexport const USER_NAME_KEY = \"tandem:userName\";\nexport const USER_NAME_DEFAULT = \"You\";\n\n// Toast notifications\nexport const TOAST_DISMISS_MS = { error: 8000, warning: 6000, info: 4000 } as const;\nexport const MAX_VISIBLE_TOASTS = 5;\nexport const NOTIFICATION_BUFFER_SIZE = 50;\n\n// Onboarding tutorial\nexport const TUTORIAL_COMPLETED_KEY = \"tandem:tutorialCompleted\";\nexport const TUTORIAL_ANNOTATION_PREFIX = \"tutorial-\";\n\n// Editor layout\nexport const EDITOR_WIDTH_MODE_KEY = \"tandem:editorWidthMode\";\n\n// Channel / event queue\nexport const CHANNEL_EVENT_BUFFER_SIZE = 200;\nexport const CHANNEL_EVENT_BUFFER_AGE_MS = 60_000; // 60 seconds\nexport const CHANNEL_SSE_KEEPALIVE_MS = 15_000; // 15 seconds\nexport const CHANNEL_MAX_RETRIES = 5;\nexport const CHANNEL_RETRY_DELAY_MS = 2_000;\n","/**\n * SKILL.md content installed to ~/.claude/skills/tandem/ by `tandem setup`.\n * Claude Code auto-discovers this and uses it when tandem_* tools are present.\n */\nexport const SKILL_CONTENT = `---\nname: tandem\ndescription: >\n Use when tandem_* MCP tools are available, the user asks about Tandem\n document editing, or collaborative document review. Provides workflow\n guidance, annotation strategy, and tool usage patterns for the Tandem\n collaborative editor.\n---\n\n# Tandem — Collaborative Document Editor\n\nTandem lets you annotate and edit documents alongside the user in real time. The user sees your changes in a browser editor; you interact via 28 tandem_* MCP tools.\n\n## Hard Rules\n\nThese prevent the most common failures. Follow them always.\n\n1. **Resolve before mutating.** Call \\`tandem_resolveRange\\` (or \\`tandem_search\\`) to get offsets before calling \\`tandem_edit\\`, \\`tandem_highlight\\`, \\`tandem_comment\\`, \\`tandem_suggest\\`, or \\`tandem_flag\\`. Never compute offsets by counting characters in previously-read text — they go stale when the user edits.\n2. **Pass \\`textSnapshot\\`.** Include the matched text as \\`textSnapshot\\` on mutations and annotations. If the text moved, the server returns \\`RANGE_MOVED\\` with relocated coordinates instead of corrupting the document.\n3. **Use \\`tandem_getTextContent\\`, not \\`tandem_getContent\\`.** \\`getContent\\` returns ProseMirror JSON and burns tokens. Use \\`getTextContent({ section: \"Section Name\" })\\` for targeted reads. The \\`section\\` parameter is case-insensitive.\n4. **\\`tandem_edit\\` cannot create paragraphs.** Newlines become literal characters. For multi-paragraph changes, use multiple \\`tandem_edit\\` calls or \\`tandem_suggest\\`.\n5. **\\`.docx\\` files are read-only.** Use annotations instead of \\`tandem_edit\\`. Offer \\`tandem_convertToMarkdown\\` if the user wants an editable copy.\n\n## Workflow\n\nStandard review sequence:\n\n1. \\`tandem_status\\` — check for already-open documents (sessions restore automatically)\n2. \\`tandem_getOutline\\` — understand document structure\n3. \\`tandem_setStatus(\"Reviewing [section]...\", { focusParagraph: N })\\` — show progress (use \\`index\\` from outline)\n4. \\`tandem_getTextContent({ section: \"...\" })\\` — read one section at a time\n5. Annotate findings (see annotation guide below)\n6. \\`tandem_checkInbox\\` — check for user messages and actions\n7. Repeat steps 3-6 for each section\n8. \\`tandem_save\\` — persist edits to disk when done\n\n## Annotation Guide\n\nChoose the right type for each finding:\n\n- **\\`tandem_highlight\\`** — Visual marker with a short note. Colors: green (verified/good), red (problem), yellow (needs attention). Use when the finding is self-evident from the color and a brief note.\n- **\\`tandem_comment\\`** — Observation requiring explanation. Use when you need more than one sentence to convey reasoning.\n- **\\`tandem_suggest\\`** — Specific text replacement. **Prefer over comment when you can provide replacement text** — the user gets one-click accept/reject. Cannot create new paragraphs.\n- **\\`tandem_flag\\`** — Factual errors, compliance risks, missing required content. Signals a blocking issue the user must address before the document ships.\n\n**Priority:** Set \\`priority: 'urgent'\\` on any annotation type when the finding is critical.\n\n**User-created types:** \\`question\\` and \\`overlay\\` annotations are created by users, not Claude. When you see a \\`question\\` in \\`tandem_checkInbox\\` or \\`tandem_getAnnotations\\`, respond with a \\`tandem_comment\\` on the same range or \\`tandem_reply\\` for conversational answers.\n\n## Collaboration Mode\n\nCheck \\`mode\\` from \\`tandem_status\\` or \\`tandem_checkInbox\\` and adapt:\n\n- **Tandem** (\\`\"tandem\"\\`, default) — Full collaboration. Annotate freely and react to selections and document changes.\n- **Solo** (\\`\"solo\"\\`) — The user wants to write undisturbed. Only respond when the user sends a chat message. Do not proactively annotate or react to document activity.\n\n## Collaboration Etiquette\n\n- Check \\`tandem_getActivity()\\` before annotating near the user's cursor. If \\`isTyping\\` is true, wait for typing to stop before annotating that area.\n- Use \\`tandem_setStatus\\` to show what you're working on — the user sees it in the browser status bar.\n- **Call \\`tandem_checkInbox\\` every 2-3 tool calls**, not just at the end of a task. The real-time channel is often not connected; polling is the reliable path.\n- Reply to chat messages with \\`tandem_reply\\`, not annotations.\n\n## .docx Review Workflow\n\n1. \\`tandem_open\\` — opens in read-only mode (\\`readOnly: true\\`)\n2. \\`tandem_getAnnotations({ author: \"import\" })\\` — check for imported Word comments; read and act on them\n3. Annotate with findings (highlight, comment, suggest, flag)\n4. \\`tandem_exportAnnotations\\` — generate a review summary the user can share\n5. If the user wants editable text, offer \\`tandem_convertToMarkdown\\`\n\n## Error Recovery\n\n- **\\`RANGE_MOVED\\`** — Text shifted since you read it. The response includes \\`resolvedFrom\\`/\\`resolvedTo\\` — use those coordinates for your next call.\n- **\\`RANGE_GONE\\`** — The text was deleted. Re-read the section with \\`tandem_getTextContent\\` and re-assess.\n- **\\`INVALID_RANGE\\`** — You hit heading markup (e.g., \\`## \\`). Target text content only, not the heading prefix.\n- **\\`FORMAT_ERROR\\`** — Attempted \\`tandem_edit\\` on a read-only \\`.docx\\`. Use annotations instead.\n\n## Session Handoff\n\nWhen starting a new Claude session with Tandem already running:\n\n1. \\`tandem_status()\\` — check \\`openDocuments\\` array for restored sessions\n2. \\`tandem_listDocuments()\\` — see all open docs with details\n3. \\`tandem_getOutline()\\` — orient on the active document\n4. \\`tandem_getAnnotations()\\` — see what was already reviewed\n5. Continue where the previous session left off\n\n## Multi-Document\n\nWhen multiple documents are open, always pass \\`documentId\\` explicitly — omitting it targets the active document, which may have changed since your last call. Use \\`tandem_listDocuments\\` to see what's available. Cross-reference by reading both docs via \\`tandem_getTextContent({ documentId: \"...\" })\\` and annotating the relevant one.\n`;\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { writeFile, rename, copyFile, unlink, mkdir } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join, dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { randomUUID } from \"node:crypto\";\nimport { DEFAULT_MCP_PORT } from \"../shared/constants.js\";\nimport { SKILL_CONTENT } from \"./skill-content.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Absolute path to dist/channel/index.js (sibling of dist/cli/)\nconst CHANNEL_DIST = resolve(__dirname, \"../channel/index.js\");\n\nconst MCP_URL = `http://localhost:${DEFAULT_MCP_PORT}`;\n\nexport interface McpEntry {\n type?: \"http\";\n url?: string;\n command?: string;\n args?: string[];\n env?: Record<string, string>;\n}\n\nexport interface McpEntries {\n tandem: McpEntry;\n \"tandem-channel\": McpEntry;\n}\n\nexport function buildMcpEntries(channelPath: string): McpEntries {\n return {\n tandem: {\n type: \"http\",\n url: `${MCP_URL}/mcp`,\n },\n \"tandem-channel\": {\n command: \"node\",\n args: [channelPath],\n env: { TANDEM_URL: MCP_URL },\n },\n };\n}\n\nexport interface DetectedTarget {\n label: string;\n configPath: string;\n}\n\ninterface DetectOptions {\n homeOverride?: string;\n force?: boolean;\n}\n\nexport function detectTargets(opts: DetectOptions = {}): DetectedTarget[] {\n const home = opts.homeOverride ?? homedir();\n const targets: DetectedTarget[] = [];\n\n // Claude Code — cross-platform.\n // MCP servers are configured in ~/.claude.json under the \"mcpServers\" key.\n // Detect if the file exists OR if ~/.claude directory exists (Claude Code is installed).\n // With --force, always include regardless.\n const claudeCodeConfig = join(home, \".claude.json\");\n const claudeCodeDir = join(home, \".claude\");\n if (opts.force || existsSync(claudeCodeConfig) || existsSync(claudeCodeDir)) {\n targets.push({ label: \"Claude Code\", configPath: claudeCodeConfig });\n }\n\n // Claude Desktop — platform-specific.\n // Only detect if the config file already exists (user has launched Desktop at least once).\n // With --force, always include.\n let desktopConfig: string | null = null;\n if (process.platform === \"win32\") {\n const appdata = process.env.APPDATA ?? join(home, \"AppData\", \"Roaming\");\n desktopConfig = join(appdata, \"Claude\", \"claude_desktop_config.json\");\n } else if (process.platform === \"darwin\") {\n desktopConfig = join(\n home,\n \"Library\",\n \"Application Support\",\n \"Claude\",\n \"claude_desktop_config.json\",\n );\n } else {\n desktopConfig = join(home, \".config\", \"claude\", \"claude_desktop_config.json\");\n }\n\n if (desktopConfig && (opts.force || existsSync(desktopConfig))) {\n targets.push({ label: \"Claude Desktop\", configPath: desktopConfig });\n }\n\n return targets;\n}\n\n/**\n * Atomic write: write to a temp file in the SAME directory as the destination,\n * then rename. Using the same directory avoids EXDEV errors on Windows when\n * %TEMP% and %APPDATA% are on different drives.\n */\nasync function atomicWrite(content: string, dest: string): Promise<void> {\n const tmp = join(dirname(dest), `.tandem-setup-${randomUUID()}.tmp`);\n await writeFile(tmp, content, \"utf-8\");\n try {\n await rename(tmp, dest);\n } catch (err) {\n // EXDEV: cross-device link — fall back to copy + delete\n if ((err as NodeJS.ErrnoException).code === \"EXDEV\") {\n await copyFile(tmp, dest);\n await unlink(tmp).catch((cleanupErr: Error) => {\n console.error(` Warning: could not remove temp file ${tmp}: ${cleanupErr.message}`);\n });\n } else {\n await unlink(tmp).catch((cleanupErr: Error) => {\n console.error(` Warning: could not remove temp file ${tmp}: ${cleanupErr.message}`);\n });\n throw err;\n }\n }\n}\n\nexport async function applyConfig(configPath: string, entries: McpEntries): Promise<void> {\n // Read existing config or start fresh — no existsSync guard needed.\n // ENOENT and malformed JSON start fresh; other errors (permissions, disk) propagate.\n let existing: { mcpServers?: Record<string, McpEntry> } = {};\n try {\n existing = JSON.parse(readFileSync(configPath, \"utf-8\"));\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n // File doesn't exist yet — start fresh\n } else if (err instanceof SyntaxError) {\n console.error(\n ` Warning: ${configPath} contains malformed JSON — replacing with fresh config`,\n );\n } else {\n throw err; // Permission errors, disk errors, etc. should not be silently swallowed\n }\n }\n\n const updated = {\n ...existing,\n mcpServers: {\n ...(existing.mcpServers ?? {}),\n ...entries,\n },\n };\n\n await mkdir(dirname(configPath), { recursive: true });\n await atomicWrite(JSON.stringify(updated, null, 2) + \"\\n\", configPath);\n}\n\n/**\n * Install the Tandem skill to ~/.claude/skills/tandem/SKILL.md.\n * Claude Code auto-discovers skills in this directory and uses the description\n * field to trigger them when tandem_* tools are present.\n */\nexport async function installSkill(opts: { homeOverride?: string } = {}): Promise<void> {\n const home = opts.homeOverride ?? homedir();\n const skillPath = join(home, \".claude\", \"skills\", \"tandem\", \"SKILL.md\");\n await mkdir(dirname(skillPath), { recursive: true });\n await atomicWrite(SKILL_CONTENT, skillPath);\n}\n\n/** Run the setup command. Writes MCP config to all detected Claude installs. */\nexport async function runSetup(opts: { force?: boolean } = {}): Promise<void> {\n console.error(\"\\nTandem Setup\\n\");\n console.error(\"Detecting Claude installations...\");\n\n const targets = detectTargets({ force: opts.force });\n\n if (targets.length === 0) {\n console.error(\n \" No Claude installations detected.\\n\" +\n \" If Claude Code is installed, ensure ~/.claude exists.\\n\" +\n \" You can force configuration to default paths with: tandem setup --force\",\n );\n return;\n }\n\n for (const t of targets) {\n console.error(` Found: ${t.label} (${t.configPath})`);\n }\n\n console.error(\"\\nWriting MCP configuration...\");\n const entries = buildMcpEntries(CHANNEL_DIST);\n\n let failures = 0;\n for (const t of targets) {\n try {\n await applyConfig(t.configPath, entries);\n console.error(` \\x1b[32m✓\\x1b[0m ${t.label}`);\n } catch (err) {\n failures++;\n console.error(\n ` \\x1b[31m✗\\x1b[0m ${t.label}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n if (failures === targets.length) {\n console.error(\"\\nSetup failed — could not write any configuration. Check file permissions.\");\n process.exit(1);\n } else if (failures > 0) {\n console.error(\n `\\nSetup partially complete (${failures} target(s) failed). Start Tandem with: tandem`,\n );\n } else {\n console.error(\"\\nSetup complete! Start Tandem with: tandem\");\n console.error(\"Then in Claude, your tandem_* tools will be available.\");\n }\n\n // Install Claude Code skill (best-effort — doesn't block MCP setup)\n console.error(\"\\nInstalling Claude Code skill...\");\n try {\n await installSkill();\n console.error(\" \\x1b[32m✓\\x1b[0m ~/.claude/skills/tandem/SKILL.md\");\n } catch (err) {\n console.error(\n ` \\x1b[33m⚠\\x1b[0m Could not install skill: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Channel activation instructions (shown on all successful setups)\n if (failures < targets.length) {\n console.error(\n \"\\n\\x1b[1mReal-time push notifications (optional):\\x1b[0m\\n\" +\n \" To receive chat messages and events instantly (instead of polling),\\n\" +\n \" start Claude Code with the channel flag:\\n\\n\" +\n \" claude --dangerously-load-development-channels server:tandem-channel\\n\\n\" +\n \" Without this flag, Claude still works but relies on tandem_checkInbox polling.\\n\",\n );\n }\n}\n","import { spawn } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst SERVER_DIST = resolve(__dirname, \"../server/index.js\");\n\nexport function runStart(): void {\n if (!existsSync(SERVER_DIST)) {\n console.error(`[Tandem] Server not found at ${SERVER_DIST}`);\n console.error(\"[Tandem] The installation may be corrupted. Try: npm install -g tandem-editor\");\n process.exit(1);\n }\n\n console.error(\"[Tandem] Starting server...\");\n\n const proc = spawn(\"node\", [SERVER_DIST], {\n stdio: \"inherit\",\n env: { ...process.env, TANDEM_OPEN_BROWSER: \"1\" },\n });\n\n proc.on(\"error\", (err) => {\n console.error(`[Tandem] Failed to start server: ${err.message}`);\n process.exit(1);\n });\n\n proc.on(\"exit\", (code) => {\n process.exit(code ?? 0);\n });\n\n // Forward signals — proc.kill() with no argument uses SIGTERM on Unix\n // and TerminateProcess on Windows (correct cross-platform behavior).\n // On Windows SIGTERM is not emitted by the OS, but SIGINT (Ctrl+C) works.\n // Both are listed for Unix compatibility.\n for (const sig of [\"SIGINT\", \"SIGTERM\"] as const) {\n process.once(sig, () => proc.kill());\n }\n}\n","/**\n * Tandem CLI — entry point for the `tandem` global command.\n * Shebang is added by tsup banner at build time.\n *\n * Usage:\n * tandem Start the Tandem server and open the browser\n * tandem setup Register Tandem MCP tools with Claude Code / Claude Desktop\n * tandem setup --force Register even if no Claude install is auto-detected\n * tandem --help Show this help\n * tandem --version Show version\n */\n\nimport updateNotifier from \"update-notifier\";\n\n// Injected at build time by tsup define; declared here for TypeScript\ndeclare const __TANDEM_VERSION__: string;\nconst version = typeof __TANDEM_VERSION__ !== \"undefined\" ? __TANDEM_VERSION__ : \"0.0.0-dev\";\n\n// Check for updates in background (non-blocking, throttled to once/day)\nupdateNotifier({ pkg: { name: \"tandem-editor\", version } }).notify();\n\nconst args = process.argv.slice(2);\n\nif (args.includes(\"--help\") || args.includes(\"-h\")) {\n console.log(`tandem v${version}\n\nUsage:\n tandem Start Tandem server and open the browser\n tandem setup Register MCP tools with Claude Code / Claude Desktop\n tandem setup --force Register to default paths regardless of detection\n tandem --version\n tandem --help\n`);\n process.exit(0);\n}\n\nif (args.includes(\"--version\") || args.includes(\"-v\")) {\n console.log(version);\n process.exit(0);\n}\n\ntry {\n if (args[0] === \"setup\") {\n const { runSetup } = await import(\"./setup.js\");\n await runSetup({ force: args.includes(\"--force\") });\n } else if (!args[0] || args[0] === \"start\") {\n const { runStart } = await import(\"./start.js\");\n runStart();\n } else {\n console.error(`Unknown command: ${args[0]}`);\n console.error(\"Run 'tandem --help' for usage.\");\n process.exit(1);\n }\n} catch (err) {\n console.error(`\\n[Tandem] Fatal error: ${err instanceof Error ? err.message : String(err)}`);\n console.error(\"If this persists, try reinstalling: npm install -g tandem-editor\\n\");\n process.exit(1);\n}\n"],"mappings":";;;;;;;;;;;;AAAA,IACa,kBAIA,eACA,gBAEA,cACA;AATb;AAAA;AAAA;AACO,IAAM,mBAAmB;AAIzB,IAAM,gBAAgB,KAAK,OAAO;AAClC,IAAM,iBAAiB,KAAK,OAAO;AAEnC,IAAM,eAAe,KAAK,KAAK;AAC/B,IAAM,kBAAkB,KAAK,KAAK,KAAK,KAAK;AAAA;AAAA;;;ACTnD,IAIa;AAJb;AAAA;AAAA;AAIO,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACJ7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAS,cAAc,kBAAkB;AACzC,SAAS,WAAW,QAAQ,UAAU,QAAQ,aAAa;AAC3D,SAAS,eAAe;AACxB,SAAS,MAAM,SAAS,eAAe;AACvC,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAwBpB,SAAS,gBAAgB,aAAiC;AAC/D,SAAO;AAAA,IACL,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,KAAK,GAAG,OAAO;AAAA,IACjB;AAAA,IACA,kBAAkB;AAAA,MAChB,SAAS;AAAA,MACT,MAAM,CAAC,WAAW;AAAA,MAClB,KAAK,EAAE,YAAY,QAAQ;AAAA,IAC7B;AAAA,EACF;AACF;AAYO,SAAS,cAAc,OAAsB,CAAC,GAAqB;AACxE,QAAM,OAAO,KAAK,gBAAgB,QAAQ;AAC1C,QAAM,UAA4B,CAAC;AAMnC,QAAM,mBAAmB,KAAK,MAAM,cAAc;AAClD,QAAM,gBAAgB,KAAK,MAAM,SAAS;AAC1C,MAAI,KAAK,SAAS,WAAW,gBAAgB,KAAK,WAAW,aAAa,GAAG;AAC3E,YAAQ,KAAK,EAAE,OAAO,eAAe,YAAY,iBAAiB,CAAC;AAAA,EACrE;AAKA,MAAI,gBAA+B;AACnC,MAAI,QAAQ,aAAa,SAAS;AAChC,UAAM,UAAU,QAAQ,IAAI,WAAW,KAAK,MAAM,WAAW,SAAS;AACtE,oBAAgB,KAAK,SAAS,UAAU,4BAA4B;AAAA,EACtE,WAAW,QAAQ,aAAa,UAAU;AACxC,oBAAgB;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,OAAO;AACL,oBAAgB,KAAK,MAAM,WAAW,UAAU,4BAA4B;AAAA,EAC9E;AAEA,MAAI,kBAAkB,KAAK,SAAS,WAAW,aAAa,IAAI;AAC9D,YAAQ,KAAK,EAAE,OAAO,kBAAkB,YAAY,cAAc,CAAC;AAAA,EACrE;AAEA,SAAO;AACT;AAOA,eAAe,YAAY,SAAiB,MAA6B;AACvE,QAAM,MAAM,KAAK,QAAQ,IAAI,GAAG,iBAAiB,WAAW,CAAC,MAAM;AACnE,QAAM,UAAU,KAAK,SAAS,OAAO;AACrC,MAAI;AACF,UAAM,OAAO,KAAK,IAAI;AAAA,EACxB,SAAS,KAAK;AAEZ,QAAK,IAA8B,SAAS,SAAS;AACnD,YAAM,SAAS,KAAK,IAAI;AACxB,YAAM,OAAO,GAAG,EAAE,MAAM,CAAC,eAAsB;AAC7C,gBAAQ,MAAM,yCAAyC,GAAG,KAAK,WAAW,OAAO,EAAE;AAAA,MACrF,CAAC;AAAA,IACH,OAAO;AACL,YAAM,OAAO,GAAG,EAAE,MAAM,CAAC,eAAsB;AAC7C,gBAAQ,MAAM,yCAAyC,GAAG,KAAK,WAAW,OAAO,EAAE;AAAA,MACrF,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,eAAsB,YAAY,YAAoB,SAAoC;AAGxF,MAAI,WAAsD,CAAC;AAC3D,MAAI;AACF,eAAW,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AAAA,EACzD,SAAS,KAAK;AACZ,UAAM,OAAQ,IAA8B;AAC5C,QAAI,SAAS,UAAU;AAAA,IAEvB,WAAW,eAAe,aAAa;AACrC,cAAQ;AAAA,QACN,cAAc,UAAU;AAAA,MAC1B;AAAA,IACF,OAAO;AACL,YAAM;AAAA,IACR;AAAA,EACF;AAEA,QAAM,UAAU;AAAA,IACd,GAAG;AAAA,IACH,YAAY;AAAA,MACV,GAAI,SAAS,cAAc,CAAC;AAAA,MAC5B,GAAG;AAAA,IACL;AAAA,EACF;AAEA,QAAM,MAAM,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AACpD,QAAM,YAAY,KAAK,UAAU,SAAS,MAAM,CAAC,IAAI,MAAM,UAAU;AACvE;AAOA,eAAsB,aAAa,OAAkC,CAAC,GAAkB;AACtF,QAAM,OAAO,KAAK,gBAAgB,QAAQ;AAC1C,QAAM,YAAY,KAAK,MAAM,WAAW,UAAU,UAAU,UAAU;AACtE,QAAM,MAAM,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AACnD,QAAM,YAAY,eAAe,SAAS;AAC5C;AAGA,eAAsB,SAAS,OAA4B,CAAC,GAAkB;AAC5E,UAAQ,MAAM,kBAAkB;AAChC,UAAQ,MAAM,mCAAmC;AAEjD,QAAM,UAAU,cAAc,EAAE,OAAO,KAAK,MAAM,CAAC;AAEnD,MAAI,QAAQ,WAAW,GAAG;AACxB,YAAQ;AAAA,MACN;AAAA,IAGF;AACA;AAAA,EACF;AAEA,aAAW,KAAK,SAAS;AACvB,YAAQ,MAAM,YAAY,EAAE,KAAK,KAAK,EAAE,UAAU,GAAG;AAAA,EACvD;AAEA,UAAQ,MAAM,gCAAgC;AAC9C,QAAM,UAAU,gBAAgB,YAAY;AAE5C,MAAI,WAAW;AACf,aAAW,KAAK,SAAS;AACvB,QAAI;AACF,YAAM,YAAY,EAAE,YAAY,OAAO;AACvC,cAAQ,MAAM,2BAAsB,EAAE,KAAK,EAAE;AAAA,IAC/C,SAAS,KAAK;AACZ;AACA,cAAQ;AAAA,QACN,2BAAsB,EAAE,KAAK,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACpF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,aAAa,QAAQ,QAAQ;AAC/B,YAAQ,MAAM,kFAA6E;AAC3F,YAAQ,KAAK,CAAC;AAAA,EAChB,WAAW,WAAW,GAAG;AACvB,YAAQ;AAAA,MACN;AAAA,4BAA+B,QAAQ;AAAA,IACzC;AAAA,EACF,OAAO;AACL,YAAQ,MAAM,6CAA6C;AAC3D,YAAQ,MAAM,wDAAwD;AAAA,EACxE;AAGA,UAAQ,MAAM,mCAAmC;AACjD,MAAI;AACF,UAAM,aAAa;AACnB,YAAQ,MAAM,0DAAqD;AAAA,EACrE,SAAS,KAAK;AACZ,YAAQ;AAAA,MACN,oDAA+C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IACjG;AAAA,EACF;AAGA,MAAI,WAAW,QAAQ,QAAQ;AAC7B,YAAQ;AAAA,MACN;AAAA,IAKF;AAAA,EACF;AACF;AAvOA,IASM,WAGA,cAEA;AAdN;AAAA;AAAA;AAMA;AACA;AAEA,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AAGxD,IAAM,eAAe,QAAQ,WAAW,qBAAqB;AAE7D,IAAM,UAAU,oBAAoB,gBAAgB;AAAA;AAAA;;;ACdpD;AAAA;AAAA;AAAA;AAAA,SAAS,aAAa;AACtB,SAAS,cAAAA,mBAAkB;AAC3B,SAAS,WAAAC,UAAS,WAAAC,gBAAe;AACjC,SAAS,iBAAAC,sBAAqB;AAKvB,SAAS,WAAiB;AAC/B,MAAI,CAACH,YAAW,WAAW,GAAG;AAC5B,YAAQ,MAAM,gCAAgC,WAAW,EAAE;AAC3D,YAAQ,MAAM,+EAA+E;AAC7F,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,UAAQ,MAAM,6BAA6B;AAE3C,QAAM,OAAO,MAAM,QAAQ,CAAC,WAAW,GAAG;AAAA,IACxC,OAAO;AAAA,IACP,KAAK,EAAE,GAAG,QAAQ,KAAK,qBAAqB,IAAI;AAAA,EAClD,CAAC;AAED,OAAK,GAAG,SAAS,CAAC,QAAQ;AACxB,YAAQ,MAAM,oCAAoC,IAAI,OAAO,EAAE;AAC/D,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AAED,OAAK,GAAG,QAAQ,CAAC,SAAS;AACxB,YAAQ,KAAK,QAAQ,CAAC;AAAA,EACxB,CAAC;AAMD,aAAW,OAAO,CAAC,UAAU,SAAS,GAAY;AAChD,YAAQ,KAAK,KAAK,MAAM,KAAK,KAAK,CAAC;AAAA,EACrC;AACF;AAtCA,IAKMI,YACA;AANN;AAAA;AAAA;AAKA,IAAMA,aAAYH,SAAQE,eAAc,YAAY,GAAG,CAAC;AACxD,IAAM,cAAcD,SAAQE,YAAW,oBAAoB;AAAA;AAAA;;;ACM3D,OAAO,oBAAoB;AAI3B,IAAM,UAAU,OAA4C,UAAqB;AAGjF,eAAe,EAAE,KAAK,EAAE,MAAM,iBAAiB,QAAQ,EAAE,CAAC,EAAE,OAAO;AAEnE,IAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AAEjC,IAAI,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,IAAI,GAAG;AAClD,UAAQ,IAAI,WAAW,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAQ/B;AACC,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAI,KAAK,SAAS,WAAW,KAAK,KAAK,SAAS,IAAI,GAAG;AACrD,UAAQ,IAAI,OAAO;AACnB,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAI;AACF,MAAI,KAAK,CAAC,MAAM,SAAS;AACvB,UAAM,EAAE,UAAAC,UAAS,IAAI,MAAM;AAC3B,UAAMA,UAAS,EAAE,OAAO,KAAK,SAAS,SAAS,EAAE,CAAC;AAAA,EACpD,WAAW,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,MAAM,SAAS;AAC1C,UAAM,EAAE,UAAAC,UAAS,IAAI,MAAM;AAC3B,IAAAA,UAAS;AAAA,EACX,OAAO;AACL,YAAQ,MAAM,oBAAoB,KAAK,CAAC,CAAC,EAAE;AAC3C,YAAQ,MAAM,gCAAgC;AAC9C,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,SAAS,KAAK;AACZ,UAAQ,MAAM;AAAA,wBAA2B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC3F,UAAQ,MAAM,oEAAoE;AAClF,UAAQ,KAAK,CAAC;AAChB;","names":["existsSync","dirname","resolve","fileURLToPath","__dirname","runSetup","runStart"]}