kojee-mcp 0.7.1 → 0.7.2

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-UPJV7GBE.js → chunk-E35VWFZV.js} +1 -1
  2. package/dist/{chunk-UFHGZUST.js → chunk-EDHG4375.js} +19 -13
  3. package/dist/{chunk-FSKGQ6GT.js → chunk-HI42GBQ3.js} +5 -0
  4. package/dist/chunk-IZN7IZPW.js +132 -0
  5. package/dist/{chunk-FBJCPRVH.js → chunk-R5GC2GRD.js} +62 -11
  6. package/dist/{chunk-KPMD72FY.js → chunk-RVLUZLXD.js} +51 -21
  7. package/dist/{chunk-QJFMU4QC.js → chunk-TMCNB4JH.js} +1 -1
  8. package/dist/{chunk-SSW5AQSR.js → chunk-XFGGMDZ4.js} +3 -6
  9. package/dist/chunk-XLRF5ATG.js +103 -0
  10. package/dist/{chunk-GNLCUJBK.js → chunk-ZUIYFRO5.js} +52 -1
  11. package/dist/cli.js +23 -14
  12. package/dist/codex-prompt-submit-hook-FOBPGZHJ.js +39 -0
  13. package/dist/codex-stop-hook-PNWYNQKM.js +136 -0
  14. package/dist/{connect-handler-4DRTFMOB.js → connect-handler-2JFIMQY6.js} +14 -7
  15. package/dist/{doctor-5QJ3HGNR.js → doctor-HXMCO5PR.js} +2 -2
  16. package/dist/doctor-codex-ZSALZPH3.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-GGI6GU6C.js} +1 -1
  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-42Y45L6I.js} +114 -51
  24. package/dist/{server-ITPFQVTK.js → server-JWIH7OFA.js} +4 -2
  25. package/dist/{setup-handler-JFM45NCN.js → setup-handler-GBPXDETR.js} +7 -7
  26. package/dist/{stop-hook-5ABGTC2O.js → stop-hook-PWTAP227.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 +1 -1
  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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildReplyRecipe
3
- } from "./chunk-SSW5AQSR.js";
3
+ } from "./chunk-XFGGMDZ4.js";
4
4
 
5
5
  // src/adapters/claude-code.ts
6
6
  function computeSeverity(event) {
@@ -1,14 +1,19 @@
1
- import {
2
- removeOpenclawMcpServer,
3
- writeOpenclawMcpConfig
4
- } from "./chunk-E6WMFMM2.js";
5
1
  import {
6
2
  buildCodexMcpServerTable,
3
+ buildCodexPromptSubmitHookBlock,
7
4
  buildCodexStopHookBlock,
8
5
  isPlaceholderWebhookUrl,
9
6
  removeCodexConfig,
10
7
  writeCodexConfig
11
- } from "./chunk-KPMD72FY.js";
8
+ } from "./chunk-RVLUZLXD.js";
9
+ import {
10
+ removeOpenclawMcpServer,
11
+ writeOpenclawMcpConfig
12
+ } from "./chunk-E6WMFMM2.js";
13
+ import {
14
+ WIZARD_RUNTIMES,
15
+ isWizardRuntime
16
+ } from "./chunk-77HWBSRH.js";
12
17
  import {
13
18
  CONNECT_RUNTIMES
14
19
  } from "./chunk-EIH2LNF4.js";
