gitnexus 1.6.12-rc.36 → 1.6.12-rc.38
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/mcp/local/local-backend.js +8 -6
- package/dist/mcp/server.js +7 -0
- package/dist/mcp/tool-arguments.d.ts +47 -0
- package/dist/mcp/tool-arguments.js +147 -0
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.js +22 -0
- package/dist/storage/git.js +12 -3
- package/package.json +1 -1
|
@@ -55,6 +55,7 @@ import { stalenessPayload, } from '../../core/staleness-status.js';
|
|
|
55
55
|
import { logger } from '../../core/logger.js';
|
|
56
56
|
import { isLocalEmbeddingRuntimeBlockerMessage, isMissingLocalEmbeddingStackMessage, } from '../../core/embeddings/runtime-support.js';
|
|
57
57
|
import { LIST_REPOS_DEFAULT_LIMIT, LIST_REPOS_MAX_LIMIT, EXPLAIN_DEFAULT_LIMIT, EXPLAIN_MAX_LIMIT, PDG_QUERY_DEFAULT_LIMIT, PDG_QUERY_MAX_LIMIT, } from '../tools.js';
|
|
58
|
+
import { foldNumericToolArgumentAliases } from '../tool-arguments.js';
|
|
58
59
|
import { findImportCycles, IMPORT_CYCLE_LIMIT } from '../../core/graph/import-cycles.js';
|
|
59
60
|
import { decodeTaintPath } from '../../core/ingestion/taint/path-codec.js';
|
|
60
61
|
import { decodeReachingDefReason } from '../../core/ingestion/cfg/reaching-def-reason-codec.js';
|
|
@@ -154,10 +155,8 @@ const TOOL_STRING_ALIASES = {
|
|
|
154
155
|
function normalizeToolParams(method, params) {
|
|
155
156
|
const input = params && typeof params === 'object' ? params : {};
|
|
156
157
|
const definitions = TOOL_STRING_ALIASES[method];
|
|
157
|
-
if (!definitions)
|
|
158
|
-
return { params: input };
|
|
159
158
|
const normalized = { ...input };
|
|
160
|
-
for (const { canonical, aliases } of definitions) {
|
|
159
|
+
for (const { canonical, aliases } of definitions ?? []) {
|
|
161
160
|
const keys = [canonical, ...aliases];
|
|
162
161
|
const supplied = [];
|
|
163
162
|
for (const key of keys) {
|
|
@@ -191,12 +190,15 @@ function normalizeToolParams(method, params) {
|
|
|
191
190
|
if (supplied.length > 0)
|
|
192
191
|
normalized[canonical] = supplied[0].value;
|
|
193
192
|
}
|
|
193
|
+
const folded = foldNumericToolArgumentAliases(method, normalized);
|
|
194
|
+
if ('error' in folded)
|
|
195
|
+
return folded;
|
|
194
196
|
if (method === 'impact' &&
|
|
195
|
-
typeof
|
|
196
|
-
(typeof
|
|
197
|
+
typeof folded.params.target !== 'string' &&
|
|
198
|
+
(typeof folded.params.target_uid !== 'string' || !folded.params.target_uid.trim())) {
|
|
197
199
|
return { error: 'MCP impact requires target, name, symbol, or target_uid.' };
|
|
198
200
|
}
|
|
199
|
-
return { params:
|
|
201
|
+
return { params: folded.params };
|
|
200
202
|
}
|
|
201
203
|
// AI context generation is CLI-only (gitnexus analyze)
|
|
202
204
|
// import { generateAIContextFiles } from '../../cli/ai-context.js';
|
package/dist/mcp/server.js
CHANGED
|
@@ -20,6 +20,7 @@ import { getResourceDefinitions, getResourceTemplates, readResource } from './re
|
|
|
20
20
|
import { assertMcpReadOnlyResource, assertMcpReadOnlyToolCall, filterMcpReadOnlyResourceContent, MCP_READ_ONLY_TOOLS, readOnlyResourceTemplateAllowed, resolveMcpReadOnlyMode, toolForReadOnlyMcp, } from './read-only-policy.js';
|
|
21
21
|
import { createMcpRepositoryPolicy, McpRepositoryPolicy, mcpRepositoryPolicyConfigured, } from './repository-policy.js';
|
|
22
22
|
import { applyMcpMaxTokens, resolveMcpMaxTokens, withoutMcpBudgetArg } from './output-budget.js';
|
|
23
|
+
import { assertKnownMcpToolArguments, schemaSourceToolName } from './tool-arguments.js';
|
|
23
24
|
/**
|
|
24
25
|
* Next-step hints appended to tool responses.
|
|
25
26
|
*
|
|
@@ -164,6 +165,12 @@ export function createMCPServer(backend, options = {}) {
|
|
|
164
165
|
try {
|
|
165
166
|
const typedArgs = args;
|
|
166
167
|
assertMcpReadOnlyToolCall(name, typedArgs, readOnly);
|
|
168
|
+
const schemaSource = schemaSourceToolName(name);
|
|
169
|
+
const advertisedTool = GITNEXUS_TOOLS.find((tool) => tool.name === schemaSource);
|
|
170
|
+
if (advertisedTool) {
|
|
171
|
+
const listed = toolForReadOnlyMcp(repositoryPolicy.toolForMcp(advertisedTool), readOnly);
|
|
172
|
+
assertKnownMcpToolArguments(name, typedArgs, listed.inputSchema.properties);
|
|
173
|
+
}
|
|
167
174
|
maxTokens = resolveMcpMaxTokens(name, typedArgs);
|
|
168
175
|
const result = await scopedBackend.callTool(name, withoutMcpBudgetArg(typedArgs));
|
|
169
176
|
const resultText = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool-argument contract (#3261).
|
|
3
|
+
*
|
|
4
|
+
* `tools/list` advertises `inputSchema`; `tools/call` used to forward any JSON
|
|
5
|
+
* object. A misspelled or CLI-taught key (`depth` instead of `maxDepth`) then
|
|
6
|
+
* produced a well-formed answer computed from the server default — no error,
|
|
7
|
+
* no warning. This module is the single dispatch-time check that the keys a
|
|
8
|
+
* caller sent are ones the advertised schema (or an unpublished handler alias)
|
|
9
|
+
* actually reads.
|
|
10
|
+
*/
|
|
11
|
+
/** Legacy MCP names that reuse another tool's advertised schema. */
|
|
12
|
+
export declare const LEGACY_TOOL_SCHEMA_SOURCE: Readonly<Record<string, string>>;
|
|
13
|
+
/**
|
|
14
|
+
* Keys the handler still reads but that must NOT appear in `inputSchema`
|
|
15
|
+
* (#2175: advertising `query` makes Claude Code drop the argument).
|
|
16
|
+
*/
|
|
17
|
+
export declare const UNPUBLISHED_TOOL_ARGUMENT_ALIASES: Readonly<Record<string, readonly string[]>>;
|
|
18
|
+
export interface NumericArgumentAlias {
|
|
19
|
+
canonical: string;
|
|
20
|
+
aliases: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Numeric aliases that the backend folds onto the advertised canonical key.
|
|
24
|
+
* `depth` is the CLI flag name for `maxDepth` on impact and trace.
|
|
25
|
+
*/
|
|
26
|
+
export declare const TOOL_NUMERIC_ARGUMENT_ALIASES: Readonly<Record<string, readonly NumericArgumentAlias[]>>;
|
|
27
|
+
export declare function schemaSourceToolName(toolName: string): string;
|
|
28
|
+
export declare function advertisedToolPropertyNames(toolName: string): string[] | undefined;
|
|
29
|
+
export declare function suggestKnownToolArgument(unknownKey: string, knownKeys: readonly string[]): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Reject top-level tool arguments that are neither advertised nor an
|
|
32
|
+
* unpublished handler alias. `advertisedProperties` should be the schema the
|
|
33
|
+
* caller actually saw (`tools/list` after read-only / repository-policy
|
|
34
|
+
* scrubbing). When it is omitted, the canonical `GITNEXUS_TOOLS` schema is
|
|
35
|
+
* used. Tools with no schema (legacy `overview`) are left unchecked.
|
|
36
|
+
*/
|
|
37
|
+
export declare function assertKnownMcpToolArguments(toolName: string, args: Record<string, unknown> | undefined, advertisedProperties?: Record<string, unknown>): void;
|
|
38
|
+
/**
|
|
39
|
+
* Fold numeric aliases onto their canonical key (e.g. `depth` → `maxDepth`).
|
|
40
|
+
* Conflicting values error; a single agreed value is written to the canonical
|
|
41
|
+
* key and the alias keys are removed so every downstream reader sees one name.
|
|
42
|
+
*/
|
|
43
|
+
export declare function foldNumericToolArgumentAliases(toolName: string, params: Record<string, unknown>): {
|
|
44
|
+
params: Record<string, unknown>;
|
|
45
|
+
} | {
|
|
46
|
+
error: string;
|
|
47
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool-argument contract (#3261).
|
|
3
|
+
*
|
|
4
|
+
* `tools/list` advertises `inputSchema`; `tools/call` used to forward any JSON
|
|
5
|
+
* object. A misspelled or CLI-taught key (`depth` instead of `maxDepth`) then
|
|
6
|
+
* produced a well-formed answer computed from the server default — no error,
|
|
7
|
+
* no warning. This module is the single dispatch-time check that the keys a
|
|
8
|
+
* caller sent are ones the advertised schema (or an unpublished handler alias)
|
|
9
|
+
* actually reads.
|
|
10
|
+
*/
|
|
11
|
+
import { GITNEXUS_TOOLS } from './tools.js';
|
|
12
|
+
/** Legacy MCP names that reuse another tool's advertised schema. */
|
|
13
|
+
export const LEGACY_TOOL_SCHEMA_SOURCE = {
|
|
14
|
+
search: 'query',
|
|
15
|
+
explore: 'context',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Keys the handler still reads but that must NOT appear in `inputSchema`
|
|
19
|
+
* (#2175: advertising `query` makes Claude Code drop the argument).
|
|
20
|
+
*/
|
|
21
|
+
export const UNPUBLISHED_TOOL_ARGUMENT_ALIASES = {
|
|
22
|
+
query: ['query'],
|
|
23
|
+
cypher: ['query'],
|
|
24
|
+
// Group-mode context still reads `target` as the symbol name; local
|
|
25
|
+
// `name` is the advertised key. Advertising `target` would collide with
|
|
26
|
+
// impact's target vocabulary and is not in tools/list. Legacy `search`
|
|
27
|
+
// and `explore` inherit via schemaSourceToolName.
|
|
28
|
+
context: ['target'],
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Numeric aliases that the backend folds onto the advertised canonical key.
|
|
32
|
+
* `depth` is the CLI flag name for `maxDepth` on impact and trace.
|
|
33
|
+
*/
|
|
34
|
+
export const TOOL_NUMERIC_ARGUMENT_ALIASES = {
|
|
35
|
+
impact: [{ canonical: 'maxDepth', aliases: ['depth'] }],
|
|
36
|
+
trace: [{ canonical: 'maxDepth', aliases: ['depth'] }],
|
|
37
|
+
};
|
|
38
|
+
export function schemaSourceToolName(toolName) {
|
|
39
|
+
return LEGACY_TOOL_SCHEMA_SOURCE[toolName] ?? toolName;
|
|
40
|
+
}
|
|
41
|
+
export function advertisedToolPropertyNames(toolName) {
|
|
42
|
+
const source = schemaSourceToolName(toolName);
|
|
43
|
+
const tool = GITNEXUS_TOOLS.find((entry) => entry.name === source);
|
|
44
|
+
if (!tool)
|
|
45
|
+
return undefined;
|
|
46
|
+
return Object.keys(tool.inputSchema.properties);
|
|
47
|
+
}
|
|
48
|
+
function normalizeArgumentKey(key) {
|
|
49
|
+
return key.toLowerCase().replace(/_/gu, '');
|
|
50
|
+
}
|
|
51
|
+
export function suggestKnownToolArgument(unknownKey, knownKeys) {
|
|
52
|
+
const needle = normalizeArgumentKey(unknownKey);
|
|
53
|
+
if (!needle)
|
|
54
|
+
return undefined;
|
|
55
|
+
const exact = knownKeys.find((key) => normalizeArgumentKey(key) === needle);
|
|
56
|
+
if (exact)
|
|
57
|
+
return exact;
|
|
58
|
+
const contained = knownKeys.filter((key) => {
|
|
59
|
+
const normalized = normalizeArgumentKey(key);
|
|
60
|
+
return normalized.includes(needle) || needle.includes(normalized);
|
|
61
|
+
});
|
|
62
|
+
return contained.length === 1 ? contained[0] : undefined;
|
|
63
|
+
}
|
|
64
|
+
function formatUnknownArgumentError(toolName, unknownKeys, advertisedKeys) {
|
|
65
|
+
const quoted = unknownKeys.map((key) => `"${key}"`).join(', ');
|
|
66
|
+
const noun = unknownKeys.length === 1 ? 'argument' : 'arguments';
|
|
67
|
+
const verb = unknownKeys.length === 1 ? 'does' : 'do';
|
|
68
|
+
const suggestion = unknownKeys.length === 1 ? suggestKnownToolArgument(unknownKeys[0], advertisedKeys) : undefined;
|
|
69
|
+
if (suggestion) {
|
|
70
|
+
return `Unknown ${noun} ${quoted} for tool "${toolName}". Did you mean "${suggestion}"?`;
|
|
71
|
+
}
|
|
72
|
+
return (`Unknown ${noun} ${quoted} for tool "${toolName}". ` +
|
|
73
|
+
`The advertised inputSchema ${verb} not include ${unknownKeys.length === 1 ? 'this key' : 'these keys'}.`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reject top-level tool arguments that are neither advertised nor an
|
|
77
|
+
* unpublished handler alias. `advertisedProperties` should be the schema the
|
|
78
|
+
* caller actually saw (`tools/list` after read-only / repository-policy
|
|
79
|
+
* scrubbing). When it is omitted, the canonical `GITNEXUS_TOOLS` schema is
|
|
80
|
+
* used. Tools with no schema (legacy `overview`) are left unchecked.
|
|
81
|
+
*/
|
|
82
|
+
export function assertKnownMcpToolArguments(toolName, args, advertisedProperties) {
|
|
83
|
+
if (!args)
|
|
84
|
+
return;
|
|
85
|
+
const propertyNames = advertisedProperties !== undefined
|
|
86
|
+
? Object.keys(advertisedProperties)
|
|
87
|
+
: advertisedToolPropertyNames(toolName);
|
|
88
|
+
if (!propertyNames)
|
|
89
|
+
return;
|
|
90
|
+
const unpublished = UNPUBLISHED_TOOL_ARGUMENT_ALIASES[toolName] ??
|
|
91
|
+
UNPUBLISHED_TOOL_ARGUMENT_ALIASES[schemaSourceToolName(toolName)] ??
|
|
92
|
+
[];
|
|
93
|
+
const allowed = new Set([...propertyNames, ...unpublished]);
|
|
94
|
+
const unknownKeys = Object.keys(args).filter((key) => !allowed.has(key));
|
|
95
|
+
if (unknownKeys.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
throw new Error(formatUnknownArgumentError(toolName, unknownKeys, propertyNames));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Fold numeric aliases onto their canonical key (e.g. `depth` → `maxDepth`).
|
|
101
|
+
* Conflicting values error; a single agreed value is written to the canonical
|
|
102
|
+
* key and the alias keys are removed so every downstream reader sees one name.
|
|
103
|
+
*/
|
|
104
|
+
export function foldNumericToolArgumentAliases(toolName, params) {
|
|
105
|
+
const definitions = TOOL_NUMERIC_ARGUMENT_ALIASES[toolName];
|
|
106
|
+
if (!definitions)
|
|
107
|
+
return { params };
|
|
108
|
+
const normalized = { ...params };
|
|
109
|
+
for (const { canonical, aliases } of definitions) {
|
|
110
|
+
const keys = [canonical, ...aliases];
|
|
111
|
+
const supplied = [];
|
|
112
|
+
for (const key of keys) {
|
|
113
|
+
if (!Object.prototype.hasOwnProperty.call(normalized, key))
|
|
114
|
+
continue;
|
|
115
|
+
const value = normalized[key];
|
|
116
|
+
if (value === undefined)
|
|
117
|
+
continue;
|
|
118
|
+
if (typeof value !== 'number') {
|
|
119
|
+
return { error: `MCP parameter ${toolName}.${key} must be a number.` };
|
|
120
|
+
}
|
|
121
|
+
// #2279: some MCP adapters materialize an omitted optional number as 0,
|
|
122
|
+
// and a coerced missing value arrives as NaN. Treat both sentinels as
|
|
123
|
+
// absent so they cannot conflict with a real maxDepth or fold onto
|
|
124
|
+
// `params.maxDepth || 3`. The handlers already map a non-positive or
|
|
125
|
+
// non-integer maxDepth to their default; erroring here turned that
|
|
126
|
+
// contract into an error payload instead.
|
|
127
|
+
if (value === 0 || Number.isNaN(value))
|
|
128
|
+
continue;
|
|
129
|
+
supplied.push({ key, value });
|
|
130
|
+
}
|
|
131
|
+
const distinctValues = new Set(supplied.map(({ value }) => value));
|
|
132
|
+
if (distinctValues.size > 1) {
|
|
133
|
+
return {
|
|
134
|
+
error: `Conflicting MCP parameters for ${toolName}.${canonical}: ${supplied
|
|
135
|
+
.map(({ key }) => key)
|
|
136
|
+
.join(', ')} must agree.`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Drop every source key, then write back the single agreed value (if any),
|
|
140
|
+
// so a sentinel 0/NaN never survives on the canonical key.
|
|
141
|
+
for (const key of keys)
|
|
142
|
+
delete normalized[key];
|
|
143
|
+
if (supplied.length > 0)
|
|
144
|
+
normalized[canonical] = supplied[0].value;
|
|
145
|
+
}
|
|
146
|
+
return { params: normalized };
|
|
147
|
+
}
|
package/dist/mcp/tools.d.ts
CHANGED
package/dist/mcp/tools.js
CHANGED
|
@@ -526,6 +526,12 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
|
|
|
526
526
|
minimum: 1,
|
|
527
527
|
maximum: IMPACT_MAX_DEPTH,
|
|
528
528
|
},
|
|
529
|
+
depth: {
|
|
530
|
+
type: 'number',
|
|
531
|
+
description: 'Compatibility alias for maxDepth (CLI --depth). Values must agree when both are present. Literal 0 is an omitted-value compatibility sentinel.',
|
|
532
|
+
minimum: 0,
|
|
533
|
+
maximum: IMPACT_MAX_DEPTH,
|
|
534
|
+
},
|
|
529
535
|
crossDepth: {
|
|
530
536
|
type: 'number',
|
|
531
537
|
description: 'Cross-repository hop depth via contract bridge (default: 1; values above server maximum are clamped)',
|
|
@@ -865,6 +871,12 @@ DESTINATION TRACE (cross-repo): for an "@groupName" trace, OMIT to/to_uid/to_fil
|
|
|
865
871
|
minimum: 1,
|
|
866
872
|
maximum: 30,
|
|
867
873
|
},
|
|
874
|
+
depth: {
|
|
875
|
+
type: 'number',
|
|
876
|
+
description: 'Compatibility alias for maxDepth (CLI --depth). Values must agree when both are present. Literal 0 is an omitted-value compatibility sentinel.',
|
|
877
|
+
minimum: 0,
|
|
878
|
+
maximum: 30,
|
|
879
|
+
},
|
|
868
880
|
includeTests: {
|
|
869
881
|
type: 'boolean',
|
|
870
882
|
description: 'Include test-file symbols in traversal (default: false)',
|
|
@@ -922,6 +934,16 @@ export const REPO_SCOPED_TOOLS = new Set([
|
|
|
922
934
|
'trace',
|
|
923
935
|
]);
|
|
924
936
|
for (const tool of GITNEXUS_TOOLS) {
|
|
937
|
+
// Advertises a closed schema; tools/call still fail-closes on the scrubbed key list.
|
|
938
|
+
// The unpublished handler aliases in tool-arguments.ts stay off this schema on
|
|
939
|
+
// purpose (#2175), and closing it strands no caller: every alias has an
|
|
940
|
+
// advertised counterpart reaching the same handler — `query` → `search_query`
|
|
941
|
+
// on query, `query` → `statement` on cypher, and `target` → `name` on group
|
|
942
|
+
// context, which local-backend maps to the group target (the group name comes
|
|
943
|
+
// from `repo: "@group"`, not from `name`; see test/unit/mcp/group-repo-routing).
|
|
944
|
+
// A schema-validating client therefore has a valid call for every tool, and
|
|
945
|
+
// advertising the aliases instead would re-break Claude Code on `query`.
|
|
946
|
+
tool.inputSchema.additionalProperties = false;
|
|
925
947
|
if (!REPO_SCOPED_TOOLS.has(tool.name))
|
|
926
948
|
continue;
|
|
927
949
|
if (tool.inputSchema.properties.branch)
|
package/dist/storage/git.js
CHANGED
|
@@ -903,12 +903,16 @@ export function parseDiffHunksResult(diffOutput) {
|
|
|
903
903
|
// `+++` after the first `@@` of a file is hunk body (`+` plus source text
|
|
904
904
|
// that itself starts `++ …`), not another file header.
|
|
905
905
|
let inHunk = false;
|
|
906
|
+
// A deleted file has no new-side coordinates. Its indexed symbols still use
|
|
907
|
+
// the pre-delete file, so map those hunks with the old-side range instead.
|
|
908
|
+
let currentFileDeleted = false;
|
|
906
909
|
for (const line of diffOutput.split('\n')) {
|
|
907
910
|
if (line.startsWith(DIFF_GIT_PREFIX)) {
|
|
908
911
|
// Drop the previous file first: an unparsed header must not leave
|
|
909
912
|
// `current` live for a later `@@` / quoted `+++` to steal.
|
|
910
913
|
current = null;
|
|
911
914
|
inHunk = false;
|
|
915
|
+
currentFileDeleted = false;
|
|
912
916
|
const filePath = filePathFromGitHeader(line);
|
|
913
917
|
if (filePath) {
|
|
914
918
|
current = { filePath, hunks: [] };
|
|
@@ -929,6 +933,9 @@ export function parseDiffHunksResult(diffOutput) {
|
|
|
929
933
|
files.push(current);
|
|
930
934
|
}
|
|
931
935
|
}
|
|
936
|
+
else if (!inHunk && line === '+++ /dev/null') {
|
|
937
|
+
currentFileDeleted = true;
|
|
938
|
+
}
|
|
932
939
|
else if (!inHunk && line.startsWith('+++ ')) {
|
|
933
940
|
const filePath = pathFromPlusPlusPlus(line);
|
|
934
941
|
if (!filePath)
|
|
@@ -940,10 +947,12 @@ export function parseDiffHunksResult(diffOutput) {
|
|
|
940
947
|
}
|
|
941
948
|
else if (line.startsWith('@@') && current) {
|
|
942
949
|
inHunk = true;
|
|
943
|
-
const match = line.match(/@@
|
|
950
|
+
const match = line.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
944
951
|
if (match) {
|
|
945
|
-
const
|
|
946
|
-
const
|
|
952
|
+
const sideOffset = currentFileDeleted ? 1 : 3;
|
|
953
|
+
const start = parseInt(match[sideOffset], 10);
|
|
954
|
+
const rawCount = match[sideOffset + 1];
|
|
955
|
+
const count = rawCount !== undefined ? parseInt(rawCount, 10) : 1;
|
|
947
956
|
if (count > 0) {
|
|
948
957
|
current.hunks.push({ startLine: start, endLine: start + count - 1 });
|
|
949
958
|
}
|
package/package.json
CHANGED