tandem-editor 0.4.0 → 0.5.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/dist/cli/index.js CHANGED
@@ -31,7 +31,7 @@ var init_skill_content = __esm({
31
31
  name: tandem
32
32
  description: >
33
33
  Use when tandem_* MCP tools are available, the user asks about Tandem
34
- document editing, or collaborative document review. Provides workflow
34
+ document editing, or iterating on text collaboratively. Provides workflow
35
35
  guidance, annotation strategy, and tool usage patterns for the Tandem
36
36
  collaborative editor.
37
37
  ---
@@ -52,13 +52,13 @@ These prevent the most common failures. Follow them always.
52
52
 
53
53
  ## Workflow
54
54
 
55
- Standard review sequence:
55
+ Standard workflow:
56
56
 
57
57
  1. \`tandem_status\` \u2014 check for already-open documents (sessions restore automatically)
58
58
  2. \`tandem_getOutline\` \u2014 understand document structure
59
- 3. \`tandem_setStatus("Reviewing [section]...", { focusParagraph: N })\` \u2014 show progress (use \`index\` from outline)
59
+ 3. \`tandem_setStatus("Working on [section]...", { focusParagraph: N })\` \u2014 show progress (use \`index\` from outline)
60
60
  4. \`tandem_getTextContent({ section: "..." })\` \u2014 read one section at a time
61
- 5. Annotate findings (see annotation guide below)
61
+ 5. Annotate or edit as needed (see annotation guide below)
62
62
  6. \`tandem_checkInbox\` \u2014 check for user messages and actions
63
63
  7. Repeat steps 3-6 for each section
64
64
  8. \`tandem_save\` \u2014 persist edits to disk when done
@@ -83,7 +83,7 @@ Check \`mode\` from \`tandem_status\` or \`tandem_checkInbox\` and adapt:
83
83
 
84
84
  ## Reacting to Document Events
85
85
 
86
- Selection events can reach you two ways. Over the real-time channel they arrive as notifications with \`meta.respond_via = "tandem_reply"\`. When polling via \`tandem_checkInbox\`, the current selection shows up under \`activity.selectedText\` (no \`meta\` field \u2014 that only exists on channel pushes). Either way, when the user holds a selection, briefly acknowledge what they highlighted via \`tandem_reply\` \u2014 don't annotate unless asked. Use \`tandem_reply\` for any document-context reaction (chat messages, selections, question annotations); reserve terminal output for non-document work the user explicitly requests. In Solo mode, hold reactions until the user sends a chat message.
86
+ Selections are **not** sent as standalone events. Instead, when the user sends a chat message, any buffered selection is attached as a \`selection\` field on the \`chat:message\` payload. This gives you context about what text the user was looking at when they wrote their message. When polling via \`tandem_checkInbox\`, the current selection shows up under \`activity.selectedText\`. Use \`tandem_reply\` for any document-context reaction (chat messages, question annotations); reserve terminal output for non-document work the user explicitly requests. In Solo mode, hold reactions until the user sends a chat message.
87
87
 
88
88
  ## Collaboration Etiquette
89
89
 
@@ -338,7 +338,7 @@ var init_start = __esm({
338
338
 
339
339
  // src/cli/index.ts
340
340
  import updateNotifier from "update-notifier";
341
- var version = true ? "0.4.0" : "0.0.0-dev";
341
+ var version = true ? "0.5.0" : "0.0.0-dev";
342
342
  updateNotifier({ pkg: { name: "tandem-editor", version } }).notify();
343
343
  var args = process.argv.slice(2);
344
344
  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\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\";\n// Panel-width localStorage keys.\n//\n// NOTE: these use legacy hyphen naming (vs the neighboring colon convention\n// `tandem:mode`/`tandem:settings`) because they predate the colon scheme and\n// changing the strings would invalidate every existing user's saved widths.\n// Do not \"fix\" the style — the key string is the persistence contract.\n//\n// Right-side panel width is shared between the tabbed layout and the\n// three-panel right panel. The left key only applies in three-panel mode.\nexport const PANEL_WIDTH_KEY = \"tandem-panel-width\";\nexport const LEFT_PANEL_WIDTH_KEY = \"tandem-left-panel-width\";\n\nexport type PanelSide = \"left\" | \"right\";\n\n/**\n * Maps a panel side to its localStorage key. Using a Record instead of two\n * bare constants makes the \"both handles write to the same key\" regression\n * (#228) structurally impossible — you can't accidentally map both sides to\n * the same value at a callsite.\n *\n * Uses `as const satisfies Record<PanelSide, string>` so the value type stays\n * as the literal strings rather than widening to `string` — this preserves\n * the persistence-key identity at every callsite while still enforcing\n * exhaustive coverage of `PanelSide`.\n */\nexport const PANEL_WIDTH_KEYS = {\n left: LEFT_PANEL_WIDTH_KEY,\n right: PANEL_WIDTH_KEY,\n} as const satisfies Record<PanelSide, string>;\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_DWELL_MS = \"selectionDwellMs\";\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/** Tauri WebView origin hostname — must be accepted alongside localhost. */\nexport const TAURI_HOSTNAME = \"tauri.localhost\";\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 the tandem_* MCP tool suite.\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**User-created types:** \\`question\\` annotation is 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## Reacting to Document Events\n\nSelection events can reach you two ways. Over the real-time channel they arrive as notifications with \\`meta.respond_via = \"tandem_reply\"\\`. When polling via \\`tandem_checkInbox\\`, the current selection shows up under \\`activity.selectedText\\` (no \\`meta\\` field — that only exists on channel pushes). Either way, when the user holds a selection, briefly acknowledge what they highlighted via \\`tandem_reply\\` — don't annotate unless asked. Use \\`tandem_reply\\` for any document-context reaction (chat messages, selections, question annotations); reserve terminal output for non-document work the user explicitly requests. In Solo mode, hold reactions until the user sends a chat message.\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 { randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { copyFile, mkdir, rename, unlink, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\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, nodeBinary = \"node\"): McpEntries {\n return {\n tandem: {\n type: \"http\",\n url: `${MCP_URL}/mcp`,\n },\n \"tandem-channel\": {\n command: nodeBinary,\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\";\n// Panel-width localStorage keys.\n//\n// NOTE: these use legacy hyphen naming (vs the neighboring colon convention\n// `tandem:mode`/`tandem:settings`) because they predate the colon scheme and\n// changing the strings would invalidate every existing user's saved widths.\n// Do not \"fix\" the style — the key string is the persistence contract.\n//\n// Right-side panel width is shared between the tabbed layout and the\n// three-panel right panel. The left key only applies in three-panel mode.\nexport const PANEL_WIDTH_KEY = \"tandem-panel-width\";\nexport const LEFT_PANEL_WIDTH_KEY = \"tandem-left-panel-width\";\n\nexport type PanelSide = \"left\" | \"right\";\n\n/**\n * Maps a panel side to its localStorage key. Using a Record instead of two\n * bare constants makes the \"both handles write to the same key\" regression\n * (#228) structurally impossible — you can't accidentally map both sides to\n * the same value at a callsite.\n *\n * Uses `as const satisfies Record<PanelSide, string>` so the value type stays\n * as the literal strings rather than widening to `string` — this preserves\n * the persistence-key identity at every callsite while still enforcing\n * exhaustive coverage of `PanelSide`.\n */\nexport const PANEL_WIDTH_KEYS = {\n left: LEFT_PANEL_WIDTH_KEY,\n right: PANEL_WIDTH_KEY,\n} as const satisfies Record<PanelSide, string>;\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_DWELL_MS = \"selectionDwellMs\";\nexport const Y_MAP_CHAT = \"chat\";\nexport const Y_MAP_DOCUMENT_META = \"documentMeta\";\nexport const Y_MAP_ANNOTATION_REPLIES = \"annotationReplies\";\nexport const Y_MAP_SAVED_AT_VERSION = \"savedAtVersion\";\nexport const Y_MAP_AUTHORSHIP = \"authorship\";\n\nexport const AUTHORSHIP_TOGGLE_KEY = \"tandem:showAuthorship\";\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/** Tauri WebView origin hostname — must be accepted alongside localhost. */\nexport const TAURI_HOSTNAME = \"tauri.localhost\";\n\n// Zoom persistence (Tauri desktop)\nexport const ZOOM_STORAGE_KEY = \"tandem:zoomLevel\";\nexport const ZOOM_MIN = 0.5;\nexport const ZOOM_MAX = 2.0;\nexport const ZOOM_DEFAULT = 1.0;\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 iterating on text collaboratively. 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 the tandem_* MCP tool suite.\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 workflow:\n\n1. \\`tandem_status\\` — check for already-open documents (sessions restore automatically)\n2. \\`tandem_getOutline\\` — understand document structure\n3. \\`tandem_setStatus(\"Working on [section]...\", { focusParagraph: N })\\` — show progress (use \\`index\\` from outline)\n4. \\`tandem_getTextContent({ section: \"...\" })\\` — read one section at a time\n5. Annotate or edit as needed (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**User-created types:** \\`question\\` annotation is 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## Reacting to Document Events\n\nSelections are **not** sent as standalone events. Instead, when the user sends a chat message, any buffered selection is attached as a \\`selection\\` field on the \\`chat:message\\` payload. This gives you context about what text the user was looking at when they wrote their message. When polling via \\`tandem_checkInbox\\`, the current selection shows up under \\`activity.selectedText\\`. Use \\`tandem_reply\\` for any document-context reaction (chat messages, question annotations); reserve terminal output for non-document work the user explicitly requests. In Solo mode, hold reactions until the user sends a chat message.\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 { randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { copyFile, mkdir, rename, unlink, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\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, nodeBinary = \"node\"): McpEntries {\n return {\n tandem: {\n type: \"http\",\n url: `${MCP_URL}/mcp`,\n },\n \"tandem-channel\": {\n command: nodeBinary,\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"]}