tandem-editor 0.2.6 → 0.2.8
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 +75 -0
- package/dist/cli/index.js +1 -1
- package/dist/server/index.js +116 -59
- package/dist/server/index.js.map +1 -1
- package/package.json +122 -121
- package/sample/demo-script.md +23 -23
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Tandem will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.8] - 2026-04-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- CHANGELOG.md opens as the active tab on first startup after an npm update
|
|
13
|
+
- `checkVersionChange` helper tracks version transitions via `last-seen-version` file
|
|
14
|
+
- CHANGELOG.md now ships in the npm package
|
|
15
|
+
|
|
16
|
+
## [0.2.7] - 2026-04-05
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Force-reload (`tandem_open` with `force: true`) now clears Y.Doc in-place instead of destroying the Hocuspocus room — sidebar, observers, and connections survive
|
|
21
|
+
- TOCTOU fix: session deletion moved after successful reload transaction
|
|
22
|
+
- Observer ownership table corrected in architecture docs
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- 4 new tests for force-reload (annotation clearing, awareness clearing, .txt reload, metadata)
|
|
27
|
+
|
|
28
|
+
## [0.2.6] - 2026-04-05
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Demo script rewritten to be self-referential for recording
|
|
33
|
+
- Observer ownership documentation added to architecture.md
|
|
34
|
+
|
|
35
|
+
## [0.2.5] - 2026-04-05
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- `tandem setup` Claude Code MCP config path updated
|
|
40
|
+
|
|
41
|
+
## [0.2.4] - 2026-04-05
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Security audit findings (DNS rebinding, CORS, input validation)
|
|
46
|
+
|
|
47
|
+
## [0.2.3] - 2026-04-05
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- `tandem setup` now writes Claude Code MCP config to `~/.claude.json` instead of `~/.claude/mcp_settings.json`, which Claude Code no longer reads
|
|
52
|
+
|
|
53
|
+
## [0.2.2] - 2025-04-05
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- Silent failure review findings
|
|
58
|
+
|
|
59
|
+
## [0.2.1] - 2025-04-05
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- Full security audit — 25 findings across 7 categories (#172)
|
|
64
|
+
|
|
65
|
+
## [0.2.0] - 2025-04-04
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- Initial public release on npm as `tandem-editor`
|
|
70
|
+
- 30 MCP tools for collaborative document editing
|
|
71
|
+
- Multi-document tabs with CRDT-anchored annotations
|
|
72
|
+
- Chat sidebar with real-time channel push
|
|
73
|
+
- Support for .md, .docx, .txt, .html files
|
|
74
|
+
- `tandem` CLI with `setup` and `start` commands
|
|
75
|
+
- Claude Code skill auto-installation
|
package/dist/cli/index.js
CHANGED
|
@@ -337,7 +337,7 @@ var init_start = __esm({
|
|
|
337
337
|
|
|
338
338
|
// src/cli/index.ts
|
|
339
339
|
import updateNotifier from "update-notifier";
|
|
340
|
-
var version = true ? "0.2.
|
|
340
|
+
var version = true ? "0.2.8" : "0.0.0-dev";
|
|
341
341
|
updateNotifier({ pkg: { name: "tandem-editor", version } }).notify();
|
|
342
342
|
var args = process.argv.slice(2);
|
|
343
343
|
if (args.includes("--help") || args.includes("-h")) {
|
package/dist/server/index.js
CHANGED
|
@@ -60,9 +60,6 @@ function getOrCreateDocument(name) {
|
|
|
60
60
|
}
|
|
61
61
|
return doc;
|
|
62
62
|
}
|
|
63
|
-
function removeDocument(name) {
|
|
64
|
-
return documents.delete(name);
|
|
65
|
-
}
|
|
66
63
|
async function startHocuspocus(port) {
|
|
67
64
|
hocuspocusInstance = new Hocuspocus({
|
|
68
65
|
port,
|
|
@@ -119,9 +116,6 @@ async function startHocuspocus(port) {
|
|
|
119
116
|
}
|
|
120
117
|
return hocuspocusInstance;
|
|
121
118
|
}
|
|
122
|
-
function getHocuspocus() {
|
|
123
|
-
return hocuspocusInstance;
|
|
124
|
-
}
|
|
125
119
|
var hocuspocusInstance, documents, shouldKeepDocument, onDocSwapped, onDocUnloaded;
|
|
126
120
|
var init_provider = __esm({
|
|
127
121
|
"src/server/yjs/provider.ts"() {
|
|
@@ -220,12 +214,13 @@ function freePortUnix(port) {
|
|
|
220
214
|
}
|
|
221
215
|
}
|
|
222
216
|
}
|
|
223
|
-
var paths, SESSION_DIR;
|
|
217
|
+
var paths, SESSION_DIR, LAST_SEEN_VERSION_FILE;
|
|
224
218
|
var init_platform = __esm({
|
|
225
219
|
"src/server/platform.ts"() {
|
|
226
220
|
"use strict";
|
|
227
221
|
paths = envPaths("tandem", { suffix: "" });
|
|
228
222
|
SESSION_DIR = path.join(paths.data, "sessions");
|
|
223
|
+
LAST_SEEN_VERSION_FILE = path.join(paths.data, "last-seen-version");
|
|
229
224
|
}
|
|
230
225
|
});
|
|
231
226
|
|
|
@@ -2827,7 +2822,25 @@ async function openFileByPath(filePath, options) {
|
|
|
2827
2822
|
};
|
|
2828
2823
|
}
|
|
2829
2824
|
if (forceReload) {
|
|
2830
|
-
|
|
2825
|
+
const doc2 = getDocument(id) ?? getOrCreateDocument(id);
|
|
2826
|
+
const fileName2 = path5.basename(resolved);
|
|
2827
|
+
await clearAndReload(id, doc2, resolved, format, existing);
|
|
2828
|
+
addDoc(id, { id, filePath: resolved, format, readOnly, source: "file" });
|
|
2829
|
+
setActiveDocId(id);
|
|
2830
|
+
broadcastOpenDocs();
|
|
2831
|
+
ensureAutoSave();
|
|
2832
|
+
return {
|
|
2833
|
+
...buildResult(doc2, {
|
|
2834
|
+
documentId: id,
|
|
2835
|
+
filePath: resolved,
|
|
2836
|
+
fileName: fileName2,
|
|
2837
|
+
format,
|
|
2838
|
+
readOnly,
|
|
2839
|
+
source: "file",
|
|
2840
|
+
restoredFromSession: false
|
|
2841
|
+
}),
|
|
2842
|
+
forceReloaded: true
|
|
2843
|
+
};
|
|
2831
2844
|
}
|
|
2832
2845
|
const doc = getOrCreateDocument(id);
|
|
2833
2846
|
const fileName = path5.basename(resolved);
|
|
@@ -2868,7 +2881,7 @@ async function openFileByPath(filePath, options) {
|
|
|
2868
2881
|
source: "file",
|
|
2869
2882
|
restoredFromSession
|
|
2870
2883
|
}),
|
|
2871
|
-
forceReloaded:
|
|
2884
|
+
forceReloaded: false
|
|
2872
2885
|
};
|
|
2873
2886
|
}
|
|
2874
2887
|
async function openFileFromContent(fileName, content) {
|
|
@@ -2908,57 +2921,64 @@ async function openFileFromContent(fileName, content) {
|
|
|
2908
2921
|
restoredFromSession: false
|
|
2909
2922
|
});
|
|
2910
2923
|
}
|
|
2911
|
-
async function
|
|
2912
|
-
console.error(`[Tandem]
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
const hpDoc = hp.documents.get(id);
|
|
2932
|
-
if (hpDoc) {
|
|
2933
|
-
hp.documents.delete(id);
|
|
2934
|
-
hpDoc.destroy();
|
|
2935
|
-
}
|
|
2936
|
-
hp.loadingDocuments.delete(id);
|
|
2937
|
-
}
|
|
2938
|
-
} catch (err) {
|
|
2939
|
-
errors++;
|
|
2940
|
-
console.error(`[Tandem] forceCloseDocument: Hocuspocus cleanup failed for ${id}:`, err);
|
|
2941
|
-
}
|
|
2942
|
-
try {
|
|
2943
|
-
const oldDoc = getDocument(id);
|
|
2944
|
-
if (oldDoc) {
|
|
2945
|
-
oldDoc.destroy();
|
|
2946
|
-
removeDocument(id);
|
|
2947
|
-
}
|
|
2948
|
-
} catch (err) {
|
|
2949
|
-
errors++;
|
|
2950
|
-
console.error(`[Tandem] forceCloseDocument: Y.Doc removal failed for ${id}:`, err);
|
|
2924
|
+
async function clearAndReload(id, doc, filePath, format, existing) {
|
|
2925
|
+
console.error(`[Tandem] clearAndReload: reloading ${id} from disk`);
|
|
2926
|
+
const isDocx = format === "docx";
|
|
2927
|
+
let preparedHtml;
|
|
2928
|
+
let preparedComments;
|
|
2929
|
+
let preparedContent;
|
|
2930
|
+
if (isDocx) {
|
|
2931
|
+
const buffer3 = await fs3.readFile(filePath);
|
|
2932
|
+
[preparedHtml, preparedComments] = await Promise.all([
|
|
2933
|
+
loadDocx(buffer3),
|
|
2934
|
+
extractDocxComments(buffer3).catch((err) => {
|
|
2935
|
+
console.error(
|
|
2936
|
+
"[docx-comments] Comment extraction failed; document will reload without imported comments:",
|
|
2937
|
+
err
|
|
2938
|
+
);
|
|
2939
|
+
return [];
|
|
2940
|
+
})
|
|
2941
|
+
]);
|
|
2942
|
+
} else {
|
|
2943
|
+
preparedContent = await fs3.readFile(filePath, "utf-8");
|
|
2951
2944
|
}
|
|
2952
2945
|
try {
|
|
2953
|
-
|
|
2946
|
+
doc.transact(() => {
|
|
2947
|
+
const annotations = doc.getMap(Y_MAP_ANNOTATIONS);
|
|
2948
|
+
annotations.forEach((_, k) => annotations.delete(k));
|
|
2949
|
+
const awareness = doc.getMap(Y_MAP_AWARENESS);
|
|
2950
|
+
awareness.forEach((_, k) => awareness.delete(k));
|
|
2951
|
+
const userAwareness = doc.getMap(Y_MAP_USER_AWARENESS);
|
|
2952
|
+
userAwareness.forEach((_, k) => userAwareness.delete(k));
|
|
2953
|
+
if (isDocx && preparedHtml !== void 0) {
|
|
2954
|
+
htmlToYDoc(doc, preparedHtml);
|
|
2955
|
+
if (preparedComments && preparedComments.length > 0) {
|
|
2956
|
+
injectCommentsAsAnnotations(doc, preparedComments);
|
|
2957
|
+
}
|
|
2958
|
+
} else if (format === "md" && preparedContent !== void 0) {
|
|
2959
|
+
loadMarkdown(doc, preparedContent);
|
|
2960
|
+
} else if (preparedContent !== void 0) {
|
|
2961
|
+
populateYDoc(doc, preparedContent);
|
|
2962
|
+
}
|
|
2963
|
+
const meta = doc.getMap(Y_MAP_DOCUMENT_META);
|
|
2964
|
+
meta.set("readOnly", isDocx);
|
|
2965
|
+
meta.set("format", format);
|
|
2966
|
+
meta.set("documentId", id);
|
|
2967
|
+
meta.set("fileName", path5.basename(filePath));
|
|
2968
|
+
meta.set(Y_MAP_SAVED_AT_VERSION, Date.now());
|
|
2969
|
+
}, MCP_ORIGIN);
|
|
2970
|
+
attachObservers(id, doc);
|
|
2954
2971
|
} catch (err) {
|
|
2955
|
-
|
|
2956
|
-
|
|
2972
|
+
console.error(
|
|
2973
|
+
`[Tandem] clearAndReload: failed for ${id} (format=${format}). Y.Doc may be in a partially cleared state:`,
|
|
2974
|
+
err
|
|
2975
|
+
);
|
|
2976
|
+
throw err;
|
|
2957
2977
|
}
|
|
2958
|
-
await
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
);
|
|
2978
|
+
await deleteSession(existing.filePath).catch((err) => {
|
|
2979
|
+
console.error(`[Tandem] clearAndReload: deleteSession failed for ${id}:`, err);
|
|
2980
|
+
});
|
|
2981
|
+
console.error(`[Tandem] clearAndReload: complete for ${id}`);
|
|
2962
2982
|
}
|
|
2963
2983
|
function initSavedBaseline(doc) {
|
|
2964
2984
|
const meta = doc.getMap(Y_MAP_DOCUMENT_META);
|
|
@@ -3010,6 +3030,10 @@ var init_file_opener = __esm({
|
|
|
3010
3030
|
init_constants();
|
|
3011
3031
|
init_queue();
|
|
3012
3032
|
init_file_io();
|
|
3033
|
+
init_markdown();
|
|
3034
|
+
init_docx();
|
|
3035
|
+
init_docx_html();
|
|
3036
|
+
init_docx_comments();
|
|
3013
3037
|
init_manager();
|
|
3014
3038
|
init_document_model();
|
|
3015
3039
|
init_document_service();
|
|
@@ -3554,7 +3578,7 @@ var init_launcher = __esm({
|
|
|
3554
3578
|
});
|
|
3555
3579
|
|
|
3556
3580
|
// src/server/index.ts
|
|
3557
|
-
import
|
|
3581
|
+
import path10 from "path";
|
|
3558
3582
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3559
3583
|
|
|
3560
3584
|
// src/server/mcp/server.ts
|
|
@@ -5655,6 +5679,26 @@ init_constants();
|
|
|
5655
5679
|
init_manager();
|
|
5656
5680
|
init_platform();
|
|
5657
5681
|
|
|
5682
|
+
// src/server/version-check.ts
|
|
5683
|
+
import fs6 from "fs/promises";
|
|
5684
|
+
import path9 from "path";
|
|
5685
|
+
async function checkVersionChange(currentVersion, versionFilePath) {
|
|
5686
|
+
let storedVersion = null;
|
|
5687
|
+
try {
|
|
5688
|
+
storedVersion = (await fs6.readFile(versionFilePath, "utf-8")).trim();
|
|
5689
|
+
} catch (err) {
|
|
5690
|
+
if (err.code !== "ENOENT") {
|
|
5691
|
+
console.error("[Tandem] Failed to read last-seen-version:", err);
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
const result = storedVersion === null ? "first-install" : storedVersion === currentVersion ? "current" : "upgraded";
|
|
5695
|
+
if (result !== "current") {
|
|
5696
|
+
await fs6.mkdir(path9.dirname(versionFilePath), { recursive: true });
|
|
5697
|
+
await fs6.writeFile(versionFilePath, currentVersion, "utf-8");
|
|
5698
|
+
}
|
|
5699
|
+
return result;
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5658
5702
|
// src/server/error-filter.ts
|
|
5659
5703
|
function isKnownHocuspocusError(err) {
|
|
5660
5704
|
if (!(err instanceof Error)) return false;
|
|
@@ -5871,9 +5915,22 @@ async function main() {
|
|
|
5871
5915
|
})
|
|
5872
5916
|
]);
|
|
5873
5917
|
httpServer = srv;
|
|
5918
|
+
try {
|
|
5919
|
+
const versionStatus = await checkVersionChange(APP_VERSION, LAST_SEEN_VERSION_FILE);
|
|
5920
|
+
if (versionStatus === "upgraded") {
|
|
5921
|
+
const changelogPath = path10.resolve(
|
|
5922
|
+
path10.dirname(fileURLToPath2(import.meta.url)),
|
|
5923
|
+
"../../CHANGELOG.md"
|
|
5924
|
+
);
|
|
5925
|
+
await openFileByPath(changelogPath);
|
|
5926
|
+
console.error(`[Tandem] Opened CHANGELOG.md (upgraded to v${APP_VERSION})`);
|
|
5927
|
+
}
|
|
5928
|
+
} catch (err) {
|
|
5929
|
+
console.error("[Tandem] Version check / changelog open failed (non-fatal):", err);
|
|
5930
|
+
}
|
|
5874
5931
|
if (getOpenDocs().size === 0 && !process.env.TANDEM_NO_SAMPLE) {
|
|
5875
|
-
const samplePath =
|
|
5876
|
-
|
|
5932
|
+
const samplePath = path10.resolve(
|
|
5933
|
+
path10.dirname(fileURLToPath2(import.meta.url)),
|
|
5877
5934
|
"../../sample/welcome.md"
|
|
5878
5935
|
);
|
|
5879
5936
|
openFileByPath(samplePath).then(() => {
|