@@ -20,10 +25,6 @@ import {
20
25
  import {
21
26
  kojeeHomeDir
22
27
  } from "./chunk-SQL56SEB.js";
23
- import {
24
- WIZARD_RUNTIMES,
25
- isWizardRuntime
26
- } from "./chunk-77HWBSRH.js";
27
28
  import {
28
29
  resolveSignatureEmission,
29
30
  resolveWebhookConfig
@@ -48,7 +49,7 @@ import {
48
49
  CODEX_LISTEN_CAP_MS,
49
50
  buildCondensedTandemRules,
50
51
  buildWebhookReceiverNote
51
- } from "./chunk-SSW5AQSR.js";
52
+ } from "./chunk-XFGGMDZ4.js";
52
53
 
53
54
  // src/wizard/pair-slot.ts
54
55
  import fs from "fs";
@@ -789,7 +790,7 @@ function whatHappensNext(runtime, opts) {
789
790
  return lines.join("\n");
790
791
  }
791
792
  async function configureClaudeCode(opts) {
792
- const { runInit } = await import("./install-V7LSQCYZ.js");
793
+ const { runInit } = await import("./install-GGI6GU6C.js");
793
794
  const report = runInit({
794
795
  ...opts.configPath ? { configPath: opts.configPath } : {},
795
796
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
@@ -885,8 +886,13 @@ function configureCodex(opts) {
885
886
  })));
886
887
  if (wh.warning) lines.push(`webhook WARNING: ${wh.warning}`);
887
888
  lines.push("");
888
- lines.push("Wrote the Codex Stop hook (~/.codex/hooks.json; inline TOML form):");
889
+ lines.push("Wrote the Codex Stop + UserPromptSubmit hooks (~/.codex/hooks.json; inline TOML form):");
889
890
  lines.push(indent(buildCodexStopHookBlock()));
891
+ lines.push(indent(buildCodexPromptSubmitHookBlock()));
892
+ lines.push("");
893
+ lines.push(
894
+ "IMPORTANT: Codex will re-prompt to trust hooks on its next launch \u2014 approve it, or wakes silently stop."
895
+ );
890
896
  lines.push("");
891
897
  lines.push(
892
898
  connectPairedMode ? "webhook: (unchanged \u2014 `connect` rewrites only the credential args, not the wake path)" : `webhook: ${wh.url ? wh.redactedSummary : "OPTIONAL \u2014 not configured (wake is self-contained; set KOJEE_WEBHOOK_URL only for an extra low-latency push path)"}`
@@ -1027,7 +1033,7 @@ async function runWizardUninstall(runtime, opts) {
1027
1033
  const effective = opts.runtime !== void 0 ? runtime : readRecordedRuntime() ?? runtime;
1028
1034
  const lines = [`Uninstalling runtime: ${effective}`];
1029
1035
  if (effective === "claude-code") {
1030
- const { runUninstall } = await import("./install-V7LSQCYZ.js");
1036
+ const { runUninstall } = await import("./install-GGI6GU6C.js");
1031
1037
  const report = runUninstall({
1032
1038
  ...opts.configPath ? { configPath: opts.configPath } : {},
1033
1039
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {}
@@ -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
  claudeCodeAdapter
3
- } from "./chunk-UPJV7GBE.js";
3
+ } from "./chunk-E35VWFZV.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-ZUIYFRO5.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-42Y45L6I.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();
@@ -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) {
@@ -112,17 +133,21 @@ function writeCodexConfig(inputs) {
112
133
  inputs.pairedConfigPath
113
134
  );
114
135
  writeFile600(configPath, toml);
115
- const command = codexStopHookCommand();
116
136
  const hooks = readJson(hooksPath);
117
137
  hooks.hooks ??= {};
118
- hooks.hooks.Stop ??= [];
138
+ upsertKojeeHookEntry(hooks, "Stop", codexStopHookCommand());
139
+ upsertKojeeHookEntry(hooks, "UserPromptSubmit", codexPromptSubmitHookCommand());
140
+ writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
141
+ }
142
+ function upsertKojeeHookEntry(hooks, event, command) {
143
+ hooks.hooks ??= {};
144
+ hooks.hooks[event] ??= [];
119
145
  let present = false;
120
- for (const e of hooks.hooks.Stop) {
146
+ for (const e of hooks.hooks[event]) {
121
147
  for (const h of e.hooks ?? []) {
122
- if (h.command === command) {
123
- present = true;
124
- } else if (isKojeeHookCommand(h.command)) {
148
+ if (h.command === command || isKojeeHookCommand(h.command)) {
125
149
  h.command = command;
150
+ h.timeout = CODEX_HOOK_TIMEOUT_SEC;
126
151
  present = true;
127
152
  }
128
153
  if (present) break;
@@ -130,11 +155,10 @@ function writeCodexConfig(inputs) {
130
155
  if (present) break;
131
156
  }
132
157
  if (!present) {
133
- hooks.hooks.Stop.push({
134
- hooks: [{ type: "command", command }]
158
+ hooks.hooks[event].push({
159
+ hooks: [{ type: "command", command, timeout: CODEX_HOOK_TIMEOUT_SEC }]
135
160
  });
136
161
  }
137
- writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
138
162
  }
139
163
  function removeCodexConfig(opts = {}) {
140
164
  const configPath = opts.configPath ?? defaultCodexConfigPath();
@@ -151,18 +175,23 @@ function removeCodexConfig(opts = {}) {
151
175
  }
152
176
  try {
153
177
  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(
178
+ let changed = false;
179
+ for (const event of ["Stop", "UserPromptSubmit"]) {
180
+ const entries = hooks.hooks?.[event];
181
+ if (!entries || entries.length === 0) continue;
182
+ const kept = entries.filter(
158
183
  // F10: strip kojee entries in EITHER persisted form (npx or pinned).
159
184
  (e) => !e.hooks?.some((h) => isKojeeHookCommand(h.command))
160
185
  );
161
- if (hooks.hooks.Stop.length !== before) {
162
- result.stopHook = true;
163
- writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
186
+ if (kept.length !== entries.length) {
187
+ hooks.hooks[event] = kept;
188
+ changed = true;
164
189
  }
165
190
  }
191
+ if (changed) {
192
+ result.stopHook = true;
193
+ writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
194
+ }
166
195
  } catch {
167
196
  }
168
197
  return result;
@@ -309,6 +338,7 @@ export {
309
338
  defaultCodexHooksPath,
310
339
  buildCodexMcpServerTable,
311
340
  buildCodexStopHookBlock,
341
+ buildCodexPromptSubmitHookBlock,
312
342
  isPlaceholderWebhookUrl,
313
343
  writeCodexConfig,
314
344
  removeCodexConfig
@@ -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);
@@ -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.";
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. 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
  };
@@ -0,0 +1,103 @@
1
+ // src/delivery/codex-status.ts
2
+ import fs from "fs";
3
+ import fsp from "fs/promises";
4
+ import os from "os";
5
+ import path from "path";
6
+ import crypto from "crypto";
7
+ var CODEX_STATUS_HEARTBEAT_MS = 6e4;
8
+ var CODEX_STATUS_STALE_MS = 5 * 6e4;
9
+ function codexStatusDir() {
10
+ return path.join(os.homedir(), ".kojee");
11
+ }
12
+ function codexStatusPath(pid = process.pid) {
13
+ return path.join(codexStatusDir(), `codex-status-${pid}.json`);
14
+ }
15
+ var STATUS_FILE_RE = /^codex-status(?:-(\d+))?\.json$/;
16
+ function readAllCodexStatusRaw() {
17
+ const dir = codexStatusDir();
18
+ let names;
19
+ try {
20
+ names = fs.readdirSync(dir);
21
+ } catch {
22
+ return [];
23
+ }
24
+ const out = [];
25
+ for (const name of names.filter((n) => STATUS_FILE_RE.test(n)).sort()) {
26
+ try {
27
+ out.push(fs.readFileSync(path.join(dir, name), "utf8"));
28
+ } catch {
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ async function sweepDeadCodexStatusFiles(isPidAlive = defaultIsPidAlive) {
34
+ const dir = codexStatusDir();
35
+ let names;
36
+ try {
37
+ names = await fsp.readdir(dir);
38
+ } catch {
39
+ return;
40
+ }
41
+ for (const name of names) {
42
+ const m = STATUS_FILE_RE.exec(name);
43
+ if (!m) continue;
44
+ const full = path.join(dir, name);
45
+ let pid = m[1] ? Number(m[1]) : null;
46
+ if (pid === null) {
47
+ try {
48
+ pid = readCodexStatus(await fsp.readFile(full, "utf8"))?.pid ?? null;
49
+ } catch {
50
+ pid = null;
51
+ }
52
+ }
53
+ if (pid === process.pid) continue;
54
+ if (pid !== null && isPidAlive(pid)) continue;
55
+ try {
56
+ await fsp.unlink(full);
57
+ } catch {
58
+ }
59
+ }
60
+ }
61
+ function defaultIsPidAlive(pid) {
62
+ try {
63
+ process.kill(pid, 0);
64
+ return true;
65
+ } catch (err) {
66
+ return err?.code === "EPERM";
67
+ }
68
+ }
69
+ function readCodexStatus(raw) {
70
+ if (!raw) return null;
71
+ try {
72
+ const parsed = JSON.parse(raw);
73
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.pid !== "number" || typeof parsed.updated_at !== "string" || typeof parsed.stream !== "object" || parsed.stream === null) {
74
+ return null;
75
+ }
76
+ return parsed;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ async function writeCodexStatus(status) {
82
+ const statusPath = codexStatusPath(status.pid);
83
+ const tmp = `${statusPath}.tmp-${process.pid}-${crypto.randomUUID()}`;
84
+ try {
85
+ await fsp.mkdir(path.dirname(statusPath), { recursive: true });
86
+ await fsp.writeFile(tmp, JSON.stringify(status, null, 2), { mode: 384 });
87
+ await fsp.rename(tmp, statusPath);
88
+ } catch {
89
+ try {
90
+ await fsp.unlink(tmp);
91
+ } catch {
92
+ }
93
+ }
94
+ }
95
+
96
+ export {
97
+ CODEX_STATUS_HEARTBEAT_MS,
98
+ CODEX_STATUS_STALE_MS,
99
+ readAllCodexStatusRaw,
100
+ sweepDeadCodexStatusFiles,
101
+ readCodexStatus,
102
+ writeCodexStatus
103
+ };