kojee-mcp 0.4.0 → 0.5.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.
@@ -0,0 +1,161 @@
1
+ import {
2
+ createAdaptiveWatchdog
3
+ } from "./chunk-QB22PD6T.js";
4
+ import {
5
+ STATUS_LINE_PREFIX,
6
+ monitorHeartbeatPath,
7
+ statusLogPath
8
+ } from "./chunk-VLZADEFC.js";
9
+ import "./chunk-W6YRLSD4.js";
10
+
11
+ // src/tail-stream.ts
12
+ import fs from "fs";
13
+ var STATUS_LINE_RE = new RegExp(`^\\[[^\\]]*\\] ${STATUS_LINE_PREFIX} (.*)$`);
14
+ var HEARTBEAT_FIELDS_RE = /(^|\s)status=heartbeat(\s|$)/;
15
+ var HEARTBEAT_STALE_FLOOR_MS = envInt("KOJEE_TAIL_STALE_MS", 12e4);
16
+ var STALE_CHECK_INTERVAL_MS = envInt("KOJEE_TAIL_CHECK_MS", 5e3);
17
+ function envInt(name, fallback) {
18
+ const v = Number.parseInt(process.env[name] ?? "", 10);
19
+ return Number.isFinite(v) && v > 0 ? v : fallback;
20
+ }
21
+ async function runTail(messagesPath) {
22
+ const statusPath = statusLogPath(messagesPath);
23
+ let lastLifeAt = Date.now();
24
+ const watchdog = createAdaptiveWatchdog({ floorMs: HEARTBEAT_STALE_FLOOR_MS });
25
+ let staleAnnounced = false;
26
+ const heartbeatPath = monitorHeartbeatPath(messagesPath);
27
+ const touchHeartbeat = () => {
28
+ const now = /* @__PURE__ */ new Date();
29
+ try {
30
+ fs.utimesSync(heartbeatPath, now, now);
31
+ } catch {
32
+ try {
33
+ fs.writeFileSync(heartbeatPath, "", { mode: 384 });
34
+ } catch {
35
+ }
36
+ }
37
+ };
38
+ function onMessageLine(line) {
39
+ lastLifeAt = Date.now();
40
+ if (staleAnnounced) {
41
+ staleAnnounced = false;
42
+ process.stdout.write("[kojee] stream recovered \u2014 events flowing again.\n");
43
+ }
44
+ process.stdout.write(line + "\n");
45
+ }
46
+ function onStatusLine(line) {
47
+ const now = Date.now();
48
+ const m = STATUS_LINE_RE.exec(line);
49
+ const fields = m ? m[1] ?? "" : "";
50
+ if (HEARTBEAT_FIELDS_RE.test(fields)) {
51
+ watchdog.onHeartbeat(now);
52
+ }
53
+ lastLifeAt = now;
54
+ if (staleAnnounced) {
55
+ staleAnnounced = false;
56
+ process.stdout.write("[kojee] stream recovered \u2014 heartbeats resumed.\n");
57
+ }
58
+ }
59
+ const followers = [
60
+ makeFollower(messagesPath, onMessageLine, touchHeartbeat),
61
+ makeFollower(statusPath, onStatusLine, touchHeartbeat)
62
+ ];
63
+ void Promise.all(followers.map((f) => f.start()));
64
+ const staleTimer = setInterval(() => {
65
+ if (staleAnnounced) return;
66
+ const now = Date.now();
67
+ if (!watchdog.shouldAbort(lastLifeAt, now)) return;
68
+ staleAnnounced = true;
69
+ const silentMs = now - lastLifeAt;
70
+ process.stdout.write(
71
+ `[kojee] WARNING: no events or heartbeat for ${Math.round(silentMs / 1e3)}s (learned cadence) \u2014 stream may be dead; run \`npx kojee-mcp doctor\`.
72
+ `
73
+ );
74
+ }, STALE_CHECK_INTERVAL_MS);
75
+ await new Promise((resolve) => {
76
+ const cleanup = () => {
77
+ clearInterval(staleTimer);
78
+ for (const f of followers) f.stop();
79
+ try {
80
+ fs.unlinkSync(heartbeatPath);
81
+ } catch {
82
+ }
83
+ resolve();
84
+ };
85
+ process.on("SIGTERM", cleanup);
86
+ process.on("SIGINT", cleanup);
87
+ });
88
+ }
89
+ function makeFollower(filePath, onLine, onActivity) {
90
+ let buffer = "";
91
+ let offset = 0;
92
+ let watcher = null;
93
+ let pollTimer = null;
94
+ let stopped = false;
95
+ async function _drain() {
96
+ let stat;
97
+ try {
98
+ stat = await fs.promises.stat(filePath);
99
+ } catch {
100
+ return;
101
+ }
102
+ if (stat.size < offset) {
103
+ buffer = "";
104
+ offset = 0;
105
+ }
106
+ if (stat.size === offset) return;
107
+ const fh = await fs.promises.open(filePath, "r");
108
+ try {
109
+ const len = stat.size - offset;
110
+ const buf = Buffer.alloc(len);
111
+ await fh.read(buf, 0, len, offset);
112
+ buffer += buf.toString("utf8");
113
+ offset = stat.size;
114
+ let nl;
115
+ while ((nl = buffer.indexOf("\n")) !== -1) {
116
+ const line = buffer.slice(0, nl);
117
+ buffer = buffer.slice(nl + 1);
118
+ onLine(line);
119
+ }
120
+ } finally {
121
+ await fh.close();
122
+ }
123
+ }
124
+ let draining = Promise.resolve();
125
+ const scheduleDrain = () => {
126
+ draining = draining.then(_drain, _drain);
127
+ return draining;
128
+ };
129
+ return {
130
+ async start() {
131
+ while (!stopped && !fs.existsSync(filePath)) {
132
+ await sleep(250);
133
+ }
134
+ if (stopped) return;
135
+ onActivity();
136
+ await scheduleDrain();
137
+ watcher = fs.watch(filePath, { persistent: true });
138
+ watcher.on("change", () => {
139
+ onActivity();
140
+ void scheduleDrain();
141
+ });
142
+ watcher.on("error", () => {
143
+ });
144
+ pollTimer = setInterval(() => {
145
+ onActivity();
146
+ void scheduleDrain();
147
+ }, 1e3);
148
+ },
149
+ stop() {
150
+ stopped = true;
151
+ if (pollTimer) clearInterval(pollTimer);
152
+ if (watcher) watcher.close();
153
+ }
154
+ };
155
+ }
156
+ function sleep(ms) {
157
+ return new Promise((r) => setTimeout(r, ms));
158
+ }
159
+ export {
160
+ runTail
161
+ };
@@ -1,18 +1,18 @@
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-W6YRLSD4.js";
11
11
 
