kojee-mcp 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +13 -8
  2. package/dist/ancestry-A2F5KQ6A.js +10 -0
  3. package/dist/cc-session-id-RURNIHHC.js +10 -0
  4. package/dist/{chunk-KH5YEJIM.js → chunk-247WFMCJ.js} +7 -20
  5. package/dist/{chunk-BYV7CWNY.js → chunk-5H75AEZ2.js} +22 -9
  6. package/dist/{chunk-YH27B6SW.js → chunk-5SZHXYPK.js} +16 -3
  7. package/dist/{chunk-DO42NPNR.js → chunk-67F67AQ6.js} +14 -3
  8. package/dist/{chunk-CH32ELFX.js → chunk-6G6YYST6.js} +1 -1
  9. package/dist/chunk-D6JKFJ6A.js +11 -0
  10. package/dist/chunk-E6WMFMM2.js +100 -0
  11. package/dist/chunk-EIAUW6KO.js +82 -0
  12. package/dist/chunk-EIH2LNF4.js +45 -0
  13. package/dist/{chunk-XN3IC4B5.js → chunk-FSKGQ6GT.js} +30 -5
  14. package/dist/{chunk-GI2CKKBL.js → chunk-GKXHKR3B.js} +1 -1
  15. package/dist/{setup-handler-7HOFAD7C.js → chunk-GVWYW7MY.js} +247 -246
  16. package/dist/{chunk-JXMVZEQ7.js → chunk-I67C2HYA.js} +1 -1
  17. package/dist/chunk-KNEJTD6G.js +31 -0
  18. package/dist/{chunk-NR4Y54OL.js → chunk-MIEI4PLB.js} +1 -19
  19. package/dist/chunk-NE2F5CKS.js +113 -0
  20. package/dist/{chunk-N35X22G7.js → chunk-OB5T6P24.js} +1 -1
  21. package/dist/{chunk-65KRRDHP.js → chunk-OBIJ6WQP.js} +82 -27
  22. package/dist/{chunk-Q7T3WAST.js → chunk-QEJUNP3X.js} +2 -1
  23. package/dist/chunk-QJFMU4QC.js +33 -0
  24. package/dist/{chunk-EW72ZNQL.js → chunk-RDLF4NQC.js} +1 -1
  25. package/dist/{chunk-ALZX3SGI.js → chunk-SGRVG4HW.js} +8 -3
  26. package/dist/{chunk-DURRQDYI.js → chunk-TNZITJAB.js} +45 -15
  27. package/dist/chunk-U5HHHRXA.js +103 -0
  28. package/dist/chunk-WBXU27BF.js +35 -0
  29. package/dist/chunk-WIU5WGDF.js +10 -0
  30. package/dist/chunk-XJEBJIQE.js +52 -0
  31. package/dist/chunk-Z5LPNJQ6.js +20 -0
  32. package/dist/cli.js +62 -25
  33. package/dist/{chunk-KFDZWNQO.js → codex-stop-hook-EOMQD3J4.js} +11 -15
  34. package/dist/connect-handler-HIRM624N.js +137 -0
  35. package/dist/{control-token-4BUCTYQB.js → control-token-KXSZ3GBY.js} +2 -2
  36. package/dist/{doctor-WVX3WCYX.js → doctor-PLKLHTJM.js} +48 -22
  37. package/dist/{doctor-codex-Z5M4SFBG.js → doctor-codex-HARAAH4Y.js} +23 -6
  38. package/dist/doctor-openclaw-LL6NSPLC.js +86 -0
  39. package/dist/{event-log-HOBO3ECB.js → event-log-2NBJEIEP.js} +5 -3
  40. package/dist/{event-stream-SYBUJ5EH.js → event-stream-KRYWEYWO.js} +7 -4
  41. package/dist/index.js +12 -10
  42. package/dist/{install-LJY2CHKG.js → install-V7LSQCYZ.js} +37 -22
  43. package/dist/{keystore-XLEV3FL5.js → keystore-LQNFDHGO.js} +2 -2
  44. package/dist/lib.d.ts +4 -1
  45. package/dist/lib.js +11 -8
  46. package/dist/pair-HTCVSMR6.js +11 -0
  47. package/dist/{paired-config-JTFLHMZ2.js → paired-config-GUDDO2CH.js} +4 -2
  48. package/dist/{parent-watchdog-TLU355FB.js → parent-watchdog-LTUNF7PD.js} +24 -5
  49. package/dist/{registry-YLOFZDI2.js → registry-VRFHKOPP.js} +193 -24
  50. package/dist/resolve-slot-BFIB46Y5.js +13 -0
  51. package/dist/{runtime-record-WO4IECM6.js → runtime-record-OXRLTOLC.js} +2 -2
  52. package/dist/{send-cli-IYJF32PE.js → send-cli-45RLGYIC.js} +22 -10
  53. package/dist/{server-R24AVNTI.js → server-YTHOMA4H.js} +4 -2
  54. package/dist/{session-discovery-FNMJGFPM.js → session-discovery-NHCZ2XWA.js} +2 -2
  55. package/dist/setup-handler-UHVKSX7E.js +159 -0
  56. package/dist/{stop-hook-TCPJFGFF.js → stop-hook-YI4KRKF2.js} +17 -20
  57. package/dist/{tail-stream-4IMXI44S.js → tail-stream-NBGHHBS4.js} +40 -22
  58. package/dist/{user-prompt-submit-hook-FQ43NIHC.js → user-prompt-submit-hook-DTXXFDSD.js} +11 -14
  59. package/package.json +1 -1
  60. package/dist/ancestry-ONFBQEP5.js +0 -8
  61. package/dist/chunk-BLEGIR35.js +0 -43
  62. package/dist/chunk-CLKCNV2A.js +0 -100
  63. package/dist/chunk-OGHDTFAX.js +0 -50
  64. package/dist/chunk-VHKPWUX7.js +0 -41
  65. package/dist/codex-stop-hook-RWQSZTST.js +0 -16
  66. package/dist/doctor-openclaw-SS2TMQOX.js +0 -95
  67. package/dist/pair-P4ILCMT7.js +0 -10
