kojee-mcp 0.2.1 → 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/dist/cli.js CHANGED
@@ -1,41 +1,211 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AuthModule,
3
4
  startProxy
4
- } from "./chunk-RNKJONY3.js";
5
+ } from "./chunk-ZGVUM4AG.js";
6
+ import {
7
+ loadPairedConfig,
8
+ pairedConfigPath,
9
+ savePairedConfig
10
+ } from "./chunk-E7TE4QZD.js";
11
+ import "./chunk-36DMIXH7.js";
5
12
 
6
13
  // src/cli.ts
7
14
  import { Command } from "commander";
15
+ import crypto from "crypto";
8
16
  import path from "path";
9
- var DEFAULT_KEYSTORE_PATH = path.join(
10
- process.env["HOME"] ?? "~",
11
- ".kojee",
12
- "keypair.json"
13
- );
17
+
18
+ // src/tandem/pair.ts
19
+ import fs from "fs";
20
+ async function runPair(opts) {
21
+ if (loadPairedConfig(opts.configPath) !== null) {
22
+ throw new Error(
23
+ `Already paired (config exists at ${opts.configPath}). To re-pair this slot, delete the config file first. For a second account, use --keystore-path /custom/keypair.json.`
24
+ );
25
+ }
26
+ try {
27
+ fs.unlinkSync(opts.keystorePath);
28
+ } catch {
29
+ }
30
+ const auth = new AuthModule(opts.code, opts.url, opts.keystorePath);
31
+ await auth.ensureEnrolled();
32
+ let principal_id;
33
+ let agent_id;
34
+ try {
35
+ const me = await fetch(`${opts.url}/api/v1/users/me/`, {
36
+ headers: { Authorization: `DPoP ${opts.code}` }
37
+ });
38
+ if (me.ok) {
39
+ const body = await me.json();
40
+ principal_id = body.principal_id;
41
+ agent_id = body.agent_id;
42
+ }
43
+ } catch {
44
+ }
45
+ const config = {
46
+ token: opts.code,
47
+ broker_url: opts.url,
48
+ paired_at: (/* @__PURE__ */ new Date()).toISOString(),
49
+ ...principal_id ? { principal_id } : {},
50
+ ...agent_id ? { agent_id } : {}
51
+ };
52
+ savePairedConfig(opts.configPath, config);
53
+ const who = principal_id && agent_id ? `${principal_id} (${agent_id})` : "(use kojee-mcp without args to start the proxy)";
54
+ return { message: `Paired as ${who}. Keypair: ${opts.keystorePath}. Config: ${opts.configPath}.` };
55
+ }
56
+
57
+ // src/cli.ts
58
+ var KOJEE_DIR = path.join(process.env["HOME"] ?? "~", ".kojee");
59
+ function deriveKeystorePath(token) {
60
+ const hash = crypto.createHash("sha256").update(token).digest("hex").slice(0, 12);
61
+ return path.join(KOJEE_DIR, `keypair-${hash}.json`);
62
+ }
63
+ function defaultPairedKeystorePath() {
64
+ return path.join(KOJEE_DIR, "keypair.json");
65
+ }
14
66
  var program = new Command().name("kojee-mcp").description(
15
67
  "Local MCP proxy for Kojee \u2014 handles DPoP auth, tool discovery, and governance transparently"
16
- ).version("0.1.0").requiredOption(
17
- "--token <token>",
18
- "Gateway token (gw_...)"
19
- ).requiredOption(
20
- "--url <url>",
21
- "Broker base URL (e.g. https://kojee.ai)"
22
- ).option(
23
- "--keystore-path <path>",
24
- "Path to persisted keypair file",
25
- DEFAULT_KEYSTORE_PATH
26
- ).action(async (opts) => {
27
- if (!opts.token.startsWith("gw_")) {
68
+ ).version("0.3.0").enablePositionalOptions();
69
+ program.command("pair <code>").description("Pair this machine against Kojee using a pair code from the dashboard").requiredOption("--url <url>", "Broker base URL (e.g. https://rosie-server.kojee.net)").option("--keystore-path <path>", "Path to keypair.json (default ~/.kojee/keypair.json)").action(async (code, opts) => {
70
+ const url = opts.url.replace(/\/+$/, "");
71
+ const keystorePath = opts.keystorePath ?? defaultPairedKeystorePath();
72
+ const configPath = opts.keystorePath ? path.join(path.dirname(opts.keystorePath), "config.json") : pairedConfigPath();
73
+ try {
74
+ const result = await runPair({ code, url, keystorePath, configPath });
75
+ console.error(result.message);
76
+ } catch (err) {
77
+ console.error("[kojee-mcp pair] Failed:", err.message);
78
+ process.exit(1);
79
+ }
80
+ });
81
+ program.command("hook").description("Run a kojee MCP hook script (called by Claude Code via ~/.claude/settings.json)").requiredOption("--type <type>", "Hook type: stop or user-prompt-submit").action(async (opts) => {
82
+ if (opts.type === "stop") {
83
+ const { runStopHook } = await import("./stop-hook-5XU3EQAE.js");
84
+ await runStopHook();
85
+ process.exit(0);
86
+ } else if (opts.type === "user-prompt-submit") {
87
+ const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-WSRIJVF4.js");
88
+ await runUserPromptSubmitHook();
89
+ process.exit(0);
90
+ } else {
91
+ console.error(`Unknown hook type: ${opts.type}. Expected 'stop' or 'user-prompt-submit'.`);
92
+ process.exit(1);
93
+ }
94
+ });
95
+ program.command("install-hooks").description("Install kojee Stop + UserPromptSubmit hooks in ~/.claude/settings.json (idempotent)").option("--hooks-path <path>", "Override default ~/.claude/settings.json").option("--uninstall", "Remove kojee hook entries instead of installing them").action(async (opts) => {
96
+ const { installHooks, uninstallHooks } = await import("./install-WV25CRU2.js");
97
+ if (opts.uninstall) {
98
+ const removed = uninstallHooks({ hooksPath: opts.hooksPath });
99
+ console.error(removed ? "Removed kojee hook entries." : "No kojee hook entries found.");
100
+ } else {
101
+ const { stop, ups } = installHooks({ hooksPath: opts.hooksPath });
28
102
  console.error(
29
- "Warning: Gateway token does not start with 'gw_'. Ensure you are using a valid gateway token."
103
+ "Installed kojee hooks in " + (opts.hooksPath ?? "~/.claude/settings.json") + `:
104
+ - Stop: ${stop === "added" ? "\u2713 added" : "\u21BB already installed"}
105
+ - UserPromptSubmit: ${ups === "added" ? "\u2713 added" : "\u21BB already installed"}
106
+ Restart Claude Code for hooks to take effect.`
30
107
  );
31
108
  }
32
- const url = opts.url.replace(/\/+$/, "");
109
+ });
110
+ program.command("init").description("Install kojee into Claude Code (MCP server entry + hooks). Run after `kojee-mcp pair`.").option("--config-path <path>", "Override default ~/.claude.json").option("--uninstall", "Remove the kojee MCP server entry and hooks").action(async (opts) => {
111
+ const { loadPairedConfig: loadPairedConfig2 } = await import("./paired-config-OAR3O3XY.js");
112
+ if (loadPairedConfig2() === null && !opts.uninstall) {
113
+ console.error("Not paired. Run `kojee-mcp pair <code> --url <broker>` first, then re-run `init`.");
114
+ process.exit(1);
115
+ }
116
+ const { runInit, runUninstall } = await import("./install-WV25CRU2.js");
117
+ if (opts.uninstall) {
118
+ const report = runUninstall({ configPath: opts.configPath });
119
+ console.error(formatUninstall(report));
120
+ } else {
121
+ const report = runInit({ configPath: opts.configPath });
122
+ console.error(formatInit(report));
123
+ }
124
+ });
125
+ function formatInit(report) {
126
+ const tick = (s) => {
127
+ if (s === "added") return "\u2713 added";
128
+ if (s === "already-installed") return "\u21BB already installed";
129
+ if (s === "preserved-different") return "\u26A0 preserved (existing entry differs \u2014 left untouched)";
130
+ if (s === "not-found") return "\u2014 not found";
131
+ return s ?? "\u2014";
132
+ };
133
+ const lines = ["Installing kojee for Claude:"];
134
+ for (const t of report.targets) {
135
+ const label = t.kind === "cli" ? "CLI" : "Claude.app";
136
+ lines.push("");
137
+ lines.push(` ${t.path} (${label})`);
138
+ lines.push(` mcpServers.kojee ${tick(t.mcpServer)}`);
139
+ if (t.kind === "cli") {
140
+ if (t.hooksPath) {
141
+ lines.push(` ${t.hooksPath} (hooks)`);
142
+ }
143
+ lines.push(` hooks.Stop ${tick(t.stopHook)}`);
144
+ lines.push(` hooks.UserPromptSubmit ${tick(t.userPromptSubmitHook)}`);
145
+ } else {
146
+ lines.push(` (hooks not applicable for Claude.app agent mode)`);
147
+ }
148
+ }
149
+ lines.push("");
150
+ const desktopWritten = report.targets.some((t) => t.kind === "desktop" && t.mcpServer === "added");
151
+ if (desktopWritten) {
152
+ lines.push(
153
+ "Existing Claude.app agent-mode sessions snapshotted the previous config",
154
+ "and won't pick up this change automatically. Start a NEW agent-mode",
155
+ "session (not a resumed one) to use the updated kojee config.",
156
+ ""
157
+ );
158
+ }
159
+ lines.push("To verify: in any new CC session, run /mcp and confirm `kojee` is listed.");
160
+ lines.push(" run /hooks and confirm both Stop and UserPromptSubmit show the kojee entries.");
161
+ lines.push("To remove: `kojee-mcp init --uninstall`");
162
+ return lines.join("\n");
163
+ }
164
+ function formatUninstall(report) {
165
+ const lines = ["Removing kojee from Claude:"];
166
+ for (const t of report.targets) {
167
+ const label = t.kind === "cli" ? "CLI" : "Claude.app";
168
+ lines.push("");
169
+ lines.push(` ${t.path} (${label})`);
170
+ lines.push(` mcpServers.kojee ${t.mcpServer ? "\u2713 removed" : "\u2014 not found"}`);
171
+ if (t.kind === "cli") {
172
+ if (t.hooksPath) {
173
+ lines.push(` ${t.hooksPath} (hooks)`);
174
+ }
175
+ lines.push(` hook entries ${t.hooks ? "\u2713 removed" : "\u2014 not found"}`);
176
+ }
177
+ }
178
+ return lines.join("\n");
179
+ }
180
+ program.option("--token <token>", "Gateway token (for token-mode)").option("--url <url>", "Broker base URL (for token-mode; required if --token is passed)").option(
181
+ "--keystore-path <path>",
182
+ "Path to keypair file (defaults to per-token path under ~/.kojee/)"
183
+ ).action(async (opts) => {
184
+ let token = opts.token;
185
+ let url = opts.url;
186
+ let keystorePath = opts.keystorePath;
187
+ if (token) {
188
+ if (!url) {
189
+ console.error("--url is required when --token is provided");
190
+ process.exit(1);
191
+ }
192
+ url = url.replace(/\/+$/, "");
193
+ keystorePath ??= deriveKeystorePath(token);
194
+ } else {
195
+ const { loadPairedConfig: loadPairedConfig2 } = await import("./paired-config-OAR3O3XY.js");
196
+ const cfg = loadPairedConfig2();
197
+ if (!cfg) {
198
+ console.error(
199
+ "No --token provided and no ~/.kojee/config.json found. Run `kojee-mcp pair <code> --url <broker>` first."
200
+ );
201
+ process.exit(1);
202
+ }
203
+ token = cfg.token;
204
+ url = (opts.url ?? cfg.broker_url).replace(/\/+$/, "");
205
+ keystorePath ??= defaultPairedKeystorePath();
206
+ }
33
207
  try {
34
- await startProxy({
35
- token: opts.token,
36
- url,
37
- keystorePath: opts.keystorePath
38
- });
208
+ await startProxy({ token, url, keystorePath });
39
209
  } catch (err) {
40
210
  console.error("[kojee-mcp] Fatal error:", err);
41
211
  process.exit(1);
@@ -0,0 +1,112 @@
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
+ };
@@ -0,0 +1,43 @@
1
+ // src/tandem/event-queue.ts
2
+ var EventQueue = class {
3
+ entries = [];
4
+ capacity;
5
+ maxAgeMs;
6
+ constructor(opts = {}) {
7
+ this.capacity = opts.capacity ?? 200;
8
+ this.maxAgeMs = opts.maxAgeMs ?? 10 * 6e4;
9
+ }
10
+ push(event) {
11
+ const now = Date.now();
12
+ this.entries = this.entries.filter((e) => now - e.receivedAt <= this.maxAgeMs);
13
+ this.entries.push({
14
+ event,
15
+ receivedAt: now,
16
+ deliveredViaChannel: false,
17
+ deliveredViaMonitor: false,
18
+ deliveredViaHook: false
19
+ });
20
+ while (this.entries.length > this.capacity) this.entries.shift();
21
+ }
22
+ markChannelDelivered(eventId) {
23
+ const entry = this.entries.find((e) => e.event.id === eventId);
24
+ if (entry) entry.deliveredViaChannel = true;
25
+ }
26
+ markMonitorDelivered(eventId) {
27
+ const entry = this.entries.find((e) => e.event.id === eventId);
28
+ if (entry) entry.deliveredViaMonitor = true;
29
+ }
30
+ takeForHook() {
31
+ const eligible = this.entries.filter(
32
+ (e) => !e.deliveredViaChannel && !e.deliveredViaMonitor && !e.deliveredViaHook
33
+ );
34
+ for (const e of eligible) e.deliveredViaHook = true;
35
+ return eligible;
36
+ }
37
+ snapshot() {
38
+ return [...this.entries];
39
+ }
40
+ };
41
+ export {
42
+ EventQueue
43
+ };
@@ -0,0 +1,71 @@
1
+ // src/tandem/hook-server.ts
2
+ import { createServer } from "http";
3
+ async function startHookServer(opts) {
4
+ const server = createServer((req, res) => {
5
+ handleRequest(req, res, opts).catch((err) => {
6
+ console.error("[hook-server] error:", err);
7
+ if (!res.headersSent) {
8
+ res.writeHead(500, { "Content-Type": "application/json" });
9
+ res.end(JSON.stringify({ error: "internal_error" }));
10
+ }
11
+ });
12
+ });
13
+ await new Promise((resolve) => server.listen(opts.port ?? 0, "127.0.0.1", resolve));
14
+ const addr = server.address();
15
+ return {
16
+ port: addr.port,
17
+ stop: () => new Promise((resolve, reject) => {
18
+ server.close((err) => err ? reject(err) : resolve());
19
+ })
20
+ };
21
+ }
22
+ async function handleRequest(req, res, opts) {
23
+ const url = new URL(req.url ?? "/", "http://x");
24
+ if (req.method === "GET" && url.pathname === "/health") {
25
+ return json(res, 200, { ok: true });
26
+ }
27
+ if (req.method === "GET" && url.pathname === "/poll") {
28
+ const type = url.searchParams.get("type") ?? "";
29
+ const timeoutMs = Number.parseInt(url.searchParams.get("timeout_ms") ?? "0", 10);
30
+ if (type === "user-prompt-submit") {
31
+ return respondWithEvents(res, opts);
32
+ }
33
+ if (type === "stop") {
34
+ return longPollAndRespond(res, opts, Math.max(0, timeoutMs));
35
+ }
36
+ return json(res, 400, { error: "unknown type", detail: "type must be 'stop' or 'user-prompt-submit'" });
37
+ }
38
+ return json(res, 404, { error: "not_found", detail: `${req.method} ${url.pathname}` });
39
+ }
40
+ function respondWithEvents(res, opts) {
41
+ const entries = opts.queue.takeForHook();
42
+ const events = entries.map((entry) => opts.adapter.formatTandemEvent(entry.event));
43
+ json(res, 200, { events, count: events.length });
44
+ }
45
+ async function longPollAndRespond(res, opts, timeoutMs) {
46
+ const immediate = opts.queue.takeForHook();
47
+ if (immediate.length > 0) {
48
+ const events = immediate.map((e) => opts.adapter.formatTandemEvent(e.event));
49
+ return json(res, 200, { events, count: events.length });
50
+ }
51
+ const deadline = Date.now() + timeoutMs;
52
+ while (Date.now() < deadline) {
53
+ await sleep(100);
54
+ const batch = opts.queue.takeForHook();
55
+ if (batch.length > 0) {
56
+ const events = batch.map((e) => opts.adapter.formatTandemEvent(e.event));
57
+ return json(res, 200, { events, count: events.length });
58
+ }
59
+ }
60
+ json(res, 200, { events: [], count: 0 });
61
+ }
62
+ function json(res, status, body) {
63
+ res.writeHead(status, { "Content-Type": "application/json" });
64
+ res.end(JSON.stringify(body));
65
+ }
66
+ function sleep(ms) {
67
+ return new Promise((r) => setTimeout(r, ms));
68
+ }
69
+ export {
70
+ startHookServer
71
+ };
package/dist/index.d.ts CHANGED
@@ -4,16 +4,6 @@ interface ProxyConfig {
4
4
  keystorePath: string;
5
5
  }
6
6
 
7
- /**
8
- * Bootstrap and start the Kojee MCP proxy.
9
- *
10
- * Startup sequence:
11
- * 1. Enroll keypair (or load existing)
12
- * 2. Derive session ID
13
- * 3. Create gateway client
14
- * 4. Discover tools from gateway
15
- * 5. Start MCP stdio server
16
- */
17
7
  declare function startProxy(config: ProxyConfig): Promise<void>;
18
8
 
19
9
  export { type ProxyConfig, startProxy };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  startProxy
3
- } from "./chunk-RNKJONY3.js";
3
+ } from "./chunk-ZGVUM4AG.js";
4
+ import "./chunk-36DMIXH7.js";
4
5
  export {
5
6
  startProxy
6
7
  };
@@ -0,0 +1,182 @@
1
+ // src/hooks/install.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ function discoverInstallTargets() {
5
+ const home = process.env["HOME"] ?? "~";
6
+ const targets = [];
7
+ const cliPath = path.join(home, ".claude.json");
8
+ const cliHooksPath = path.join(home, ".claude", "settings.json");
9
+ targets.push({ kind: "cli", path: cliPath, exists: fs.existsSync(cliPath), hooksPath: cliHooksPath });
10
+ let desktopPath = null;
11
+ if (process.platform === "darwin") {
12
+ desktopPath = path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
13
+ } else if (process.platform === "win32") {
14
+ const appData = process.env["APPDATA"] ?? path.join(home, "AppData", "Roaming");
15
+ desktopPath = path.join(appData, "Claude", "claude_desktop_config.json");
16
+ } else if (process.platform === "linux") {
17
+ const xdgConfig = process.env["XDG_CONFIG_HOME"] ?? path.join(home, ".config");
18
+ desktopPath = path.join(xdgConfig, "Claude", "claude_desktop_config.json");
19
+ }
20
+ if (desktopPath) {
21
+ targets.push({ kind: "desktop", path: desktopPath, exists: fs.existsSync(desktopPath) });
22
+ }
23
+ return targets;
24
+ }
25
+ var STOP_COMMAND = "npx -y kojee-mcp hook --type=stop";
26
+ var UPS_COMMAND = "npx -y kojee-mcp hook --type=user-prompt-submit";
27
+ var MCP_SERVER_CMD = "npx";
28
+ var MCP_SERVER_ARGS = ["kojee-mcp"];
29
+ var MCP_SERVER_ENV = { KOJEE_RUNTIME: "claude-code" };
30
+ function defaultConfigPath() {
31
+ return path.join(process.env["HOME"] ?? "~", ".claude.json");
32
+ }
33
+ function defaultHooksPath() {
34
+ return path.join(process.env["HOME"] ?? "~", ".claude", "settings.json");
35
+ }
36
+ function deriveHooksPath(configPath) {
37
+ return path.join(path.dirname(configPath), ".claude", "settings.json");
38
+ }
39
+ function readConfig(p) {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(p, "utf8"));
42
+ } catch {
43
+ return {};
44
+ }
45
+ }
46
+ function writeConfig(p, cfg) {
47
+ const dir = path.dirname(p);
48
+ fs.mkdirSync(dir, { recursive: true });
49
+ fs.writeFileSync(p, JSON.stringify(cfg, null, 2), { mode: 384 });
50
+ try {
51
+ fs.chmodSync(p, 384);
52
+ } catch {
53
+ }
54
+ }
55
+ function hasKojeeEntry(arr, command) {
56
+ if (!arr) return false;
57
+ return arr.some((e) => e.hooks.some((h) => h.command === command));
58
+ }
59
+ function installHookEntry(cfg, event, command) {
60
+ cfg.hooks ??= {};
61
+ cfg.hooks[event] ??= [];
62
+ if (hasKojeeEntry(cfg.hooks[event], command)) return "already-installed";
63
+ cfg.hooks[event].push({ hooks: [{ type: "command", command }] });
64
+ return "added";
65
+ }
66
+ function uninstallHookEntries(cfg) {
67
+ let removed = false;
68
+ if (!cfg.hooks) return false;
69
+ for (const event of ["Stop", "UserPromptSubmit"]) {
70
+ const arr = cfg.hooks[event];
71
+ if (!arr) continue;
72
+ const before = arr.length;
73
+ cfg.hooks[event] = arr.filter(
74
+ (entry) => !entry.hooks.some((h) => h.command.startsWith("npx -y kojee-mcp hook"))
75
+ );
76
+ if (cfg.hooks[event].length !== before) removed = true;
77
+ }
78
+ return removed;
79
+ }
80
+ function installHooks(opts = {}) {
81
+ const p = opts.hooksPath ?? (opts.configPath ? deriveHooksPath(opts.configPath) : defaultHooksPath());
82
+ const cfg = readConfig(p);
83
+ const stop = installHookEntry(cfg, "Stop", STOP_COMMAND);
84
+ const ups = installHookEntry(cfg, "UserPromptSubmit", UPS_COMMAND);
85
+ writeConfig(p, cfg);
86
+ return { stop, ups };
87
+ }
88
+ function uninstallHooks(opts = {}) {
89
+ const p = opts.hooksPath ?? (opts.configPath ? deriveHooksPath(opts.configPath) : defaultHooksPath());
90
+ const cfg = readConfig(p);
91
+ const removed = uninstallHookEntries(cfg);
92
+ writeConfig(p, cfg);
93
+ return removed;
94
+ }
95
+ function installMcpServer(opts = {}) {
96
+ const p = opts.configPath ?? defaultConfigPath();
97
+ const cfg = readConfig(p);
98
+ cfg.mcpServers ??= {};
99
+ const existing = cfg.mcpServers["kojee"];
100
+ if (existing) {
101
+ const sameCommand = existing.command === MCP_SERVER_CMD;
102
+ const sameArgs = JSON.stringify(existing.args ?? []) === JSON.stringify(MCP_SERVER_ARGS);
103
+ const sameEnv = JSON.stringify(existing.env ?? {}) === JSON.stringify(MCP_SERVER_ENV);
104
+ if (sameCommand && sameArgs && sameEnv) return "already-installed";
105
+ return "preserved-different";
106
+ }
107
+ cfg.mcpServers["kojee"] = {
108
+ command: MCP_SERVER_CMD,
109
+ args: [...MCP_SERVER_ARGS],
110
+ env: { ...MCP_SERVER_ENV }
111
+ };
112
+ writeConfig(p, cfg);
113
+ return "added";
114
+ }
115
+ function uninstallMcpServer(opts = {}) {
116
+ const p = opts.configPath ?? defaultConfigPath();
117
+ const cfg = readConfig(p);
118
+ if (!cfg.mcpServers || !cfg.mcpServers["kojee"]) return false;
119
+ delete cfg.mcpServers["kojee"];
120
+ writeConfig(p, cfg);
121
+ return true;
122
+ }
123
+ function runInit(opts = {}) {
124
+ const targets = opts.configPath ? [{
125
+ kind: "cli",
126
+ path: opts.configPath,
127
+ exists: true,
128
+ hooksPath: opts.hooksPath ?? deriveHooksPath(opts.configPath)
129
+ }] : discoverInstallTargets();
130
+ const reports = [];
131
+ for (const t of targets) {
132
+ if (!t.exists) {
133
+ reports.push({ kind: t.kind, path: t.path, ...t.hooksPath ? { hooksPath: t.hooksPath } : {}, mcpServer: "not-found" });
134
+ continue;
135
+ }
136
+ const mcpServer = installMcpServer({ configPath: t.path });
137
+ let stopHook;
138
+ let upsHook;
139
+ if (t.kind === "cli") {
140
+ const hookReport = installHooks({ hooksPath: t.hooksPath ?? deriveHooksPath(t.path) });
141
+ stopHook = hookReport.stop;
142
+ upsHook = hookReport.ups;
143
+ }
144
+ reports.push({
145
+ kind: t.kind,
146
+ path: t.path,
147
+ ...t.hooksPath ? { hooksPath: t.hooksPath } : {},
148
+ mcpServer,
149
+ ...stopHook ? { stopHook } : {},
150
+ ...upsHook ? { userPromptSubmitHook: upsHook } : {}
151
+ });
152
+ }
153
+ return { targets: reports, configPath: targets[0]?.path };
154
+ }
155
+ function runUninstall(opts = {}) {
156
+ const targets = opts.configPath ? [{
157
+ kind: "cli",
158
+ path: opts.configPath,
159
+ exists: true,
160
+ hooksPath: opts.hooksPath ?? deriveHooksPath(opts.configPath)
161
+ }] : discoverInstallTargets();
162
+ const reports = [];
163
+ for (const t of targets) {
164
+ if (!t.exists) {
165
+ reports.push({ kind: t.kind, path: t.path, ...t.hooksPath ? { hooksPath: t.hooksPath } : {}, mcpServer: false, hooks: false });
166
+ continue;
167
+ }
168
+ const mcpServer = uninstallMcpServer({ configPath: t.path });
169
+ const hooks = t.kind === "cli" ? uninstallHooks({ hooksPath: t.hooksPath ?? deriveHooksPath(t.path) }) : false;
170
+ reports.push({ kind: t.kind, path: t.path, ...t.hooksPath ? { hooksPath: t.hooksPath } : {}, mcpServer, hooks });
171
+ }
172
+ return { targets: reports, configPath: targets[0]?.path };
173
+ }
174
+ export {
175
+ discoverInstallTargets,
176
+ installHooks,
177
+ installMcpServer,
178
+ runInit,
179
+ runUninstall,
180
+ uninstallHooks,
181
+ uninstallMcpServer
182
+ };
@@ -0,0 +1,10 @@
1
+ import {
2
+ loadPairedConfig,
3
+ pairedConfigPath,
4
+ savePairedConfig
5
+ } from "./chunk-E7TE4QZD.js";
6
+ export {
7
+ loadPairedConfig,
8
+ pairedConfigPath,
9
+ savePairedConfig
10
+ };