kojee-mcp 0.5.10 → 0.5.12
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/README.md +24 -0
- package/dist/ancestry-ONFBQEP5.js +8 -0
- package/dist/{codex-stop-hook-SWA53ECG.js → chunk-35XBRG3V.js} +4 -3
- package/dist/{chunk-MKDMAAMN.js → chunk-5XP2UOFK.js} +12 -0
- package/dist/{chunk-DS26OORG.js → chunk-CO73VGWM.js} +41 -23
- package/dist/chunk-FQZCENSG.js +459 -0
- package/dist/{chunk-YKS6YZKM.js → chunk-PHXO5P25.js} +1 -4
- package/dist/chunk-WLMPCX7T.js +116 -0
- package/dist/chunk-XLKGPGZT.js +0 -0
- package/dist/chunk-XXFVWP6H.js +44 -0
- package/dist/{ensure-join-7AEDJMPE.js → chunk-YKW54DKF.js} +45 -15
- package/dist/cli.js +16 -13
- package/dist/codex-stop-hook-VY7DOMAG.js +16 -0
- package/dist/{doctor-XK335W7B.js → doctor-FVTALRQD.js} +110 -15
- package/dist/ensure-join-5Y5IJ7HN.js +8 -0
- package/dist/{event-log-B27VVEMK.js → event-log-VZD7NKYX.js} +1 -1
- package/dist/event-stream-FOT7MJZH.js +19 -0
- package/dist/{gateway-client-93P1E0CZ.d.ts → gateway-client-C6yx1mfM.d.ts} +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -5
- package/dist/lib.d.ts +181 -3
- package/dist/lib.js +7 -7
- package/dist/{parent-watchdog-RZLHYP7T.js → parent-watchdog-TLU355FB.js} +1 -1
- package/dist/registry-A3VT6VJD.js +348 -0
- package/dist/server-77QRWKJM.js +14 -0
- package/dist/{stop-hook-OTCJGL6V.js → stop-hook-CUVDKXP7.js} +8 -7
- package/dist/{tail-stream-JNR4WFW3.js → tail-stream-VZ462ZON.js} +3 -2
- package/dist/{user-prompt-submit-hook-QXMC7EZU.js → user-prompt-submit-hook-PMBUPKUV.js} +6 -6
- package/dist/{webhook-sink-NWGCUDGY.js → webhook-sink-N6AUTFL3.js} +1 -1
- package/package.json +1 -1
- package/dist/chunk-SCDWPGH3.js +0 -637
- package/dist/{chunk-BJMASMKX.js → chunk-VHKPWUX7.js} +0 -0
- package/dist/{doctor-codex-SMROUYGV.js → doctor-codex-PA3WO6LR.js} +1 -1
- package/dist/{send-cli-CN5EX7PO.js → send-cli-NZP5XE7T.js} +5 -5
- package/dist/{wizard-PLGHYCT3.js → wizard-L4MYRLJI.js} +11 -11
package/package.json
CHANGED
package/dist/chunk-SCDWPGH3.js
DELETED
|
@@ -1,637 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildCatchUpNote,
|
|
3
|
-
buildMonitorSpawn,
|
|
4
|
-
buildReplyRecipe
|
|
5
|
-
} from "./chunk-X672ZN7V.js";
|
|
6
|
-
import {
|
|
7
|
-
deriveDiscoveryKey,
|
|
8
|
-
findClaudeAncestorPid
|
|
9
|
-
} from "./chunk-BJMASMKX.js";
|
|
10
|
-
import {
|
|
11
|
-
AuthModule
|
|
12
|
-
} from "./chunk-JXMVZEQ7.js";
|
|
13
|
-
import {
|
|
14
|
-
GatewayClient
|
|
15
|
-
} from "./chunk-HSR3GXCL.js";
|
|
16
|
-
import {
|
|
17
|
-
startEventStream
|
|
18
|
-
} from "./chunk-MKDMAAMN.js";
|
|
19
|
-
import {
|
|
20
|
-
translateToolCallResult
|
|
21
|
-
} from "./chunk-LDZXU3DW.js";
|
|
22
|
-
|
|
23
|
-
// src/index.ts
|
|
24
|
-
import fs2 from "fs";
|
|
25
|
-
import os from "os";
|
|
26
|
-
import path2 from "path";
|
|
27
|
-
|
|
28
|
-
// src/tool-registry.ts
|
|
29
|
-
var ToolRegistry = class {
|
|
30
|
-
constructor(gateway) {
|
|
31
|
-
this.gateway = gateway;
|
|
32
|
-
}
|
|
33
|
-
gateway;
|
|
34
|
-
/** Flat map: tool name → full tool definition */
|
|
35
|
-
tools = /* @__PURE__ */ new Map();
|
|
36
|
-
/**
|
|
37
|
-
* Fetch all tools with full schemas from the gateway in a single RPC call.
|
|
38
|
-
*/
|
|
39
|
-
async discoverTools() {
|
|
40
|
-
console.error("[tools] Fetching tools with full schemas from gateway...");
|
|
41
|
-
const result = await this.gateway.sendRpc("tools/list", {
|
|
42
|
-
include_schema: true
|
|
43
|
-
});
|
|
44
|
-
const maybeError = result;
|
|
45
|
-
if (maybeError.isError) {
|
|
46
|
-
const msg = maybeError.content?.[0]?.text ?? "unknown error";
|
|
47
|
-
throw new Error(`Gateway rejected tools/list: ${msg}`);
|
|
48
|
-
}
|
|
49
|
-
const toolList = result?.tools;
|
|
50
|
-
if (!toolList || !Array.isArray(toolList)) {
|
|
51
|
-
console.error("[tools] No tools returned from gateway");
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
for (const tool of toolList) {
|
|
55
|
-
if (this.tools.has(tool.name)) {
|
|
56
|
-
console.error(`[tools] Warning: duplicate tool name "${tool.name}" \u2014 overwriting`);
|
|
57
|
-
}
|
|
58
|
-
this.tools.set(tool.name, tool);
|
|
59
|
-
}
|
|
60
|
-
console.error(`[tools] Registered ${this.tools.size} tools from gateway`);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Return all registered tools for the MCP ListTools response.
|
|
64
|
-
*/
|
|
65
|
-
getAllTools() {
|
|
66
|
-
return Array.from(this.tools.values());
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Call a tool through the gateway.
|
|
70
|
-
*/
|
|
71
|
-
async callTool(name, args) {
|
|
72
|
-
if (!this.tools.has(name)) {
|
|
73
|
-
const available = Array.from(this.tools.keys()).slice(0, 10).join(", ");
|
|
74
|
-
return {
|
|
75
|
-
content: [
|
|
76
|
-
{
|
|
77
|
-
type: "text",
|
|
78
|
-
text: `Tool '${name}' not found. Some available tools: ${available}${this.tools.size > 10 ? ", ..." : ""}`
|
|
79
|
-
}
|
|
80
|
-
],
|
|
81
|
-
isError: true
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return this.gateway.sendRpc("tools/call", {
|
|
85
|
-
name,
|
|
86
|
-
arguments: args
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
/** Total number of registered tools. */
|
|
90
|
-
get toolCount() {
|
|
91
|
-
return this.tools.size;
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// src/server.ts
|
|
96
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
97
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
98
|
-
import {
|
|
99
|
-
ListToolsRequestSchema,
|
|
100
|
-
CallToolRequestSchema
|
|
101
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
102
|
-
|
|
103
|
-
// src/version.ts
|
|
104
|
-
import fs from "fs";
|
|
105
|
-
import path from "path";
|
|
106
|
-
import { fileURLToPath } from "url";
|
|
107
|
-
var FALLBACK_VERSION = "0.0.0-unknown";
|
|
108
|
-
function resolveVersion() {
|
|
109
|
-
try {
|
|
110
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
111
|
-
const parsed = JSON.parse(
|
|
112
|
-
fs.readFileSync(path.join(here, "..", "package.json"), "utf8")
|
|
113
|
-
);
|
|
114
|
-
return typeof parsed?.version === "string" && parsed.version ? parsed.version : FALLBACK_VERSION;
|
|
115
|
-
} catch (err) {
|
|
116
|
-
process.stderr.write(
|
|
117
|
-
`kojee-mcp: could not resolve version from package.json, falling back to ${FALLBACK_VERSION}: ${String(err)}
|
|
118
|
-
`
|
|
119
|
-
);
|
|
120
|
-
return FALLBACK_VERSION;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
var VERSION = resolveVersion();
|
|
124
|
-
|
|
125
|
-
// src/server.ts
|
|
126
|
-
function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
|
|
127
|
-
const intro = `Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:
|
|
128
|
-
|
|
129
|
-
(1) If channel notifications are available, you'll see them as \`<channel source="kojee-mcp" ...>\` tags in your context. Each tag carries tandem_id, message_id, from_principal, kind, severity. To respond: ${buildReplyRecipe()}.
|
|
130
|
-
|
|
131
|
-
`;
|
|
132
|
-
const monitorSection = `(2) For sustained wake-from-idle WITHOUT channel notifications, the proxy writes one event line per Tandem message to ${eventLogPath}. At the start of every session, spawn ONE Monitor task: ${buildMonitorSpawn(eventLogPath)}. Each appended line will arrive as a separate wake notification, and carries msg=<id> and cursor=<n>. To respond: ${buildReplyRecipe()}. ${buildCatchUpNote()} (\`kojee-mcp tail\` is a portable line-streamer shipped with this proxy \u2014 works on macOS, Linux, and Windows. It follows BOTH the messages log above and a status sibling; status/heartbeat telemetry never wakes you \u2014 only real messages do.)
|
|
133
|
-
|
|
134
|
-
`;
|
|
135
|
-
const listenSection = "(3) If you want to BLOCK until any single reply lands (rather than receive a stream of events), call tandem_listen(tandem_id, since=cursor, timeout_ms=N) instead.";
|
|
136
|
-
const advice = "\n\nPrefer (2) at session start \u2014 it's the default no-allowlist wake mechanism. (1) supplements it when channels are enabled; (3) is for one-shot blocking waits.";
|
|
137
|
-
return intro + monitorSection + listenSection + advice;
|
|
138
|
-
}
|
|
139
|
-
async function executeToolCall(registry, name, args, hooks) {
|
|
140
|
-
const rawResult = await registry.callTool(name, args);
|
|
141
|
-
const result = translateToolCallResult(rawResult);
|
|
142
|
-
if (name === "tandem_join" && !result.isError) {
|
|
143
|
-
try {
|
|
144
|
-
hooks?.onTandemJoin?.();
|
|
145
|
-
} catch (err) {
|
|
146
|
-
console.error("[mcp] onTandemJoin hook failed:", err?.message ?? String(err));
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return result;
|
|
150
|
-
}
|
|
151
|
-
function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath, hooks) {
|
|
152
|
-
const capabilities = { tools: {} };
|
|
153
|
-
if (adapter.supportsChannels) {
|
|
154
|
-
capabilities.experimental = { "claude/channel": {} };
|
|
155
|
-
}
|
|
156
|
-
const server = new Server(
|
|
157
|
-
{ name: "kojee-mcp", version: VERSION },
|
|
158
|
-
{
|
|
159
|
-
capabilities,
|
|
160
|
-
...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
|
|
161
|
-
}
|
|
162
|
-
);
|
|
163
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
164
|
-
const tools = registry.getAllTools().map((t) => ({
|
|
165
|
-
name: t.name,
|
|
166
|
-
description: t.description,
|
|
167
|
-
inputSchema: t.inputSchema
|
|
168
|
-
}));
|
|
169
|
-
return { tools };
|
|
170
|
-
});
|
|
171
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
172
|
-
const { name, arguments: args } = request.params;
|
|
173
|
-
const result = await executeToolCall(registry, name, args ?? {}, hooks);
|
|
174
|
-
return { content: result.content, isError: result.isError };
|
|
175
|
-
});
|
|
176
|
-
return server;
|
|
177
|
-
}
|
|
178
|
-
async function startMcpServer(server) {
|
|
179
|
-
const transport = new StdioServerTransport();
|
|
180
|
-
await server.connect(transport);
|
|
181
|
-
console.error("[mcp] Server started on stdio transport");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// src/runtime/detect.ts
|
|
185
|
-
import psList from "ps-list";
|
|
186
|
-
async function detectRuntime(env = process.env) {
|
|
187
|
-
if (env["KOJEE_RUNTIME"] === "claude-code") return "claude-code";
|
|
188
|
-
if (env["KOJEE_RUNTIME"] === "codex") return "codex";
|
|
189
|
-
if (env["CLAUDE_CODE_SESSION_ID"]) return "claude-code";
|
|
190
|
-
const ancestor = await detectRuntimeFromAncestry();
|
|
191
|
-
if (ancestor) return ancestor;
|
|
192
|
-
return "unknown";
|
|
193
|
-
}
|
|
194
|
-
async function detectRuntimeFromAncestry() {
|
|
195
|
-
try {
|
|
196
|
-
const processes = await psList();
|
|
197
|
-
const byPid = new Map(processes.map((p) => [p.pid, p]));
|
|
198
|
-
let pid = process.ppid;
|
|
199
|
-
for (let depth = 0; depth < 5 && pid && pid > 1; depth++) {
|
|
200
|
-
const row = byPid.get(pid);
|
|
201
|
-
if (!row) return null;
|
|
202
|
-
const haystack = `${row.name} ${row.cmd ?? ""}`;
|
|
203
|
-
if (/(^|\/|\\)claude(\.app|\.exe)?(\/|$|\s|\\)/i.test(haystack) || /Claude Helper/i.test(haystack)) {
|
|
204
|
-
return "claude-code";
|
|
205
|
-
}
|
|
206
|
-
if (/(^|\/|\\)codex(\.exe)?(\/|$|\s|\\)/i.test(haystack)) {
|
|
207
|
-
return "codex";
|
|
208
|
-
}
|
|
209
|
-
pid = row.ppid;
|
|
210
|
-
}
|
|
211
|
-
} catch {
|
|
212
|
-
}
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// src/adapters/claude-code.ts
|
|
217
|
-
function computeSeverity(event) {
|
|
218
|
-
if (event.type === "state_change") return "high";
|
|
219
|
-
if (event.mentions && event.mentions.length > 0) {
|
|
220
|
-
return "high";
|
|
221
|
-
}
|
|
222
|
-
return "normal";
|
|
223
|
-
}
|
|
224
|
-
function formatBody(event) {
|
|
225
|
-
if (event.type === "state_change") {
|
|
226
|
-
return `[Tandem: ${event.tandem_id}] ${event.from.displayname}: ${event.content.body}`;
|
|
227
|
-
}
|
|
228
|
-
return [
|
|
229
|
-
`[Tandem: ${event.tandem_id}] ${event.from.displayname} (${event.from.principal}) \u2014 ${event.kind}:`,
|
|
230
|
-
event.content.body,
|
|
231
|
-
"",
|
|
232
|
-
`> ${buildReplyRecipe({ tandem_id: event.tandem_id, message_id: event.id })}`
|
|
233
|
-
].join("\n");
|
|
234
|
-
}
|
|
235
|
-
var claudeCodeAdapter = {
|
|
236
|
-
runtime: "claude-code",
|
|
237
|
-
supportsChannels: true,
|
|
238
|
-
formatTandemEvent(event) {
|
|
239
|
-
const meta = {
|
|
240
|
-
tandem_id: event.tandem_id,
|
|
241
|
-
message_id: event.id,
|
|
242
|
-
cursor: String(event.cursor),
|
|
243
|
-
kind: event.kind,
|
|
244
|
-
from_principal: event.from.principal,
|
|
245
|
-
from_display: event.from.displayname,
|
|
246
|
-
severity: computeSeverity(event)
|
|
247
|
-
};
|
|
248
|
-
if (event.wake_reason) meta.wake_reason = event.wake_reason;
|
|
249
|
-
return { content: formatBody(event), meta };
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
// src/adapters/codex.ts
|
|
254
|
-
var codexAdapter = {
|
|
255
|
-
runtime: "codex",
|
|
256
|
-
supportsChannels: false,
|
|
257
|
-
// Codex has NO Claude-style channel injection
|
|
258
|
-
formatTandemEvent() {
|
|
259
|
-
throw new Error(
|
|
260
|
-
"codexAdapter.formatTandemEvent() is unreachable \u2014 Codex has no channel injection; server.ts gates this on supportsChannels. Codex receives events via the webhook sink + a model-chosen bounded tandem_listen."
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
// src/adapters/unknown.ts
|
|
266
|
-
var unknownAdapter = {
|
|
267
|
-
runtime: "unknown",
|
|
268
|
-
supportsChannels: false,
|
|
269
|
-
formatTandemEvent() {
|
|
270
|
-
throw new Error(
|
|
271
|
-
"unknownAdapter.formatTandemEvent() is unreachable \u2014 caller should gate on supportsChannels"
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
// src/index.ts
|
|
277
|
-
var DEFAULT_KEYSTORE_PATH = path2.join(os.homedir(), ".kojee", "keypair.json");
|
|
278
|
-
function isDPoPEnrollmentError(err) {
|
|
279
|
-
const msg = String(err?.message ?? err ?? "").toLowerCase();
|
|
280
|
-
if (msg.includes("invalid or expired") && msg.includes("token")) return false;
|
|
281
|
-
if (msg.includes("generate a new")) return false;
|
|
282
|
-
return msg.includes("401") || msg.includes("authentication failed") || msg.includes("dpop") || msg.includes("key enrollment") || msg.includes("kid");
|
|
283
|
-
}
|
|
284
|
-
async function selectAdapter() {
|
|
285
|
-
const runtime = await detectRuntime();
|
|
286
|
-
if (runtime === "claude-code") return claudeCodeAdapter;
|
|
287
|
-
if (runtime === "codex") return codexAdapter;
|
|
288
|
-
return unknownAdapter;
|
|
289
|
-
}
|
|
290
|
-
async function listTandemIds(gateway) {
|
|
291
|
-
const result = await gateway.sendRpc("tools/call", { name: "tandem_list", arguments: {} });
|
|
292
|
-
const maybeErr = result;
|
|
293
|
-
if (maybeErr.isError) return null;
|
|
294
|
-
const text = maybeErr.content?.[0]?.text;
|
|
295
|
-
try {
|
|
296
|
-
const parsed = text ? JSON.parse(text) : {};
|
|
297
|
-
const list = Array.isArray(parsed.tandems) ? parsed.tandems : Array.isArray(parsed) ? parsed : null;
|
|
298
|
-
if (!Array.isArray(list)) return [];
|
|
299
|
-
return list.map((t) => {
|
|
300
|
-
if (typeof t === "string") return t;
|
|
301
|
-
const obj = t;
|
|
302
|
-
if (obj?.my_membership?.is_member !== true) return void 0;
|
|
303
|
-
return obj?.tandem_id ?? obj?.id;
|
|
304
|
-
}).filter((id) => typeof id === "string" && id.length > 0);
|
|
305
|
-
} catch {
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function needsWebhookEventStream() {
|
|
310
|
-
return (process.env["KOJEE_WEBHOOK_URL"] ?? "").trim().length > 0;
|
|
311
|
-
}
|
|
312
|
-
async function startProxy(config) {
|
|
313
|
-
const keystorePath = config.keystorePath || DEFAULT_KEYSTORE_PATH;
|
|
314
|
-
const adapter = await selectAdapter();
|
|
315
|
-
console.error(`[kojee-mcp] Starting proxy for ${config.url} (runtime=${adapter.runtime})`);
|
|
316
|
-
const { registry, gateway } = await enrollAndDiscover(config, keystorePath);
|
|
317
|
-
console.error(
|
|
318
|
-
`[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
|
|
319
|
-
);
|
|
320
|
-
let activeStreamHandle = null;
|
|
321
|
-
const { createJoinReconnectScheduler } = await import("./reconnect-scheduler-ARV6JIWK.js");
|
|
322
|
-
const joinReconnect = createJoinReconnectScheduler({
|
|
323
|
-
// BOOT-RACE (Bug B): report whether the stream handle was actually ready.
|
|
324
|
-
// `false` ⇒ activeStreamHandle is still null (tandem_join fired before the
|
|
325
|
-
// stream was set up) → the scheduler queues the reconnect and flushes it on
|
|
326
|
-
// notifyReady() once the handle is assigned (see below), instead of silently
|
|
327
|
-
// dropping it as the old `activeStreamHandle?.reconnect()` no-op did.
|
|
328
|
-
reconnect: () => {
|
|
329
|
-
if (!activeStreamHandle) return false;
|
|
330
|
-
activeStreamHandle.reconnect();
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
const onTandemJoin = () => joinReconnect.requestReconnect();
|
|
335
|
-
const teardownSteps = [];
|
|
336
|
-
let shuttingDown = false;
|
|
337
|
-
function shutdown(reason) {
|
|
338
|
-
if (shuttingDown) return;
|
|
339
|
-
shuttingDown = true;
|
|
340
|
-
activeStreamHandle?.();
|
|
341
|
-
for (const step of teardownSteps) {
|
|
342
|
-
try {
|
|
343
|
-
const maybe = step();
|
|
344
|
-
if (maybe && typeof maybe.catch === "function") {
|
|
345
|
-
maybe.catch((err) => {
|
|
346
|
-
console.error("[kojee-mcp] async shutdown step failed:", err?.message ?? err);
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
} catch (err) {
|
|
350
|
-
console.error("[kojee-mcp] shutdown step failed:", err?.message ?? err);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
console.error(`[kojee-mcp] shutting down (${reason}), exiting`);
|
|
354
|
-
process.exit(0);
|
|
355
|
-
}
|
|
356
|
-
const ccPid = await findClaudeAncestorPid();
|
|
357
|
-
const { ensureJoinTandems } = await import("./ensure-join-7AEDJMPE.js");
|
|
358
|
-
await ensureJoinTandems({
|
|
359
|
-
gateway,
|
|
360
|
-
env: process.env["KOJEE_TANDEMS"],
|
|
361
|
-
listTandems: () => listTandemIds(gateway),
|
|
362
|
-
onJoined: () => joinReconnect.requestReconnect()
|
|
363
|
-
});
|
|
364
|
-
let tandemMembershipCount = -1;
|
|
365
|
-
try {
|
|
366
|
-
const bootIds = await listTandemIds(gateway);
|
|
367
|
-
tandemMembershipCount = bootIds === null ? -1 : bootIds.length;
|
|
368
|
-
} catch (err) {
|
|
369
|
-
console.error("[kojee-mcp] tandem_list probe failed:", err.message);
|
|
370
|
-
}
|
|
371
|
-
console.error(`[kojee-mcp] Tandem memberships: ${tandemMembershipCount === -1 ? "unknown" : tandemMembershipCount}`);
|
|
372
|
-
let server;
|
|
373
|
-
if (adapter.supportsChannels) {
|
|
374
|
-
const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
|
|
375
|
-
const { startHookServer } = await import("./hook-server-37E2LUKJ.js");
|
|
376
|
-
const {
|
|
377
|
-
writeDiscoveryByKey,
|
|
378
|
-
cleanupDiscoveryByKey,
|
|
379
|
-
sweepStaleDiscovery
|
|
380
|
-
} = await import("./session-discovery-FNMJGFPM.js");
|
|
381
|
-
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-B27VVEMK.js");
|
|
382
|
-
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
|
383
|
-
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
384
|
-
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
385
|
-
sweepStaleDiscovery();
|
|
386
|
-
sweepStaleEventLogs();
|
|
387
|
-
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
388
|
-
const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
|
|
389
|
-
const eventLog = startEventLog({ key: discoveryKey });
|
|
390
|
-
const webhookResolution = resolveWebhookConfig();
|
|
391
|
-
if (webhookResolution.error) {
|
|
392
|
-
console.error(`[kojee-mcp] webhook sink ERROR: ${webhookResolution.error}`);
|
|
393
|
-
void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
if (webhookResolution.warning) {
|
|
397
|
-
console.error(`[kojee-mcp] webhook sink WARNING: ${webhookResolution.warning}`);
|
|
398
|
-
void eventLog.appendStatus(`status=webhook warning="${webhookResolution.warning}"`).catch(() => {
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
|
|
402
|
-
// Route delivery/failure observability to the STATUS sink.
|
|
403
|
-
log: (line) => {
|
|
404
|
-
void eventLog.appendStatus(`status=webhook ${line}`).catch(() => {
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
}) : null;
|
|
408
|
-
if (webhookSink) {
|
|
409
|
-
console.error(`[kojee-mcp] webhook sink ENABLED (${webhookSink.configSummary()})`);
|
|
410
|
-
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path, {
|
|
414
|
-
onTandemJoin
|
|
415
|
-
});
|
|
416
|
-
const { issueControlToken, controlTokenPath } = await import("./control-token-4BUCTYQB.js");
|
|
417
|
-
let controlToken = null;
|
|
418
|
-
try {
|
|
419
|
-
controlToken = issueControlToken();
|
|
420
|
-
} catch (err) {
|
|
421
|
-
console.error(
|
|
422
|
-
"[kojee-mcp] control token write failed \u2014 POST /send disabled; GET /poll and /status left UNGATED (degrade open):",
|
|
423
|
-
err.message
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
const queue = new EventQueue();
|
|
427
|
-
let streamHandle = null;
|
|
428
|
-
const hookServer = await startHookServer({
|
|
429
|
-
port: 0,
|
|
430
|
-
queue,
|
|
431
|
-
adapter,
|
|
432
|
-
// 0.5.4 hardening: the same bearer gates POST /send AND the data-bearing
|
|
433
|
-
// reads (GET /poll, GET /status). When token issuance failed both stay
|
|
434
|
-
// available-but-degraded: /send answers 503, the reads stay open.
|
|
435
|
-
...controlToken !== null ? { controlToken, send: { gateway, authToken: controlToken } } : {},
|
|
436
|
-
getStreamState: () => streamHandle ? streamHandle.getState() : {
|
|
437
|
-
connected: false,
|
|
438
|
-
connectedSince: null,
|
|
439
|
-
lastEventAt: null,
|
|
440
|
-
lastHeartbeatAt: null,
|
|
441
|
-
cursors: {},
|
|
442
|
-
reconnectCount: 0,
|
|
443
|
-
// Adaptive: unknown until the watchdog observes ≥2 heartbeats.
|
|
444
|
-
staleAfterMs: null
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
writeDiscoveryByKey(discoveryKey, {
|
|
448
|
-
schema: 2,
|
|
449
|
-
discoveryKey,
|
|
450
|
-
ccPid,
|
|
451
|
-
projectDir: projectDir ?? null,
|
|
452
|
-
proxyPid: process.pid,
|
|
453
|
-
// Legacy `pid` mirrors `proxyPid` so the existing event-log sweep
|
|
454
|
-
// (which reads `data.pid`) still considers this entry live.
|
|
455
|
-
pid: process.pid,
|
|
456
|
-
port: hookServer.port,
|
|
457
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
458
|
-
brokerUrl: config.url,
|
|
459
|
-
eventLogPath: eventLog.path,
|
|
460
|
-
// Advertise where the POST /send bearer lives so local consumers
|
|
461
|
-
// (native gateway plugins) can find it without guessing ~/.kojee.
|
|
462
|
-
...controlToken !== null ? { controlTokenPath: controlTokenPath() } : {},
|
|
463
|
-
// Stamp the auth mode so `kojee-mcp doctor` renders the pairing check
|
|
464
|
-
// honestly: a token-mode box has no ~/.kojee/config.json by design and
|
|
465
|
-
// must not hard-fail on "paired config: MISSING". Defaults to "paired"
|
|
466
|
-
// for back-compat with callers that don't set it.
|
|
467
|
-
authMode: config.authMode ?? "paired"
|
|
468
|
-
});
|
|
469
|
-
const cleanupDiscoveryFile = () => cleanupDiscoveryByKey(discoveryKey);
|
|
470
|
-
process.on("exit", () => {
|
|
471
|
-
cleanupDiscoveryFile();
|
|
472
|
-
eventLog.cleanup();
|
|
473
|
-
});
|
|
474
|
-
teardownSteps.push(() => {
|
|
475
|
-
void webhookSink?.stop();
|
|
476
|
-
});
|
|
477
|
-
teardownSteps.push(() => cleanupDiscoveryFile());
|
|
478
|
-
teardownSteps.push(() => eventLog.cleanup());
|
|
479
|
-
teardownSteps.push(() => hookServer.stop());
|
|
480
|
-
streamHandle = await startEventStream({
|
|
481
|
-
brokerUrl: config.url,
|
|
482
|
-
token: config.token,
|
|
483
|
-
gateway,
|
|
484
|
-
adapter,
|
|
485
|
-
server,
|
|
486
|
-
queue,
|
|
487
|
-
eventLog,
|
|
488
|
-
// Generic webhook sink (null unless KOJEE_WEBHOOK_URL + _SECRET are set).
|
|
489
|
-
// Wired LAST in the fan-out, fire-and-forget — can't delay a wake.
|
|
490
|
-
...webhookSink ? { webhookSink } : {},
|
|
491
|
-
// Resubscribe-on-start (P0 #2): touch all memberships + write a
|
|
492
|
-
// `status=subscribed n=<count>` line on every (re)connect, so a backend
|
|
493
|
-
// restart / scope reset self-heals and the log is never ambiguously
|
|
494
|
-
// empty. See resubscribe.ts for the unverified-touch caveat.
|
|
495
|
-
//
|
|
496
|
-
// MINOR 6: `listTandems` re-fetches the membership list per reconnect (a
|
|
497
|
-
// mid-session join is touched next reconnect, not boot-frozen), each
|
|
498
|
-
// touch is timeout-bounded + run with bounded concurrency, and the whole
|
|
499
|
-
// routine runs concurrently with consumeSse (never blocks first-event
|
|
500
|
-
// delivery). `listTandemIds` may return null (unknown) → treat as empty.
|
|
501
|
-
// MINOR E: a shared debounce cursor damps a connect/drop flap storm — a
|
|
502
|
-
// resubscribe within 30s of the last successful one is skipped.
|
|
503
|
-
onConnected: /* @__PURE__ */ (() => {
|
|
504
|
-
const debounceState = { lastRunAt: 0 };
|
|
505
|
-
return async () => {
|
|
506
|
-
await resubscribeMemberships({
|
|
507
|
-
gateway,
|
|
508
|
-
eventLog,
|
|
509
|
-
listTandems: async () => await listTandemIds(gateway) ?? [],
|
|
510
|
-
debounceState
|
|
511
|
-
});
|
|
512
|
-
};
|
|
513
|
-
})()
|
|
514
|
-
});
|
|
515
|
-
activeStreamHandle = streamHandle;
|
|
516
|
-
joinReconnect.notifyReady();
|
|
517
|
-
} else if (needsWebhookEventStream()) {
|
|
518
|
-
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-B27VVEMK.js");
|
|
519
|
-
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
520
|
-
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
521
|
-
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
|
522
|
-
sweepStaleEventLogs();
|
|
523
|
-
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
524
|
-
const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
|
|
525
|
-
const eventLog = startEventLog({ key: discoveryKey });
|
|
526
|
-
const webhookResolution = resolveWebhookConfig();
|
|
527
|
-
if (webhookResolution.error) {
|
|
528
|
-
console.error(`[kojee-mcp] webhook sink ERROR: ${webhookResolution.error}`);
|
|
529
|
-
void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
if (webhookResolution.warning) {
|
|
533
|
-
console.error(`[kojee-mcp] webhook sink WARNING: ${webhookResolution.warning}`);
|
|
534
|
-
void eventLog.appendStatus(`status=webhook warning="${webhookResolution.warning}"`).catch(() => {
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
|
|
538
|
-
log: (line) => {
|
|
539
|
-
void eventLog.appendStatus(`status=webhook ${line}`).catch(() => {
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
}) : null;
|
|
543
|
-
if (webhookSink) {
|
|
544
|
-
console.error(`[kojee-mcp] webhook sink ENABLED (${webhookSink.configSummary()})`);
|
|
545
|
-
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
server = createMcpServer(registry, adapter, tandemMembershipCount, void 0, {
|
|
549
|
-
onTandemJoin
|
|
550
|
-
});
|
|
551
|
-
process.on("exit", () => eventLog.cleanup());
|
|
552
|
-
teardownSteps.push(() => {
|
|
553
|
-
void webhookSink?.stop();
|
|
554
|
-
});
|
|
555
|
-
teardownSteps.push(() => eventLog.cleanup());
|
|
556
|
-
const streamHandle = await startEventStream({
|
|
557
|
-
brokerUrl: config.url,
|
|
558
|
-
token: config.token,
|
|
559
|
-
gateway,
|
|
560
|
-
adapter,
|
|
561
|
-
server,
|
|
562
|
-
eventLog,
|
|
563
|
-
...webhookSink ? { webhookSink } : {},
|
|
564
|
-
onConnected: /* @__PURE__ */ (() => {
|
|
565
|
-
const debounceState = { lastRunAt: 0 };
|
|
566
|
-
return async () => {
|
|
567
|
-
await resubscribeMemberships({
|
|
568
|
-
gateway,
|
|
569
|
-
eventLog,
|
|
570
|
-
listTandems: async () => await listTandemIds(gateway) ?? [],
|
|
571
|
-
debounceState
|
|
572
|
-
});
|
|
573
|
-
};
|
|
574
|
-
})()
|
|
575
|
-
});
|
|
576
|
-
activeStreamHandle = streamHandle;
|
|
577
|
-
joinReconnect.notifyReady();
|
|
578
|
-
} else {
|
|
579
|
-
server = createMcpServer(registry, adapter, tandemMembershipCount);
|
|
580
|
-
}
|
|
581
|
-
process.stdin.on("end", () => shutdown("stdin end"));
|
|
582
|
-
process.stdin.on("close", () => shutdown("stdin close"));
|
|
583
|
-
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
584
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
585
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
586
|
-
if (ccPid !== null) {
|
|
587
|
-
const { createParentWatchdog } = await import("./parent-watchdog-RZLHYP7T.js");
|
|
588
|
-
const watchdog = createParentWatchdog({
|
|
589
|
-
ccPid,
|
|
590
|
-
onParentGone: () => shutdown("parent (Claude Code) gone")
|
|
591
|
-
});
|
|
592
|
-
watchdog.start();
|
|
593
|
-
teardownSteps.push(() => watchdog.stop());
|
|
594
|
-
} else {
|
|
595
|
-
console.error(
|
|
596
|
-
"[kojee-mcp] no Claude Code ancestor found \u2014 parent-liveness watchdog NOT armed (stdin/signal handlers still cover clean exits)"
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
await startMcpServer(server);
|
|
600
|
-
}
|
|
601
|
-
async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
602
|
-
const auth = new AuthModule(config.token, config.url, keystorePath);
|
|
603
|
-
const keyPair = await auth.ensureEnrolled();
|
|
604
|
-
const sessionId = GatewayClient.deriveSessionId(config.token);
|
|
605
|
-
console.error(`[kojee-mcp] Session: ${sessionId}`);
|
|
606
|
-
const gateway = new GatewayClient(
|
|
607
|
-
config.url,
|
|
608
|
-
config.token,
|
|
609
|
-
keyPair.privateKey,
|
|
610
|
-
keyPair.kid,
|
|
611
|
-
sessionId
|
|
612
|
-
);
|
|
613
|
-
const registry = new ToolRegistry(gateway);
|
|
614
|
-
try {
|
|
615
|
-
await registry.discoverTools();
|
|
616
|
-
return { registry, gateway };
|
|
617
|
-
} catch (err) {
|
|
618
|
-
if (isRetry || !isDPoPEnrollmentError(err)) {
|
|
619
|
-
throw err;
|
|
620
|
-
}
|
|
621
|
-
console.error(
|
|
622
|
-
"[kojee-mcp] Auth failed, attempting recovery with fresh enrollment..."
|
|
623
|
-
);
|
|
624
|
-
try {
|
|
625
|
-
if (fs2.existsSync(keystorePath)) fs2.unlinkSync(keystorePath);
|
|
626
|
-
} catch (unlinkErr) {
|
|
627
|
-
console.error("[kojee-mcp] Could not remove stale keystore:", unlinkErr);
|
|
628
|
-
}
|
|
629
|
-
return enrollAndDiscover(config, keystorePath, true);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
export {
|
|
634
|
-
VERSION,
|
|
635
|
-
listTandemIds,
|
|
636
|
-
startProxy
|
|
637
|
-
};
|
|
File without changes
|
|
@@ -6,10 +6,10 @@ import "./chunk-SQL56SEB.js";
|
|
|
6
6
|
import {
|
|
7
7
|
resolveWebhookConfig
|
|
8
8
|
} from "./chunk-V5VZPYMZ.js";
|
|
9
|
+
import "./chunk-BLEGIR35.js";
|
|
9
10
|
import {
|
|
10
11
|
CODEX_LISTEN_CAP_MS
|
|
11
12
|
} from "./chunk-X672ZN7V.js";
|
|
12
|
-
import "./chunk-BLEGIR35.js";
|
|
13
13
|
|
|
14
14
|
// src/doctor-codex.ts
|
|
15
15
|
import fs from "fs";
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executeSend,
|
|
3
|
+
parseSendRequest,
|
|
4
|
+
sendFailure
|
|
5
|
+
} from "./chunk-HIZ4NDWN.js";
|
|
1
6
|
import {
|
|
2
7
|
loadPairedConfig
|
|
3
8
|
} from "./chunk-YH27B6SW.js";
|
|
@@ -9,11 +14,6 @@ import {
|
|
|
9
14
|
loadKeystore
|
|
10
15
|
} from "./chunk-CH32ELFX.js";
|
|
11
16
|
import "./chunk-BLEGIR35.js";
|
|
12
|
-
import {
|
|
13
|
-
executeSend,
|
|
14
|
-
parseSendRequest,
|
|
15
|
-
sendFailure
|
|
16
|
-
} from "./chunk-HIZ4NDWN.js";
|
|
17
17
|
import "./chunk-LDZXU3DW.js";
|
|
18
18
|
|
|
19
19
|
// src/tandem/send-cli.ts
|