kibi-mcp 0.1.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/bin/kibi-mcp ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
4
+ Copyright (C) 2026 Piotr Franczyk
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+ */
19
+ import { startServer } from "../dist/server.js";
20
+
21
+ if (process.env.KIBI_MCP_DEBUG) {
22
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
23
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
24
+
25
+ process.stdout.write = function(chunk, encoding, callback) {
26
+ const str = chunk.toString().trim();
27
+ if (str) {
28
+ originalStderrWrite(`[KIBI-MCP-OUT] ${str}\n`);
29
+ }
30
+ return originalStdoutWrite(chunk, encoding, callback);
31
+ };
32
+
33
+ process.stdin.on('data', (data) => {
34
+ const str = data.toString().trim();
35
+ if (str) {
36
+ originalStderrWrite(`[KIBI-MCP-IN] ${str}\n`);
37
+ }
38
+ });
39
+ }
40
+
41
+ process.on('unhandledRejection', (reason, promise) => {
42
+ console.error('[KIBI-MCP] Unhandled rejection at promise:', promise);
43
+ console.error('[KIBI-MCP] Reason:', reason);
44
+ if (reason instanceof Error) {
45
+ console.error('[KIBI-MCP] Stack:', reason.stack);
46
+ }
47
+ process.exit(1);
48
+ });
49
+
50
+ process.on('uncaughtException', (error) => {
51
+ console.error('[KIBI-MCP] Uncaught exception:', error.message);
52
+ console.error('[KIBI-MCP] Stack:', error.stack);
53
+ process.exit(1);
54
+ });
55
+
56
+ startServer().catch((error) => {
57
+ console.error("Fatal error:", error);
58
+ process.exit(1);
59
+ });
package/dist/env.js ADDED
@@ -0,0 +1,99 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ /*
19
+ How to apply this header to source files (examples)
20
+
21
+ 1) Prepend header to a single file (POSIX shells):
22
+
23
+ cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
24
+
25
+ 2) Apply to multiple files (example: the project's main entry files):
26
+
27
+ for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
28
+ if [ -f "$f" ]; then
29
+ cp "$f" "$f".bak
30
+ (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
31
+ fi
32
+ done
33
+
34
+ 3) Avoid duplicating the header: run a quick guard to only add if missing
35
+
36
+ for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
37
+ if [ -f "$f" ]; then
38
+ if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
39
+ cp "$f" "$f".bak
40
+ (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
41
+ fi
42
+ fi
43
+ done
44
+ */
45
+ import fs from "node:fs";
46
+ import { resolveEnvFilePath, resolveWorkspaceRoot } from "./workspace.js";
47
+ const DEFAULT_ENV_FILE = ".env";
48
+ export function loadDefaultEnvFile() {
49
+ const envFileName = process.env.KIBI_ENV_FILE ?? DEFAULT_ENV_FILE;
50
+ const workspaceRoot = resolveWorkspaceRoot();
51
+ return loadEnvFile({ envFileName, workspaceRoot });
52
+ }
53
+ export function loadEnvFile(options) {
54
+ const { envFileName, workspaceRoot } = options;
55
+ const envFilePath = resolveEnvFilePath(envFileName, workspaceRoot);
56
+ const keysLoaded = [];
57
+ if (!fs.existsSync(envFilePath)) {
58
+ return { loaded: false, envFilePath, keysLoaded };
59
+ }
60
+ try {
61
+ const raw = fs.readFileSync(envFilePath, "utf8");
62
+ for (const { key, value } of parseEnvContent(raw)) {
63
+ if (!key || Object.prototype.hasOwnProperty.call(process.env, key)) {
64
+ continue;
65
+ }
66
+ process.env[key] = value;
67
+ keysLoaded.push(key);
68
+ }
69
+ return { loaded: true, envFilePath, keysLoaded };
70
+ }
71
+ catch (error) {
72
+ console.error(`[Kibi] Unable to load environment file ${envFilePath}: ${error instanceof Error ? error.message : String(error)}`);
73
+ return { loaded: false, envFilePath, keysLoaded };
74
+ }
75
+ }
76
+ function parseEnvContent(content) {
77
+ const lines = content.split(/\r?\n/);
78
+ const entries = [];
79
+ for (const rawLine of lines) {
80
+ const line = rawLine.trim();
81
+ if (!line || line.startsWith("#")) {
82
+ continue;
83
+ }
84
+ const eqIndex = line.indexOf("=");
85
+ if (eqIndex <= 0) {
86
+ continue;
87
+ }
88
+ const key = line.substring(0, eqIndex).trim();
89
+ let value = line.substring(eqIndex + 1).trim();
90
+ if (value.startsWith('"') && value.endsWith('"')) {
91
+ value = value.slice(1, -1);
92
+ }
93
+ else if (value.startsWith("'") && value.endsWith("'")) {
94
+ value = value.slice(1, -1);
95
+ }
96
+ entries.push({ key, value });
97
+ }
98
+ return entries;
99
+ }
package/dist/mcpcat.js ADDED
@@ -0,0 +1,129 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ /*
19
+ How to apply this header to source files (examples)
20
+
21
+ 1) Prepend header to a single file (POSIX shells):
22
+
23
+ cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
24
+
25
+ 2) Apply to multiple files (example: the project's main entry files):
26
+
27
+ for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
28
+ if [ -f "$f" ]; then
29
+ cp "$f" "$f".bak
30
+ (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
31
+ fi
32
+ done
33
+
34
+ 3) Avoid duplicating the header: run a quick guard to only add if missing
35
+
36
+ for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
37
+ if [ -f "$f" ]; then
38
+ if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
39
+ cp "$f" "$f".bak
40
+ (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
41
+ fi
42
+ fi
43
+ done
44
+ */
45
+ import { createHash } from "node:crypto";
46
+ import fs from "node:fs";
47
+ import os from "node:os";
48
+ import path from "node:path";
49
+ import * as mcpcat from "mcpcat";
50
+ import { resolveWorkspaceRoot } from "./workspace.js";
51
+ const projectId = (process.env.MCPCAT_PROJECT_ID ?? "").trim();
52
+ const trackedIdentity = resolveTrackedIdentity();
53
+ /**
54
+ * Attach mcpcat analytics tracking to the MCP server.
55
+ *
56
+ * NOTE ON SESSIONS: With stdio transport, many MCP clients (including OpenCode)
57
+ * spawn a new process for each tool call. This means each tool call gets a new
58
+ * MCP session ID, resulting in single-tool-call "sessions" in mcpcat.
59
+ *
60
+ * This is expected behavior for stdio transport - each process IS a different
61
+ * session. User identity (via the identify() function) still provides useful
62
+ * aggregation across all tool calls from the same user/machine.
63
+ *
64
+ * For true session aggregation, clients would need to either:
65
+ * 1. Use HTTP transport with persistent connections
66
+ * 2. Maintain long-lived stdio connections across multiple tool calls
67
+ * 3. Implement custom session headers
68
+ */
69
+ export function attachMcpcat(server) {
70
+ if (!projectId) {
71
+ return;
72
+ }
73
+ try {
74
+ mcpcat.track(server, projectId, {
75
+ identify: async () => trackedIdentity,
76
+ enableReportMissing: false, // Don't add get_more_tools tool - it's internal
77
+ enableTracing: true,
78
+ enableToolCallContext: false, // Don't inject context parameter into tools
79
+ });
80
+ if (process.env.KIBI_MCP_DEBUG) {
81
+ console.error(`[KIBI-MCP] MCPcat tracking enabled for project ${projectId}`);
82
+ }
83
+ }
84
+ catch (error) {
85
+ const details = error instanceof Error ? error.message : String(error);
86
+ console.error(`[KIBI-MCP] MCPcat tracking attach failed: ${details}`);
87
+ }
88
+ }
89
+ function resolveTrackedIdentity() {
90
+ const explicitUserId = readEnv("MCPCAT_USER_ID");
91
+ if (explicitUserId) {
92
+ return {
93
+ userId: explicitUserId,
94
+ userName: readEnv("MCPCAT_USER_NAME") ?? "local-operator",
95
+ userData: { identitySource: "env" },
96
+ };
97
+ }
98
+ const repoRoot = findRepoRoot(resolveWorkspaceRoot());
99
+ const repoName = path.basename(repoRoot);
100
+ const username = readEnv("USER") ?? readEnv("USERNAME") ?? "unknown-user";
101
+ const host = os.hostname() || "unknown-host";
102
+ const stableId = createHash("sha256")
103
+ .update(`${host}:${username}:${repoRoot}`)
104
+ .digest("hex")
105
+ .slice(0, 24);
106
+ return {
107
+ userId: `anon_${stableId}`,
108
+ userName: `local-${repoName}`,
109
+ userData: { identitySource: "host-user-repo-hash", repo: repoName },
110
+ };
111
+ }
112
+ function readEnv(name) {
113
+ const value = process.env[name]?.trim();
114
+ return value ? value : null;
115
+ }
116
+ function findRepoRoot(startDir) {
117
+ let current = path.resolve(startDir);
118
+ while (true) {
119
+ const gitMarker = path.join(current, ".git");
120
+ if (fs.existsSync(gitMarker)) {
121
+ return current;
122
+ }
123
+ const parent = path.dirname(current);
124
+ if (parent === current) {
125
+ return path.resolve(startDir);
126
+ }
127
+ current = parent;
128
+ }
129
+ }