@@ -2,7 +2,7 @@ import {
2
2
  generateES256KeyPair,
3
3
  loadKeystore,
4
4
  saveKeystore
5
- } from "./chunk-CH32ELFX.js";
5
+ } from "./chunk-6G6YYST6.js";
6
6
 
7
7
  // src/auth/auth-module.ts
8
8
  import { calculateJwkThumbprint } from "jose";
@@ -0,0 +1,31 @@
1
+ // src/runtime/cc-session-id.ts
2
+ import { randomUUID } from "crypto";
3
+ var SESSION_ID_ENV_VARS = ["CLAUDE_CODE_SESSION_ID"];
4
+ function sanitizeKey(value) {
5
+ return value.replace(/[^A-Za-z0-9_-]/g, "");
6
+ }
7
+ function resolveInstanceKey(deps = {}) {
8
+ const env = deps.env ?? process.env;
9
+ for (const name of SESSION_ID_ENV_VARS) {
10
+ const sid = sanitizeKey((env[name] ?? "").trim());
11
+ if (sid) return sid;
12
+ }
13
+ const explicit = sanitizeKey((env.KOJEE_INSTANCE ?? "").trim());
14
+ if (explicit) return `inst-${explicit}`;
15
+ const mint = deps.randomUUID ?? randomUUID;
16
+ return `mint-${mint()}`;
17
+ }
18
+ function resolveSharedSessionId(deps = {}) {
19
+ const env = deps.env ?? process.env;
20
+ const fromEnv = sanitizeKey((env["CLAUDE_CODE_SESSION_ID"] ?? "").trim());
21
+ if (fromEnv) return fromEnv;
22
+ const fromStdin = sanitizeKey((deps.stdinSessionId ?? "").trim());
23
+ if (fromStdin) return fromStdin;
24
+ return null;
25
+ }
26
+
27
+ export {
28
+ SESSION_ID_ENV_VARS,
29
+ resolveInstanceKey,
30
+ resolveSharedSessionId
31
+ };
@@ -25,24 +25,6 @@ function computeAth(accessToken) {
25
25
  return base64url.encode(hash);
26
26
  }
27
27
 
28
- // src/tandem/session-id.ts
29
- import crypto2 from "crypto";
30
- import { ulid } from "ulidx";
31
- var MCP_SESSION_ID = ulid();
32
- function deriveStableSessionId(token, instanceKey) {
33
- return crypto2.createHash("sha256").update(`${token}:${instanceKey}`).digest("hex").slice(0, 32);
34
- }
35
- var _stableSessionId = null;
36
- function setStableSessionId(id) {
37
- _stableSessionId = id;
38
- }
39
- function getSessionId() {
40
- return _stableSessionId ?? MCP_SESSION_ID;
41
- }
42
-
43
28
  export {
44
- createDPoPProof,
45
- deriveStableSessionId,
46
- setStableSessionId,
47
- getSessionId
29
+ createDPoPProof
48
30
  };
