kojee-mcp 0.4.0 → 0.5.2
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 +98 -10
- package/dist/chunk-2TUAFAIW.js +244 -0
- package/dist/{chunk-36DMIXH7.js → chunk-BJMASMKX.js} +13 -23
- package/dist/chunk-BLEGIR35.js +43 -0
- package/dist/chunk-C6GZ2L2W.js +38 -0
- package/dist/{chunk-VZVGTHGF.js → chunk-DO42NPNR.js} +11 -17
- package/dist/chunk-EW72ZNQL.js +39 -0
- package/dist/chunk-F7L25L2J.js +60 -0
- package/dist/{chunk-WHTH6WBP.js → chunk-LSUB6QMP.js} +3 -0
- package/dist/chunk-LVL25VLO.js +22 -0
- package/dist/chunk-SQL56SEB.js +14 -0
- package/dist/chunk-WBMX4CHB.js +378 -0
- package/dist/{chunk-ZGVUM4AG.js → chunk-YEC7IHIG.js} +276 -318
- package/dist/{chunk-E7TE4QZD.js → chunk-YH27B6SW.js} +9 -9
- package/dist/chunk-ZW4SW7LJ.js +225 -0
- package/dist/cli.js +70 -78
- package/dist/codex-stop-hook-JOTBCS5K.js +72 -0
- package/dist/doctor-TSHOMT5X.js +237 -0
- package/dist/doctor-codex-BMI5JOO6.js +130 -0
- package/dist/event-log-RSTM4PLL.js +18 -0
- package/dist/{hook-server-43QS7L7P.js → hook-server-QF5JVUHV.js} +28 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -2
- package/dist/{install-WV25CRU2.js → install-WBIUVBZW.js} +9 -7
- package/dist/{paired-config-OAR3O3XY.js → paired-config-JTFLHMZ2.js} +2 -1
- package/dist/resubscribe-SLZNA76S.js +59 -0
- package/dist/runtime-record-WO4IECM6.js +14 -0
- package/dist/runtimes-CO43XUUK.js +12 -0
- package/dist/{session-discovery-WSHLR4OV.js → session-discovery-FNMJGFPM.js} +2 -1
- package/dist/stop-hook-SEPWWETV.js +119 -0
- package/dist/tail-stream-BYKO4DW6.js +162 -0
- package/dist/{user-prompt-submit-hook-WSRIJVF4.js → user-prompt-submit-hook-ARPEO6FF.js} +5 -4
- package/dist/webhook-config-5TLLX7RA.js +10 -0
- package/dist/webhook-sink-7OYZBWXA.js +163 -0
- package/dist/wizard-7KHD5JT4.js +265 -0
- package/package.json +9 -7
- package/dist/event-log-ETWR6PPY.js +0 -112
- package/dist/stop-hook-5XU3EQAE.js +0 -76
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadPairedConfig
|
|
3
|
+
} from "./chunk-YH27B6SW.js";
|
|
4
|
+
import {
|
|
5
|
+
deriveDiscoveryKey,
|
|
6
|
+
findClaudeAncestorPid
|
|
7
|
+
} from "./chunk-BJMASMKX.js";
|
|
8
|
+
import {
|
|
9
|
+
buildMonitorSpawn,
|
|
10
|
+
buildReplyRecipe
|
|
11
|
+
} from "./chunk-C6GZ2L2W.js";
|
|
12
|
+
import {
|
|
13
|
+
monitorHeartbeatPath,
|
|
14
|
+
statusLogPath
|
|
15
|
+
} from "./chunk-2TUAFAIW.js";
|
|
16
|
+
import {
|
|
17
|
+
discoveryPathForKey,
|
|
18
|
+
readSessionDiscoveryByKey
|
|
19
|
+
} from "./chunk-DO42NPNR.js";
|
|
20
|
+
import "./chunk-BLEGIR35.js";
|
|
21
|
+
|
|
22
|
+
// src/doctor.ts
|
|
23
|
+
import fs from "fs";
|
|
24
|
+
function defaultIsPidAlive(pid) {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code === "EPERM") return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function defaultStat(path) {
|
|
34
|
+
try {
|
|
35
|
+
const s = fs.statSync(path);
|
|
36
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
var STATUS_TAIL_BYTES = 64 * 1024;
|
|
42
|
+
function defaultReadLastStatusLine(path) {
|
|
43
|
+
let fd = null;
|
|
44
|
+
try {
|
|
45
|
+
const { size } = fs.statSync(path);
|
|
46
|
+
const start = Math.max(0, size - STATUS_TAIL_BYTES);
|
|
47
|
+
const len = size - start;
|
|
48
|
+
if (len === 0) return null;
|
|
49
|
+
fd = fs.openSync(path, "r");
|
|
50
|
+
const buf = Buffer.alloc(len);
|
|
51
|
+
fs.readSync(fd, buf, 0, len, start);
|
|
52
|
+
const text = buf.toString("utf8");
|
|
53
|
+
const statusLines = text.split("\n").filter((l) => l.includes("kojee-status"));
|
|
54
|
+
return statusLines.length > 0 ? statusLines[statusLines.length - 1] : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
} finally {
|
|
58
|
+
if (fd !== null) {
|
|
59
|
+
try {
|
|
60
|
+
fs.closeSync(fd);
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function collectDoctorReport(deps = {}) {
|
|
67
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
68
|
+
const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
|
|
69
|
+
const statFile = deps.statFile ?? defaultStat;
|
|
70
|
+
const readLastStatusLine = deps.readLastStatusLine ?? defaultReadLastStatusLine;
|
|
71
|
+
const readDiscovery = deps.readDiscovery ?? readSessionDiscoveryByKey;
|
|
72
|
+
const projectDir = deps.projectDir ?? process.env["CLAUDE_PROJECT_DIR"];
|
|
73
|
+
const ccPid = deps.ccPid !== void 0 ? deps.ccPid : await findClaudeAncestorPid();
|
|
74
|
+
const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
|
|
75
|
+
const discoveryPath = discoveryPathForKey(discoveryKey);
|
|
76
|
+
const checks = [];
|
|
77
|
+
const discovery = readDiscovery(discoveryKey);
|
|
78
|
+
const tokenMode = discovery?.authMode === "token";
|
|
79
|
+
const paired = deps.pairedConfigPresent !== void 0 ? deps.pairedConfigPresent : loadPairedConfig() !== null;
|
|
80
|
+
checks.push(
|
|
81
|
+
tokenMode ? {
|
|
82
|
+
name: "paired config",
|
|
83
|
+
ok: true,
|
|
84
|
+
detail: "n/a (token mode) \u2014 proxy launched with --token; no ~/.kojee/config.json by design"
|
|
85
|
+
} : {
|
|
86
|
+
name: "paired config",
|
|
87
|
+
ok: paired,
|
|
88
|
+
detail: paired ? "present" : "MISSING \u2014 run `kojee-mcp pair <code> --url <broker>`"
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
let logPath = null;
|
|
92
|
+
if (!discovery) {
|
|
93
|
+
checks.push({
|
|
94
|
+
name: "session discovery",
|
|
95
|
+
ok: false,
|
|
96
|
+
detail: `no discovery file at ${discoveryPath} (key=${discoveryKey}) \u2014 proxy not running for this session?`
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
logPath = discovery.eventLogPath ?? null;
|
|
100
|
+
const proxyPid = discovery.proxyPid ?? discovery.pid;
|
|
101
|
+
const alive = typeof proxyPid === "number" && isPidAlive(proxyPid);
|
|
102
|
+
checks.push({
|
|
103
|
+
name: "proxy process",
|
|
104
|
+
ok: alive,
|
|
105
|
+
detail: alive ? `alive (pid=${proxyPid})` : `DEAD (pid=${proxyPid ?? "?"}) \u2014 restart Claude Code`
|
|
106
|
+
});
|
|
107
|
+
const base = `http://127.0.0.1:${discovery.port}`;
|
|
108
|
+
const health = await probeJson(fetchFn, `${base}/health`);
|
|
109
|
+
checks.push({
|
|
110
|
+
name: "hook-server /health",
|
|
111
|
+
ok: health !== null,
|
|
112
|
+
detail: health !== null ? "ok" : "unreachable"
|
|
113
|
+
});
|
|
114
|
+
const statusProbe = await probeJsonWithStatus(fetchFn, `${base}/status`);
|
|
115
|
+
if (statusProbe.json === null && statusProbe.routeAbsent) {
|
|
116
|
+
checks.push({
|
|
117
|
+
name: "hook-server /status",
|
|
118
|
+
ok: "unknown",
|
|
119
|
+
detail: "proxy predates /status (old version) \u2014 stream state unknown; restart the session to upgrade"
|
|
120
|
+
});
|
|
121
|
+
} else if (statusProbe.json === null) {
|
|
122
|
+
checks.push({ name: "hook-server /status", ok: false, detail: "unreachable" });
|
|
123
|
+
} else {
|
|
124
|
+
const status = statusProbe.json;
|
|
125
|
+
const s = status;
|
|
126
|
+
const connected = s.connected === true;
|
|
127
|
+
const stale = s.stale === true;
|
|
128
|
+
const hbAge = s.lastHeartbeatAgeMs;
|
|
129
|
+
const hbStr = hbAge === null || hbAge === void 0 ? "no heartbeat yet" : `last heartbeat ${Math.round(hbAge / 1e3)}s ago`;
|
|
130
|
+
checks.push({
|
|
131
|
+
name: "SSE stream",
|
|
132
|
+
ok: connected && !stale ? true : connected ? "warn" : false,
|
|
133
|
+
detail: `${s.stream ?? "?"}; ${hbStr}; subscribed=${s.subscribedTandemCount ?? 0}; reconnects=${s.reconnectCount ?? 0}${stale ? " \u2014 STALE" : ""}`
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (logPath) {
|
|
138
|
+
const logStat = statFile(logPath);
|
|
139
|
+
if (!logStat) {
|
|
140
|
+
checks.push({ name: "messages log", ok: false, detail: `missing at ${logPath}` });
|
|
141
|
+
} else {
|
|
142
|
+
const ageMs = Date.now() - logStat.mtimeMs;
|
|
143
|
+
checks.push({
|
|
144
|
+
name: "messages log",
|
|
145
|
+
ok: true,
|
|
146
|
+
detail: `${logPath} (${logStat.size} bytes, mtime ${Math.round(ageMs / 1e3)}s ago)` + (logStat.size === 0 ? " \u2014 no Tandem messages yet (normal)" : "")
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const statusPath = statusLogPath(logPath);
|
|
150
|
+
const statusStat = statFile(statusPath);
|
|
151
|
+
const lastStatus = readLastStatusLine(statusPath);
|
|
152
|
+
if (!statusStat) {
|
|
153
|
+
checks.push({ name: "status stream", ok: "warn", detail: `no status sibling at ${statusPath}` });
|
|
154
|
+
} else {
|
|
155
|
+
const sAge = Date.now() - statusStat.mtimeMs;
|
|
156
|
+
checks.push({
|
|
157
|
+
name: "status stream",
|
|
158
|
+
ok: true,
|
|
159
|
+
detail: `${statusPath} (${statusStat.size} bytes, mtime ${Math.round(sAge / 1e3)}s ago)` + (lastStatus ? `
|
|
160
|
+
last status: ${lastStatus.trim()}` : " \u2014 no status lines yet")
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const sentinel = monitorHeartbeatPath(logPath);
|
|
164
|
+
const sentinelStat = statFile(sentinel);
|
|
165
|
+
const sentinelFresh = sentinelStat !== null && Date.now() - sentinelStat.mtimeMs < 5e3;
|
|
166
|
+
checks.push({
|
|
167
|
+
name: "Monitor (tail) heartbeat",
|
|
168
|
+
ok: sentinelFresh ? true : "warn",
|
|
169
|
+
detail: sentinelFresh ? "live (a Monitor is reading the log)" : "no fresh sentinel \u2014 spawn the Monitor (recipe below)"
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
checks.push({
|
|
173
|
+
name: "wire contract",
|
|
174
|
+
ok: "unknown",
|
|
175
|
+
detail: "fixture-vs-wire drift is NOT unit-testable \u2014 periodically reconcile the SSE wire `kind` enum (spec [tell,ask,ack,status] vs live [message,status,system]) against a live backend; see tests/fixtures/tandem-event.ts NOTE 7."
|
|
176
|
+
});
|
|
177
|
+
const recipe = logPath ? { monitorSpawn: buildMonitorSpawn(logPath), reply: buildReplyRecipe() } : null;
|
|
178
|
+
const verdict = checks.some((c) => c.ok === false) ? "broken" : checks.some((c) => c.ok === "warn") ? "degraded" : "healthy";
|
|
179
|
+
return { discoveryKey, discoveryPath, logPath, checks, recipe, verdict };
|
|
180
|
+
}
|
|
181
|
+
async function probeJson(fetchFn, url) {
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetchFn(url);
|
|
184
|
+
if (!res.ok) return null;
|
|
185
|
+
return await res.json();
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function probeJsonWithStatus(fetchFn, url) {
|
|
191
|
+
try {
|
|
192
|
+
const res = await fetchFn(url);
|
|
193
|
+
if (res.ok) return { json: await res.json(), routeAbsent: false };
|
|
194
|
+
const routeAbsent = res.status === 404 || res.status === 405;
|
|
195
|
+
return { json: null, routeAbsent };
|
|
196
|
+
} catch {
|
|
197
|
+
return { json: null, routeAbsent: false };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function formatDoctorReport(report) {
|
|
201
|
+
const mark = (ok) => ok === true ? "\u2713" : ok === "warn" ? "\u26A0" : ok === "unknown" ? "?" : "\u2717";
|
|
202
|
+
const lines = [];
|
|
203
|
+
lines.push(`kojee-mcp doctor \u2014 verdict: ${report.verdict.toUpperCase()}`);
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push(` discovery key: ${report.discoveryKey}`);
|
|
206
|
+
for (const c of report.checks) {
|
|
207
|
+
lines.push(` ${mark(c.ok)} ${c.name}: ${c.detail}`);
|
|
208
|
+
}
|
|
209
|
+
lines.push("");
|
|
210
|
+
if (report.recipe) {
|
|
211
|
+
lines.push("Wake recipe (spawn once at session start):");
|
|
212
|
+
lines.push(` ${report.recipe.monitorSpawn}`);
|
|
213
|
+
lines.push(`Then ${report.recipe.reply}.`);
|
|
214
|
+
} else {
|
|
215
|
+
lines.push("No event-log path resolved \u2014 the proxy isn't running for this session.");
|
|
216
|
+
lines.push("Start Claude Code (which spawns the kojee proxy), then re-run doctor.");
|
|
217
|
+
}
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
async function runDoctor() {
|
|
221
|
+
const { readRecordedRuntime } = await import("./runtime-record-WO4IECM6.js");
|
|
222
|
+
if (readRecordedRuntime() === "codex") {
|
|
223
|
+
const { collectCodexDoctorReport, formatCodexDoctorReport } = await import("./doctor-codex-BMI5JOO6.js");
|
|
224
|
+
const report2 = collectCodexDoctorReport();
|
|
225
|
+
console.error(formatCodexDoctorReport(report2));
|
|
226
|
+
return report2.verdict === "broken" ? 1 : 0;
|
|
227
|
+
}
|
|
228
|
+
const report = await collectDoctorReport();
|
|
229
|
+
console.error(formatDoctorReport(report));
|
|
230
|
+
return report.verdict === "broken" ? 1 : 0;
|
|
231
|
+
}
|
|
232
|
+
export {
|
|
233
|
+
collectDoctorReport,
|
|
234
|
+
defaultReadLastStatusLine,
|
|
235
|
+
formatDoctorReport,
|
|
236
|
+
runDoctor
|
|
237
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultCodexConfigPath,
|
|
3
|
+
defaultCodexHooksPath
|
|
4
|
+
} from "./chunk-ZW4SW7LJ.js";
|
|
5
|
+
import "./chunk-SQL56SEB.js";
|
|
6
|
+
import {
|
|
7
|
+
CODEX_LISTEN_CAP_MS
|
|
8
|
+
} from "./chunk-C6GZ2L2W.js";
|
|
9
|
+
import "./chunk-BLEGIR35.js";
|
|
10
|
+
import {
|
|
11
|
+
resolveWebhookConfig
|
|
12
|
+
} from "./chunk-F7L25L2J.js";
|
|
13
|
+
|
|
14
|
+
// src/doctor-codex.ts
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
function parseCodexEnvFromToml(toml) {
|
|
17
|
+
const env = {};
|
|
18
|
+
let inEnv = false;
|
|
19
|
+
for (const line of toml.split("\n")) {
|
|
20
|
+
const header = line.trim();
|
|
21
|
+
const isTableHeader = /^\[\[?[^\]]+\]\]?$/.test(header);
|
|
22
|
+
if (isTableHeader) {
|
|
23
|
+
inEnv = header === "[mcp_servers.kojee.env]";
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!inEnv) continue;
|
|
27
|
+
const eq = line.indexOf("=");
|
|
28
|
+
if (eq <= 0) continue;
|
|
29
|
+
const key = line.slice(0, eq).trim();
|
|
30
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) continue;
|
|
31
|
+
let value = line.slice(eq + 1).trim();
|
|
32
|
+
const quote = value[0];
|
|
33
|
+
if ((quote === '"' || quote === "'") && value.endsWith(quote)) {
|
|
34
|
+
value = value.slice(1, -1);
|
|
35
|
+
if (quote === '"') value = value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
36
|
+
}
|
|
37
|
+
env[key] = value;
|
|
38
|
+
}
|
|
39
|
+
return env;
|
|
40
|
+
}
|
|
41
|
+
var WIZARD_RERUN = "re-run `kojee-mcp init --runtime codex`";
|
|
42
|
+
function defaultReadFile(path) {
|
|
43
|
+
return () => {
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(path, "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function collectCodexDoctorReport(deps = {}) {
|
|
52
|
+
const readConfigToml = deps.readConfigToml ?? defaultReadFile(defaultCodexConfigPath());
|
|
53
|
+
const readHooksJson = deps.readHooksJson ?? defaultReadFile(defaultCodexHooksPath());
|
|
54
|
+
const checks = [];
|
|
55
|
+
const toml = readConfigToml() ?? "";
|
|
56
|
+
const env = deps.env ?? parseCodexEnvFromToml(toml);
|
|
57
|
+
const hasKojeeTable = toml.includes("[mcp_servers.kojee]");
|
|
58
|
+
const hasRuntimeEnv = /KOJEE_RUNTIME\s*=\s*"codex"/.test(toml);
|
|
59
|
+
const configOk = hasKojeeTable && hasRuntimeEnv;
|
|
60
|
+
checks.push({
|
|
61
|
+
name: "~/.codex/config.toml [mcp_servers.kojee]",
|
|
62
|
+
ok: configOk,
|
|
63
|
+
detail: configOk ? 'present with env.KOJEE_RUNTIME="codex" (Tier-1 contract)' : `MISSING [mcp_servers.kojee] or env.KOJEE_RUNTIME="codex" \u2014 ${WIZARD_RERUN}`
|
|
64
|
+
});
|
|
65
|
+
const hooksRaw = readHooksJson() ?? "{}";
|
|
66
|
+
let hookPresent = false;
|
|
67
|
+
try {
|
|
68
|
+
const hooks = JSON.parse(hooksRaw);
|
|
69
|
+
hookPresent = !!hooks.hooks?.Stop?.some(
|
|
70
|
+
(e) => e.hooks?.some((h) => (h.command ?? "").includes("hook --type=codex-stop"))
|
|
71
|
+
);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
if (!hookPresent && /hook --type=codex-stop/.test(toml)) hookPresent = true;
|
|
75
|
+
checks.push({
|
|
76
|
+
name: "Codex Stop hook (codex-stop)",
|
|
77
|
+
ok: hookPresent,
|
|
78
|
+
detail: hookPresent ? "present (fast PEEK + model-chosen bounded listen)" : `MISSING in ~/.codex/hooks.json / [[hooks.Stop]] \u2014 ${WIZARD_RERUN}`
|
|
79
|
+
});
|
|
80
|
+
const resolution = resolveWebhookConfig(env);
|
|
81
|
+
if (resolution.enabled && resolution.config) {
|
|
82
|
+
checks.push({
|
|
83
|
+
name: "webhook sink",
|
|
84
|
+
ok: true,
|
|
85
|
+
detail: `enabled \u2014 ${resolution.config.redactedSummary}`
|
|
86
|
+
});
|
|
87
|
+
} else if (resolution.error) {
|
|
88
|
+
checks.push({
|
|
89
|
+
name: "webhook sink",
|
|
90
|
+
ok: false,
|
|
91
|
+
detail: `${resolution.error} \u2014 ${WIZARD_RERUN} (it generates a secret)`
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
checks.push({
|
|
95
|
+
name: "webhook sink",
|
|
96
|
+
ok: false,
|
|
97
|
+
detail: `not configured (no KOJEE_WEBHOOK_URL) \u2014 ${WIZARD_RERUN}`
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (deps.pingReceiver) {
|
|
101
|
+
const reachable = deps.pingReceiver();
|
|
102
|
+
checks.push({
|
|
103
|
+
name: "webhook receiver (optional ping)",
|
|
104
|
+
ok: reachable ? true : "warn",
|
|
105
|
+
detail: reachable ? "reachable" : "unreachable \u2014 stand up your receiver at KOJEE_WEBHOOK_URL (owner-built; see doctor note)"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const verdict = checks.some((c) => c.ok === false) ? "broken" : checks.some((c) => c.ok === "warn") ? "degraded" : "healthy";
|
|
109
|
+
return { checks, verdict };
|
|
110
|
+
}
|
|
111
|
+
function formatCodexDoctorReport(report) {
|
|
112
|
+
const mark = (ok) => ok === true ? "\u2713" : ok === "warn" ? "\u26A0" : ok === "unknown" ? "?" : "\u2717";
|
|
113
|
+
const lines = [];
|
|
114
|
+
lines.push(`kojee-mcp doctor (codex) \u2014 verdict: ${report.verdict.toUpperCase()}`);
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push(" Wake mode: webhook-sink + stop-hook peek (Codex has no channel injection).");
|
|
117
|
+
lines.push(` Bounded-listen cap: ${CODEX_LISTEN_CAP_MS}ms (model picks listen vs drain vs ignore).`);
|
|
118
|
+
lines.push("");
|
|
119
|
+
for (const c of report.checks) {
|
|
120
|
+
lines.push(` ${mark(c.ok)} ${c.name}: ${c.detail}`);
|
|
121
|
+
}
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push("NOTE: live Codex verification (hook fires, MCP connects, bounded listen) is an owner step.");
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
collectCodexDoctorReport,
|
|
128
|
+
formatCodexDoctorReport,
|
|
129
|
+
parseCodexEnvFromToml
|
|
130
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
STATUS_LINE_PREFIX,
|
|
3
|
+
monitorHeartbeatPath,
|
|
4
|
+
nudgeSentinelPath,
|
|
5
|
+
startEventLog,
|
|
6
|
+
statusLogPath,
|
|
7
|
+
sweepStaleEventLogs
|
|
8
|
+
} from "./chunk-2TUAFAIW.js";
|
|
9
|
+
import "./chunk-DO42NPNR.js";
|
|
10
|
+
import "./chunk-BLEGIR35.js";
|
|
11
|
+
export {
|
|
12
|
+
STATUS_LINE_PREFIX,
|
|
13
|
+
monitorHeartbeatPath,
|
|
14
|
+
nudgeSentinelPath,
|
|
15
|
+
startEventLog,
|
|
16
|
+
statusLogPath,
|
|
17
|
+
sweepStaleEventLogs
|
|
18
|
+
};
|
|
@@ -24,6 +24,9 @@ async function handleRequest(req, res, opts) {
|
|
|
24
24
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
25
25
|
return json(res, 200, { ok: true });
|
|
26
26
|
}
|
|
27
|
+
if (req.method === "GET" && url.pathname === "/status") {
|
|
28
|
+
return respondWithStatus(res, opts);
|
|
29
|
+
}
|
|
27
30
|
if (req.method === "GET" && url.pathname === "/poll") {
|
|
28
31
|
const type = url.searchParams.get("type") ?? "";
|
|
29
32
|
const timeoutMs = Number.parseInt(url.searchParams.get("timeout_ms") ?? "0", 10);
|
|
@@ -42,6 +45,31 @@ function respondWithEvents(res, opts) {
|
|
|
42
45
|
const events = entries.map((entry) => opts.adapter.formatTandemEvent(entry.event));
|
|
43
46
|
json(res, 200, { events, count: events.length });
|
|
44
47
|
}
|
|
48
|
+
function respondWithStatus(res, opts) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (!opts.getStreamState) {
|
|
51
|
+
return json(res, 200, { stream: "unknown", now });
|
|
52
|
+
}
|
|
53
|
+
const s = opts.getStreamState();
|
|
54
|
+
const lastEventAgeMs = s.lastEventAt === null ? null : now - s.lastEventAt;
|
|
55
|
+
const lastHeartbeatAgeMs = s.lastHeartbeatAt === null ? null : now - s.lastHeartbeatAt;
|
|
56
|
+
const stale = s.connected && s.staleAfterMs !== null && lastEventAgeMs !== null && lastEventAgeMs >= s.staleAfterMs;
|
|
57
|
+
json(res, 200, {
|
|
58
|
+
stream: s.connected ? "connected" : "disconnected",
|
|
59
|
+
connected: s.connected,
|
|
60
|
+
connectedSince: s.connectedSince,
|
|
61
|
+
lastEventAt: s.lastEventAt,
|
|
62
|
+
lastEventAgeMs,
|
|
63
|
+
lastHeartbeatAt: s.lastHeartbeatAt,
|
|
64
|
+
lastHeartbeatAgeMs,
|
|
65
|
+
reconnectCount: s.reconnectCount,
|
|
66
|
+
subscribedTandemCount: Object.keys(s.cursors).length,
|
|
67
|
+
cursors: s.cursors,
|
|
68
|
+
staleAfterMs: s.staleAfterMs,
|
|
69
|
+
stale,
|
|
70
|
+
now
|
|
71
|
+
});
|
|
72
|
+
}
|
|
45
73
|
async function longPollAndRespond(res, opts, timeoutMs) {
|
|
46
74
|
const immediate = opts.queue.takeForHook();
|
|
47
75
|
if (immediate.length > 0) {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,15 @@ interface ProxyConfig {
|
|
|
2
2
|
token: string;
|
|
3
3
|
url: string;
|
|
4
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";
|
|
5
14
|
}
|
|
6
15
|
|
|
7
16
|
declare function startProxy(config: ProxyConfig): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
startProxy
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-YEC7IHIG.js";
|
|
4
|
+
import "./chunk-BJMASMKX.js";
|
|
5
|
+
import "./chunk-C6GZ2L2W.js";
|
|
6
|
+
import "./chunk-WBMX4CHB.js";
|
|
7
|
+
import "./chunk-BLEGIR35.js";
|
|
5
8
|
export {
|
|
6
9
|
startProxy
|
|
7
10
|
};
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
secureFile
|
|
3
|
+
} from "./chunk-BLEGIR35.js";
|
|
4
|
+
|
|
1
5
|
// src/hooks/install.ts
|
|
2
6
|
import fs from "fs";
|
|
7
|
+
import os from "os";
|
|
3
8
|
import path from "path";
|
|
4
9
|
function discoverInstallTargets() {
|
|
5
|
-
const home =
|
|
10
|
+
const home = os.homedir();
|
|
6
11
|
const targets = [];
|
|
7
12
|
const cliPath = path.join(home, ".claude.json");
|
|
8
13
|
const cliHooksPath = path.join(home, ".claude", "settings.json");
|
|
@@ -28,10 +33,10 @@ var MCP_SERVER_CMD = "npx";
|
|
|
28
33
|
var MCP_SERVER_ARGS = ["kojee-mcp"];
|
|
29
34
|
var MCP_SERVER_ENV = { KOJEE_RUNTIME: "claude-code" };
|
|
30
35
|
function defaultConfigPath() {
|
|
31
|
-
return path.join(
|
|
36
|
+
return path.join(os.homedir(), ".claude.json");
|
|
32
37
|
}
|
|
33
38
|
function defaultHooksPath() {
|
|
34
|
-
return path.join(
|
|
39
|
+
return path.join(os.homedir(), ".claude", "settings.json");
|
|
35
40
|
}
|
|
36
41
|
function deriveHooksPath(configPath) {
|
|
37
42
|
return path.join(path.dirname(configPath), ".claude", "settings.json");
|
|
@@ -47,10 +52,7 @@ function writeConfig(p, cfg) {
|
|
|
47
52
|
const dir = path.dirname(p);
|
|
48
53
|
fs.mkdirSync(dir, { recursive: true });
|
|
49
54
|
fs.writeFileSync(p, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
50
|
-
|
|
51
|
-
fs.chmodSync(p, 384);
|
|
52
|
-
} catch {
|
|
53
|
-
}
|
|
55
|
+
secureFile(p);
|
|
54
56
|
}
|
|
55
57
|
function hasKojeeEntry(arr, command) {
|
|
56
58
|
if (!arr) return false;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/tandem/resubscribe.ts
|
|
2
|
+
var DEFAULT_PER_CALL_TIMEOUT_MS = 1e4;
|
|
3
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
4
|
+
var DEFAULT_DEBOUNCE_MS = 3e4;
|
|
5
|
+
async function resubscribeMemberships(opts) {
|
|
6
|
+
const { gateway, eventLog } = opts;
|
|
7
|
+
const perCallTimeoutMs = opts.perCallTimeoutMs ?? DEFAULT_PER_CALL_TIMEOUT_MS;
|
|
8
|
+
const concurrency = Math.max(1, opts.concurrency ?? DEFAULT_CONCURRENCY);
|
|
9
|
+
const now = opts.now ?? Date.now;
|
|
10
|
+
const debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
11
|
+
if (opts.debounceState && now() - opts.debounceState.lastRunAt < debounceMs) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
let tandemIds;
|
|
15
|
+
try {
|
|
16
|
+
tandemIds = opts.listTandems ? await opts.listTandems() : opts.tandemIds ?? [];
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error("[resubscribe] re-list failed:", err.message);
|
|
19
|
+
tandemIds = [];
|
|
20
|
+
}
|
|
21
|
+
let touched = 0;
|
|
22
|
+
async function touchOne(tandemId) {
|
|
23
|
+
const ac = new AbortController();
|
|
24
|
+
const timer = setTimeout(() => ac.abort(), perCallTimeoutMs);
|
|
25
|
+
try {
|
|
26
|
+
const result = await gateway.sendRpc(
|
|
27
|
+
"tools/call",
|
|
28
|
+
{ name: "tandem_messages", arguments: { tandem_id: tandemId, limit: 1 } },
|
|
29
|
+
ac.signal
|
|
30
|
+
);
|
|
31
|
+
const maybeErr = result;
|
|
32
|
+
if (!maybeErr.isError) touched += 1;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(
|
|
35
|
+
`[resubscribe] touch failed for ${tandemId}:`,
|
|
36
|
+
err.message
|
|
37
|
+
);
|
|
38
|
+
} finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let next = 0;
|
|
43
|
+
async function worker() {
|
|
44
|
+
while (next < tandemIds.length) {
|
|
45
|
+
const i = next++;
|
|
46
|
+
await touchOne(tandemIds[i]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await Promise.all(
|
|
50
|
+
Array.from({ length: Math.min(concurrency, tandemIds.length) }, () => worker())
|
|
51
|
+
);
|
|
52
|
+
if (opts.debounceState) opts.debounceState.lastRunAt = now();
|
|
53
|
+
await eventLog?.appendStatus(`status=subscribed n=${tandemIds.length} touched=${touched}`).catch(() => {
|
|
54
|
+
});
|
|
55
|
+
return touched;
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
resubscribeMemberships
|
|
59
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearRuntimeRecord,
|
|
3
|
+
readRecordedRuntime,
|
|
4
|
+
recordRuntime,
|
|
5
|
+
runtimeRecordPath
|
|
6
|
+
} from "./chunk-EW72ZNQL.js";
|
|
7
|
+
import "./chunk-SQL56SEB.js";
|
|
8
|
+
import "./chunk-BLEGIR35.js";
|
|
9
|
+
export {
|
|
10
|
+
clearRuntimeRecord,
|
|
11
|
+
readRecordedRuntime,
|
|
12
|
+
recordRuntime,
|
|
13
|
+
runtimeRecordPath
|
|
14
|
+
};
|