tandem-editor 0.9.0 → 0.11.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 +9 -7
- package/CHANGELOG.md +133 -9
- package/LICENSE +58 -21
- package/README.md +44 -42
- package/dist/channel/index.js +364 -153
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +260 -122
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/CoworkSettings-ChE5WuAe.js +3 -0
- package/dist/client/assets/index-BJKuWd_k.css +1 -0
- package/dist/client/assets/index-vu_QxvyU.js +310 -0
- package/dist/client/assets/webview-Ben21ZLJ.js +1 -0
- package/dist/client/assets/window-BxBvHL5k.js +1 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/inter-tight-latin.woff2 +0 -0
- package/dist/client/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/dist/client/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/client/index.html +201 -49
- package/dist/monitor/index.js +53 -19
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +830 -263
- package/dist/server/index.js.map +1 -1
- package/package.json +22 -19
- package/sample/table-test.md +8 -0
- package/skills/tandem/SKILL.md +7 -7
- package/dist/client/assets/CoworkSettings-C9Dd4D9z.js +0 -1
- package/dist/client/assets/index-Bz3WFCWw.css +0 -1
- package/dist/client/assets/index-CN_6DqdC.js +0 -228
- package/dist/client/assets/webview-BQBJMQvJ.js +0 -1
package/dist/channel/index.js
CHANGED
|
@@ -3106,6 +3106,9 @@ var require_utils = __commonJS({
|
|
|
3106
3106
|
"use strict";
|
|
3107
3107
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
3108
3108
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3109
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3110
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3111
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3109
3112
|
function stringArrayToHexStripped(input) {
|
|
3110
3113
|
let acc = "";
|
|
3111
3114
|
let code = 0;
|
|
@@ -3298,27 +3301,77 @@ var require_utils = __commonJS({
|
|
|
3298
3301
|
}
|
|
3299
3302
|
return output.join("");
|
|
3300
3303
|
}
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3304
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3305
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3306
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3307
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
3308
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3309
|
+
re.lastIndex = 0;
|
|
3310
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3311
|
+
}
|
|
3312
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
3313
|
+
if (input.indexOf("%") === -1) {
|
|
3314
|
+
return input;
|
|
3311
3315
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3316
|
+
let output = "";
|
|
3317
|
+
for (let i = 0; i < input.length; i++) {
|
|
3318
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3319
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3320
|
+
if (isHexPair(hex)) {
|
|
3321
|
+
const normalizedHex = hex.toUpperCase();
|
|
3322
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3323
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3324
|
+
output += decoded;
|
|
3325
|
+
} else {
|
|
3326
|
+
output += "%" + normalizedHex;
|
|
3327
|
+
}
|
|
3328
|
+
i += 2;
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
output += input[i];
|
|
3314
3333
|
}
|
|
3315
|
-
|
|
3316
|
-
|
|
3334
|
+
return output;
|
|
3335
|
+
}
|
|
3336
|
+
function normalizePathEncoding(input) {
|
|
3337
|
+
let output = "";
|
|
3338
|
+
for (let i = 0; i < input.length; i++) {
|
|
3339
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3340
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3341
|
+
if (isHexPair(hex)) {
|
|
3342
|
+
const normalizedHex = hex.toUpperCase();
|
|
3343
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3344
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3345
|
+
output += decoded;
|
|
3346
|
+
} else {
|
|
3347
|
+
output += "%" + normalizedHex;
|
|
3348
|
+
}
|
|
3349
|
+
i += 2;
|
|
3350
|
+
continue;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
if (isPathCharacter(input[i])) {
|
|
3354
|
+
output += input[i];
|
|
3355
|
+
} else {
|
|
3356
|
+
output += escape(input[i]);
|
|
3357
|
+
}
|
|
3317
3358
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3359
|
+
return output;
|
|
3360
|
+
}
|
|
3361
|
+
function escapePreservingEscapes(input) {
|
|
3362
|
+
let output = "";
|
|
3363
|
+
for (let i = 0; i < input.length; i++) {
|
|
3364
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3365
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3366
|
+
if (isHexPair(hex)) {
|
|
3367
|
+
output += "%" + hex.toUpperCase();
|
|
3368
|
+
i += 2;
|
|
3369
|
+
continue;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
output += escape(input[i]);
|
|
3320
3373
|
}
|
|
3321
|
-
return
|
|
3374
|
+
return output;
|
|
3322
3375
|
}
|
|
3323
3376
|
function recomposeAuthority(component) {
|
|
3324
3377
|
const uriTokens = [];
|
|
@@ -3333,7 +3386,7 @@ var require_utils = __commonJS({
|
|
|
3333
3386
|
if (ipV6res.isIPV6 === true) {
|
|
3334
3387
|
host = `[${ipV6res.escapedHost}]`;
|
|
3335
3388
|
} else {
|
|
3336
|
-
host =
|
|
3389
|
+
host = reescapeHostDelimiters(host, false);
|
|
3337
3390
|
}
|
|
3338
3391
|
}
|
|
3339
3392
|
uriTokens.push(host);
|
|
@@ -3347,7 +3400,10 @@ var require_utils = __commonJS({
|
|
|
3347
3400
|
module.exports = {
|
|
3348
3401
|
nonSimpleDomain,
|
|
3349
3402
|
recomposeAuthority,
|
|
3350
|
-
|
|
3403
|
+
reescapeHostDelimiters,
|
|
3404
|
+
normalizePercentEncoding,
|
|
3405
|
+
normalizePathEncoding,
|
|
3406
|
+
escapePreservingEscapes,
|
|
3351
3407
|
removeDotSegments,
|
|
3352
3408
|
isIPv4,
|
|
3353
3409
|
isUUID,
|
|
@@ -3571,12 +3627,12 @@ var require_schemes = __commonJS({
|
|
|
3571
3627
|
var require_fast_uri = __commonJS({
|
|
3572
3628
|
"node_modules/fast-uri/index.js"(exports, module) {
|
|
3573
3629
|
"use strict";
|
|
3574
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
3630
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3575
3631
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3576
3632
|
function normalize(uri, options) {
|
|
3577
3633
|
if (typeof uri === "string") {
|
|
3578
3634
|
uri = /** @type {T} */
|
|
3579
|
-
|
|
3635
|
+
normalizeString(uri, options);
|
|
3580
3636
|
} else if (typeof uri === "object") {
|
|
3581
3637
|
uri = /** @type {T} */
|
|
3582
3638
|
parse3(serialize(uri, options), options);
|
|
@@ -3643,19 +3699,9 @@ var require_fast_uri = __commonJS({
|
|
|
3643
3699
|
return target;
|
|
3644
3700
|
}
|
|
3645
3701
|
function equal(uriA, uriB, options) {
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
} else if (typeof uriA === "object") {
|
|
3650
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
3651
|
-
}
|
|
3652
|
-
if (typeof uriB === "string") {
|
|
3653
|
-
uriB = unescape(uriB);
|
|
3654
|
-
uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
|
|
3655
|
-
} else if (typeof uriB === "object") {
|
|
3656
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
3657
|
-
}
|
|
3658
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
3702
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3703
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3704
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3659
3705
|
}
|
|
3660
3706
|
function serialize(cmpts, opts) {
|
|
3661
3707
|
const component = {
|
|
@@ -3680,12 +3726,12 @@ var require_fast_uri = __commonJS({
|
|
|
3680
3726
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
3681
3727
|
if (component.path !== void 0) {
|
|
3682
3728
|
if (!options.skipEscape) {
|
|
3683
|
-
component.path =
|
|
3729
|
+
component.path = escapePreservingEscapes(component.path);
|
|
3684
3730
|
if (component.scheme !== void 0) {
|
|
3685
3731
|
component.path = component.path.split("%3A").join(":");
|
|
3686
3732
|
}
|
|
3687
3733
|
} else {
|
|
3688
|
-
component.path =
|
|
3734
|
+
component.path = normalizePercentEncoding(component.path);
|
|
3689
3735
|
}
|
|
3690
3736
|
}
|
|
3691
3737
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -3720,7 +3766,16 @@ var require_fast_uri = __commonJS({
|
|
|
3720
3766
|
return uriTokens.join("");
|
|
3721
3767
|
}
|
|
3722
3768
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3723
|
-
function
|
|
3769
|
+
function getParseError(parsed, matches) {
|
|
3770
|
+
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3771
|
+
return 'URI path must start with "/" when authority is present.';
|
|
3772
|
+
}
|
|
3773
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
3774
|
+
return "URI port is malformed.";
|
|
3775
|
+
}
|
|
3776
|
+
return void 0;
|
|
3777
|
+
}
|
|
3778
|
+
function parseWithStatus(uri, opts) {
|
|
3724
3779
|
const options = Object.assign({}, opts);
|
|
3725
3780
|
const parsed = {
|
|
3726
3781
|
scheme: void 0,
|
|
@@ -3731,6 +3786,7 @@ var require_fast_uri = __commonJS({
|
|
|
3731
3786
|
query: void 0,
|
|
3732
3787
|
fragment: void 0
|
|
3733
3788
|
};
|
|
3789
|
+
let malformedAuthorityOrPort = false;
|
|
3734
3790
|
let isIP = false;
|
|
3735
3791
|
if (options.reference === "suffix") {
|
|
3736
3792
|
if (options.scheme) {
|
|
@@ -3751,6 +3807,11 @@ var require_fast_uri = __commonJS({
|
|
|
3751
3807
|
if (isNaN(parsed.port)) {
|
|
3752
3808
|
parsed.port = matches[5];
|
|
3753
3809
|
}
|
|
3810
|
+
const parseError = getParseError(parsed, matches);
|
|
3811
|
+
if (parseError !== void 0) {
|
|
3812
|
+
parsed.error = parsed.error || parseError;
|
|
3813
|
+
malformedAuthorityOrPort = true;
|
|
3814
|
+
}
|
|
3754
3815
|
if (parsed.host) {
|
|
3755
3816
|
const ipv4result = isIPv4(parsed.host);
|
|
3756
3817
|
if (ipv4result === false) {
|
|
@@ -3789,14 +3850,18 @@ var require_fast_uri = __commonJS({
|
|
|
3789
3850
|
parsed.scheme = unescape(parsed.scheme);
|
|
3790
3851
|
}
|
|
3791
3852
|
if (parsed.host !== void 0) {
|
|
3792
|
-
parsed.host = unescape(parsed.host);
|
|
3853
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
3793
3854
|
}
|
|
3794
3855
|
}
|
|
3795
3856
|
if (parsed.path) {
|
|
3796
|
-
parsed.path =
|
|
3857
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3797
3858
|
}
|
|
3798
3859
|
if (parsed.fragment) {
|
|
3799
|
-
|
|
3860
|
+
try {
|
|
3861
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3862
|
+
} catch {
|
|
3863
|
+
parsed.error = parsed.error || "URI malformed";
|
|
3864
|
+
}
|
|
3800
3865
|
}
|
|
3801
3866
|
}
|
|
3802
3867
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -3805,7 +3870,29 @@ var require_fast_uri = __commonJS({
|
|
|
3805
3870
|
} else {
|
|
3806
3871
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3807
3872
|
}
|
|
3808
|
-
return parsed;
|
|
3873
|
+
return { parsed, malformedAuthorityOrPort };
|
|
3874
|
+
}
|
|
3875
|
+
function parse3(uri, opts) {
|
|
3876
|
+
return parseWithStatus(uri, opts).parsed;
|
|
3877
|
+
}
|
|
3878
|
+
function normalizeString(uri, opts) {
|
|
3879
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3880
|
+
}
|
|
3881
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
3882
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3883
|
+
return {
|
|
3884
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3885
|
+
malformedAuthorityOrPort
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
function normalizeComparableURI(uri, opts) {
|
|
3889
|
+
if (typeof uri === "string") {
|
|
3890
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
3891
|
+
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3892
|
+
}
|
|
3893
|
+
if (typeof uri === "object") {
|
|
3894
|
+
return serialize(uri, opts);
|
|
3895
|
+
}
|
|
3809
3896
|
}
|
|
3810
3897
|
var fastUri = {
|
|
3811
3898
|
SCHEMES,
|
|
@@ -17914,6 +18001,14 @@ var IDLE_TIMEOUT = 30 * 60 * 1e3;
|
|
|
17914
18001
|
var SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
|
|
17915
18002
|
var CHANNEL_MAX_RETRIES = 5;
|
|
17916
18003
|
var CHANNEL_RETRY_DELAY_MS = 2e3;
|
|
18004
|
+
var CHANNEL_CONNECT_FETCH_TIMEOUT_MS = 1e4;
|
|
18005
|
+
var CHANNEL_SSE_INACTIVITY_TIMEOUT_MS = 6e4;
|
|
18006
|
+
var CHANNEL_MODE_FETCH_TIMEOUT_MS = 2e3;
|
|
18007
|
+
var CHANNEL_AWARENESS_FETCH_TIMEOUT_MS = 5e3;
|
|
18008
|
+
var CHANNEL_ERROR_REPORT_TIMEOUT_MS = 3e3;
|
|
18009
|
+
var CHANNEL_REPLY_FETCH_TIMEOUT_MS = 5e3;
|
|
18010
|
+
var CHANNEL_PERMISSION_FETCH_TIMEOUT_MS = 5e3;
|
|
18011
|
+
var CHANNEL_MAX_SSE_BUFFER_BYTES = 1e6;
|
|
17917
18012
|
|
|
17918
18013
|
// src/shared/cli-runtime.ts
|
|
17919
18014
|
function redirectConsoleToStderr() {
|
|
@@ -17922,34 +18017,73 @@ function redirectConsoleToStderr() {
|
|
|
17922
18017
|
console.info = console.error;
|
|
17923
18018
|
}
|
|
17924
18019
|
function resolveTandemUrl(override) {
|
|
17925
|
-
|
|
17926
|
-
|
|
18020
|
+
return resolveTandemUrlCandidate(override).replace(/\/+$/, "");
|
|
18021
|
+
}
|
|
18022
|
+
function resolveTandemUrlCandidate(override) {
|
|
18023
|
+
const candidates = [
|
|
18024
|
+
override,
|
|
18025
|
+
process.env.CLAUDE_PLUGIN_OPTION_SERVER_URL,
|
|
18026
|
+
process.env.TANDEM_URL
|
|
18027
|
+
];
|
|
18028
|
+
for (const url of candidates) {
|
|
18029
|
+
if (url !== void 0 && url.trim() !== "") return url.trim();
|
|
18030
|
+
}
|
|
18031
|
+
return `http://localhost:${DEFAULT_MCP_PORT}`;
|
|
18032
|
+
}
|
|
18033
|
+
function resolveAuthTokenCandidate(override) {
|
|
18034
|
+
const candidates = [
|
|
18035
|
+
["explicit override", override],
|
|
18036
|
+
["CLAUDE_PLUGIN_OPTION_AUTH_TOKEN", process.env.CLAUDE_PLUGIN_OPTION_AUTH_TOKEN],
|
|
18037
|
+
["TANDEM_AUTH_TOKEN", process.env.TANDEM_AUTH_TOKEN]
|
|
18038
|
+
];
|
|
18039
|
+
for (const [source, token] of candidates) {
|
|
18040
|
+
if (token !== void 0 && token.trim() !== "") return { token, source };
|
|
18041
|
+
}
|
|
18042
|
+
return { token: void 0, source: void 0 };
|
|
17927
18043
|
}
|
|
17928
18044
|
var VALID_TOKEN_RE = /^[A-Za-z0-9_\-]{32,}$/;
|
|
17929
18045
|
var _warnedInvalidToken = false;
|
|
17930
18046
|
async function authFetch(url, init) {
|
|
17931
|
-
const token =
|
|
17932
|
-
if (token !== void 0
|
|
17933
|
-
|
|
18047
|
+
const { token, source } = resolveAuthTokenCandidate();
|
|
18048
|
+
if (token !== void 0) {
|
|
18049
|
+
const trimmed = token.trim();
|
|
18050
|
+
if (VALID_TOKEN_RE.test(trimmed)) {
|
|
17934
18051
|
const headers = new Headers(init?.headers);
|
|
17935
|
-
headers.set("Authorization", `Bearer ${
|
|
18052
|
+
headers.set("Authorization", `Bearer ${trimmed}`);
|
|
17936
18053
|
return fetch(url, { ...init, headers });
|
|
17937
18054
|
}
|
|
17938
18055
|
if (!_warnedInvalidToken) {
|
|
17939
18056
|
_warnedInvalidToken = true;
|
|
17940
18057
|
console.error(
|
|
17941
|
-
|
|
18058
|
+
`[tandem] authFetch: ${source} is set but invalid (must be 32+ alphanumeric chars [A-Za-z0-9_-]); sending without Authorization header`
|
|
17942
18059
|
);
|
|
17943
18060
|
}
|
|
17944
18061
|
}
|
|
17945
18062
|
return fetch(url, init);
|
|
17946
18063
|
}
|
|
17947
18064
|
|
|
18065
|
+
// src/shared/fetch-with-timeout.ts
|
|
18066
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
18067
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
18068
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
18069
|
+
return authFetch(url, { ...init, signal });
|
|
18070
|
+
}
|
|
18071
|
+
function describeFetchError(err, endpoint, timeoutMs) {
|
|
18072
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
18073
|
+
return `${endpoint} timed out after ${timeoutMs}ms`;
|
|
18074
|
+
}
|
|
18075
|
+
return err instanceof Error ? err.message : String(err);
|
|
18076
|
+
}
|
|
18077
|
+
function isAbortOrTimeoutError(err) {
|
|
18078
|
+
return err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
18079
|
+
}
|
|
18080
|
+
|
|
17948
18081
|
// src/shared/events/types.ts
|
|
17949
18082
|
var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
17950
18083
|
"annotation:created",
|
|
17951
18084
|
"annotation:accepted",
|
|
17952
18085
|
"annotation:dismissed",
|
|
18086
|
+
"annotation:edited",
|
|
17953
18087
|
"annotation:reply",
|
|
17954
18088
|
"chat:message",
|
|
17955
18089
|
"document:opened",
|
|
@@ -17966,9 +18100,9 @@ function formatEventContent(event) {
|
|
|
17966
18100
|
const doc = event.documentId ? ` [doc: ${event.documentId}]` : "";
|
|
17967
18101
|
switch (event.type) {
|
|
17968
18102
|
case "annotation:created": {
|
|
17969
|
-
const { annotationType, content, textSnippet, hasSuggestedText
|
|
18103
|
+
const { annotationType, content, textSnippet, hasSuggestedText } = event.payload;
|
|
17970
18104
|
const snippet = textSnippet ? ` on "${textSnippet}"` : "";
|
|
17971
|
-
const label = hasSuggestedText ? "replacement" :
|
|
18105
|
+
const label = hasSuggestedText ? "replacement" : annotationType;
|
|
17972
18106
|
return `User created ${label}${snippet}: ${content || "(no content)"}${doc}`;
|
|
17973
18107
|
}
|
|
17974
18108
|
case "annotation:accepted": {
|
|
@@ -17979,6 +18113,10 @@ function formatEventContent(event) {
|
|
|
17979
18113
|
const { annotationId, textSnippet } = event.payload;
|
|
17980
18114
|
return `User dismissed annotation ${annotationId}${textSnippet ? ` ("${textSnippet}")` : ""}${doc}`;
|
|
17981
18115
|
}
|
|
18116
|
+
case "annotation:edited": {
|
|
18117
|
+
const { content } = event.payload;
|
|
18118
|
+
return `User edited annotation: "${content}"${doc}`;
|
|
18119
|
+
}
|
|
17982
18120
|
case "annotation:reply": {
|
|
17983
18121
|
const { annotationId, replyAuthor, replyText, textSnippet } = event.payload;
|
|
17984
18122
|
const who = replyAuthor === "claude" ? "Claude" : "User";
|
|
@@ -18021,6 +18159,10 @@ function formatEventMeta(event) {
|
|
|
18021
18159
|
case "annotation:dismissed":
|
|
18022
18160
|
meta.annotation_id = event.payload.annotationId;
|
|
18023
18161
|
break;
|
|
18162
|
+
case "annotation:edited":
|
|
18163
|
+
meta.annotation_id = event.payload.annotationId;
|
|
18164
|
+
meta.edited_at = String(event.payload.editedAt);
|
|
18165
|
+
break;
|
|
18024
18166
|
case "annotation:reply":
|
|
18025
18167
|
meta.annotation_id = event.payload.annotationId;
|
|
18026
18168
|
meta.reply_id = event.payload.replyId;
|
|
@@ -18063,18 +18205,22 @@ async function startEventBridge(mcp, tandemUrl) {
|
|
|
18063
18205
|
if (retries >= CHANNEL_MAX_RETRIES) {
|
|
18064
18206
|
console.error("[Channel] SSE connection exhausted, reporting error and exiting");
|
|
18065
18207
|
try {
|
|
18066
|
-
await
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
|
|
18072
|
-
|
|
18073
|
-
|
|
18208
|
+
await fetchWithTimeout(
|
|
18209
|
+
`${tandemUrl}/api/channel-error`,
|
|
18210
|
+
{
|
|
18211
|
+
method: "POST",
|
|
18212
|
+
headers: { "Content-Type": "application/json" },
|
|
18213
|
+
body: JSON.stringify({
|
|
18214
|
+
error: "CHANNEL_CONNECT_FAILED",
|
|
18215
|
+
message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`
|
|
18216
|
+
})
|
|
18217
|
+
},
|
|
18218
|
+
CHANNEL_ERROR_REPORT_TIMEOUT_MS
|
|
18219
|
+
);
|
|
18074
18220
|
} catch (reportErr) {
|
|
18075
18221
|
console.error(
|
|
18076
18222
|
"[Channel] Could not report failure to server:",
|
|
18077
|
-
reportErr
|
|
18223
|
+
describeFetchError(reportErr, "/api/channel-error", CHANNEL_ERROR_REPORT_TIMEOUT_MS)
|
|
18078
18224
|
);
|
|
18079
18225
|
}
|
|
18080
18226
|
process.exit(1);
|
|
@@ -18086,29 +18232,52 @@ async function startEventBridge(mcp, tandemUrl) {
|
|
|
18086
18232
|
async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
18087
18233
|
const headers = { Accept: "text/event-stream" };
|
|
18088
18234
|
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
18089
|
-
const
|
|
18235
|
+
const connectCtrl = new AbortController();
|
|
18236
|
+
const connectTimer = setTimeout(
|
|
18237
|
+
() => connectCtrl.abort(new Error("handshake timeout")),
|
|
18238
|
+
CHANNEL_CONNECT_FETCH_TIMEOUT_MS
|
|
18239
|
+
);
|
|
18240
|
+
let res;
|
|
18241
|
+
try {
|
|
18242
|
+
res = await authFetch(`${tandemUrl}/api/events`, { headers, signal: connectCtrl.signal });
|
|
18243
|
+
} finally {
|
|
18244
|
+
clearTimeout(connectTimer);
|
|
18245
|
+
}
|
|
18090
18246
|
if (!res.ok) throw new Error(`SSE endpoint returned ${res.status}`);
|
|
18091
18247
|
if (!res.body) throw new Error("SSE endpoint returned no body");
|
|
18092
18248
|
const reader = res.body.getReader();
|
|
18093
18249
|
const decoder = new TextDecoder();
|
|
18094
18250
|
let buffer = "";
|
|
18251
|
+
let lastActivityAt = Date.now();
|
|
18252
|
+
let inactivityTimedOut = false;
|
|
18253
|
+
const watchdog = setInterval(() => {
|
|
18254
|
+
if (Date.now() - lastActivityAt > CHANNEL_SSE_INACTIVITY_TIMEOUT_MS) {
|
|
18255
|
+
inactivityTimedOut = true;
|
|
18256
|
+
reader.cancel(new Error("SSE inactivity timeout")).catch(() => {
|
|
18257
|
+
});
|
|
18258
|
+
}
|
|
18259
|
+
}, CHANNEL_SSE_INACTIVITY_TIMEOUT_MS / 4);
|
|
18095
18260
|
let awarenessTimer = null;
|
|
18096
18261
|
let clearAwarenessTimer = null;
|
|
18097
18262
|
let pendingAwareness = null;
|
|
18098
18263
|
const AWARENESS_CLEAR_MS = 3e3;
|
|
18099
18264
|
function clearAwareness(documentId) {
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
18103
|
-
|
|
18104
|
-
|
|
18105
|
-
|
|
18106
|
-
|
|
18107
|
-
|
|
18108
|
-
|
|
18265
|
+
fetchWithTimeout(
|
|
18266
|
+
`${tandemUrl}/api/channel-awareness`,
|
|
18267
|
+
{
|
|
18268
|
+
method: "POST",
|
|
18269
|
+
headers: { "Content-Type": "application/json" },
|
|
18270
|
+
body: JSON.stringify({
|
|
18271
|
+
documentId: documentId ?? null,
|
|
18272
|
+
status: "idle",
|
|
18273
|
+
active: false
|
|
18274
|
+
})
|
|
18275
|
+
},
|
|
18276
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18277
|
+
).catch((err) => {
|
|
18109
18278
|
console.error(
|
|
18110
18279
|
"[Channel] clearAwareness failed (non-fatal):",
|
|
18111
|
-
err
|
|
18280
|
+
describeFetchError(err, "/api/channel-awareness clear", CHANNEL_AWARENESS_FETCH_TIMEOUT_MS)
|
|
18112
18281
|
);
|
|
18113
18282
|
});
|
|
18114
18283
|
}
|
|
@@ -18116,16 +18285,27 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18116
18285
|
if (!pendingAwareness) return;
|
|
18117
18286
|
const event = pendingAwareness;
|
|
18118
18287
|
pendingAwareness = null;
|
|
18119
|
-
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18123
|
-
|
|
18124
|
-
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
|
|
18128
|
-
|
|
18288
|
+
fetchWithTimeout(
|
|
18289
|
+
`${tandemUrl}/api/channel-awareness`,
|
|
18290
|
+
{
|
|
18291
|
+
method: "POST",
|
|
18292
|
+
headers: { "Content-Type": "application/json" },
|
|
18293
|
+
body: JSON.stringify({
|
|
18294
|
+
documentId: event.documentId,
|
|
18295
|
+
status: `processing: ${event.type}`,
|
|
18296
|
+
active: true
|
|
18297
|
+
})
|
|
18298
|
+
},
|
|
18299
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18300
|
+
).catch((err) => {
|
|
18301
|
+
console.error(
|
|
18302
|
+
"[Channel] Awareness update failed:",
|
|
18303
|
+
describeFetchError(
|
|
18304
|
+
err,
|
|
18305
|
+
"/api/channel-awareness update",
|
|
18306
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18307
|
+
)
|
|
18308
|
+
);
|
|
18129
18309
|
});
|
|
18130
18310
|
if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
|
|
18131
18311
|
clearAwarenessTimer = setTimeout(() => clearAwareness(event.documentId), AWARENESS_CLEAR_MS);
|
|
@@ -18135,66 +18315,81 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18135
18315
|
if (awarenessTimer) clearTimeout(awarenessTimer);
|
|
18136
18316
|
awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);
|
|
18137
18317
|
}
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
if (line.startsWith("id: ")) eventId = line.slice(4);
|
|
18151
|
-
else if (line.startsWith("data: ")) data = line.slice(6);
|
|
18152
|
-
}
|
|
18153
|
-
if (!data) continue;
|
|
18154
|
-
let event;
|
|
18155
|
-
try {
|
|
18156
|
-
event = parseTandemEvent(JSON.parse(data));
|
|
18157
|
-
} catch {
|
|
18158
|
-
console.error(
|
|
18159
|
-
"[Channel] Malformed SSE event data (skipping), eventId=%s:",
|
|
18160
|
-
eventId,
|
|
18161
|
-
data.slice(0, 200)
|
|
18162
|
-
);
|
|
18163
|
-
if (eventId) onEventId(eventId);
|
|
18164
|
-
continue;
|
|
18165
|
-
}
|
|
18166
|
-
if (!event) {
|
|
18167
|
-
console.error(
|
|
18168
|
-
"[Channel] Invalid SSE event structure (skipping), eventId=%s:",
|
|
18169
|
-
eventId,
|
|
18170
|
-
data.slice(0, 200)
|
|
18318
|
+
try {
|
|
18319
|
+
while (true) {
|
|
18320
|
+
const { done, value } = await reader.read();
|
|
18321
|
+
if (done) {
|
|
18322
|
+
if (inactivityTimedOut) throw new Error("SSE inactivity timeout");
|
|
18323
|
+
throw new Error("SSE stream ended");
|
|
18324
|
+
}
|
|
18325
|
+
lastActivityAt = Date.now();
|
|
18326
|
+
buffer += decoder.decode(value, { stream: true });
|
|
18327
|
+
if (buffer.length > CHANNEL_MAX_SSE_BUFFER_BYTES) {
|
|
18328
|
+
throw new Error(
|
|
18329
|
+
`SSE buffer exceeded ${CHANNEL_MAX_SSE_BUFFER_BYTES} bytes without a frame boundary`
|
|
18171
18330
|
);
|
|
18172
|
-
if (eventId) onEventId(eventId);
|
|
18173
|
-
continue;
|
|
18174
18331
|
}
|
|
18175
|
-
|
|
18176
|
-
|
|
18177
|
-
|
|
18178
|
-
|
|
18332
|
+
let boundary;
|
|
18333
|
+
while ((boundary = buffer.indexOf("\n\n")) !== -1) {
|
|
18334
|
+
const frame = buffer.slice(0, boundary);
|
|
18335
|
+
buffer = buffer.slice(boundary + 2);
|
|
18336
|
+
if (frame.startsWith(":")) continue;
|
|
18337
|
+
let eventId;
|
|
18338
|
+
let data;
|
|
18339
|
+
for (const line of frame.split("\n")) {
|
|
18340
|
+
if (line.startsWith("id: ")) eventId = line.slice(4);
|
|
18341
|
+
else if (line.startsWith("data: ")) data = line.slice(6);
|
|
18342
|
+
}
|
|
18343
|
+
if (!data) continue;
|
|
18344
|
+
let event;
|
|
18345
|
+
try {
|
|
18346
|
+
event = parseTandemEvent(JSON.parse(data));
|
|
18347
|
+
} catch {
|
|
18348
|
+
console.error(
|
|
18349
|
+
"[Channel] Malformed SSE event data (skipping), eventId=%s:",
|
|
18350
|
+
eventId,
|
|
18351
|
+
data.slice(0, 200)
|
|
18352
|
+
);
|
|
18179
18353
|
if (eventId) onEventId(eventId);
|
|
18180
18354
|
continue;
|
|
18181
18355
|
}
|
|
18182
|
-
|
|
18183
|
-
|
|
18184
|
-
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
18356
|
+
if (!event) {
|
|
18357
|
+
console.error(
|
|
18358
|
+
"[Channel] Invalid SSE event structure (skipping), eventId=%s:",
|
|
18359
|
+
eventId,
|
|
18360
|
+
data.slice(0, 200)
|
|
18361
|
+
);
|
|
18362
|
+
if (eventId) onEventId(eventId);
|
|
18363
|
+
continue;
|
|
18364
|
+
}
|
|
18365
|
+
if (event.type !== "chat:message") {
|
|
18366
|
+
const mode = await getCachedMode(tandemUrl);
|
|
18367
|
+
if (mode === "solo") {
|
|
18368
|
+
console.error(`[Channel] Solo mode: suppressed ${event.type} event`);
|
|
18369
|
+
if (eventId) onEventId(eventId);
|
|
18370
|
+
continue;
|
|
18189
18371
|
}
|
|
18190
|
-
}
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18372
|
+
}
|
|
18373
|
+
try {
|
|
18374
|
+
await mcp.notification({
|
|
18375
|
+
method: "notifications/claude/channel",
|
|
18376
|
+
params: {
|
|
18377
|
+
content: formatEventContent(event),
|
|
18378
|
+
meta: formatEventMeta(event)
|
|
18379
|
+
}
|
|
18380
|
+
});
|
|
18381
|
+
} catch (err) {
|
|
18382
|
+
console.error("[Channel] MCP notification failed (transport broken?):", err);
|
|
18383
|
+
throw err;
|
|
18384
|
+
}
|
|
18385
|
+
if (eventId) onEventId(eventId);
|
|
18386
|
+
scheduleAwareness(event);
|
|
18194
18387
|
}
|
|
18195
|
-
if (eventId) onEventId(eventId);
|
|
18196
|
-
scheduleAwareness(event);
|
|
18197
18388
|
}
|
|
18389
|
+
} finally {
|
|
18390
|
+
clearInterval(watchdog);
|
|
18391
|
+
if (awarenessTimer) clearTimeout(awarenessTimer);
|
|
18392
|
+
if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
|
|
18198
18393
|
}
|
|
18199
18394
|
}
|
|
18200
18395
|
var cachedMode = "tandem";
|
|
@@ -18203,7 +18398,7 @@ async function getCachedMode(tandemUrl) {
|
|
|
18203
18398
|
const now = Date.now();
|
|
18204
18399
|
if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;
|
|
18205
18400
|
try {
|
|
18206
|
-
const res = await
|
|
18401
|
+
const res = await fetchWithTimeout(`${tandemUrl}/api/mode`, {}, CHANNEL_MODE_FETCH_TIMEOUT_MS);
|
|
18207
18402
|
if (res.ok) {
|
|
18208
18403
|
const { mode } = await res.json();
|
|
18209
18404
|
cachedMode = mode;
|
|
@@ -18214,7 +18409,7 @@ async function getCachedMode(tandemUrl) {
|
|
|
18214
18409
|
} catch (err) {
|
|
18215
18410
|
console.error(
|
|
18216
18411
|
"[Channel] Mode check failed, delivering event (fail-open):",
|
|
18217
|
-
err
|
|
18412
|
+
describeFetchError(err, "/api/mode", CHANNEL_MODE_FETCH_TIMEOUT_MS)
|
|
18218
18413
|
);
|
|
18219
18414
|
cachedModeAt = now;
|
|
18220
18415
|
}
|
|
@@ -18241,7 +18436,7 @@ async function runChannel(opts = {}) {
|
|
|
18241
18436
|
"Event types: annotation:created, annotation:accepted, annotation:dismissed, annotation:reply,",
|
|
18242
18437
|
"chat:message, document:opened, document:closed, document:switched.",
|
|
18243
18438
|
"Chat messages may include a 'selection' field with buffered selection context.",
|
|
18244
|
-
"Use your tandem MCP tools (tandem_getTextContent, tandem_comment,
|
|
18439
|
+
"Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_edit, etc.) to act on them.",
|
|
18245
18440
|
"Reply to chat messages using tandem_reply. Pass document_id from the tag attributes.",
|
|
18246
18441
|
"Do not reply to non-chat events \u2014 just act on them using tools.",
|
|
18247
18442
|
"If you haven't received channel notifications recently, call tandem_checkInbox as a fallback."
|
|
@@ -18275,15 +18470,20 @@ async function runChannel(opts = {}) {
|
|
|
18275
18470
|
if (req.params.name === "tandem_reply") {
|
|
18276
18471
|
const args = req.params.arguments;
|
|
18277
18472
|
try {
|
|
18278
|
-
const res = await
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18282
|
-
|
|
18473
|
+
const res = await fetchWithTimeout(
|
|
18474
|
+
`${tandemUrl}/api/channel-reply`,
|
|
18475
|
+
{
|
|
18476
|
+
method: "POST",
|
|
18477
|
+
headers: { "Content-Type": "application/json" },
|
|
18478
|
+
body: JSON.stringify(args)
|
|
18479
|
+
},
|
|
18480
|
+
CHANNEL_REPLY_FETCH_TIMEOUT_MS
|
|
18481
|
+
);
|
|
18283
18482
|
let data;
|
|
18284
18483
|
try {
|
|
18285
18484
|
data = await res.json();
|
|
18286
|
-
} catch {
|
|
18485
|
+
} catch (parseErr) {
|
|
18486
|
+
if (isAbortOrTimeoutError(parseErr)) throw parseErr;
|
|
18287
18487
|
data = { message: "Non-JSON response" };
|
|
18288
18488
|
}
|
|
18289
18489
|
if (!res.ok) {
|
|
@@ -18303,7 +18503,11 @@ async function runChannel(opts = {}) {
|
|
|
18303
18503
|
content: [
|
|
18304
18504
|
{
|
|
18305
18505
|
type: "text",
|
|
18306
|
-
text: `Failed to send reply: ${
|
|
18506
|
+
text: `Failed to send reply: ${describeFetchError(
|
|
18507
|
+
err,
|
|
18508
|
+
"/api/channel-reply",
|
|
18509
|
+
CHANNEL_REPLY_FETCH_TIMEOUT_MS
|
|
18510
|
+
)}`
|
|
18307
18511
|
}
|
|
18308
18512
|
],
|
|
18309
18513
|
isError: true
|
|
@@ -18323,23 +18527,30 @@ async function runChannel(opts = {}) {
|
|
|
18323
18527
|
});
|
|
18324
18528
|
mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
|
|
18325
18529
|
try {
|
|
18326
|
-
const res = await
|
|
18327
|
-
|
|
18328
|
-
|
|
18329
|
-
|
|
18330
|
-
|
|
18331
|
-
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
|
|
18335
|
-
|
|
18530
|
+
const res = await fetchWithTimeout(
|
|
18531
|
+
`${tandemUrl}/api/channel-permission`,
|
|
18532
|
+
{
|
|
18533
|
+
method: "POST",
|
|
18534
|
+
headers: { "Content-Type": "application/json" },
|
|
18535
|
+
body: JSON.stringify({
|
|
18536
|
+
requestId: params.request_id,
|
|
18537
|
+
toolName: params.tool_name,
|
|
18538
|
+
description: params.description,
|
|
18539
|
+
inputPreview: params.input_preview
|
|
18540
|
+
})
|
|
18541
|
+
},
|
|
18542
|
+
CHANNEL_PERMISSION_FETCH_TIMEOUT_MS
|
|
18543
|
+
);
|
|
18336
18544
|
if (!res.ok) {
|
|
18337
18545
|
console.error(
|
|
18338
18546
|
`[Channel] Permission relay got HTTP ${res.status} \u2014 browser may not see prompt`
|
|
18339
18547
|
);
|
|
18340
18548
|
}
|
|
18341
18549
|
} catch (err) {
|
|
18342
|
-
console.error(
|
|
18550
|
+
console.error(
|
|
18551
|
+
"[Channel] Failed to forward permission request:",
|
|
18552
|
+
describeFetchError(err, "/api/channel-permission", CHANNEL_PERMISSION_FETCH_TIMEOUT_MS)
|
|
18553
|
+
);
|
|
18343
18554
|
}
|
|
18344
18555
|
});
|
|
18345
18556
|
console.error(`[Channel] Tandem channel shim starting (server: ${tandemUrl})`);
|