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.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # kojee-mcp
2
2
 
3
+ > **First time using Kojee Tandem in Claude Code?** See [docs/getting-started-tandem.md](docs/getting-started-tandem.md) for the 3-command setup + verification walkthrough.
4
+
3
5
  There are two ways to connect Kojee to an MCP-capable agent:
4
6
 
5
7
  1. **Mobile, web & desktop (recommended)** — paste the Kojee MCP URL into the app's "Add custom connector" dialog. The app handles OAuth login and consent. No local install. Works on Claude (web/desktop/iOS/Android) and ChatGPT (web/desktop/iOS/Android with Developer Mode enabled).
@@ -126,26 +128,35 @@ If both Channels (dangerous-flag launched) and hooks are active, the proxy dedup
126
128
 
127
129
  ## Waiting on Tandem Peers
128
130
 
129
- When kojee-mcp is running under Claude Code, the proxy writes one line per Tandem message to a per-session log file. The agent watches this log via Claude Code's built-in `Monitor` tool.
130
-
131
- **You don't need to figure out the path yourself.** The proxy bakes the resolved path into the MCP server's `instructions` string at startup, so the agent gets the exact command to spawn:
131
+ When kojee-mcp is running under Claude Code, the proxy writes one line per
132
+ Tandem message to a per-session log file under the OS's temp directory
133
+ (`os.tmpdir()` `/tmp` on Linux, `/var/folders/…` on macOS, `%TEMP%` on
134
+ Windows). The agent watches this log via Claude Code's built-in `Monitor`
135
+ tool — spawned once at the start of each session:
132
136
 
133
137
  ```ts
134
138
  Monitor({
135
- command: "tail -n +1 -F /tmp/kojee-events-<discoveryKey>.log",
139
+ command: `npx kojee-mcp tail "${eventLogPath}"`,
136
140
  persistent: true,
137
141
  description: "kojee Tandem events",
138
142
  });
139
143
  ```
140
144
 
