tandem-editor 0.16.0 → 0.18.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/.claude-plugin/plugin.json +4 -4
- package/CHANGELOG.md +38 -1
- package/dist/cli/index.js +486 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/{CoworkSettings-B4LapjhG.js → CoworkSettings-Brg8F0cb.js} +1 -1
- package/dist/client/assets/{dist-CJtsfiU2.js → dist-CHppK3gQ.js} +1 -1
- package/dist/client/assets/index-Boow3rxN.css +1 -0
- package/dist/client/assets/index-BymYYAwj.js +271 -0
- package/dist/client/assets/{prod-p35ShryZ.js → prod-Pu_njNLv.js} +1 -1
- package/dist/client/index.html +21 -5
- package/dist/monitor/index.js +6 -3
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +324 -14
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/tandem/SKILL.md +2 -2
- package/dist/client/assets/index-BIEOrpU_.js +0 -270
- package/dist/client/assets/index-DdjGUhNd.css +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -290,7 +290,7 @@ function resolveChannelDist(env = process.env, exists = existsSync) {
|
|
|
290
290
|
return resolve2(PACKAGE_ROOT, "dist/channel/index.js");
|
|
291
291
|
}
|
|
292
292
|
function resolveCliVersion() {
|
|
293
|
-
if (true) return "0.
|
|
293
|
+
if (true) return "0.18.0";
|
|
294
294
|
if (typeof __APP_VERSION__ !== "undefined") return __APP_VERSION__;
|
|
295
295
|
for (const rel of ["../../package.json", "../../../package.json"]) {
|
|
296
296
|
try {
|
|
@@ -1238,7 +1238,7 @@ function printPushStatus() {
|
|
|
1238
1238
|
|
|
1239
1239
|
` : "";
|
|
1240
1240
|
console.error(
|
|
1241
|
-
"\n\x1B[1mReal-time push notifications:\x1B[0m\n" + (channelRegistered ? " \x1B[32mEnabled\x1B[0m \u2014 the channel shim is registered; Claude Code receives events in real time.\n Relaunch any Claude Code session you started manually so it picks up the new server.\n\n" : " \x1B[33mUnavailable\x1B[0m \u2014 dist/channel/index.js not found; Claude Code will poll via tandem_checkInbox.\n Run 'npm run build' and re-run setup to enable push.\n\n") + " A Tandem plugin is also published (skill + MCP
|
|
1241
|
+
"\n\x1B[1mReal-time push notifications:\x1B[0m\n" + (channelRegistered ? " \x1B[32mEnabled\x1B[0m \u2014 the channel shim is registered; Claude Code receives events in real time.\n Relaunch any Claude Code session you started manually so it picks up the new server.\n\n" : " \x1B[33mUnavailable\x1B[0m \u2014 dist/channel/index.js not found; Claude Code will poll via tandem_checkInbox.\n Run 'npm run build' and re-run setup to enable push.\n\n") + " A Tandem plugin is also published (skill + MCP + a real-time monitor that\n activates on Claude Code 2.1.212+ and needs no --dangerously-... flag):\n\n claude plugin marketplace add bloknayrb/tandem\n claude plugin install tandem@tandem-editor\n\n" + devInstructions
|
|
1242
1242
|
);
|
|
1243
1243
|
}
|
|
1244
1244
|
var init_setup = __esm({
|
|
@@ -1822,7 +1822,7 @@ var init_types2 = __esm({
|
|
|
1822
1822
|
|
|
1823
1823
|
// src/shared/types.ts
|
|
1824
1824
|
import { z } from "zod";
|
|
1825
|
-
var AnnotationTypeSchema, AnnotationStatusSchema, HighlightColorSchema, SeveritySchema, TandemModeSchema, AuthorSchema, ReplyAuthorSchema, AnnotationActionSchema, ExportFormatSchema, DocumentFormatSchema, ToolErrorCodeSchema, ChannelErrorCodeSchema, CHANNEL_CONNECT_FAILED;
|
|
1825
|
+
var AnnotationTypeSchema, AnnotationStatusSchema, HighlightColorSchema, SeveritySchema, TandemModeSchema, AuthorSchema, ReplyAuthorSchema, AnnotationActionSchema, ExportFormatSchema, DocumentFormatSchema, ToolErrorCodeSchema, ChannelErrorCodeSchema, CHANNEL_CONNECT_FAILED, MONITOR_CONNECT_FAILED;
|
|
1826
1826
|
var init_types3 = __esm({
|
|
1827
1827
|
"src/shared/types.ts"() {
|
|
1828
1828
|
"use strict";
|
|
@@ -1852,6 +1852,7 @@ var init_types3 = __esm({
|
|
|
1852
1852
|
]);
|
|
1853
1853
|
ChannelErrorCodeSchema = z.enum(["CHANNEL_CONNECT_FAILED", "MONITOR_CONNECT_FAILED"]);
|
|
1854
1854
|
CHANNEL_CONNECT_FAILED = "CHANNEL_CONNECT_FAILED";
|
|
1855
|
+
MONITOR_CONNECT_FAILED = "MONITOR_CONNECT_FAILED";
|
|
1855
1856
|
}
|
|
1856
1857
|
});
|
|
1857
1858
|
|
|
@@ -2155,6 +2156,63 @@ function refreshMode(tandemUrl, logPrefix) {
|
|
|
2155
2156
|
cachedModeFailedAt = Date.now();
|
|
2156
2157
|
});
|
|
2157
2158
|
}
|
|
2159
|
+
async function flushFinalAwareness(tandemUrl, logPrefix = "[Tandem]") {
|
|
2160
|
+
if (shutdownTimers.awarenessTimer) clearTimeout(shutdownTimers.awarenessTimer);
|
|
2161
|
+
if (shutdownTimers.clearAwarenessTimer) clearTimeout(shutdownTimers.clearAwarenessTimer);
|
|
2162
|
+
if (outstandingAwareness.size > 0) {
|
|
2163
|
+
await Promise.allSettled(outstandingAwareness);
|
|
2164
|
+
}
|
|
2165
|
+
if (shutdownTimers.lastDocumentId === null) return true;
|
|
2166
|
+
try {
|
|
2167
|
+
const res = await fetchWithTimeout(
|
|
2168
|
+
`${tandemUrl}${API_CHANNEL_AWARENESS}`,
|
|
2169
|
+
{
|
|
2170
|
+
method: "POST",
|
|
2171
|
+
headers: { "Content-Type": "application/json" },
|
|
2172
|
+
body: JSON.stringify({
|
|
2173
|
+
documentId: shutdownTimers.lastDocumentId,
|
|
2174
|
+
status: "idle",
|
|
2175
|
+
active: false
|
|
2176
|
+
})
|
|
2177
|
+
},
|
|
2178
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
2179
|
+
);
|
|
2180
|
+
if (!res.ok) {
|
|
2181
|
+
console.error(`${logPrefix} Shutdown awareness clear returned ${res.status}`);
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
return true;
|
|
2185
|
+
} catch (err) {
|
|
2186
|
+
console.error(
|
|
2187
|
+
`${logPrefix} Shutdown awareness clear failed:`,
|
|
2188
|
+
describeFetchError(
|
|
2189
|
+
err,
|
|
2190
|
+
`${API_CHANNEL_AWARENESS} shutdown`,
|
|
2191
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
2192
|
+
)
|
|
2193
|
+
);
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
function _resetSseConsumerStateForTests() {
|
|
2198
|
+
cachedMode = TANDEM_MODE_DEFAULT;
|
|
2199
|
+
cachedModeAt = 0;
|
|
2200
|
+
cachedModeFailedAt = 0;
|
|
2201
|
+
_modeRefreshInFlight = null;
|
|
2202
|
+
shutdownTimers.awarenessTimer = null;
|
|
2203
|
+
shutdownTimers.clearAwarenessTimer = null;
|
|
2204
|
+
shutdownTimers.lastDocumentId = null;
|
|
2205
|
+
outstandingAwareness.clear();
|
|
2206
|
+
}
|
|
2207
|
+
function _setLastDocumentIdForTests(id) {
|
|
2208
|
+
shutdownTimers.lastDocumentId = id;
|
|
2209
|
+
}
|
|
2210
|
+
function _getLastDocumentIdForTests() {
|
|
2211
|
+
return shutdownTimers.lastDocumentId;
|
|
2212
|
+
}
|
|
2213
|
+
function _addOutstandingAwarenessForTests(p) {
|
|
2214
|
+
trackAwareness(p);
|
|
2215
|
+
}
|
|
2158
2216
|
var AWARENESS_DEBOUNCE_MS, AWARENESS_CLEAR_MS, MODE_CACHE_TTL_MS, STABLE_CONNECTION_MS, RETRY_MAX_DELAY_MS, shutdownTimers, outstandingAwareness, cachedMode, cachedModeAt, cachedModeFailedAt, _modeRefreshInFlight;
|
|
2159
2217
|
var init_sse_consumer = __esm({
|
|
2160
2218
|
"src/shared/sse-consumer.ts"() {
|
|
@@ -2417,6 +2475,114 @@ var init_channel = __esm({
|
|
|
2417
2475
|
}
|
|
2418
2476
|
});
|
|
2419
2477
|
|
|
2478
|
+
// src/monitor/run.ts
|
|
2479
|
+
var run_exports = {};
|
|
2480
|
+
__export(run_exports, {
|
|
2481
|
+
_addOutstandingAwarenessForTests: () => _addOutstandingAwarenessForTests2,
|
|
2482
|
+
_getLastDocumentIdForTests: () => _getLastDocumentIdForTests2,
|
|
2483
|
+
_monitorTestExports: () => _monitorTestExports,
|
|
2484
|
+
_resetMonitorStateForTests: () => _resetMonitorStateForTests,
|
|
2485
|
+
_setLastDocumentIdForTests: () => _setLastDocumentIdForTests2,
|
|
2486
|
+
connectAndStream: () => connectAndStream,
|
|
2487
|
+
getCachedMode: () => getCachedMode2,
|
|
2488
|
+
getModeSync: () => getModeSync2,
|
|
2489
|
+
main: () => main,
|
|
2490
|
+
shutdownMonitor: () => shutdownMonitor
|
|
2491
|
+
});
|
|
2492
|
+
function buildOptions() {
|
|
2493
|
+
return {
|
|
2494
|
+
tandemUrl: TANDEM_URL,
|
|
2495
|
+
logPrefix: LOG_PREFIX,
|
|
2496
|
+
errorCode: MONITOR_CONNECT_FAILED,
|
|
2497
|
+
onEvent: (event) => {
|
|
2498
|
+
const content = formatEventContent(event).replace(/\n/g, " ");
|
|
2499
|
+
process.stdout.write(content + "\n");
|
|
2500
|
+
},
|
|
2501
|
+
onExhaustion: () => {
|
|
2502
|
+
process.stdout.write(
|
|
2503
|
+
"Tandem monitor disconnected \u2014 restart Tandem to restore real-time events\n"
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
async function main() {
|
|
2509
|
+
installShutdownHandlers();
|
|
2510
|
+
installStdoutErrorHandler();
|
|
2511
|
+
console.error(`${LOG_PREFIX} Tandem monitor starting (server: ${TANDEM_URL})`);
|
|
2512
|
+
await getCachedMode2().catch(() => {
|
|
2513
|
+
});
|
|
2514
|
+
await runEventConsumer(buildOptions());
|
|
2515
|
+
}
|
|
2516
|
+
async function connectAndStream(lastEventId, onEventId, onStable = () => {
|
|
2517
|
+
}) {
|
|
2518
|
+
return connectAndStreamOnce(buildOptions(), lastEventId, { onEventId, onStable });
|
|
2519
|
+
}
|
|
2520
|
+
async function shutdownMonitor(signal) {
|
|
2521
|
+
console.error(`${LOG_PREFIX} Received ${signal}, clearing awareness and exiting`);
|
|
2522
|
+
const ok = await flushFinalAwareness(TANDEM_URL, LOG_PREFIX);
|
|
2523
|
+
process.exit(ok ? 0 : 1);
|
|
2524
|
+
}
|
|
2525
|
+
function _setLastDocumentIdForTests2(id) {
|
|
2526
|
+
_setLastDocumentIdForTests(id);
|
|
2527
|
+
}
|
|
2528
|
+
function _getLastDocumentIdForTests2() {
|
|
2529
|
+
return _getLastDocumentIdForTests();
|
|
2530
|
+
}
|
|
2531
|
+
function _addOutstandingAwarenessForTests2(p) {
|
|
2532
|
+
_addOutstandingAwarenessForTests(p);
|
|
2533
|
+
}
|
|
2534
|
+
function installShutdownHandlers() {
|
|
2535
|
+
if (IS_VITEST) return;
|
|
2536
|
+
const handler = (signal) => {
|
|
2537
|
+
shutdownMonitor(signal).catch((err) => {
|
|
2538
|
+
console.error(`${LOG_PREFIX} Shutdown handler failed:`, err);
|
|
2539
|
+
process.exit(1);
|
|
2540
|
+
});
|
|
2541
|
+
};
|
|
2542
|
+
process.on("SIGINT", () => handler("SIGINT"));
|
|
2543
|
+
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
2544
|
+
}
|
|
2545
|
+
function onStdoutError(err) {
|
|
2546
|
+
console.error(`${LOG_PREFIX} stdout error (plugin-host pipe likely closed):`, err);
|
|
2547
|
+
process.exit(1);
|
|
2548
|
+
}
|
|
2549
|
+
function installStdoutErrorHandler() {
|
|
2550
|
+
if (IS_VITEST) return;
|
|
2551
|
+
process.stdout.on("error", onStdoutError);
|
|
2552
|
+
}
|
|
2553
|
+
async function getCachedMode2() {
|
|
2554
|
+
return getCachedMode(TANDEM_URL, LOG_PREFIX);
|
|
2555
|
+
}
|
|
2556
|
+
function getModeSync2() {
|
|
2557
|
+
return getModeSync();
|
|
2558
|
+
}
|
|
2559
|
+
function _resetMonitorStateForTests() {
|
|
2560
|
+
_resetSseConsumerStateForTests();
|
|
2561
|
+
process.removeAllListeners("SIGINT");
|
|
2562
|
+
process.removeAllListeners("SIGTERM");
|
|
2563
|
+
}
|
|
2564
|
+
var IS_VITEST, TANDEM_URL, LOG_PREFIX, _monitorTestExports;
|
|
2565
|
+
var init_run2 = __esm({
|
|
2566
|
+
"src/monitor/run.ts"() {
|
|
2567
|
+
"use strict";
|
|
2568
|
+
init_cli_runtime();
|
|
2569
|
+
init_types();
|
|
2570
|
+
init_sse_consumer();
|
|
2571
|
+
init_types3();
|
|
2572
|
+
IS_VITEST = process.env.VITEST === "true";
|
|
2573
|
+
if (!IS_VITEST) {
|
|
2574
|
+
console.log = console.error;
|
|
2575
|
+
console.warn = console.error;
|
|
2576
|
+
console.info = console.error;
|
|
2577
|
+
}
|
|
2578
|
+
TANDEM_URL = resolveTandemUrl();
|
|
2579
|
+
LOG_PREFIX = "[Monitor]";
|
|
2580
|
+
_monitorTestExports = {
|
|
2581
|
+
onStdoutError
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2420
2586
|
// src/server/annotations/lockfile.ts
|
|
2421
2587
|
function parseLockfile(raw) {
|
|
2422
2588
|
const trimmed = raw.trim();
|
|
@@ -2447,8 +2613,12 @@ var init_lockfile = __esm({
|
|
|
2447
2613
|
// src/cli/doctor.ts
|
|
2448
2614
|
var doctor_exports = {};
|
|
2449
2615
|
__export(doctor_exports, {
|
|
2616
|
+
evaluateNpmStaleness: () => evaluateNpmStaleness,
|
|
2617
|
+
evaluateOrphanedVite: () => evaluateOrphanedVite,
|
|
2450
2618
|
evaluateStaleGlobal: () => evaluateStaleGlobal,
|
|
2451
2619
|
globalTandemEditorVersion: () => globalTandemEditorVersion,
|
|
2620
|
+
isTandemEditorRepo: () => isTandemEditorRepo,
|
|
2621
|
+
probeTandemEditorRepo: () => probeTandemEditorRepo,
|
|
2452
2622
|
runDoctor: () => runDoctor,
|
|
2453
2623
|
runDoctorCli: () => runDoctorCli,
|
|
2454
2624
|
summarizeDoctorResults: () => summarizeDoctorResults
|
|
@@ -2459,6 +2629,10 @@ import { request } from "http";
|
|
|
2459
2629
|
import { createConnection as createConnection2 } from "net";
|
|
2460
2630
|
import { homedir as homedir3, platform } from "os";
|
|
2461
2631
|
import { join as join5 } from "path";
|
|
2632
|
+
function errorClass(err) {
|
|
2633
|
+
if (err instanceof Error) return err.name;
|
|
2634
|
+
return typeof err;
|
|
2635
|
+
}
|
|
2462
2636
|
function checkNodeVersion(r) {
|
|
2463
2637
|
const version2 = process.version;
|
|
2464
2638
|
const major = Number.parseInt(version2.slice(1), 10);
|
|
@@ -2478,6 +2652,204 @@ function checkNodeModules(r) {
|
|
|
2478
2652
|
r.fail("node_modules/ not found", "npm install");
|
|
2479
2653
|
}
|
|
2480
2654
|
}
|
|
2655
|
+
function recordEvaluation(r, result) {
|
|
2656
|
+
if (!result) return;
|
|
2657
|
+
if (result.status === "warn") {
|
|
2658
|
+
r.warn(result.message, result.fix, result.data);
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
if (result.status === "skip") {
|
|
2662
|
+
r.pass(`skipped \u2014 ${result.message}`, result.fix, { ...result.data, skipped: true });
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
r.pass(result.message, result.fix, result.data);
|
|
2666
|
+
}
|
|
2667
|
+
function probeTandemEditorRepo(dir) {
|
|
2668
|
+
const read = readJson(join5(dir, "package.json"));
|
|
2669
|
+
if (read.kind === "absent") return "no";
|
|
2670
|
+
if (read.kind === "unreadable") return "unreadable";
|
|
2671
|
+
const parsed = read.value;
|
|
2672
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2673
|
+
return "unreadable";
|
|
2674
|
+
}
|
|
2675
|
+
return parsed.name === "tandem-editor" ? "yes" : "no";
|
|
2676
|
+
}
|
|
2677
|
+
function isTandemEditorRepo(dir) {
|
|
2678
|
+
return probeTandemEditorRepo(dir) === "yes";
|
|
2679
|
+
}
|
|
2680
|
+
function readJson(path8) {
|
|
2681
|
+
let raw;
|
|
2682
|
+
try {
|
|
2683
|
+
raw = readFileSync3(path8, "utf-8");
|
|
2684
|
+
} catch (err) {
|
|
2685
|
+
const code = err.code;
|
|
2686
|
+
if (code === "ENOENT") return { kind: "absent" };
|
|
2687
|
+
return { kind: "unreadable", reason: code ?? errorClass(err) };
|
|
2688
|
+
}
|
|
2689
|
+
try {
|
|
2690
|
+
return { kind: "ok", value: JSON.parse(raw) };
|
|
2691
|
+
} catch {
|
|
2692
|
+
return { kind: "unreadable", reason: "not valid JSON" };
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
function parseLockfileEntry(value) {
|
|
2696
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
2697
|
+
const entry = value;
|
|
2698
|
+
if (entry.version !== void 0 && typeof entry.version !== "string") return null;
|
|
2699
|
+
if (entry.optional !== void 0 && typeof entry.optional !== "boolean") return null;
|
|
2700
|
+
return {
|
|
2701
|
+
version: entry.version,
|
|
2702
|
+
optional: entry.optional
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
function parseLockfileJson(value) {
|
|
2706
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
2707
|
+
const obj = value;
|
|
2708
|
+
if (obj.version !== void 0 && typeof obj.version !== "string") return null;
|
|
2709
|
+
const version2 = obj.version;
|
|
2710
|
+
if (obj.packages === void 0) return { version: version2 };
|
|
2711
|
+
if (typeof obj.packages !== "object" || obj.packages === null || Array.isArray(obj.packages)) {
|
|
2712
|
+
return null;
|
|
2713
|
+
}
|
|
2714
|
+
const packages = {};
|
|
2715
|
+
for (const [path8, raw] of Object.entries(obj.packages)) {
|
|
2716
|
+
const entry = parseLockfileEntry(raw);
|
|
2717
|
+
if (!entry) return null;
|
|
2718
|
+
packages[path8] = entry;
|
|
2719
|
+
}
|
|
2720
|
+
return { version: version2, packages };
|
|
2721
|
+
}
|
|
2722
|
+
function evaluateNpmStaleness(pkgInput, lockInput, hiddenLockInput) {
|
|
2723
|
+
if (!pkgInput || !lockInput || !hiddenLockInput) return null;
|
|
2724
|
+
const lock = parseLockfileJson(lockInput);
|
|
2725
|
+
if (!lock) {
|
|
2726
|
+
return {
|
|
2727
|
+
status: "skip",
|
|
2728
|
+
message: "cannot compare (package-lock.json has an unexpected structure)",
|
|
2729
|
+
fix: "Restore it from git: git checkout package-lock.json",
|
|
2730
|
+
data: { reason: "malformed-lock" }
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
const hiddenLock = parseLockfileJson(hiddenLockInput);
|
|
2734
|
+
if (!hiddenLock) {
|
|
2735
|
+
return {
|
|
2736
|
+
status: "skip",
|
|
2737
|
+
message: "cannot compare (node_modules/.package-lock.json has an unexpected structure)",
|
|
2738
|
+
fix: "npm install",
|
|
2739
|
+
data: { reason: "malformed-hidden-lock" }
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
const pkg = typeof pkgInput === "object" && !Array.isArray(pkgInput) ? pkgInput : {};
|
|
2743
|
+
const pkgVersion = typeof pkg.version === "string" ? pkg.version : void 0;
|
|
2744
|
+
if (pkgVersion && lock.version && pkgVersion !== lock.version) {
|
|
2745
|
+
return {
|
|
2746
|
+
status: "warn",
|
|
2747
|
+
message: `package-lock.json (v${lock.version}) is out of date with package.json (v${pkgVersion})`,
|
|
2748
|
+
fix: "npm install",
|
|
2749
|
+
data: { packageVersion: pkgVersion, lockVersion: lock.version }
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
if (lock.version && hiddenLock.version && lock.version !== hiddenLock.version) {
|
|
2753
|
+
return {
|
|
2754
|
+
status: "warn",
|
|
2755
|
+
message: `node_modules was installed from a different lockfile (v${hiddenLock.version} installed, v${lock.version} expected)`,
|
|
2756
|
+
fix: "npm install",
|
|
2757
|
+
data: { lockVersion: lock.version, installedVersion: hiddenLock.version }
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
const wanted = lock.packages ?? {};
|
|
2761
|
+
const installed = hiddenLock.packages ?? {};
|
|
2762
|
+
const wantedCount = Object.keys(wanted).filter((path8) => path8 !== "").length;
|
|
2763
|
+
if (wantedCount === 0) {
|
|
2764
|
+
return {
|
|
2765
|
+
status: "skip",
|
|
2766
|
+
message: "cannot compare (package-lock.json lists no packages \u2014 a lockfileVersion 1 file, or one written by something other than npm)",
|
|
2767
|
+
fix: "npm install",
|
|
2768
|
+
// No inferred `lockfileVersion` here: we did not read that field, and
|
|
2769
|
+
// guessing it from the presence of `packages` would put a fabricated
|
|
2770
|
+
// value under a real npm field name.
|
|
2771
|
+
data: { reason: "no-comparable-packages" }
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
const drifted = [];
|
|
2775
|
+
for (const [path8, entry] of Object.entries(wanted)) {
|
|
2776
|
+
if (path8 === "") continue;
|
|
2777
|
+
const got = installed[path8];
|
|
2778
|
+
if (!got) {
|
|
2779
|
+
if (!entry.optional) drifted.push(path8);
|
|
2780
|
+
} else if (entry.version !== got.version) {
|
|
2781
|
+
drifted.push(path8);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
for (const path8 of Object.keys(installed)) {
|
|
2785
|
+
if (path8 !== "" && !(path8 in wanted)) drifted.push(path8);
|
|
2786
|
+
}
|
|
2787
|
+
if (drifted.length > 0) {
|
|
2788
|
+
return {
|
|
2789
|
+
status: "warn",
|
|
2790
|
+
message: `node_modules is stale \u2014 ${drifted.length} package(s) differ from package-lock.json (e.g. after a pull or branch switch)`,
|
|
2791
|
+
fix: "npm install",
|
|
2792
|
+
data: { driftCount: drifted.length, sample: drifted.slice(0, 5) }
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
const missingVersions = [];
|
|
2796
|
+
if (!pkgVersion) missingVersions.push("package.json");
|
|
2797
|
+
if (!lock.version) missingVersions.push("package-lock.json");
|
|
2798
|
+
if (!hiddenLock.version) missingVersions.push("node_modules/.package-lock.json");
|
|
2799
|
+
if (missingVersions.length > 0) {
|
|
2800
|
+
return {
|
|
2801
|
+
status: "skip",
|
|
2802
|
+
message: `node_modules matches package-lock.json, but the version check could not run \u2014 no "version" field in ${missingVersions.join(", ")}`,
|
|
2803
|
+
data: { reason: "no-comparable-version", missingVersions }
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
return {
|
|
2807
|
+
status: "pass",
|
|
2808
|
+
message: "node_modules matches package-lock.json",
|
|
2809
|
+
data: { packageCount: Object.keys(installed).length }
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
function readLockfileOrReport(r, path8, label) {
|
|
2813
|
+
const read = readJson(path8);
|
|
2814
|
+
if (read.kind === "absent") {
|
|
2815
|
+
r.pass(`skipped \u2014 cannot compare (${label} not found)`, void 0, {
|
|
2816
|
+
skipped: true,
|
|
2817
|
+
reason: "absent",
|
|
2818
|
+
path: label
|
|
2819
|
+
});
|
|
2820
|
+
return { ok: false };
|
|
2821
|
+
}
|
|
2822
|
+
if (read.kind === "ok") {
|
|
2823
|
+
if (typeof read.value === "object" && read.value !== null && !Array.isArray(read.value)) {
|
|
2824
|
+
return { ok: true, value: read.value };
|
|
2825
|
+
}
|
|
2826
|
+
r.warn(
|
|
2827
|
+
`${label} is not a JSON object \u2014 npm install staleness cannot be checked`,
|
|
2828
|
+
"Check for a truncated or hand-edited file, then: npm install",
|
|
2829
|
+
{ reason: "not-an-object", path: label }
|
|
2830
|
+
);
|
|
2831
|
+
return { ok: false };
|
|
2832
|
+
}
|
|
2833
|
+
r.warn(
|
|
2834
|
+
`${label} could not be read (${read.reason}) \u2014 npm install staleness cannot be checked`,
|
|
2835
|
+
"Check for merge-conflict markers or a truncated file, then: npm install",
|
|
2836
|
+
{ reason: read.reason, path: label }
|
|
2837
|
+
);
|
|
2838
|
+
return { ok: false };
|
|
2839
|
+
}
|
|
2840
|
+
function checkNpmStaleness(r, repoDir) {
|
|
2841
|
+
const pkgRead = readJson(join5(repoDir, "package.json"));
|
|
2842
|
+
const pkg = pkgRead.kind === "ok" ? pkgRead.value : null;
|
|
2843
|
+
const lock = readLockfileOrReport(r, join5(repoDir, "package-lock.json"), "package-lock.json");
|
|
2844
|
+
if (!lock.ok) return;
|
|
2845
|
+
const hiddenLock = readLockfileOrReport(
|
|
2846
|
+
r,
|
|
2847
|
+
join5(repoDir, "node_modules", ".package-lock.json"),
|
|
2848
|
+
"node_modules/.package-lock.json"
|
|
2849
|
+
);
|
|
2850
|
+
if (!hiddenLock.ok) return;
|
|
2851
|
+
recordEvaluation(r, evaluateNpmStaleness(pkg, lock.value, hiddenLock.value));
|
|
2852
|
+
}
|
|
2481
2853
|
function checkMcpJson(r) {
|
|
2482
2854
|
const mcpPath = join5(process.cwd(), ".mcp.json");
|
|
2483
2855
|
if (!existsSync3(mcpPath)) {
|
|
@@ -2603,14 +2975,64 @@ async function checkPorts(r, wsPort, mcpPort) {
|
|
|
2603
2975
|
}
|
|
2604
2976
|
return { ws, mcp };
|
|
2605
2977
|
}
|
|
2978
|
+
function evaluateOrphanedVite(input) {
|
|
2979
|
+
const { viteUp, viteConfirmed, wsUp, mcpUp, wsPort, mcpPort, vitePort } = input;
|
|
2980
|
+
if (!viteUp) return null;
|
|
2981
|
+
if (!viteConfirmed) {
|
|
2982
|
+
return {
|
|
2983
|
+
status: "skip",
|
|
2984
|
+
message: `cannot identify the process on :${vitePort} \u2014 it is listening but did not answer /@vite/client, so it is probably not a Vite dev server`,
|
|
2985
|
+
data: { vite: false, ws: wsUp, mcp: mcpUp, reason: "unconfirmed-vite" }
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
if (!wsUp && !mcpUp) {
|
|
2989
|
+
return {
|
|
2990
|
+
status: "warn",
|
|
2991
|
+
message: `Vite dev server on :${vitePort} is running but the backend (:${wsPort} + :${mcpPort}) is down \u2014 likely orphaned by a crashed dev session`,
|
|
2992
|
+
fix: `If you meant to run the client alone (npm run dev:client), this is expected. Otherwise kill the process on :${vitePort} and restart: npm run dev:standalone`,
|
|
2993
|
+
data: { vite: true, ws: wsUp, mcp: mcpUp }
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
if (wsUp !== mcpUp) {
|
|
2997
|
+
return {
|
|
2998
|
+
status: "skip",
|
|
2999
|
+
message: `cannot tell whether the Vite dev server on :${vitePort} is orphaned \u2014 the backend is only partially up (:${wsPort} ${wsUp ? "up" : "down"}, :${mcpPort} ${mcpUp ? "up" : "down"}); see the ports check`,
|
|
3000
|
+
data: { vite: true, ws: wsUp, mcp: mcpUp, reason: "partial-backend" }
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
return {
|
|
3004
|
+
status: "pass",
|
|
3005
|
+
message: `Vite dev server (:${vitePort}) running alongside the backend`,
|
|
3006
|
+
data: { vite: true, ws: wsUp, mcp: mcpUp }
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
async function checkOrphanedVite(r, wsUp, mcpUp, wsPort, mcpPort, vitePort) {
|
|
3010
|
+
const viteUp = await probePort(vitePort);
|
|
3011
|
+
const viteConfirmed = viteUp ? await isViteDevServer(vitePort) : false;
|
|
3012
|
+
recordEvaluation(
|
|
3013
|
+
r,
|
|
3014
|
+
evaluateOrphanedVite({ viteUp, viteConfirmed, wsUp, mcpUp, wsPort, mcpPort, vitePort })
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
2606
3017
|
function httpGet(url, timeoutMs = 3e3) {
|
|
2607
3018
|
return new Promise((resolve4) => {
|
|
2608
3019
|
const req = request(url, { timeout: timeoutMs }, (res) => {
|
|
2609
3020
|
let body = "";
|
|
3021
|
+
let bytes = 0;
|
|
3022
|
+
let truncated = false;
|
|
2610
3023
|
res.on("data", (chunk) => {
|
|
3024
|
+
if (truncated) return;
|
|
3025
|
+
bytes += Buffer.byteLength(chunk);
|
|
3026
|
+
if (bytes > HTTP_MAX_BYTES) {
|
|
3027
|
+
truncated = true;
|
|
3028
|
+
res.destroy();
|
|
3029
|
+
resolve4({ status: res.statusCode, data: null });
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
2611
3032
|
body += chunk;
|
|
2612
3033
|
});
|
|
2613
3034
|
res.on("end", () => {
|
|
3035
|
+
if (truncated) return;
|
|
2614
3036
|
try {
|
|
2615
3037
|
resolve4({ status: res.statusCode, data: JSON.parse(body) });
|
|
2616
3038
|
} catch {
|
|
@@ -2626,6 +3048,10 @@ function httpGet(url, timeoutMs = 3e3) {
|
|
|
2626
3048
|
req.end();
|
|
2627
3049
|
});
|
|
2628
3050
|
}
|
|
3051
|
+
async function isViteDevServer(port) {
|
|
3052
|
+
const result = await httpGet(`http://127.0.0.1:${port}/@vite/client`, 2e3);
|
|
3053
|
+
return result?.status === 200;
|
|
3054
|
+
}
|
|
2629
3055
|
async function checkHealth(r, mcpPort) {
|
|
2630
3056
|
const result = await httpGet(`http://127.0.0.1:${mcpPort}/health`);
|
|
2631
3057
|
if (!result) {
|
|
@@ -2865,7 +3291,7 @@ function evaluateStaleGlobal(bundled, globalVersion) {
|
|
|
2865
3291
|
};
|
|
2866
3292
|
}
|
|
2867
3293
|
async function checkStaleGlobal(r) {
|
|
2868
|
-
const bundled = true ? "0.
|
|
3294
|
+
const bundled = true ? "0.18.0" : null;
|
|
2869
3295
|
if (!bundled) return;
|
|
2870
3296
|
let globalVersion;
|
|
2871
3297
|
try {
|
|
@@ -2884,16 +3310,37 @@ async function checkStaleGlobal(r) {
|
|
|
2884
3310
|
async function runDoctor(opts = {}) {
|
|
2885
3311
|
const wsPort = opts.wsPort ?? DEFAULT_WS_PORT;
|
|
2886
3312
|
const mcpPort = opts.mcpPort ?? DEFAULT_MCP_PORT;
|
|
3313
|
+
const vitePort = opts.vitePort ?? VITE_DEV_PORT;
|
|
2887
3314
|
const r = new Recorder();
|
|
3315
|
+
const cwd = process.cwd();
|
|
3316
|
+
const repo = probeTandemEditorRepo(cwd);
|
|
3317
|
+
const devRepo = repo === "yes";
|
|
2888
3318
|
await r.check("node-version", () => checkNodeVersion(r));
|
|
2889
3319
|
await r.check("node-modules", () => checkNodeModules(r));
|
|
3320
|
+
if (repo === "unreadable") {
|
|
3321
|
+
await r.check(
|
|
3322
|
+
"dev-repo",
|
|
3323
|
+
() => r.warn(
|
|
3324
|
+
"package.json in the current directory could not be read \u2014 if this is the tandem-editor checkout, the npm-staleness and orphaned-Vite checks are being skipped",
|
|
3325
|
+
"Check for merge-conflict markers or a truncated file: git checkout package.json"
|
|
3326
|
+
)
|
|
3327
|
+
);
|
|
3328
|
+
}
|
|
3329
|
+
if (devRepo) {
|
|
3330
|
+
await r.check("npm-staleness", () => checkNpmStaleness(r, cwd));
|
|
3331
|
+
}
|
|
2890
3332
|
await r.check("mcp-json", () => checkMcpJson(r));
|
|
2891
3333
|
await r.check("user-mcp-config", () => checkUserMcpConfig(r));
|
|
2892
3334
|
await r.check("annotation-store", () => checkAnnotationStore(r));
|
|
2893
3335
|
await r.check("stale-global", () => checkStaleGlobal(r));
|
|
2894
|
-
const
|
|
3336
|
+
const ports = await r.check("ports", () => checkPorts(r, wsPort, mcpPort));
|
|
3337
|
+
const ws = ports?.ws ?? false;
|
|
3338
|
+
const mcp = ports?.mcp ?? false;
|
|
3339
|
+
if (devRepo) {
|
|
3340
|
+
await r.check("orphaned-vite", () => checkOrphanedVite(r, ws, mcp, wsPort, mcpPort, vitePort));
|
|
3341
|
+
}
|
|
2895
3342
|
if (mcp) {
|
|
2896
|
-
const healthy = await r.check("health", () => checkHealth(r, mcpPort));
|
|
3343
|
+
const healthy = await r.check("health", () => checkHealth(r, mcpPort)) ?? false;
|
|
2897
3344
|
if (healthy) {
|
|
2898
3345
|
await r.check("sse", () => checkSseEndpoint(r, mcpPort));
|
|
2899
3346
|
}
|
|
@@ -2976,7 +3423,7 @@ async function runDoctorCli(opts = {}) {
|
|
|
2976
3423
|
out("");
|
|
2977
3424
|
return exitCode;
|
|
2978
3425
|
}
|
|
2979
|
-
var Recorder;
|
|
3426
|
+
var Recorder, VITE_DEV_PORT, HTTP_MAX_BYTES;
|
|
2980
3427
|
var init_doctor = __esm({
|
|
2981
3428
|
"src/cli/doctor.ts"() {
|
|
2982
3429
|
"use strict";
|
|
@@ -2987,11 +3434,29 @@ var init_doctor = __esm({
|
|
|
2987
3434
|
warnings = 0;
|
|
2988
3435
|
results = [];
|
|
2989
3436
|
currentCheck = "";
|
|
3437
|
+
/**
|
|
3438
|
+
* Run one check under `name`. A throwing check is contained here and
|
|
3439
|
+
* recorded as a `fail` rather than taking the whole report down.
|
|
3440
|
+
*
|
|
3441
|
+
* Returns `T | undefined` — NOT `T`. The `undefined` is load-bearing and
|
|
3442
|
+
* must not be cast away: a crashed check produced no value, and the two
|
|
3443
|
+
* callers that consume one (`ports` destructures `{ws, mcp}`; `health`
|
|
3444
|
+
* gates the `sse` check) have to say what they want instead. Returning
|
|
3445
|
+
* `undefined as T` would satisfy the compiler and then throw a TypeError
|
|
3446
|
+
* on the destructure — converting a clear crash into a confusing one on
|
|
3447
|
+
* the exact path this containment exists to protect.
|
|
3448
|
+
*/
|
|
2990
3449
|
async check(name, fn) {
|
|
2991
3450
|
const prev = this.currentCheck;
|
|
2992
3451
|
this.currentCheck = name;
|
|
2993
3452
|
try {
|
|
2994
3453
|
return await fn();
|
|
3454
|
+
} catch (err) {
|
|
3455
|
+
this.fail(
|
|
3456
|
+
`${name} check crashed (${errorClass(err)}) \u2014 the rest of the report is still valid`,
|
|
3457
|
+
"Please report this at https://github.com/bloknayrb/tandem/issues"
|
|
3458
|
+
);
|
|
3459
|
+
return void 0;
|
|
2995
3460
|
} finally {
|
|
2996
3461
|
this.currentCheck = prev;
|
|
2997
3462
|
}
|
|
@@ -3014,6 +3479,8 @@ var init_doctor = __esm({
|
|
|
3014
3479
|
this.record("fail", msg, fix, fields);
|
|
3015
3480
|
}
|
|
3016
3481
|
};
|
|
3482
|
+
VITE_DEV_PORT = 5173;
|
|
3483
|
+
HTTP_MAX_BYTES = 256 * 1024;
|
|
3017
3484
|
}
|
|
3018
3485
|
});
|
|
3019
3486
|
|
|
@@ -3696,7 +4163,7 @@ import fs4 from "fs";
|
|
|
3696
4163
|
function knownVersion(v) {
|
|
3697
4164
|
return typeof v === "string" && KNOWN_VERSION_MAJORS.has(v.split(".")[0]);
|
|
3698
4165
|
}
|
|
3699
|
-
function
|
|
4166
|
+
function readJson2(filePath) {
|
|
3700
4167
|
try {
|
|
3701
4168
|
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
3702
4169
|
} catch {
|
|
@@ -3709,7 +4176,7 @@ function resolveLicenseState(deps) {
|
|
|
3709
4176
|
return { gateActive: false };
|
|
3710
4177
|
}
|
|
3711
4178
|
const nowMs = now();
|
|
3712
|
-
const lf =
|
|
4179
|
+
const lf = readJson2(licenseFilePath(appDataDir));
|
|
3713
4180
|
if (lf?.blob) {
|
|
3714
4181
|
try {
|
|
3715
4182
|
const meta = verify(lf.blob);
|
|
@@ -3726,7 +4193,7 @@ function resolveLicenseState(deps) {
|
|
|
3726
4193
|
} catch {
|
|
3727
4194
|
}
|
|
3728
4195
|
}
|
|
3729
|
-
const tf =
|
|
4196
|
+
const tf = readJson2(trialFilePath(appDataDir));
|
|
3730
4197
|
const firstRunAt = tf?.firstRunAt ? new Date(tf.firstRunAt).getTime() : nowMs;
|
|
3731
4198
|
const expiresAt = firstRunAt + TRIAL_MS;
|
|
3732
4199
|
if (nowMs < expiresAt) {
|
|
@@ -3917,7 +4384,7 @@ process.once("unhandledRejection", (reason) => {
|
|
|
3917
4384
|
`);
|
|
3918
4385
|
process.exit(1);
|
|
3919
4386
|
});
|
|
3920
|
-
var version = true ? "0.
|
|
4387
|
+
var version = true ? "0.18.0" : "0.0.0-dev";
|
|
3921
4388
|
var args = process.argv.slice(2);
|
|
3922
4389
|
if (args.includes("--help") || args.includes("-h")) {
|
|
3923
4390
|
console.log(`tandem v${version}
|
|
@@ -3945,6 +4412,10 @@ Usage:
|
|
|
3945
4412
|
tandem server running on the host)
|
|
3946
4413
|
tandem channel Run the Tandem channel shim (stdio MCP)
|
|
3947
4414
|
(used by the plugin's tandem-channel entry)
|
|
4415
|
+
tandem monitor Run the Tandem plugin monitor: stream event
|
|
4416
|
+
lines to stdout for Claude Code to surface as
|
|
4417
|
+
notifications (used by the plugin's
|
|
4418
|
+
experimental.monitors entry; needs no flag)
|
|
3948
4419
|
tandem --version
|
|
3949
4420
|
tandem --help
|
|
3950
4421
|
`);
|
|
@@ -3954,7 +4425,7 @@ if (args.includes("--version") || args.includes("-v")) {
|
|
|
3954
4425
|
console.log(version);
|
|
3955
4426
|
process.exit(0);
|
|
3956
4427
|
}
|
|
3957
|
-
var isStdioMode = args[0] === "mcp-stdio" || args[0] === "channel";
|
|
4428
|
+
var isStdioMode = args[0] === "mcp-stdio" || args[0] === "channel" || args[0] === "monitor";
|
|
3958
4429
|
if (!isStdioMode) {
|
|
3959
4430
|
const { default: updateNotifier } = await import("update-notifier");
|
|
3960
4431
|
updateNotifier({ pkg: { name: "tandem-editor", version } }).notify();
|
|
@@ -3990,6 +4461,9 @@ try {
|
|
|
3990
4461
|
} else if (args[0] === "channel") {
|
|
3991
4462
|
const { runChannelCli: runChannelCli2 } = await Promise.resolve().then(() => (init_channel(), channel_exports));
|
|
3992
4463
|
await runChannelCli2();
|
|
4464
|
+
} else if (args[0] === "monitor") {
|
|
4465
|
+
const { main: main2 } = await Promise.resolve().then(() => (init_run2(), run_exports));
|
|
4466
|
+
await main2();
|
|
3993
4467
|
} else if (args[0] === "doctor") {
|
|
3994
4468
|
const { runDoctorCli: runDoctorCli2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
3995
4469
|
const exitCode = await runDoctorCli2({ json: args.includes("--json") });
|