kojee-mcp 0.2.1 → 0.4.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/README.md +140 -7
- package/dist/chunk-36DMIXH7.js +51 -0
- package/dist/chunk-E7TE4QZD.js +33 -0
- package/dist/chunk-VZVGTHGF.js +142 -0
- package/dist/chunk-WHTH6WBP.js +72 -0
- package/dist/{chunk-RNKJONY3.js → chunk-ZGVUM4AG.js} +437 -24
- package/dist/cli.js +195 -25
- package/dist/event-log-ETWR6PPY.js +112 -0
- package/dist/event-queue-5YVJFR3E.js +43 -0
- package/dist/hook-server-43QS7L7P.js +71 -0
- package/dist/index.d.ts +0 -10
- package/dist/index.js +2 -1
- package/dist/install-WV25CRU2.js +182 -0
- package/dist/paired-config-OAR3O3XY.js +10 -0
- package/dist/session-discovery-WSHLR4OV.js +26 -0
- package/dist/stop-hook-5XU3EQAE.js +76 -0
- package/dist/user-prompt-submit-hook-WSRIJVF4.js +54 -0
- package/package.json +9 -13
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deriveDiscoveryKey,
|
|
3
|
+
findClaudeAncestorPid
|
|
4
|
+
} from "./chunk-36DMIXH7.js";
|
|
5
|
+
|
|
1
6
|
// src/index.ts
|
|
7
|
+
import fs2 from "fs";
|
|
2
8
|
import path2 from "path";
|
|
3
9
|
|
|
4
10
|
// src/auth/auth-module.ts
|
|
@@ -277,9 +283,41 @@ function translateHttpError(status, errorCode, _trigger) {
|
|
|
277
283
|
}
|
|
278
284
|
return null;
|
|
279
285
|
}
|
|
286
|
+
var TANDEM_ERROR_MESSAGES = {
|
|
287
|
+
[-32003]: () => "This Tandem is hardened to owner-only membership; you can't join.",
|
|
288
|
+
[-32004]: () => "You aren't a member of that Tandem. Use tandem_join(join_link) first.",
|
|
289
|
+
[-32006]: (data) => {
|
|
290
|
+
const retry = data?.["retry_after_seconds"] ?? "a moment";
|
|
291
|
+
return `Rate limit hit on this Tandem. Retry after ${retry} seconds.`;
|
|
292
|
+
},
|
|
293
|
+
[-32007]: (data) => {
|
|
294
|
+
const rule = data?.["rule"] ?? "policy";
|
|
295
|
+
return `Message rejected by Tandem policy (${rule}).`;
|
|
296
|
+
},
|
|
297
|
+
[-32011]: (data) => {
|
|
298
|
+
const id = data?.["tandem_id"] ?? "unknown";
|
|
299
|
+
return `Tandem ${id} doesn't exist or isn't visible to you.`;
|
|
300
|
+
},
|
|
301
|
+
[-32015]: (data) => {
|
|
302
|
+
const candidates = data?.["candidates"] ?? [];
|
|
303
|
+
return `An @-mention matched multiple members and is ambiguous. Retry with explicit mentions[]. Candidates: ${candidates.join(", ")}.`;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
280
306
|
function translateJsonRpcError(error) {
|
|
281
307
|
const msg = error.message ?? "";
|
|
282
308
|
const msgLower = msg.toLowerCase();
|
|
309
|
+
const tandemMessage = TANDEM_ERROR_MESSAGES[error.code];
|
|
310
|
+
if (tandemMessage) {
|
|
311
|
+
return {
|
|
312
|
+
content: [
|
|
313
|
+
{
|
|
314
|
+
type: "text",
|
|
315
|
+
text: tandemMessage(error.data)
|
|
316
|
+
}
|
|
317
|
+
],
|
|
318
|
+
isError: true
|
|
319
|
+
};
|
|
320
|
+
}
|
|
283
321
|
switch (error.code) {
|
|
284
322
|
case -32601:
|
|
285
323
|
return makeError(
|
|
@@ -346,6 +384,10 @@ function makeError(text) {
|
|
|
346
384
|
};
|
|
347
385
|
}
|
|
348
386
|
|
|
387
|
+
// src/tandem/session-id.ts
|
|
388
|
+
import { ulid } from "ulidx";
|
|
389
|
+
var MCP_SESSION_ID = ulid();
|
|
390
|
+
|
|
349
391
|
// src/gateway-client.ts
|
|
350
392
|
var STEP_UP_POLL_INTERVAL_MS = 5e3;
|
|
351
393
|
var STEP_UP_MAX_TIMEOUT_MS = 3e5;
|
|
@@ -364,6 +406,21 @@ var GatewayClient = class {
|
|
|
364
406
|
currentNonce;
|
|
365
407
|
requestCounter = 0;
|
|
366
408
|
endpoint;
|
|
409
|
+
/**
|
|
410
|
+
* Expose the DPoP signing key so peer modules sharing auth state
|
|
411
|
+
* (e.g. tandem/event-stream.ts) can sign their own requests.
|
|
412
|
+
*/
|
|
413
|
+
getPrivateKey() {
|
|
414
|
+
return this.privateKey;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Expose the bot_key_id (kid) for DPoP proof headers. Paired with
|
|
418
|
+
* getPrivateKey() so peer modules can construct proofs without
|
|
419
|
+
* threading the key material through their own constructors.
|
|
420
|
+
*/
|
|
421
|
+
getKid() {
|
|
422
|
+
return this.kid;
|
|
423
|
+
}
|
|
367
424
|
/**
|
|
368
425
|
* Derive a deterministic session ID from the gateway token.
|
|
369
426
|
* session_id = sha256(token + "proxy").slice(0, 16)
|
|
@@ -446,7 +503,8 @@ var GatewayClient = class {
|
|
|
446
503
|
headers: {
|
|
447
504
|
"Content-Type": "application/json",
|
|
448
505
|
Authorization: `DPoP ${this.token}`,
|
|
449
|
-
DPoP: proof
|
|
506
|
+
DPoP: proof,
|
|
507
|
+
"Mcp-Session-Id": MCP_SESSION_ID
|
|
450
508
|
},
|
|
451
509
|
body: JSON.stringify(rpcRequest)
|
|
452
510
|
});
|
|
@@ -590,16 +648,28 @@ import {
|
|
|
590
648
|
ListToolsRequestSchema,
|
|
591
649
|
CallToolRequestSchema
|
|
592
650
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
593
|
-
function
|
|
651
|
+
function buildChannelInstructions(tandemMembershipCount, eventLogPath) {
|
|
652
|
+
const intro = 'Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:\n\n(1) If channel notifications are available, you\'ll see them as `<channel source="kojee-mcp" ...>` tags in your context. Each tag carries tandem_id, message_id, from_principal, kind, severity. Reply with tandem_send(tandem_id, text, reply_to=<message_id>); read more context with tandem_messages(tandem_id, since=cursor).\n\n';
|
|
653
|
+
const monitorSection = `(2) For sustained wake-from-idle WITHOUT channel notifications, the proxy writes one event line per Tandem message to ${eventLogPath}. At the start of every session, spawn ONE Monitor task: Monitor(command="tail -n +1 -F ${eventLogPath}", persistent=true, description="kojee Tandem events"). Each appended line will arrive as a separate wake notification. Use the cursor in the line to call tandem_messages(tandem_id, since=cursor-1) for the full message body.
|
|
654
|
+
|
|
655
|
+
`;
|
|
656
|
+
const listenSection = "(3) If you want to BLOCK until any single reply lands (rather than receive a stream of events), call tandem_listen(tandem_id, since=cursor, timeout_ms=N) instead.";
|
|
657
|
+
const advice = "\n\nPrefer (2) at session start \u2014 it's the default no-allowlist wake mechanism. (1) supplements it when channels are enabled; (3) is for one-shot blocking waits.";
|
|
658
|
+
if (tandemMembershipCount === 0) {
|
|
659
|
+
return intro + listenSection;
|
|
660
|
+
}
|
|
661
|
+
return intro + monitorSection + listenSection + advice;
|
|
662
|
+
}
|
|
663
|
+
function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath) {
|
|
664
|
+
const capabilities = { tools: {} };
|
|
665
|
+
if (adapter.supportsChannels) {
|
|
666
|
+
capabilities.experimental = { "claude/channel": {} };
|
|
667
|
+
}
|
|
594
668
|
const server = new Server(
|
|
669
|
+
{ name: "kojee-mcp", version: "0.3.0" },
|
|
595
670
|
{
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
},
|
|
599
|
-
{
|
|
600
|
-
capabilities: {
|
|
601
|
-
tools: {}
|
|
602
|
-
}
|
|
671
|
+
capabilities,
|
|
672
|
+
...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
|
|
603
673
|
}
|
|
604
674
|
);
|
|
605
675
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -614,10 +684,7 @@ function createMcpServer(registry) {
|
|
|
614
684
|
const { name, arguments: args } = request.params;
|
|
615
685
|
const rawResult = await registry.callTool(name, args ?? {});
|
|
616
686
|
const result = translateToolCallResult(rawResult);
|
|
617
|
-
return {
|
|
618
|
-
content: result.content,
|
|
619
|
-
isError: result.isError
|
|
620
|
-
};
|
|
687
|
+
return { content: result.content, isError: result.isError };
|
|
621
688
|
});
|
|
622
689
|
return server;
|
|
623
690
|
}
|
|
@@ -627,15 +694,353 @@ async function startMcpServer(server) {
|
|
|
627
694
|
console.error("[mcp] Server started on stdio transport");
|
|
628
695
|
}
|
|
629
696
|
|
|
697
|
+
// src/runtime/detect.ts
|
|
698
|
+
import childProcess from "child_process";
|
|
699
|
+
function detectRuntime(env = process.env) {
|
|
700
|
+
if (env["KOJEE_RUNTIME"] === "claude-code") return "claude-code";
|
|
701
|
+
if (env["CLAUDE_CODE_SESSION_ID"]) return "claude-code";
|
|
702
|
+
if (parentProcessLooksLikeClaude()) return "claude-code";
|
|
703
|
+
return "unknown";
|
|
704
|
+
}
|
|
705
|
+
function parentProcessLooksLikeClaude() {
|
|
706
|
+
if (process.platform === "win32") return false;
|
|
707
|
+
try {
|
|
708
|
+
let pid = process.ppid;
|
|
709
|
+
for (let depth = 0; depth < 5 && pid && pid > 1; depth++) {
|
|
710
|
+
const cmd = childProcess.execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
711
|
+
encoding: "utf8",
|
|
712
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
713
|
+
}).trim();
|
|
714
|
+
if (/(^|\/)claude(\.app)?(\/|$|\s|\\)/i.test(cmd) || /Claude Helper/i.test(cmd)) {
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
const ppidOut = childProcess.execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
|
|
718
|
+
encoding: "utf8",
|
|
719
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
720
|
+
}).trim();
|
|
721
|
+
pid = Number.parseInt(ppidOut, 10);
|
|
722
|
+
if (!Number.isFinite(pid)) return false;
|
|
723
|
+
}
|
|
724
|
+
} catch {
|
|
725
|
+
}
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/adapters/claude-code.ts
|
|
730
|
+
function computeSeverity(event) {
|
|
731
|
+
if (event.type === "state_change") return "high";
|
|
732
|
+
if (event.mentions && event.mentions.length > 0) {
|
|
733
|
+
return "high";
|
|
734
|
+
}
|
|
735
|
+
return "normal";
|
|
736
|
+
}
|
|
737
|
+
function formatBody(event) {
|
|
738
|
+
if (event.type === "state_change") {
|
|
739
|
+
return `[Tandem: ${event.tandem_id}] ${event.from.displayname}: ${event.content.body}`;
|
|
740
|
+
}
|
|
741
|
+
return [
|
|
742
|
+
`[Tandem: ${event.tandem_id}] ${event.from.displayname} (${event.from.principal}) \u2014 ${event.kind}:`,
|
|
743
|
+
event.content.body,
|
|
744
|
+
"",
|
|
745
|
+
`> reply with tandem_send(tandem_id="${event.tandem_id}", text="...", reply_to="${event.id}")`
|
|
746
|
+
].join("\n");
|
|
747
|
+
}
|
|
748
|
+
var claudeCodeAdapter = {
|
|
749
|
+
runtime: "claude-code",
|
|
750
|
+
supportsChannels: true,
|
|
751
|
+
formatTandemEvent(event) {
|
|
752
|
+
const meta = {
|
|
753
|
+
tandem_id: event.tandem_id,
|
|
754
|
+
message_id: event.id,
|
|
755
|
+
cursor: String(event.cursor),
|
|
756
|
+
kind: event.kind,
|
|
757
|
+
from_principal: event.from.principal,
|
|
758
|
+
from_display: event.from.displayname,
|
|
759
|
+
severity: computeSeverity(event)
|
|
760
|
+
};
|
|
761
|
+
return { content: formatBody(event), meta };
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// src/adapters/unknown.ts
|
|
766
|
+
var unknownAdapter = {
|
|
767
|
+
runtime: "unknown",
|
|
768
|
+
supportsChannels: false,
|
|
769
|
+
formatTandemEvent() {
|
|
770
|
+
throw new Error(
|
|
771
|
+
"unknownAdapter.formatTandemEvent() is unreachable \u2014 caller should gate on supportsChannels"
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// src/tandem/event-stream.ts
|
|
777
|
+
async function startEventStream(opts) {
|
|
778
|
+
let stopped = false;
|
|
779
|
+
let lastCursor = null;
|
|
780
|
+
const controller = new AbortController();
|
|
781
|
+
void (async function loop() {
|
|
782
|
+
let backoffMs = 1e3;
|
|
783
|
+
while (!stopped) {
|
|
784
|
+
try {
|
|
785
|
+
await connectAndConsume(opts, lastCursor, controller, (cursor) => {
|
|
786
|
+
lastCursor = cursor;
|
|
787
|
+
backoffMs = 1e3;
|
|
788
|
+
});
|
|
789
|
+
} catch (err) {
|
|
790
|
+
if (stopped) return;
|
|
791
|
+
console.error("[event-stream] disconnect:", err.message);
|
|
792
|
+
}
|
|
793
|
+
if (stopped) return;
|
|
794
|
+
const jitter = Math.random() * backoffMs;
|
|
795
|
+
await sleep2(jitter);
|
|
796
|
+
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
797
|
+
}
|
|
798
|
+
})();
|
|
799
|
+
return () => {
|
|
800
|
+
stopped = true;
|
|
801
|
+
controller.abort();
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
async function connectAndConsume(opts, sinceCursor, controller, onCursor) {
|
|
805
|
+
const params = new URLSearchParams();
|
|
806
|
+
if (sinceCursor !== null) params.set("since", String(sinceCursor));
|
|
807
|
+
const url = `${opts.brokerUrl}/api/v2/tandems/stream${params.toString() ? "?" + params.toString() : ""}`;
|
|
808
|
+
const proof = await createDPoPProof(
|
|
809
|
+
opts.gateway.getPrivateKey(),
|
|
810
|
+
opts.gateway.getKid(),
|
|
811
|
+
"GET",
|
|
812
|
+
url,
|
|
813
|
+
void 0,
|
|
814
|
+
opts.token
|
|
815
|
+
);
|
|
816
|
+
const res = await fetch(url, {
|
|
817
|
+
method: "GET",
|
|
818
|
+
headers: {
|
|
819
|
+
Authorization: `DPoP ${opts.token}`,
|
|
820
|
+
DPoP: proof,
|
|
821
|
+
"Mcp-Session-Id": MCP_SESSION_ID,
|
|
822
|
+
Accept: "text/event-stream"
|
|
823
|
+
},
|
|
824
|
+
signal: controller.signal
|
|
825
|
+
});
|
|
826
|
+
if (!res.ok) throw new Error(`SSE connect failed: ${res.status}`);
|
|
827
|
+
if (!res.body) throw new Error("SSE response has no body");
|
|
828
|
+
await consumeSse(res.body, opts, onCursor);
|
|
829
|
+
}
|
|
830
|
+
async function consumeSse(body, opts, onCursor) {
|
|
831
|
+
const reader = body.getReader();
|
|
832
|
+
const decoder = new TextDecoder();
|
|
833
|
+
let buffer = "";
|
|
834
|
+
while (true) {
|
|
835
|
+
const { value, done } = await reader.read();
|
|
836
|
+
if (done) return;
|
|
837
|
+
buffer += decoder.decode(value, { stream: true });
|
|
838
|
+
const events = drainSseEvents(buffer);
|
|
839
|
+
buffer = events.remaining;
|
|
840
|
+
for (const evt of events.events) {
|
|
841
|
+
if (evt.event === "stream_revoked") {
|
|
842
|
+
throw new Error("stream_revoked \u2014 reconnect needed");
|
|
843
|
+
}
|
|
844
|
+
if (evt.event === "heartbeat") continue;
|
|
845
|
+
if (evt.event === "message" || evt.event === "state_change") {
|
|
846
|
+
try {
|
|
847
|
+
const raw = JSON.parse(evt.data);
|
|
848
|
+
const parsed = normalizeBackendEvent(raw, evt.event);
|
|
849
|
+
onCursor(parsed.cursor);
|
|
850
|
+
opts.queue?.push(parsed);
|
|
851
|
+
const channel = opts.adapter.formatTandemEvent(parsed);
|
|
852
|
+
await opts.server.notification({
|
|
853
|
+
method: "notifications/claude/channel",
|
|
854
|
+
params: channel
|
|
855
|
+
});
|
|
856
|
+
opts.queue?.markChannelDelivered(parsed.id);
|
|
857
|
+
if (opts.eventLog) {
|
|
858
|
+
try {
|
|
859
|
+
await opts.eventLog.append(parsed);
|
|
860
|
+
opts.queue?.markMonitorDelivered(parsed.id);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
console.error("[event-stream] event-log append failed:", err);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
} catch (err) {
|
|
866
|
+
console.error("[event-stream] failed to handle event:", err);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function drainSseEvents(input) {
|
|
873
|
+
const events = [];
|
|
874
|
+
const parts = input.split("\n\n");
|
|
875
|
+
const remaining = parts.pop() ?? "";
|
|
876
|
+
for (const block of parts) {
|
|
877
|
+
let id;
|
|
878
|
+
let event = "message";
|
|
879
|
+
const dataLines = [];
|
|
880
|
+
for (const line of block.split("\n")) {
|
|
881
|
+
if (line.startsWith("id: ")) id = line.slice(4);
|
|
882
|
+
else if (line.startsWith("event: ")) event = line.slice(7);
|
|
883
|
+
else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
|
|
884
|
+
}
|
|
885
|
+
if (dataLines.length > 0) events.push({ id, event, data: dataLines.join("\n") });
|
|
886
|
+
}
|
|
887
|
+
return { events, remaining };
|
|
888
|
+
}
|
|
889
|
+
function sleep2(ms) {
|
|
890
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
891
|
+
}
|
|
892
|
+
function normalizeBackendEvent(raw, sseEventType) {
|
|
893
|
+
const obj = raw ?? {};
|
|
894
|
+
const maybeFrom = obj["from"];
|
|
895
|
+
if (maybeFrom && typeof maybeFrom["principal"] === "string") {
|
|
896
|
+
return raw;
|
|
897
|
+
}
|
|
898
|
+
const sender = obj["sender"] ?? {};
|
|
899
|
+
const principal = sender["principal_id"] ?? "";
|
|
900
|
+
const agentId = sender["agent_id"];
|
|
901
|
+
const displayname = principal ? `principal:${principal.slice(0, 8)}` : "unknown";
|
|
902
|
+
const type = sseEventType === "state_change" ? "state_change" : "message";
|
|
903
|
+
const kind = obj["kind"] ?? "message";
|
|
904
|
+
return {
|
|
905
|
+
type,
|
|
906
|
+
id: obj["message_id"] ?? obj["id"] ?? "",
|
|
907
|
+
tandem_id: obj["tandem_id"] ?? "",
|
|
908
|
+
cursor: obj["cursor"] ?? 0,
|
|
909
|
+
time: obj["time"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
910
|
+
from: {
|
|
911
|
+
member_id: "",
|
|
912
|
+
principal,
|
|
913
|
+
...agentId ? { agent_id: agentId } : {},
|
|
914
|
+
displayname
|
|
915
|
+
},
|
|
916
|
+
kind,
|
|
917
|
+
content: {
|
|
918
|
+
body: obj["body"] ?? "",
|
|
919
|
+
...typeof obj["format"] === "string" ? { format: obj["format"] } : {}
|
|
920
|
+
},
|
|
921
|
+
...Array.isArray(obj["mentions"]) ? { mentions: obj["mentions"] } : {},
|
|
922
|
+
...obj["reply_to"] !== void 0 ? { reply_to: obj["reply_to"] } : {}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
630
926
|
// src/index.ts
|
|
631
927
|
var DEFAULT_KEYSTORE_PATH = path2.join(
|
|
632
928
|
process.env["HOME"] ?? "~",
|
|
633
929
|
".kojee",
|
|
634
930
|
"keypair.json"
|
|
635
931
|
);
|
|
932
|
+
function isDPoPEnrollmentError(err) {
|
|
933
|
+
const msg = String(err?.message ?? err ?? "").toLowerCase();
|
|
934
|
+
if (msg.includes("invalid or expired") && msg.includes("token")) return false;
|
|
935
|
+
if (msg.includes("generate a new")) return false;
|
|
936
|
+
return msg.includes("401") || msg.includes("authentication failed") || msg.includes("dpop") || msg.includes("key enrollment") || msg.includes("kid");
|
|
937
|
+
}
|
|
938
|
+
function selectAdapter() {
|
|
939
|
+
const runtime = detectRuntime();
|
|
940
|
+
if (runtime === "claude-code") return claudeCodeAdapter;
|
|
941
|
+
return unknownAdapter;
|
|
942
|
+
}
|
|
636
943
|
async function startProxy(config) {
|
|
637
944
|
const keystorePath = config.keystorePath || DEFAULT_KEYSTORE_PATH;
|
|
638
|
-
|
|
945
|
+
const adapter = selectAdapter();
|
|
946
|
+
console.error(`[kojee-mcp] Starting proxy for ${config.url} (runtime=${adapter.runtime})`);
|
|
947
|
+
const { registry, gateway } = await enrollAndDiscover(config, keystorePath);
|
|
948
|
+
console.error(
|
|
949
|
+
`[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
|
|
950
|
+
);
|
|
951
|
+
let tandemMembershipCount = -1;
|
|
952
|
+
try {
|
|
953
|
+
const result = await gateway.sendRpc("tools/call", { name: "tandem_list", arguments: {} });
|
|
954
|
+
const maybeErr = result;
|
|
955
|
+
if (!maybeErr.isError) {
|
|
956
|
+
const text = maybeErr.content?.[0]?.text;
|
|
957
|
+
try {
|
|
958
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
959
|
+
if (Array.isArray(parsed.tandems)) {
|
|
960
|
+
tandemMembershipCount = parsed.tandems.length;
|
|
961
|
+
} else if (Array.isArray(parsed)) {
|
|
962
|
+
tandemMembershipCount = parsed.length;
|
|
963
|
+
} else {
|
|
964
|
+
tandemMembershipCount = 0;
|
|
965
|
+
}
|
|
966
|
+
} catch {
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
} catch (err) {
|
|
970
|
+
console.error("[kojee-mcp] tandem_list probe failed:", err.message);
|
|
971
|
+
}
|
|
972
|
+
console.error(`[kojee-mcp] Tandem memberships: ${tandemMembershipCount === -1 ? "unknown" : tandemMembershipCount}`);
|
|
973
|
+
let server;
|
|
974
|
+
if (adapter.supportsChannels) {
|
|
975
|
+
const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
|
|
976
|
+
const { startHookServer } = await import("./hook-server-43QS7L7P.js");
|
|
977
|
+
const {
|
|
978
|
+
writeDiscoveryByKey,
|
|
979
|
+
cleanupDiscoveryByKey,
|
|
980
|
+
sweepStaleDiscovery
|
|
981
|
+
} = await import("./session-discovery-WSHLR4OV.js");
|
|
982
|
+
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-ETWR6PPY.js");
|
|
983
|
+
sweepStaleDiscovery();
|
|
984
|
+
sweepStaleEventLogs();
|
|
985
|
+
const ccPid = findClaudeAncestorPid();
|
|
986
|
+
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
987
|
+
const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
|
|
988
|
+
const eventLog = startEventLog({ key: discoveryKey });
|
|
989
|
+
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path);
|
|
990
|
+
const queue = new EventQueue();
|
|
991
|
+
const hookServer = await startHookServer({ port: 0, queue, adapter });
|
|
992
|
+
writeDiscoveryByKey(discoveryKey, {
|
|
993
|
+
schema: 2,
|
|
994
|
+
discoveryKey,
|
|
995
|
+
ccPid,
|
|
996
|
+
projectDir: projectDir ?? null,
|
|
997
|
+
proxyPid: process.pid,
|
|
998
|
+
// Legacy `pid` mirrors `proxyPid` so the existing event-log sweep
|
|
999
|
+
// (which reads `data.pid`) still considers this entry live.
|
|
1000
|
+
pid: process.pid,
|
|
1001
|
+
port: hookServer.port,
|
|
1002
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1003
|
+
brokerUrl: config.url
|
|
1004
|
+
});
|
|
1005
|
+
const cleanupDiscoveryFile = () => cleanupDiscoveryByKey(discoveryKey);
|
|
1006
|
+
process.on("exit", () => cleanupDiscoveryFile());
|
|
1007
|
+
process.on("SIGINT", () => {
|
|
1008
|
+
cleanupDiscoveryFile();
|
|
1009
|
+
process.exit(0);
|
|
1010
|
+
});
|
|
1011
|
+
process.on("SIGTERM", () => {
|
|
1012
|
+
cleanupDiscoveryFile();
|
|
1013
|
+
process.exit(0);
|
|
1014
|
+
});
|
|
1015
|
+
process.on("exit", () => eventLog.cleanup());
|
|
1016
|
+
const cancelStream = await startEventStream({
|
|
1017
|
+
brokerUrl: config.url,
|
|
1018
|
+
token: config.token,
|
|
1019
|
+
gateway,
|
|
1020
|
+
adapter,
|
|
1021
|
+
server,
|
|
1022
|
+
queue,
|
|
1023
|
+
eventLog
|
|
1024
|
+
});
|
|
1025
|
+
process.stdin.on("end", () => {
|
|
1026
|
+
cancelStream();
|
|
1027
|
+
cleanupDiscoveryFile();
|
|
1028
|
+
eventLog.cleanup();
|
|
1029
|
+
hookServer.stop().finally(() => {
|
|
1030
|
+
console.error("[kojee-mcp] stdin closed, exiting");
|
|
1031
|
+
process.exit(0);
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
} else {
|
|
1035
|
+
server = createMcpServer(registry, adapter, tandemMembershipCount);
|
|
1036
|
+
process.stdin.on("end", () => {
|
|
1037
|
+
console.error("[kojee-mcp] stdin closed, exiting");
|
|
1038
|
+
process.exit(0);
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
await startMcpServer(server);
|
|
1042
|
+
}
|
|
1043
|
+
async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
639
1044
|
const auth = new AuthModule(config.token, config.url, keystorePath);
|
|
640
1045
|
const keyPair = await auth.ensureEnrolled();
|
|
641
1046
|
const sessionId = GatewayClient.deriveSessionId(config.token);
|
|
@@ -648,18 +1053,26 @@ async function startProxy(config) {
|
|
|
648
1053
|
sessionId
|
|
649
1054
|
);
|
|
650
1055
|
const registry = new ToolRegistry(gateway);
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
)
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1056
|
+
try {
|
|
1057
|
+
await registry.discoverTools();
|
|
1058
|
+
return { registry, gateway };
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
if (isRetry || !isDPoPEnrollmentError(err)) {
|
|
1061
|
+
throw err;
|
|
1062
|
+
}
|
|
1063
|
+
console.error(
|
|
1064
|
+
"[kojee-mcp] Auth failed, attempting recovery with fresh enrollment..."
|
|
1065
|
+
);
|
|
1066
|
+
try {
|
|
1067
|
+
if (fs2.existsSync(keystorePath)) fs2.unlinkSync(keystorePath);
|
|
1068
|
+
} catch (unlinkErr) {
|
|
1069
|
+
console.error("[kojee-mcp] Could not remove stale keystore:", unlinkErr);
|
|
1070
|
+
}
|
|
1071
|
+
return enrollAndDiscover(config, keystorePath, true);
|
|
1072
|
+
}
|
|
661
1073
|
}
|
|
662
1074
|
|
|
663
1075
|
export {
|
|
1076
|
+
AuthModule,
|
|
664
1077
|
startProxy
|
|
665
1078
|
};
|