kojee-mcp 0.6.0 → 0.7.1

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 (70) 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-5DHIUN73.js +25 -0
  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-EBYUJM3H.js +14 -0
  12. package/dist/chunk-EIAUW6KO.js +82 -0
  13. package/dist/chunk-EIH2LNF4.js +45 -0
  14. package/dist/{chunk-BYV7CWNY.js → chunk-FBJCPRVH.js} +22 -9
  15. package/dist/{chunk-XN3IC4B5.js → chunk-FSKGQ6GT.js} +30 -5
  16. package/dist/{chunk-GI2CKKBL.js → chunk-GKXHKR3B.js} +1 -1
  17. package/dist/{chunk-ALZX3SGI.js → chunk-GNLCUJBK.js} +13 -27
  18. package/dist/{chunk-JXMVZEQ7.js → chunk-I67C2HYA.js} +1 -1
  19. package/dist/chunk-KNEJTD6G.js +31 -0
  20. package/dist/{chunk-65KRRDHP.js → chunk-KPMD72FY.js} +94 -28
  21. package/dist/{chunk-NR4Y54OL.js → chunk-MIEI4PLB.js} +1 -19
  22. package/dist/chunk-NE2F5CKS.js +113 -0
  23. package/dist/chunk-QJFMU4QC.js +33 -0
  24. package/dist/{chunk-EW72ZNQL.js → chunk-RDLF4NQC.js} +1 -1
  25. package/dist/{chunk-Q7T3WAST.js → chunk-SSW5AQSR.js} +6 -1
  26. package/dist/{chunk-DURRQDYI.js → chunk-TNZITJAB.js} +45 -15
  27. package/dist/chunk-U5HHHRXA.js +103 -0
  28. package/dist/{setup-handler-7HOFAD7C.js → chunk-UFHGZUST.js} +431 -253
  29. package/dist/{chunk-N35X22G7.js → chunk-UPJV7GBE.js} +1 -1
  30. package/dist/chunk-WBXU27BF.js +35 -0
  31. package/dist/chunk-XJEBJIQE.js +52 -0
  32. package/dist/chunk-Z5LPNJQ6.js +20 -0
  33. package/dist/cli.js +63 -25
  34. package/dist/codex-stop-hook-BMOJVM6O.js +96 -0
  35. package/dist/connect-handler-4DRTFMOB.js +138 -0
  36. package/dist/{control-token-4BUCTYQB.js → control-token-KXSZ3GBY.js} +2 -2
  37. package/dist/{doctor-WVX3WCYX.js → doctor-5QJ3HGNR.js} +48 -22
  38. package/dist/{doctor-codex-Z5M4SFBG.js → doctor-codex-VGJKTX2E.js} +33 -7
  39. package/dist/doctor-openclaw-LL6NSPLC.js +86 -0
  40. package/dist/{event-log-HOBO3ECB.js → event-log-2NBJEIEP.js} +5 -3
  41. package/dist/{event-stream-SYBUJ5EH.js → event-stream-KRYWEYWO.js} +7 -4
  42. package/dist/index.js +13 -10
  43. package/dist/{install-LJY2CHKG.js → install-V7LSQCYZ.js} +37 -22
  44. package/dist/{keystore-XLEV3FL5.js → keystore-LQNFDHGO.js} +2 -2
  45. package/dist/lib.d.ts +4 -1
  46. package/dist/lib.js +11 -8
  47. package/dist/pair-HTCVSMR6.js +11 -0
  48. package/dist/{paired-config-JTFLHMZ2.js → paired-config-GUDDO2CH.js} +4 -2
  49. package/dist/{parent-watchdog-TLU355FB.js → parent-watchdog-LTUNF7PD.js} +24 -5
  50. package/dist/{registry-YLOFZDI2.js → registry-ZZZ26WGA.js} +197 -24
  51. package/dist/resolve-slot-BFIB46Y5.js +13 -0
  52. package/dist/{runtime-record-WO4IECM6.js → runtime-record-OXRLTOLC.js} +2 -2
  53. package/dist/{send-cli-IYJF32PE.js → send-cli-45RLGYIC.js} +22 -10
  54. package/dist/{server-R24AVNTI.js → server-ITPFQVTK.js} +5 -2
  55. package/dist/{session-discovery-FNMJGFPM.js → session-discovery-NHCZ2XWA.js} +2 -2
  56. package/dist/setup-handler-JFM45NCN.js +160 -0
  57. package/dist/{stop-hook-TCPJFGFF.js → stop-hook-5ABGTC2O.js} +17 -20
  58. package/dist/{tail-stream-4IMXI44S.js → tail-stream-NBGHHBS4.js} +40 -22
  59. package/dist/{user-prompt-submit-hook-FQ43NIHC.js → user-prompt-submit-hook-DTXXFDSD.js} +11 -14
  60. package/package.json +3 -2
  61. package/skills/using-tandems/SKILL.md +52 -0
  62. package/dist/ancestry-ONFBQEP5.js +0 -8
  63. package/dist/chunk-BLEGIR35.js +0 -43
  64. package/dist/chunk-CLKCNV2A.js +0 -100
  65. package/dist/chunk-KFDZWNQO.js +0 -73
  66. package/dist/chunk-OGHDTFAX.js +0 -50
  67. package/dist/chunk-VHKPWUX7.js +0 -41
  68. package/dist/codex-stop-hook-RWQSZTST.js +0 -16
  69. package/dist/doctor-openclaw-SS2TMQOX.js +0 -95
  70. package/dist/pair-P4ILCMT7.js +0 -10