@@ -0,0 +1,113 @@
1
+ // src/delivery/openclaw-wake.ts
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ var DEFAULT_GATEWAY_PORT = 18789;
6
+ var DEFAULT_HOOKS_PATH = "/hooks";
7
+ function resolveOpenclawConfigPath(env = process.env) {
8
+ const explicit = (env["KOJEE_OPENCLAW_CONFIG"] ?? env["OPENCLAW_CONFIG_PATH"] ?? "").trim();
9
+ return explicit || path.join(os.homedir(), ".openclaw", "openclaw.json");
10
+ }
11
+ function parsePort(raw) {
12
+ const t = raw?.trim();
13
+ if (!t || !/^\d+$/.test(t)) return null;
14
+ const n = Number.parseInt(t, 10);
15
+ return Number.isFinite(n) && n > 0 ? n : null;
16
+ }
17
+ function resolveOpenclawWakeTarget(opts = {}) {
18
+ const env = opts.env ?? process.env;
19
+ const configPath = opts.configPath ?? resolveOpenclawConfigPath(env);
20
+ let raw = null;
21
+ try {
22
+ raw = fs.readFileSync(configPath, "utf8");
23
+ } catch {
24
+ raw = null;
25
+ }
26
+ return resolveWakeTargetFromRaw(raw, configPath, env);
27
+ }
28
+ function resolveWakeTargetFromRaw(raw, configPath, env) {
29
+ let cfg = {};
30
+ if (raw !== null) {
31
+ try {
32
+ const parsed = JSON.parse(raw);
33
+ cfg = parsed && typeof parsed === "object" ? parsed : {};
34
+ } catch {
35
+ cfg = {};
36
+ }
37
+ }
38
+ const envUrl = (env["KOJEE_OPENCLAW_WAKE_URL"] ?? "").trim();
39
+ const envToken = (env["KOJEE_OPENCLAW_WAKE_TOKEN"] ?? "").trim();
40
+ const cfgToken = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : "";
41
+ const token = envToken || cfgToken;
42
+ const hooksEnabled = cfg.hooks?.enabled === true;
43
+ let url = envUrl;
44
+ if (!url) {
45
+ const cfgPort = typeof cfg.gateway?.port === "number" && cfg.gateway.port > 0 ? cfg.gateway.port : DEFAULT_GATEWAY_PORT;
46
+ const port = parsePort(env["OPENCLAW_GATEWAY_PORT"]) ?? cfgPort;
47
+ const rawPath = typeof cfg.hooks?.path === "string" && cfg.hooks.path.trim() || DEFAULT_HOOKS_PATH;
48
+ const withSlash = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
49
+ const base = withSlash.length > 1 ? withSlash.replace(/\/+$/, "") : withSlash;
50
+ url = `http://127.0.0.1:${port}${base}/wake`;
51
+ }
52
+ if (!token) {
53
+ return {
54
+ ok: false,
55
+ reason: `OpenClaw hooks token not found (KOJEE_OPENCLAW_WAKE_TOKEN or hooks.token in ${configPath}). Run \`kojee-mcp connect <code> --runtime openclaw\` to enable hooks.`
56
+ };
57
+ }
58
+ if (!hooksEnabled && !envToken && !envUrl) {
59
+ return {
60
+ ok: false,
61
+ reason: `OpenClaw hooks disabled (hooks.enabled != true in ${configPath}); /wake is not mounted.`
62
+ };
63
+ }
64
+ return { ok: true, target: { url, token } };
65
+ }
66
+ function createCachedOpenclawWakeResolver(opts = {}) {
67
+ let cache = null;
68
+ return async () => {
69
+ const env = opts.env ?? process.env;
70
+ const configPath = opts.configPath ?? resolveOpenclawConfigPath(env);
71
+ let mtimeMs = -1;
72
+ try {
73
+ mtimeMs = (await fs.promises.stat(configPath)).mtimeMs;
74
+ } catch {
75
+ }
76
+ if (cache && cache.mtimeMs === mtimeMs) return cache.resolution;
77
+ let raw = null;
78
+ if (mtimeMs !== -1) {
79
+ try {
80
+ raw = await fs.promises.readFile(configPath, "utf8");
81
+ } catch {
82
+ raw = null;
83
+ }
84
+ }
85
+ const resolution = resolveWakeTargetFromRaw(raw, configPath, env);
86
+ cache = { mtimeMs, resolution };
87
+ return resolution;
88
+ };
89
+ }
90
+ async function postOpenclawWake(target, body, deps = {}) {
91
+ const doFetch = deps.fetch ?? fetch;
92
+ const controller = new AbortController();
93
+ const timer = setTimeout(() => controller.abort(), deps.timeoutMs ?? 4e3);
94
+ try {
95
+ const res = await doFetch(target.url, {
96
+ method: "POST",
97
+ headers: { "content-type": "application/json", authorization: `Bearer ${target.token}` },
98
+ body: JSON.stringify(body),
99
+ signal: controller.signal
100
+ });
101
+ return res.ok ? { ok: true, status: res.status } : { ok: false, status: res.status, error: `wake POST ${res.status}` };
102
+ } catch (err) {
103
+ return { ok: false, error: err.message };
104
+ } finally {
105
+ clearTimeout(timer);
106
+ }
107
+ }
108
+
109
+ export {
110
+ resolveOpenclawWakeTarget,
111
+ createCachedOpenclawWakeResolver,
112
+ postOpenclawWake
113
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildReplyRecipe
3
- } from "./chunk-Q7T3WAST.js";
3
+ } from "./chunk-QEJUNP3X.js";
4
4
 
