kojee-mcp 0.4.0 → 0.5.2
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 +98 -10
- package/dist/chunk-2TUAFAIW.js +244 -0
- package/dist/{chunk-36DMIXH7.js → chunk-BJMASMKX.js} +13 -23
- package/dist/chunk-BLEGIR35.js +43 -0
- package/dist/chunk-C6GZ2L2W.js +38 -0
- package/dist/{chunk-VZVGTHGF.js → chunk-DO42NPNR.js} +11 -17
- package/dist/chunk-EW72ZNQL.js +39 -0
- package/dist/chunk-F7L25L2J.js +60 -0
- package/dist/{chunk-WHTH6WBP.js → chunk-LSUB6QMP.js} +3 -0
- package/dist/chunk-LVL25VLO.js +22 -0
- package/dist/chunk-SQL56SEB.js +14 -0
- package/dist/chunk-WBMX4CHB.js +378 -0
- package/dist/{chunk-ZGVUM4AG.js → chunk-YEC7IHIG.js} +276 -318
- package/dist/{chunk-E7TE4QZD.js → chunk-YH27B6SW.js} +9 -9
- package/dist/chunk-ZW4SW7LJ.js +225 -0
- package/dist/cli.js +70 -78
- package/dist/codex-stop-hook-JOTBCS5K.js +72 -0
- package/dist/doctor-TSHOMT5X.js +237 -0
- package/dist/doctor-codex-BMI5JOO6.js +130 -0
- package/dist/event-log-RSTM4PLL.js +18 -0
- package/dist/{hook-server-43QS7L7P.js → hook-server-QF5JVUHV.js} +28 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -2
- package/dist/{install-WV25CRU2.js → install-WBIUVBZW.js} +9 -7
- package/dist/{paired-config-OAR3O3XY.js → paired-config-JTFLHMZ2.js} +2 -1
- package/dist/resubscribe-SLZNA76S.js +59 -0
- package/dist/runtime-record-WO4IECM6.js +14 -0
- package/dist/runtimes-CO43XUUK.js +12 -0
- package/dist/{session-discovery-WSHLR4OV.js → session-discovery-FNMJGFPM.js} +2 -1
- package/dist/stop-hook-SEPWWETV.js +119 -0
- package/dist/tail-stream-BYKO4DW6.js +162 -0
- package/dist/{user-prompt-submit-hook-WSRIJVF4.js → user-prompt-submit-hook-ARPEO6FF.js} +5 -4
- package/dist/webhook-config-5TLLX7RA.js +10 -0
- package/dist/webhook-sink-7OYZBWXA.js +163 -0
- package/dist/wizard-7KHD5JT4.js +265 -0
- package/package.json +9 -7
- package/dist/event-log-ETWR6PPY.js +0 -112
- package/dist/stop-hook-5XU3EQAE.js +0 -76
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deriveDiscoveryKey,
|
|
3
3
|
findClaudeAncestorPid
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BJMASMKX.js";
|
|
5
|
+
import {
|
|
6
|
+
buildCatchUpNote,
|
|
7
|
+
buildMonitorSpawn,
|
|
8
|
+
buildReplyRecipe
|
|
9
|
+
} from "./chunk-C6GZ2L2W.js";
|
|
10
|
+
import {
|
|
11
|
+
MCP_SESSION_ID,
|
|
12
|
+
createDPoPProof,
|
|
13
|
+
startEventStream
|
|
14
|
+
} from "./chunk-WBMX4CHB.js";
|
|
15
|
+
import {
|
|
16
|
+
secureDir,
|
|
17
|
+
secureFile
|
|
18
|
+
} from "./chunk-BLEGIR35.js";
|
|
5
19
|
|
|
6
20
|
// src/index.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
21
|
+
import fs3 from "fs";
|
|
22
|
+
import os2 from "os";
|
|
23
|
+
import path3 from "path";
|
|
9
24
|
|
|
10
25
|
// src/auth/auth-module.ts
|
|
11
26
|
import { calculateJwkThumbprint } from "jose";
|
|
@@ -14,12 +29,9 @@ import crypto from "crypto";
|
|
|
14
29
|
// src/auth/keystore.ts
|
|
15
30
|
import { importJWK, exportJWK, generateKeyPair } from "jose";
|
|
16
31
|
import fs from "fs";
|
|
32
|
+
import os from "os";
|
|
17
33
|
import path from "path";
|
|
18
|
-
var DEFAULT_PATH = path.join(
|
|
19
|
-
process.env["HOME"] ?? "~",
|
|
20
|
-
".kojee",
|
|
21
|
-
"keypair.json"
|
|
22
|
-
);
|
|
34
|
+
var DEFAULT_PATH = path.join(os.homedir(), ".kojee", "keypair.json");
|
|
23
35
|
async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
|
|
24
36
|
if (!fs.existsSync(keystorePath)) {
|
|
25
37
|
return null;
|
|
@@ -39,9 +51,8 @@ async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
|
|
|
39
51
|
}
|
|
40
52
|
async function saveKeystore(privateKey, publicJwk, kid, brokerUrl, keystorePath = DEFAULT_PATH) {
|
|
41
53
|
const dir = path.dirname(keystorePath);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
55
|
+
secureDir(dir);
|
|
45
56
|
const privateJwk = await exportJWK(privateKey);
|
|
46
57
|
const data = {
|
|
47
58
|
private_key_jwk: privateJwk,
|
|
@@ -53,6 +64,7 @@ async function saveKeystore(privateKey, publicJwk, kid, brokerUrl, keystorePath
|
|
|
53
64
|
fs.writeFileSync(keystorePath, JSON.stringify(data, null, 2), {
|
|
54
65
|
mode: 384
|
|
55
66
|
});
|
|
67
|
+
secureFile(keystorePath);
|
|
56
68
|
}
|
|
57
69
|
async function generateES256KeyPair() {
|
|
58
70
|
const { privateKey, publicKey } = await generateKeyPair("ES256");
|
|
@@ -192,34 +204,7 @@ var AuthModule = class {
|
|
|
192
204
|
};
|
|
193
205
|
|
|
194
206
|
// src/gateway-client.ts
|
|
195
|
-
import crypto3 from "crypto";
|
|
196
|
-
|
|
197
|
-
// src/auth/dpop.ts
|
|
198
|
-
import { SignJWT, base64url } from "jose";
|
|
199
207
|
import crypto2 from "crypto";
|
|
200
|
-
async function createDPoPProof(privateKey, kid, method, url, nonce, accessToken) {
|
|
201
|
-
const payload = {
|
|
202
|
-
htm: method,
|
|
203
|
-
htu: url,
|
|
204
|
-
jti: crypto2.randomUUID()
|
|
205
|
-
};
|
|
206
|
-
if (nonce) {
|
|
207
|
-
payload.nonce = nonce;
|
|
208
|
-
}
|
|
209
|
-
if (accessToken) {
|
|
210
|
-
payload.ath = computeAth(accessToken);
|
|
211
|
-
}
|
|
212
|
-
const header = {
|
|
213
|
-
typ: "dpop+jwt",
|
|
214
|
-
alg: "ES256",
|
|
215
|
-
jwk: { kid }
|
|
216
|
-
};
|
|
217
|
-
return new SignJWT(payload).setProtectedHeader(header).setIssuedAt().sign(privateKey);
|
|
218
|
-
}
|
|
219
|
-
function computeAth(accessToken) {
|
|
220
|
-
const hash = crypto2.createHash("sha256").update(accessToken).digest();
|
|
221
|
-
return base64url.encode(hash);
|
|
222
|
-
}
|
|
223
208
|
|
|
224
209
|
// src/error-translator.ts
|
|
225
210
|
function translateGovernanceResult(result) {
|
|
@@ -249,7 +234,7 @@ function formatDenied(governance) {
|
|
|
249
234
|
isError: true
|
|
250
235
|
};
|
|
251
236
|
}
|
|
252
|
-
function translateHttpError(status, errorCode,
|
|
237
|
+
function translateHttpError(status, errorCode, trigger) {
|
|
253
238
|
if (status === 401) {
|
|
254
239
|
if (errorCode === "use_dpop_nonce") {
|
|
255
240
|
return null;
|
|
@@ -267,8 +252,9 @@ function translateHttpError(status, errorCode, _trigger) {
|
|
|
267
252
|
);
|
|
268
253
|
}
|
|
269
254
|
if (status === 403 && errorCode === "step_up_required") {
|
|
255
|
+
const reason = trigger ? ` (reason: ${trigger})` : "";
|
|
270
256
|
return makeError(
|
|
271
|
-
|
|
257
|
+
`Device re-authorization required${reason}. This action can't proceed until the user re-authorizes this device in the Kojee dashboard.`
|
|
272
258
|
);
|
|
273
259
|
}
|
|
274
260
|
if (status === 429) {
|
|
@@ -384,13 +370,7 @@ function makeError(text) {
|
|
|
384
370
|
};
|
|
385
371
|
}
|
|
386
372
|
|
|
387
|
-
// src/tandem/session-id.ts
|
|
388
|
-
import { ulid } from "ulidx";
|
|
389
|
-
var MCP_SESSION_ID = ulid();
|
|
390
|
-
|
|
391
373
|
// src/gateway-client.ts
|
|
392
|
-
var STEP_UP_POLL_INTERVAL_MS = 5e3;
|
|
393
|
-
var STEP_UP_MAX_TIMEOUT_MS = 3e5;
|
|
394
374
|
var GatewayClient = class {
|
|
395
375
|
constructor(brokerUrl, token, privateKey, kid, sessionId) {
|
|
396
376
|
this.brokerUrl = brokerUrl;
|
|
@@ -426,26 +406,35 @@ var GatewayClient = class {
|
|
|
426
406
|
* session_id = sha256(token + "proxy").slice(0, 16)
|
|
427
407
|
*/
|
|
428
408
|
static deriveSessionId(token) {
|
|
429
|
-
const hash =
|
|
409
|
+
const hash = crypto2.createHash("sha256").update(token + "proxy").digest("hex");
|
|
430
410
|
return hash.slice(0, 16);
|
|
431
411
|
}
|
|
432
412
|
/**
|
|
433
|
-
* Send a JSON-RPC 2.0 request to the gateway, handling DPoP auth
|
|
434
|
-
* nonce retry
|
|
413
|
+
* Send a JSON-RPC 2.0 request to the gateway, handling DPoP auth and
|
|
414
|
+
* nonce retry transparently. A 403 `step_up_required` (deprecated feature,
|
|
415
|
+
* owner ruling 2026-06-10) is no longer polled — it surfaces immediately as
|
|
416
|
+
* a structured tool error via translateHttpError.
|
|
417
|
+
*
|
|
418
|
+
* `signal` (ROUND-3 MAJOR A) is a REAL AbortSignal threaded into the
|
|
419
|
+
* underlying `fetch` option — NOT placed inside `params`/`arguments`. A
|
|
420
|
+
* caller with a per-call timeout budget (e.g. resubscribeMemberships) passes
|
|
421
|
+
* its controller's signal here so a hung backend aborts at the budget instead
|
|
422
|
+
* of hanging forever. Putting the signal in `arguments` (the round-2 bug) both
|
|
423
|
+
* left fetch un-aborted AND serialized a junk `{}` onto the wire body.
|
|
435
424
|
*/
|
|
436
|
-
async sendRpc(method, params = {}) {
|
|
425
|
+
async sendRpc(method, params = {}, signal) {
|
|
437
426
|
const rpcRequest = {
|
|
438
427
|
jsonrpc: "2.0",
|
|
439
428
|
id: ++this.requestCounter,
|
|
440
429
|
method,
|
|
441
430
|
params
|
|
442
431
|
};
|
|
443
|
-
return this.executeWithRetries(rpcRequest);
|
|
432
|
+
return this.executeWithRetries(rpcRequest, signal);
|
|
444
433
|
}
|
|
445
|
-
async executeWithRetries(rpcRequest) {
|
|
434
|
+
async executeWithRetries(rpcRequest, signal) {
|
|
446
435
|
let response;
|
|
447
436
|
try {
|
|
448
|
-
response = await this.sendHttpRequest(rpcRequest);
|
|
437
|
+
response = await this.sendHttpRequest(rpcRequest, signal);
|
|
449
438
|
} catch (err) {
|
|
450
439
|
return translateNetworkError(err);
|
|
451
440
|
}
|
|
@@ -455,7 +444,7 @@ var GatewayClient = class {
|
|
|
455
444
|
if (body?.error === "use_dpop_nonce") {
|
|
456
445
|
console.error("[gateway] Nonce expired, retrying with fresh nonce...");
|
|
457
446
|
try {
|
|
458
|
-
response = await this.sendHttpRequest(rpcRequest);
|
|
447
|
+
response = await this.sendHttpRequest(rpcRequest, signal);
|
|
459
448
|
} catch (err) {
|
|
460
449
|
return translateNetworkError(err);
|
|
461
450
|
}
|
|
@@ -467,9 +456,6 @@ var GatewayClient = class {
|
|
|
467
456
|
}
|
|
468
457
|
if (response.status === 403) {
|
|
469
458
|
const body = await this.tryParseErrorBody(response);
|
|
470
|
-
if (body?.error === "step_up_required") {
|
|
471
|
-
return this.handleStepUp(rpcRequest, body.trigger);
|
|
472
|
-
}
|
|
473
459
|
const translated = translateHttpError(403, body?.error, body?.trigger);
|
|
474
460
|
if (translated) return translated;
|
|
475
461
|
}
|
|
@@ -489,7 +475,7 @@ var GatewayClient = class {
|
|
|
489
475
|
const result = rpcResponse.result;
|
|
490
476
|
return result ?? { content: [{ type: "text", text: "No result" }] };
|
|
491
477
|
}
|
|
492
|
-
async sendHttpRequest(rpcRequest) {
|
|
478
|
+
async sendHttpRequest(rpcRequest, signal) {
|
|
493
479
|
const proof = await createDPoPProof(
|
|
494
480
|
this.privateKey,
|
|
495
481
|
this.kid,
|
|
@@ -506,56 +492,13 @@ var GatewayClient = class {
|
|
|
506
492
|
DPoP: proof,
|
|
507
493
|
"Mcp-Session-Id": MCP_SESSION_ID
|
|
508
494
|
},
|
|
509
|
-
body: JSON.stringify(rpcRequest)
|
|
495
|
+
body: JSON.stringify(rpcRequest),
|
|
496
|
+
// ROUND-3 MAJOR A: the caller's AbortSignal rides HERE (a real fetch
|
|
497
|
+
// option), never inside the JSON-RPC body. `undefined` is a valid value
|
|
498
|
+
// for the fetch `signal` option (no abort wired).
|
|
499
|
+
...signal ? { signal } : {}
|
|
510
500
|
});
|
|
511
501
|
}
|
|
512
|
-
/**
|
|
513
|
-
* Handle step-up retry: poll with backoff until user approves
|
|
514
|
-
* or timeout is reached.
|
|
515
|
-
*/
|
|
516
|
-
async handleStepUp(rpcRequest, trigger) {
|
|
517
|
-
console.error(
|
|
518
|
-
`[gateway] Device re-authorization required (${trigger ?? "unknown"}). Waiting for user approval...`
|
|
519
|
-
);
|
|
520
|
-
const deadline = Date.now() + STEP_UP_MAX_TIMEOUT_MS;
|
|
521
|
-
while (Date.now() < deadline) {
|
|
522
|
-
await sleep(STEP_UP_POLL_INTERVAL_MS);
|
|
523
|
-
let response;
|
|
524
|
-
try {
|
|
525
|
-
response = await this.sendHttpRequest(rpcRequest);
|
|
526
|
-
} catch {
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
this.trackNonce(response);
|
|
530
|
-
if (response.status === 403) {
|
|
531
|
-
const body2 = await this.tryParseErrorBody(response);
|
|
532
|
-
if (body2?.error === "step_up_required") {
|
|
533
|
-
console.error("[gateway] Still waiting for step-up approval...");
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (response.ok) {
|
|
538
|
-
const rpcResponse = await response.json();
|
|
539
|
-
if (rpcResponse.error) {
|
|
540
|
-
return translateJsonRpcError(rpcResponse.error);
|
|
541
|
-
}
|
|
542
|
-
const result = rpcResponse.result;
|
|
543
|
-
return result ?? { content: [{ type: "text", text: "No result" }] };
|
|
544
|
-
}
|
|
545
|
-
const body = await this.tryParseErrorBody(response);
|
|
546
|
-
const translated = translateHttpError(response.status, body?.error);
|
|
547
|
-
if (translated) return translated;
|
|
548
|
-
}
|
|
549
|
-
return {
|
|
550
|
-
content: [
|
|
551
|
-
{
|
|
552
|
-
type: "text",
|
|
553
|
-
text: "Device re-authorization was not approved within 5 minutes. Try again later."
|
|
554
|
-
}
|
|
555
|
-
],
|
|
556
|
-
isError: true
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
502
|
trackNonce(response) {
|
|
560
503
|
const nonce = response.headers.get("DPoP-Nonce");
|
|
561
504
|
if (nonce) {
|
|
@@ -570,9 +513,6 @@ var GatewayClient = class {
|
|
|
570
513
|
}
|
|
571
514
|
}
|
|
572
515
|
};
|
|
573
|
-
function sleep(ms) {
|
|
574
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
575
|
-
}
|
|
576
516
|
|
|
577
517
|
// src/tool-registry.ts
|
|
578
518
|
var ToolRegistry = class {
|
|
@@ -648,16 +588,41 @@ import {
|
|
|
648
588
|
ListToolsRequestSchema,
|
|
649
589
|
CallToolRequestSchema
|
|
650
590
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
591
|
+
|
|
592
|
+
// src/version.ts
|
|
593
|
+
import fs2 from "fs";
|
|
594
|
+
import path2 from "path";
|
|
595
|
+
import { fileURLToPath } from "url";
|
|
596
|
+
var FALLBACK_VERSION = "0.0.0-unknown";
|
|
597
|
+
function resolveVersion() {
|
|
598
|
+
try {
|
|
599
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
600
|
+
const parsed = JSON.parse(
|
|
601
|
+
fs2.readFileSync(path2.join(here, "..", "package.json"), "utf8")
|
|
602
|
+
);
|
|
603
|
+
return typeof parsed?.version === "string" && parsed.version ? parsed.version : FALLBACK_VERSION;
|
|
604
|
+
} catch (err) {
|
|
605
|
+
process.stderr.write(
|
|
606
|
+
`kojee-mcp: could not resolve version from package.json, falling back to ${FALLBACK_VERSION}: ${String(err)}
|
|
607
|
+
`
|
|
608
|
+
);
|
|
609
|
+
return FALLBACK_VERSION;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
var VERSION = resolveVersion();
|
|
613
|
+
|
|
614
|
+
// src/server.ts
|
|
615
|
+
function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
|
|
616
|
+
const intro = `Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:
|
|
617
|
+
|
|
618
|
+
(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. To respond: ${buildReplyRecipe()}.
|
|
619
|
+
|
|
620
|
+
`;
|
|
621
|
+
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: ${buildMonitorSpawn(eventLogPath)}. Each appended line will arrive as a separate wake notification, and carries msg=<id> and cursor=<n>. To respond: ${buildReplyRecipe()}. ${buildCatchUpNote()} (\`kojee-mcp tail\` is a portable line-streamer shipped with this proxy \u2014 works on macOS, Linux, and Windows. It follows BOTH the messages log above and a status sibling; status/heartbeat telemetry never wakes you \u2014 only real messages do.)
|
|
654
622
|
|
|
655
623
|
`;
|
|
656
624
|
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
625
|
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
626
|
return intro + monitorSection + listenSection + advice;
|
|
662
627
|
}
|
|
663
628
|
function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath) {
|
|
@@ -666,7 +631,7 @@ function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLog
|
|
|
666
631
|
capabilities.experimental = { "claude/channel": {} };
|
|
667
632
|
}
|
|
668
633
|
const server = new Server(
|
|
669
|
-
{ name: "kojee-mcp", version:
|
|
634
|
+
{ name: "kojee-mcp", version: VERSION },
|
|
670
635
|
{
|
|
671
636
|
capabilities,
|
|
672
637
|
...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
|
|
@@ -695,35 +660,35 @@ async function startMcpServer(server) {
|
|
|
695
660
|
}
|
|
696
661
|
|
|
697
662
|
// src/runtime/detect.ts
|
|
698
|
-
import
|
|
699
|
-
function detectRuntime(env = process.env) {
|
|
663
|
+
import psList from "ps-list";
|
|
664
|
+
async function detectRuntime(env = process.env) {
|
|
700
665
|
if (env["KOJEE_RUNTIME"] === "claude-code") return "claude-code";
|
|
666
|
+
if (env["KOJEE_RUNTIME"] === "codex") return "codex";
|
|
701
667
|
if (env["CLAUDE_CODE_SESSION_ID"]) return "claude-code";
|
|
702
|
-
|
|
668
|
+
const ancestor = await detectRuntimeFromAncestry();
|
|
669
|
+
if (ancestor) return ancestor;
|
|
703
670
|
return "unknown";
|
|
704
671
|
}
|
|
705
|
-
function
|
|
706
|
-
if (process.platform === "win32") return false;
|
|
672
|
+
async function detectRuntimeFromAncestry() {
|
|
707
673
|
try {
|
|
674
|
+
const processes = await psList();
|
|
675
|
+
const byPid = new Map(processes.map((p) => [p.pid, p]));
|
|
708
676
|
let pid = process.ppid;
|
|
709
677
|
for (let depth = 0; depth < 5 && pid && pid > 1; depth++) {
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
678
|
+
const row = byPid.get(pid);
|
|
679
|
+
if (!row) return null;
|
|
680
|
+
const haystack = `${row.name} ${row.cmd ?? ""}`;
|
|
681
|
+
if (/(^|\/|\\)claude(\.app|\.exe)?(\/|$|\s|\\)/i.test(haystack) || /Claude Helper/i.test(haystack)) {
|
|
682
|
+
return "claude-code";
|
|
683
|
+
}
|
|
684
|
+
if (/(^|\/|\\)codex(\.exe)?(\/|$|\s|\\)/i.test(haystack)) {
|
|
685
|
+
return "codex";
|
|
716
686
|
}
|
|
717
|
-
|
|
718
|
-
encoding: "utf8",
|
|
719
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
720
|
-
}).trim();
|
|
721
|
-
pid = Number.parseInt(ppidOut, 10);
|
|
722
|
-
if (!Number.isFinite(pid)) return false;
|
|
687
|
+
pid = row.ppid;
|
|
723
688
|
}
|
|
724
689
|
} catch {
|
|
725
690
|
}
|
|
726
|
-
return
|
|
691
|
+
return null;
|
|
727
692
|
}
|
|
728
693
|
|
|
729
694
|
// src/adapters/claude-code.ts
|
|
@@ -742,7 +707,7 @@ function formatBody(event) {
|
|
|
742
707
|
`[Tandem: ${event.tandem_id}] ${event.from.displayname} (${event.from.principal}) \u2014 ${event.kind}:`,
|
|
743
708
|
event.content.body,
|
|
744
709
|
"",
|
|
745
|
-
`>
|
|
710
|
+
`> ${buildReplyRecipe({ tandem_id: event.tandem_id, message_id: event.id })}`
|
|
746
711
|
].join("\n");
|
|
747
712
|
}
|
|
748
713
|
var claudeCodeAdapter = {
|
|
@@ -762,6 +727,18 @@ var claudeCodeAdapter = {
|
|
|
762
727
|
}
|
|
763
728
|
};
|
|
764
729
|
|
|
730
|
+
// src/adapters/codex.ts
|
|
731
|
+
var codexAdapter = {
|
|
732
|
+
runtime: "codex",
|
|
733
|
+
supportsChannels: false,
|
|
734
|
+
// Codex has NO Claude-style channel injection
|
|
735
|
+
formatTandemEvent() {
|
|
736
|
+
throw new Error(
|
|
737
|
+
"codexAdapter.formatTandemEvent() is unreachable \u2014 Codex has no channel injection; server.ts gates this on supportsChannels. Codex receives events via the webhook sink + a model-chosen bounded tandem_listen."
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
765
742
|
// src/adapters/unknown.ts
|
|
766
743
|
var unknownAdapter = {
|
|
767
744
|
runtime: "unknown",
|
|
@@ -773,176 +750,44 @@ var unknownAdapter = {
|
|
|
773
750
|
}
|
|
774
751
|
};
|
|
775
752
|
|
|
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
|
-
|
|
926
753
|
// src/index.ts
|
|
927
|
-
var DEFAULT_KEYSTORE_PATH =
|
|
928
|
-
process.env["HOME"] ?? "~",
|
|
929
|
-
".kojee",
|
|
930
|
-
"keypair.json"
|
|
931
|
-
);
|
|
754
|
+
var DEFAULT_KEYSTORE_PATH = path3.join(os2.homedir(), ".kojee", "keypair.json");
|
|
932
755
|
function isDPoPEnrollmentError(err) {
|
|
933
756
|
const msg = String(err?.message ?? err ?? "").toLowerCase();
|
|
934
757
|
if (msg.includes("invalid or expired") && msg.includes("token")) return false;
|
|
935
758
|
if (msg.includes("generate a new")) return false;
|
|
936
759
|
return msg.includes("401") || msg.includes("authentication failed") || msg.includes("dpop") || msg.includes("key enrollment") || msg.includes("kid");
|
|
937
760
|
}
|
|
938
|
-
function selectAdapter() {
|
|
939
|
-
const runtime = detectRuntime();
|
|
761
|
+
async function selectAdapter() {
|
|
762
|
+
const runtime = await detectRuntime();
|
|
940
763
|
if (runtime === "claude-code") return claudeCodeAdapter;
|
|
764
|
+
if (runtime === "codex") return codexAdapter;
|
|
941
765
|
return unknownAdapter;
|
|
942
766
|
}
|
|
767
|
+
async function listTandemIds(gateway) {
|
|
768
|
+
const result = await gateway.sendRpc("tools/call", { name: "tandem_list", arguments: {} });
|
|
769
|
+
const maybeErr = result;
|
|
770
|
+
if (maybeErr.isError) return null;
|
|
771
|
+
const text = maybeErr.content?.[0]?.text;
|
|
772
|
+
try {
|
|
773
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
774
|
+
const list = Array.isArray(parsed.tandems) ? parsed.tandems : Array.isArray(parsed) ? parsed : null;
|
|
775
|
+
if (!Array.isArray(list)) return [];
|
|
776
|
+
return list.map((t) => {
|
|
777
|
+
if (typeof t === "string") return t;
|
|
778
|
+
const obj = t;
|
|
779
|
+
return obj?.tandem_id ?? obj?.id;
|
|
780
|
+
}).filter((id) => typeof id === "string" && id.length > 0);
|
|
781
|
+
} catch {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function needsWebhookEventStream() {
|
|
786
|
+
return (process.env["KOJEE_WEBHOOK_URL"] ?? "").trim().length > 0;
|
|
787
|
+
}
|
|
943
788
|
async function startProxy(config) {
|
|
944
789
|
const keystorePath = config.keystorePath || DEFAULT_KEYSTORE_PATH;
|
|
945
|
-
const adapter = selectAdapter();
|
|
790
|
+
const adapter = await selectAdapter();
|
|
946
791
|
console.error(`[kojee-mcp] Starting proxy for ${config.url} (runtime=${adapter.runtime})`);
|
|
947
792
|
const { registry, gateway } = await enrollAndDiscover(config, keystorePath);
|
|
948
793
|
console.error(
|
|
@@ -950,22 +795,8 @@ async function startProxy(config) {
|
|
|
950
795
|
);
|
|
951
796
|
let tandemMembershipCount = -1;
|
|
952
797
|
try {
|
|
953
|
-
const
|
|
954
|
-
|
|
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
|
-
}
|
|
798
|
+
const bootIds = await listTandemIds(gateway);
|
|
799
|
+
tandemMembershipCount = bootIds === null ? -1 : bootIds.length;
|
|
969
800
|
} catch (err) {
|
|
970
801
|
console.error("[kojee-mcp] tandem_list probe failed:", err.message);
|
|
971
802
|
}
|
|
@@ -973,22 +804,58 @@ async function startProxy(config) {
|
|
|
973
804
|
let server;
|
|
974
805
|
if (adapter.supportsChannels) {
|
|
975
806
|
const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
|
|
976
|
-
const { startHookServer } = await import("./hook-server-
|
|
807
|
+
const { startHookServer } = await import("./hook-server-QF5JVUHV.js");
|
|
977
808
|
const {
|
|
978
809
|
writeDiscoveryByKey,
|
|
979
810
|
cleanupDiscoveryByKey,
|
|
980
811
|
sweepStaleDiscovery
|
|
981
|
-
} = await import("./session-discovery-
|
|
982
|
-
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-
|
|
812
|
+
} = await import("./session-discovery-FNMJGFPM.js");
|
|
813
|
+
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
|
|
814
|
+
const { resubscribeMemberships } = await import("./resubscribe-SLZNA76S.js");
|
|
815
|
+
const { resolveWebhookConfig } = await import("./webhook-config-5TLLX7RA.js");
|
|
816
|
+
const { createWebhookSink } = await import("./webhook-sink-7OYZBWXA.js");
|
|
983
817
|
sweepStaleDiscovery();
|
|
984
818
|
sweepStaleEventLogs();
|
|
985
|
-
const ccPid = findClaudeAncestorPid();
|
|
819
|
+
const ccPid = await findClaudeAncestorPid();
|
|
986
820
|
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
987
821
|
const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
|
|
988
822
|
const eventLog = startEventLog({ key: discoveryKey });
|
|
823
|
+
const webhookResolution = resolveWebhookConfig();
|
|
824
|
+
if (webhookResolution.error) {
|
|
825
|
+
console.error(`[kojee-mcp] webhook sink ERROR: ${webhookResolution.error}`);
|
|
826
|
+
void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
|
|
830
|
+
// Route delivery/failure observability to the STATUS sink.
|
|
831
|
+
log: (line) => {
|
|
832
|
+
void eventLog.appendStatus(`status=webhook ${line}`).catch(() => {
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}) : null;
|
|
836
|
+
if (webhookSink) {
|
|
837
|
+
console.error(`[kojee-mcp] webhook sink ENABLED (${webhookSink.configSummary()})`);
|
|
838
|
+
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
839
|
+
});
|
|
840
|
+
}
|
|
989
841
|
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path);
|
|
990
842
|
const queue = new EventQueue();
|
|
991
|
-
|
|
843
|
+
let streamHandle = null;
|
|
844
|
+
const hookServer = await startHookServer({
|
|
845
|
+
port: 0,
|
|
846
|
+
queue,
|
|
847
|
+
adapter,
|
|
848
|
+
getStreamState: () => streamHandle ? streamHandle.getState() : {
|
|
849
|
+
connected: false,
|
|
850
|
+
connectedSince: null,
|
|
851
|
+
lastEventAt: null,
|
|
852
|
+
lastHeartbeatAt: null,
|
|
853
|
+
cursors: {},
|
|
854
|
+
reconnectCount: 0,
|
|
855
|
+
// Adaptive: unknown until the watchdog observes ≥2 heartbeats.
|
|
856
|
+
staleAfterMs: null
|
|
857
|
+
}
|
|
858
|
+
});
|
|
992
859
|
writeDiscoveryByKey(discoveryKey, {
|
|
993
860
|
schema: 2,
|
|
994
861
|
discoveryKey,
|
|
@@ -1000,7 +867,13 @@ async function startProxy(config) {
|
|
|
1000
867
|
pid: process.pid,
|
|
1001
868
|
port: hookServer.port,
|
|
1002
869
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1003
|
-
brokerUrl: config.url
|
|
870
|
+
brokerUrl: config.url,
|
|
871
|
+
eventLogPath: eventLog.path,
|
|
872
|
+
// Stamp the auth mode so `kojee-mcp doctor` renders the pairing check
|
|
873
|
+
// honestly: a token-mode box has no ~/.kojee/config.json by design and
|
|
874
|
+
// must not hard-fail on "paired config: MISSING". Defaults to "paired"
|
|
875
|
+
// for back-compat with callers that don't set it.
|
|
876
|
+
authMode: config.authMode ?? "paired"
|
|
1004
877
|
});
|
|
1005
878
|
const cleanupDiscoveryFile = () => cleanupDiscoveryByKey(discoveryKey);
|
|
1006
879
|
process.on("exit", () => cleanupDiscoveryFile());
|
|
@@ -1013,17 +886,45 @@ async function startProxy(config) {
|
|
|
1013
886
|
process.exit(0);
|
|
1014
887
|
});
|
|
1015
888
|
process.on("exit", () => eventLog.cleanup());
|
|
1016
|
-
|
|
889
|
+
streamHandle = await startEventStream({
|
|
1017
890
|
brokerUrl: config.url,
|
|
1018
891
|
token: config.token,
|
|
1019
892
|
gateway,
|
|
1020
893
|
adapter,
|
|
1021
894
|
server,
|
|
1022
895
|
queue,
|
|
1023
|
-
eventLog
|
|
896
|
+
eventLog,
|
|
897
|
+
// Generic webhook sink (null unless KOJEE_WEBHOOK_URL + _SECRET are set).
|
|
898
|
+
// Wired LAST in the fan-out, fire-and-forget — can't delay a wake.
|
|
899
|
+
...webhookSink ? { webhookSink } : {},
|
|
900
|
+
// Resubscribe-on-start (P0 #2): touch all memberships + write a
|
|
901
|
+
// `status=subscribed n=<count>` line on every (re)connect, so a backend
|
|
902
|
+
// restart / scope reset self-heals and the log is never ambiguously
|
|
903
|
+
// empty. See resubscribe.ts for the unverified-touch caveat.
|
|
904
|
+
//
|
|
905
|
+
// MINOR 6: `listTandems` re-fetches the membership list per reconnect (a
|
|
906
|
+
// mid-session join is touched next reconnect, not boot-frozen), each
|
|
907
|
+
// touch is timeout-bounded + run with bounded concurrency, and the whole
|
|
908
|
+
// routine runs concurrently with consumeSse (never blocks first-event
|
|
909
|
+
// delivery). `listTandemIds` may return null (unknown) → treat as empty.
|
|
910
|
+
// MINOR E: a shared debounce cursor damps a connect/drop flap storm — a
|
|
911
|
+
// resubscribe within 30s of the last successful one is skipped.
|
|
912
|
+
onConnected: /* @__PURE__ */ (() => {
|
|
913
|
+
const debounceState = { lastRunAt: 0 };
|
|
914
|
+
return async () => {
|
|
915
|
+
await resubscribeMemberships({
|
|
916
|
+
gateway,
|
|
917
|
+
eventLog,
|
|
918
|
+
listTandems: async () => await listTandemIds(gateway) ?? [],
|
|
919
|
+
debounceState
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
})()
|
|
1024
923
|
});
|
|
924
|
+
const cancelStream = streamHandle;
|
|
1025
925
|
process.stdin.on("end", () => {
|
|
1026
926
|
cancelStream();
|
|
927
|
+
void webhookSink?.stop();
|
|
1027
928
|
cleanupDiscoveryFile();
|
|
1028
929
|
eventLog.cleanup();
|
|
1029
930
|
hookServer.stop().finally(() => {
|
|
@@ -1031,6 +932,62 @@ async function startProxy(config) {
|
|
|
1031
932
|
process.exit(0);
|
|
1032
933
|
});
|
|
1033
934
|
});
|
|
935
|
+
} else if (needsWebhookEventStream()) {
|
|
936
|
+
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
|
|
937
|
+
const { resolveWebhookConfig } = await import("./webhook-config-5TLLX7RA.js");
|
|
938
|
+
const { createWebhookSink } = await import("./webhook-sink-7OYZBWXA.js");
|
|
939
|
+
const { resubscribeMemberships } = await import("./resubscribe-SLZNA76S.js");
|
|
940
|
+
sweepStaleEventLogs();
|
|
941
|
+
const ccPid = await findClaudeAncestorPid();
|
|
942
|
+
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
943
|
+
const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
|
|
944
|
+
const eventLog = startEventLog({ key: discoveryKey });
|
|
945
|
+
const webhookResolution = resolveWebhookConfig();
|
|
946
|
+
if (webhookResolution.error) {
|
|
947
|
+
console.error(`[kojee-mcp] webhook sink ERROR: ${webhookResolution.error}`);
|
|
948
|
+
void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
|
|
952
|
+
log: (line) => {
|
|
953
|
+
void eventLog.appendStatus(`status=webhook ${line}`).catch(() => {
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}) : null;
|
|
957
|
+
if (webhookSink) {
|
|
958
|
+
console.error(`[kojee-mcp] webhook sink ENABLED (${webhookSink.configSummary()})`);
|
|
959
|
+
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
server = createMcpServer(registry, adapter, tandemMembershipCount);
|
|
963
|
+
process.on("exit", () => eventLog.cleanup());
|
|
964
|
+
const streamHandle = await startEventStream({
|
|
965
|
+
brokerUrl: config.url,
|
|
966
|
+
token: config.token,
|
|
967
|
+
gateway,
|
|
968
|
+
adapter,
|
|
969
|
+
server,
|
|
970
|
+
eventLog,
|
|
971
|
+
...webhookSink ? { webhookSink } : {},
|
|
972
|
+
onConnected: /* @__PURE__ */ (() => {
|
|
973
|
+
const debounceState = { lastRunAt: 0 };
|
|
974
|
+
return async () => {
|
|
975
|
+
await resubscribeMemberships({
|
|
976
|
+
gateway,
|
|
977
|
+
eventLog,
|
|
978
|
+
listTandems: async () => await listTandemIds(gateway) ?? [],
|
|
979
|
+
debounceState
|
|
980
|
+
});
|
|
981
|
+
};
|
|
982
|
+
})()
|
|
983
|
+
});
|
|
984
|
+
process.stdin.on("end", () => {
|
|
985
|
+
streamHandle();
|
|
986
|
+
void webhookSink?.stop();
|
|
987
|
+
eventLog.cleanup();
|
|
988
|
+
console.error("[kojee-mcp] stdin closed, exiting");
|
|
989
|
+
process.exit(0);
|
|
990
|
+
});
|
|
1034
991
|
} else {
|
|
1035
992
|
server = createMcpServer(registry, adapter, tandemMembershipCount);
|
|
1036
993
|
process.stdin.on("end", () => {
|
|
@@ -1064,7 +1021,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
|
1064
1021
|
"[kojee-mcp] Auth failed, attempting recovery with fresh enrollment..."
|
|
1065
1022
|
);
|
|
1066
1023
|
try {
|
|
1067
|
-
if (
|
|
1024
|
+
if (fs3.existsSync(keystorePath)) fs3.unlinkSync(keystorePath);
|
|
1068
1025
|
} catch (unlinkErr) {
|
|
1069
1026
|
console.error("[kojee-mcp] Could not remove stale keystore:", unlinkErr);
|
|
1070
1027
|
}
|
|
@@ -1074,5 +1031,6 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
|
1074
1031
|
|
|
1075
1032
|
export {
|
|
1076
1033
|
AuthModule,
|
|
1034
|
+
VERSION,
|
|
1077
1035
|
startProxy
|
|
1078
1036
|
};
|