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.
- package/dist/{chunk-KPMD72FY.js → chunk-6XWTUDWW.js} +65 -26
- package/dist/{chunk-GNLCUJBK.js → chunk-A4IOKD4Z.js} +100 -2
- package/dist/{chunk-FBJCPRVH.js → chunk-ABECLEYE.js} +62 -11
- package/dist/{chunk-SSW5AQSR.js → chunk-FJUAMJHU.js} +3 -6
- package/dist/{chunk-FSKGQ6GT.js → chunk-HI42GBQ3.js} +5 -0
- package/dist/chunk-IZN7IZPW.js +132 -0
- package/dist/{chunk-UPJV7GBE.js → chunk-TCWIXG5C.js} +1 -1
- package/dist/{chunk-QJFMU4QC.js → chunk-TMCNB4JH.js} +1 -1
- package/dist/chunk-XLRF5ATG.js +103 -0
- package/dist/{chunk-UFHGZUST.js → chunk-XPIW4N55.js} +22 -14
- package/dist/cli.js +23 -14
- package/dist/codex-prompt-submit-hook-J5PZEJSK.js +39 -0
- package/dist/codex-stop-hook-32W4BIOM.js +136 -0
- package/dist/{connect-handler-4DRTFMOB.js → connect-handler-NZINEMG3.js} +14 -7
- package/dist/{doctor-5QJ3HGNR.js → doctor-WUU5BVPT.js} +2 -2
- package/dist/doctor-codex-PJFWIABF.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-JQNDGAAQ.js} +14 -2
- 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-2QS42EQF.js} +114 -51
- package/dist/{server-ITPFQVTK.js → server-DU3LFS32.js} +4 -2
- package/dist/{setup-handler-JFM45NCN.js → setup-handler-44ASXYMS.js} +7 -7
- package/dist/{stop-hook-5ABGTC2O.js → stop-hook-N6TX4YQT.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 +2 -2
- package/dist/chunk-EBYUJM3H.js +0 -14
- package/dist/codex-stop-hook-BMOJVM6O.js +0 -96
- 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-
|
|
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) {
|
|
@@ -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
|
|
121
|
+
let existingToml = "";
|
|
101
122
|
try {
|
|
102
|
-
|
|
123
|
+
existingToml = fs.readFileSync(configPath, "utf8");
|
|
103
124
|
} catch {
|
|
104
125
|
}
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
115
|
-
|
|
135
|
+
const tomlChanged = newToml !== existingToml;
|
|
136
|
+
if (tomlChanged) writeFile600(configPath, newToml);
|
|
116
137
|
const hooks = readJson(hooksPath);
|
|
117
138
|
hooks.hooks ??= {};
|
|
118
|
-
hooks
|
|
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
|
|
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.
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
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 (
|
|
162
|
-
|
|
163
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
@@ -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);
|