kojee-mcp 0.5.4 → 0.5.7
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 -5
- package/dist/{chunk-36L3GCU3.js → chunk-3XDJOHMZ.js} +12 -2
- package/dist/chunk-6SK6ITFE.js +142 -0
- package/dist/{chunk-62KH6VNQ.js → chunk-GATXJ6UT.js} +122 -190
- package/dist/{control-token-TYDAL477.js → chunk-GI2CKKBL.js} +13 -2
- package/dist/{resubscribe-SLZNA76S.js → chunk-OT2GILXC.js} +1 -0
- package/dist/{chunk-YVUXQ4Z2.js → chunk-UEGQGXPY.js} +53 -8
- package/dist/{chunk-OSKHA5DS.js → chunk-V5VZPYMZ.js} +2 -2
- package/dist/cli.js +19 -24
- package/dist/control-token-4BUCTYQB.js +13 -0
- package/dist/{doctor-TXWMMSRC.js → doctor-QCQDFLEH.js} +29 -16
- package/dist/{doctor-codex-3A7KYOVX.js → doctor-codex-NZ53ROQA.js} +3 -3
- package/dist/ensure-join-7AEDJMPE.js +96 -0
- package/dist/gateway-client-93P1E0CZ.d.ts +92 -0
- package/dist/{hook-server-NDJSV22J.js → hook-server-37E2LUKJ.js} +6 -0
- package/dist/index.d.ts +18 -15
- package/dist/index.js +7 -4
- package/dist/lib.d.ts +427 -0
- package/dist/lib.js +44 -0
- package/dist/parent-watchdog-RZLHYP7T.js +65 -0
- package/dist/reconnect-scheduler-ARV6JIWK.js +36 -0
- package/dist/resubscribe-G5OGDZJD.js +6 -0
- package/dist/{send-cli-7QJ36YY7.js → send-cli-C2F4WTBN.js} +1 -1
- package/dist/{stop-hook-GO363SMD.js → stop-hook-46BJD55B.js} +15 -7
- package/dist/{tail-stream-U436QL2X.js → tail-stream-VUZBYKXS.js} +4 -4
- package/dist/{user-prompt-submit-hook-ARPEO6FF.js → user-prompt-submit-hook-ZD2XKN7U.js} +7 -1
- package/dist/{webhook-config-UKUSI2FE.js → webhook-config-O4WMQ532.js} +1 -1
- package/dist/{webhook-sink-GCLL2S6S.js → webhook-sink-NWGCUDGY.js} +17 -3
- package/dist/{wizard-Z5JA3YPV.js → wizard-UOXQYJLP.js} +7 -7
- package/package.json +11 -2
|
@@ -16,7 +16,12 @@ function issueControlToken(filePath = controlTokenPath()) {
|
|
|
16
16
|
const dir = path.dirname(filePath);
|
|
17
17
|
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
18
18
|
secureDir(dir);
|
|
19
|
-
|
|
19
|
+
try {
|
|
20
|
+
fs.unlinkSync(filePath);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.code !== "ENOENT") throw err;
|
|
23
|
+
}
|
|
24
|
+
fs.writeFileSync(filePath, token + "\n", { mode: 384, flag: "wx" });
|
|
20
25
|
secureFile(filePath);
|
|
21
26
|
return token;
|
|
22
27
|
}
|
|
@@ -28,8 +33,14 @@ function loadControlToken(filePath = controlTokenPath()) {
|
|
|
28
33
|
return null;
|
|
29
34
|
}
|
|
30
35
|
}
|
|
36
|
+
function controlTokenAuthHeaders(filePath) {
|
|
37
|
+
const token = loadControlToken(filePath);
|
|
38
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
export {
|
|
32
42
|
controlTokenPath,
|
|
33
43
|
issueControlToken,
|
|
34
|
-
loadControlToken
|
|
44
|
+
loadControlToken,
|
|
45
|
+
controlTokenAuthHeaders
|
|
35
46
|
};
|
|
@@ -125,6 +125,13 @@ async function startEventStream(opts) {
|
|
|
125
125
|
state.connected = false;
|
|
126
126
|
currentController.abort();
|
|
127
127
|
});
|
|
128
|
+
handle.reconnect = () => {
|
|
129
|
+
if (stopped) return;
|
|
130
|
+
console.error(
|
|
131
|
+
"[event-stream] reconnect requested (membership change) \u2014 closing current connection"
|
|
132
|
+
);
|
|
133
|
+
currentController.abort();
|
|
134
|
+
};
|
|
128
135
|
handle.getState = () => ({
|
|
129
136
|
connected: state.connected,
|
|
130
137
|
connectedSince: state.connectedSince,
|
|
@@ -298,26 +305,62 @@ function sleep(ms) {
|
|
|
298
305
|
return new Promise((r) => setTimeout(r, ms));
|
|
299
306
|
}
|
|
300
307
|
var MAX_DISPLAYNAME_CHARS = 64;
|
|
308
|
+
var MAX_COMBINING_RUN = 4;
|
|
309
|
+
var STRIP_CATEGORIES_RE = /[\p{Cc}\p{Cf}\p{Cs}\p{Co}\p{Cn}]/u;
|
|
310
|
+
var INVISIBLE_MARKS_RE = /[\u034f\ufe00-\ufe0f\u{e0100}-\u{e01ef}]/u;
|
|
311
|
+
var COMBINING_MARK_RE = /\p{Mn}/u;
|
|
301
312
|
function sanitizeDisplayname(name) {
|
|
302
|
-
|
|
313
|
+
const normalized = name.normalize("NFC");
|
|
314
|
+
let kept = "";
|
|
315
|
+
let combiningRun = 0;
|
|
316
|
+
for (const ch of normalized) {
|
|
317
|
+
if (STRIP_CATEGORIES_RE.test(ch) || INVISIBLE_MARKS_RE.test(ch)) continue;
|
|
318
|
+
if (COMBINING_MARK_RE.test(ch)) {
|
|
319
|
+
combiningRun += 1;
|
|
320
|
+
if (combiningRun > MAX_COMBINING_RUN) continue;
|
|
321
|
+
} else {
|
|
322
|
+
combiningRun = 0;
|
|
323
|
+
}
|
|
324
|
+
kept += ch;
|
|
325
|
+
}
|
|
326
|
+
const collapsed = kept.split(/\s+/).filter(Boolean).join(" ");
|
|
327
|
+
let capped = "";
|
|
328
|
+
for (const ch of collapsed) {
|
|
329
|
+
if (capped.length + ch.length > MAX_DISPLAYNAME_CHARS) break;
|
|
330
|
+
capped += ch;
|
|
331
|
+
}
|
|
332
|
+
return capped.trimEnd();
|
|
333
|
+
}
|
|
334
|
+
function resolveDisplayname(rawDisplay, principal) {
|
|
335
|
+
const trimmedDisplay = typeof rawDisplay === "string" ? rawDisplay.trim() : "";
|
|
336
|
+
const safeDisplay = trimmedDisplay ? sanitizeDisplayname(trimmedDisplay) : "";
|
|
337
|
+
if (safeDisplay) return safeDisplay;
|
|
338
|
+
if (!principal) return "unknown";
|
|
339
|
+
return `principal:${Array.from(principal).slice(0, 8).join("")}`;
|
|
303
340
|
}
|
|
304
341
|
function normalizeBackendEvent(raw, sseEventType) {
|
|
305
342
|
const obj = raw ?? {};
|
|
306
343
|
const maybeFrom = obj["from"];
|
|
307
344
|
if (maybeFrom && typeof maybeFrom["principal"] === "string") {
|
|
308
|
-
|
|
345
|
+
const canonical = raw;
|
|
346
|
+
const canonicalPrincipal = sanitizeDisplayname(maybeFrom["principal"]);
|
|
347
|
+
return {
|
|
348
|
+
...canonical,
|
|
349
|
+
from: {
|
|
350
|
+
...canonical.from,
|
|
351
|
+
principal: canonicalPrincipal,
|
|
352
|
+
displayname: resolveDisplayname(maybeFrom["displayname"], canonicalPrincipal)
|
|
353
|
+
}
|
|
354
|
+
};
|
|
309
355
|
}
|
|
310
356
|
const sender = obj["sender"] ?? {};
|
|
311
|
-
const principal = sender["principal_id"] ?? "";
|
|
357
|
+
const principal = sanitizeDisplayname(sender["principal_id"] ?? "");
|
|
312
358
|
const agentId = sender["agent_id"];
|
|
313
359
|
const rawSessionId = sender["session_id"];
|
|
314
360
|
const sessionId = typeof rawSessionId === "string" && rawSessionId.trim() ? rawSessionId : void 0;
|
|
315
361
|
const rawSeverity = obj["severity"];
|
|
316
362
|
const severity = typeof rawSeverity === "string" && rawSeverity.trim() ? rawSeverity : void 0;
|
|
317
|
-
const
|
|
318
|
-
const trimmedDisplay = typeof rawDisplay === "string" ? rawDisplay.trim() : "";
|
|
319
|
-
const safeDisplay = trimmedDisplay ? sanitizeDisplayname(trimmedDisplay) : "";
|
|
320
|
-
const displayname = safeDisplay ? safeDisplay : principal ? `principal:${principal.slice(0, 8)}` : "unknown";
|
|
363
|
+
const displayname = resolveDisplayname(sender["display"], principal);
|
|
321
364
|
const type = sseEventType === "state_change" ? "state_change" : "message";
|
|
322
365
|
const kind = obj["kind"] ?? "message";
|
|
323
366
|
return {
|
|
@@ -346,5 +389,7 @@ function normalizeBackendEvent(raw, sseEventType) {
|
|
|
346
389
|
|
|
347
390
|
export {
|
|
348
391
|
createAdaptiveWatchdog,
|
|
349
|
-
startEventStream
|
|
392
|
+
startEventStream,
|
|
393
|
+
sanitizeDisplayname,
|
|
394
|
+
normalizeBackendEvent
|
|
350
395
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/tandem/webhook-config.ts
|
|
2
|
-
var WEBHOOK_DEFAULT_TIMEOUT_MS =
|
|
3
|
-
var WEBHOOK_DEFAULT_MAX_RETRIES =
|
|
2
|
+
var WEBHOOK_DEFAULT_TIMEOUT_MS = 3e4;
|
|
3
|
+
var WEBHOOK_DEFAULT_MAX_RETRIES = 2;
|
|
4
4
|
var WEBHOOK_DEFAULT_SIGNATURE_HEADER = "X-Kojee-Signature";
|
|
5
5
|
var WEBHOOK_DEFAULT_SIGNATURE_PREFIX = "";
|
|
6
6
|
var WEBHOOK_SIGNATURE_FORMATS = {
|
package/dist/cli.js
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
VERSION,
|
|
4
|
+
startProxy
|
|
5
|
+
} from "./chunk-GATXJ6UT.js";
|
|
6
|
+
import "./chunk-X672ZN7V.js";
|
|
7
|
+
import "./chunk-BJMASMKX.js";
|
|
2
8
|
import {
|
|
3
9
|
loadPairedConfig,
|
|
4
10
|
pairedConfigPath,
|
|
5
11
|
savePairedConfig
|
|
6
12
|
} from "./chunk-YH27B6SW.js";
|
|
7
13
|
import {
|
|
8
|
-
AuthModule
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-36L3GCU3.js";
|
|
14
|
+
AuthModule
|
|
15
|
+
} from "./chunk-6SK6ITFE.js";
|
|
16
|
+
import {
|
|
17
|
+
defaultPairedKeystorePath,
|
|
18
|
+
deriveKeystorePath
|
|
19
|
+
} from "./chunk-3XDJOHMZ.js";
|
|
20
|
+
import "./chunk-UEGQGXPY.js";
|
|
16
21
|
import "./chunk-2MIISF2W.js";
|
|
17
22
|
import "./chunk-LDZXU3DW.js";
|
|
18
23
|
import "./chunk-BLEGIR35.js";
|
|
19
24
|
|
|
20
25
|
// src/cli.ts
|
|
21
26
|
import { Command } from "commander";
|
|
22
|
-
import crypto from "crypto";
|
|
23
|
-
import os from "os";
|
|
24
27
|
import path from "path";
|
|
25
28
|
|
|
26
29
|
// src/tandem/pair.ts
|
|
@@ -63,14 +66,6 @@ async function runPair(opts) {
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
// src/cli.ts
|
|
66
|
-
var KOJEE_DIR = path.join(os.homedir(), ".kojee");
|
|
67
|
-
function deriveKeystorePath(token) {
|
|
68
|
-
const hash = crypto.createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
69
|
-
return path.join(KOJEE_DIR, `keypair-${hash}.json`);
|
|
70
|
-
}
|
|
71
|
-
function defaultPairedKeystorePath() {
|
|
72
|
-
return path.join(KOJEE_DIR, "keypair.json");
|
|
73
|
-
}
|
|
74
69
|
var program = new Command().name("kojee-mcp").description(
|
|
75
70
|
"Local MCP proxy for Kojee \u2014 handles DPoP auth, tool discovery, and governance transparently"
|
|
76
71
|
).version(VERSION).enablePositionalOptions();
|
|
@@ -88,11 +83,11 @@ program.command("pair <code>").description("Pair this machine against Kojee usin
|
|
|
88
83
|
});
|
|
89
84
|
program.command("hook").description("Run a kojee MCP hook script (called by Claude Code via ~/.claude/settings.json)").requiredOption("--type <type>", "Hook type: stop, user-prompt-submit, or codex-stop").action(async (opts) => {
|
|
90
85
|
if (opts.type === "stop") {
|
|
91
|
-
const { runStopHook } = await import("./stop-hook-
|
|
86
|
+
const { runStopHook } = await import("./stop-hook-46BJD55B.js");
|
|
92
87
|
await runStopHook();
|
|
93
88
|
process.exit(0);
|
|
94
89
|
} else if (opts.type === "user-prompt-submit") {
|
|
95
|
-
const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-
|
|
90
|
+
const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-ZD2XKN7U.js");
|
|
96
91
|
await runUserPromptSubmitHook();
|
|
97
92
|
process.exit(0);
|
|
98
93
|
} else if (opts.type === "codex-stop") {
|
|
@@ -122,7 +117,7 @@ Restart Claude Code for hooks to take effect.`
|
|
|
122
117
|
program.command("send <tandem_id>").description(
|
|
123
118
|
"Send a Tandem message using this machine's paired credentials (~/.kojee). Prints one JSON envelope to stdout: {ok, message_id, cursor, text} on success, {ok:false, error:<typed code>, message} on failure (exit 1)."
|
|
124
119
|
).requiredOption("--body <text>", "Message body (required)").option("--reply-to <message_id>", "Message id this send replies to").option("--kind <kind>", "Message kind: message | status (default: backend default)").action(async (tandemId, opts) => {
|
|
125
|
-
const { runSendCli } = await import("./send-cli-
|
|
120
|
+
const { runSendCli } = await import("./send-cli-C2F4WTBN.js");
|
|
126
121
|
const { exitCode, envelope } = await runSendCli({
|
|
127
122
|
tandemId,
|
|
128
123
|
body: opts.body,
|
|
@@ -133,7 +128,7 @@ program.command("send <tandem_id>").description(
|
|
|
133
128
|
process.exit(exitCode);
|
|
134
129
|
});
|
|
135
130
|
program.command("tail <path>").description("Stream a file's contents and follow appends (portable replacement for `tail -F`)").action(async (filePath) => {
|
|
136
|
-
const { runTail } = await import("./tail-stream-
|
|
131
|
+
const { runTail } = await import("./tail-stream-VUZBYKXS.js");
|
|
137
132
|
try {
|
|
138
133
|
await runTail(filePath);
|
|
139
134
|
} catch (err) {
|
|
@@ -142,7 +137,7 @@ program.command("tail <path>").description("Stream a file's contents and follow
|
|
|
142
137
|
}
|
|
143
138
|
});
|
|
144
139
|
program.command("doctor").description("Diagnose the kojee wake path (proxy, hook-server, SSE stream, event log, Monitor) and print the exact wake recipe").action(async () => {
|
|
145
|
-
const { runDoctor } = await import("./doctor-
|
|
140
|
+
const { runDoctor } = await import("./doctor-QCQDFLEH.js");
|
|
146
141
|
const code = await runDoctor();
|
|
147
142
|
process.exit(code);
|
|
148
143
|
});
|
|
@@ -163,7 +158,7 @@ program.command("init").description(
|
|
|
163
158
|
console.error("Not paired. Run `kojee-mcp pair <code> --url <broker>` first, then re-run `init`.");
|
|
164
159
|
process.exit(1);
|
|
165
160
|
}
|
|
166
|
-
const { runWizard } = await import("./wizard-
|
|
161
|
+
const { runWizard } = await import("./wizard-UOXQYJLP.js");
|
|
167
162
|
const interactive = process.stdin.isTTY === true && opts.runtime === void 0;
|
|
168
163
|
const result = await runWizard({
|
|
169
164
|
...opts.runtime !== void 0 ? { runtime: opts.runtime } : {},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
controlTokenAuthHeaders,
|
|
3
|
+
controlTokenPath,
|
|
4
|
+
issueControlToken,
|
|
5
|
+
loadControlToken
|
|
6
|
+
} from "./chunk-GI2CKKBL.js";
|
|
7
|
+
import "./chunk-BLEGIR35.js";
|
|
8
|
+
export {
|
|
9
|
+
controlTokenAuthHeaders,
|
|
10
|
+
controlTokenPath,
|
|
11
|
+
issueControlToken,
|
|
12
|
+
loadControlToken
|
|
13
|
+
};
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
monitorHeartbeatPath,
|
|
3
|
+
statusLogPath
|
|
4
|
+
} from "./chunk-2TUAFAIW.js";
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "./chunk-BJMASMKX.js";
|
|
6
|
+
loadControlToken
|
|
7
|
+
} from "./chunk-GI2CKKBL.js";
|
|
8
8
|
import {
|
|
9
9
|
buildMonitorSpawn,
|
|
10
10
|
buildReplyRecipe
|
|
11
11
|
} from "./chunk-X672ZN7V.js";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "./chunk-
|
|
13
|
+
deriveDiscoveryKey,
|
|
14
|
+
findClaudeAncestorPid
|
|
15
|
+
} from "./chunk-BJMASMKX.js";
|
|
16
|
+
import {
|
|
17
|
+
loadPairedConfig
|
|
18
|
+
} from "./chunk-YH27B6SW.js";
|
|
16
19
|
import {
|
|
17
20
|
discoveryPathForKey,
|
|
18
21
|
readSessionDiscoveryByKey
|
|
@@ -111,8 +114,17 @@ async function collectDoctorReport(deps = {}) {
|
|
|
111
114
|
ok: health !== null,
|
|
112
115
|
detail: health !== null ? "ok" : "unreachable"
|
|
113
116
|
});
|
|
114
|
-
const
|
|
115
|
-
|
|
117
|
+
const readControlToken = deps.readControlToken ?? loadControlToken;
|
|
118
|
+
const controlToken = readControlToken(discovery.controlTokenPath);
|
|
119
|
+
const statusHeaders = controlToken ? { Authorization: `Bearer ${controlToken}` } : {};
|
|
120
|
+
const statusProbe = await probeJsonWithStatus(fetchFn, `${base}/status`, statusHeaders);
|
|
121
|
+
if (statusProbe.json === null && statusProbe.unauthorized) {
|
|
122
|
+
checks.push({
|
|
123
|
+
name: "hook-server /status",
|
|
124
|
+
ok: "warn",
|
|
125
|
+
detail: `401 unauthorized \u2014 /status is gated by the control token (0.5.4); could not present a valid bearer from ${discovery.controlTokenPath ?? "~/.kojee/control-token"} (daemon restarted? re-run doctor; file unreadable? check perms)`
|
|
126
|
+
});
|
|
127
|
+
} else if (statusProbe.json === null && statusProbe.routeAbsent) {
|
|
116
128
|
checks.push({
|
|
117
129
|
name: "hook-server /status",
|
|
118
130
|
ok: "unknown",
|
|
@@ -187,14 +199,15 @@ async function probeJson(fetchFn, url) {
|
|
|
187
199
|
return null;
|
|
188
200
|
}
|
|
189
201
|
}
|
|
190
|
-
async function probeJsonWithStatus(fetchFn, url) {
|
|
202
|
+
async function probeJsonWithStatus(fetchFn, url, headers = {}) {
|
|
191
203
|
try {
|
|
192
|
-
const res = await fetchFn(url);
|
|
193
|
-
if (res.ok) return { json: await res.json(), routeAbsent: false };
|
|
204
|
+
const res = await fetchFn(url, { headers });
|
|
205
|
+
if (res.ok) return { json: await res.json(), routeAbsent: false, unauthorized: false };
|
|
194
206
|
const routeAbsent = res.status === 404 || res.status === 405;
|
|
195
|
-
|
|
207
|
+
const unauthorized = res.status === 401;
|
|
208
|
+
return { json: null, routeAbsent, unauthorized };
|
|
196
209
|
} catch {
|
|
197
|
-
return { json: null, routeAbsent: false };
|
|
210
|
+
return { json: null, routeAbsent: false, unauthorized: false };
|
|
198
211
|
}
|
|
199
212
|
}
|
|
200
213
|
function formatDoctorReport(report) {
|
|
@@ -220,7 +233,7 @@ function formatDoctorReport(report) {
|
|
|
220
233
|
async function runDoctor() {
|
|
221
234
|
const { readRecordedRuntime } = await import("./runtime-record-WO4IECM6.js");
|
|
222
235
|
if (readRecordedRuntime() === "codex") {
|
|
223
|
-
const { collectCodexDoctorReport, formatCodexDoctorReport } = await import("./doctor-codex-
|
|
236
|
+
const { collectCodexDoctorReport, formatCodexDoctorReport } = await import("./doctor-codex-NZ53ROQA.js");
|
|
224
237
|
const report2 = collectCodexDoctorReport();
|
|
225
238
|
console.error(formatCodexDoctorReport(report2));
|
|
226
239
|
return report2.verdict === "broken" ? 1 : 0;
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
defaultCodexHooksPath
|
|
4
4
|
} from "./chunk-64EOLZNI.js";
|
|
5
5
|
import "./chunk-SQL56SEB.js";
|
|
6
|
+
import {
|
|
7
|
+
resolveWebhookConfig
|
|
8
|
+
} from "./chunk-V5VZPYMZ.js";
|
|
6
9
|
import {
|
|
7
10
|
CODEX_LISTEN_CAP_MS
|
|
8
11
|
} from "./chunk-X672ZN7V.js";
|
|
9
12
|
import "./chunk-BLEGIR35.js";
|
|
10
|
-
import {
|
|
11
|
-
resolveWebhookConfig
|
|
12
|
-
} from "./chunk-OSKHA5DS.js";
|
|
13
13
|
|
|
14
14
|
// src/doctor-codex.ts
|
|
15
15
|
import fs from "fs";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/tandem/ensure-join.ts
|
|
2
|
+
var OBJECT_ID_RE = /^[0-9a-f]{24}$/i;
|
|
3
|
+
var DEFAULT_PER_CALL_TIMEOUT_MS = 1e4;
|
|
4
|
+
function parseTandemsConfig(raw) {
|
|
5
|
+
const trimmed = (raw ?? "").trim();
|
|
6
|
+
if (trimmed.length === 0) return { mode: "auto", ids: [], invalid: [] };
|
|
7
|
+
if (trimmed.toLowerCase() === "none") return { mode: "disabled", ids: [], invalid: [] };
|
|
8
|
+
const ids = [];
|
|
9
|
+
const invalid = [];
|
|
10
|
+
for (const entry of trimmed.split(/[\s,]+/)) {
|
|
11
|
+
if (entry.length === 0) continue;
|
|
12
|
+
(OBJECT_ID_RE.test(entry) ? ids : invalid).push(entry);
|
|
13
|
+
}
|
|
14
|
+
return { mode: "explicit", ids, invalid };
|
|
15
|
+
}
|
|
16
|
+
async function ensureJoinTandems(opts) {
|
|
17
|
+
const log = opts.log ?? ((line) => console.error(line));
|
|
18
|
+
const perCallTimeoutMs = opts.perCallTimeoutMs ?? DEFAULT_PER_CALL_TIMEOUT_MS;
|
|
19
|
+
const config = parseTandemsConfig(opts.env);
|
|
20
|
+
const result = { mode: config.mode, joined: [], already: [], failed: [] };
|
|
21
|
+
if (config.mode === "disabled") {
|
|
22
|
+
log("[ensure-join] disabled (KOJEE_TANDEMS=none)");
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
for (const bad of config.invalid) {
|
|
26
|
+
log(`[ensure-join] skipping invalid tandem id "${bad}" (not a 24-hex ObjectId)`);
|
|
27
|
+
}
|
|
28
|
+
let ids;
|
|
29
|
+
if (config.mode === "explicit") {
|
|
30
|
+
ids = config.ids;
|
|
31
|
+
log(`[ensure-join] mode=explicit n=${ids.length} (KOJEE_TANDEMS)`);
|
|
32
|
+
} else {
|
|
33
|
+
let listed = null;
|
|
34
|
+
try {
|
|
35
|
+
listed = opts.listTandems ? await opts.listTandems() : null;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
log(`[ensure-join] tandem_list threw: ${err.message}`);
|
|
38
|
+
listed = null;
|
|
39
|
+
}
|
|
40
|
+
if (listed === null) {
|
|
41
|
+
log(
|
|
42
|
+
"[ensure-join] tandem_list failed \u2014 cannot auto re-seat this session (set KOJEE_TANDEMS=<ids> to pin, or KOJEE_TANDEMS=none to silence)"
|
|
43
|
+
);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
ids = listed;
|
|
47
|
+
log(`[ensure-join] mode=auto n=${ids.length} (KOJEE_TANDEMS unset \u2014 re-seating where this agent already holds a seat)`);
|
|
48
|
+
}
|
|
49
|
+
for (const id of ids) {
|
|
50
|
+
const outcome = await joinOne(opts.gateway, id, perCallTimeoutMs);
|
|
51
|
+
if (outcome.kind === "failed") {
|
|
52
|
+
result.failed.push(id);
|
|
53
|
+
log(`[ensure-join] ${id}: FAILED \u2014 ${outcome.detail} (continuing)`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (outcome.kind === "already") {
|
|
57
|
+
result.already.push(id);
|
|
58
|
+
log(`[ensure-join] ${id}: already seated`);
|
|
59
|
+
} else {
|
|
60
|
+
result.joined.push(id);
|
|
61
|
+
log(`[ensure-join] ${id}: joined fresh`);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
opts.onJoined?.(id);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
async function joinOne(gateway, tandemId, perCallTimeoutMs) {
|
|
71
|
+
const ac = new AbortController();
|
|
72
|
+
const timer = setTimeout(() => ac.abort(), perCallTimeoutMs);
|
|
73
|
+
try {
|
|
74
|
+
const result = await gateway.sendRpc(
|
|
75
|
+
"tools/call",
|
|
76
|
+
{ name: "tandem_join", arguments: { tandem_id: tandemId } },
|
|
77
|
+
ac.signal
|
|
78
|
+
);
|
|
79
|
+
const text = (result.content ?? []).map((c) => typeof c?.text === "string" ? c.text : "").filter(Boolean).join("\n");
|
|
80
|
+
if (result.isError) {
|
|
81
|
+
return { kind: "failed", detail: text || "tandem_join returned an error with no text" };
|
|
82
|
+
}
|
|
83
|
+
if (/already[\s_-]?(a[\s_-]?)?(member|seated|joined)/i.test(text)) {
|
|
84
|
+
return { kind: "already" };
|
|
85
|
+
}
|
|
86
|
+
return { kind: "joined" };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return { kind: "failed", detail: err?.message ?? String(err) };
|
|
89
|
+
} finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
ensureJoinTandems,
|
|
95
|
+
parseTandemsConfig
|
|
96
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { KeyLike, JWK } from 'jose';
|
|
2
|
+
|
|
3
|
+
interface ProxyConfig {
|
|
4
|
+
token: string;
|
|
5
|
+
url: string;
|
|
6
|
+
keystorePath: string;
|
|
7
|
+
/**
|
|
8
|
+
* How credentials were resolved at launch: "token" when `--token` was passed
|
|
9
|
+
* on the CLI, "paired" when read from ~/.kojee/config.json. The proxy records
|
|
10
|
+
* this in its session-discovery file so `kojee-mcp doctor` can render the
|
|
11
|
+
* pairing check honestly on a token-mode box (no config.json by design).
|
|
12
|
+
* Defaults to "paired" when unset (back-compat with callers predating the
|
|
13
|
+
* field).
|
|
14
|
+
*/
|
|
15
|
+
authMode?: "token" | "paired";
|
|
16
|
+
}
|
|
17
|
+
interface KeystoreData {
|
|
18
|
+
private_key_jwk: JWK;
|
|
19
|
+
kid: string;
|
|
20
|
+
broker_url: string;
|
|
21
|
+
public_jwk: JWK;
|
|
22
|
+
enrolled_at: string;
|
|
23
|
+
}
|
|
24
|
+
interface LoadedKeyPair {
|
|
25
|
+
privateKey: KeyLike;
|
|
26
|
+
publicJwk: JWK;
|
|
27
|
+
kid: string;
|
|
28
|
+
}
|
|
29
|
+
interface GovernanceMeta {
|
|
30
|
+
decision: "require_approval" | "deny" | "allow";
|
|
31
|
+
approval_id?: string;
|
|
32
|
+
expires_at?: string;
|
|
33
|
+
triggered_guardrails?: string[];
|
|
34
|
+
}
|
|
35
|
+
interface ToolCallContent {
|
|
36
|
+
type: string;
|
|
37
|
+
text: string;
|
|
38
|
+
}
|
|
39
|
+
interface ToolCallResult {
|
|
40
|
+
content: ToolCallContent[];
|
|
41
|
+
isError?: boolean;
|
|
42
|
+
_meta?: {
|
|
43
|
+
governance?: GovernanceMeta;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare class GatewayClient {
|
|
48
|
+
private readonly brokerUrl;
|
|
49
|
+
private readonly token;
|
|
50
|
+
private readonly privateKey;
|
|
51
|
+
private readonly kid;
|
|
52
|
+
private currentNonce;
|
|
53
|
+
private requestCounter;
|
|
54
|
+
private readonly endpoint;
|
|
55
|
+
constructor(brokerUrl: string, token: string, privateKey: KeyLike, kid: string, sessionId: string);
|
|
56
|
+
/**
|
|
57
|
+
* Expose the DPoP signing key so peer modules sharing auth state
|
|
58
|
+
* (e.g. tandem/event-stream.ts) can sign their own requests.
|
|
59
|
+
*/
|
|
60
|
+
getPrivateKey(): KeyLike;
|
|
61
|
+
/**
|
|
62
|
+
* Expose the bot_key_id (kid) for DPoP proof headers. Paired with
|
|
63
|
+
* getPrivateKey() so peer modules can construct proofs without
|
|
64
|
+
* threading the key material through their own constructors.
|
|
65
|
+
*/
|
|
66
|
+
getKid(): string;
|
|
67
|
+
/**
|
|
68
|
+
* Derive a deterministic session ID from the gateway token.
|
|
69
|
+
* session_id = sha256(token + "proxy").slice(0, 16)
|
|
70
|
+
*/
|
|
71
|
+
static deriveSessionId(token: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* Send a JSON-RPC 2.0 request to the gateway, handling DPoP auth and
|
|
74
|
+
* nonce retry transparently. A 403 `step_up_required` (deprecated feature,
|
|
75
|
+
* owner ruling 2026-06-10) is no longer polled — it surfaces immediately as
|
|
76
|
+
* a structured tool error via translateHttpError.
|
|
77
|
+
*
|
|
78
|
+
* `signal` (ROUND-3 MAJOR A) is a REAL AbortSignal threaded into the
|
|
79
|
+
* underlying `fetch` option — NOT placed inside `params`/`arguments`. A
|
|
80
|
+
* caller with a per-call timeout budget (e.g. resubscribeMemberships) passes
|
|
81
|
+
* its controller's signal here so a hung backend aborts at the budget instead
|
|
82
|
+
* of hanging forever. Putting the signal in `arguments` (the round-2 bug) both
|
|
83
|
+
* left fetch un-aborted AND serialized a junk `{}` onto the wire body.
|
|
84
|
+
*/
|
|
85
|
+
sendRpc(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<ToolCallResult>;
|
|
86
|
+
private executeWithRetries;
|
|
87
|
+
private sendHttpRequest;
|
|
88
|
+
private trackNonce;
|
|
89
|
+
private tryParseErrorBody;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { GatewayClient as G, type KeystoreData as K, type LoadedKeyPair as L, type ProxyConfig as P, type ToolCallResult as T };
|
|
@@ -33,6 +33,12 @@ async function handleRequest(req, res, opts) {
|
|
|
33
33
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
34
34
|
return json(res, 200, { ok: true });
|
|
35
35
|
}
|
|
36
|
+
if (req.method === "GET" && (url.pathname === "/status" || url.pathname === "/poll") && opts.controlToken !== void 0 && !bearerMatches(req.headers.authorization, opts.controlToken)) {
|
|
37
|
+
return json(res, 401, {
|
|
38
|
+
error: "unauthorized",
|
|
39
|
+
detail: "GET /poll and /status are gated by the control token (0.5.4) \u2014 read the file named by the discovery file's controlTokenPath and send it as `Authorization: Bearer <token>`"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
36
42
|
if (req.method === "GET" && url.pathname === "/status") {
|
|
37
43
|
return respondWithStatus(res, opts);
|
|
38
44
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
url: string;
|
|
4
|
-
keystorePath: string;
|
|
5
|
-
/**
|
|
6
|
-
* How credentials were resolved at launch: "token" when `--token` was passed
|
|
7
|
-
* on the CLI, "paired" when read from ~/.kojee/config.json. The proxy records
|
|
8
|
-
* this in its session-discovery file so `kojee-mcp doctor` can render the
|
|
9
|
-
* pairing check honestly on a token-mode box (no config.json by design).
|
|
10
|
-
* Defaults to "paired" when unset (back-compat with callers predating the
|
|
11
|
-
* field).
|
|
12
|
-
*/
|
|
13
|
-
authMode?: "token" | "paired";
|
|
14
|
-
}
|
|
1
|
+
import { G as GatewayClient, P as ProxyConfig } from './gateway-client-93P1E0CZ.js';
|
|
2
|
+
import 'jose';
|
|
15
3
|
|
|
4
|
+
/**
|
|
5
|
+
* List the tandem_ids where THIS AGENT holds an active seat, via a
|
|
6
|
+
* `tandem_list` tool call. The backend's tandem_list is PRINCIPAL-scoped:
|
|
7
|
+
* rows include rooms where only SIBLING agents of the principal sit
|
|
8
|
+
* (`my_membership: {is_member:false, principal_is_member:true}`). This is
|
|
9
|
+
* the auto ensure-join feed, so object rows are filtered to
|
|
10
|
+
* `my_membership.is_member === true` — FAIL CLOSED: a row missing the flag
|
|
11
|
+
* is excluded, because joining a room the agent was never in is the harm
|
|
12
|
+
* (live-reproduced 2026-06-10: 5 rows listed, only 2 agent seats).
|
|
13
|
+
* Returns the id array, or null when the list could not be determined (tool
|
|
14
|
+
* error or unparseable result) — null is the "unknown" signal callers map to a
|
|
15
|
+
* -1 membership count. MINOR 6: called fresh on every reconnect so the touch
|
|
16
|
+
* set tracks mid-session joins (not boot-frozen).
|
|
17
|
+
*/
|
|
18
|
+
declare function listTandemIds(gateway: GatewayClient): Promise<string[] | null>;
|
|
16
19
|
declare function startProxy(config: ProxyConfig): Promise<void>;
|
|
17
20
|
|
|
18
|
-
export {
|
|
21
|
+
export { ProxyConfig, listTandemIds, startProxy };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
listTandemIds,
|
|
2
3
|
startProxy
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-YVUXQ4Z2.js";
|
|
5
|
-
import "./chunk-BJMASMKX.js";
|
|
4
|
+
} from "./chunk-GATXJ6UT.js";
|
|
6
5
|
import "./chunk-X672ZN7V.js";
|
|
7
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-BJMASMKX.js";
|
|
7
|
+
import "./chunk-6SK6ITFE.js";
|
|
8
|
+
import "./chunk-3XDJOHMZ.js";
|
|
9
|
+
import "./chunk-UEGQGXPY.js";
|
|
8
10
|
import "./chunk-2MIISF2W.js";
|
|
9
11
|
import "./chunk-LDZXU3DW.js";
|
|
10
12
|
import "./chunk-BLEGIR35.js";
|
|
11
13
|
export {
|
|
14
|
+
listTandemIds,
|
|
12
15
|
startProxy
|
|
13
16
|
};
|