@@ -1,8 +1,12 @@
1
+ import {
2
+ VERSION
3
+ } from "./chunk-5DHIUN73.js";
1
4
  import {
2
5
  buildCatchUpNote,
6
+ buildCondensedTandemRules,
3
7
  buildMonitorSpawn,
4
8
  buildReplyRecipe
5
- } from "./chunk-Q7T3WAST.js";
9
+ } from "./chunk-SSW5AQSR.js";
6
10
  import {
7
11
  translateToolCallResult
8
12
  } from "./chunk-PPTKGWFF.js";
@@ -14,31 +18,12 @@ import {
14
18
  ListToolsRequestSchema,
15
19
  CallToolRequestSchema
16
20
  } from "@modelcontextprotocol/sdk/types.js";
17
-
18
- // src/version.ts
19
- import fs from "fs";
20
- import path from "path";
21
- import { fileURLToPath } from "url";
22
- var FALLBACK_VERSION = "0.0.0-unknown";
23
- function resolveVersion() {
24
- try {
25
- const here = path.dirname(fileURLToPath(import.meta.url));
26
- const parsed = JSON.parse(
27
- fs.readFileSync(path.join(here, "..", "package.json"), "utf8")
28
- );
29
- return typeof parsed?.version === "string" && parsed.version ? parsed.version : FALLBACK_VERSION;
30
- } catch (err) {
31
- process.stderr.write(
32
- `kojee-mcp: could not resolve version from package.json, falling back to ${FALLBACK_VERSION}: ${String(err)}
33
- `
34
- );
35
- return FALLBACK_VERSION;
36
- }
21
+ function buildNonChannelInstructions(runtime) {
22
+ const skillPointer = runtime === "codex" ? " Before working in rooms, read the using-tandems skill at ~/.agents/skills/using-tandems/SKILL.md (if present)." : "";
23
+ 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.\n\n" + buildCondensedTandemRules() + skillPointer;
37
24
  }
38
- var VERSION = resolveVersion();
39
-
40
- // src/server.ts
41
25
  function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
26
+ 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
27
  const intro = `Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:
43
28
 
44
29
  (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 +34,8 @@ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
49
34
  `;
50
35
  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
36
  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;
37
+ const skillPointer = "\n\nTeamwork guide: read the using-tandems skill at ~/.claude/skills/using-tandems/SKILL.md (if present) before working in rooms.";
38
+ return localProxyRule + intro + monitorSection + listenSection + advice + skillPointer;
53
39
  }
54
40
  function tandemIdArg(args) {
55
41
  return typeof args["tandem_id"] === "string" ? args["tandem_id"] : null;
@@ -75,7 +61,7 @@ function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLog
75
61
  { name: "kojee-mcp", version: VERSION },
76
62
  {
77
63
  capabilities,
78
- ...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
64
+ ...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : { instructions: buildNonChannelInstructions(adapter.runtime) }
79
65
  }
80
66
  );
81
67
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -100,7 +86,7 @@ async function startMcpServer(server) {
100
86
  }
101
87
 
