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.
Files changed (38) hide show
  1. package/README.md +98 -10
  2. package/dist/chunk-2TUAFAIW.js +244 -0
  3. package/dist/{chunk-36DMIXH7.js → chunk-BJMASMKX.js} +13 -23
  4. package/dist/chunk-BLEGIR35.js +43 -0
  5. package/dist/chunk-C6GZ2L2W.js +38 -0
  6. package/dist/{chunk-VZVGTHGF.js → chunk-DO42NPNR.js} +11 -17
  7. package/dist/chunk-EW72ZNQL.js +39 -0
  8. package/dist/chunk-F7L25L2J.js +60 -0
  9. package/dist/{chunk-WHTH6WBP.js → chunk-LSUB6QMP.js} +3 -0
  10. package/dist/chunk-LVL25VLO.js +22 -0
  11. package/dist/chunk-SQL56SEB.js +14 -0
  12. package/dist/chunk-WBMX4CHB.js +378 -0
  13. package/dist/{chunk-ZGVUM4AG.js → chunk-YEC7IHIG.js} +276 -318
  14. package/dist/{chunk-E7TE4QZD.js → chunk-YH27B6SW.js} +9 -9
  15. package/dist/chunk-ZW4SW7LJ.js +225 -0
  16. package/dist/cli.js +70 -78
  17. package/dist/codex-stop-hook-JOTBCS5K.js +72 -0
  18. package/dist/doctor-TSHOMT5X.js +237 -0
  19. package/dist/doctor-codex-BMI5JOO6.js +130 -0
  20. package/dist/event-log-RSTM4PLL.js +18 -0
  21. package/dist/{hook-server-43QS7L7P.js → hook-server-QF5JVUHV.js} +28 -0
  22. package/dist/index.d.ts +9 -0
  23. package/dist/index.js +5 -2
  24. package/dist/{install-WV25CRU2.js → install-WBIUVBZW.js} +9 -7
  25. package/dist/{paired-config-OAR3O3XY.js → paired-config-JTFLHMZ2.js} +2 -1
  26. package/dist/resubscribe-SLZNA76S.js +59 -0
  27. package/dist/runtime-record-WO4IECM6.js +14 -0
  28. package/dist/runtimes-CO43XUUK.js +12 -0
  29. package/dist/{session-discovery-WSHLR4OV.js → session-discovery-FNMJGFPM.js} +2 -1
  30. package/dist/stop-hook-SEPWWETV.js +119 -0
  31. package/dist/tail-stream-BYKO4DW6.js +162 -0
  32. package/dist/{user-prompt-submit-hook-WSRIJVF4.js → user-prompt-submit-hook-ARPEO6FF.js} +5 -4
  33. package/dist/webhook-config-5TLLX7RA.js +10 -0
  34. package/dist/webhook-sink-7OYZBWXA.js +163 -0
  35. package/dist/wizard-7KHD5JT4.js +265 -0
  36. package/package.json +9 -7
  37. package/dist/event-log-ETWR6PPY.js +0 -112
  38. package/dist/stop-hook-5XU3EQAE.js +0 -76
