kojee-mcp 0.2.2 → 0.4.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 +140 -7
- package/dist/chunk-36DMIXH7.js +51 -0
- package/dist/chunk-E7TE4QZD.js +33 -0
- package/dist/chunk-VZVGTHGF.js +142 -0
- package/dist/chunk-WHTH6WBP.js +72 -0
- package/dist/{chunk-QKAUM3TR.js → chunk-ZGVUM4AG.js} +409 -24
- package/dist/cli.js +190 -16
- package/dist/event-log-ETWR6PPY.js +112 -0
- package/dist/event-queue-5YVJFR3E.js +43 -0
- package/dist/hook-server-43QS7L7P.js +71 -0
- package/dist/index.d.ts +0 -13
- package/dist/index.js +2 -1
- package/dist/install-WV25CRU2.js +182 -0
- package/dist/paired-config-OAR3O3XY.js +10 -0
- package/dist/session-discovery-WSHLR4OV.js +26 -0
- package/dist/stop-hook-5XU3EQAE.js +76 -0
- package/dist/user-prompt-submit-hook-WSRIJVF4.js +54 -0
- package/package.json +9 -13
package/README.md
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
# kojee-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
There are two ways to connect Kojee to an MCP-capable agent:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
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).
|
|
6
|
+
2. **Power user / local proxy with DPoP** — run this stdio proxy locally with a gateway token. Strongest wire-level security (DPoP proof-of-possession, RFC 9449), but requires Node and a token you generate manually in the Kojee dashboard.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
2. **Add to your MCP config** (see examples below).
|
|
9
|
-
3. **Tools appear automatically** -- all Kojee tools your token has access to are exposed directly (e.g. `gmail_send_email`, `github_list_repos`, `calendar_create_event`). Agents call them like any native MCP tool — no discovery step, no wrapper, no indirection.
|
|
8
|
+
## Mobile, Web & Desktop (Recommended)
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
Paste this URL into the app's connector dialog:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
https://api.kojee.ai/mcp
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- **Claude (any surface):** Settings → Connectors → Add custom connector → paste URL → Connect → log in to Kojee → approve scopes.
|
|
17
|
+
- **ChatGPT (any surface, Developer Mode on):** Settings → Connectors → New connector → paste URL → choose OAuth → Connect.
|
|
18
|
+
|
|
19
|
+
The OAuth login + consent flow takes care of everything — no token paste, no config file, no Node required. Tokens are short-lived (1 h) and audience-bound to the MCP URL (RFC 8707). Refresh tokens rotate on every use.
|
|
20
|
+
|
|
21
|
+
### Remote MCP URL details
|
|
22
|
+
|
|
23
|
+
| Field | Value |
|
|
24
|
+
|---|---|
|
|
25
|
+
| MCP URL | `https://api.kojee.ai/mcp` |
|
|
26
|
+
| Transport | Streamable HTTP (POST + GET-SSE) |
|
|
27
|
+
| Auth | OAuth 2.1 + Dynamic Client Registration (RFC 7591) |
|
|
28
|
+
| Discovery | `https://api.kojee.ai/.well-known/oauth-protected-resource` |
|
|
29
|
+
| Scopes | `mcp:tools`, `mcp:read` |
|
|
30
|
+
|
|
31
|
+
## Power User: Local Proxy with DPoP
|
|
32
|
+
|
|
33
|
+
The `kojee-mcp` stdio proxy holds a `gw_` gateway token + ES256 keypair locally and signs every request with DPoP (RFC 9449). This survives token-in-transit theft (TLS-terminating proxy logs, etc.) — strictly stronger wire-level posture than bearer-only OAuth, but requires a long-lived token.
|
|
12
34
|
|
|
13
35
|
### Claude Code / Claude Desktop
|
|
14
36
|
|
|
@@ -25,7 +47,7 @@ kojee-mcp is a local MCP proxy that lets any MCP-capable agent use Kojee-managed
|
|
|
25
47
|
|
|
26
48
|
### Generic MCP Client
|
|
27
49
|
|
|
28
|
-
Any MCP client that supports stdio transports can use kojee-mcp
|
|
50
|
+
Any MCP client that supports stdio transports can use kojee-mcp:
|
|
29
51
|
|
|
30
52
|
```bash
|
|
31
53
|
npx kojee-mcp --token gw_abc123... --url https://kojee.ai
|
|
@@ -46,6 +68,117 @@ kojee-mcp --token gw_abc123... --url https://kojee.ai
|
|
|
46
68
|
| `--url` | yes | -- | Kojee broker URL (e.g. `https://kojee.ai`) |
|
|
47
69
|
| `--keystore-path` | no | `~/.kojee/keypair.json` | Path for DPoP keypair storage |
|
|
48
70
|
|
|
71
|
+
## Pair Mode (Tandem)
|
|
72
|
+
|
|
73
|
+
If your token comes from the Kojee Tandem web wizard (a "pair code"), you can persist it for future runs:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# One-time setup:
|
|
77
|
+
npx kojee-mcp pair ABCD-1234 --url https://rosie-server.kojee.net
|
|
78
|
+
npx kojee-mcp init # adds the MCP entry to EVERY detected Claude installation
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`init` writes to both `~/.claude.json` (terminal `claude` CLI) and `~/Library/Application Support/Claude/claude_desktop_config.json` (Claude.app on macOS — Windows / Linux paths in the spec) if either exists. The canonical MCP entry includes `env.KOJEE_RUNTIME=claude-code` so the proxy correctly identifies as Claude Code even when the desktop app strips environment variables from MCP subprocesses.
|
|
82
|
+
|
|
83
|
+
**Important for Claude.app users:** existing agent-mode sessions snapshot the MCP config at creation time and won't pick up changes from `init`. Start a NEW session (not a resumed one) to use the updated kojee config.
|
|
84
|
+
|
|
85
|
+
Subsequent runs of `claude` will find kojee automatically. The proxy writes credentials to `~/.kojee/config.json` (mode 0600). Existing `--token` users are unaffected.
|
|
86
|
+
|
|
87
|
+
To remove kojee from Claude:
|
|
88
|
+
```bash
|
|
89
|
+
npx kojee-mcp init --uninstall
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Claude Code Channels Support
|
|
93
|
+
|
|
94
|
+
When this proxy detects it's running under Claude Code, it declares the `claude/channel` capability and pushes Tandem messages directly into the Claude session via `notifications/claude/channel`. Other MCP clients (Cursor, Codex, Openclaw, etc.) see no behavior change.
|
|
95
|
+
|
|
96
|
+
**Runtime detection (3 tiers, first match wins):**
|
|
97
|
+
1. `KOJEE_RUNTIME=claude-code` env var (set by `kojee-mcp init` in the MCP entry's `env` block — survives Claude.app's env strip)
|
|
98
|
+
2. `CLAUDE_CODE_SESSION_ID` env var (set by the terminal `claude` CLI; **not** forwarded by Claude.app to MCP stdio servers)
|
|
99
|
+
3. Process-ancestry walk for a parent process whose command line matches `\bclaude\b` (handles fresh installs where neither env var is present)
|
|
100
|
+
|
|
101
|
+
CC Channels are in research preview — you must launch CC with `claude --dangerously-load-development-channels server:kojee` until kojee is on the official allowlist.
|
|
102
|
+
|
|
103
|
+
## Hooks Support (always-on, no allowlist required)
|
|
104
|
+
|
|
105
|
+
While Channels is in research preview and gated behind Anthropic's allowlist, kojee also supports the standard Claude Code hooks system as a complementary wake path. Hooks require no special launch flag.
|
|
106
|
+
|
|
107
|
+
If you used `kojee-mcp init` above, hooks are already installed. The standalone command is:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx kojee-mcp install-hooks
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This writes to **`~/.claude/settings.json`** (the file CC reads for hooks — NOT `~/.claude.json`, which is the MCP-servers file). Foreign hooks already in `settings.json` (e.g. babysitter wrappers) are preserved as siblings.
|
|
114
|
+
|
|
115
|
+
Restart Claude Code. Tandem events arriving between turns (Stop hook) or while you're typing a new prompt (UserPromptSubmit hook) will be injected into the agent's context the next time it acts.
|
|
116
|
+
|
|
117
|
+
To remove:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx kojee-mcp install-hooks --uninstall
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Override the default `~/.claude/settings.json` path with `--hooks-path <path>` if needed.
|
|
124
|
+
|
|
125
|
+
If both Channels (dangerous-flag launched) and hooks are active, the proxy deduplicates so events arrive exactly once. Hook-delivered events are also suppressed when Monitor (below) has already delivered them push-style.
|
|
126
|
+
|
|
127
|
+
## Waiting on Tandem Peers
|
|
128
|
+
|
|
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:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
Monitor({
|
|
135
|
+
command: "tail -n +1 -F /tmp/kojee-events-<discoveryKey>.log",
|
|
136
|
+
persistent: true,
|
|
137
|
+
description: "kojee Tandem events",
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
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.
|
|
142
|
+
|
|
143
|
+
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
|
+
|
|
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.
|
|
146
|
+
|
|
147
|
+
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
|
+
|
|
149
|
+
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
|
+
|
|
151
|
+
## Backend SSE Wire Compatibility
|
|
152
|
+
|
|
153
|
+
The proxy normalizes incoming SSE events from `/api/v2/tandems/stream` so that the on-the-wire payload shape (which currently differs from the spec'd `TandemEvent`) is mapped transparently:
|
|
154
|
+
|
|
155
|
+
| Wire field | Internal field |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `message_id` | `id` |
|
|
158
|
+
| `sender.principal_id` | `from.principal` |
|
|
159
|
+
| `sender.agent_id` | `from.agent_id` |
|
|
160
|
+
| top-level `body` | `content.body` |
|
|
161
|
+
| top-level `format` | `content.format` |
|
|
162
|
+
| (missing) `displayname` | synthesized as `principal:<first-8-chars-of-principal_id>` |
|
|
163
|
+
|
|
164
|
+
The normalizer also accepts canonical (spec-aligned) payloads unchanged, so the proxy works against either shape during any future backend migration.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
Run tests:
|
|
169
|
+
```bash
|
|
170
|
+
npm install
|
|
171
|
+
npm test
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The test suite uses an in-process stub broker (`dev-tools/stub-broker.ts`) — no external dependencies, no Docker, no MongoDB. CI runs the same way.
|
|
175
|
+
|
|
176
|
+
Run the stub manually for hands-on CC verification:
|
|
177
|
+
```bash
|
|
178
|
+
npm run dev:stub
|
|
179
|
+
# Stub listens on http://localhost:8765
|
|
180
|
+
```
|
|
181
|
+
|
|
49
182
|
## How Approvals Work
|
|
50
183
|
|
|
51
184
|
Some tools are governed by approval policies configured in the Kojee dashboard. When an agent calls a governed tool:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/runtime/ancestry.ts
|
|
2
|
+
import childProcess from "child_process";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
function findClaudeAncestorPid(startPid = process.ppid) {
|
|
5
|
+
if (process.platform === "win32") return null;
|
|
6
|
+
let pid = startPid;
|
|
7
|
+
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
|
+
}
|
|
14
|
+
if (!row) return null;
|
|
15
|
+
if (/\bclaude\b/.test(row.command)) return pid;
|
|
16
|
+
if (row.ppid === void 0 || row.ppid === pid) return null;
|
|
17
|
+
pid = row.ppid;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
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
|
+
function deriveDiscoveryKey(projectDir, ccPid) {
|
|
35
|
+
const hasProjectDir = typeof projectDir === "string" && projectDir.length > 0;
|
|
36
|
+
if (!hasProjectDir && ccPid === null) {
|
|
37
|
+
return `orphan-${process.pid}`;
|
|
38
|
+
}
|
|
39
|
+
if (!hasProjectDir) {
|
|
40
|
+
const hash2 = createHash("sha256").update("<no-project>").digest("hex").slice(0, 12);
|
|
41
|
+
return `${hash2}-${ccPid}`;
|
|
42
|
+
}
|
|
43
|
+
const hash = createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
|
|
44
|
+
if (ccPid === null) return `${hash}-orphan`;
|
|
45
|
+
return `${hash}-${ccPid}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
findClaudeAncestorPid,
|
|
50
|
+
deriveDiscoveryKey
|
|
51
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/auth/paired-config.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function pairedConfigPath() {
|
|
5
|
+
return path.join(process.env["HOME"] ?? "~", ".kojee", "config.json");
|
|
6
|
+
}
|
|
7
|
+
function loadPairedConfig(filePath = pairedConfigPath()) {
|
|
8
|
+
try {
|
|
9
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function savePairedConfig(filePath, config) {
|
|
16
|
+
const dir = path.dirname(filePath);
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
18
|
+
try {
|
|
19
|
+
fs.chmodSync(dir, 448);
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
23
|
+
try {
|
|
24
|
+
fs.chmodSync(filePath, 384);
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
pairedConfigPath,
|
|
31
|
+
loadPairedConfig,
|
|
32
|
+
savePairedConfig
|
|
33
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/tandem/session-discovery.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function sessionDiscoveryDir() {
|
|
5
|
+
return path.join(process.env["HOME"] ?? "~", ".kojee", "sessions");
|
|
6
|
+
}
|
|
7
|
+
function sessionDiscoveryPath(sessionId) {
|
|
8
|
+
return path.join(sessionDiscoveryDir(), `${sessionId}.json`);
|
|
9
|
+
}
|
|
10
|
+
function discoveryFileName(key) {
|
|
11
|
+
return `cc-${key}.json`;
|
|
12
|
+
}
|
|
13
|
+
function writeSessionDiscovery(sessionId, entry) {
|
|
14
|
+
const dir = sessionDiscoveryDir();
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
16
|
+
try {
|
|
17
|
+
fs.chmodSync(dir, 448);
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
const filePath = sessionDiscoveryPath(sessionId);
|
|
21
|
+
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), { mode: 384 });
|
|
22
|
+
try {
|
|
23
|
+
fs.chmodSync(filePath, 384);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readSessionDiscovery(sessionId) {
|
|
28
|
+
try {
|
|
29
|
+
const raw = fs.readFileSync(sessionDiscoveryPath(sessionId), "utf8");
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function cleanupSessionDiscovery(sessionId) {
|
|
36
|
+
try {
|
|
37
|
+
fs.unlinkSync(sessionDiscoveryPath(sessionId));
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function discoveryPathForKey(key) {
|
|
42
|
+
return path.join(sessionDiscoveryDir(), discoveryFileName(key));
|
|
43
|
+
}
|
|
44
|
+
function writeDiscoveryByKey(key, entry) {
|
|
45
|
+
const dir = sessionDiscoveryDir();
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
47
|
+
try {
|
|
48
|
+
fs.chmodSync(dir, 448);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
const filePath = discoveryPathForKey(key);
|
|
52
|
+
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), { mode: 384 });
|
|
53
|
+
try {
|
|
54
|
+
fs.chmodSync(filePath, 384);
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function cleanupDiscoveryByKey(key) {
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(discoveryPathForKey(key));
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function readSessionDiscoveryByKey(key) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = fs.readFileSync(discoveryPathForKey(key), "utf8");
|
|
67
|
+
return JSON.parse(raw);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var DEFAULT_SWEEP_MIN_AGE_MS = 6e4;
|
|
73
|
+
function isProcessAlive(pid) {
|
|
74
|
+
try {
|
|
75
|
+
process.kill(pid, 0);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.code === "EPERM") return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function sweepStaleDiscovery(opts = {}) {
|
|
83
|
+
const minAgeMs = opts.minAgeMs ?? DEFAULT_SWEEP_MIN_AGE_MS;
|
|
84
|
+
const dir = sessionDiscoveryDir();
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = fs.readdirSync(dir);
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
for (const name of entries) {
|
|
93
|
+
if (!name.endsWith(".json")) continue;
|
|
94
|
+
const filePath = path.join(dir, name);
|
|
95
|
+
let stat;
|
|
96
|
+
try {
|
|
97
|
+
stat = fs.statSync(filePath);
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const ageMs = Math.max(0, now - stat.mtimeMs);
|
|
102
|
+
if (ageMs < minAgeMs) continue;
|
|
103
|
+
let parsed = null;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
106
|
+
} catch {
|
|
107
|
+
try {
|
|
108
|
+
fs.unlinkSync(filePath);
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (parsed?.schema !== 2) {
|
|
114
|
+
try {
|
|
115
|
+
fs.unlinkSync(filePath);
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (typeof parsed.proxyPid !== "number" || !isProcessAlive(parsed.proxyPid)) {
|
|
121
|
+
try {
|
|
122
|
+
fs.unlinkSync(filePath);
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
sessionDiscoveryDir,
|
|
132
|
+
sessionDiscoveryPath,
|
|
133
|
+
discoveryFileName,
|
|
134
|
+
writeSessionDiscovery,
|
|
135
|
+
readSessionDiscovery,
|
|
136
|
+
cleanupSessionDiscovery,
|
|
137
|
+
discoveryPathForKey,
|
|
138
|
+
writeDiscoveryByKey,
|
|
139
|
+
cleanupDiscoveryByKey,
|
|
140
|
+
readSessionDiscoveryByKey,
|
|
141
|
+
sweepStaleDiscovery
|
|
142
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/hooks/hook-input.ts
|
|
2
|
+
var MAX_STDIN_BYTES = 1024 * 1024;
|
|
3
|
+
var IDLE_TIMEOUT_MS = 50;
|
|
4
|
+
var NULL_RESULT = {
|
|
5
|
+
sessionId: null,
|
|
6
|
+
transcriptPath: null,
|
|
7
|
+
hookEventName: null,
|
|
8
|
+
raw: ""
|
|
9
|
+
};
|
|
10
|
+
async function readHookStdin() {
|
|
11
|
+
const raw = await drainStdinBuffered();
|
|
12
|
+
if (raw === null) {
|
|
13
|
+
return { ...NULL_RESULT };
|
|
14
|
+
}
|
|
15
|
+
if (raw === "") {
|
|
16
|
+
return { ...NULL_RESULT };
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
return {
|
|
21
|
+
sessionId: stringOrNull(parsed["session_id"]),
|
|
22
|
+
transcriptPath: stringOrNull(parsed["transcript_path"]),
|
|
23
|
+
hookEventName: stringOrNull(parsed["hook_event_name"]),
|
|
24
|
+
raw
|
|
25
|
+
};
|
|
26
|
+
} catch {
|
|
27
|
+
return {
|
|
28
|
+
sessionId: null,
|
|
29
|
+
transcriptPath: null,
|
|
30
|
+
hookEventName: null,
|
|
31
|
+
raw
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function stringOrNull(value) {
|
|
36
|
+
return typeof value === "string" ? value : null;
|
|
37
|
+
}
|
|
38
|
+
function drainStdinBuffered() {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let total = 0;
|
|
42
|
+
let overflowed = false;
|
|
43
|
+
let settled = false;
|
|
44
|
+
const finish = (value) => {
|
|
45
|
+
if (settled) return;
|
|
46
|
+
settled = true;
|
|
47
|
+
resolve(value);
|
|
48
|
+
};
|
|
49
|
+
const onData = (chunk) => {
|
|
50
|
+
if (overflowed) return;
|
|
51
|
+
total += chunk.length;
|
|
52
|
+
if (total > MAX_STDIN_BYTES) {
|
|
53
|
+
overflowed = true;
|
|
54
|
+
chunks.length = 0;
|
|
55
|
+
finish(null);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
chunks.push(chunk);
|
|
59
|
+
};
|
|
60
|
+
process.stdin.on("data", onData);
|
|
61
|
+
process.stdin.on("end", () => finish(Buffer.concat(chunks).toString("utf8")));
|
|
62
|
+
process.stdin.on("error", () => finish(Buffer.concat(chunks).toString("utf8")));
|
|
63
|
+
setTimeout(
|
|
64
|
+
() => finish(Buffer.concat(chunks).toString("utf8")),
|
|
65
|
+
IDLE_TIMEOUT_MS
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
readHookStdin
|
|
72
|
+
};
|