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,265 @@
1
+ import {
2
+ buildCodexMcpServerTable,
3
+ buildCodexStopHookBlock,
4
+ removeCodexConfig,
5
+ writeCodexConfig
6
+ } from "./chunk-ZW4SW7LJ.js";
7
+ import {
8
+ WIZARD_RUNTIMES,
9
+ isWizardRuntime
10
+ } from "./chunk-LVL25VLO.js";
11
+ import {
12
+ clearRuntimeRecord,
13
+ readRecordedRuntime,
14
+ recordRuntime
15
+ } from "./chunk-EW72ZNQL.js";
16
+ import {
17
+ kojeeHomeDir
18
+ } from "./chunk-SQL56SEB.js";
19
+ import {
20
+ CODEX_LISTEN_CAP_MS,
21
+ buildWebhookReceiverNote
22
+ } from "./chunk-C6GZ2L2W.js";
23
+ import {
24
+ secureFile
25
+ } from "./chunk-BLEGIR35.js";
26
+ import {
27
+ resolveWebhookConfig
28
+ } from "./chunk-F7L25L2J.js";
29
+
30
+ // src/wizard/wizard.ts
31
+ import crypto from "crypto";
32
+ import fs from "fs";
33
+ import path from "path";
34
+ function generateWebhookSecret() {
35
+ return crypto.randomBytes(32).toString("hex");
36
+ }
37
+ async function resolveRuntime(opts) {
38
+ if (opts.runtime !== void 0) {
39
+ if (!isWizardRuntime(opts.runtime)) {
40
+ return {
41
+ error: `Unknown --runtime "${opts.runtime}". Expected one of: ${WIZARD_RUNTIMES.join(", ")}.`
42
+ };
43
+ }
44
+ return { runtime: opts.runtime };
45
+ }
46
+ if (opts.interactive && opts.promptRuntime) {
47
+ const picked = await opts.promptRuntime();
48
+ if (!isWizardRuntime(picked)) {
49
+ return { error: `Unknown runtime "${picked}". Expected one of: ${WIZARD_RUNTIMES.join(", ")}.` };
50
+ }
51
+ return { runtime: picked };
52
+ }
53
+ return { runtime: "claude-code" };
54
+ }
55
+ function resolveWizardWebhook(opts) {
56
+ const env = opts.env ?? process.env;
57
+ const url = (opts.webhookUrl ?? env["KOJEE_WEBHOOK_URL"] ?? "").trim();
58
+ let secret = (opts.webhookSecret ?? env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
59
+ if (url && !secret) secret = generateWebhookSecret();
60
+ const resolution = resolveWebhookConfig({
61
+ KOJEE_WEBHOOK_URL: url,
62
+ KOJEE_WEBHOOK_SECRET: secret,
63
+ ...env["KOJEE_WEBHOOK_TIMEOUT_MS"] !== void 0 ? { KOJEE_WEBHOOK_TIMEOUT_MS: env["KOJEE_WEBHOOK_TIMEOUT_MS"] } : {},
64
+ ...env["KOJEE_WEBHOOK_MAX_RETRIES"] !== void 0 ? { KOJEE_WEBHOOK_MAX_RETRIES: env["KOJEE_WEBHOOK_MAX_RETRIES"] } : {}
65
+ });
66
+ if (resolution.error) {
67
+ return { url, secret, redactedSummary: "", error: resolution.error };
68
+ }
69
+ return {
70
+ url,
71
+ secret,
72
+ redactedSummary: resolution.config?.redactedSummary ?? `url=${url} secret=<redacted>`
73
+ };
74
+ }
75
+ function writeRuntimeEnvFile(runtime, url, secret) {
76
+ const envPath = path.join(kojeeHomeDir(), ".kojee", `${runtime}.env`);
77
+ const body = [
78
+ `# kojee daemon env for runtime=${runtime} (source this before starting the daemon)`,
79
+ `export KOJEE_RUNTIME="${runtime}"`,
80
+ `export KOJEE_WEBHOOK_URL="${url}"`,
81
+ `export KOJEE_WEBHOOK_SECRET="${secret}"`,
82
+ ""
83
+ ].join("\n");
84
+ fs.mkdirSync(path.dirname(envPath), { recursive: true, mode: 448 });
85
+ fs.writeFileSync(envPath, body, { mode: 384 });
86
+ secureFile(envPath);
87
+ return envPath;
88
+ }
89
+ var CODEX_UNVERIFIED_NOTE = "NOTE: live Codex verification (hook fires, MCP server connects, bounded listen works) has not been run on this build \u2014 confirm in a real Codex session. This is the owner morning step.";
90
+ async function runWizard(opts) {
91
+ const resolved = await resolveRuntime(opts);
92
+ if ("error" in resolved) {
93
+ return { runtime: "claude-code", output: resolved.error, exitCode: 2 };
94
+ }
95
+ const runtime = resolved.runtime;
96
+ if (opts.uninstall) {
97
+ return runWizardUninstall(runtime, opts);
98
+ }
99
+ if (runtime === "claude-code") return configureClaudeCode(opts);
100
+ if (runtime === "codex") return configureCodex(opts);
101
+ return configureWebhookDaemon(runtime, opts);
102
+ }
103
+ async function configureClaudeCode(opts) {
104
+ const { runInit } = await import("./install-WBIUVBZW.js");
105
+ const report = runInit({
106
+ ...opts.configPath ? { configPath: opts.configPath } : {},
107
+ ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {}
108
+ });
109
+ recordRuntime("claude-code");
110
+ return { runtime: "claude-code", output: formatClaudeInit(report), exitCode: 0 };
111
+ }
112
+ function formatClaudeInit(report) {
113
+ const tick = (s) => {
114
+ if (s === "added") return "\u2713 added";
115
+ if (s === "already-installed") return "\u21BB already installed";
116
+ if (s === "preserved-different") return "\u26A0 preserved (existing entry differs \u2014 left untouched)";
117
+ if (s === "not-found") return "\u2014 not found";
118
+ return s ?? "\u2014";
119
+ };
120
+ const lines = ["Configured runtime: claude-code", "", "Installing kojee for Claude:"];
121
+ for (const t of report.targets) {
122
+ const label = t.kind === "cli" ? "CLI" : "Claude.app";
123
+ lines.push("");
124
+ lines.push(` ${t.path} (${label})`);
125
+ lines.push(` mcpServers.kojee ${tick(t.mcpServer)}`);
126
+ if (t.kind === "cli") {
127
+ if (t.hooksPath) lines.push(` ${t.hooksPath} (hooks)`);
128
+ lines.push(` hooks.Stop ${tick(t.stopHook)}`);
129
+ lines.push(` hooks.UserPromptSubmit ${tick(t.userPromptSubmitHook)}`);
130
+ } else {
131
+ lines.push(` (hooks not applicable for Claude.app agent mode)`);
132
+ }
133
+ }
134
+ lines.push("");
135
+ if (report.targets.some((t) => t.kind === "desktop" && t.mcpServer === "added")) {
136
+ lines.push(
137
+ "Existing Claude.app agent-mode sessions snapshotted the previous config",
138
+ "and won't pick up this change automatically. Start a NEW agent-mode",
139
+ "session (not a resumed one) to use the updated kojee config.",
140
+ ""
141
+ );
142
+ }
143
+ lines.push("To verify: in any new CC session, run /mcp and confirm `kojee` is listed.");
144
+ lines.push(" run /hooks and confirm both Stop and UserPromptSubmit show the kojee entries.");
145
+ lines.push("To remove: `kojee-mcp init --uninstall`");
146
+ return lines.join("\n");
147
+ }
148
+ function configureCodex(opts) {
149
+ const wh = resolveWizardWebhook(opts);
150
+ if (wh.error) {
151
+ return { runtime: "codex", output: `webhook env ERROR: ${wh.error}`, exitCode: 2 };
152
+ }
153
+ const url = wh.url || "https://YOUR-CODEX-WEBHOOK-RECEIVER.local/kojee";
154
+ const secret = wh.secret || generateWebhookSecret();
155
+ writeCodexConfig({
156
+ ...opts.configPath ? { configPath: opts.configPath } : {},
157
+ ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
158
+ webhookUrl: url,
159
+ webhookSecret: secret
160
+ });
161
+ recordRuntime("codex");
162
+ const lines = [];
163
+ lines.push("Configured runtime: codex");
164
+ lines.push("Wake mode: webhook-sink + stop-hook peek (Codex has no channel injection).");
165
+ lines.push("");
166
+ lines.push("Wrote [mcp_servers.kojee] to ~/.codex/config.toml:");
167
+ lines.push(indent(buildCodexMcpServerTable({ webhookUrl: url, webhookSecret: "<redacted>" })));
168
+ lines.push("");
169
+ lines.push("Wrote the Codex Stop hook (~/.codex/hooks.json; inline TOML form):");
170
+ lines.push(indent(buildCodexStopHookBlock()));
171
+ lines.push("");
172
+ lines.push(`webhook: ${wh.url ? wh.redactedSummary : "(receiver URL not set \u2014 fill KOJEE_WEBHOOK_URL above)"}`);
173
+ lines.push(`bounded-listen cap: ${CODEX_LISTEN_CAP_MS}ms (the model picks listen vs drain vs ignore).`);
174
+ lines.push("");
175
+ lines.push("Next steps (codex):");
176
+ lines.push(" Restart Codex (or start a new `codex` / `codex exec` session) to load the MCP server and hook.");
177
+ lines.push(" Stand up your webhook receiver at KOJEE_WEBHOOK_URL \u2014 contract:");
178
+ lines.push(indent(buildWebhookReceiverNote()));
179
+ lines.push(" Verify: kojee-mcp doctor");
180
+ lines.push("");
181
+ lines.push(CODEX_UNVERIFIED_NOTE);
182
+ return { runtime: "codex", output: lines.join("\n"), exitCode: 0 };
183
+ }
184
+ function configureWebhookDaemon(runtime, opts) {
185
+ const wh = resolveWizardWebhook(opts);
186
+ if (wh.error) {
187
+ return { runtime, output: `webhook env ERROR: ${wh.error}`, exitCode: 2 };
188
+ }
189
+ recordRuntime(runtime);
190
+ const lines = [];
191
+ lines.push(`Configured runtime: ${runtime}`);
192
+ lines.push("Wake mode: webhook sink (daemon-consumed). NO MCP-config file, NO hooks written.");
193
+ lines.push("");
194
+ if (wh.url) {
195
+ const envFile = writeRuntimeEnvFile(runtime, wh.url, wh.secret);
196
+ lines.push("Export these before starting the daemon (also written to a source-able file):");
197
+ lines.push(` export KOJEE_RUNTIME="${runtime}"`);
198
+ lines.push(` export KOJEE_WEBHOOK_URL="${wh.url}"`);
199
+ lines.push(` export KOJEE_WEBHOOK_SECRET=<generated; in ${envFile}>`);
200
+ lines.push(` (validated: ${wh.redactedSummary})`);
201
+ lines.push(` source ${envFile}`);
202
+ } else {
203
+ lines.push("Set the daemon env (no receiver URL supplied yet):");
204
+ lines.push(` export KOJEE_RUNTIME="${runtime}"`);
205
+ lines.push(` export KOJEE_WEBHOOK_URL="https://YOUR-RECEIVER.local/kojee"`);
206
+ lines.push(` export KOJEE_WEBHOOK_SECRET=<generate a hex secret>`);
207
+ }
208
+ lines.push("");
209
+ lines.push("Receiver contract:");
210
+ lines.push(indent(buildWebhookReceiverNote()));
211
+ lines.push("");
212
+ lines.push(`Next steps (${runtime}):`);
213
+ lines.push(" Start the daemon with the env above. Verify: kojee-mcp doctor (after the daemon is up).");
214
+ return { runtime, output: lines.join("\n"), exitCode: 0 };
215
+ }
216
+ async function runWizardUninstall(runtime, opts) {
217
+ const effective = opts.runtime !== void 0 ? runtime : readRecordedRuntime() ?? runtime;
218
+ const lines = [`Uninstalling runtime: ${effective}`];
219
+ if (effective === "claude-code") {
220
+ const { runUninstall } = await import("./install-WBIUVBZW.js");
221
+ const report = runUninstall({
222
+ ...opts.configPath ? { configPath: opts.configPath } : {},
223
+ ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {}
224
+ });
225
+ lines.push("Removing kojee from Claude:");
226
+ for (const t of report.targets) {
227
+ const label = t.kind === "cli" ? "CLI" : "Claude.app";
228
+ lines.push("");
229
+ lines.push(` ${t.path} (${label})`);
230
+ lines.push(` mcpServers.kojee ${t.mcpServer ? "\u2713 removed" : "\u2014 not found"}`);
231
+ if (t.kind === "cli") {
232
+ if (t.hooksPath) lines.push(` ${t.hooksPath} (hooks)`);
233
+ lines.push(` hook entries ${t.hooks ? "\u2713 removed" : "\u2014 not found"}`);
234
+ }
235
+ }
236
+ } else if (effective === "codex") {
237
+ const removed = removeCodexConfig({
238
+ ...opts.configPath ? { configPath: opts.configPath } : {},
239
+ ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {}
240
+ });
241
+ lines.push(` config.toml [mcp_servers.kojee]: ${removed.mcpServer ? "removed" : "not found"}`);
242
+ lines.push(` hooks.json Stop: ${removed.stopHook ? "removed" : "not found"}`);
243
+ } else {
244
+ lines.push(" (hermes/openclaw write no MCP-config or hooks \u2014 nothing to tear down.");
245
+ lines.push(" Stop the daemon and unset KOJEE_WEBHOOK_URL/SECRET to disable the sink.)");
246
+ const envPath = path.join(kojeeHomeDir(), ".kojee", `${effective}.env`);
247
+ try {
248
+ fs.unlinkSync(envPath);
249
+ lines.push(` removed ${envPath}`);
250
+ } catch {
251
+ }
252
+ }
253
+ clearRuntimeRecord();
254
+ return { runtime: effective, output: lines.join("\n"), exitCode: 0 };
255
+ }
256
+ function indent(s) {
257
+ return s.split("\n").map((l) => " " + l).join("\n");
258
+ }
259
+ export {
260
+ CODEX_UNVERIFIED_NOTE,
261
+ generateWebhookSecret,
262
+ resolveRuntime,
263
+ resolveWizardWebhook,
264
+ runWizard
265
+ };
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.2",
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
- };