@@ -0,0 +1,119 @@
1
+ import {
2
+ readHookStdin
3
+ } from "./chunk-LSUB6QMP.js";
4
+ import {
5
+ deriveDiscoveryKey,
6
+ findClaudeAncestorPid
7
+ } from "./chunk-BJMASMKX.js";
8
+ import {
9
+ buildMonitorNudge
10
+ } from "./chunk-C6GZ2L2W.js";
11
+ import {
12
+ monitorHeartbeatPath,
13
+ nudgeSentinelPath
14
+ } from "./chunk-2TUAFAIW.js";
15
+ import {
16
+ readSessionDiscoveryByKey
17
+ } from "./chunk-DO42NPNR.js";
18
+ import "./chunk-BLEGIR35.js";
19
+
20
+ // src/hooks/stop-hook.ts
21
+ import fs from "fs";
22
+ var STOP_POLL_TIMEOUT_MS = Number.parseInt(
23
+ process.env["KOJEE_STOP_HOOK_TIMEOUT_MS"] ?? "0",
24
+ 10
25
+ );
26
+ var MONITOR_HEARTBEAT_STALE_MS = 5e3;
27
+ var NUDGE_WINDOW_MS = Number.parseInt(
28
+ process.env["KOJEE_STOP_NUDGE_WINDOW_MS"] ?? "300000",
29
+ // 5 min
30
+ 10
31
+ );
32
+ async function decideStopHook(deps) {
33
+ if (deps.stopHookActive) return "{}";
34
+ if (!deps.discovery) return "{}";
35
+ const body = await deps.pollEvents().catch(() => null);
36
+ if (body && body.count > 0) {
37
+ return JSON.stringify({ decision: "block", reason: formatEvents(body.events) });
38
+ }
39
+ const logPath = deps.discovery.eventLogPath;
40
+ if (logPath && deps.logHasContent(logPath) && !deps.monitorIsLive(logPath)) {
41
+ if (deps.nudgedRecently(logPath)) return "{}";
42
+ deps.recordNudge(logPath);
43
+ return JSON.stringify({ decision: "block", reason: buildMonitorNudge(logPath) });
44
+ }
45
+ return "{}";
46
+ }
47
+ function defaultNudgedRecently(logPath) {
48
+ try {
49
+ const { mtimeMs } = fs.statSync(nudgeSentinelPath(logPath));
50
+ return Date.now() - mtimeMs < NUDGE_WINDOW_MS;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ function defaultRecordNudge(logPath) {
56
+ const sentinel = nudgeSentinelPath(logPath);
57
+ const now = /* @__PURE__ */ new Date();
58
+ try {
59
+ fs.utimesSync(sentinel, now, now);
60
+ } catch {
61
+ try {
62
+ fs.writeFileSync(sentinel, "", { mode: 384 });
63
+ } catch {
64
+ }
65
+ }
66
+ }
67
+ async function runStopHook() {
68
+ const { stopHookActive } = await readHookStdin();
69
+ const ccPid = await findClaudeAncestorPid();
70
+ const key = deriveDiscoveryKey(process.env["CLAUDE_PROJECT_DIR"], ccPid);
71
+ const discovery = readSessionDiscoveryByKey(key);
72
+ const out = await decideStopHook({
73
+ stopHookActive,
74
+ discovery: discovery ? { port: discovery.port, eventLogPath: discovery.eventLogPath } : null,
75
+ pollEvents: async () => {
76
+ const res = await fetch(
77
+ `http://127.0.0.1:${discovery.port}/poll?type=stop&timeout_ms=${STOP_POLL_TIMEOUT_MS}`
78
+ );
79
+ if (!res.ok) return { events: [], count: 0 };
80
+ return await res.json();
81
+ },
82
+ logHasContent,
83
+ monitorIsLive,
84
+ nudgedRecently: defaultNudgedRecently,
85
+ recordNudge: defaultRecordNudge
86
+ });
87
+ process.stdout.write(out);
88
+ }
89
+ function logHasContent(logPath) {
90
+ try {
91
+ return fs.statSync(logPath).size > 0;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ function monitorIsLive(logPath) {
97
+ try {
98
+ const { mtimeMs } = fs.statSync(monitorHeartbeatPath(logPath));
99
+ return Date.now() - mtimeMs < MONITOR_HEARTBEAT_STALE_MS;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+ function formatEvents(events) {
105
+ const header = `[${events.length} unread Tandem ${events.length === 1 ? "event" : "events"}]
106
+
107
+ `;
108
+ const bodies = events.map((evt) => {
109
+ const attrs = Object.entries(evt.meta).map(([k, v]) => `${k}="${v}"`).join(" ");
110
+ return `<channel source="kojee-mcp" ${attrs}>
111
+ ${evt.content}
112
+ </channel>`;
113
+ });
114
+ return header + bodies.join("\n\n");
115
+ }
116
+ export {
117
+ decideStopHook,
118
+ runStopHook
119
+ };
@@ -0,0 +1,162 @@
1
+ import {
2
+ createAdaptiveWatchdog
3
+ } from "./chunk-WBMX4CHB.js";
4
+ import {
5
+ STATUS_LINE_PREFIX,
6
+ monitorHeartbeatPath,
7
+ statusLogPath
8
+ } from "./chunk-2TUAFAIW.js";
9
+ import "./chunk-DO42NPNR.js";
10
+ import "./chunk-BLEGIR35.js";
11
+
12
+ // src/tail-stream.ts
13
+ import fs from "fs";
14
+ var STATUS_LINE_RE = new RegExp(`^\\[[^\\]]*\\] ${STATUS_LINE_PREFIX} (.*)$`);
15
+ var HEARTBEAT_FIELDS_RE = /(^|\s)status=heartbeat(\s|$)/;
16
+ var HEARTBEAT_STALE_FLOOR_MS = envInt("KOJEE_TAIL_STALE_MS", 12e4);
17
+ var STALE_CHECK_INTERVAL_MS = envInt("KOJEE_TAIL_CHECK_MS", 5e3);
18
+ function envInt(name, fallback) {
19
+ const v = Number.parseInt(process.env[name] ?? "", 10);
20
+ return Number.isFinite(v) && v > 0 ? v : fallback;
21
+ }
22
+ async function runTail(messagesPath) {
23
+ const statusPath = statusLogPath(messagesPath);
24
+ let lastLifeAt = Date.now();
25
+ const watchdog = createAdaptiveWatchdog({ floorMs: HEARTBEAT_STALE_FLOOR_MS });
26
+ let staleAnnounced = false;
27
+ const heartbeatPath = monitorHeartbeatPath(messagesPath);
28
+ const touchHeartbeat = () => {
29
+ const now = /* @__PURE__ */ new Date();
30
+ try {
31
+ fs.utimesSync(heartbeatPath, now, now);
32
+ } catch {
33
+ try {
34
+ fs.writeFileSync(heartbeatPath, "", { mode: 384 });
35
+ } catch {
36
+ }
37
+ }
38
+ };
39
+ function onMessageLine(line) {
40
+ lastLifeAt = Date.now();
41
+ if (staleAnnounced) {
42
+ staleAnnounced = false;
43
+ process.stdout.write("[kojee] stream recovered \u2014 events flowing again.\n");
44
+ }
45
+ process.stdout.write(line + "\n");
46
+ }
47
+ function onStatusLine(line) {
48
+ const now = Date.now();
49
+ const m = STATUS_LINE_RE.exec(line);
50
+ const fields = m ? m[1] ?? "" : "";
51
+ if (HEARTBEAT_FIELDS_RE.test(fields)) {
52
+ watchdog.onHeartbeat(now);
53
+ }
54
+ lastLifeAt = now;
55
+ if (staleAnnounced) {
56
+ staleAnnounced = false;
57
+ process.stdout.write("[kojee] stream recovered \u2014 heartbeats resumed.\n");
58
+ }
59
+ }
60
+ const followers = [
61
+ makeFollower(messagesPath, onMessageLine, touchHeartbeat),
62
+ makeFollower(statusPath, onStatusLine, touchHeartbeat)
63
+ ];
64
+ void Promise.all(followers.map((f) => f.start()));
65
+ const staleTimer = setInterval(() => {
66
+ if (staleAnnounced) return;
67
+ const now = Date.now();
68
+ if (!watchdog.shouldAbort(lastLifeAt, now)) return;
69
+ staleAnnounced = true;
70
+ const silentMs = now - lastLifeAt;
71
+ process.stdout.write(
72
+ `[kojee] WARNING: no events or heartbeat for ${Math.round(silentMs / 1e3)}s (learned cadence) \u2014 stream may be dead; run \`npx kojee-mcp doctor\`.
73
+ `
74
+ );
75
+ }, STALE_CHECK_INTERVAL_MS);
76
+ await new Promise((resolve) => {
77
+ const cleanup = () => {
78
+ clearInterval(staleTimer);
79
+ for (const f of followers) f.stop();
80
+ try {
81
+ fs.unlinkSync(heartbeatPath);
82
+ } catch {
83
+ }
84
+ resolve();
85
+ };
86
+ process.on("SIGTERM", cleanup);
87
+ process.on("SIGINT", cleanup);
88
+ });
89
+ }
90
+ function makeFollower(filePath, onLine, onActivity) {
91
+ let buffer = "";
92
+ let offset = 0;
93
+ let watcher = null;
94
+ let pollTimer = null;
95
+ let stopped = false;
96
+ async function _drain() {
97
+ let stat;
98
+ try {
99
+ stat = await fs.promises.stat(filePath);
100
+ } catch {
101
+ return;
102
+ }
103
+ if (stat.size < offset) {
104
+ buffer = "";
105
+ offset = 0;
106
+ }
107
+ if (stat.size === offset) return;
108
+ const fh = await fs.promises.open(filePath, "r");
109
+ try {
110
+ const len = stat.size - offset;
111
+ const buf = Buffer.alloc(len);
112
+ await fh.read(buf, 0, len, offset);
113
+ buffer += buf.toString("utf8");
114
+ offset = stat.size;
115
+ let nl;
116
+ while ((nl = buffer.indexOf("\n")) !== -1) {
117
+ const line = buffer.slice(0, nl);
118
+ buffer = buffer.slice(nl + 1);
119
+ onLine(line);
120
+ }
121
+ } finally {
122
+ await fh.close();
123
+ }
124
+ }
125
+ let draining = Promise.resolve();
126
+ const scheduleDrain = () => {
127
+ draining = draining.then(_drain, _drain);
128
+ return draining;
129
+ };
130
+ return {
131
+ async start() {
132
+ while (!stopped && !fs.existsSync(filePath)) {
133
+ await sleep(250);
134
+ }
135
+ if (stopped) return;
136
+ onActivity();
137
+ await scheduleDrain();
138
+ watcher = fs.watch(filePath, { persistent: true });
139
+ watcher.on("change", () => {
140
+ onActivity();
141
+ void scheduleDrain();
142
+ });
143
+ watcher.on("error", () => {
144
+ });
145
+ pollTimer = setInterval(() => {
146
+ onActivity();
147
+ void scheduleDrain();
148
+ }, 1e3);
149
+ },
150
+ stop() {
151
+ stopped = true;
152
+ if (pollTimer) clearInterval(pollTimer);
153
+ if (watcher) watcher.close();
154
+ }
155
+ };
156
+ }
157
+ function sleep(ms) {
158
+ return new Promise((r) => setTimeout(r, ms));
159
+ }
160
+ export {
161
+ runTail
162
+ };
@@ -1,18 +1,19 @@
1
1
  import {
2
2
  readHookStdin
3
- } from "./chunk-WHTH6WBP.js";
3
+ } from "./chunk-LSUB6QMP.js";
4
4
  import {
5
5
  deriveDiscoveryKey,
6
6
  findClaudeAncestorPid
7
- } from "./chunk-36DMIXH7.js";
7
+ } from "./chunk-BJMASMKX.js";
8
8
  import {
9
9
  readSessionDiscoveryByKey
10
- } from "./chunk-VZVGTHGF.js";
10
+ } from "./chunk-DO42NPNR.js";
11
+ import "./chunk-BLEGIR35.js";
11
12
 
12
13
  // src/hooks/user-prompt-submit-hook.ts
13
14
  async function runUserPromptSubmitHook() {
14
15
  await readHookStdin();
15
- const ccPid = findClaudeAncestorPid();
16
+ const ccPid = await findClaudeAncestorPid();
16
17
  const key = deriveDiscoveryKey(process.env["CLAUDE_PROJECT_DIR"], ccPid);
17
18
  const discovery = readSessionDiscoveryByKey(key);
18
19
  if (!discovery) {
@@ -0,0 +1,10 @@
1
+ import {
2
+ WEBHOOK_DEFAULT_MAX_RETRIES,
3
+ WEBHOOK_DEFAULT_TIMEOUT_MS,
4
+ resolveWebhookConfig
5
+ } from "./chunk-F7L25L2J.js";
6
+ export {
7
+ WEBHOOK_DEFAULT_MAX_RETRIES,
8
+ WEBHOOK_DEFAULT_TIMEOUT_MS,
9
+ resolveWebhookConfig
10
+ };
@@ -0,0 +1,163 @@
1
+ // src/tandem/webhook-sink.ts
2
+ import crypto from "crypto";
3
+ import { ulid } from "ulidx";
4
+ var DEFAULT_MAX_QUEUE = 1e3;
5
+ var BACKOFF_BASE_MS = 500;
6
+ var BACKOFF_CEILING_MS = 3e4;
7
+ function computeWebhookSignature(secret, rawBody) {
8
+ return crypto.createHmac("sha256", secret).update(Buffer.from(rawBody, "utf8")).digest("hex");
9
+ }
10
+ function defaultBackoffMs(attempt) {
11
+ const exp = Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_CEILING_MS);
12
+ return Math.random() * exp;
13
+ }
14
+ function classifyStatus(status) {
15
+ if (status >= 200 && status < 300) return "success";
16
+ if (status === 408 || status === 429) return "retry";
17
+ if (status >= 500) return "retry";
18
+ return "permanent";
19
+ }
20
+ function sleep(ms) {
21
+ if (ms <= 0) return Promise.resolve();
22
+ return new Promise((r) => setTimeout(r, ms));
23
+ }
24
+ function createWebhookSink(config, options = {}) {
25
+ const fetchImpl = options.fetchImpl ?? fetch;
26
+ const maxQueue = options.maxQueue ?? DEFAULT_MAX_QUEUE;
27
+ const backoffMs = options.backoffMs ?? defaultBackoffMs;
28
+ const log = options.log ?? ((line) => console.error(`[webhook] ${line}`));
29
+ const queue = [];
30
+ let inFlight = false;
31
+ let stopped = false;
32
+ let draining = false;
33
+ const stats = {
34
+ delivered: 0,
35
+ dropped: 0,
36
+ overflowDropped: 0,
37
+ queueDepth: 0,
38
+ lastOutcome: "none",
39
+ lastAttemptAt: null
40
+ };
41
+ let idleResolvers = [];
42
+ function maybeResolveIdle() {
43
+ if (queue.length === 0 && !inFlight) {
44
+ const r = idleResolvers;
45
+ idleResolvers = [];
46
+ for (const fn of r) fn();
47
+ }
48
+ }
49
+ function currentDepth() {
50
+ return queue.length + (inFlight ? 1 : 0);
51
+ }
52
+ function prepare(event) {
53
+ const body = JSON.stringify(event);
54
+ const signature = computeWebhookSignature(config.secret, body);
55
+ const deliveryId = event.id || ulid();
56
+ return { event, body, signature, deliveryId };
57
+ }
58
+ async function attempt(item) {
59
+ const controller = new AbortController();
60
+ const timer = setTimeout(() => controller.abort(), config.timeoutMs);
61
+ try {
62
+ const res = await fetchImpl(config.url, {
63
+ method: "POST",
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ // hex SHA-256 HMAC of the RAW body bytes — the receiver re-verifies.
67
+ "X-Kojee-Signature": item.signature,
68
+ "X-Kojee-Delivery": item.deliveryId
69
+ },
70
+ body: item.body,
71
+ signal: controller.signal
72
+ });
73
+ return classifyStatus(res.status);
74
+ } catch {
75
+ return "retry";
76
+ } finally {
77
+ clearTimeout(timer);
78
+ }
79
+ }
80
+ async function deliver(item) {
81
+ for (let n = 0; n <= config.maxRetries; n++) {
82
+ if (stopped) return;
83
+ const outcome = await attempt(item);
84
+ if (outcome === "success") {
85
+ stats.delivered += 1;
86
+ stats.lastOutcome = "delivered";
87
+ stats.lastAttemptAt = Date.now();
88
+ return;
89
+ }
90
+ if (outcome === "permanent") {
91
+ stats.dropped += 1;
92
+ stats.lastOutcome = "dropped";
93
+ stats.lastAttemptAt = Date.now();
94
+ log(`delivery=${item.deliveryId} dropped: permanent failure (4xx rejection), no retry`);
95
+ return;
96
+ }
97
+ if (n < config.maxRetries) {
98
+ await sleep(backoffMs(n));
99
+ }
100
+ }
101
+ stats.dropped += 1;
102
+ stats.lastOutcome = "dropped";
103
+ stats.lastAttemptAt = Date.now();
104
+ log(`delivery=${item.deliveryId} dropped: retries exhausted (${config.maxRetries})`);
105
+ }
106
+ async function drain() {
107
+ if (draining) return;
108
+ draining = true;
109
+ try {
110
+ while (queue.length > 0 && !stopped) {
111
+ const item = queue.shift();
112
+ inFlight = true;
113
+ stats.queueDepth = currentDepth();
114
+ try {
115
+ await deliver(item);
116
+ } catch {
117
+ stats.dropped += 1;
118
+ } finally {
119
+ inFlight = false;
120
+ stats.queueDepth = currentDepth();
121
+ }
122
+ }
123
+ } finally {
124
+ draining = false;
125
+ maybeResolveIdle();
126
+ }
127
+ }
128
+ return {
129
+ enqueue(event) {
130
+ try {
131
+ if (stopped) return;
132
+ if (queue.length >= maxQueue) {
133
+ stats.overflowDropped += 1;
134
+ log(`queue overflow (cap=${maxQueue}) \u2014 dropping newest event; receiver will redeliver on replay`);
135
+ return;
136
+ }
137
+ queue.push(prepare(event));
138
+ stats.queueDepth = currentDepth();
139
+ void drain();
140
+ } catch {
141
+ }
142
+ },
143
+ stats() {
144
+ return { ...stats, queueDepth: currentDepth() };
145
+ },
146
+ configSummary() {
147
+ return config.redactedSummary;
148
+ },
149
+ idle() {
150
+ if (queue.length === 0 && !inFlight) return Promise.resolve();
151
+ return new Promise((resolve) => idleResolvers.push(resolve));
152
+ },
153
+ async stop() {
154
+ stopped = true;
155
+ queue.length = 0;
156
+ maybeResolveIdle();
157
+ }
158
+ };
159
+ }
160
+ export {
161
+ computeWebhookSignature,
162
+ createWebhookSink
163
+ };