kojee-mcp 0.5.10 → 0.5.11

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.
@@ -16,14 +16,21 @@ import {
16
16
  import {
17
17
  startEventStream
18
18
  } from "./chunk-MKDMAAMN.js";
19
+ import {
20
+ secureDir,
21
+ secureFile
22
+ } from "./chunk-BLEGIR35.js";
23
+ import {
24
+ parseTandemsConfig
25
+ } from "./chunk-YKW54DKF.js";
19
26
  import {
20
27
  translateToolCallResult
21
28
  } from "./chunk-LDZXU3DW.js";
22
29
 
23
30
  // src/index.ts
24
- import fs2 from "fs";
25
- import os from "os";
26
- import path2 from "path";
31
+ import fs4 from "fs";
32
+ import os2 from "os";
33
+ import path3 from "path";
27
34
 
28
35
  // src/tool-registry.ts
29
36
  var ToolRegistry = class {
@@ -136,14 +143,25 @@ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
136
143
  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.";
137
144
  return intro + monitorSection + listenSection + advice;
138
145
  }
146
+ function tandemIdArg(args) {
147
+ return typeof args["tandem_id"] === "string" ? args["tandem_id"] : null;
148
+ }
139
149
  async function executeToolCall(registry, name, args, hooks) {
140
150
  const rawResult = await registry.callTool(name, args);
141
151
  const result = translateToolCallResult(rawResult);
142
- if (name === "tandem_join" && !result.isError) {
143
- try {
144
- hooks?.onTandemJoin?.();
145
- } catch (err) {
146
- console.error("[mcp] onTandemJoin hook failed:", err?.message ?? String(err));
152
+ if (!result.isError) {
153
+ if (name === "tandem_join") {
154
+ try {
155
+ hooks?.onTandemJoin?.(tandemIdArg(args));
156
+ } catch (err) {
157
+ console.error("[mcp] onTandemJoin hook failed:", err?.message ?? String(err));
158
+ }
159
+ } else if (name === "tandem_leave") {
160
+ try {
161
+ hooks?.onTandemLeave?.(tandemIdArg(args));
162
+ } catch (err) {
163
+ console.error("[mcp] onTandemLeave hook failed:", err?.message ?? String(err));
164
+ }
147
165
  }
148
166
  }
149
167
  return result;
@@ -273,8 +291,116 @@ var unknownAdapter = {
273
291
  }
274
292
  };
275
293
 
294
+ // src/runtime/cc-session-id.ts
295
+ import fs2 from "fs";
296
+ import { execFileSync } from "child_process";
297
+ import { createHash } from "crypto";
298
+ var CC_SESSION_ENV = "CLAUDE_CODE_SESSION_ID";
299
+ function extractCcSessionId(raw) {
300
+ if (raw.includes("\0")) {
301
+ for (const entry of raw.split("\0")) {
302
+ if (entry.startsWith(`${CC_SESSION_ENV}=`)) {
303
+ const v = entry.slice(CC_SESSION_ENV.length + 1);
304
+ return v.length > 0 ? v : null;
305
+ }
306
+ }
307
+ return null;
308
+ }
309
+ const re = new RegExp(`(?:^|\\s)${CC_SESSION_ENV}=([^\\s]+)`, "g");
310
+ let last = null;
311
+ let m;
312
+ while ((m = re.exec(raw)) !== null) last = m[1];
313
+ return last;
314
+ }
315
+ function defaultReadProcessEnvRaw(pid, platform) {
316
+ try {
317
+ if (platform === "linux") {
318
+ return fs2.readFileSync(`/proc/${pid}/environ`, "utf8");
319
+ }
320
+ if (platform === "darwin") {
321
+ return execFileSync("ps", ["eww", "-p", String(pid), "-o", "command="], {
322
+ encoding: "utf8",
323
+ timeout: 2e3
324
+ });
325
+ }
326
+ } catch {
327
+ return null;
328
+ }
329
+ return null;
330
+ }
331
+ function sanitizeKey(value) {
332
+ return value.replace(/[^A-Za-z0-9_-]/g, "");
333
+ }
334
+ function resolveInstanceKey(deps = {}) {
335
+ const env = deps.env ?? process.env;
336
+ const platform = deps.platform ?? process.platform;
337
+ const ccPid = deps.ccPid ?? null;
338
+ if (ccPid !== null) {
339
+ const reader = deps.readProcessEnvRaw ?? defaultReadProcessEnvRaw;
340
+ const raw = reader(ccPid, platform);
341
+ if (raw) {
342
+ const sid = sanitizeKey(extractCcSessionId(raw) ?? "");
343
+ if (sid) return sid;
344
+ }
345
+ }
346
+ const explicit = sanitizeKey((env.KOJEE_INSTANCE ?? "").trim());
347
+ if (explicit) return `inst-${explicit}`;
348
+ const base = deps.projectDir ?? env.CLAUDE_PROJECT_DIR ?? deps.cwd ?? process.cwd() ?? "";
349
+ const hash = createHash("sha256").update(base).digest("hex").slice(0, 12);
350
+ return `wd-${hash}`;
351
+ }
352
+
353
+ // src/tandem/room-memory.ts
354
+ import fs3 from "fs";
355
+ import os from "os";
356
+ import path2 from "path";
357
+ function defaultKojeeDir() {
358
+ return path2.join(os.homedir(), ".kojee");
359
+ }
360
+ function seatedRoomsPath(key, dir = defaultKojeeDir()) {
361
+ return path2.join(dir, `seated-rooms-cc-${key}.json`);
362
+ }
363
+ function readSeatedRooms(key, dir = defaultKojeeDir()) {
364
+ let raw;
365
+ try {
366
+ raw = fs3.readFileSync(seatedRoomsPath(key, dir), "utf8");
367
+ } catch {
368
+ return [];
369
+ }
370
+ try {
371
+ const parsed = JSON.parse(raw);
372
+ return Array.isArray(parsed.rooms) ? parsed.rooms.filter((r) => typeof r === "string") : [];
373
+ } catch {
374
+ return [];
375
+ }
376
+ }
377
+ function hasSeatedRoomsFile(key, dir = defaultKojeeDir()) {
378
+ return fs3.existsSync(seatedRoomsPath(key, dir));
379
+ }
380
+ function seedSeatedRooms(key, rooms, dir = defaultKojeeDir()) {
381
+ writeSeatedRooms(key, [...new Set(rooms)], dir);
382
+ }
383
+ function writeSeatedRooms(key, rooms, dir) {
384
+ fs3.mkdirSync(dir, { recursive: true, mode: 448 });
385
+ secureDir(dir);
386
+ const filePath = seatedRoomsPath(key, dir);
387
+ const body = { schema: 1, rooms, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
388
+ fs3.writeFileSync(filePath, JSON.stringify(body, null, 2), { mode: 384 });
389
+ secureFile(filePath);
390
+ }
391
+ function addSeatedRoom(key, tandemId, dir = defaultKojeeDir()) {
392
+ const rooms = readSeatedRooms(key, dir);
393
+ if (rooms.includes(tandemId)) return;
394
+ writeSeatedRooms(key, [...rooms, tandemId], dir);
395
+ }
396
+ function removeSeatedRoom(key, tandemId, dir = defaultKojeeDir()) {
397
+ const rooms = readSeatedRooms(key, dir);
398
+ if (!rooms.includes(tandemId)) return;
399
+ writeSeatedRooms(key, rooms.filter((r) => r !== tandemId), dir);
400
+ }
401
+
276
402
  // src/index.ts
277
- var DEFAULT_KEYSTORE_PATH = path2.join(os.homedir(), ".kojee", "keypair.json");
403
+ var DEFAULT_KEYSTORE_PATH = path3.join(os2.homedir(), ".kojee", "keypair.json");
278
404
  function isDPoPEnrollmentError(err) {
279
405
  const msg = String(err?.message ?? err ?? "").toLowerCase();
280
406
  if (msg.includes("invalid or expired") && msg.includes("token")) return false;
@@ -317,6 +443,19 @@ async function startProxy(config) {
317
443
  console.error(
318
444
  `[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
319
445
  );
446
+ const ccPid = await findClaudeAncestorPid();
447
+ const instanceKey = resolveInstanceKey({ ccPid });
448
+ const roomMemory = {
449
+ hasMemory: () => hasSeatedRoomsFile(instanceKey),
450
+ read: () => readSeatedRooms(instanceKey),
451
+ seed: (rooms) => seedSeatedRooms(instanceKey, rooms)
452
+ };
453
+ const recordRooms = parseTandemsConfig(process.env["KOJEE_TANDEMS"]).mode === "auto-local";
454
+ if (recordRooms && instanceKey.startsWith("wd-")) {
455
+ console.error(
456
+ "[kojee-mcp] #28: no Claude Code session id available \u2014 room-memory keyed on project-dir hash; concurrent windows in the same dir will share it. Set KOJEE_INSTANCE=<unique-per-window> to keep them session-faithful."
457
+ );
458
+ }
320
459
  let activeStreamHandle = null;
321
460
  const { createJoinReconnectScheduler } = await import("./reconnect-scheduler-ARV6JIWK.js");
322
461
  const joinReconnect = createJoinReconnectScheduler({
@@ -331,7 +470,13 @@ async function startProxy(config) {
331
470
  return true;
332
471
  }
333
472
  });
334
- const onTandemJoin = () => joinReconnect.requestReconnect();
473
+ const onTandemJoin = (tandemId) => {
474
+ joinReconnect.requestReconnect();
475
+ if (recordRooms && tandemId) addSeatedRoom(instanceKey, tandemId);
476
+ };
477
+ const onTandemLeave = (tandemId) => {
478
+ if (recordRooms && tandemId) removeSeatedRoom(instanceKey, tandemId);
479
+ };
335
480
  const teardownSteps = [];
336
481
  let shuttingDown = false;
337
482
  function shutdown(reason) {
@@ -353,12 +498,12 @@ async function startProxy(config) {
353
498
  console.error(`[kojee-mcp] shutting down (${reason}), exiting`);
354
499
  process.exit(0);
355
500
  }
356
- const ccPid = await findClaudeAncestorPid();
357
- const { ensureJoinTandems } = await import("./ensure-join-7AEDJMPE.js");
501
+ const { ensureJoinTandems } = await import("./ensure-join-5Y5IJ7HN.js");
358
502
  await ensureJoinTandems({
359
503
  gateway,
360
504
  env: process.env["KOJEE_TANDEMS"],
361
505
  listTandems: () => listTandemIds(gateway),
506
+ roomMemory,
362
507
  onJoined: () => joinReconnect.requestReconnect()
363
508
  });
364
509
  let tandemMembershipCount = -1;
@@ -411,7 +556,8 @@ async function startProxy(config) {
411
556
  });
412
557
  }
413
558
  server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path, {
414
- onTandemJoin
559
+ onTandemJoin,
560
+ onTandemLeave
415
561
  });
416
562
  const { issueControlToken, controlTokenPath } = await import("./control-token-4BUCTYQB.js");
417
563
  let controlToken = null;
@@ -546,7 +692,8 @@ async function startProxy(config) {
546
692
  });
547
693
  }
548
694
  server = createMcpServer(registry, adapter, tandemMembershipCount, void 0, {
549
- onTandemJoin
695
+ onTandemJoin,
696
+ onTandemLeave
550
697
  });
551
698
  process.on("exit", () => eventLog.cleanup());
552
699
  teardownSteps.push(() => {
@@ -622,7 +769,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
622
769
  "[kojee-mcp] Auth failed, attempting recovery with fresh enrollment..."
623
770
  );
624
771
  try {
625
- if (fs2.existsSync(keystorePath)) fs2.unlinkSync(keystorePath);
772
+ if (fs4.existsSync(keystorePath)) fs4.unlinkSync(keystorePath);
626
773
  } catch (unlinkErr) {
627
774
  console.error("[kojee-mcp] Could not remove stale keystore:", unlinkErr);
628
775
  }
@@ -2,9 +2,6 @@
2
2
  function sanitizeChannelAttr(value) {
3
3
  return String(value ?? "").replace(/["<>\u0000-\u001f\u007f]/g, "");
4
4
  }
5
- function defangChannelBody(content) {
6
- return String(content ?? "").replace(/<(\/?)channel/gi, "&lt;$1channel");
7
- }
8
5
  function formatChannelEvents(events) {
9
6
  const header = `[${events.length} unread Tandem ${events.length === 1 ? "event" : "events"}]
10
7
 
@@ -12,7 +9,7 @@ function formatChannelEvents(events) {
12
9
  const bodies = events.map((evt) => {
13
10
  const attrs = Object.entries(evt.meta).map(([k, v]) => `${k}="${sanitizeChannelAttr(v)}"`).join(" ");
14
11
  return `<channel source="kojee-mcp" ${attrs}>
15
- ${defangChannelBody(evt.content)}
12
+ ${evt.content}
16
13
  </channel>`;
17
14
  });
18
15
  return header + bodies.join("\n\n");
@@ -3,8 +3,9 @@ var OBJECT_ID_RE = /^[0-9a-f]{24}$/i;
3
3
  var DEFAULT_PER_CALL_TIMEOUT_MS = 1e4;
4
4
  function parseTandemsConfig(raw) {
5
5
  const trimmed = (raw ?? "").trim();
6
- if (trimmed.length === 0) return { mode: "auto", ids: [], invalid: [] };
6
+ if (trimmed.length === 0) return { mode: "auto-local", ids: [], invalid: [] };
7
7
  if (trimmed.toLowerCase() === "none") return { mode: "disabled", ids: [], invalid: [] };
8
+ if (trimmed.toLowerCase() === "auto-agent") return { mode: "auto-agent", ids: [], invalid: [] };
8
9
  const ids = [];
9
10
  const invalid = [];
10
11
  for (const entry of trimmed.split(/[\s,]+/)) {
@@ -25,26 +26,54 @@ async function ensureJoinTandems(opts) {
25
26
  for (const bad of config.invalid) {
26
27
  log(`[ensure-join] skipping invalid tandem id "${bad}" (not a 24-hex ObjectId)`);
27
28
  }
28
- let ids;
29
- if (config.mode === "explicit") {
30
- ids = config.ids;
31
- log(`[ensure-join] mode=explicit n=${ids.length} (KOJEE_TANDEMS)`);
32
- } else {
33
- let listed = null;
29
+ const listAgentTandems = async () => {
34
30
  try {
35
- listed = opts.listTandems ? await opts.listTandems() : null;
31
+ return opts.listTandems ? await opts.listTandems() : null;
36
32
  } catch (err) {
37
33
  log(`[ensure-join] tandem_list threw: ${err.message}`);
38
- listed = null;
34
+ return null;
39
35
  }
36
+ };
37
+ const warnListFailed = () => {
38
+ log(
39
+ "[ensure-join] tandem_list failed \u2014 cannot re-seat this session (set KOJEE_TANDEMS=<ids> to pin, or KOJEE_TANDEMS=none to silence)"
40
+ );
41
+ };
42
+ let ids;
43
+ if (config.mode === "explicit") {
44
+ ids = config.ids;
45
+ log(`[ensure-join] mode=explicit n=${ids.length} (KOJEE_TANDEMS)`);
46
+ } else if (config.mode === "auto-agent") {
47
+ const listed = await listAgentTandems();
40
48
  if (listed === null) {
41
- log(
42
- "[ensure-join] tandem_list failed \u2014 cannot auto re-seat this session (set KOJEE_TANDEMS=<ids> to pin, or KOJEE_TANDEMS=none to silence)"
43
- );
49
+ warnListFailed();
44
50
  return result;
45
51
  }
46
52
  ids = listed;
47
- log(`[ensure-join] mode=auto n=${ids.length} (KOJEE_TANDEMS unset \u2014 re-seating where this agent already holds a seat)`);
53
+ log(`[ensure-join] mode=auto-agent n=${ids.length} (legacy \u2014 re-seating every room this agent holds a seat)`);
54
+ } else {
55
+ const mem = opts.roomMemory;
56
+ if (mem && mem.hasMemory()) {
57
+ ids = mem.read().filter((id) => OBJECT_ID_RE.test(id));
58
+ log(`[ensure-join] mode=auto-local n=${ids.length} (rejoining this session's own rooms from local memory)`);
59
+ } else {
60
+ const listed = await listAgentTandems();
61
+ if (listed === null) {
62
+ warnListFailed();
63
+ return result;
64
+ }
65
+ ids = listed;
66
+ if (mem) {
67
+ try {
68
+ mem.seed(ids);
69
+ log(`[ensure-join] mode=auto-local n=${ids.length} (first run \u2014 seeded local memory from membership)`);
70
+ } catch (err) {
71
+ log(`[ensure-join] mode=auto-local n=${ids.length} (first run \u2014 seed write FAILED: ${err.message}; joining without persisting)`);
72
+ }
73
+ } else {
74
+ log(`[ensure-join] mode=auto-local n=${ids.length} (no room-memory port \u2014 falling back to agent-scoped list)`);
75
+ }
76
+ }
48
77
  }
49
78
  for (const id of ids) {
50
79
  const outcome = await joinOne(opts.gateway, id, perCallTimeoutMs);
@@ -90,7 +119,8 @@ async function joinOne(gateway, tandemId, perCallTimeoutMs) {
90
119
  clearTimeout(timer);
91
120
  }
92
121
  }
122
+
93
123
  export {
94
- ensureJoinTandems,
95
- parseTandemsConfig
124
+ parseTandemsConfig,
125
+ ensureJoinTandems
96
126
  };
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  VERSION,
7
7
  startProxy
8
- } from "./chunk-SCDWPGH3.js";
8
+ } from "./chunk-D42PZX2I.js";
9
9
  import "./chunk-X672ZN7V.js";
10
10
  import "./chunk-BJMASMKX.js";
11
11
  import {
@@ -20,6 +20,7 @@ import {
20
20
  deriveKeystorePath
21
21
  } from "./chunk-CH32ELFX.js";
22
22
  import "./chunk-BLEGIR35.js";
23
+ import "./chunk-YKW54DKF.js";
23
24
  import "./chunk-LDZXU3DW.js";
24
25
 
25
26
  // src/cli.ts
@@ -42,11 +43,11 @@ program.command("pair <code>").description("Pair this machine against Kojee usin
42
43
  });
43
44
  program.command("hook").description("Run a kojee MCP hook script (called by Claude Code via ~/.claude/settings.json)").requiredOption("--type <type>", "Hook type: stop, user-prompt-submit, or codex-stop").action(async (opts) => {
44
45
  if (opts.type === "stop") {
45
- const { runStopHook } = await import("./stop-hook-OTCJGL6V.js");
46
+ const { runStopHook } = await import("./stop-hook-GEJF47SN.js");
46
47
  await runStopHook();
47
48
  process.exit(0);
48
49
  } else if (opts.type === "user-prompt-submit") {
49
- const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-QXMC7EZU.js");
50
+ const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-DGRRFHOB.js");
50
51
  await runUserPromptSubmitHook();
51
52
  process.exit(0);
52
53
  } else if (opts.type === "codex-stop") {
@@ -0,0 +1,8 @@
1
+ import {
2
+ ensureJoinTandems,
3
+ parseTandemsConfig
4
+ } from "./chunk-YKW54DKF.js";
5
+ export {
6
+ ensureJoinTandems,
7
+ parseTandemsConfig
8
+ };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  listTandemIds,
3
3
  startProxy
4
- } from "./chunk-SCDWPGH3.js";
4
+ } from "./chunk-D42PZX2I.js";
5
5
  import "./chunk-X672ZN7V.js";
6
6
  import "./chunk-BJMASMKX.js";
7
7
  import "./chunk-JXMVZEQ7.js";
@@ -10,6 +10,7 @@ import "./chunk-MKDMAAMN.js";
10
10
  import "./chunk-2MIISF2W.js";
11
11
  import "./chunk-CH32ELFX.js";
12
12
  import "./chunk-BLEGIR35.js";
13
+ import "./chunk-YKW54DKF.js";
13
14
  import "./chunk-LDZXU3DW.js";
14
15
  export {
15
16
  listTandemIds,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  formatChannelEvents
3
- } from "./chunk-YKS6YZKM.js";
3
+ } from "./chunk-PHXO5P25.js";
4
4
  import {
5
5
  readHookStdin
6
6
  } from "./chunk-LSUB6QMP.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  formatChannelEvents
3
- } from "./chunk-YKS6YZKM.js";
3
+ } from "./chunk-PHXO5P25.js";
4
4
  import {
5
5
  readHookStdin
6
6
  } from "./chunk-LSUB6QMP.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.5.10",
3
+ "version": "0.5.11",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {