kibi-mcp 0.2.0 → 0.2.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.
package/dist/server.js CHANGED
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import fs from "node:fs";
19
19
  import { createRequire } from "node:module";
20
+ import path from "node:path";
20
21
  /*
21
22
  How to apply this header to source files (examples)
22
23
 
@@ -51,13 +52,64 @@ import { PrologProcess } from "kibi-cli/prolog";
51
52
  import { copyCleanSnapshot, getBranchDiagnostic, isValidBranchName, resolveActiveBranch, } from "kibi-cli/public/branch-resolver";
52
53
  import { z } from "zod";
53
54
  import { loadDefaultEnvFile } from "./env.js";
54
- import { attachMcpcat } from "./mcpcat.js";
55
55
  import { TOOLS } from "./tools-config.js";
56
56
  import { handleKbCheck } from "./tools/check.js";
57
57
  import { handleKbDelete } from "./tools/delete.js";
58
58
  import { handleKbQuery } from "./tools/query.js";
59
59
  import { handleKbUpsert } from "./tools/upsert.js";
60
60
  import { resolveKbPath, resolveWorkspaceRoot } from "./workspace.js";
61
+ const DIAGNOSTIC_MODE_FLAG = "--diagnostic-mode";
62
+ const DIAGNOSTIC_MODE_ENABLED = process.argv.includes(DIAGNOSTIC_MODE_FLAG);
63
+ const DIAGNOSTIC_TELEMETRY_SCHEMA = {
64
+ type: "object",
65
+ description: "REQUIRED when diagnostic mode is on. Provide self-reflection metadata about this tool call.",
66
+ properties: {
67
+ is_autonomous: {
68
+ type: "boolean",
69
+ description: "Set to TRUE if you are calling this tool on your own initiative to retrieve context. Set to FALSE if the user explicitly commanded you to use the knowledge base.",
70
+ },
71
+ reasoning: {
72
+ type: "string",
73
+ description: "A brief, 1-2 sentence internal thought explaining exactly why you are calling this tool right now and what information you expect to get.",
74
+ },
75
+ confidence_score: {
76
+ type: "number",
77
+ description: "A score from 0.0 to 1.0 representing your confidence that the exact parameters, IDs, or tags you provided will yield a successful result.",
78
+ },
79
+ attempt_number: {
80
+ type: "integer",
81
+ description: "If you are retrying this exact task because a previous tool call failed or returned empty results, increment this number (start at 1).",
82
+ },
83
+ missing_context: {
84
+ type: "string",
85
+ description: "If you had to split your task into multiple steps because this tool lacks a specific filtering or querying capability, describe what parameter is missing. Otherwise, leave empty.",
86
+ },
87
+ },
88
+ };
89
+ function withDiagnosticTelemetrySchema(tools) {
90
+ return tools.map((tool) => {
91
+ const schema = tool.inputSchema && typeof tool.inputSchema === "object"
92
+ ? tool.inputSchema
93
+ : {};
94
+ const properties = schema.properties && typeof schema.properties === "object"
95
+ ? schema.properties
96
+ : {};
97
+ return {
98
+ ...tool,
99
+ inputSchema: {
100
+ ...schema,
101
+ properties: {
102
+ ...properties,
103
+ _diagnostic_telemetry: DIAGNOSTIC_TELEMETRY_SCHEMA,
104
+ },
105
+ },
106
+ };
107
+ });
108
+ }
109
+ const BASE_TOOLS = TOOLS;
110
+ const ACTIVE_TOOLS = DIAGNOSTIC_MODE_ENABLED
111
+ ? withDiagnosticTelemetrySchema(BASE_TOOLS)
112
+ : BASE_TOOLS;
61
113
  function renderToolsDoc() {
62
114
  const lines = [
63
115
  "# kibi-mcp Tools",
@@ -67,7 +119,7 @@ function renderToolsDoc() {
67
119
  "| Tool | Summary | Required Parameters |",
68
120
  "| --- | --- | --- |",
69
121
  ];
70
- for (const tool of TOOLS) {
122
+ for (const tool of ACTIVE_TOOLS) {
71
123
  const required = Array.isArray(tool.inputSchema?.required)
72
124
  ? tool.inputSchema.required.join(", ")
73
125
  : "none";
@@ -239,10 +291,70 @@ function getHelpText(topic) {
239
291
  let prologProcess = null;
240
292
  let isInitialized = false;
241
293
  let activeBranchName = "develop";
294
+ let ensurePrologTail = Promise.resolve();
242
295
  // Shutdown tracking state
243
296
  let isShuttingDown = false;
244
297
  let shutdownTimeout = null;
245
298
  const inFlightRequests = new Map();
299
+ let diagnosticUsageLogPath = null;
300
+ function extractToolCallPayload(args) {
301
+ const { _diagnostic_telemetry, ...businessArgs } = args;
302
+ const telemetry = _diagnostic_telemetry && typeof _diagnostic_telemetry === "object"
303
+ ? _diagnostic_telemetry
304
+ : null;
305
+ return { businessArgs, telemetry };
306
+ }
307
+ function appendUsageLogLine(entry) {
308
+ if (!DIAGNOSTIC_MODE_ENABLED || !diagnosticUsageLogPath) {
309
+ return;
310
+ }
311
+ const logDir = path.dirname(diagnosticUsageLogPath);
312
+ fs.mkdirSync(logDir, { recursive: true });
313
+ fs.appendFileSync(diagnosticUsageLogPath, `${JSON.stringify(entry)}\n`, {
314
+ encoding: "utf8",
315
+ });
316
+ }
317
+ function extractContradictionSignal(tool, args, result) {
318
+ if (tool !== "kb_upsert") {
319
+ return undefined;
320
+ }
321
+ const id = typeof args.id === "string" ? args.id : undefined;
322
+ if (!id) {
323
+ return undefined;
324
+ }
325
+ const structured = result && typeof result === "object"
326
+ ? result
327
+ .structuredContent
328
+ : undefined;
329
+ if (!structured || typeof structured !== "object") {
330
+ return undefined;
331
+ }
332
+ const rawCount = structured.contradiction_pairs_detected;
333
+ const count = typeof rawCount === "number" ? rawCount : Number(rawCount);
334
+ if (!Number.isFinite(count) || count < 0) {
335
+ return undefined;
336
+ }
337
+ return {
338
+ attempted_entity_id: id,
339
+ contradiction_pairs_detected: count,
340
+ };
341
+ }
342
+ async function probeContradictionsForReq(reqId) {
343
+ if (!prologProcess?.isRunning()) {
344
+ return { count: null, error: "prolog_process_not_running" };
345
+ }
346
+ const escaped = reqId.replace(/'/g, "\\'");
347
+ const goal = `aggregate_all(count, (contradicting_reqs(A, B, _), (A = '${escaped}' ; B = '${escaped}' ; A = 'file:///${escaped}' ; B = 'file:///${escaped}')), Count)`;
348
+ const result = await prologProcess.query(goal);
349
+ if (!result.success) {
350
+ return { count: null, error: result.error ?? "probe_query_failed" };
351
+ }
352
+ const count = Number(result.bindings.Count);
353
+ if (!Number.isFinite(count) || count < 0) {
354
+ return { count: null, error: "invalid_probe_count" };
355
+ }
356
+ return { count };
357
+ }
246
358
  function ensureBranchKbExists(workspaceRoot, branch) {
247
359
  if (!isValidBranchName(branch)) {
248
360
  throw new Error(`Invalid branch name: ${branch}`);
@@ -309,7 +421,7 @@ async function initiateGracefulShutdown(exitCode = 0) {
309
421
  // Exit
310
422
  process.exit(exitCode);
311
423
  }
312
- async function ensureProlog() {
424
+ async function ensurePrologUnsafe() {
313
425
  const workspaceRoot = resolveWorkspaceRoot();
314
426
  // Determine target branch: respect KIBI_BRANCH override or resolve from git
315
427
  const envBranch = process.env.KIBI_BRANCH?.trim();
@@ -410,6 +522,20 @@ async function ensureProlog() {
410
522
  debugLog(`[KIBI-MCP] KB attached: ${kbPath}`);
411
523
  return prologProcess;
412
524
  }
525
+ async function ensureProlog() {
526
+ const previous = ensurePrologTail;
527
+ let release;
528
+ ensurePrologTail = new Promise((resolve) => {
529
+ release = resolve;
530
+ });
531
+ await previous;
532
+ try {
533
+ return await ensurePrologUnsafe();
534
+ }
535
+ finally {
536
+ release();
537
+ }
538
+ }
413
539
  function jsonSchemaToZod(schema) {
414
540
  if (!schema || typeof schema !== "object") {
415
541
  return z.any();
@@ -512,32 +638,89 @@ function jsonSchemaToZod(schema) {
512
638
  }
513
639
  function addTool(server, name, description, inputSchema, handler) {
514
640
  const wrappedHandler = async (args) => {
641
+ let telemetry = null;
642
+ let businessArgs = {};
643
+ const startedAt = new Date();
515
644
  try {
516
645
  // Validate that args is a valid object
517
646
  if (typeof args !== "object" || args === null) {
518
647
  throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
519
648
  }
649
+ if (DIAGNOSTIC_MODE_ENABLED) {
650
+ const payload = extractToolCallPayload(args);
651
+ telemetry = payload.telemetry;
652
+ businessArgs = payload.businessArgs;
653
+ }
654
+ else {
655
+ businessArgs = args;
656
+ }
520
657
  // Check if shutting down before processing
521
658
  if (isShuttingDown) {
522
659
  throw new Error(`Tool ${name} rejected: server is shutting down`);
523
660
  }
524
661
  // Extract or generate requestId from args
525
- const requestIdArg = args._requestId;
662
+ const requestIdArg = businessArgs._requestId;
526
663
  const requestId = typeof requestIdArg === "string"
527
664
  ? requestIdArg
528
665
  : `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
529
666
  // Log tool call for debugging (to stderr to avoid breaking stdio protocol)
530
667
  if (process.env.KIBI_MCP_DEBUG) {
531
- console.error(`[KIBI-MCP] Tool called: ${name} (requestId: ${requestId}) with args:`, JSON.stringify(args));
668
+ console.error(`[KIBI-MCP] Tool called: ${name} (requestId: ${requestId}) with args:`, JSON.stringify(businessArgs));
532
669
  }
533
670
  // Track the handler promise in inFlightRequests Map
534
- const handlerPromise = handler(args);
671
+ const handlerPromise = handler(businessArgs);
535
672
  inFlightRequests.set(requestId, handlerPromise);
536
673
  try {
537
674
  // Execute handler
538
675
  const result = await handlerPromise;
676
+ const finishedAt = new Date();
677
+ const contradictionSignal = extractContradictionSignal(name, businessArgs, result);
678
+ let contradictionSignalFinal = contradictionSignal;
679
+ if (name === "kb_upsert" && typeof businessArgs.id === "string") {
680
+ const probe = businessArgs.type === "req"
681
+ ? await probeContradictionsForReq(businessArgs.id)
682
+ : { count: null, error: "non_req_entity" };
683
+ contradictionSignalFinal = {
684
+ attempted_entity_id: businessArgs.id,
685
+ contradiction_pairs_detected: probe.count !== null ? probe.count : -1,
686
+ probe_error: probe.error,
687
+ };
688
+ }
689
+ appendUsageLogLine({
690
+ timestamp: finishedAt.toISOString(),
691
+ request_id: requestId,
692
+ tool: name,
693
+ telemetry,
694
+ business_args: businessArgs,
695
+ status: "success",
696
+ started_at: startedAt.toISOString(),
697
+ finished_at: finishedAt.toISOString(),
698
+ duration_ms: finishedAt.getTime() - startedAt.getTime(),
699
+ prolog_pid: prologProcess?.getPid() ?? null,
700
+ active_branch: activeBranchName,
701
+ contradiction_signal: contradictionSignalFinal,
702
+ });
539
703
  return result;
540
704
  }
705
+ catch (error) {
706
+ const finishedAt = new Date();
707
+ const err = error instanceof Error ? error : new Error(String(error));
708
+ appendUsageLogLine({
709
+ timestamp: finishedAt.toISOString(),
710
+ request_id: requestId,
711
+ tool: name,
712
+ telemetry,
713
+ business_args: businessArgs,
714
+ status: "error",
715
+ started_at: startedAt.toISOString(),
716
+ finished_at: finishedAt.toISOString(),
717
+ duration_ms: finishedAt.getTime() - startedAt.getTime(),
718
+ prolog_pid: prologProcess?.getPid() ?? null,
719
+ active_branch: activeBranchName,
720
+ error_message: err.message,
721
+ });
722
+ throw error;
723
+ }
541
724
  finally {
542
725
  // Always clean up from Map when done (success or failure)
543
726
  inFlightRequests.delete(requestId);
@@ -556,8 +739,12 @@ function addTool(server, name, description, inputSchema, handler) {
556
739
  }
557
740
  export async function startServer() {
558
741
  loadDefaultEnvFile();
559
- const server = new McpServer({ name: "kibi-mcp", version: "0.2.0" });
560
- attachMcpcat(server);
742
+ if (DIAGNOSTIC_MODE_ENABLED) {
743
+ const workspaceRoot = resolveWorkspaceRoot();
744
+ diagnosticUsageLogPath = path.join(workspaceRoot, ".kb", "usage.log");
745
+ process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
746
+ }
747
+ const server = new McpServer({ name: "kibi-mcp", version: "0.2.1" });
561
748
  for (const prompt of PROMPTS) {
562
749
  server.prompt(prompt.name, prompt.description, async () => ({
563
750
  messages: [
@@ -580,7 +767,7 @@ export async function startServer() {
580
767
  }));
581
768
  }
582
769
  const toolDef = (name) => {
583
- const t = TOOLS.find((t) => t.name === name);
770
+ const t = ACTIVE_TOOLS.find((t) => t.name === name);
584
771
  if (!t)
585
772
  throw new Error(`Unknown tool: ${name}`);
586
773
  return t;
@@ -44,9 +44,9 @@
44
44
  */
45
45
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
46
46
  import path from "node:path";
47
- import { resolveWorkspaceRoot } from "../workspace.js";
48
- import { enrichSymbolCoordinates, } from "kibi-cli/extractors/symbols-coordinator";
49
47
  import { dump as dumpYAML, load as parseYAML } from "js-yaml";
48
+ import { enrichSymbolCoordinates, } from "kibi-cli/extractors/symbols-coordinator";
49
+ import { resolveWorkspaceRoot } from "../workspace.js";
50
50
  const COMMENT_BLOCK = `# symbols.yaml
51
51
  # AUTHORED fields (edit freely):
52
52
  # id, title, sourceFile, links, status, tags, owner, priority
@@ -177,11 +177,18 @@ export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = re
177
177
  }
178
178
  return { refreshed, found: true };
179
179
  }
180
- function resolveManifestPath(workspaceRoot) {
180
+ export function resolveManifestPath(workspaceRoot) {
181
181
  const configPath = path.join(workspaceRoot, ".kb", "config.json");
182
182
  if (existsSync(configPath)) {
183
183
  try {
184
184
  const config = JSON.parse(readFileSync(configPath, "utf8"));
185
+ // Prefer paths.symbols (new standard) over symbolsManifest (legacy)
186
+ if (config.paths?.symbols) {
187
+ return path.isAbsolute(config.paths.symbols)
188
+ ? config.paths.symbols
189
+ : path.resolve(workspaceRoot, config.paths.symbols);
190
+ }
191
+ // Backward compatibility: check legacy symbolsManifest field
185
192
  if (config.symbolsManifest) {
186
193
  return path.isAbsolute(config.symbolsManifest)
187
194
  ? config.symbolsManifest
@@ -118,6 +118,10 @@ export async function handleKbUpsert(prolog, args) {
118
118
  }
119
119
  // Save KB to disk
120
120
  await prolog.query("kb_save");
121
+ let contradictionPairsDetected;
122
+ if (type === "req") {
123
+ contradictionPairsDetected = await detectContradictionPairs(prolog, id);
124
+ }
121
125
  if (type === "symbol") {
122
126
  try {
123
127
  await refreshCoordinatesForSymbolId(id);
@@ -133,13 +137,16 @@ export async function handleKbUpsert(prolog, args) {
133
137
  content: [
134
138
  {
135
139
  type: "text",
136
- text: `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s).`,
140
+ text: contradictionPairsDetected && contradictionPairsDetected > 0
141
+ ? `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s). Contradiction probe detected ${contradictionPairsDetected} potential conflict pair(s).`
142
+ : `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s).`,
137
143
  },
138
144
  ],
139
145
  structuredContent: {
140
146
  created,
141
147
  updated,
142
148
  relationships_created: relationshipsCreated,
149
+ contradiction_pairs_detected: contradictionPairsDetected,
143
150
  },
144
151
  };
145
152
  }
@@ -148,6 +155,17 @@ export async function handleKbUpsert(prolog, args) {
148
155
  throw new Error(`Upsert execution failed: ${message}`);
149
156
  }
150
157
  }
158
+ async function detectContradictionPairs(prolog, reqId) {
159
+ const escaped = escapeAtom(reqId);
160
+ const goal = `aggregate_all(count, (contradicting_reqs(A, B, _), (A = '${escaped}' ; B = '${escaped}' ; A = 'file:///${escaped}' ; B = 'file:///${escaped}')), Count)`;
161
+ const result = await prolog.query(goal);
162
+ if (!result.success) {
163
+ return 0;
164
+ }
165
+ const raw = result.bindings.Count;
166
+ const count = Number(raw);
167
+ return Number.isFinite(count) ? count : 0;
168
+ }
151
169
  /**
152
170
  * Build Prolog property list from entity object
153
171
  * Returns simple Key=Value format without typed literals
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.26.0",
6
6
  "ajv": "^8.18.0",
@@ -9,8 +9,8 @@
9
9
  "fast-glob": "^3.2.12",
10
10
  "gray-matter": "^4.0.3",
11
11
  "js-yaml": "^4.1.0",
12
- "kibi-cli": "^0.2.0",
13
- "kibi-core": "^0.1.6",
12
+ "kibi-cli": "^0.2.3",
13
+ "kibi-core": "^0.1.7",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
16
16
  "zod": "^4.3.6"
@@ -27,7 +27,10 @@
27
27
  "build": "tsc -p tsconfig.json",
28
28
  "prepack": "npm run build"
29
29
  },
30
- "files": ["dist", "bin"],
30
+ "files": [
31
+ "dist",
32
+ "bin"
33
+ ],
31
34
  "engines": {
32
35
  "node": ">=18",
33
36
  "bun": ">=1.0"
package/dist/mcpcat.js DELETED
@@ -1,129 +0,0 @@
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
- }