@juspay/yama 2.2.0 → 2.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.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Local diff source for SDK mode.
3
+ * Extracts git diffs from a local repository without requiring MCP tools.
4
+ */
5
+ import { existsSync } from "fs";
6
+ import { resolve } from "path";
7
+ import { spawnSync } from "child_process";
8
+ export class LocalDiffSource {
9
+ getDiffContext(request) {
10
+ const repoPath = resolve(request.repoPath || process.cwd());
11
+ if (!existsSync(repoPath)) {
12
+ throw new Error(`Repository path does not exist: ${repoPath}`);
13
+ }
14
+ this.ensureGitRepo(repoPath);
15
+ const diffSource = request.diffSource || "uncommitted";
16
+ const includePaths = request.includePaths || [];
17
+ const maxDiffChars = request.maxDiffChars || 120_000;
18
+ const contextLines = 3;
19
+ const diffArgs = this.buildDiffArgs(diffSource, request.baseRef, request.headRef, contextLines, includePaths);
20
+ const nameOnlyArgs = this.buildNameOnlyArgs(diffSource, request.baseRef, request.headRef, includePaths);
21
+ const numStatArgs = this.buildNumStatArgs(diffSource, request.baseRef, request.headRef, includePaths);
22
+ const diffOutput = this.runGit(repoPath, diffArgs);
23
+ const changedFilesOutput = this.runGit(repoPath, nameOnlyArgs);
24
+ const numStatOutput = this.runGit(repoPath, numStatArgs);
25
+ const changedFiles = changedFilesOutput
26
+ .split("\n")
27
+ .map((line) => line.trim())
28
+ .filter(Boolean);
29
+ const { additions, deletions } = this.parseNumStat(numStatOutput);
30
+ const truncated = diffOutput.length > maxDiffChars;
31
+ const diff = truncated ? diffOutput.slice(0, maxDiffChars) : diffOutput;
32
+ return {
33
+ repoPath,
34
+ diffSource,
35
+ baseRef: diffSource === "range" ? request.baseRef : undefined,
36
+ headRef: diffSource === "range" ? request.headRef || "HEAD" : undefined,
37
+ changedFiles,
38
+ additions,
39
+ deletions,
40
+ diff,
41
+ truncated,
42
+ };
43
+ }
44
+ ensureGitRepo(repoPath) {
45
+ const probe = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
46
+ cwd: repoPath,
47
+ encoding: "utf-8",
48
+ });
49
+ if (probe.status !== 0 || probe.stdout.trim() !== "true") {
50
+ throw new Error(`Path is not a git repository: ${repoPath}`);
51
+ }
52
+ }
53
+ runGit(repoPath, args) {
54
+ const result = spawnSync("git", args, {
55
+ cwd: repoPath,
56
+ encoding: "utf-8",
57
+ maxBuffer: 10 * 1024 * 1024,
58
+ });
59
+ if (result.status !== 0) {
60
+ throw new Error(`git ${args.join(" ")} failed: ${(result.stderr || result.stdout || "").trim()}`);
61
+ }
62
+ return result.stdout || "";
63
+ }
64
+ buildDiffArgs(diffSource, baseRef, headRef, contextLines, includePaths) {
65
+ const args = ["diff", "--no-color", `--unified=${contextLines}`];
66
+ if (diffSource === "staged") {
67
+ args.push("--staged");
68
+ }
69
+ else if (diffSource === "range") {
70
+ if (!baseRef) {
71
+ throw new Error("baseRef is required when diffSource is 'range'");
72
+ }
73
+ args.push(`${baseRef}..${headRef || "HEAD"}`);
74
+ }
75
+ if (includePaths.length > 0) {
76
+ args.push("--", ...includePaths);
77
+ }
78
+ return args;
79
+ }
80
+ buildNameOnlyArgs(diffSource, baseRef, headRef, includePaths) {
81
+ const args = ["diff", "--name-only"];
82
+ if (diffSource === "staged") {
83
+ args.push("--staged");
84
+ }
85
+ else if (diffSource === "range") {
86
+ if (!baseRef) {
87
+ throw new Error("baseRef is required when diffSource is 'range'");
88
+ }
89
+ args.push(`${baseRef}..${headRef || "HEAD"}`);
90
+ }
91
+ if (includePaths.length > 0) {
92
+ args.push("--", ...includePaths);
93
+ }
94
+ return args;
95
+ }
96
+ buildNumStatArgs(diffSource, baseRef, headRef, includePaths) {
97
+ const args = ["diff", "--numstat"];
98
+ if (diffSource === "staged") {
99
+ args.push("--staged");
100
+ }
101
+ else if (diffSource === "range") {
102
+ if (!baseRef) {
103
+ throw new Error("baseRef is required when diffSource is 'range'");
104
+ }
105
+ args.push(`${baseRef}..${headRef || "HEAD"}`);
106
+ }
107
+ if (includePaths.length > 0) {
108
+ args.push("--", ...includePaths);
109
+ }
110
+ return args;
111
+ }
112
+ parseNumStat(numStat) {
113
+ let additions = 0;
114
+ let deletions = 0;
115
+ const lines = numStat
116
+ .split("\n")
117
+ .map((line) => line.trim())
118
+ .filter(Boolean);
119
+ for (const line of lines) {
120
+ const [addRaw, delRaw] = line.split("\t");
121
+ const addCount = addRaw === "-" ? 0 : parseInt(addRaw, 10);
122
+ const delCount = delRaw === "-" ? 0 : parseInt(delRaw, 10);
123
+ additions += Number.isNaN(addCount) ? 0 : addCount;
124
+ deletions += Number.isNaN(delCount) ? 0 : delCount;
125
+ }
126
+ return { additions, deletions };
127
+ }
128
+ }
129
+ //# sourceMappingURL=LocalDiffSource.js.map
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MCP Server Manager for Yama V2
3
- * Manages lifecycle and health of Bitbucket and Jira MCP servers
3
+ * Manages lifecycle and health of PR/local MCP servers
4
4
  */
