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.
Files changed (33) hide show
  1. package/README.md +110 -4
  2. package/dist/{chunk-VLZADEFC.js → chunk-2TUAFAIW.js} +6 -9
  3. package/dist/chunk-BLEGIR35.js +43 -0
  4. package/dist/chunk-C6GZ2L2W.js +38 -0
  5. package/dist/{chunk-W6YRLSD4.js → chunk-DO42NPNR.js} +9 -16
  6. package/dist/chunk-EW72ZNQL.js +39 -0
  7. package/dist/chunk-F7L25L2J.js +60 -0
  8. package/dist/chunk-LVL25VLO.js +22 -0
  9. package/dist/chunk-SQL56SEB.js +14 -0
  10. package/dist/{chunk-QB22PD6T.js → chunk-WBMX4CHB.js} +29 -9
  11. package/dist/{chunk-LCFCCWMM.js → chunk-YEC7IHIG.js} +136 -78
  12. package/dist/{chunk-GBOTBYEP.js → chunk-YH27B6SW.js} +7 -8
  13. package/dist/chunk-ZW4SW7LJ.js +225 -0
  14. package/dist/cli.js +54 -80
  15. package/dist/codex-stop-hook-JOTBCS5K.js +72 -0
  16. package/dist/{doctor-GILTOH2R.js → doctor-TSHOMT5X.js} +29 -14
  17. package/dist/doctor-codex-BMI5JOO6.js +130 -0
  18. package/dist/{event-log-R6VW6GAF.js → event-log-RSTM4PLL.js} +3 -2
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.js +4 -3
  21. package/dist/{install-D2HIPOMT.js → install-WBIUVBZW.js} +5 -4
  22. package/dist/{paired-config-RB4SABOS.js → paired-config-JTFLHMZ2.js} +2 -1
  23. package/dist/runtime-record-WO4IECM6.js +14 -0
  24. package/dist/runtimes-CO43XUUK.js +12 -0
  25. package/dist/{session-discovery-QE5TTAPS.js → session-discovery-FNMJGFPM.js} +2 -1
  26. package/dist/{stop-hook-VLQS6QPR.js → stop-hook-SEPWWETV.js} +6 -5
  27. package/dist/{tail-stream-UZ42UIWO.js → tail-stream-BYKO4DW6.js} +4 -3
  28. package/dist/{user-prompt-submit-hook-C42DPDBO.js → user-prompt-submit-hook-ARPEO6FF.js} +2 -1
  29. package/dist/webhook-config-5TLLX7RA.js +10 -0
  30. package/dist/webhook-sink-7OYZBWXA.js +163 -0
  31. package/dist/wizard-7KHD5JT4.js +265 -0
  32. package/package.json +1 -1
  33. package/dist/chunk-E26AHU6J.js +0 -27
@@ -1,17 +1,21 @@
1
1
  import {
2
- MCP_SESSION_ID,
3
- createDPoPProof,
4
- startEventStream
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-E26AHU6J.js";
9
+ } from "./chunk-C6GZ2L2W.js";
11
10
  import {
12
- deriveDiscoveryKey,
13
- findClaudeAncestorPid
14
- } from "./chunk-BJMASMKX.js";
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
- if (!fs.existsSync(dir)) {
51
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
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, _trigger) {
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
- "Device re-authorization required. The user has been notified but has not yet approved. Try again later."
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, and step-up retry transparently.
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
- if (await parentProcessLooksLikeClaude()) return "claude-code";
668
+ const ancestor = await detectRuntimeFromAncestry();
669
+ if (ancestor) return ancestor;
716
670
  return "unknown";
717
671
  }
718
- async function parentProcessLooksLikeClaude() {
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 false;
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 true;
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 false;
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-QE5TTAPS.js");
840
- const { startEventLog, sweepStaleEventLogs } = await import("./event-log-R6VW6GAF.js");
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
- try {
20
- fs.chmodSync(dir, 448);
21
- } catch {
22
- }
24
+ secureDir(dir);
23
25
  fs.writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
24
- try {
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
+ };