12
12
  // src/hooks/user-prompt-submit-hook.ts
13
13
  async function runUserPromptSubmitHook() {
14
14
  await readHookStdin();
15
- const ccPid = findClaudeAncestorPid();
15
+ const ccPid = await findClaudeAncestorPid();
16
16
  const key = deriveDiscoveryKey(process.env["CLAUDE_PROJECT_DIR"], ccPid);
17
17
  const discovery = readSessionDiscoveryByKey(key);
18
18
  if (!discovery) {
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "kojee-mcp": "dist/cli.js"
8
8
  },
9
+ "_buildNote": "src/version.ts resolves package.json via '../package.json' from import.meta.url; this assumes dist output stays flat one level under the package root (tsup default outDir=dist, no nesting). If the build adds outDir nesting or a deeper entry, update version.ts's relative path.",
9
10
  "scripts": {
10
11
  "build": "tsup src/cli.ts src/index.ts --format esm --dts --clean",
11
12
  "dev": "tsup src/cli.ts --format esm --watch",
@@ -19,16 +20,17 @@
19
20
  ],
20
21
  "dependencies": {
21
22
  "@modelcontextprotocol/sdk": "latest",
22
- "jose": "^5.0.0",
23
23
  "commander": "^12.0.0",
24
- "zod": "^3.22.0",
25
- "ulidx": "^2.3.0"
24
+ "jose": "^5.0.0",
25
+ "ps-list": "^8.1.1",
26
+ "ulidx": "^2.3.0",
27
+ "zod": "^3.22.0"
26
28
  },
27
29
  "devDependencies": {
28
- "typescript": "^5.4.0",
29
30
  "@types/node": "^20.0.0",
30
31
  "tsup": "^8.0.0",
31
- "vitest": "^1.0.0",
32
- "tsx": "^4.7.0"
32
+ "tsx": "^4.7.0",
33
+ "typescript": "^5.4.0",
34
+ "vitest": "^1.0.0"
33
35
  }
34
36
  }