5
5
  // src/adapters/claude-code.ts
6
6
  function computeSeverity(event) {
@@ -1,36 +1,72 @@
1
+ import {
2
+ buildHookCommand,
3
+ isKojeeHookCommand,
4
+ npxHookCommand
5
+ } from "./chunk-QJFMU4QC.js";
6
+ import {
7
+ wrapNpxLauncher
8
+ } from "./chunk-D6JKFJ6A.js";
1
9
  import {
2
10
  kojeeHomeDir
3
11
  } from "./chunk-SQL56SEB.js";
4
12
  import {
5
13
  secureFile
6
- } from "./chunk-BLEGIR35.js";
14
+ } from "./chunk-U5HHHRXA.js";
7
15
 
8
16
  // src/wizard/codex-config.ts
9
17
  import fs from "fs";
18
+ import os from "os";
10
19
  import path from "path";
20
+ function win32DriveQualified(p, ...homeSegments) {
21
+ if (process.platform === "win32" && !/^[a-zA-Z]:/.test(p)) {
22
+ return path.win32.resolve(path.win32.join(os.homedir(), ...homeSegments));
23
+ }
24
+ return p;
25
+ }
11
26
  function defaultCodexConfigPath() {
12
- return path.join(kojeeHomeDir(), ".codex", "config.toml");
27
+ return win32DriveQualified(path.join(kojeeHomeDir(), ".codex", "config.toml"), ".codex", "config.toml");
13
28
  }
14
29
  function defaultCodexHooksPath() {
15
- return path.join(kojeeHomeDir(), ".codex", "hooks.json");
30
+ return win32DriveQualified(path.join(kojeeHomeDir(), ".codex", "hooks.json"), ".codex", "hooks.json");
16
31
  }