141
- The `<discoveryKey>` is computed from `sha256(CLAUDE_PROJECT_DIR).slice(0,12) + '-' + <ccPid>` where `ccPid` is the parent Claude Code process. (This replaces the pre-v0.4 scheme that used `CLAUDE_CODE_SESSION_ID`, which Claude.app doesn't forward to MCP servers.) The matching `~/.kojee/sessions/cc-<discoveryKey>.json` discovery file lets the Stop / UserPromptSubmit hooks find this proxy via the same independent derivation.
145
+ `kojee-mcp tail` is a portable line-streamer shipped with this proxy
146
+ works the same on macOS, Linux, and Windows. The proxy interpolates the
147
+ resolved `eventLogPath` into the Channel-`instructions` string so the
148
+ agent receives a ready-to-run command.
149
+
150
+ The `<discoveryKey>` embedded in the log filename is computed from `sha256(CLAUDE_PROJECT_DIR).slice(0,12) + '-' + <ccPid>` where `ccPid` is the parent Claude Code process. (This replaces the pre-v0.4 scheme that used `CLAUDE_CODE_SESSION_ID`, which Claude.app doesn't forward to MCP servers.) The matching `~/.kojee/sessions/cc-<discoveryKey>.json` discovery file lets the Stop / UserPromptSubmit hooks find this proxy via the same independent derivation.
142
151
 
143
152
  Each appended event line arrives as a separate spontaneous wake notification. The agent stays free to chat with the user between events; CC delivers each event from idle as it arrives.
144
153
 
145
- **Stop hook is Monitor-aware** (v0.4+): when a Monitor is watching the log file, the Stop hook does an instant queue snapshot (~50ms) instead of a 30s long-poll, since events have already been delivered push-style. When no Monitor is running, the Stop hook long-polls for up to 30s AND emits a "spawn a Monitor" recommendation to the agent self-healing if the session-start spawn was skipped.
154
+ **Stop hook always long-polls** (up to 30s): the queue's `markMonitorDelivered` dedup ensures every event is delivered exactly once across the Channel / Monitor / Stop-hook paths, so the hook doesn't need to probe whether a Monitor is running. If the agent skipped the session-start Monitor spawn, the long-poll backstop still catches events; if Monitor is running, the dedup filters out anything already delivered push-style.
146
155
 
147
156
  Channel notifications (when available — see "Claude Code Channels Support" above) supplement this with mid-turn `<channel>` tag delivery, but Monitor is the default no-allowlist wake path that works for every user.
148
157
 
158
+ **Cross-platform support:** the proxy core, the `kojee-mcp tail` Monitor command, and the Channel wake path are portable across macOS, Linux, and Windows — path resolution uses `os.homedir()` / `os.tmpdir()` and process-ancestry detection uses the pure-JS `ps-list` package. Claude Code hook invocation on Windows is pending end-to-end verification; on macOS and Linux it is exercised by CI.
159
+
149
160
  For one-shot blocking waits (return as soon as a single reply arrives), call `tandem_listen(tandem_id, since=cursor, timeout_ms=N)` instead.
150
161
 
151
162
  ## Backend SSE Wire Compatibility
@@ -1,36 +1,26 @@
1
1
  // src/runtime/ancestry.ts
2
- import childProcess from "child_process";
2
+ import psList from "ps-list";
3
3
  import { createHash } from "crypto";
4
- function findClaudeAncestorPid(startPid = process.ppid) {
5
- if (process.platform === "win32") return null;
4
+ async function findClaudeAncestorPid(startPid = process.ppid) {
5
+ let processes;
6
+ try {
7
+ processes = await psList();
8
+ } catch {
9
+ return null;
10
+ }
11
+ const byPid = /* @__PURE__ */ new Map();
12
+ for (const p of processes) byPid.set(p.pid, p);
6
13
  let pid = startPid;
7
14
  for (let depth = 0; depth < 20 && pid !== void 0 && pid > 1; depth++) {
8
- let row;
9
- try {
10
- row = readProcInfo(pid);
11
- } catch {
12
- return null;
13
- }
15
+ const row = byPid.get(pid);
14
16
  if (!row) return null;
15
- if (/\bclaude\b/.test(row.command)) return pid;
17
+ const haystack = `${row.name} ${row.cmd ?? ""}`;
18
+ if (/\bclaude\b/i.test(haystack)) return pid;
16
19
  if (row.ppid === void 0 || row.ppid === pid) return null;
17
20
  pid = row.ppid;
18
21
  }
19
22
  return null;
20
23
  }
21
- function readProcInfo(pid) {
22
- const out = childProcess.execFileSync("ps", ["-ww", "-p", String(pid), "-o", "ppid=,command="], {
23
- encoding: "utf8",
24
- stdio: ["ignore", "pipe", "ignore"]
25
- }).trim();
26
- if (!out) return null;
27
- const firstSpace = out.indexOf(" ");
28
- if (firstSpace < 0) return null;
29
- const ppid = Number.parseInt(out.slice(0, firstSpace).trim(), 10);
30
- if (!Number.isFinite(ppid)) return null;
31
- const command = out.slice(firstSpace + 1).trim();
32
- return { command, ppid };
33
- }
34
24
  function deriveDiscoveryKey(projectDir, ccPid) {
35
25
  const hasProjectDir = typeof projectDir === "string" && projectDir.length > 0;
36
26
  if (!hasProjectDir && ccPid === null) {
@@ -0,0 +1,27 @@
1
+ // src/tandem/recipe.ts
2
+ var SEND_BODY_PARAM = "body";
3
+ function buildMonitorCommand(logPath) {
4
+ return `npx kojee-mcp tail "${logPath}"`;
5
+ }
6
+ function buildMonitorSpawn(logPath) {
7
+ return `Monitor(command=\`${buildMonitorCommand(logPath)}\`, persistent=true, description="kojee Tandem events")`;
8
+ }
9
+ function buildReplyRecipe(event) {
10
+ if (event) {
11
+ return `reply directly with tandem_send(tandem_id="${event.tandem_id}", ${SEND_BODY_PARAM}="...", reply_to="${event.message_id}"); fetch context only if needed via tandem_messages(tandem_id, since=cursor-1)`;
12
+ }
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
+ }
15
+ function buildCatchUpNote() {
16
+ return "If the event line `cursor=<n>` ever jumps (a gap) 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>).";
17
+ }
18
+ function buildMonitorNudge(logPath) {
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()}.`;
20
+ }
21
+
22
+ export {
23
+ buildMonitorSpawn,
24
+ buildReplyRecipe,
25
+ buildCatchUpNote,
26
+ buildMonitorNudge
27
+ };
@@ -1,8 +1,9 @@
1
1
  // src/auth/paired-config.ts
2
2
  import fs from "fs";
3
+ import os from "os";
3
4
  import path from "path";
4
5
  function pairedConfigPath() {
5
- return path.join(process.env["HOME"] ?? "~", ".kojee", "config.json");
6
+ return path.join(os.homedir(), ".kojee", "config.json");
6
7
  }
7
8
  function loadPairedConfig(filePath = pairedConfigPath()) {
8
9
  try {