kojee-mcp 0.2.2 → 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-QKAUM3TR.js → chunk-ZGVUM4AG.js} +409 -24
- package/dist/cli.js +190 -16
- 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 -13
- 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,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deriveDiscoveryKey,
|
|
3
|
+
findClaudeAncestorPid
|
|
4
|
+
} from "./chunk-36DMIXH7.js";
|
|
5
|
+
|
|
1
6
|
// src/index.ts
|
|
2
7
|
import fs2 from "fs";
|
|
3
8
|
import path2 from "path";
|
|
@@ -278,9 +283,41 @@ function translateHttpError(status, errorCode, _trigger) {
|
|
|
278
283
|
}
|
|
279
284
|
return null;
|
|
280
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
|
+
};
|
|
281
306
|
function translateJsonRpcError(error) {
|
|
282
307
|
const msg = error.message ?? "";
|
|
283
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
|
+
}
|
|
284
321
|
switch (error.code) {
|
|
285
322
|
case -32601:
|
|
286
323
|
return makeError(
|
|
@@ -347,6 +384,10 @@ function makeError(text) {
|
|
|
347
384
|
};
|
|
348
385
|
}
|
|
349
386
|
|
|
387
|
+
// src/tandem/session-id.ts
|
|
388
|
+
import { ulid } from "ulidx";
|
|
389
|
+
var MCP_SESSION_ID = ulid();
|
|
390
|
+
|
|
350
391
|
// src/gateway-client.ts
|
|
351
392
|
var STEP_UP_POLL_INTERVAL_MS = 5e3;
|
|
352
393
|
var STEP_UP_MAX_TIMEOUT_MS = 3e5;
|
|
@@ -365,6 +406,21 @@ var GatewayClient = class {
|
|
|
365
406
|
currentNonce;
|
|
366
407
|
requestCounter = 0;
|
|
367
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
|
+
}
|
|
368
424
|
/**
|
|
369
425
|
* Derive a deterministic session ID from the gateway token.
|
|
370
426
|
* session_id = sha256(token + "proxy").slice(0, 16)
|
|
@@ -447,7 +503,8 @@ var GatewayClient = class {
|
|
|
447
503
|
headers: {
|
|
448
504
|
"Content-Type": "application/json",
|
|
449
505
|
Authorization: `DPoP ${this.token}`,
|
|
450
|
-
DPoP: proof
|
|
506
|
+
DPoP: proof,
|
|
507
|
+
"Mcp-Session-Id": MCP_SESSION_ID
|
|
451
508
|
},
|
|
452
509
|
body: JSON.stringify(rpcRequest)
|
|
453
510
|
});
|
|
@@ -591,16 +648,28 @@ import {
|
|
|
591
648
|
ListToolsRequestSchema,
|
|
592
649
|
CallToolRequestSchema
|
|
593
650
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
594
|
-
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
|
+
}
|
|
595
668
|
const server = new Server(
|
|
669
|
+
{ name: "kojee-mcp", version: "0.3.0" },
|
|
596
670
|
{
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
capabilities: {
|
|
602
|
-
tools: {}
|
|
603
|
-
}
|
|
671
|
+
capabilities,
|
|
672
|
+
...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
|
|
604
673
|
}
|
|
605
674
|
);
|
|
606
675
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -615,10 +684,7 @@ function createMcpServer(registry) {
|
|
|
615
684
|
const { name, arguments: args } = request.params;
|
|
616
685
|
const rawResult = await registry.callTool(name, args ?? {});
|
|
617
686
|
const result = translateToolCallResult(rawResult);
|
|
618
|
-
return {
|
|
619
|
-
content: result.content,
|
|
620
|
-
isError: result.isError
|
|
621
|
-
};
|
|
687
|
+
return { content: result.content, isError: result.isError };
|
|
622
688
|
});
|
|
623
689
|
return server;
|
|
624
690
|
}
|
|
@@ -628,6 +694,235 @@ async function startMcpServer(server) {
|
|
|
628
694
|
console.error("[mcp] Server started on stdio transport");
|
|
629
695
|
}
|
|
630
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
|
+
|
|
631
926
|
// src/index.ts
|
|
632
927
|
var DEFAULT_KEYSTORE_PATH = path2.join(
|
|
633
928
|
process.env["HOME"] ?? "~",
|
|
@@ -640,18 +935,109 @@ function isDPoPEnrollmentError(err) {
|
|
|
640
935
|
if (msg.includes("generate a new")) return false;
|
|
641
936
|
return msg.includes("401") || msg.includes("authentication failed") || msg.includes("dpop") || msg.includes("key enrollment") || msg.includes("kid");
|
|
642
937
|
}
|
|
938
|
+
function selectAdapter() {
|
|
939
|
+
const runtime = detectRuntime();
|
|
940
|
+
if (runtime === "claude-code") return claudeCodeAdapter;
|
|
941
|
+
return unknownAdapter;
|
|
942
|
+
}
|
|
643
943
|
async function startProxy(config) {
|
|
644
944
|
const keystorePath = config.keystorePath || DEFAULT_KEYSTORE_PATH;
|
|
645
|
-
|
|
646
|
-
|
|
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);
|
|
647
948
|
console.error(
|
|
648
949
|
`[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
|
|
649
950
|
);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
}
|
|
655
1041
|
await startMcpServer(server);
|
|
656
1042
|
}
|
|
657
1043
|
async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
@@ -669,7 +1055,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
|
669
1055
|
const registry = new ToolRegistry(gateway);
|
|
670
1056
|
try {
|
|
671
1057
|
await registry.discoverTools();
|
|
672
|
-
return registry;
|
|
1058
|
+
return { registry, gateway };
|
|
673
1059
|
} catch (err) {
|
|
674
1060
|
if (isRetry || !isDPoPEnrollmentError(err)) {
|
|
675
1061
|
throw err;
|
|
@@ -678,9 +1064,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
|
678
1064
|
"[kojee-mcp] Auth failed, attempting recovery with fresh enrollment..."
|
|
679
1065
|
);
|
|
680
1066
|
try {
|
|
681
|
-
if (fs2.existsSync(keystorePath))
|
|
682
|
-
fs2.unlinkSync(keystorePath);
|
|
683
|
-
}
|
|
1067
|
+
if (fs2.existsSync(keystorePath)) fs2.unlinkSync(keystorePath);
|
|
684
1068
|
} catch (unlinkErr) {
|
|
685
1069
|
console.error("[kojee-mcp] Could not remove stale keystore:", unlinkErr);
|
|
686
1070
|
}
|
|
@@ -689,5 +1073,6 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
|
689
1073
|
}
|
|
690
1074
|
|
|
691
1075
|
export {
|
|
1076
|
+
AuthModule,
|
|
692
1077
|
startProxy
|
|
693
1078
|
};
|