5
5
  import { MCPServersConfig } from "../types/config.types.js";
6
6
  export declare class MCPServerManager {
@@ -10,6 +10,14 @@ export declare class MCPServerManager {
10
10
  * Bitbucket is always enabled, Jira is optional based on config
11
11
  */
12
12
  setupMCPServers(neurolink: any, config: MCPServersConfig): Promise<void>;
13
+ /**
14
+ * Setup local git MCP server for SDK/local mode.
15
+ * Mandatory in local mode.
16
+ */
17
+ setupLocalGitMCPServer(neurolink: any): Promise<void>;
18
+ private buildLocalGitServerConfig;
19
+ private getLocalGitToolNames;
20
+ private isMutatingGitTool;
13
21
  /**
14
22
  * Setup Bitbucket MCP server (hardcoded, always enabled)
15
23
  */
@@ -18,5 +26,9 @@ export declare class MCPServerManager {
18
26
  * Setup Jira MCP server (hardcoded, optionally enabled)
19
27
  */
20
28
  private setupJiraMCP;
29
+ /**
30
+ * MCP preflight diagnostics after registration.
31
+ */
32
+ private logDiagnostics;
21
33
  }
22
34
  //# sourceMappingURL=MCPServerManager.d.ts.map
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * MCP Server Manager for Yama V2
3
- * Manages lifecycle and health of Bitbucket and Jira MCP servers
3
+ * Manages lifecycle and health of PR/local MCP servers
4
4
  */
5
+ import { join } from "path";
5
6
  import { MCPServerError } from "../types/v2.types.js";
6
7
  export class MCPServerManager {
7
8
  // MCP servers are managed entirely by NeuroLink
@@ -12,6 +13,9 @@ export class MCPServerManager {
12
13
  * Bitbucket is always enabled, Jira is optional based on config
13
14
  */
14
15
  async setupMCPServers(neurolink, config) {
16
+ if (this.initialized) {
17
+ return;
18
+ }
15
19
  console.log("🔌 Setting up MCP servers...");
16
20
  // Setup Bitbucket MCP (always enabled)
17
21
  await this.setupBitbucketMCP(neurolink, config.bitbucket?.blockedTools);
@@ -23,8 +27,79 @@ export class MCPServerManager {
23
27
  console.log(" ⏭️ Jira MCP disabled in config");
24
28
  }
25
29
  this.initialized = true;
30
+ await this.logDiagnostics(neurolink);
26
31
  console.log("✅ MCP servers configured\n");
27
32
  }
33
+ /**
34
+ * Setup local git MCP server for SDK/local mode.
35
+ * Mandatory in local mode.
36
+ */
37
+ async setupLocalGitMCPServer(neurolink) {
38
+ try {
39
+ console.log("🔌 Setting up local Git MCP server...");
40
+ await neurolink.addExternalMCPServer("local-git", this.buildLocalGitServerConfig([]));
41
+ const discoveredToolNames = this.getLocalGitToolNames(neurolink);
42
+ // Fail closed: if tool discovery returns nothing we cannot verify read-only
43
+ // safety, so refuse to proceed rather than silently exposing write operations.
44
+ if (discoveredToolNames.length === 0) {
45
+ await neurolink
46
+ .removeExternalMCPServer("local-git")
47
+ .catch(() => undefined);
48
+ throw new MCPServerError("local-git MCP server returned no tools — cannot verify read-only filtering. Aborting local mode setup.");
49
+ }
50
+ const mutatingTools = discoveredToolNames.filter((name) => this.isMutatingGitTool(name));
51
+ // Enforce hard safety at MCP layer: mutating tools are removed from registry.
52
+ if (mutatingTools.length > 0) {
53
+ await neurolink.removeExternalMCPServer("local-git");
54
+ await neurolink.addExternalMCPServer("local-git", this.buildLocalGitServerConfig(mutatingTools));
55
+ // Verify the re-registration actually removed all mutating tools.
56
+ const remainingMutating = this.getLocalGitToolNames(neurolink).filter((name) => this.isMutatingGitTool(name));
57
+ if (remainingMutating.length > 0) {
58
+ await neurolink
59
+ .removeExternalMCPServer("local-git")
60
+ .catch(() => undefined);
61
+ throw new MCPServerError(`Read-only enforcement failed — mutating tools still present after blocking: ${remainingMutating.join(", ")}`);
62
+ }
63
+ }
64
+ console.log(" ✅ Local Git MCP server registered");
65
+ console.log(" 🔒 Local mode enforces regex-derived read-only blocking at MCP layer");
66
+ if (mutatingTools.length > 0) {
67
+ console.log(` 🚫 Blocked local-git tools: ${mutatingTools.join(", ")}`);
68
+ }
69
+ try {
70
+ const toolNames = this.getLocalGitToolNames(neurolink);
71
+ if (toolNames.length > 0) {
72
+ console.log(` 🧰 local-git tools (available): ${toolNames.join(", ")}`);
73
+ }
74
+ }
75
+ catch {
76
+ // Optional introspection only
77
+ }
78
+ await this.logDiagnostics(neurolink);
79
+ }
80
+ catch (error) {
81
+ throw new MCPServerError(`Failed to setup local Git MCP server: ${error.message}`);
82
+ }
83
+ }
84
+ buildLocalGitServerConfig(blockedTools) {
85
+ return {
86
+ // Launch via package script: tries uvx first, falls back to npx package.
87
+ command: "npm",
88
+ args: ["run", "-s", "mcp:git:server"],
89
+ transport: "stdio",
90
+ blockedTools,
91
+ };
92
+ }
93
+ getLocalGitToolNames(neurolink) {
94
+ return (neurolink.getExternalMCPServerTools?.("local-git") || [])
95
+ .map((tool) => tool?.name)
96
+ .filter((name) => typeof name === "string");
97
+ }
98
+ isMutatingGitTool(toolName) {
99
+ // Handles plain names (git_commit) and prefixed names (local-git.git_commit).
100
+ const normalized = toolName.split(/[.:/]/).pop() || toolName;
101
+ return /^git_(commit|push|add|checkout|create_branch|merge|rebase|cherry_pick|reset|revert|tag|rm|clean|stash|apply)\b/i.test(normalized);
102
+ }
28
103
  /**
29
104
  * Setup Bitbucket MCP server (hardcoded, always enabled)
30
105
  */
@@ -37,10 +112,13 @@ export class MCPServerManager {
37
112
  !process.env.BITBUCKET_BASE_URL) {
38
113
  throw new MCPServerError("Missing required environment variables: BITBUCKET_USERNAME, BITBUCKET_TOKEN, or BITBUCKET_BASE_URL");
39
114
  }
40
- // Hardcoded Bitbucket MCP configuration
115
+ // Use the locally installed binary instead of `npx @latest`, which forces
116
+ // a registry check + possible download in CI and causes the 30s circuit-breaker
117
+ // to fire intermittently.
118
+ const bitbucketBin = join(process.cwd(), "node_modules/.bin/bitbucket-mcp-server");
41
119
  await neurolink.addExternalMCPServer("bitbucket", {
42
- command: "npx",
43
- args: ["-y", "@nexus2520/bitbucket-mcp-server"],
120
+ command: bitbucketBin,
121
+ args: [],
44
122
  transport: "stdio",
45
123
  env: {
46
124
  BITBUCKET_USERNAME: process.env.BITBUCKET_USERNAME,
@@ -49,6 +127,15 @@ export class MCPServerManager {
49
127
  },
50
128
  blockedTools: blockedTools || [],
51
129
  });
130
+ // Verify the server actually connected — NeuroLink resolves the promise
131
+ // even on timeout, so we must check the status explicitly.
132
+ const servers = await neurolink.listMCPServers();
133
+ const bbServer = (servers || []).find((s) => s.name === "bitbucket");
134
+ if (!bbServer ||
135
+ bbServer.status !== "connected" ||
136
+ bbServer.tools?.length === 0) {
137
+ throw new MCPServerError(`Bitbucket MCP server registered but not connected (status: ${bbServer?.status ?? "unknown"}, tools: ${bbServer?.tools?.length ?? 0}). Possible startup timeout.`);
138
+ }
52
139
  console.log(" ✅ Bitbucket MCP server registered and tools available");
53
140
  if (blockedTools && blockedTools.length > 0) {
54
141
  console.log(` 🚫 Blocked tools: ${blockedTools.join(", ")}`);
@@ -73,10 +160,11 @@ export class MCPServerManager {
73
160
  console.warn(" Skipping Jira integration...");
74
161
  return;
75
162
  }
76
- // Hardcoded Jira MCP configuration
163
+ // Use the locally installed binary (same reason as Bitbucket above).
164
+ const jiraBin = join(process.cwd(), "node_modules/.bin/jira-mcp-server");
77
165
  await neurolink.addExternalMCPServer("jira", {
78
- command: "npx",
79
- args: ["-y", "@nexus2520/jira-mcp-server"],
166
+ command: jiraBin,
167
+ args: [],
80
168
  transport: "stdio",
81
169
  env: {
82
170
  JIRA_EMAIL: process.env.JIRA_EMAIL,
@@ -96,5 +184,21 @@ export class MCPServerManager {
96
184
  console.warn(" Continuing without Jira integration...");
97
185
  }
98
186
  }
187
+ /**
188
+ * MCP preflight diagnostics after registration.
189
+ */
190
+ async logDiagnostics(neurolink) {
191
+ try {
192
+ const status = await neurolink.getMCPStatus();
193
+ const servers = await neurolink.listMCPServers();
194
+ console.log(" 📊 MCP diagnostics:");
195
+ console.log(` Servers: ${status.totalServers}`);
196
+ console.log(` Tools: ${status.totalTools}`);
197
+ console.log(` Connected: ${(servers || []).filter((server) => server?.status === "connected").length}`);
198
+ }
199
+ catch (error) {
200
+ console.warn(` ⚠️ MCP diagnostics unavailable: ${error.message}`);
201
+ }
202
+ }
99
203
  }
100
204
  //# sourceMappingURL=MCPServerManager.js.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Session Manager for Yama V2
2
+ * Session Manager for Yama
3
3
  * Tracks review sessions, tool calls, and maintains state
4
4
  */
5
5
  import { ReviewSession, ReviewRequest, ReviewResult, SessionMetadata } from "../types/v2.types.js";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Session Manager for Yama V2
2
+ * Session Manager for Yama
3
3
  * Tracks review sessions, tool calls, and maintains state
4
4
  */
5
5
  import { randomBytes } from "crypto";
@@ -18,7 +18,7 @@ export class SessionManager {
18
18
  status: "running",
19
19
  toolCalls: [],
20
20
  metadata: {
21
- yamaVersion: "2.0.0",
21
+ yamaVersion: "2.2.1",
22
22
  aiProvider: "auto",
23
23
  aiModel: "unknown",
24
24
  totalTokens: 0,
@@ -122,7 +122,7 @@ export class SessionManager {
122
122
  generateSessionId() {
123
123
  const timestamp = Date.now().toString(36);
124
124
  const random = randomBytes(4).toString("hex");
125
- return `yama-v2-${timestamp}-${random}`;
125
+ return `yama-${timestamp}-${random}`;
126
126
  }
127
127
  /**
128
128
  * Clean up old sessions (keep most recent 100)
@@ -1,25 +1,38 @@
1
1
  /**
2
- * Yama V2 Orchestrator
2
+ * Yama Orchestrator
3
3
  * Main entry point for AI-native autonomous code review
4
4
  */
5
- import { ReviewRequest, ReviewResult, ReviewUpdate } from "../types/v2.types.js";
6
- export declare class YamaV2Orchestrator {
5
+ import { LocalReviewRequest, LocalReviewResult, ReviewRequest, ReviewResult, ReviewMode, ReviewUpdate, UnifiedReviewRequest } from "../types/v2.types.js";
6
+ import { YamaInitOptions } from "../types/config.types.js";
7
+ export declare class YamaOrchestrator {
7
8
  private neurolink;
8
9
  private mcpManager;
9
10
  private configLoader;
10
11
  private promptBuilder;
11
12
  private sessionManager;
13
+ private localDiffSource;
12
14
  private config;
13
15
  private initialized;
14
- constructor();
16
+ private mcpInitialized;
17
+ private localGitMcpInitialized;
18
+ private initOptions;
19
+ constructor(options?: YamaInitOptions);
15
20
  /**
16
- * Initialize Yama V2 with configuration and MCP servers
21
+ * Initialize Yama with configuration and MCP servers
17
22
  */
18
- initialize(configPath?: string): Promise<void>;
23
+ initialize(configPath?: string, mode?: ReviewMode): Promise<void>;
19
24
  /**
20
25
  * Start autonomous AI review
21
26
  */
22
27
  startReview(request: ReviewRequest): Promise<ReviewResult>;
28
+ /**
29
+ * Unified review entry for SDK consumers.
30
+ */
31
+ review(request: UnifiedReviewRequest): Promise<ReviewResult | LocalReviewResult>;
32
+ /**
33
+ * Local SDK mode review from git diff (no PR/MCP dependency).
34
+ */
35
+ reviewLocalDiff(request: LocalReviewRequest): Promise<LocalReviewResult>;
23
36
  /**
24
37
  * Stream review with real-time updates (for verbose mode)
25
38
  */
@@ -59,6 +72,18 @@ export declare class YamaV2Orchestrator {
59
72
  * Parse AI response into structured review result
60
73
  */
61
74
  private parseReviewResult;
75
+ private recordToolCallsFromResponse;
76
+ /**
77
+ * Parse local SDK mode response into strict LocalReviewResult.
78
+ */
79
+ private parseLocalReviewResult;
80
+ private sanitizeLocalSummary;
81
+ private removeDelimitedSections;
82
+ private extractJsonPayload;
83
+ private normalizeFindings;
84
+ private normalizeSeverity;
85
+ private countFindingsBySeverity;
86
+ private normalizeDecision;
62
87
  /**
63
88
  * Extract decision from AI response
64
89
  */
@@ -79,10 +104,23 @@ export declare class YamaV2Orchestrator {
79
104
  * Calculate cost estimate from token usage
80
105
  */
81
106
  private calculateCost;
107
+ private toSafeNumber;
82
108
  /**
83
109
  * Generate userId for NeuroLink context from repository and branch/PR
84
110
  */
85
111
  private generateUserId;
112
+ private isLocalReviewRequest;
113
+ /**
114
+ * Query-level tool filtering for PR mode.
115
+ * Conservative strategy: only exclude Jira tools when there is no Jira signal.
116
+ */
117
+ private getPRToolFilteringOptions;
118
+ /**
119
+ * Query-level read-only filtering for local-git MCP tools.
120
+ * Uses regex-based mutation detection instead of hardcoded tool-name lists.
121
+ */
122
+ private getLocalToolFilteringOptions;
123
+ private normalizeToolName;
86
124
  /**
87
125
  * Initialize NeuroLink with observability configuration
88
126
  */
@@ -92,7 +130,7 @@ export declare class YamaV2Orchestrator {
92
130
  */
93
131
  private ensureInitialized;
94
132
  /**
95
- * Show Yama V2 banner
133
+ * Show Yama banner
96
134
  */
97
135
  private showBanner;
98
136
  /**
@@ -108,5 +146,7 @@ export declare class YamaV2Orchestrator {
108
146
  */
109
147
  private formatDecision;
110
148
  }
111
- export declare function createYamaV2(): YamaV2Orchestrator;
149
+ export declare function createYamaV2(options?: YamaInitOptions): YamaOrchestrator;
150
+ export declare function createYama(options?: YamaInitOptions): YamaOrchestrator;
151
+ export { YamaOrchestrator as YamaV2Orchestrator };
112
152
  //# sourceMappingURL=YamaV2Orchestrator.d.ts.map