17
- var CODEX_STOP_HOOK_COMMAND = "npx -y kojee-mcp hook --type=codex-stop";
18
- function codexArgsLiteral(token, url) {
32
+ var CODEX_STOP_HOOK_COMMAND = npxHookCommand("codex-stop");
33
+ function codexStopHookCommand() {
34
+ return buildHookCommand("codex-stop");
35
+ }
36
+ function codexRestArgs(token, url, pairedConfigPath) {
19
37
  if (token && url) {
20
- return `["-y", "kojee-mcp", "--token", "${escapeTomlString(token)}", "--url", "${escapeTomlString(url)}"]`;
38
+ return ["-y", "kojee-mcp", "--token", token, "--url", url];
39
+ }
40
+ if (pairedConfigPath) {
41
+ return ["-y", "kojee-mcp", "--paired-config", pairedConfigPath];
21
42
  }
22
- return '["-y", "kojee-mcp"]';
43
+ return ["-y", "kojee-mcp"];
44
+ }
45
+ function codexLauncherLiterals(token, url, pairedConfigPath) {
46
+ const rest = codexRestArgs(token, url, pairedConfigPath);
47
+ const { command, args } = wrapNpxLauncher("npx", rest);
48
+ return {
49
+ commandLiteral: `"${escapeTomlString(command)}"`,
50
+ argsLiteral: "[" + args.map((a) => `"${escapeTomlString(a)}"`).join(", ") + "]"
51
+ };
23
52
  }
24
53
  function buildCodexMcpServerTable(opts) {
54
+ const { commandLiteral, argsLiteral } = codexLauncherLiterals(
55
+ opts.token,
56
+ opts.url,
57
+ opts.pairedConfigPath
58
+ );
25
59
  return [
26
60
  "[mcp_servers.kojee]",
27
- 'command = "npx"',
28
- `args = ${codexArgsLiteral(opts.token, opts.url)}`,
61
+ `command = ${commandLiteral}`,
62
+ `args = ${argsLiteral}`,
29
63
  "",
30
64
  "[mcp_servers.kojee.env]",
31
65
  'KOJEE_RUNTIME = "codex"',
32
- `KOJEE_WEBHOOK_URL = "${escapeTomlString(opts.webhookUrl)}"`,
33
- `KOJEE_WEBHOOK_SECRET = "${escapeTomlString(opts.webhookSecret)}"`,
66
+ // Webhook env is emitted ONLY when supplied (the credential-only `connect`
67
+ // path omits both → no placeholder URL, no fresh secret).
68
+ ...opts.webhookUrl !== void 0 ? [`KOJEE_WEBHOOK_URL = "${escapeTomlString(opts.webhookUrl)}"`] : [],
69
+ ...opts.webhookSecret !== void 0 ? [`KOJEE_WEBHOOK_SECRET = "${escapeTomlString(opts.webhookSecret)}"`] : [],
34
70
  // Signature emission overrides (0.5.3) — emitted only when configured.
35
71
  ...(opts.signatureEnv ?? []).map(([k, v]) => `${k} = "${escapeTomlString(v)}"`)
36
72
  ].join("\n");
@@ -40,7 +76,7 @@ function buildCodexStopHookBlock() {
40
76
  "[[hooks.Stop]]",
41
77
  "[[hooks.Stop.hooks]]",
42
78
  'type = "command"',
43
- `command = "${escapeTomlString(CODEX_STOP_HOOK_COMMAND)}"`
79
+ `command = "${escapeTomlString(codexStopHookCommand())}"`
44
80
  ].join("\n");
45
81
  }
46
82
  function escapeTomlString(s) {
@@ -62,18 +98,30 @@ function writeCodexConfig(inputs) {
62
98
  inputs.webhookSecret,
63
99
  inputs.signatureEnv ?? [],
64
100
  inputs.token,
65
- inputs.url
101
+ inputs.url,
102
+ inputs.pairedConfigPath
66
103
  );
67
104
  writeFile600(configPath, toml);
105
+ const command = codexStopHookCommand();
68
106
  const hooks = readJson(hooksPath);
69
107
  hooks.hooks ??= {};
70
108
  hooks.hooks.Stop ??= [];
71
- const already = hooks.hooks.Stop.some(
72
- (e) => e.hooks?.some((h) => h.command === CODEX_STOP_HOOK_COMMAND)
73
- );
74
- if (!already) {
109
+ let present = false;
110
+ for (const e of hooks.hooks.Stop) {
111
+ for (const h of e.hooks ?? []) {
112
+ if (h.command === command) {
113
+ present = true;
114
+ } else if (isKojeeHookCommand(h.command)) {
115
+ h.command = command;
116
+ present = true;
117
+ }
118
+ if (present) break;
119
+ }
120
+ if (present) break;
121
+ }
122
+ if (!present) {
75
123
  hooks.hooks.Stop.push({
76
- hooks: [{ type: "command", command: CODEX_STOP_HOOK_COMMAND }]
124
+ hooks: [{ type: "command", command }]
77
125
  });
78
126
  }
79
127
  writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
@@ -97,7 +145,8 @@ function removeCodexConfig(opts = {}) {
97
145
  if (stop && stop.length > 0) {
98
146
  const before = stop.length;
99
147
  hooks.hooks.Stop = stop.filter(
100
- (e) => !e.hooks?.some((h) => h.command.startsWith("npx -y kojee-mcp hook"))
148
+ // F10: strip kojee entries in EITHER persisted form (npx or pinned).
149
+ (e) => !e.hooks?.some((h) => isKojeeHookCommand(h.command))
101
150
  );
102
151
  if (hooks.hooks.Stop.length !== before) {
103
152
  result.stopHook = true;
@@ -108,27 +157,33 @@ function removeCodexConfig(opts = {}) {
108
157
  }
109
158
  return result;
110
159
  }
111
- function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret, signatureEnv = [], token, url) {
160
+ function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret, signatureEnv = [], token, url, pairedConfigPath) {
112
161
  const parsed = extractKojeeBlock(existing);
113
162
  if (!parsed) {
114
163
  const block = buildCodexMcpServerTable({
115
- webhookUrl,
116
- webhookSecret,
164
+ ...webhookUrl !== void 0 ? { webhookUrl } : {},
165
+ ...webhookSecret !== void 0 ? { webhookSecret } : {},
117
166
  ...signatureEnv.length > 0 ? { signatureEnv } : {},
118
167
  ...token ? { token } : {},
119
- ...url ? { url } : {}
168
+ ...url ? { url } : {},
169
+ ...pairedConfigPath ? { pairedConfigPath } : {}
120
170
  });
121
171
  const base2 = existing.replace(/\s*$/, "");
122
172
  return base2.length === 0 ? block + "\n" : base2 + "\n\n" + block + "\n";
123
173
  }
174
+ const { commandLiteral, argsLiteral } = codexLauncherLiterals(token, url, pairedConfigPath);
124
175
  const tableKeys = upsertKeyLines(parsed.tableKeys, [
125
- ["command", '"npx"'],
126
- ["args", codexArgsLiteral(token, url)]
176
+ ["command", commandLiteral],
177
+ ["args", argsLiteral]
127
178
  ]);
128
179
  const envKeys = upsertKeyLines(parsed.envKeys, [
129
180
  ["KOJEE_RUNTIME", '"codex"'],
130
- ["KOJEE_WEBHOOK_URL", `"${escapeTomlString(webhookUrl)}"`],
131
- ["KOJEE_WEBHOOK_SECRET", `"${escapeTomlString(webhookSecret)}"`],
181
+ // Own KOJEE_WEBHOOK_URL/SECRET ONLY when supplied this run. The credential-
182
+ // only `connect` path passes neither → they drop out of the owned set and
183
+ // any existing operator/receiver values are PRESERVED verbatim (no clobber,
184
+ // no rotated secret, byte-stable re-runs).
185
+ ...webhookUrl !== void 0 ? [["KOJEE_WEBHOOK_URL", `"${escapeTomlString(webhookUrl)}"`]] : [],
186
+ ...webhookSecret !== void 0 ? [["KOJEE_WEBHOOK_SECRET", `"${escapeTomlString(webhookSecret)}"`]] : [],
132
187
  // Signature emission overrides (0.5.3) — owned only when configured this
133
188
  // run; an operator's existing signature lines are otherwise preserved.
134
189
  ...signatureEnv.map(([k, v]) => [k, `"${escapeTomlString(v)}"`])
@@ -27,7 +27,8 @@ function buildWebhookReceiverNote(sig) {
27
27
  }
28
28
  var CODEX_LISTEN_CAP_MS = 8e3;
29
29
  function buildCodexWakeReason(cursor) {
30
- return `[kojee] new Tandem event(s) pending (cursor=${cursor}). If relevant to your task, drain them now: call tandem_messages(tandem_id, since=${cursor - 1}) to read, then ` + buildReplyRecipe() + `. If you expect an imminent reply and want to wait for exactly one, call tandem_listen(tandem_id, since=${cursor}, timeout_ms<=${CODEX_LISTEN_CAP_MS}) \u2014 a BOUNDED wait, cap 8s, NEVER an unconditional long-poll. If not relevant, ignore.`;
30
+ const drainSince = Math.max(0, cursor - 1);
31
+ return `[kojee] new Tandem event(s) pending (cursor=${cursor}). If relevant to your task, drain them now: call tandem_messages(tandem_id, since=${drainSince}) to read, then ` + buildReplyRecipe() + `. If you expect an imminent reply and want to wait for exactly one, call tandem_listen(tandem_id, since=${cursor}, timeout_ms<=${CODEX_LISTEN_CAP_MS}) \u2014 a BOUNDED wait, cap 8s, NEVER an unconditional long-poll. If not relevant, ignore.`;
31
32
  }
32
33
 
33
34
  export {
@@ -0,0 +1,33 @@
1
+ // src/hooks/hook-command.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ function npxHookCommand(type) {
6
+ return `npx -y kojee-mcp hook --type=${type}`;
7
+ }
8
+ function resolveCliEntry(moduleUrl = import.meta.url) {
9
+ try {
10
+ const here = fileURLToPath(moduleUrl);
11
+ const candidate = path.join(path.dirname(here), "cli.js");
12
+ return fs.existsSync(candidate) ? candidate : null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function buildHookCommand(type, opts = {}) {
18
+ const cliEntry = opts.cliEntry !== void 0 ? opts.cliEntry : resolveCliEntry();
19
+ if (!cliEntry) return npxHookCommand(type);
20
+ const execPath = opts.execPath ?? process.execPath;
21
+ return `"${execPath}" "${cliEntry}" hook --type=${type}`;
22
+ }
23
+ var PINNED_HOOK_RE = /^"[^"]+" "[^"]+" hook --type=(stop|user-prompt-submit|codex-stop)$/;
24
+ function isKojeeHookCommand(command) {
25
+ if (command.startsWith("npx -y kojee-mcp hook --type=")) return true;
26
+ return PINNED_HOOK_RE.test(command);
27
+ }
28
+
29
+ export {
30
+ npxHookCommand,
31
+ buildHookCommand,
32
+ isKojeeHookCommand
33
+ };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-SQL56SEB.js";
4
4
  import {
5
5
  secureFile
6
- } from "./chunk-BLEGIR35.js";
6
+ } from "./chunk-U5HHHRXA.js";
7
7
 
8
8
  // src/wizard/runtime-record.ts
9
9
  import fs from "fs";
@@ -2,7 +2,7 @@ import {
2
2
  buildCatchUpNote,
3
3
  buildMonitorSpawn,
4
4
  buildReplyRecipe
5
- } from "./chunk-Q7T3WAST.js";
5
+ } from "./chunk-QEJUNP3X.js";
6
6
  import {
7
7
  translateToolCallResult
8
8
  } from "./chunk-PPTKGWFF.js";
@@ -38,7 +38,11 @@ function resolveVersion() {
38
38
  var VERSION = resolveVersion();
39
39
 
40
40
  // src/server.ts
41
+ function buildNonChannelInstructions() {
42
+ return "You are connected through the LOCAL kojee proxy. Do ALL Tandem operations (tandem_join/tandem_messages/tandem_send/tandem_set_wake_filter) through THESE tools \u2014 this is your ONLY wake-capable path: the proxy can only wake you for Tandem traffic that flows through it. If a REMOTE Kojee connector (e.g. a claude.ai-hosted 'Kojee' MCP) is ALSO available, do NOT use its Tandem tools \u2014 they cannot wake you.";
43
+ }
41
44
  function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
45
+ const localProxyRule = "You are connected through the LOCAL kojee proxy. Do ALL Tandem operations (join/read/send/set_wake_filter) through THESE tools \u2014 this is your ONLY wake-capable path: it writes the wake log the Monitor and stop-hook read. If a REMOTE Kojee connector (e.g. a claude.ai-hosted 'Kojee' MCP) is ALSO available, do NOT use its Tandem tools \u2014 they cannot wake you.\n\n";
42
46
  const intro = `Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:
43
47
 
44
48
  (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()}.
@@ -49,7 +53,7 @@ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
49
53
  `;
50
54
  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.";
51
55
  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.";
52
- return intro + monitorSection + listenSection + advice;
56
+ return localProxyRule + intro + monitorSection + listenSection + advice;
53
57
  }
54
58
  function tandemIdArg(args) {
55
59
  return typeof args["tandem_id"] === "string" ? args["tandem_id"] : null;
@@ -75,7 +79,7 @@ function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLog
75
79
  { name: "kojee-mcp", version: VERSION },
76
80
  {
77
81
  capabilities,
78
- ...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
82
+ ...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : { instructions: buildNonChannelInstructions() }
79
83
  }
80
84
  );
81
85
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -101,6 +105,7 @@ async function startMcpServer(server) {
101
105
 
102
106
  export {
103
107
  VERSION,
108
+ buildNonChannelInstructions,
104
109
  buildChannelInstructions,
105
110
  executeToolCall,
106
111
  createMcpServer,
@@ -1,9 +1,10 @@
1
1
  import {
2
+ readSessionDiscoveryByKey,
2
3
  sessionDiscoveryDir
3
- } from "./chunk-DO42NPNR.js";
4
+ } from "./chunk-67F67AQ6.js";
4
5
  import {
5
6
  secureFile
6
- } from "./chunk-BLEGIR35.js";
7
+ } from "./chunk-U5HHHRXA.js";
7
8
 
8
9
  // src/delivery/lib/event-log.ts
9
10
  import fs from "fs";
@@ -21,23 +22,35 @@ function statusLogPath(eventLogPath) {
21
22
  const m = base.match(/^kojee-events-(.+)\.log$/);
22
23
  return path.join(dir, m ? `kojee-status-${m[1]}.log` : `${base}.status`);
23
24
  }
24
- function livePathOwnedBySibling(candidatePath) {
25
+ function livePathOwnedByForeignProxy(candidatePath, selfKey) {
25
26
  try {
26
- const { livePaths } = listActiveSessionIds();
27
- return livePaths.has(path.resolve(candidatePath));
27
+ const resolved = path.resolve(candidatePath);
28
+ const { liveOwners } = scanDiscovery(false);
29
+ for (const owner of liveOwners) {
30
+ if (owner.eventLogPath !== resolved) continue;
31
+ if (selfKey && owner.discoveryKey === selfKey) continue;
32
+ return true;
33
+ }
34
+ return false;
28
35
  } catch {
29
36
  return false;
30
37
  }
31
38
  }
39
+ function resolveEventLogPath(opts) {
40
+ const dir = opts.dir ?? DEFAULT_DIR;
41
+ const base = path.join(dir, `kojee-events-${opts.key}.log`);
42
+ if (livePathOwnedByForeignProxy(base, opts.selfKey)) {
43
+ const suffix = opts.selfKey ?? String(process.pid);
44
+ return path.join(dir, `kojee-events-${opts.key}-${suffix}.log`);
45
+ }
46
+ return base;
47
+ }
32
48
  function startEventLog(opts) {
33
49
  const dir = opts.dir ?? DEFAULT_DIR;
34
50
  const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
35
51
  const statusMaxBytes = opts.statusMaxBytes ?? DEFAULT_STATUS_MAX_BYTES;
36
52
  fs.mkdirSync(dir, { recursive: true });
37
- let filePath = path.join(dir, `kojee-events-${opts.key}.log`);
38
- if (livePathOwnedBySibling(filePath)) {
39
- filePath = path.join(dir, `kojee-events-${opts.key}-${process.pid}.log`);
40
- }
53
+ const filePath = resolveEventLogPath(opts);
41
54
  const statusPath = statusLogPath(filePath);
42
55
  fs.writeFileSync(filePath, "", { mode: 384 });
43
56
  secureFile(filePath);
@@ -114,6 +127,13 @@ function startEventLog(opts) {
114
127
  });
115
128
  },
116
129
  cleanup() {
130
+ if (opts.selfKey) {
131
+ const disc = readSessionDiscoveryByKey(opts.selfKey);
132
+ const owner = disc?.proxyPid ?? disc?.pid;
133
+ if (disc && typeof owner === "number" && owner !== process.pid && isProcessAlive(owner) && disc.eventLogPath && path.resolve(disc.eventLogPath) === path.resolve(filePath)) {
134
+ return;
135
+ }
136
+ }
117
137
  try {
118
138
  fs.unlinkSync(filePath);
119
139
  } catch {
@@ -175,19 +195,21 @@ function isProcessAlive(pid) {
175
195
  process.kill(pid, 0);
176
196
  return true;
177
197
  } catch (err) {
178
- if (err.code === "EPERM") return true;
198
+ const code = err.code;
199
+ if (code === "EPERM" || code === "EACCES") return true;
179
200
  return false;
180
201
  }
181
202
  }
182
- function listActiveSessionIds() {
203
+ function scanDiscovery(reap) {
183
204
  const dir = sessionDiscoveryDir();
184
205
  const active = /* @__PURE__ */ new Set();
185
206
  const livePaths = /* @__PURE__ */ new Set();
207
+ const liveOwners = [];
186
208
  let entries;
187
209
  try {
188
210
  entries = fs.readdirSync(dir);
189
211
  } catch {
190
- return { active, livePaths };
212
+ return { active, livePaths, liveOwners };
191
213
  }
192
214
  const dead = [];
193
215
  for (const name of entries) {
@@ -200,13 +222,20 @@ function listActiveSessionIds() {
200
222
  const livenessPid = data.proxyPid ?? data.pid;
201
223
  if (typeof livenessPid === "number" && isProcessAlive(livenessPid)) {
202
224
  active.add(sessionId);
203
- if (data.eventLogPath) livePaths.add(path.resolve(data.eventLogPath));
225
+ if (data.eventLogPath) {
226
+ livePaths.add(path.resolve(data.eventLogPath));
227
+ liveOwners.push({
228
+ discoveryKey: data.discoveryKey ?? sessionId,
229
+ eventLogPath: path.resolve(data.eventLogPath)
230
+ });
231
+ }
204
232
  } else {
205
233
  dead.push({ filePath, eventLogPath: data.eventLogPath });
206
234
  }
207
235
  } catch {
208
236
  }
209
237
  }
238
+ if (!reap) return { active, livePaths, liveOwners };
210
239
  for (const { filePath, eventLogPath } of dead) {
211
240
  try {
212
241
  fs.unlinkSync(filePath);
@@ -216,14 +245,14 @@ function listActiveSessionIds() {
216
245
  reapEventLogSiblings(eventLogPath);
217
246
  }
218
247
  }
219
- return { active, livePaths };
248
+ return { active, livePaths, liveOwners };
220
249
  }
221
250
  function sweepStaleEventLogs(dir = DEFAULT_DIR, minAgeMs = DEFAULT_MIN_AGE_MS) {
222
251
  try {
223
252
  fs.unlinkSync(path.join(dir, "kojee-events-no-session.log"));
224
253
  } catch {
225
254
  }
226
- const { active, livePaths } = listActiveSessionIds();
255
+ const { active, livePaths } = scanDiscovery(true);
227
256
  let entries;
228
257
  try {
229
258
  entries = fs.readdirSync(dir);
@@ -266,6 +295,7 @@ function sweepStaleEventLogs(dir = DEFAULT_DIR, minAgeMs = DEFAULT_MIN_AGE_MS) {
266
295
  export {
267
296
  STATUS_LINE_PREFIX,
268
297
  statusLogPath,
298
+ resolveEventLogPath,
269
299
  startEventLog,
270
300
  monitorHeartbeatPath,
271
301
  nudgeSentinelPath,