kojee-mcp 0.5.19 → 0.7.0
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 +13 -8
- package/dist/ancestry-A2F5KQ6A.js +10 -0
- package/dist/cc-session-id-RURNIHHC.js +10 -0
- package/dist/{chunk-U4MNICQI.js → chunk-247WFMCJ.js} +12 -22
- package/dist/{chunk-PSVVRGJC.js → chunk-5H75AEZ2.js} +22 -9
- package/dist/{chunk-YH27B6SW.js → chunk-5SZHXYPK.js} +16 -3
- package/dist/{chunk-DO42NPNR.js → chunk-67F67AQ6.js} +14 -3
- package/dist/{chunk-CH32ELFX.js → chunk-6G6YYST6.js} +1 -1
- package/dist/chunk-D6JKFJ6A.js +11 -0
- package/dist/chunk-E6WMFMM2.js +100 -0
- package/dist/chunk-EIAUW6KO.js +82 -0
- package/dist/chunk-EIH2LNF4.js +45 -0
- package/dist/{chunk-74XFVX6Z.js → chunk-FSKGQ6GT.js} +47 -10
- package/dist/{chunk-GI2CKKBL.js → chunk-GKXHKR3B.js} +1 -1
- package/dist/{wizard-Y3ULSO2C.js → chunk-GVWYW7MY.js} +247 -138
- package/dist/{chunk-JXMVZEQ7.js → chunk-I67C2HYA.js} +1 -1
- package/dist/chunk-KNEJTD6G.js +31 -0
- package/dist/{chunk-NR4Y54OL.js → chunk-MIEI4PLB.js} +1 -19
- package/dist/chunk-NE2F5CKS.js +113 -0
- package/dist/{chunk-XXFVWP6H.js → chunk-OB5T6P24.js} +1 -1
- package/dist/{chunk-65KRRDHP.js → chunk-OBIJ6WQP.js} +82 -27
- package/dist/{chunk-DXJ6QLSJ.js → chunk-PKOBR7ZN.js} +1 -1
- package/dist/{chunk-2OLXXOT3.js → chunk-PPTKGWFF.js} +69 -3
- package/dist/{chunk-X672ZN7V.js → chunk-QEJUNP3X.js} +3 -2
- package/dist/chunk-QJFMU4QC.js +33 -0
- package/dist/{chunk-EW72ZNQL.js → chunk-RDLF4NQC.js} +1 -1
- package/dist/{chunk-XIDCYUTK.js → chunk-SGRVG4HW.js} +9 -4
- package/dist/{chunk-DURRQDYI.js → chunk-TNZITJAB.js} +45 -15
- package/dist/chunk-U5HHHRXA.js +103 -0
- package/dist/chunk-WBXU27BF.js +35 -0
- package/dist/chunk-WIU5WGDF.js +10 -0
- package/dist/chunk-XJEBJIQE.js +52 -0
- package/dist/chunk-Z5LPNJQ6.js +20 -0
- package/dist/cli.js +121 -89
- package/dist/{chunk-35XBRG3V.js → codex-stop-hook-EOMQD3J4.js} +11 -15
- package/dist/connect-handler-HIRM624N.js +137 -0
- package/dist/{control-token-4BUCTYQB.js → control-token-KXSZ3GBY.js} +2 -2
- package/dist/{doctor-U3MTB4GY.js → doctor-PLKLHTJM.js} +48 -22
- package/dist/{doctor-codex-PA3WO6LR.js → doctor-codex-HARAAH4Y.js} +23 -6
- package/dist/doctor-openclaw-LL6NSPLC.js +86 -0
- package/dist/{event-log-HOBO3ECB.js → event-log-2NBJEIEP.js} +5 -3
- package/dist/{event-stream-XX5EZ6HN.js → event-stream-KRYWEYWO.js} +7 -4
- package/dist/{hook-server-XK2NHLJV.js → hook-server-CWBH6PFL.js} +2 -2
- package/dist/index.js +13 -11
- package/dist/{install-LJY2CHKG.js → install-V7LSQCYZ.js} +37 -22
- package/dist/{keystore-XLEV3FL5.js → keystore-LQNFDHGO.js} +2 -2
- package/dist/lib.d.ts +4 -1
- package/dist/lib.js +12 -9
- package/dist/pair-HTCVSMR6.js +11 -0
- package/dist/{paired-config-JTFLHMZ2.js → paired-config-GUDDO2CH.js} +4 -2
- package/dist/{parent-watchdog-TLU355FB.js → parent-watchdog-LTUNF7PD.js} +24 -5
- package/dist/{registry-CGLE5X54.js → registry-VRFHKOPP.js} +195 -26
- package/dist/resolve-slot-BFIB46Y5.js +13 -0
- package/dist/{runtime-record-WO4IECM6.js → runtime-record-OXRLTOLC.js} +2 -2
- package/dist/{send-cli-IPADRDD2.js → send-cli-45RLGYIC.js} +24 -12
- package/dist/{server-LTFG4GMV.js → server-YTHOMA4H.js} +5 -3
- package/dist/{session-discovery-FNMJGFPM.js → session-discovery-NHCZ2XWA.js} +2 -2
- package/dist/setup-handler-UHVKSX7E.js +159 -0
- package/dist/{stop-hook-QZTFH5WV.js → stop-hook-YI4KRKF2.js} +17 -20
- package/dist/{tail-stream-CEJZGSIR.js → tail-stream-NBGHHBS4.js} +61 -22
- package/dist/{user-prompt-submit-hook-FQ43NIHC.js → user-prompt-submit-hook-DTXXFDSD.js} +11 -14
- package/package.json +2 -1
- package/dist/ancestry-ONFBQEP5.js +0 -8
- package/dist/chunk-BLEGIR35.js +0 -43
- package/dist/chunk-CLKCNV2A.js +0 -100
- package/dist/chunk-OGHDTFAX.js +0 -50
- package/dist/chunk-VHKPWUX7.js +0 -41
- package/dist/codex-stop-hook-VY7DOMAG.js +0 -16
- package/dist/doctor-openclaw-SS2TMQOX.js +0 -95
- package/dist/pair-P4ILCMT7.js +0 -10
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/runtime/cc-session-id.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
var SESSION_ID_ENV_VARS = ["CLAUDE_CODE_SESSION_ID"];
|
|
4
|
+
function sanitizeKey(value) {
|
|
5
|
+
return value.replace(/[^A-Za-z0-9_-]/g, "");
|
|
6
|
+
}
|
|
7
|
+
function resolveInstanceKey(deps = {}) {
|
|
8
|
+
const env = deps.env ?? process.env;
|
|
9
|
+
for (const name of SESSION_ID_ENV_VARS) {
|
|
10
|
+
const sid = sanitizeKey((env[name] ?? "").trim());
|
|
11
|
+
if (sid) return sid;
|
|
12
|
+
}
|
|
13
|
+
const explicit = sanitizeKey((env.KOJEE_INSTANCE ?? "").trim());
|
|
14
|
+
if (explicit) return `inst-${explicit}`;
|
|
15
|
+
const mint = deps.randomUUID ?? randomUUID;
|
|
16
|
+
return `mint-${mint()}`;
|
|
17
|
+
}
|
|
18
|
+
function resolveSharedSessionId(deps = {}) {
|
|
19
|
+
const env = deps.env ?? process.env;
|
|
20
|
+
const fromEnv = sanitizeKey((env["CLAUDE_CODE_SESSION_ID"] ?? "").trim());
|
|
21
|
+
if (fromEnv) return fromEnv;
|
|
22
|
+
const fromStdin = sanitizeKey((deps.stdinSessionId ?? "").trim());
|
|
23
|
+
if (fromStdin) return fromStdin;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
SESSION_ID_ENV_VARS,
|
|
29
|
+
resolveInstanceKey,
|
|
30
|
+
resolveSharedSessionId
|
|
31
|
+
};
|
|
@@ -25,24 +25,6 @@ function computeAth(accessToken) {
|
|
|
25
25
|
return base64url.encode(hash);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// src/tandem/session-id.ts
|
|
29
|
-
import crypto2 from "crypto";
|
|
30
|
-
import { ulid } from "ulidx";
|
|
31
|
-
var MCP_SESSION_ID = ulid();
|
|
32
|
-
function deriveStableSessionId(token, instanceKey) {
|
|
33
|
-
return crypto2.createHash("sha256").update(`${token}:${instanceKey}`).digest("hex").slice(0, 32);
|
|
34
|
-
}
|
|
35
|
-
var _stableSessionId = null;
|
|
36
|
-
function setStableSessionId(id) {
|
|
37
|
-
_stableSessionId = id;
|
|
38
|
-
}
|
|
39
|
-
function getSessionId() {
|
|
40
|
-
return _stableSessionId ?? MCP_SESSION_ID;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
28
|
export {
|
|
44
|
-
createDPoPProof
|
|
45
|
-
deriveStableSessionId,
|
|
46
|
-
setStableSessionId,
|
|
47
|
-
getSessionId
|
|
29
|
+
createDPoPProof
|
|
48
30
|
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/delivery/openclaw-wake.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
var DEFAULT_GATEWAY_PORT = 18789;
|
|
6
|
+
var DEFAULT_HOOKS_PATH = "/hooks";
|
|
7
|
+
function resolveOpenclawConfigPath(env = process.env) {
|
|
8
|
+
const explicit = (env["KOJEE_OPENCLAW_CONFIG"] ?? env["OPENCLAW_CONFIG_PATH"] ?? "").trim();
|
|
9
|
+
return explicit || path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
10
|
+
}
|
|
11
|
+
function parsePort(raw) {
|
|
12
|
+
const t = raw?.trim();
|
|
13
|
+
if (!t || !/^\d+$/.test(t)) return null;
|
|
14
|
+
const n = Number.parseInt(t, 10);
|
|
15
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
16
|
+
}
|
|
17
|
+
function resolveOpenclawWakeTarget(opts = {}) {
|
|
18
|
+
const env = opts.env ?? process.env;
|
|
19
|
+
const configPath = opts.configPath ?? resolveOpenclawConfigPath(env);
|
|
20
|
+
let raw = null;
|
|
21
|
+
try {
|
|
22
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
23
|
+
} catch {
|
|
24
|
+
raw = null;
|
|
25
|
+
}
|
|
26
|
+
return resolveWakeTargetFromRaw(raw, configPath, env);
|
|
27
|
+
}
|
|
28
|
+
function resolveWakeTargetFromRaw(raw, configPath, env) {
|
|
29
|
+
let cfg = {};
|
|
30
|
+
if (raw !== null) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(raw);
|
|
33
|
+
cfg = parsed && typeof parsed === "object" ? parsed : {};
|
|
34
|
+
} catch {
|
|
35
|
+
cfg = {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const envUrl = (env["KOJEE_OPENCLAW_WAKE_URL"] ?? "").trim();
|
|
39
|
+
const envToken = (env["KOJEE_OPENCLAW_WAKE_TOKEN"] ?? "").trim();
|
|
40
|
+
const cfgToken = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : "";
|
|
41
|
+
const token = envToken || cfgToken;
|
|
42
|
+
const hooksEnabled = cfg.hooks?.enabled === true;
|
|
43
|
+
let url = envUrl;
|
|
44
|
+
if (!url) {
|
|
45
|
+
const cfgPort = typeof cfg.gateway?.port === "number" && cfg.gateway.port > 0 ? cfg.gateway.port : DEFAULT_GATEWAY_PORT;
|
|
46
|
+
const port = parsePort(env["OPENCLAW_GATEWAY_PORT"]) ?? cfgPort;
|
|
47
|
+
const rawPath = typeof cfg.hooks?.path === "string" && cfg.hooks.path.trim() || DEFAULT_HOOKS_PATH;
|
|
48
|
+
const withSlash = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
|
|
49
|
+
const base = withSlash.length > 1 ? withSlash.replace(/\/+$/, "") : withSlash;
|
|
50
|
+
url = `http://127.0.0.1:${port}${base}/wake`;
|
|
51
|
+
}
|
|
52
|
+
if (!token) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: `OpenClaw hooks token not found (KOJEE_OPENCLAW_WAKE_TOKEN or hooks.token in ${configPath}). Run \`kojee-mcp connect <code> --runtime openclaw\` to enable hooks.`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!hooksEnabled && !envToken && !envUrl) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
reason: `OpenClaw hooks disabled (hooks.enabled != true in ${configPath}); /wake is not mounted.`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, target: { url, token } };
|
|
65
|
+
}
|
|
66
|
+
function createCachedOpenclawWakeResolver(opts = {}) {
|
|
67
|
+
let cache = null;
|
|
68
|
+
return async () => {
|
|
69
|
+
const env = opts.env ?? process.env;
|
|
70
|
+
const configPath = opts.configPath ?? resolveOpenclawConfigPath(env);
|
|
71
|
+
let mtimeMs = -1;
|
|
72
|
+
try {
|
|
73
|
+
mtimeMs = (await fs.promises.stat(configPath)).mtimeMs;
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
if (cache && cache.mtimeMs === mtimeMs) return cache.resolution;
|
|
77
|
+
let raw = null;
|
|
78
|
+
if (mtimeMs !== -1) {
|
|
79
|
+
try {
|
|
80
|
+
raw = await fs.promises.readFile(configPath, "utf8");
|
|
81
|
+
} catch {
|
|
82
|
+
raw = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const resolution = resolveWakeTargetFromRaw(raw, configPath, env);
|
|
86
|
+
cache = { mtimeMs, resolution };
|
|
87
|
+
return resolution;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function postOpenclawWake(target, body, deps = {}) {
|
|
91
|
+
const doFetch = deps.fetch ?? fetch;
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timer = setTimeout(() => controller.abort(), deps.timeoutMs ?? 4e3);
|
|
94
|
+
try {
|
|
95
|
+
const res = await doFetch(target.url, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${target.token}` },
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
signal: controller.signal
|
|
100
|
+
});
|
|
101
|
+
return res.ok ? { ok: true, status: res.status } : { ok: false, status: res.status, error: `wake POST ${res.status}` };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return { ok: false, error: err.message };
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
resolveOpenclawWakeTarget,
|
|
111
|
+
createCachedOpenclawWakeResolver,
|
|
112
|
+
postOpenclawWake
|
|
113
|
+
};
|
|
@@ -1,36 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildHookCommand,
|
|
3
|
+
isKojeeHookCommand,
|
|
4
|
+
npxHookCommand
|
|
5
|
+
} from "./chunk-QJFMU4QC.js";
|
|
6
|
+
import {
|
|
7
|
+
wrapNpxLauncher
|
|
8
|
+
} from "./chunk-D6JKFJ6A.js";
|
|
1
9
|
import {
|
|
2
10
|
kojeeHomeDir
|
|
3
11
|
} from "./chunk-SQL56SEB.js";
|
|
4
12
|
import {
|
|
5
13
|
secureFile
|
|
6
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-U5HHHRXA.js";
|
|
7
15
|
|
|
8
16
|
// src/wizard/codex-config.ts
|
|
9
17
|
import fs from "fs";
|
|
18
|
+
import os from "os";
|
|
10
19
|
import path from "path";
|
|
20
|
+
function win32DriveQualified(p, ...homeSegments) {
|
|
21
|
+
if (process.platform === "win32" && !/^[a-zA-Z]:/.test(p)) {
|
|
22
|
+
return path.win32.resolve(path.win32.join(os.homedir(), ...homeSegments));
|
|
23
|
+
}
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
11
26
|
function defaultCodexConfigPath() {
|
|
12
|
-
return path.join(kojeeHomeDir(), ".codex", "config.toml");
|
|
27
|
+
return win32DriveQualified(path.join(kojeeHomeDir(), ".codex", "config.toml"), ".codex", "config.toml");
|
|
13
28
|
}
|
|
14
29
|
function defaultCodexHooksPath() {
|
|
15
|
-
return path.join(kojeeHomeDir(), ".codex", "hooks.json");
|
|
30
|
+
return win32DriveQualified(path.join(kojeeHomeDir(), ".codex", "hooks.json"), ".codex", "hooks.json");
|
|
16
31
|
}
|
|
17
|
-
var CODEX_STOP_HOOK_COMMAND = "
|
|
18
|
-
function
|
|
32
|
+
var CODEX_STOP_HOOK_COMMAND = npxHookCommand("codex-stop");
|
|
33
|
+
function codexStopHookCommand() {
|
|
34
|
+
return buildHookCommand("codex-stop");
|
|
35
|
+
}
|
|
36
|
+
function codexRestArgs(token, url, pairedConfigPath) {
|
|
19
37
|
if (token && url) {
|
|
20
|
-
return
|
|
38
|
+
return ["-y", "kojee-mcp", "--token", token, "--url", url];
|
|
39
|
+
}
|
|
40
|
+
if (pairedConfigPath) {
|
|
41
|
+
return ["-y", "kojee-mcp", "--paired-config", pairedConfigPath];
|
|
21
42
|
}
|
|
22
|
-
return
|
|
43
|
+
return ["-y", "kojee-mcp"];
|
|
44
|
+
}
|
|
45
|
+
function codexLauncherLiterals(token, url, pairedConfigPath) {
|
|
46
|
+
const rest = codexRestArgs(token, url, pairedConfigPath);
|
|
47
|
+
const { command, args } = wrapNpxLauncher("npx", rest);
|
|
48
|
+
return {
|
|
49
|
+
commandLiteral: `"${escapeTomlString(command)}"`,
|
|
50
|
+
argsLiteral: "[" + args.map((a) => `"${escapeTomlString(a)}"`).join(", ") + "]"
|
|
51
|
+
};
|
|
23
52
|
}
|
|
24
53
|
function buildCodexMcpServerTable(opts) {
|
|
54
|
+
const { commandLiteral, argsLiteral } = codexLauncherLiterals(
|
|
55
|
+
opts.token,
|
|
56
|
+
opts.url,
|
|
57
|
+
opts.pairedConfigPath
|
|
58
|
+
);
|
|
25
59
|
return [
|
|
26
60
|
"[mcp_servers.kojee]",
|
|
27
|
-
|
|
28
|
-
`args = ${
|
|
61
|
+
`command = ${commandLiteral}`,
|
|
62
|
+
`args = ${argsLiteral}`,
|
|
29
63
|
"",
|
|
30
64
|
"[mcp_servers.kojee.env]",
|
|
31
65
|
'KOJEE_RUNTIME = "codex"',
|
|
32
|
-
|
|
33
|
-
|
|
66
|
+
// Webhook env is emitted ONLY when supplied (the credential-only `connect`
|
|
67
|
+
// path omits both → no placeholder URL, no fresh secret).
|
|
68
|
+
...opts.webhookUrl !== void 0 ? [`KOJEE_WEBHOOK_URL = "${escapeTomlString(opts.webhookUrl)}"`] : [],
|
|
69
|
+
...opts.webhookSecret !== void 0 ? [`KOJEE_WEBHOOK_SECRET = "${escapeTomlString(opts.webhookSecret)}"`] : [],
|
|
34
70
|
// Signature emission overrides (0.5.3) — emitted only when configured.
|
|
35
71
|
...(opts.signatureEnv ?? []).map(([k, v]) => `${k} = "${escapeTomlString(v)}"`)
|
|
36
72
|
].join("\n");
|
|
@@ -40,7 +76,7 @@ function buildCodexStopHookBlock() {
|
|
|
40
76
|
"[[hooks.Stop]]",
|
|
41
77
|
"[[hooks.Stop.hooks]]",
|
|
42
78
|
'type = "command"',
|
|
43
|
-
`command = "${escapeTomlString(
|
|
79
|
+
`command = "${escapeTomlString(codexStopHookCommand())}"`
|
|
44
80
|
].join("\n");
|
|
45
81
|
}
|
|
46
82
|
function escapeTomlString(s) {
|
|
@@ -62,18 +98,30 @@ function writeCodexConfig(inputs) {
|
|
|
62
98
|
inputs.webhookSecret,
|
|
63
99
|
inputs.signatureEnv ?? [],
|
|
64
100
|
inputs.token,
|
|
65
|
-
inputs.url
|
|
101
|
+
inputs.url,
|
|
102
|
+
inputs.pairedConfigPath
|
|
66
103
|
);
|
|
67
104
|
writeFile600(configPath, toml);
|
|
105
|
+
const command = codexStopHookCommand();
|
|
68
106
|
const hooks = readJson(hooksPath);
|
|
69
107
|
hooks.hooks ??= {};
|
|
70
108
|
hooks.hooks.Stop ??= [];
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
let present = false;
|
|
110
|
+
for (const e of hooks.hooks.Stop) {
|
|
111
|
+
for (const h of e.hooks ?? []) {
|
|
112
|
+
if (h.command === command) {
|
|
113
|
+
present = true;
|
|
114
|
+
} else if (isKojeeHookCommand(h.command)) {
|
|
115
|
+
h.command = command;
|
|
116
|
+
present = true;
|
|
117
|
+
}
|
|
118
|
+
if (present) break;
|
|
119
|
+
}
|
|
120
|
+
if (present) break;
|
|
121
|
+
}
|
|
122
|
+
if (!present) {
|
|
75
123
|
hooks.hooks.Stop.push({
|
|
76
|
-
hooks: [{ type: "command", command
|
|
124
|
+
hooks: [{ type: "command", command }]
|
|
77
125
|
});
|
|
78
126
|
}
|
|
79
127
|
writeFile600(hooksPath, JSON.stringify(hooks, null, 2));
|
|
@@ -97,7 +145,8 @@ function removeCodexConfig(opts = {}) {
|
|
|
97
145
|
if (stop && stop.length > 0) {
|
|
98
146
|
const before = stop.length;
|
|
99
147
|
hooks.hooks.Stop = stop.filter(
|
|
100
|
-
|
|
148
|
+
// F10: strip kojee entries in EITHER persisted form (npx or pinned).
|
|
149
|
+
(e) => !e.hooks?.some((h) => isKojeeHookCommand(h.command))
|
|
101
150
|
);
|
|
102
151
|
if (hooks.hooks.Stop.length !== before) {
|
|
103
152
|
result.stopHook = true;
|
|
@@ -108,27 +157,33 @@ function removeCodexConfig(opts = {}) {
|
|
|
108
157
|
}
|
|
109
158
|
return result;
|
|
110
159
|
}
|
|
111
|
-
function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret, signatureEnv = [], token, url) {
|
|
160
|
+
function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret, signatureEnv = [], token, url, pairedConfigPath) {
|
|
112
161
|
const parsed = extractKojeeBlock(existing);
|
|
113
162
|
if (!parsed) {
|
|
114
163
|
const block = buildCodexMcpServerTable({
|
|
115
|
-
webhookUrl,
|
|
116
|
-
webhookSecret,
|
|
164
|
+
...webhookUrl !== void 0 ? { webhookUrl } : {},
|
|
165
|
+
...webhookSecret !== void 0 ? { webhookSecret } : {},
|
|
117
166
|
...signatureEnv.length > 0 ? { signatureEnv } : {},
|
|
118
167
|
...token ? { token } : {},
|
|
119
|
-
...url ? { url } : {}
|
|
168
|
+
...url ? { url } : {},
|
|
169
|
+
...pairedConfigPath ? { pairedConfigPath } : {}
|
|
120
170
|
});
|
|
121
171
|
const base2 = existing.replace(/\s*$/, "");
|
|
122
172
|
return base2.length === 0 ? block + "\n" : base2 + "\n\n" + block + "\n";
|
|
123
173
|
}
|
|
174
|
+
const { commandLiteral, argsLiteral } = codexLauncherLiterals(token, url, pairedConfigPath);
|
|
124
175
|
const tableKeys = upsertKeyLines(parsed.tableKeys, [
|
|
125
|
-
["command",
|
|
126
|
-
["args",
|
|
176
|
+
["command", commandLiteral],
|
|
177
|
+
["args", argsLiteral]
|
|
127
178
|
]);
|
|
128
179
|
const envKeys = upsertKeyLines(parsed.envKeys, [
|
|
129
180
|
["KOJEE_RUNTIME", '"codex"'],
|
|
130
|
-
|
|
131
|
-
|
|
181
|
+
// Own KOJEE_WEBHOOK_URL/SECRET ONLY when supplied this run. The credential-
|
|
182
|
+
// only `connect` path passes neither → they drop out of the owned set and
|
|
183
|
+
// any existing operator/receiver values are PRESERVED verbatim (no clobber,
|
|
184
|
+
// no rotated secret, byte-stable re-runs).
|
|
185
|
+
...webhookUrl !== void 0 ? [["KOJEE_WEBHOOK_URL", `"${escapeTomlString(webhookUrl)}"`]] : [],
|
|
186
|
+
...webhookSecret !== void 0 ? [["KOJEE_WEBHOOK_SECRET", `"${escapeTomlString(webhookSecret)}"`]] : [],
|
|
132
187
|
// Signature emission overrides (0.5.3) — owned only when configured this
|
|
133
188
|
// run; an operator's existing signature lines are otherwise preserved.
|
|
134
189
|
...signatureEnv.map(([k, v]) => [k, `"${escapeTomlString(v)}"`])
|
|
@@ -33,7 +33,7 @@ function translateHttpError(status, errorCode, trigger) {
|
|
|
33
33
|
}
|
|
34
34
|
if (errorCode === "invalid_dpop_proof") {
|
|
35
35
|
return makeError(
|
|
36
|
-
"Authentication failed.
|
|
36
|
+
"Authentication failed (DPoP key rejected). Restart the proxy so it re-enrolls its key. If this persists, regenerate your gateway token."
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
if (errorCode === "key_enrollment_required") {
|
|
@@ -61,8 +61,71 @@ function translateHttpError(status, errorCode, trigger) {
|
|
|
61
61
|
}
|
|
62
62
|
return null;
|
|
63
63
|
}
|
|
64
|
+
function translateTandemNotActive(reason) {
|
|
65
|
+
switch (reason) {
|
|
66
|
+
// --- genuine inactive / reaped seat: re-joining is the right fix ---
|
|
67
|
+
case "not_a_member":
|
|
68
|
+
case "not_active_member":
|
|
69
|
+
return "You aren't an active member of that Tandem (the seat may have been reaped). Run tandem_join to reactivate it, then retry.";
|
|
70
|
+
// --- authentication: pairing problem, tandem_join won't help ---
|
|
71
|
+
case "unauthenticated":
|
|
72
|
+
return "Your proxy session isn't authenticated/paired with Kojee, so this call was rejected. Re-pair the proxy \u2014 tandem_join won't fix this.";
|
|
73
|
+
// --- existence / visibility ---
|
|
74
|
+
case "tandem_not_found":
|
|
75
|
+
return "That Tandem doesn't exist or isn't visible to you.";
|
|
76
|
+
// --- power / scope: you lack the required role ---
|
|
77
|
+
case "owner_only":
|
|
78
|
+
return "This action requires owner power in this Tandem, which you don't have.";
|
|
79
|
+
case "admin_only":
|
|
80
|
+
return "This action requires admin power in this Tandem, which you don't have.";
|
|
81
|
+
// --- capacity ---
|
|
82
|
+
case "member_cap_reached":
|
|
83
|
+
return "This Tandem is full (member cap reached); rejoining won't help \u2014 ask an admin to make room.";
|
|
84
|
+
// --- message-scoped ---
|
|
85
|
+
case "message_not_found":
|
|
86
|
+
return "That message doesn't exist in this Tandem (wrong id, or it was already redacted).";
|
|
87
|
+
case "redact_forbidden":
|
|
88
|
+
return "You can't redact that message \u2014 you're neither its author nor an admin of this Tandem.";
|
|
89
|
+
// --- targeting ---
|
|
90
|
+
case "target_principal_not_found":
|
|
91
|
+
return "The target principal isn't a member of this Tandem.";
|
|
92
|
+
// --- invitations ---
|
|
93
|
+
case "invitation_not_found":
|
|
94
|
+
return "That invitation doesn't exist, or it was already revoked or used.";
|
|
95
|
+
case "invite_required":
|
|
96
|
+
return "This Tandem requires an invite to join; ask an owner or admin to invite you.";
|
|
97
|
+
case "invite_invalid":
|
|
98
|
+
return "That invite token is invalid.";
|
|
99
|
+
case "invite_expired":
|
|
100
|
+
return "That invite has expired; request a fresh one.";
|
|
101
|
+
case "invite_exhausted":
|
|
102
|
+
return "That invite has no remaining uses; request a fresh one.";
|
|
103
|
+
case "invite_wrong_principal":
|
|
104
|
+
return "That invite was issued to a different principal and can't be used by you.";
|
|
105
|
+
// --- join requests / knocks ---
|
|
106
|
+
case "join_request_not_found":
|
|
107
|
+
return "That join request doesn't exist or was already decided.";
|
|
108
|
+
case "join_request_not_pending":
|
|
109
|
+
return "That join request was already decided (it's no longer pending).";
|
|
110
|
+
case "already_member":
|
|
111
|
+
return "You're already a member of that Tandem.";
|
|
112
|
+
case "already_requested":
|
|
113
|
+
return "You already have a pending join request for that Tandem.";
|
|
114
|
+
case "knock_required":
|
|
115
|
+
return "This Tandem requires a knock; request to join and wait for an admin to approve.";
|
|
116
|
+
case "knock_cooldown":
|
|
117
|
+
return "You're knocking too frequently; wait a bit before requesting to join again.";
|
|
118
|
+
// --- direct (1:1) Tandems ---
|
|
119
|
+
case "direct_tandem_immutable_membership":
|
|
120
|
+
case "direct_immutable":
|
|
121
|
+
return "This is a direct (1:1) Tandem; its membership can't be changed.";
|
|
122
|
+
// --- unknown token: surface the real reason verbatim, never the reaped string ---
|
|
123
|
+
default:
|
|
124
|
+
return reason ? `Tandem request refused: ${reason}.` : "Tandem request refused.";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
64
127
|
var TANDEM_ERROR_MESSAGES = {
|
|
65
|
-
[-32001]: () =>
|
|
128
|
+
[-32001]: (_data, message) => translateTandemNotActive((message ?? "").trim()),
|
|
66
129
|
[-32003]: () => "This Tandem is hardened to owner-only membership; you can't join.",
|
|
67
130
|
[-32004]: () => "You aren't a member of that Tandem. Use tandem_join(join_link) first.",
|
|
68
131
|
[-32006]: (data) => {
|
|
@@ -91,7 +154,10 @@ function translateJsonRpcError(error) {
|
|
|
91
154
|
content: [
|
|
92
155
|
{
|
|
93
156
|
type: "text",
|
|
94
|
-
text: tandemMessage(
|
|
157
|
+
text: tandemMessage(
|
|
158
|
+
error.data,
|
|
159
|
+
msg
|
|
160
|
+
)
|
|
95
161
|
}
|
|
96
162
|
],
|
|
97
163
|
isError: true
|
|
@@ -13,7 +13,7 @@ function buildReplyRecipe(event) {
|
|
|
13
13
|
return `reply directly with tandem_send(tandem_id, ${SEND_BODY_PARAM}, reply_to=<msg>); fetch context only if needed via tandem_messages(tandem_id, since=cursor-1)`;
|
|
14
14
|
}
|
|
15
15
|
function buildCatchUpNote() {
|
|
16
|
-
return "
|
|
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 buildMonitorNudge(logPath) {
|
|
19
19
|
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()}.`;
|
|
@@ -27,7 +27,8 @@ function buildWebhookReceiverNote(sig) {
|
|
|
27
27
|
}
|
|
28
28
|
var CODEX_LISTEN_CAP_MS = 8e3;
|
|
29
29
|
function buildCodexWakeReason(cursor) {
|
|
30
|
-
|
|
30
|
+
const drainSince = Math.max(0, cursor - 1);
|
|
31
|
+
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.`;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/hooks/hook-command.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
function npxHookCommand(type) {
|
|
6
|
+
return `npx -y kojee-mcp hook --type=${type}`;
|
|
7
|
+
}
|
|
8
|
+
function resolveCliEntry(moduleUrl = import.meta.url) {
|
|
9
|
+
try {
|
|
10
|
+
const here = fileURLToPath(moduleUrl);
|
|
11
|
+
const candidate = path.join(path.dirname(here), "cli.js");
|
|
12
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function buildHookCommand(type, opts = {}) {
|
|
18
|
+
const cliEntry = opts.cliEntry !== void 0 ? opts.cliEntry : resolveCliEntry();
|
|
19
|
+
if (!cliEntry) return npxHookCommand(type);
|
|
20
|
+
const execPath = opts.execPath ?? process.execPath;
|
|
21
|
+
return `"${execPath}" "${cliEntry}" hook --type=${type}`;
|
|
22
|
+
}
|
|
23
|
+
var PINNED_HOOK_RE = /^"[^"]+" "[^"]+" hook --type=(stop|user-prompt-submit|codex-stop)$/;
|
|
24
|
+
function isKojeeHookCommand(command) {
|
|
25
|
+
if (command.startsWith("npx -y kojee-mcp hook --type=")) return true;
|
|
26
|
+
return PINNED_HOOK_RE.test(command);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
npxHookCommand,
|
|
31
|
+
buildHookCommand,
|
|
32
|
+
isKojeeHookCommand
|
|
33
|
+
};
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
buildCatchUpNote,
|
|
3
3
|
buildMonitorSpawn,
|
|
4
4
|
buildReplyRecipe
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-QEJUNP3X.js";
|
|
6
6
|
import {
|
|
7
7
|
translateToolCallResult
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-PPTKGWFF.js";
|
|
9
9
|
|
|
10
10
|
// src/server.ts
|
|
11
11
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -38,7 +38,11 @@ function resolveVersion() {
|
|
|
38
38
|
var VERSION = resolveVersion();
|
|
39
39
|
|
|
40
40
|
// src/server.ts
|
|
41
|
+
function buildNonChannelInstructions() {
|
|
42
|
+
return "You are connected through the LOCAL kojee proxy. Do ALL Tandem operations (tandem_join/tandem_messages/tandem_send/tandem_set_wake_filter) through THESE tools \u2014 this is your ONLY wake-capable path: the proxy can only wake you for Tandem traffic that flows through it. If a REMOTE Kojee connector (e.g. a claude.ai-hosted 'Kojee' MCP) is ALSO available, do NOT use its Tandem tools \u2014 they cannot wake you.";
|
|
43
|
+
}
|
|
41
44
|
function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
|
|
45
|
+
const localProxyRule = "You are connected through the LOCAL kojee proxy. Do ALL Tandem operations (join/read/send/set_wake_filter) through THESE tools \u2014 this is your ONLY wake-capable path: it writes the wake log the Monitor and stop-hook read. If a REMOTE Kojee connector (e.g. a claude.ai-hosted 'Kojee' MCP) is ALSO available, do NOT use its Tandem tools \u2014 they cannot wake you.\n\n";
|
|
42
46
|
const intro = `Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:
|
|
43
47
|
|
|
44
48
|
(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()}.
|
|
@@ -49,7 +53,7 @@ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
|
|
|
49
53
|
`;
|
|
50
54
|
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.";
|
|
51
55
|
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.";
|
|
52
|
-
return intro + monitorSection + listenSection + advice;
|
|
56
|
+
return localProxyRule + intro + monitorSection + listenSection + advice;
|
|
53
57
|
}
|
|
54
58
|
function tandemIdArg(args) {
|
|
55
59
|
return typeof args["tandem_id"] === "string" ? args["tandem_id"] : null;
|
|
@@ -75,7 +79,7 @@ function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLog
|
|
|
75
79
|
{ name: "kojee-mcp", version: VERSION },
|
|
76
80
|
{
|
|
77
81
|
capabilities,
|
|
78
|
-
...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
|
|
82
|
+
...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : { instructions: buildNonChannelInstructions() }
|
|
79
83
|
}
|
|
80
84
|
);
|
|
81
85
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -101,6 +105,7 @@ async function startMcpServer(server) {
|
|
|
101
105
|
|
|
102
106
|
export {
|
|
103
107
|
VERSION,
|
|
108
|
+
buildNonChannelInstructions,
|
|
104
109
|
buildChannelInstructions,
|
|
105
110
|
executeToolCall,
|
|
106
111
|
createMcpServer,
|