kojee-mcp 0.7.1 → 0.7.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 (32) hide show
  1. package/dist/{chunk-KPMD72FY.js → chunk-6XWTUDWW.js} +65 -26
  2. package/dist/{chunk-GNLCUJBK.js → chunk-A4IOKD4Z.js} +100 -2
  3. package/dist/{chunk-FBJCPRVH.js → chunk-ABECLEYE.js} +62 -11
  4. package/dist/{chunk-SSW5AQSR.js → chunk-FJUAMJHU.js} +3 -6
  5. package/dist/{chunk-FSKGQ6GT.js → chunk-HI42GBQ3.js} +5 -0
  6. package/dist/chunk-IZN7IZPW.js +132 -0
  7. package/dist/{chunk-UPJV7GBE.js → chunk-TCWIXG5C.js} +1 -1
  8. package/dist/{chunk-QJFMU4QC.js → chunk-TMCNB4JH.js} +1 -1
  9. package/dist/chunk-XLRF5ATG.js +103 -0
  10. package/dist/{chunk-UFHGZUST.js → chunk-XPIW4N55.js} +22 -14
  11. package/dist/cli.js +23 -14
  12. package/dist/codex-prompt-submit-hook-J5PZEJSK.js +39 -0
  13. package/dist/codex-stop-hook-32W4BIOM.js +136 -0
  14. package/dist/{connect-handler-4DRTFMOB.js → connect-handler-NZINEMG3.js} +14 -7
  15. package/dist/{doctor-5QJ3HGNR.js → doctor-WUU5BVPT.js} +2 -2
  16. package/dist/doctor-codex-PJFWIABF.js +370 -0
  17. package/dist/{event-stream-KRYWEYWO.js → event-stream-WPN3EN7C.js} +5 -1
  18. package/dist/index.js +5 -5
  19. package/dist/{install-V7LSQCYZ.js → install-JQNDGAAQ.js} +14 -2
  20. package/dist/lib.d.ts +40 -15
  21. package/dist/lib.js +7 -7
  22. package/dist/pending-state-6TVRR63P.js +134 -0
  23. package/dist/{registry-ZZZ26WGA.js → registry-2QS42EQF.js} +114 -51
  24. package/dist/{server-ITPFQVTK.js → server-DU3LFS32.js} +4 -2
  25. package/dist/{setup-handler-JFM45NCN.js → setup-handler-44ASXYMS.js} +7 -7
  26. package/dist/{stop-hook-5ABGTC2O.js → stop-hook-N6TX4YQT.js} +2 -2
  27. package/dist/{tail-stream-NBGHHBS4.js → tail-stream-N43D53RC.js} +99 -19
  28. package/package.json +1 -1
  29. package/skills/using-tandems/SKILL.md +2 -2
  30. package/dist/chunk-EBYUJM3H.js +0 -14
  31. package/dist/codex-stop-hook-BMOJVM6O.js +0 -96
  32. package/dist/doctor-codex-VGJKTX2E.js +0 -163
@@ -2,7 +2,7 @@ import {
2
2
  buildHookCommand,
3
3
  isKojeeHookCommand,
4
4
  npxHookCommand
5
- } from "./chunk-QJFMU4QC.js";
5
+ } from "./chunk-TMCNB4JH.js";
6
6
  import {
7
7
  wrapNpxLauncher
8
8
  } from "./chunk-D6JKFJ6A.js";
