tandem-editor 0.14.3 → 0.15.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.
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "name": "tandem",
3
- "version": "0.8.0",
3
+ "version": "0.15.0",
4
4
  "description": "Edit and iterate on documents with any MCP-capable AI. Claude is the default and best-supported client today.",
5
5
  "author": {
6
6
  "name": "Tandem"
7
7
  },
8
8
  "repository": "https://github.com/bloknayrb/tandem",
9
- "license": "MIT",
9
+ "license": "BUSL-1.1",
10
10
  "keywords": ["editor", "collaborative", "mcp", "claude"],
11
11
  "mcpServers": {
12
12
  "tandem": {
13
13
  "command": "npx",
14
- "args": ["-y", "tandem-editor", "mcp-stdio"],
14
+ "args": ["-y", "tandem-editor@0.15.0", "mcp-stdio"],
15
15
  "env": {
16
16
  "TANDEM_URL": "http://127.0.0.1:3479"
17
17
  }
18
18
  },
19
19
  "tandem-channel": {
20
20
  "command": "npx",
21
- "args": ["-y", "tandem-editor", "channel"],
21
+ "args": ["-y", "tandem-editor@0.15.0", "channel"],
22
22
  "env": {
23
23
  "TANDEM_URL": "http://127.0.0.1:3479"
24
24
  }
package/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.15.0] - 2026-07-07
11
+
12
+ ### Added
13
+
14
+ - **Setup diagnostics now surface at the moment of connection, and a connected agent can self-diagnose (#1174).** Three gaps closed: (1) the integration wizard's final step now probes `/health` after writing your AI's MCP config and watches live for the AI actually connecting (HTTP targets); stdio targets render honestly as not-applicable. (2) A new `tandem_diagnostics` MCP tool wraps the same checks `tandem doctor` runs, so a connected agent — including a Cowork VM that can't reach `localhost:3479` — can run diagnostics over its own transport. It's read-only and ungated (an escape hatch, like the other read tools). (3) The Cowork Settings banner now derives reachability from the on-disk workspace-install truth (reachable / unreachable / pending) rather than the bare `enabled` boolean.
15
+ - **License issuance Worker landed (#1116, ADR-040).** `infra/license-issuance-worker/` is the public seam that turns a paid Polar checkout into an Ed25519-signed license: it verifies the Standard-Webhooks (svix) signature Polar actually sends (the loopback-only server handler it supersedes used an invented, wrong scheme Polar never sends), mints/signs the license, records an issuance ledger, writes the update entitlement the sibling `infra/license-update-worker/` reads, and emails the license via Resend — all before any behavior-visible change to the shipped app (the run-time license gate stays off by default per ADR-040). Owner-deployed; see `docs/licensing-operations.md`.
16
+
17
+ ### Changed
18
+
19
+ - **Find & Replace rebuilt to the B4 design (#1171).** The bar now anchors to the top-right of the editor (was bottom-right) and slides in from above, sharing the motion of the new-tab / slash-menu chrome. Borderless inputs sit on the panel surface; the find row gains a search icon, a mono match count, and an Esc badge; a quieter controls strip carries the nav arrows and mono `Aa` / `\b` / `.*` toggles (accent-tinted when on), an expand chevron, and close. The replace row is collapsed by default (Replace All is the only filled-accent button), and Escape is two-stage — collapse the replace row, then close. A no-match now tints the find row to the error family. Every find/replace testid is preserved.
20
+ - **Cowork setup dialogs now explain what actually happens, and the published plugin is current (#1173).** The in-app Cowork dialogs (Settings, integration wizard, onboarding, admin-declined modal) now state plainly what gets installed, that adding the firewall rule needs administrator rights Tandem doesn't have, how to verify it worked, and that real-time push needs the desktop app. The stale `v0.8.0` string is gone (the dialogs are now version-agnostic), the published `.claude-plugin/plugin.json` manifest is refreshed, and the README gains a `#cowork` section the dialogs link to. A new `tests/plugin-manifest.test.ts` drift-guards the plugin manifest against `package.json`.
21
+
22
+ ### Fixed
23
+
24
+ - **Enabling Cowork no longer deletes its own firewall allow-rule (#1163).** The enable path added the "Tandem Cowork" allow rule and *then* ran orphan reconciliation — whose name-prefix scan matched the rule it had just added and deleted it, so every successful enable ended with no allow rule. Orphan cleanup now runs *before* the rule is added (the only sound fix, since the scan can't tell a fresh rule from a true orphan by name), and the stale-token workspace rewrite runs only after a successful add, so a fail-closed firewall failure never reaches a workspace write. Windows-only.
25
+ - **Re-importing the same `.docx` no longer accumulates duplicate private notes (#1166, #1150).** When flat offsets drift between imports (a content edit, a heading-prefix change), imported Word comments were re-keyed by offset and injected as fresh notes. Import now dedupes on the stable Word comment id as a secondary axis and re-anchors the existing record in place. Also fixes post-promotion reply injection (a reply now follows the promoted record) and strips stale `relRange`/`textSnapshot` when re-anchoring a drifted note.
26
+ - **The `.docx` conflict banner now acts on the right document (#1167).** The unsaved-`.docx` banner's "Reload from file" / "Keep my edits" silently no-op'd whenever the server's active doc differed from the focused tab (e.g. on an upgrade boot, `CHANGELOG.md` opens after session restore and becomes server-active). The handler now honors the body's `documentId` — validated against the open-document registry, so the file path still comes from the server's own registry, never request input — mirroring `POST /api/save`.
27
+ - **The auto-launcher no longer spams doomed `claude --resume` attempts (#1169).** The old 5-second grace window was shorter than the ~6s `--resume` needs to detect a missing conversation, so a stale session was never cleared — producing a loop of failing resumes until the circuit breaker tripped (~70s), while the same timer reset the backoff index before the process exited, neutering the exponential backoff. A per-spawn confirmation flag replaces the wall-clock window: a resumed spawn confirms after 30s, and a non-zero exit before confirmation clears the stale session unconditionally (guarded against deliberate signal kills).
28
+ - **Find & Replace match count no longer freezes at "No matches" (#1171).** `find`/`findNext`/`findPrev` dispatch meta-only ProseMirror transactions that change neither the doc nor the selection, so the bar's counter — driven by `update`/`selectionUpdate` — never bumped and the readout stayed empty even with matches present. It now bumps on the `transaction` event (a superset), gated on the bar being open. Fixed alongside the B4 redesign, whose no-match error tint made the latent bug glaring.
29
+ - **`npx -y tandem-editor mcp-stdio` no longer risks silently running a stale global install.** A bare package name in `npx`'s spec resolves to `*`, which `npm exec` happily matches to *any* already-installed copy — on a machine with a leftover pre-desktop global `tandem-editor` predating the `mcp-stdio` subcommand, Claude Desktop's `tandem` MCP server showed "Server disconnected" because npx ran the stale global instead of fetching the current version. The plugin manifest, the Cowork VM installer, and `tandem setup`'s Claude Desktop config now all pin the npx spec to this build's exact version (`tandem-editor@<version>`), which forces `npm exec` past any stale global. `tandem doctor` gained a companion check that warns (with a one-line fix) when a global `tandem-editor` differs from the running build, for the case of a hand-typed `npx tandem-editor` outside the pinned entries.
30
+
31
+ ### Internal
32
+
33
+ - Bump `undici` (6.26 → 6.27) and `esbuild` (0.28.0 → 0.28.1) in the `tests/tauri-driver` WebDriver harness (#1170, Dependabot). Test-tooling only — never shipped in `dist/`.
34
+ - Track the `.claude/hooks/` README (documenting the 18 wired hook scripts) and the `tests/tauri-driver` lockfile, both previously untracked — for discoverability and reproducible installs (#1168).
35
+
10
36
  ## [0.14.3] - 2026-06-22
11
37
 
12
38
  ### Added
package/README.md CHANGED
@@ -26,6 +26,7 @@ Tandem is approaching v1.0 and ships continuous improvements. See [CHANGELOG.md]
26
26
  - [For developers and contributors](#for-developers-and-contributors)
27
27
  - [Architecture overview](#architecture-overview)
28
28
  - [The MCP integration policy](#the-mcp-integration-policy)
29
+ - [Cowork](#cowork)
29
30
  - [MCP tools at a glance](#mcp-tools-at-a-glance)
30
31
  - [Channel push and real-time updates](#channel-push-and-real-time-updates)
31
32
  - [Development setup](#development-setup)
@@ -172,11 +173,30 @@ Client compatibility:
172
173
  | AI surface | Status |
173
174
  |---|---|
174
175
  | **Claude Code** (local CLI) | Default. Validated. Channel push supported. |
175
- | **Claude Desktop** (local app) | Supported via the Cowork bridge. Channel push N/A. |
176
+ | **Claude Desktop** (local app) | Supported via the [Cowork plugin bridge](#cowork) (Windows today). Request/response only — channel push N/A. |
176
177
  | **claude.ai web chat** | Not supported. Would require exposing the local server publicly via a tunnel, which is outside scope. |
177
178
  | **Other MCP-capable clients** (Cursor, Continue.dev, LM Studio, Ollama, …) | Best-effort, MCP-contract-compatible, not validated. |
178
179
  | **Non-MCP AIs** | Not supported today. **Local models** (Ollama / LM Studio via OpenAI-compatible endpoints) are committed for v1.0 ([ADR-039](docs/decisions.md#adr-039-non-mcp-model-providers-local-slice-v10-cloud-slice-v11), tracked in #1123); cloud providers (ChatGPT direct, Gemini direct) follow in v1.1. |
179
180
 
181
+ ### Cowork
182
+
183
+ [Cowork](https://www.anthropic.com/news/claude-code-on-the-web) is Claude Desktop's local agent mode — Claude runs in an isolated VM on your machine. Tandem connects to it through Claude's **plugin system**, but you don't add a marketplace or run any `/plugin` commands yourself.
184
+
185
+ - **How to enable (Windows desktop app):** open the integration wizard (Settings → AI Assistant, or "Set up" next to Cowork) and click **Enable Cowork**, or toggle it on in Settings → Network. Tandem writes the plugin entry into every detected Cowork workspace and adds a Windows firewall rule so the VM can reach the Tandem server on this computer. That firewall step needs admin once — without it, the VM can't connect.
186
+ - **Why it's automated, not a manual marketplace install:** inside the VM the plugin must point at `host.docker.internal:3479` and carry a per-machine secret auth token. A published marketplace plugin can't carry that token, so Tandem provisions the workspace entries directly. (The published `tandem@tandem-editor` marketplace plugin is for Claude Code running *on the host*, over `127.0.0.1` — see below.)
187
+ - **Verify:** in a Cowork session, ask Claude to open or list your documents — Tandem's tools should appear. If they don't, re-run Enable.
188
+ - **Real-time updates:** live annotation/chat push needs the Tandem desktop app and Claude Code's channel flag (see [Channel push](#channel-push-and-real-time-updates)); the Cowork connection itself is request/response.
189
+ - **macOS / Linux:** not yet — tracked in #316 / #317.
190
+
191
+ For Claude Code on the host, the published plugin can be added from the marketplace instead of the wizard:
192
+
193
+ ```bash
194
+ claude plugin marketplace add bloknayrb/tandem
195
+ claude plugin install tandem@tandem-editor
196
+ ```
197
+
198
+ This activates the MCP tools and the bundled skill. It does **not** auto-enable channel push (that still needs `--dangerously-load-development-channels`, see below).
199
+
180
200
  ### MCP tools at a glance
181
201
 
182
202
  28 active tools across five capability areas. Full reference: [docs/mcp-tools.md](docs/mcp-tools.md).
package/dist/cli/index.js CHANGED
@@ -289,6 +289,19 @@ function resolveChannelDist(env = process.env, exists = existsSync) {
289
289
  }
290
290
  return resolve2(PACKAGE_ROOT, "dist/channel/index.js");
291
291
  }
292
+ function resolveCliVersion() {
293
+ if (true) return "0.15.0";
294
+ if (typeof __APP_VERSION__ !== "undefined") return __APP_VERSION__;
295
+ for (const rel of ["../../package.json", "../../../package.json"]) {
296
+ try {
297
+ const pkg = JSON.parse(readFileSync2(resolve2(__dirname2, rel), "utf8"));
298
+ if (pkg.version) return pkg.version;
299
+ } catch {
300
+ }
301
+ }
302
+ console.error("[Tandem] Could not resolve CLI version for npx pin; using unpinned fallback");
303
+ return "0.0.0-unknown";
304
+ }
292
305
  function applyOpsForCli(create, opts) {
293
306
  return {
294
307
  create,
@@ -305,7 +318,7 @@ function buildMcpEntries(channelPath, opts = {}) {
305
318
  }
306
319
  tandemEntry = {
307
320
  command: "npx",
308
- args: ["-y", "tandem-editor", "mcp-stdio"],
321
+ args: ["-y", `tandem-editor@${CLI_VERSION}`, "mcp-stdio"],
309
322
  env
310
323
  };
311
324
  } else {
@@ -660,7 +673,7 @@ async function applyConfigWithToken(token, opts = {}) {
660
673
  }
661
674
  return { updated, errors };
662
675
  }
663
- var __dirname2, PACKAGE_ROOT, CHANNEL_DIST, MCP_URL, MAX_CONFIG_BYTES, PathRejectedError, DEFAULT_ROOTS_CACHE, MSIX_PACKAGE_PATTERN, BUNDLED_SKILL_VERSION;
676
+ var __dirname2, PACKAGE_ROOT, CHANNEL_DIST, MCP_URL, CLI_VERSION, MAX_CONFIG_BYTES, PathRejectedError, DEFAULT_ROOTS_CACHE, MSIX_PACKAGE_PATTERN, BUNDLED_SKILL_VERSION;
664
677
  var init_apply = __esm({
665
678
  "src/server/integrations/apply.ts"() {
666
679
  "use strict";
@@ -677,6 +690,7 @@ var init_apply = __esm({
677
690
  })();
678
691
  CHANNEL_DIST = resolveChannelDist();
679
692
  MCP_URL = `http://127.0.0.1:${DEFAULT_MCP_PORT}`;
693
+ CLI_VERSION = resolveCliVersion();
680
694
  MAX_CONFIG_BYTES = 5 * 1024 * 1024;
681
695
  PathRejectedError = class extends Error {
682
696
  constructor(path8, reason, message) {
@@ -2433,10 +2447,13 @@ var init_lockfile = __esm({
2433
2447
  // src/cli/doctor.ts
2434
2448
  var doctor_exports = {};
2435
2449
  __export(doctor_exports, {
2450
+ evaluateStaleGlobal: () => evaluateStaleGlobal,
2451
+ globalTandemEditorVersion: () => globalTandemEditorVersion,
2436
2452
  runDoctor: () => runDoctor,
2437
2453
  runDoctorCli: () => runDoctorCli,
2438
2454
  summarizeDoctorResults: () => summarizeDoctorResults
2439
2455
  });
2456
+ import { execFile as execFile3 } from "child_process";
2440
2457
  import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
2441
2458
  import { request } from "http";
2442
2459
  import { createConnection as createConnection2 } from "net";
@@ -2812,6 +2829,58 @@ function summarizeDoctorResults(failures, warnings) {
2812
2829
  return `${warnings} warning(s) \u2014 Tandem should work, but check the items above.`;
2813
2830
  return "All checks passed. Tandem is ready.";
2814
2831
  }
2832
+ function globalTandemEditorVersion() {
2833
+ return new Promise((resolve4) => {
2834
+ execFile3(
2835
+ "npm",
2836
+ ["ls", "-g", "--depth=0", "--json", "tandem-editor"],
2837
+ { shell: true, windowsHide: true, timeout: 4e3, maxBuffer: 8 * 1024 * 1024 },
2838
+ (_err, stdout) => {
2839
+ if (!stdout || stdout.trim().length === 0) {
2840
+ resolve4(null);
2841
+ return;
2842
+ }
2843
+ try {
2844
+ const parsed = JSON.parse(stdout);
2845
+ resolve4(parsed.dependencies?.["tandem-editor"]?.version ?? null);
2846
+ } catch {
2847
+ resolve4(null);
2848
+ }
2849
+ }
2850
+ );
2851
+ });
2852
+ }
2853
+ function evaluateStaleGlobal(bundled, globalVersion) {
2854
+ if (globalVersion === null) {
2855
+ return null;
2856
+ }
2857
+ if (globalVersion === bundled) {
2858
+ return { status: "pass", message: `Global tandem-editor@${globalVersion} matches this build` };
2859
+ }
2860
+ return {
2861
+ status: "warn",
2862
+ message: `Global tandem-editor@${globalVersion} differs from this build (${bundled}) \u2014 a stale global can break \`npx tandem-editor\` (e.g. Claude Desktop's MCP bridge).`,
2863
+ fix: "npm uninstall -g tandem-editor (or: npm install -g tandem-editor@latest)",
2864
+ data: { globalVersion, bundledVersion: bundled }
2865
+ };
2866
+ }
2867
+ async function checkStaleGlobal(r) {
2868
+ const bundled = true ? "0.15.0" : null;
2869
+ if (!bundled) return;
2870
+ let globalVersion;
2871
+ try {
2872
+ globalVersion = await globalTandemEditorVersion();
2873
+ } catch {
2874
+ return;
2875
+ }
2876
+ const result = evaluateStaleGlobal(bundled, globalVersion);
2877
+ if (!result) return;
2878
+ if (result.status === "pass") {
2879
+ r.pass(result.message);
2880
+ } else {
2881
+ r.warn(result.message, result.fix, result.data);
2882
+ }
2883
+ }
2815
2884
  async function runDoctor(opts = {}) {
2816
2885
  const wsPort = opts.wsPort ?? DEFAULT_WS_PORT;
2817
2886
  const mcpPort = opts.mcpPort ?? DEFAULT_MCP_PORT;
@@ -2821,6 +2890,7 @@ async function runDoctor(opts = {}) {
2821
2890
  await r.check("mcp-json", () => checkMcpJson(r));
2822
2891
  await r.check("user-mcp-config", () => checkUserMcpConfig(r));
2823
2892
  await r.check("annotation-store", () => checkAnnotationStore(r));
2893
+ await r.check("stale-global", () => checkStaleGlobal(r));
2824
2894
  const { mcp } = await r.check("ports", () => checkPorts(r, wsPort, mcpPort));
2825
2895
  if (mcp) {
2826
2896
  const healthy = await r.check("health", () => checkHealth(r, mcpPort));
@@ -3384,6 +3454,13 @@ var init_positions2 = __esm({
3384
3454
  }
3385
3455
  });
3386
3456
 
3457
+ // src/server/file-io/docx-comment-id.ts
3458
+ var init_docx_comment_id = __esm({
3459
+ "src/server/file-io/docx-comment-id.ts"() {
3460
+ "use strict";
3461
+ }
3462
+ });
3463
+
3387
3464
  // src/server/file-io/docx-walker.ts
3388
3465
  import { parseDocument as parseDocument2 } from "htmlparser2";
3389
3466
  var init_docx_walker = __esm({
@@ -3405,6 +3482,7 @@ var init_docx_comments = __esm({
3405
3482
  init_types3();
3406
3483
  init_schema();
3407
3484
  init_positions2();
3485
+ init_docx_comment_id();
3408
3486
  init_docx_walker();
3409
3487
  }
3410
3488
  });
@@ -3424,6 +3502,7 @@ var init_docx_comment_export = __esm({
3424
3502
  init_sanitize();
3425
3503
  init_document_model();
3426
3504
  init_positions2();
3505
+ init_docx_comment_id();
3427
3506
  }
3428
3507
  });
3429
3508
 
@@ -3838,7 +3917,7 @@ process.once("unhandledRejection", (reason) => {
3838
3917
  `);
3839
3918
  process.exit(1);
3840
3919
  });
3841
- var version = true ? "0.14.3" : "0.0.0-dev";
3920
+ var version = true ? "0.15.0" : "0.0.0-dev";
3842
3921
  var args = process.argv.slice(2);
3843
3922
  if (args.includes("--help") || args.includes("-h")) {
3844
3923
  console.log(`tandem v${version}