102
88
  export {
103
- VERSION,
89
+ buildNonChannelInstructions,
104
90
  buildChannelInstructions,
105
91
  executeToolCall,
106
92
  createMcpServer,
@@ -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
+ };
@@ -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) {
@@ -48,6 +84,16 @@ function escapeTomlString(s) {
48
84
  }
49
85
  var KOJEE_TABLE_HEADER = "[mcp_servers.kojee]";
50
86
  var KOJEE_ENV_TABLE_HEADER = "[mcp_servers.kojee.env]";
87
+ function isPlaceholderWebhookUrl(url) {
88
+ return /^https?:\/\/YOUR-[A-Z0-9-]*\.local(?=[/:?#]|$)/i.test(url.trim());
89
+ }
90
+ function scrubPlaceholderWebhookEnv(envKeys) {
91
+ const urlEntry = envKeys.find(([k]) => k === "KOJEE_WEBHOOK_URL");
92
+ if (!urlEntry) return envKeys;
93
+ const raw = urlEntry[1].trim().replace(/^"(.*)"$/s, "$1").replace(/^'(.*)'$/s, "$1");
94
+ if (!isPlaceholderWebhookUrl(raw)) return envKeys;
95
+ return envKeys.filter(([k]) => k !== "KOJEE_WEBHOOK_URL" && k !== "KOJEE_WEBHOOK_SECRET");
96
+ }
51
97
  function writeCodexConfig(inputs) {
52
98
  const configPath = inputs.configPath ?? defaultCodexConfigPath();
53
99
  const hooksPath = inputs.hooksPath ?? defaultCodexHooksPath();
@@ -62,18 +108,30 @@ function writeCodexConfig(inputs) {
62
108
  inputs.webhookSecret,
63
109
  inputs.signatureEnv ?? [],
64
110
  inputs.token,
65
- inputs.url
111
+ inputs.url,
112
+ inputs.pairedConfigPath
66
113
  );
67
114
  writeFile600(configPath, toml);
115
+ const command = codexStopHookCommand();
68
116
  const hooks = readJson(hooksPath);
69
117
  hooks.hooks ??= {};
70
118
  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) {
119
+ let present = false;
120
+ for (const e of hooks.hooks.Stop) {
121
+ for (const h of e.hooks ?? []) {
122
+ if (h.command === command) {
123
+ present = true;
124
+ } else if (isKojeeHookCommand(h.command)) {
125
+ h.command = command;
126
+ present = true;
127
+ }
128
+ if (present) break;
129
+ }
130
+ if (present) break;
131
+ }
132
+ if (!present) {
75
133
  hooks.hooks.Stop.push({
76
- hooks: [{ type: "command", command: CODEX_STOP_HOOK_COMMAND }]
134
+ hooks: [{ type: "command", command }]
77
135
  });
78
136
  }
79
137
  writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
@@ -97,7 +155,8 @@ function removeCodexConfig(opts = {}) {
97
155
  if (stop && stop.length > 0) {
98
156
  const before = stop.length;
99
157
  hooks.hooks.Stop = stop.filter(
100
- (e) => !e.hooks?.some((h) => h.command.startsWith("npx -y kojee-mcp hook"))
158
+ // F10: strip kojee entries in EITHER persisted form (npx or pinned).
159
+ (e) => !e.hooks?.some((h) => isKojeeHookCommand(h.command))
101
160
  );
102
161
  if (hooks.hooks.Stop.length !== before) {
103
162
  result.stopHook = true;
@@ -108,27 +167,33 @@ function removeCodexConfig(opts = {}) {
108
167
  }
109
168
  return result;
110
169
  }
111
- function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret, signatureEnv = [], token, url) {
170
+ function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret, signatureEnv = [], token, url, pairedConfigPath) {
112
171
  const parsed = extractKojeeBlock(existing);
113
172
  if (!parsed) {
114
173
  const block = buildCodexMcpServerTable({
115
- webhookUrl,
116
- webhookSecret,
174
+ ...webhookUrl !== void 0 ? { webhookUrl } : {},
175
+ ...webhookSecret !== void 0 ? { webhookSecret } : {},
117
176
  ...signatureEnv.length > 0 ? { signatureEnv } : {},
118
177
  ...token ? { token } : {},
119
- ...url ? { url } : {}
178
+ ...url ? { url } : {},
179
+ ...pairedConfigPath ? { pairedConfigPath } : {}
120
180
  });
121
181
  const base2 = existing.replace(/\s*$/, "");
122
182
  return base2.length === 0 ? block + "\n" : base2 + "\n\n" + block + "\n";
123
183
  }
184
+ const { commandLiteral, argsLiteral } = codexLauncherLiterals(token, url, pairedConfigPath);
124
185
  const tableKeys = upsertKeyLines(parsed.tableKeys, [
125
- ["command", '"npx"'],
126
- ["args", codexArgsLiteral(token, url)]
186
+ ["command", commandLiteral],
187
+ ["args", argsLiteral]
127
188
  ]);
128
- const envKeys = upsertKeyLines(parsed.envKeys, [
189
+ const envKeys = upsertKeyLines(scrubPlaceholderWebhookEnv(parsed.envKeys), [
129
190
  ["KOJEE_RUNTIME", '"codex"'],
130
- ["KOJEE_WEBHOOK_URL", `"${escapeTomlString(webhookUrl)}"`],
131
- ["KOJEE_WEBHOOK_SECRET", `"${escapeTomlString(webhookSecret)}"`],
191
+ // Own KOJEE_WEBHOOK_URL/SECRET ONLY when supplied this run. The credential-
192
+ // only `connect` path passes neither → they drop out of the owned set and
193
+ // any existing operator/receiver values are PRESERVED verbatim (no clobber,
194
+ // no rotated secret, byte-stable re-runs).
195
+ ...webhookUrl !== void 0 ? [["KOJEE_WEBHOOK_URL", `"${escapeTomlString(webhookUrl)}"`]] : [],
196
+ ...webhookSecret !== void 0 ? [["KOJEE_WEBHOOK_SECRET", `"${escapeTomlString(webhookSecret)}"`]] : [],
132
197
  // Signature emission overrides (0.5.3) — owned only when configured this
133
198
  // run; an operator's existing signature lines are otherwise preserved.
134
199
  ...signatureEnv.map(([k, v]) => [k, `"${escapeTomlString(v)}"`])
@@ -244,6 +309,7 @@ export {
244
309
  defaultCodexHooksPath,
245
310
  buildCodexMcpServerTable,
246
311
  buildCodexStopHookBlock,
312
+ isPlaceholderWebhookUrl,
247
313
  writeCodexConfig,
248
314
  removeCodexConfig
249
315
  };
@@ -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
+ };
@@ -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";
@@ -15,6 +15,9 @@ function buildReplyRecipe(event) {
15
15
  function buildCatchUpNote() {
16
16
  return "Cursors are allocated per-Tandem, and the event-log interleaves every subscribed room into one stream \u2014 so track the last `cursor=<n>` you saw keyed by the line's `tandem=<id>` field, and only treat a jump WITHIN the same tandem_id as a gap (a jump between lines from different rooms is normal interleaving, not a missed message). If the cursor jumps within one tandem, or the log resets (the proxy caps and truncates the messages log in place on overflow \u2014 a `status=rotated` line is recorded in the status sibling), you may have missed messages: catch up with tandem_messages(tandem_id, since=<last cursor you saw for THAT tandem>).";
17
17
  }
18
+ function buildCondensedTandemRules() {
19
+ return "Tandem ground rules: (1) catch up before you speak \u2014 fetch messages since your last cursor (cursors are per-Tandem); never answer from a stale view. (2) close every loop \u2014 taking / done (with evidence) / blocked, said out loud; silence is a bug on a team. (3) your seat_name IS your room identity \u2014 keep it stable across sessions and reconnects. (4) plan your wake path per task and set your wake filter deliberately (heads-down \u2192 mentions; waiting on one reply \u2192 listen; co-working \u2192 all), and tell the room your posture.";
20
+ }
18
21
  function buildMonitorNudge(logPath) {
19
22
  return `[kojee] Tandem events are being logged but no Monitor is reading them \u2014 you may be missing wake notifications. Spawn the watcher once: ${buildMonitorSpawn(logPath)}. Then ${buildReplyRecipe()}.`;
20
23
  }
@@ -27,13 +30,15 @@ function buildWebhookReceiverNote(sig) {
27
30
  }
28
31
  var CODEX_LISTEN_CAP_MS = 8e3;
29
32
  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.`;
33
+ const drainSince = Math.max(0, cursor - 1);
34
+ 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
35
  }
32
36
 
33
37
  export {
34
38
  buildMonitorSpawn,
35
39
  buildReplyRecipe,
36
40
  buildCatchUpNote,
41
+ buildCondensedTandemRules,
37
42
  buildMonitorNudge,
38
43
  buildWebhookReceiverNote,
39
44
  CODEX_LISTEN_CAP_MS,