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.
- package/dist/{chunk-UPJV7GBE.js → chunk-E35VWFZV.js} +1 -1
- package/dist/{chunk-UFHGZUST.js → chunk-EDHG4375.js} +19 -13
- package/dist/{chunk-FSKGQ6GT.js → chunk-HI42GBQ3.js} +5 -0
- package/dist/chunk-IZN7IZPW.js +132 -0
- package/dist/{chunk-FBJCPRVH.js → chunk-R5GC2GRD.js} +62 -11
- package/dist/{chunk-KPMD72FY.js → chunk-RVLUZLXD.js} +51 -21
- package/dist/{chunk-QJFMU4QC.js → chunk-TMCNB4JH.js} +1 -1
- package/dist/{chunk-SSW5AQSR.js → chunk-XFGGMDZ4.js} +3 -6
- package/dist/chunk-XLRF5ATG.js +103 -0
- package/dist/{chunk-GNLCUJBK.js → chunk-ZUIYFRO5.js} +52 -1
- package/dist/cli.js +23 -14
- package/dist/codex-prompt-submit-hook-FOBPGZHJ.js +39 -0
- package/dist/codex-stop-hook-PNWYNQKM.js +136 -0
- package/dist/{connect-handler-4DRTFMOB.js → connect-handler-2JFIMQY6.js} +14 -7
- package/dist/{doctor-5QJ3HGNR.js → doctor-HXMCO5PR.js} +2 -2
- package/dist/doctor-codex-ZSALZPH3.js +370 -0
- package/dist/{event-stream-KRYWEYWO.js → event-stream-WPN3EN7C.js} +5 -1
- package/dist/index.js +5 -5
- package/dist/{install-V7LSQCYZ.js → install-GGI6GU6C.js} +1 -1
- package/dist/lib.d.ts +40 -15
- package/dist/lib.js +7 -7
- package/dist/pending-state-6TVRR63P.js +134 -0
- package/dist/{registry-ZZZ26WGA.js → registry-42Y45L6I.js} +114 -51
- package/dist/{server-ITPFQVTK.js → server-JWIH7OFA.js} +4 -2
- package/dist/{setup-handler-JFM45NCN.js → setup-handler-GBPXDETR.js} +7 -7
- package/dist/{stop-hook-5ABGTC2O.js → stop-hook-PWTAP227.js} +2 -2
- package/dist/{tail-stream-NBGHHBS4.js → tail-stream-N43D53RC.js} +99 -19
- package/package.json +1 -1
- package/skills/using-tandems/SKILL.md +1 -1
- package/dist/chunk-EBYUJM3H.js +0 -14
- package/dist/codex-stop-hook-BMOJVM6O.js +0 -96
- package/dist/doctor-codex-VGJKTX2E.js +0 -163
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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-
|
|
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: {
|
|
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-
|
|
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",
|
|
49
|
+
return ["-y", spec, "--token", token, "--url", url];
|
|
39
50
|
}
|
|
40
51
|
if (pairedConfigPath) {
|
|
41
|
-
return ["-y",
|
|
52
|
+
return ["-y", spec, "--paired-config", pairedConfigPath];
|
|
42
53
|
}
|
|
43
|
-
return ["-y",
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
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 (
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|