kojee-mcp 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +24 -5
  2. package/dist/{chunk-62KH6VNQ.js → chunk-2BDAM3TH.js} +61 -160
  3. package/dist/{chunk-36L3GCU3.js → chunk-3XDJOHMZ.js} +12 -2
  4. package/dist/chunk-6SK6ITFE.js +142 -0
  5. package/dist/{control-token-TYDAL477.js → chunk-GI2CKKBL.js} +13 -2
  6. package/dist/{resubscribe-SLZNA76S.js → chunk-OT2GILXC.js} +1 -0
  7. package/dist/{chunk-YVUXQ4Z2.js → chunk-UEGQGXPY.js} +53 -8
  8. package/dist/{chunk-OSKHA5DS.js → chunk-V5VZPYMZ.js} +2 -2
  9. package/dist/cli.js +19 -24
  10. package/dist/control-token-4BUCTYQB.js +13 -0
  11. package/dist/{doctor-TXWMMSRC.js → doctor-QCQDFLEH.js} +29 -16
  12. package/dist/{doctor-codex-3A7KYOVX.js → doctor-codex-NZ53ROQA.js} +3 -3
  13. package/dist/ensure-join-7AEDJMPE.js +96 -0
  14. package/dist/gateway-client-93P1E0CZ.d.ts +92 -0
  15. package/dist/{hook-server-NDJSV22J.js → hook-server-37E2LUKJ.js} +6 -0
  16. package/dist/index.d.ts +18 -15
  17. package/dist/index.js +7 -4
  18. package/dist/lib.d.ts +427 -0
  19. package/dist/lib.js +44 -0
  20. package/dist/reconnect-scheduler-JSXCJKQP.js +26 -0
  21. package/dist/resubscribe-G5OGDZJD.js +6 -0
  22. package/dist/{send-cli-7QJ36YY7.js → send-cli-C2F4WTBN.js} +1 -1
  23. package/dist/{stop-hook-GO363SMD.js → stop-hook-TRAMQYNE.js} +15 -7
  24. package/dist/{tail-stream-U436QL2X.js → tail-stream-VUZBYKXS.js} +4 -4
  25. package/dist/{user-prompt-submit-hook-ARPEO6FF.js → user-prompt-submit-hook-ZD2XKN7U.js} +7 -1
  26. package/dist/{webhook-config-UKUSI2FE.js → webhook-config-O4WMQ532.js} +1 -1
  27. package/dist/{webhook-sink-GCLL2S6S.js → webhook-sink-NWGCUDGY.js} +17 -3
  28. package/dist/{wizard-Z5JA3YPV.js → wizard-OSOAY4GO.js} +4 -4
  29. package/package.json +11 -2
@@ -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
- return name.replace(/[\x00-\x1f\x7f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_DISPLAYNAME_CHARS);
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
- return raw;
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 rawDisplay = sender["display"];
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 = 5e3;
3
- var WEBHOOK_DEFAULT_MAX_RETRIES = 4;
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-2BDAM3TH.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
- VERSION,
10
- startProxy
11
- } from "./chunk-62KH6VNQ.js";
12
- import "./chunk-YVUXQ4Z2.js";
13
- import "./chunk-BJMASMKX.js";
14
- import "./chunk-X672ZN7V.js";
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-GO363SMD.js");
86
+ const { runStopHook } = await import("./stop-hook-TRAMQYNE.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-ARPEO6FF.js");
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-7QJ36YY7.js");
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-U436QL2X.js");
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-TXWMMSRC.js");
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-Z5JA3YPV.js");
161
+ const { runWizard } = await import("./wizard-OSOAY4GO.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
- loadPairedConfig
3
- } from "./chunk-YH27B6SW.js";
2
+ monitorHeartbeatPath,
3
+ statusLogPath
4
+ } from "./chunk-2TUAFAIW.js";
4
5
  import {
5
- deriveDiscoveryKey,
6
- findClaudeAncestorPid
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
- monitorHeartbeatPath,
14
- statusLogPath
15
- } from "./chunk-2TUAFAIW.js";
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 statusProbe = await probeJsonWithStatus(fetchFn, `${base}/status`);
115
- if (statusProbe.json === null && statusProbe.routeAbsent) {
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
- return { json: null, routeAbsent };
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-3A7KYOVX.js");
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
- interface ProxyConfig {
2
- token: string;
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 { type ProxyConfig, startProxy };
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-62KH6VNQ.js";
4
- import "./chunk-YVUXQ4Z2.js";
5
- import "./chunk-BJMASMKX.js";
4
+ } from "./chunk-2BDAM3TH.js";
6
5
  import "./chunk-X672ZN7V.js";
7
- import "./chunk-36L3GCU3.js";
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
  };