@@ -12,6 +12,9 @@ import {
12
12
  import {
13
13
  secureFile
14
14
  } from "./chunk-U5HHHRXA.js";
15
+ import {
16
+ VERSION
17
+ } from "./chunk-5DHIUN73.js";
15
18
 
16
19
  // src/wizard/codex-config.ts
17
20
  import fs from "fs";
@@ -33,14 +36,22 @@ var CODEX_STOP_HOOK_COMMAND = npxHookCommand("codex-stop");
33
36
  function codexStopHookCommand() {
34
37
  return buildHookCommand("codex-stop");
35
38
  }
39
+ function codexPromptSubmitHookCommand() {
40
+ return buildHookCommand("codex-prompt-submit");
41
+ }
42
+ var CODEX_HOOK_TIMEOUT_SEC = 10;
43
+ function codexNpxPackageSpec() {
44
+ return /^\d/.test(VERSION) ? `kojee-mcp@${VERSION}` : "kojee-mcp";
45
+ }
36
46
  function codexRestArgs(token, url, pairedConfigPath) {
47
+ const spec = codexNpxPackageSpec();
37
48
  if (token && url) {
38
- return ["-y", "kojee-mcp", "--token", token, "--url", url];
49
+ return ["-y", spec, "--token", token, "--url", url];
39
50
  }
40
51
  if (pairedConfigPath) {
41
- return ["-y", "kojee-mcp", "--paired-config", pairedConfigPath];
52
+ return ["-y", spec, "--paired-config", pairedConfigPath];
42
53
  }
43
- return ["-y", "kojee-mcp"];
54
+ return ["-y", spec];
44
55
  }
45
56
  function codexLauncherLiterals(token, url, pairedConfigPath) {
46
57
  const rest = codexRestArgs(token, url, pairedConfigPath);
@@ -76,7 +87,17 @@ function buildCodexStopHookBlock() {
76
87
  "[[hooks.Stop]]",
77
88
  "[[hooks.Stop.hooks]]",
78
89
  'type = "command"',
79
- `command = "${escapeTomlString(codexStopHookCommand())}"`
90
+ `command = "${escapeTomlString(codexStopHookCommand())}"`,
91
+ `timeout = ${CODEX_HOOK_TIMEOUT_SEC}`
92
+ ].join("\n");
93
+ }
94
+ function buildCodexPromptSubmitHookBlock() {
95
+ return [
96
+ "[[hooks.UserPromptSubmit]]",
97
+ "[[hooks.UserPromptSubmit.hooks]]",
98
+ 'type = "command"',
99
+ `command = "${escapeTomlString(codexPromptSubmitHookCommand())}"`,
100
+ `timeout = ${CODEX_HOOK_TIMEOUT_SEC}`
80
101
  ].join("\n");
81
102
  }
82
103
  function escapeTomlString(s) {
@@ -97,13 +118,13 @@ function scrubPlaceholderWebhookEnv(envKeys) {
97
118
  function writeCodexConfig(inputs) {
98
119
  const configPath = inputs.configPath ?? defaultCodexConfigPath();
99
120
  const hooksPath = inputs.hooksPath ?? defaultCodexHooksPath();
100
- let toml = "";
121
+ let existingToml = "";
101
122
  try {
102
- toml = fs.readFileSync(configPath, "utf8");
123
+ existingToml = fs.readFileSync(configPath, "utf8");
103
124
  } catch {
104
125
  }
105
- toml = upsertKojeeTomlTables(
106
- toml,
126
+ const newToml = upsertKojeeTomlTables(
127
+ existingToml,
107
128
  inputs.webhookUrl,
108
129
  inputs.webhookSecret,
109
130
  inputs.signatureEnv ?? [],
@@ -111,18 +132,31 @@ function writeCodexConfig(inputs) {
111
132
  inputs.url,
112
133
  inputs.pairedConfigPath
113
134
  );
114
- writeFile600(configPath, toml);
115
- const command = codexStopHookCommand();
135
+ const tomlChanged = newToml !== existingToml;
136
+ if (tomlChanged) writeFile600(configPath, newToml);
116
137
  const hooks = readJson(hooksPath);
117
138
  hooks.hooks ??= {};
118
- hooks.hooks.Stop ??= [];
139
+ upsertKojeeHookEntry(hooks, "Stop", codexStopHookCommand());
140
+ upsertKojeeHookEntry(hooks, "UserPromptSubmit", codexPromptSubmitHookCommand());
141
+ const newHooks = JSON.stringify(hooks, null, 2);
142
+ let existingHooksRaw = "";
143
+ try {
144
+ existingHooksRaw = fs.readFileSync(hooksPath, "utf8");
145
+ } catch {
146
+ }
147
+ const hooksChanged = newHooks !== existingHooksRaw;
148
+ if (hooksChanged) writeFile600(hooksPath, newHooks);
149
+ return { tomlChanged, hooksChanged };
150
+ }
151
+ function upsertKojeeHookEntry(hooks, event, command) {
152
+ hooks.hooks ??= {};
153
+ hooks.hooks[event] ??= [];
119
154
  let present = false;
120
- for (const e of hooks.hooks.Stop) {
155
+ for (const e of hooks.hooks[event]) {
121
156
  for (const h of e.hooks ?? []) {
122
- if (h.command === command) {
123
- present = true;
124
- } else if (isKojeeHookCommand(h.command)) {
157
+ if (h.command === command || isKojeeHookCommand(h.command)) {
125
158
  h.command = command;
159
+ h.timeout = CODEX_HOOK_TIMEOUT_SEC;
126
160
  present = true;
127
161
  }
128
162
  if (present) break;
@@ -130,11 +164,10 @@ function writeCodexConfig(inputs) {
130
164
  if (present) break;
131
165
  }
132
166
  if (!present) {
133
- hooks.hooks.Stop.push({
134
- hooks: [{ type: "command", command }]
167
+ hooks.hooks[event].push({
168
+ hooks: [{ type: "command", command, timeout: CODEX_HOOK_TIMEOUT_SEC }]
135
169
  });
136
170
  }
137
- writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
138
171
  }
139
172
  function removeCodexConfig(opts = {}) {
140
173
  const configPath = opts.configPath ?? defaultCodexConfigPath();
@@ -151,18 +184,23 @@ function removeCodexConfig(opts = {}) {
151
184
  }
152
185
  try {
153
186
  const hooks = readJson(hooksPath);
154
- const stop = hooks.hooks?.Stop;
155
- if (stop && stop.length > 0) {
156
- const before = stop.length;
157
- hooks.hooks.Stop = stop.filter(
187
+ let changed = false;
188
+ for (const event of ["Stop", "UserPromptSubmit"]) {
189
+ const entries = hooks.hooks?.[event];
190
+ if (!entries || entries.length === 0) continue;
191
+ const kept = entries.filter(
158
192
  // F10: strip kojee entries in EITHER persisted form (npx or pinned).
159
193
  (e) => !e.hooks?.some((h) => isKojeeHookCommand(h.command))
160
194
  );
161
- if (hooks.hooks.Stop.length !== before) {
162
- result.stopHook = true;
163
- writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
195
+ if (kept.length !== entries.length) {
196
+ hooks.hooks[event] = kept;
197
+ changed = true;
164
198
  }
165
199
  }
200
+ if (changed) {
201
+ result.stopHook = true;
202
+ writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
203
+ }
166
204
  } catch {
167
205
  }
168
206
  return result;
@@ -309,6 +347,7 @@ export {
309
347
  defaultCodexHooksPath,
310
348
  buildCodexMcpServerTable,
311
349
  buildCodexStopHookBlock,
350
+ buildCodexPromptSubmitHookBlock,
312
351
  isPlaceholderWebhookUrl,
313
352
  writeCodexConfig,
314
353
  removeCodexConfig
@@ -6,7 +6,7 @@ import {
6
6
  buildCondensedTandemRules,
7
7
  buildMonitorSpawn,
8
8
  buildReplyRecipe
9
- } from "./chunk-SSW5AQSR.js";
9
+ } from "./chunk-FJUAMJHU.js";
10
10
  import {
11
11
  translateToolCallResult
12
12
  } from "./chunk-PPTKGWFF.js";
@@ -40,15 +40,112 @@ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
40
40
  function tandemIdArg(args) {
41
41
  return typeof args["tandem_id"] === "string" ? args["tandem_id"] : null;
42
42
  }
43
+ function parseJoinDisplayName(content) {
44
+ for (const item of content) {
45
+ if (item.type !== "text" || typeof item.text !== "string") continue;
46
+ try {
47
+ const parsed = JSON.parse(item.text);
48
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
49
+ const name = parsed["display_name"];
50
+ if (typeof name === "string" && name.length > 0) return name;
51
+ }
52
+ } catch {
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ function parseJoinStatus(content) {
58
+ for (const item of content) {
59
+ if (item.type !== "text" || typeof item.text !== "string") continue;
60
+ try {
61
+ const parsed = JSON.parse(item.text);
62
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
63
+ const status = parsed["status"];
64
+ if (typeof status === "string") return status;
65
+ }
66
+ } catch {
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ var SKIP_ADVISORY_STATUSES = /* @__PURE__ */ new Set(["pending_approval", "needs_invite"]);
72
+ function buildUnnamedJoinAdvisory(args, content) {
73
+ const seatName = args["seat_name"];
74
+ if (typeof seatName === "string" && seatName.trim().length > 0) return null;
75
+ const status = parseJoinStatus(content);
76
+ if (status !== null && SKIP_ADVISORY_STATUSES.has(status)) return null;
77
+ const displayName = parseJoinDisplayName(content);
78
+ const handleClause = displayName !== null ? `the room minted a generated handle ('${displayName}')` : "the room minted a generated handle";
79
+ const text = `[kojee] You joined WITHOUT seat_name \u2014 ${handleClause}. If you previously held a name in this room, re-join now with tandem_join(tandem_id, seat_name=<your name>) to rebind your seat; mentions to your old name will NOT reach this seat until you do.`;
80
+ return { type: "text", text };
81
+ }
82
+ var DRAIN_TOOLS = /* @__PURE__ */ new Set(["tandem_messages", "tandem_ack"]);
83
+ function extractDrainCursor(name, args, content) {
84
+ const candidates = [];
85
+ const push = (v) => {
86
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0) candidates.push(Math.floor(v));
87
+ };
88
+ if (name === "tandem_ack") push(args["cursor"]);
89
+ for (const item of content) {
90
+ if (item.type !== "text" || typeof item.text !== "string") continue;
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(item.text);
94
+ } catch {
95
+ continue;
96
+ }
97
+ const rows = Array.isArray(parsed) ? parsed : [];
98
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
99
+ const obj = parsed;
100
+ push(obj["cursor"]);
101
+ push(obj["latest_cursor"]);
102
+ push(obj["next_cursor"]);
103
+ for (const key of ["messages", "events"]) {
104
+ const arr = obj[key];
105
+ if (Array.isArray(arr)) rows.push(...arr);
106
+ }
107
+ }
108
+ for (const row of rows) {
109
+ if (row !== null && typeof row === "object") push(row["cursor"]);
110
+ }
111
+ }
112
+ return candidates.length > 0 ? Math.max(...candidates) : null;
113
+ }
43
114
  async function executeToolCall(registry, name, args, hooks) {
44
115
  const rawResult = await registry.callTool(name, args);
45
- const result = translateToolCallResult(rawResult);
116
+ let result = translateToolCallResult(rawResult);
46
117
  if (!result.isError && name === "tandem_join") {
47
118
  try {
48
119
  hooks?.onTandemJoin?.(tandemIdArg(args));
49
120
  } catch (err) {
50
121
  console.error("[mcp] onTandemJoin hook failed:", err?.message ?? String(err));
51
122
  }
123
+ try {
124
+ const advisory = buildUnnamedJoinAdvisory(args, result.content);
125
+ if (advisory !== null) {
126
+ result = { ...result, content: [...result.content, advisory] };
127
+ }
128
+ } catch (err) {
129
+ console.error("[mcp] unnamed-join advisory decoration failed:", err?.message ?? String(err));
130
+ }
131
+ }
132
+ if (!result.isError && name === "tandem_leave") {
133
+ try {
134
+ hooks?.onTandemLeave?.(tandemIdArg(args));
135
+ } catch (err) {
136
+ console.error("[mcp] onTandemLeave hook failed:", err?.message ?? String(err));
137
+ }
138
+ }
139
+ if (!result.isError && DRAIN_TOOLS.has(name)) {
140
+ const tandemId = tandemIdArg(args);
141
+ const cursor = tandemId !== null ? extractDrainCursor(name, args, result.content) : null;
142
+ if (tandemId !== null && cursor !== null) {
143
+ try {
144
+ hooks?.onTandemDrain?.(tandemId, cursor);
145
+ } catch (err) {
146
+ console.error("[mcp] onTandemDrain hook failed:", err?.message ?? String(err));
147
+ }
148
+ }
52
149
  }
53
150
  return result;
54
151
  }
@@ -88,6 +185,7 @@ async function startMcpServer(server) {
88
185
  export {
89
186
  buildNonChannelInstructions,
90
187
  buildChannelInstructions,
188
+ extractDrainCursor,
91
189
  executeToolCall,
92
190
  createMcpServer,
93
191
  startMcpServer
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  claudeCodeAdapter
3
- } from "./chunk-UPJV7GBE.js";
3
+ } from "./chunk-TCWIXG5C.js";
4
4
  import {
5
5
  GatewayClient,
6
6
  applyStableSessionId
@@ -11,7 +11,7 @@ import {
11
11
  import {
12
12
  createMcpServer,
13
13
  startMcpServer
14
- } from "./chunk-GNLCUJBK.js";
14
+ } from "./chunk-A4IOKD4Z.js";
15
15
  import {
16
16
  findClaudeAncestorPid
17
17
  } from "./chunk-XJEBJIQE.js";
@@ -29,6 +29,21 @@ var ToolRegistry = class {
29
29
  gateway;
30
30
  /** Flat map: tool name → full tool definition */
31
31
  tools = /* @__PURE__ */ new Map();
32
+ /**
33
+ * LOCAL tools answered in-process by this proxy (e.g. codex's
34
+ * `tandem_pending` — per-window state the gateway cannot know). Local wins
35
+ * over a same-named gateway tool in both list and dispatch, so a future
36
+ * backend tool can never silently shadow a proxy-local answer.
37
+ */
38
+ localTools = /* @__PURE__ */ new Map();
39
+ /**
40
+ * Register a proxy-local tool. Callers gate registration per-runtime (see
41
+ * tandem/pending-state.ts installPendingTool) — an un-opted runtime's tool
42
+ * list must stay byte-identical to the gateway's.
43
+ */
44
+ registerLocalTool(definition, handler) {
45
+ this.localTools.set(definition.name, { definition, handler });
46
+ }
32
47
  /**
33
48
  * Fetch all tools with full schemas from the gateway in a single RPC call.
34
49
  */
@@ -56,15 +71,37 @@ var ToolRegistry = class {
56
71
  console.error(`[tools] Registered ${this.tools.size} tools from gateway`);
57
72
  }
58
73
  /**
59
- * Return all registered tools for the MCP ListTools response.
74
+ * Return all registered tools for the MCP ListTools response — gateway tools
75
+ * first (minus any a local tool shadows), then local tools.
60
76
  */
61
77
  getAllTools() {
62
- return Array.from(this.tools.values());
78
+ const gatewayTools = Array.from(this.tools.values()).filter(
79
+ (t) => !this.localTools.has(t.name)
80
+ );
81
+ const localDefs = Array.from(this.localTools.values()).map((t) => t.definition);
82
+ return [...gatewayTools, ...localDefs];
63
83
  }
64
84
  /**
65
- * Call a tool through the gateway.
85
+ * Call a tool — local tools are answered in-process; everything else goes
86
+ * through the gateway.
66
87
  */
67
88
  async callTool(name, args) {
89
+ const local = this.localTools.get(name);
90
+ if (local) {
91
+ try {
92
+ return await local.handler(args);
93
+ } catch (err) {
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: `Local tool '${name}' failed: ${err?.message ?? String(err)}`
99
+ }
100
+ ],
101
+ isError: true
102
+ };
103
+ }
104
+ }
68
105
  if (!this.tools.has(name)) {
69
106
  const available = Array.from(this.tools.keys()).slice(0, 10).join(", ");
70
107
  return {
@@ -82,9 +119,9 @@ var ToolRegistry = class {
82
119
  arguments: args
83
120
  });
84
121
  }
85
- /** Total number of registered tools. */
122
+ /** Total number of registered tools (gateway + local, shadowed names once). */
86
123
  get toolCount() {
87
- return this.tools.size;
124
+ return this.getAllTools().length;
88
125
  }
89
126
  };
90
127
 
@@ -218,9 +255,16 @@ async function startProxy(config) {
218
255
  return true;
219
256
  }
220
257
  });
221
- const onTandemJoin = (_tandemId) => {
258
+ const { installPendingTool, createRuntimeDrainHook } = await import("./pending-state-6TVRR63P.js");
259
+ const pendingState = installPendingTool(adapter.runtime, registry);
260
+ const onTandemJoin = (tandemId) => {
222
261
  joinReconnect.requestReconnect();
262
+ if (tandemId !== null) pendingState?.noteSeat(tandemId);
223
263
  };
264
+ const onTandemLeave = pendingState ? (tandemId) => {
265
+ if (tandemId !== null) pendingState.dropSeat(tandemId);
266
+ } : void 0;
267
+ const onTandemDrain = createRuntimeDrainHook(adapter.runtime, pendingState);
224
268
  const teardownSteps = [];
225
269
  let shuttingDown = false;
226
270
  function shutdown(reason) {
@@ -251,9 +295,12 @@ async function startProxy(config) {
251
295
  }
252
296
  console.error(`[kojee-mcp] Tandem memberships: ${tandemMembershipCount === -1 ? "unknown" : tandemMembershipCount}`);
253
297
  let server;
254
- const { selectDelivery } = await import("./registry-ZZZ26WGA.js");
298
+ const { selectDelivery } = await import("./registry-2QS42EQF.js");
255
299
  const delivery = selectDelivery(adapter.runtime, {
256
- supportsChannels: adapter.supportsChannels
300
+ supportsChannels: adapter.supportsChannels,
301
+ // Per-window delivered mirror for the tandem_pending tool (codex only —
302
+ // other runtimes' deliveries ignore this even when passed).
303
+ ...pendingState ? { onDelivered: (e) => pendingState.noteDelivered(e.tandem_id, e.cursor) } : {}
257
304
  });
258
305
  if (delivery) {
259
306
  const started = await delivery.start({
@@ -266,7 +313,11 @@ async function startProxy(config) {
266
313
  ccPid,
267
314
  tandemMembershipCount,
268
315
  listTandemIds: () => listTandemIds(gateway),
269
- toolCallHooks: { onTandemJoin },
316
+ toolCallHooks: {
317
+ onTandemJoin,
318
+ ...onTandemLeave ? { onTandemLeave } : {},
319
+ ...onTandemDrain ? { onTandemDrain } : {}
320
+ },
270
321
  onStreamReady: (handle) => {
271
322
  activeStreamHandle = handle;
272
323
  joinReconnect.notifyReady();
@@ -16,7 +16,7 @@ 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
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.";
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 WHEN IT CHANGES \u2014 skip the check-in ceremony when you are merely re-joining/reconnecting with the same posture. (5) send all replies and check-ins as kind=message \u2014 never kind=status; status is lifecycle-only (joined/left, system-generated).";
20
20
  }
21
21
  function buildMonitorNudge(logPath) {
22
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()}.`;
@@ -29,10 +29,7 @@ function buildWebhookReceiverNote(sig) {
29
29
  return "Webhook sink (optional, OFF unless KOJEE_WEBHOOK_URL + KOJEE_WEBHOOK_SECRET are set): the proxy POSTs every Tandem event as JSON to your endpoint. The body is the canonical normalized TandemEvent \u2014 " + WEBHOOK_BODY_SHAPE + ` \u2014 where from.session_id and severity are present only when the wire carried them (the body is fully normalized: it carries from.principal, never the raw backend sender envelope). To build a receiver: (1) verify the ${header} header \u2014 it is ${digestDesc} of the RAW request body bytes keyed by your KOJEE_WEBHOOK_SECRET; recompute over the received bytes and timing-safe compare, reject mismatches. (2) Dedupe by message_id \u2014 the body's \`id\`, also in the X-Kojee-Delivery header: delivery is AT-LEAST-ONCE (the proxy replays backlog from the cursor on restart), so the same event may arrive more than once \u2014 there is no exactly-once promise.`;
30
30
  }
31
31
  var CODEX_LISTEN_CAP_MS = 8e3;
32
- function buildCodexWakeReason(cursor) {
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.`;
35
- }
32
+ var CODEX_WAKE_BELL = "[kojee] Tandem events may be pending. Call tandem_pending now and drain each room it lists (tandem_messages(id, since=cursor)), then reply in the room with a normal message (kind=message \u2014 never status). If it returns none for you, ignore this.";
36
33
 
37
34
  export {
38
35
  buildMonitorSpawn,
@@ -42,5 +39,5 @@ export {
42
39
  buildMonitorNudge,
43
40
  buildWebhookReceiverNote,
44
41
  CODEX_LISTEN_CAP_MS,
45
- buildCodexWakeReason
42
+ CODEX_WAKE_BELL
46
43
  };
@@ -41,6 +41,9 @@ function createAdaptiveWatchdog(options = {}) {
41
41
  },
42
42
  armedThresholdMs() {
43
43
  return thresholdMs();
44
+ },
45
+ resetCadenceAnchor() {
46
+ prevHeartbeatAt = null;
44
47
  }
45
48
  };
46
49
  }
@@ -443,9 +446,11 @@ function normalizeBackendEvent(raw, sseEventType) {
443
446
  }
444
447
 
445
448
  export {
449
+ UNARMED_FALLBACK_MS,
446
450
  UNDICI_DEFAULT_BODY_TIMEOUT_MS,
447
451
  createAdaptiveWatchdog,
448
452
  createBackoffController,
453
+ LOG_HEARTBEAT_INTERVAL_MS,
449
454
  startEventStream,
450
455
  statusReason,
451
456
  serializeCursorMap,
@@ -0,0 +1,132 @@
1
+ // src/delivery/pending-ledger.ts
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ var LEDGER_MAX_ROOMS = 16;
6
+ function pendingRoomsPath() {
7
+ return path.join(os.homedir(), ".kojee", "pending-rooms");
8
+ }
9
+ function drainedRoomsPath() {
10
+ return pendingRoomsPath() + ".drained";
11
+ }
12
+ function legacyMarkerPath() {
13
+ return path.join(os.homedir(), ".kojee", "codex-pending");
14
+ }
15
+ function legacyAckPath() {
16
+ return legacyMarkerPath() + ".ack";
17
+ }
18
+ function readEntries(filePath) {
19
+ let raw;
20
+ try {
21
+ raw = fs.readFileSync(filePath, "utf8");
22
+ } catch {
23
+ return [];
24
+ }
25
+ const out = [];
26
+ for (const line of raw.split("\n")) {
27
+ const trimmed = line.trim();
28
+ if (trimmed === "") continue;
29
+ const parts = trimmed.split(/\s+/);
30
+ if (parts.length !== 2) continue;
31
+ const id = parts[0];
32
+ const cursor = Number.parseInt(parts[1], 10);
33
+ if (!Number.isFinite(cursor) || cursor < 0) continue;
34
+ const existing = out.findIndex((e) => e.id === id);
35
+ if (existing !== -1) {
36
+ const prev = out[existing];
37
+ out.splice(existing, 1);
38
+ out.push({ id, cursor: Math.max(prev.cursor, cursor) });
39
+ } else {
40
+ out.push({ id, cursor });
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+ function writeEntries(filePath, entries) {
46
+ const tmp = `${filePath}.tmp-${process.pid}`;
47
+ try {
48
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
49
+ const body = entries.map((e) => `${e.id} ${e.cursor}`).join("\n") + (entries.length ? "\n" : "");
50
+ fs.writeFileSync(tmp, body, { mode: 384 });
51
+ fs.renameSync(tmp, filePath);
52
+ } catch {
53
+ try {
54
+ fs.unlinkSync(tmp);
55
+ } catch {
56
+ }
57
+ }
58
+ }
59
+ function upsert(filePath, tandemId, cursor, minCursor) {
60
+ const id = String(tandemId ?? "").replace(/\s+/g, "");
61
+ if (id === "") return;
62
+ if (!Number.isFinite(cursor) || cursor < minCursor) return;
63
+ const next = Math.floor(cursor);
64
+ const entries = readEntries(filePath);
65
+ const existing = entries.findIndex((e) => e.id === id);
66
+ let value = next;
67
+ if (existing !== -1) {
68
+ value = Math.max(entries[existing].cursor, next);
69
+ entries.splice(existing, 1);
70
+ }
71
+ entries.push({ id, cursor: value });
72
+ while (entries.length > LEDGER_MAX_ROOMS) entries.shift();
73
+ writeEntries(filePath, entries);
74
+ }
75
+ function recordDelivered(tandemId, cursor) {
76
+ try {
77
+ upsert(pendingRoomsPath(), tandemId, cursor, 1);
78
+ } catch {
79
+ }
80
+ }
81
+ function recordDrained(tandemId, cursor) {
82
+ try {
83
+ upsert(drainedRoomsPath(), tandemId, cursor, 0);
84
+ } catch {
85
+ }
86
+ }
87
+ function listPending() {
88
+ const delivered = readEntries(pendingRoomsPath());
89
+ const drained = new Map(readEntries(drainedRoomsPath()).map((e) => [e.id, e.cursor]));
90
+ const out = [];
91
+ try {
92
+ const body = fs.readFileSync(legacyMarkerPath(), "utf8").trim();
93
+ if (body !== "") {
94
+ let acked = null;
95
+ try {
96
+ acked = fs.readFileSync(legacyAckPath(), "utf8").trim();
97
+ } catch {
98
+ acked = null;
99
+ }
100
+ if (acked === null || acked !== body) {
101
+ const toks = body.split(/\s+/);
102
+ const parsedCursor = Number.parseInt(toks[0] ?? "", 10);
103
+ const cursor = Number.isFinite(parsedCursor) && parsedCursor >= 0 ? parsedCursor : 0;
104
+ const room = toks[1] ?? null;
105
+ if (room !== null) {
106
+ const drainedCursor = drained.get(room);
107
+ const inLedger = delivered.some((e) => e.id === room);
108
+ if (!inLedger && (drainedCursor === void 0 || drainedCursor < cursor)) {
109
+ out.push({ tandemId: room, cursor });
110
+ }
111
+ } else if (drained.size === 0) {
112
+ out.push({ tandemId: null, cursor });
113
+ }
114
+ }
115
+ }
116
+ } catch {
117
+ }
118
+ for (const entry of delivered) {
119
+ if (entry.cursor > (drained.get(entry.id) ?? 0)) {
120
+ out.push({ tandemId: entry.id, cursor: entry.cursor });
121
+ }
122
+ }
123
+ return out;
124
+ }
125
+
126
+ export {
127
+ pendingRoomsPath,
128
+ drainedRoomsPath,
129
+ recordDelivered,
130
+ recordDrained,
131
+ listPending
132
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildReplyRecipe
3
- } from "./chunk-SSW5AQSR.js";
3
+ } from "./chunk-FJUAMJHU.js";
4
4
 
5
5
  // src/adapters/claude-code.ts
6
6
  function computeSeverity(event) {
@@ -20,7 +20,7 @@ function buildHookCommand(type, opts = {}) {
20
20
  const execPath = opts.execPath ?? process.execPath;
21
21
  return `"${execPath}" "${cliEntry}" hook --type=${type}`;
22
22
  }
23
- var PINNED_HOOK_RE = /^"[^"]+" "[^"]+" hook --type=(stop|user-prompt-submit|codex-stop)$/;
23
+ var PINNED_HOOK_RE = /^"[^"]+" "[^"]+" hook --type=(stop|user-prompt-submit|codex-stop|codex-prompt-submit)$/;
24
24
  function isKojeeHookCommand(command) {
25
25
  if (command.startsWith("npx -y kojee-mcp hook --type=")) return true;
26
26
  return PINNED_HOOK_RE.test(command);