@@ -1,112 +0,0 @@
1
- import {
2
- sessionDiscoveryDir
3
- } from "./chunk-VZVGTHGF.js";
4
-
5
- // src/tandem/event-log.ts
6
- import fs from "fs";
7
- import path from "path";
8
- var DEFAULT_DIR = "/tmp";
9
- var MAX_BODY_CHARS = 200;
10
- var DEFAULT_MIN_AGE_MS = 6e4;
11
- function startEventLog(opts) {
12
- const dir = opts.dir ?? DEFAULT_DIR;
13
- const filePath = path.join(dir, `kojee-events-${opts.key}.log`);
14
- fs.mkdirSync(dir, { recursive: true });
15
- fs.writeFileSync(filePath, "", { mode: 384 });
16
- try {
17
- fs.chmodSync(filePath, 384);
18
- } catch {
19
- }
20
- return {
21
- path: filePath,
22
- async append(event) {
23
- const line = formatLine(event);
24
- try {
25
- await fs.promises.appendFile(filePath, line + "\n", { encoding: "utf8" });
26
- } catch (err) {
27
- console.error("[event-log] append failed:", err.message);
28
- }
29
- },
30
- cleanup() {
31
- try {
32
- fs.unlinkSync(filePath);
33
- } catch {
34
- }
35
- }
36
- };
37
- }
38
- function formatLine(event) {
39
- const body = (event.content?.body ?? "").replace(/[\r\n]+/g, " ").slice(0, MAX_BODY_CHARS + 1);
40
- const truncated = body.length > MAX_BODY_CHARS;
41
- const safeBody = truncated ? body.slice(0, MAX_BODY_CHARS) + "\u2026" : body;
42
- return `[${event.time}] tandem=${event.tandem_id} from=${event.from.displayname} (${event.from.principal}) kind=${event.kind} cursor=${event.cursor}: ${safeBody}`;
43
- }
44
- function isProcessAlive(pid) {
45
- try {
46
- process.kill(pid, 0);
47
- return true;
48
- } catch (err) {
49
- if (err.code === "EPERM") return true;
50
- return false;
51
- }
52
- }
53
- function listActiveSessionIds() {
54
- const dir = sessionDiscoveryDir();
55
- const active = /* @__PURE__ */ new Set();
56
- let entries;
57
- try {
58
- entries = fs.readdirSync(dir);
59
- } catch {
60
- return active;
61
- }
62
- for (const name of entries) {
63
- if (!name.endsWith(".json")) continue;
64
- const rawId = name.slice(0, -".json".length);
65
- const sessionId = rawId.startsWith("cc-") ? rawId.slice("cc-".length) : rawId;
66
- const filePath = path.join(dir, name);
67
- try {
68
- const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
69
- if (typeof data.pid === "number" && isProcessAlive(data.pid)) {
70
- active.add(sessionId);
71
- } else {
72
- try {
73
- fs.unlinkSync(filePath);
74
- } catch {
75
- }
76
- }
77
- } catch {
78
- }
79
- }
80
- return active;
81
- }
82
- function sweepStaleEventLogs(dir = DEFAULT_DIR, minAgeMs = DEFAULT_MIN_AGE_MS) {
83
- try {
84
- fs.unlinkSync(path.join(dir, "kojee-events-no-session.log"));
85
- } catch {
86
- }
87
- const active = listActiveSessionIds();
88
- let entries;
89
- try {
90
- entries = fs.readdirSync(dir);
91
- } catch {
92
- return;
93
- }
94
- const now = Date.now();
95
- for (const name of entries) {
96
- if (!name.startsWith("kojee-events-") || !name.endsWith(".log")) continue;
97
- const sessionId = name.slice("kojee-events-".length, -".log".length);
98
- if (active.has(sessionId)) continue;
99
- const filePath = path.join(dir, name);
100
- try {
101
- const stat = fs.statSync(filePath);
102
- const ageMs = Math.max(0, now - stat.mtimeMs);
103
- if (ageMs < minAgeMs) continue;
104
- fs.unlinkSync(filePath);
105
- } catch {
106
- }
107
- }
108
- }
109
- export {
110
- startEventLog,
111
- sweepStaleEventLogs
112
- };
@@ -1,76 +0,0 @@
1
- import {
2
- readHookStdin
3
- } from "./chunk-WHTH6WBP.js";
4
- import {
5
- deriveDiscoveryKey,
6
- findClaudeAncestorPid
7
- } from "./chunk-36DMIXH7.js";
8
- import {
9
- readSessionDiscoveryByKey
10
- } from "./chunk-VZVGTHGF.js";
11
-
12
- // src/hooks/stop-hook.ts
13
- import { spawn } from "child_process";
14
- import fs from "fs";
15
- async function isMonitorRunning(logPath) {
16
- return new Promise((resolve) => {
17
- const child = spawn("pgrep", ["-f", `tail -n \\+1 -F ${logPath}`], { stdio: "ignore" });
18
- child.on("exit", (code) => resolve(code === 0));
19
- child.on("error", () => resolve(false));
20
- });
21
- }
22
- var STOP_POLL_TIMEOUT_MS = Number.parseInt(
23
- process.env["KOJEE_STOP_HOOK_TIMEOUT_MS"] ?? "30000",
24
- 10
25
- );
26
- async function runStopHook() {
27
- await readHookStdin();
28
- const ccPid = findClaudeAncestorPid();
29
- const key = deriveDiscoveryKey(process.env["CLAUDE_PROJECT_DIR"], ccPid);
30
- const discovery = readSessionDiscoveryByKey(key);
31
- if (!discovery) {
32
- process.stdout.write("{}");
33
- return;
34
- }
35
- const logPath = `/tmp/kojee-events-${key}.log`;
36
- const monitorAlive = await isMonitorRunning(logPath);
37
- const pollTimeoutMs = monitorAlive ? 0 : STOP_POLL_TIMEOUT_MS;
38
- let body = null;
39
- try {
40
- const res = await fetch(
41
- `http://127.0.0.1:${discovery.port}/poll?type=stop&timeout_ms=${pollTimeoutMs}`
42
- );
43
- if (res.ok) {
44
- body = await res.json();
45
- }
46
- } catch {
47
- }
48
- if (body && body.count > 0) {
49
- const reason = formatEvents(body.events);
50
- process.stdout.write(JSON.stringify({ decision: "block", reason }));
51
- return;
52
- }
53
- if (!monitorAlive && fs.existsSync(logPath)) {
54
- process.stdout.write(JSON.stringify({
55
- decision: "block",
56
- reason: `Before stopping, spawn the kojee Tandem event watcher: Monitor(command="tail -n +1 -F ${logPath}", persistent=true, description="kojee Tandem events"). This is needed so Tandem peer messages can wake you from idle. After spawning it once at the start of the session, you don't need to spawn it again.`
57
- }));
58
- return;
59
- }
60
- process.stdout.write("{}");
61
- }
62
- function formatEvents(events) {
63
- const header = `[${events.length} unread Tandem ${events.length === 1 ? "event" : "events"}]
64
-
65
- `;
66
- const bodies = events.map((evt) => {
67
- const attrs = Object.entries(evt.meta).map(([k, v]) => `${k}="${v}"`).join(" ");
68
- return `<channel source="kojee-mcp" ${attrs}>
69
- ${evt.content}
70
- </channel>`;
71
- });
72
- return header + bodies.join("\n\n");
73
- }
74
- export {
75
- runStopHook
76
- };