kojee-mcp 0.5.0 → 0.5.3
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 +110 -4
- package/dist/{chunk-VLZADEFC.js → chunk-2TUAFAIW.js} +6 -9
- package/dist/chunk-BLEGIR35.js +43 -0
- package/dist/chunk-C6GZ2L2W.js +38 -0
- package/dist/{chunk-W6YRLSD4.js → chunk-DO42NPNR.js} +9 -16
- package/dist/chunk-EW72ZNQL.js +39 -0
- package/dist/chunk-F7L25L2J.js +60 -0
- package/dist/chunk-LVL25VLO.js +22 -0
- package/dist/chunk-SQL56SEB.js +14 -0
- package/dist/{chunk-QB22PD6T.js → chunk-WBMX4CHB.js} +29 -9
- package/dist/{chunk-LCFCCWMM.js → chunk-YEC7IHIG.js} +136 -78
- package/dist/{chunk-GBOTBYEP.js → chunk-YH27B6SW.js} +7 -8
- package/dist/chunk-ZW4SW7LJ.js +225 -0
- package/dist/cli.js +54 -80
- package/dist/codex-stop-hook-JOTBCS5K.js +72 -0
- package/dist/{doctor-GILTOH2R.js → doctor-TSHOMT5X.js} +29 -14
- package/dist/doctor-codex-BMI5JOO6.js +130 -0
- package/dist/{event-log-R6VW6GAF.js → event-log-RSTM4PLL.js} +3 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4 -3
- package/dist/{install-D2HIPOMT.js → install-WBIUVBZW.js} +5 -4
- package/dist/{paired-config-RB4SABOS.js → paired-config-JTFLHMZ2.js} +2 -1
- package/dist/runtime-record-WO4IECM6.js +14 -0
- package/dist/runtimes-CO43XUUK.js +12 -0
- package/dist/{session-discovery-QE5TTAPS.js → session-discovery-FNMJGFPM.js} +2 -1
- package/dist/{stop-hook-VLQS6QPR.js → stop-hook-SEPWWETV.js} +6 -5
- package/dist/{tail-stream-UZ42UIWO.js → tail-stream-BYKO4DW6.js} +4 -3
- package/dist/{user-prompt-submit-hook-C42DPDBO.js → user-prompt-submit-hook-ARPEO6FF.js} +2 -1
- 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 +1 -1
- package/dist/chunk-E26AHU6J.js +0 -27
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./chunk-QB22PD6T.js";
|
|
2
|
+
deriveDiscoveryKey,
|
|
3
|
+
findClaudeAncestorPid
|
|
4
|
+
} from "./chunk-BJMASMKX.js";
|
|
6
5
|
import {
|
|
7
6
|
buildCatchUpNote,
|
|
8
7
|
buildMonitorSpawn,
|
|
9
8
|
buildReplyRecipe
|
|
10
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-C6GZ2L2W.js";
|
|
11
10
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
MCP_SESSION_ID,
|
|
12
|
+
createDPoPProof,
|
|
13
|
+
startEventStream
|
|
14
|
+
} from "./chunk-WBMX4CHB.js";
|
|
15
|
+
import {
|
|
16
|
+
secureDir,
|
|
17
|
+
secureFile
|
|
18
|
+
} from "./chunk-BLEGIR35.js";
|
|
15
19
|
|
|
16
20
|
// src/index.ts
|
|
17
21
|
import fs3 from "fs";
|
|
@@ -47,9 +51,8 @@ async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
|
|
|
47
51
|
}
|
|
48
52
|
async function saveKeystore(privateKey, publicJwk, kid, brokerUrl, keystorePath = DEFAULT_PATH) {
|
|
49
53
|
const dir = path.dirname(keystorePath);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
55
|
+
secureDir(dir);
|
|
53
56
|
const privateJwk = await exportJWK(privateKey);
|
|
54
57
|
const data = {
|
|
55
58
|
private_key_jwk: privateJwk,
|
|
@@ -61,6 +64,7 @@ async function saveKeystore(privateKey, publicJwk, kid, brokerUrl, keystorePath
|
|
|
61
64
|
fs.writeFileSync(keystorePath, JSON.stringify(data, null, 2), {
|
|
62
65
|
mode: 384
|
|
63
66
|
});
|
|
67
|
+
secureFile(keystorePath);
|
|
64
68
|
}
|
|
65
69
|
async function generateES256KeyPair() {
|
|
66
70
|
const { privateKey, publicKey } = await generateKeyPair("ES256");
|
|
@@ -230,7 +234,7 @@ function formatDenied(governance) {
|
|
|
230
234
|
isError: true
|
|
231
235
|
};
|
|
232
236
|
}
|
|
233
|
-
function translateHttpError(status, errorCode,
|
|
237
|
+
function translateHttpError(status, errorCode, trigger) {
|
|
234
238
|
if (status === 401) {
|
|
235
239
|
if (errorCode === "use_dpop_nonce") {
|
|
236
240
|
return null;
|
|
@@ -248,8 +252,9 @@ function translateHttpError(status, errorCode, _trigger) {
|
|
|
248
252
|
);
|
|
249
253
|
}
|
|
250
254
|
if (status === 403 && errorCode === "step_up_required") {
|
|
255
|
+
const reason = trigger ? ` (reason: ${trigger})` : "";
|
|
251
256
|
return makeError(
|
|
252
|
-
|
|
257
|
+
`Device re-authorization required${reason}. This action can't proceed until the user re-authorizes this device in the Kojee dashboard.`
|
|
253
258
|
);
|
|
254
259
|
}
|
|
255
260
|
if (status === 429) {
|
|
@@ -366,8 +371,6 @@ function makeError(text) {
|
|
|
366
371
|
}
|
|
367
372
|
|
|
368
373
|
// src/gateway-client.ts
|
|
369
|
-
var STEP_UP_POLL_INTERVAL_MS = 5e3;
|
|
370
|
-
var STEP_UP_MAX_TIMEOUT_MS = 3e5;
|
|
371
374
|
var GatewayClient = class {
|
|
372
375
|
constructor(brokerUrl, token, privateKey, kid, sessionId) {
|
|
373
376
|
this.brokerUrl = brokerUrl;
|
|
@@ -407,8 +410,10 @@ var GatewayClient = class {
|
|
|
407
410
|
return hash.slice(0, 16);
|
|
408
411
|
}
|
|
409
412
|
/**
|
|
410
|
-
* Send a JSON-RPC 2.0 request to the gateway, handling DPoP auth
|
|
411
|
-
* 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.
|
|
412
417
|
*
|
|
413
418
|
* `signal` (ROUND-3 MAJOR A) is a REAL AbortSignal threaded into the
|
|
414
419
|
* underlying `fetch` option — NOT placed inside `params`/`arguments`. A
|
|
@@ -451,9 +456,6 @@ var GatewayClient = class {
|
|
|
451
456
|
}
|
|
452
457
|
if (response.status === 403) {
|
|
453
458
|
const body = await this.tryParseErrorBody(response);
|
|
454
|
-
if (body?.error === "step_up_required") {
|
|
455
|
-
return this.handleStepUp(rpcRequest, body.trigger, signal);
|
|
456
|
-
}
|
|
457
459
|
const translated = translateHttpError(403, body?.error, body?.trigger);
|
|
458
460
|
if (translated) return translated;
|
|
459
461
|
}
|
|
@@ -497,53 +499,6 @@ var GatewayClient = class {
|
|
|
497
499
|
...signal ? { signal } : {}
|
|
498
500
|
});
|
|
499
501
|
}
|
|
500
|
-
/**
|
|
501
|
-
* Handle step-up retry: poll with backoff until user approves
|
|
502
|
-
* or timeout is reached.
|
|
503
|
-
*/
|
|
504
|
-
async handleStepUp(rpcRequest, trigger, signal) {
|
|
505
|
-
console.error(
|
|
506
|
-
`[gateway] Device re-authorization required (${trigger ?? "unknown"}). Waiting for user approval...`
|
|
507
|
-
);
|
|
508
|
-
const deadline = Date.now() + STEP_UP_MAX_TIMEOUT_MS;
|
|
509
|
-
while (Date.now() < deadline) {
|
|
510
|
-
await sleep(STEP_UP_POLL_INTERVAL_MS);
|
|
511
|
-
let response;
|
|
512
|
-
try {
|
|
513
|
-
response = await this.sendHttpRequest(rpcRequest, signal);
|
|
514
|
-
} catch {
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
this.trackNonce(response);
|
|
518
|
-
if (response.status === 403) {
|
|
519
|
-
const body2 = await this.tryParseErrorBody(response);
|
|
520
|
-
if (body2?.error === "step_up_required") {
|
|
521
|
-
console.error("[gateway] Still waiting for step-up approval...");
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (response.ok) {
|
|
526
|
-
const rpcResponse = await response.json();
|
|
527
|
-
if (rpcResponse.error) {
|
|
528
|
-
return translateJsonRpcError(rpcResponse.error);
|
|
529
|
-
}
|
|
530
|
-
const result = rpcResponse.result;
|
|
531
|
-
return result ?? { content: [{ type: "text", text: "No result" }] };
|
|
532
|
-
}
|
|
533
|
-
const body = await this.tryParseErrorBody(response);
|
|
534
|
-
const translated = translateHttpError(response.status, body?.error);
|
|
535
|
-
if (translated) return translated;
|
|
536
|
-
}
|
|
537
|
-
return {
|
|
538
|
-
content: [
|
|
539
|
-
{
|
|
540
|
-
type: "text",
|
|
541
|
-
text: "Device re-authorization was not approved within 5 minutes. Try again later."
|
|
542
|
-
}
|
|
543
|
-
],
|
|
544
|
-
isError: true
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
502
|
trackNonce(response) {
|
|
548
503
|
const nonce = response.headers.get("DPoP-Nonce");
|
|
549
504
|
if (nonce) {
|
|
@@ -558,9 +513,6 @@ var GatewayClient = class {
|
|
|
558
513
|
}
|
|
559
514
|
}
|
|
560
515
|
};
|
|
561
|
-
function sleep(ms) {
|
|
562
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
563
|
-
}
|
|
564
516
|
|
|
565
517
|
// src/tool-registry.ts
|
|
566
518
|
var ToolRegistry = class {
|
|
@@ -711,27 +663,32 @@ async function startMcpServer(server) {
|
|
|
711
663
|
import psList from "ps-list";
|
|
712
664
|
async function detectRuntime(env = process.env) {
|
|
713
665
|
if (env["KOJEE_RUNTIME"] === "claude-code") return "claude-code";
|
|
666
|
+
if (env["KOJEE_RUNTIME"] === "codex") return "codex";
|
|
714
667
|
if (env["CLAUDE_CODE_SESSION_ID"]) return "claude-code";
|
|
715
|
-
|
|
668
|
+
const ancestor = await detectRuntimeFromAncestry();
|
|
669
|
+
if (ancestor) return ancestor;
|
|
716
670
|
return "unknown";
|
|
717
671
|
}
|
|
718
|
-
async function
|
|
672
|
+
async function detectRuntimeFromAncestry() {
|
|
719
673
|
try {
|
|
720
674
|
const processes = await psList();
|
|
721
675
|
const byPid = new Map(processes.map((p) => [p.pid, p]));
|
|
722
676
|
let pid = process.ppid;
|
|
723
677
|
for (let depth = 0; depth < 5 && pid && pid > 1; depth++) {
|
|
724
678
|
const row = byPid.get(pid);
|
|
725
|
-
if (!row) return
|
|
679
|
+
if (!row) return null;
|
|
726
680
|
const haystack = `${row.name} ${row.cmd ?? ""}`;
|
|
727
681
|
if (/(^|\/|\\)claude(\.app|\.exe)?(\/|$|\s|\\)/i.test(haystack) || /Claude Helper/i.test(haystack)) {
|
|
728
|
-
return
|
|
682
|
+
return "claude-code";
|
|
683
|
+
}
|
|
684
|
+
if (/(^|\/|\\)codex(\.exe)?(\/|$|\s|\\)/i.test(haystack)) {
|
|
685
|
+
return "codex";
|
|
729
686
|
}
|
|
730
687
|
pid = row.ppid;
|
|
731
688
|
}
|
|
732
689
|
} catch {
|
|
733
690
|
}
|
|
734
|
-
return
|
|
691
|
+
return null;
|
|
735
692
|
}
|
|
736
693
|
|
|
737
694
|
// src/adapters/claude-code.ts
|
|
@@ -770,6 +727,18 @@ var claudeCodeAdapter = {
|
|
|
770
727
|
}
|
|
771
728
|
};
|
|
772
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
|
+
|
|
773
742
|
// src/adapters/unknown.ts
|
|
774
743
|
var unknownAdapter = {
|
|
775
744
|
runtime: "unknown",
|
|
@@ -792,6 +761,7 @@ function isDPoPEnrollmentError(err) {
|
|
|
792
761
|
async function selectAdapter() {
|
|
793
762
|
const runtime = await detectRuntime();
|
|
794
763
|
if (runtime === "claude-code") return claudeCodeAdapter;
|
|
764
|
+
if (runtime === "codex") return codexAdapter;
|
|
795
765
|
return unknownAdapter;
|
|
796
766
|
}
|
|
797
767
|
async function listTandemIds(gateway) {
|
|
@@ -812,6 +782,9 @@ async function listTandemIds(gateway) {
|
|
|
812
782
|
return null;
|
|
813
783
|
}
|
|
814
784
|
}
|
|
785
|
+
function needsWebhookEventStream() {
|
|
786
|
+
return (process.env["KOJEE_WEBHOOK_URL"] ?? "").trim().length > 0;
|
|
787
|
+
}
|
|
815
788
|
async function startProxy(config) {
|
|
816
789
|
const keystorePath = config.keystorePath || DEFAULT_KEYSTORE_PATH;
|
|
817
790
|
const adapter = await selectAdapter();
|
|
@@ -836,15 +809,35 @@ async function startProxy(config) {
|
|
|
836
809
|
writeDiscoveryByKey,
|
|
837
810
|
cleanupDiscoveryByKey,
|
|
838
811
|
sweepStaleDiscovery
|
|
839
|
-
} = await import("./session-discovery-
|
|
840
|
-
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-
|
|
812
|
+
} = await import("./session-discovery-FNMJGFPM.js");
|
|
813
|
+
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
|
|
841
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");
|
|
842
817
|
sweepStaleDiscovery();
|
|
843
818
|
sweepStaleEventLogs();
|
|
844
819
|
const ccPid = await findClaudeAncestorPid();
|
|
845
820
|
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
846
821
|
const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
|
|
847
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
|
+
}
|
|
848
841
|
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path);
|
|
849
842
|
const queue = new EventQueue();
|
|
850
843
|
let streamHandle = null;
|
|
@@ -875,7 +868,12 @@ async function startProxy(config) {
|
|
|
875
868
|
port: hookServer.port,
|
|
876
869
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
877
870
|
brokerUrl: config.url,
|
|
878
|
-
eventLogPath: eventLog.path
|
|
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"
|
|
879
877
|
});
|
|
880
878
|
const cleanupDiscoveryFile = () => cleanupDiscoveryByKey(discoveryKey);
|
|
881
879
|
process.on("exit", () => cleanupDiscoveryFile());
|
|
@@ -896,6 +894,9 @@ async function startProxy(config) {
|
|
|
896
894
|
server,
|
|
897
895
|
queue,
|
|
898
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 } : {},
|
|
899
900
|
// Resubscribe-on-start (P0 #2): touch all memberships + write a
|
|
900
901
|
// `status=subscribed n=<count>` line on every (re)connect, so a backend
|
|
901
902
|
// restart / scope reset self-heals and the log is never ambiguously
|
|
@@ -923,6 +924,7 @@ async function startProxy(config) {
|
|
|
923
924
|
const cancelStream = streamHandle;
|
|
924
925
|
process.stdin.on("end", () => {
|
|
925
926
|
cancelStream();
|
|
927
|
+
void webhookSink?.stop();
|
|
926
928
|
cleanupDiscoveryFile();
|
|
927
929
|
eventLog.cleanup();
|
|
928
930
|
hookServer.stop().finally(() => {
|
|
@@ -930,6 +932,62 @@ async function startProxy(config) {
|
|
|
930
932
|
process.exit(0);
|
|
931
933
|
});
|
|
932
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
|
+
});
|
|
933
991
|
} else {
|
|
934
992
|
server = createMcpServer(registry, adapter, tandemMembershipCount);
|
|
935
993
|
process.stdin.on("end", () => {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
secureDir,
|
|
3
|
+
secureFile
|
|
4
|
+
} from "./chunk-BLEGIR35.js";
|
|
5
|
+
|
|
1
6
|
// src/auth/paired-config.ts
|
|
2
7
|
import fs from "fs";
|
|
3
8
|
import os from "os";
|
|
@@ -16,15 +21,9 @@ function loadPairedConfig(filePath = pairedConfigPath()) {
|
|
|
16
21
|
function savePairedConfig(filePath, config) {
|
|
17
22
|
const dir = path.dirname(filePath);
|
|
18
23
|
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
19
|
-
|
|
20
|
-
fs.chmodSync(dir, 448);
|
|
21
|
-
} catch {
|
|
22
|
-
}
|
|
24
|
+
secureDir(dir);
|
|
23
25
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
24
|
-
|
|
25
|
-
fs.chmodSync(filePath, 384);
|
|
26
|
-
} catch {
|
|
27
|
-
}
|
|
26
|
+
secureFile(filePath);
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
export {
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {
|
|
2
|
+
kojeeHomeDir
|
|
3
|
+
} from "./chunk-SQL56SEB.js";
|
|
4
|
+
import {
|
|
5
|
+
secureFile
|
|
6
|
+
} from "./chunk-BLEGIR35.js";
|
|
7
|
+
|
|
8
|
+
// src/wizard/codex-config.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
function defaultCodexConfigPath() {
|
|
12
|
+
return path.join(kojeeHomeDir(), ".codex", "config.toml");
|
|
13
|
+
}
|
|
14
|
+
function defaultCodexHooksPath() {
|
|
15
|
+
return path.join(kojeeHomeDir(), ".codex", "hooks.json");
|
|
16
|
+
}
|
|
17
|
+
var CODEX_STOP_HOOK_COMMAND = "npx -y kojee-mcp hook --type=codex-stop";
|
|
18
|
+
function buildCodexMcpServerTable(opts) {
|
|
19
|
+
return [
|
|
20
|
+
"[mcp_servers.kojee]",
|
|
21
|
+
'command = "npx"',
|
|
22
|
+
'args = ["-y", "kojee-mcp"]',
|
|
23
|
+
"",
|
|
24
|
+
"[mcp_servers.kojee.env]",
|
|
25
|
+
'KOJEE_RUNTIME = "codex"',
|
|
26
|
+
`KOJEE_WEBHOOK_URL = "${escapeTomlString(opts.webhookUrl)}"`,
|
|
27
|
+
`KOJEE_WEBHOOK_SECRET = "${escapeTomlString(opts.webhookSecret)}"`
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
function buildCodexStopHookBlock() {
|
|
31
|
+
return [
|
|
32
|
+
"[[hooks.Stop]]",
|
|
33
|
+
"[[hooks.Stop.hooks]]",
|
|
34
|
+
'type = "command"',
|
|
35
|
+
`command = "${escapeTomlString(CODEX_STOP_HOOK_COMMAND)}"`
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
function escapeTomlString(s) {
|
|
39
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
40
|
+
}
|
|
41
|
+
var KOJEE_TABLE_HEADER = "[mcp_servers.kojee]";
|
|
42
|
+
var KOJEE_ENV_TABLE_HEADER = "[mcp_servers.kojee.env]";
|
|
43
|
+
function writeCodexConfig(inputs) {
|
|
44
|
+
const configPath = inputs.configPath ?? defaultCodexConfigPath();
|
|
45
|
+
const hooksPath = inputs.hooksPath ?? defaultCodexHooksPath();
|
|
46
|
+
let toml = "";
|
|
47
|
+
try {
|
|
48
|
+
toml = fs.readFileSync(configPath, "utf8");
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
toml = upsertKojeeTomlTables(toml, inputs.webhookUrl, inputs.webhookSecret);
|
|
52
|
+
writeFile600(configPath, toml);
|
|
53
|
+
const hooks = readJson(hooksPath);
|
|
54
|
+
hooks.hooks ??= {};
|
|
55
|
+
hooks.hooks.Stop ??= [];
|
|
56
|
+
const already = hooks.hooks.Stop.some(
|
|
57
|
+
(e) => e.hooks?.some((h) => h.command === CODEX_STOP_HOOK_COMMAND)
|
|
58
|
+
);
|
|
59
|
+
if (!already) {
|
|
60
|
+
hooks.hooks.Stop.push({
|
|
61
|
+
hooks: [{ type: "command", command: CODEX_STOP_HOOK_COMMAND }]
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
|
|
65
|
+
}
|
|
66
|
+
function removeCodexConfig(opts = {}) {
|
|
67
|
+
const configPath = opts.configPath ?? defaultCodexConfigPath();
|
|
68
|
+
const hooksPath = opts.hooksPath ?? defaultCodexHooksPath();
|
|
69
|
+
const result = { mcpServer: false, stopHook: false };
|
|
70
|
+
try {
|
|
71
|
+
const toml = fs.readFileSync(configPath, "utf8");
|
|
72
|
+
const stripped = stripKojeeTomlTables(toml);
|
|
73
|
+
if (stripped !== toml) {
|
|
74
|
+
result.mcpServer = true;
|
|
75
|
+
writeFile600(configPath, stripped);
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const hooks = readJson(hooksPath);
|
|
81
|
+
const stop = hooks.hooks?.Stop;
|
|
82
|
+
if (stop && stop.length > 0) {
|
|
83
|
+
const before = stop.length;
|
|
84
|
+
hooks.hooks.Stop = stop.filter(
|
|
85
|
+
(e) => !e.hooks?.some((h) => h.command.startsWith("npx -y kojee-mcp hook"))
|
|
86
|
+
);
|
|
87
|
+
if (hooks.hooks.Stop.length !== before) {
|
|
88
|
+
result.stopHook = true;
|
|
89
|
+
writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret) {
|
|
97
|
+
const parsed = extractKojeeBlock(existing);
|
|
98
|
+
if (!parsed) {
|
|
99
|
+
const block = buildCodexMcpServerTable({ webhookUrl, webhookSecret });
|
|
100
|
+
const base2 = existing.replace(/\s*$/, "");
|
|
101
|
+
return base2.length === 0 ? block + "\n" : base2 + "\n\n" + block + "\n";
|
|
102
|
+
}
|
|
103
|
+
const tableKeys = upsertKeyLines(parsed.tableKeys, [
|
|
104
|
+
["command", '"npx"'],
|
|
105
|
+
["args", '["-y", "kojee-mcp"]']
|
|
106
|
+
]);
|
|
107
|
+
const envKeys = upsertKeyLines(parsed.envKeys, [
|
|
108
|
+
["KOJEE_RUNTIME", '"codex"'],
|
|
109
|
+
["KOJEE_WEBHOOK_URL", `"${escapeTomlString(webhookUrl)}"`],
|
|
110
|
+
["KOJEE_WEBHOOK_SECRET", `"${escapeTomlString(webhookSecret)}"`]
|
|
111
|
+
]);
|
|
112
|
+
const merged = [
|
|
113
|
+
KOJEE_TABLE_HEADER,
|
|
114
|
+
...tableKeys.map(([k, v]) => `${k} = ${v}`),
|
|
115
|
+
"",
|
|
116
|
+
KOJEE_ENV_TABLE_HEADER,
|
|
117
|
+
...envKeys.map(([k, v]) => `${k} = ${v}`)
|
|
118
|
+
].join("\n");
|
|
119
|
+
const base = parsed.rest.replace(/\s*$/, "");
|
|
120
|
+
return base.length === 0 ? merged + "\n" : base + "\n\n" + merged + "\n";
|
|
121
|
+
}
|
|
122
|
+
function upsertKeyLines(existing, owned) {
|
|
123
|
+
const ownedKeys = new Set(owned.map(([k]) => k));
|
|
124
|
+
const preserved = existing.filter(([k]) => !ownedKeys.has(k));
|
|
125
|
+
return [...preserved, ...owned];
|
|
126
|
+
}
|
|
127
|
+
function extractKojeeBlock(toml) {
|
|
128
|
+
const lines = toml.split("\n");
|
|
129
|
+
const rest = [];
|
|
130
|
+
const tableKv = /* @__PURE__ */ new Map();
|
|
131
|
+
const envKv = /* @__PURE__ */ new Map();
|
|
132
|
+
let found = false;
|
|
133
|
+
let section = "other";
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
const header = line.trim();
|
|
136
|
+
const isTableHeader = /^\[\[?[^\]]+\]\]?$/.test(header);
|
|
137
|
+
if (isTableHeader) {
|
|
138
|
+
if (header === KOJEE_TABLE_HEADER) {
|
|
139
|
+
found = true;
|
|
140
|
+
section = "table";
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (header === KOJEE_ENV_TABLE_HEADER) {
|
|
144
|
+
found = true;
|
|
145
|
+
section = "env";
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (header.startsWith("[mcp_servers.kojee.")) {
|
|
149
|
+
section = "other";
|
|
150
|
+
rest.push(line);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
section = "other";
|
|
154
|
+
rest.push(line);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (section === "table" || section === "env") {
|
|
158
|
+
const kv = parseTomlKeyValue(line);
|
|
159
|
+
if (kv) {
|
|
160
|
+
(section === "table" ? tableKv : envKv).set(kv[0], kv[1]);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (header.length > 0) rest.push(line);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
rest.push(line);
|
|
167
|
+
}
|
|
168
|
+
if (!found) return null;
|
|
169
|
+
return {
|
|
170
|
+
tableKeys: [...tableKv.entries()],
|
|
171
|
+
envKeys: [...envKv.entries()],
|
|
172
|
+
rest: rest.join("\n")
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function parseTomlKeyValue(line) {
|
|
176
|
+
const trimmed = line.trim();
|
|
177
|
+
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
178
|
+
const eq = trimmed.indexOf("=");
|
|
179
|
+
if (eq <= 0) return null;
|
|
180
|
+
const key = trimmed.slice(0, eq).trim();
|
|
181
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
182
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) return null;
|
|
183
|
+
return [key, value];
|
|
184
|
+
}
|
|
185
|
+
function stripKojeeTomlTables(toml) {
|
|
186
|
+
const lines = toml.split("\n");
|
|
187
|
+
const out = [];
|
|
188
|
+
let inKojee = false;
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
const header = line.trim();
|
|
191
|
+
const isTableHeader = /^\[\[?[^\]]+\]\]?$/.test(header);
|
|
192
|
+
if (isTableHeader) {
|
|
193
|
+
const isKojee = header === KOJEE_TABLE_HEADER || header === KOJEE_ENV_TABLE_HEADER || header.startsWith("[mcp_servers.kojee.") || header.startsWith("[mcp_servers.kojee]");
|
|
194
|
+
if (isKojee) {
|
|
195
|
+
inKojee = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
inKojee = false;
|
|
199
|
+
}
|
|
200
|
+
if (inKojee) continue;
|
|
201
|
+
out.push(line);
|
|
202
|
+
}
|
|
203
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
204
|
+
}
|
|
205
|
+
function readJson(p) {
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
208
|
+
} catch {
|
|
209
|
+
return {};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function writeFile600(p, content) {
|
|
213
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
214
|
+
fs.writeFileSync(p, content, { mode: 384 });
|
|
215
|
+
secureFile(p);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export {
|
|
219
|
+
defaultCodexConfigPath,
|
|
220
|
+
defaultCodexHooksPath,
|
|
221
|
+
buildCodexMcpServerTable,
|
|
222
|
+
buildCodexStopHookBlock,
|
|
223
|
+
writeCodexConfig,
|
|
224
|
+
removeCodexConfig
|
|
225
|
+
};
|